class CPU

Overview

The 6502 CPU

Assembly:

The main powerhouse of the emulator is the CPU#load_asm() method.

The method allows you to type in 6502 asm code and run it through Crystal.
The assembler has the ability to use labels as well as a few custom instructions
It also uses a semicolon ';' for inline comments

Example:

; Look at this cool comment!
cpu = CPU.new(1.0, 0x0600_u16, CPU::RES_LOCATION - 2)
cpu.load_asm("
lda #$14
")
cpu.execute
puts cpu.accumulator

To see all of the custom instructions, please see custom_instructions.cr

Example:

cpu.load_asm("
prt 22
")
cpu.execute # => puts "Type: UInt8 | Hex: 0x16 | Decimal: 22 | Binary: 0b00010110"

The assembler also has some predefined labels:
resvec: will set the value at RES_LOCATION to the label's memory location.
brkvec: will set the value at BRK_LOCATION to the label's memory location.

Example:

cpu = CPU.new(1.0, 0x0200_u16)
cpu.load_asm("
resvec:
nop
brkvec:
")
puts cpu.peek(CPU::BRK_LOCATION, true).to_s(16) # => 200
puts cpu.peek(CPU::RES_LOCATION, true).to_s(16) # => 201

Being written in Crystal, you can use string interpolation when writing assembly code, giving access for any UInt8 and UInt16 to be injected into the code.

Example:

x = 0xa4
cpu.load_asm("
lda ##{x}
")
cpu.execute
puts cpu.accumulator.to_s(16) # => a4

Note that the values are assigned at the assembler's compile time, therefore

x = 0xa4_u8
cpu.load_asm("
lda ##{x}
prt #{cpu.accumulator}
")
cpu.execute                   # => puts "Type: UInt8 | Hex: 0x00 | Decimal: 0 | Binary: 0b00000000"
puts cpu.accumulator.to_s(16) # => a4

In the above example, at compile time, cpu.accumulator is set to 0. It only gets changed at the runtime of the code.
You can however achieve the hoped for effect by using multiple CPU#load_asm methods:

x = 0xa4_u8
cpu.load_asm("
lda ##{x}
")

cpu.execute

cpu.load_asm("
prt #{cpu.accumulator}
")

cpu.execute # => puts "Type: UInt8 | Hex: 0xa4 | Decimal: 164 | Binary: 0b10100100"

Be careful when doing this though as you must keep in mind that the memory has not reset, but using CPU#load_asm will reset the CPU#program_counter to its original value, or to the value of a resvec:

This means that all the instructions set by any previous CPU#load_asm's will still be there in memory.

To counteract this issue, ensure that a brk is set at the end of any code

You can however "append" code by setting the start_location of CPU#load_asm manually

Example

cpu = CPU.new(1.0, 0x0600_u16, CPU::RES_LOCATION - 2)

cpu.load_asm("
lda #01
")

# Code = a9 01 #

cpu.load_asm(0x0603"
lda #01
")

# Code = a9 01 a9 01 #

You can also use resvec: to edit the default CPU#program_counter location when editing code

Example

cpu.load_asm("
lda #01
resvec:
")

# Code = a9 01 #

cpu.load_asm("
lda #01
")

# Code = a9 01 a9 01 #

Defined in:

cr6502/6502.cr
cr6502/addressing.cr
cr6502/instructions.cr
cr6502/instructions/bitwise_instructions.cr
cr6502/instructions/branch_instructions.cr
cr6502/instructions/compare_instructions.cr
cr6502/instructions/custom_instructions.cr
cr6502/instructions/flag_instructions.cr
cr6502/instructions/jump_instructions.cr
cr6502/instructions/math_instructions.cr
cr6502/instructions/memory_instructions.cr
cr6502/instructions/other_instructions.cr
cr6502/instructions/register_instructions.cr
cr6502/instructions/stack_instructions.cr
cr6502/parser.cr
cr6502/parser/error.cr
cr6502/parser/scanner.cr
cr6502/parser/token.cr

Constant Summary

BRK_LOCATION = 65534_u16

Vector address for BRK

INSTRUCTIONS = [{"BRK", 0_u8, 7}, {"ORAindx", 1_u8, 6}, {"ORAzpg", 5_u8, 3}, {"ASLzpg", 6_u8, 5}, {"PHP", 8_u8, 3}, {"ORAi", 9_u8, 2, 2}, {"ASLa", 10_u8, 2}, {"ORAabs", 13_u8, 4}, {"ASLabs", 14_u8, 6}, {"BPL", 16_u8, 2}, {"ORAindy", 17_u8, 5}, {"ORAzpgx", 21_u8, 3}, {"ASLzpgx", 22_u8, 6}, {"CLC", 24_u8, 2}, {"ORAabsy", 25_u8, 4}, {"ORAabsx", 29_u8, 4}, {"ASLabsx", 30_u8, 7}, {"JSR", 32_u8, 6}, {"ANDindx", 33_u8, 6}, {"BITzpg", 36_u8, 3}, {"ANDzpg", 37_u8, 2}, {"ROLzpg", 38_u8, 5}, {"PLP", 40_u8, 4}, {"ANDi", 41_u8, 2}, {"ROLa", 42_u8, 2}, {"BITabs", 44_u8, 4}, {"ANDabs", 45_u8, 4}, {"ROLabs", 46_u8, 6}, {"BMI", 48_u8, 2}, {"ANDindy", 49_u8, 5}, {"ANDzpgx", 53_u8, 3}, {"ROLzpgx", 54_u8, 6}, {"SEC", 56_u8, 2}, {"ANDabsy", 57_u8, 4}, {"ANDabsx", 61_u8, 4}, {"ROLabsx", 62_u8, 7}, {"RTI", 64_u8, 6}, {"EORindx", 65_u8, 6}, {"EORzpg", 69_u8, 3}, {"LSRzpg", 70_u8, 5}, {"PHA", 72_u8, 3}, {"EORi", 73_u8, 2}, {"LSRa", 74_u8, 2}, {"JMPabs", 76_u8, 3}, {"EORabs", 77_u8, 4}, {"LSRabs", 78_u8, 6}, {"BVC", 80_u8, 2}, {"EORindy", 81_u8, 5}, {"EORzpgx", 85_u8, 4}, {"LSRzpgx", 86_u8, 6}, {"CLI", 88_u8, 2}, {"EORabsy", 89_u8, 4}, {"EORabsx", 93_u8, 4}, {"LSRabs", 94_u8, 7}, {"RTS", 96_u8, 6}, {"ADCindx", 97_u8, 6}, {"ADCzpg", 101_u8, 3}, {"RORzpg", 102_u8, 5}, {"PLA", 104_u8, 4}, {"ADCi", 105_u8, 2}, {"RORa", 106_u8, 2}, {"JMPind", 108_u8, 5}, {"ADCabs", 109_u8, 4}, {"RORabs", 110_u8, 6}, {"BVS", 112_u8, 2}, {"ADCindy", 113_u8, 5}, {"ADCzpgx", 117_u8, 4}, {"RORzpgx", 118_u8, 6}, {"SEI", 120_u8, 2}, {"ADCabsy", 121_u8, 4}, {"ADCabsx", 125_u8, 4}, {"RORabsx", 126_u8, 7}, {"STAindx", 129_u8, 6}, {"STYzpg", 132_u8, 3}, {"STAzpg", 133_u8, 3}, {"STXzpg", 134_u8, 3}, {"DEY", 136_u8, 2}, {"TXA", 138_u8, 2}, {"STYabs", 140_u8, 4}, {"STAabs", 141_u8, 4}, {"STXabs", 142_u8, 4}, {"BCC", 144_u8, 2}, {"STAindy", 145_u8, 6}, {"STYzpgx", 148_u8, 4}, {"STAzpgx", 149_u8, 4}, {"STXzpgy", 150_u8, 4}, {"TYA", 152_u8, 2}, {"STAabsy", 153_u8, 5}, {"TXS", 154_u8, 2}, {"STAabsx", 157_u8, 5}, {"LDYi", 160_u8, 2}, {"LDAindx", 161_u8, 6}, {"LDXi", 162_u8, 2}, {"LDYzpg", 164_u8, 3}, {"LDAzpg", 165_u8, 3}, {"LDXzpg", 166_u8, 3}, {"TAY", 168_u8, 2}, {"LDAi", 169_u8, 2}, {"TAX", 170_u8, 2}, {"LDYabs", 172_u8, 4}, {"LDAabs", 173_u8, 4}, {"LDXabs", 174_u8, 4}, {"BCS", 176_u8, 2}, {"LDAindy", 177_u8, 5}, {"LDYzpgx", 180_u8, 4}, {"LDAzpgx", 181_u8, 4}, {"LDXzpgy", 182_u8, 4}, {"CLV", 184_u8, 2}, {"LDAabsy", 185_u8, 4}, {"TSX", 186_u8, 2}, {"LDYabsx", 188_u8, 4}, {"LDAabsx", 189_u8, 4}, {"LDXabsy", 190_u8, 4}, {"CPYi", 192_u8, 2}, {"CMPindx", 193_u8, 6}, {"CPYzpg", 196_u8, 3}, {"CMPzpg", 197_u8, 3}, {"DECzpg", 198_u8, 5}, {"INY", 200_u8, 2}, {"CMPi", 201_u8, 2}, {"DEX", 202_u8, 2}, {"CPYabs", 204_u8, 4}, {"CMPabs", 205_u8, 4}, {"DECabs", 206_u8, 6}, {"BNE", 208_u8, 2}, {"CMPindy", 209_u8, 5}, {"CMPzpgx", 213_u8, 4}, {"DECzpgx", 214_u8, 6}, {"CLD", 216_u8, 2}, {"CMPabsy", 217_u8, 4}, {"CMPabsx", 221_u8, 4}, {"DECabsx", 222_u8, 7}, {"CPXi", 224_u8, 2}, {"SBCindx", 225_u8, 6}, {"CPXzpg", 228_u8, 3}, {"SBCzpg", 229_u8, 3}, {"INCzpg", 230_u8, 5}, {"INX", 232_u8, 2}, {"SBCi", 233_u8, 2}, {"NOP", 234_u8, 2}, {"CPXabs", 236_u8, 4}, {"SBCabs", 237_u8, 4}, {"INCabs", 238_u8, 6}, {"BEQ", 240_u8, 2}, {"SBCindy", 241_u8, 5}, {"SBCzpgx", 245_u8, 4}, {"INCzpgx", 246_u8, 6}, {"SED", 248_u8, 2}, {"SBCabsy", 249_u8, 4}, {"SBCabsx", 253_u8, 4}, {"INCabsx", 254_u8, 7}, {"PRTzpg", 2_u8, 2}, {"PRTabs", 3_u8, 3}, {"LOG", 4_u8, 10}, {"STP", 7_u8, 2}, {"BCClabel", 11_u8, 2}, {"BCSlabel", 27_u8, 2}, {"BEQlabel", 43_u8, 2}, {"BMIlabel", 59_u8, 2}, {"BNElabel", 75_u8, 2}, {"BPLlabel", 91_u8, 2}, {"BVClabel", 107_u8, 2}, {"BVSlabel", 123_u8, 2}]

List of instructions sorted by its opcode

Format is {"InstructionName", opcode, cycle length, byte length}

KEYWORDS = {"ADC" => TokenType::ADC, "AND" => TokenType::AND, "ASL" => TokenType::ASL, "BCC" => TokenType::BCC, "BCS" => TokenType::BCS, "BEQ" => TokenType::BEQ, "BIT" => TokenType::BIT, "BMI" => TokenType::BMI, "BNE" => TokenType::BNE, "BPL" => TokenType::BPL, "BRK" => TokenType::BRK, "BVC" => TokenType::BVC, "BVS" => TokenType::BVS, "CLC" => TokenType::CLC, "CLD" => TokenType::CLD, "CLI" => TokenType::CLI, "CLV" => TokenType::CLV, "CMP" => TokenType::CMP, "CPX" => TokenType::CPX, "CPY" => TokenType::CPY, "DEC" => TokenType::DEC, "DEX" => TokenType::DEX, "DEY" => TokenType::DEY, "EOR" => TokenType::EOR, "INC" => TokenType::INC, "INX" => TokenType::INX, "INY" => TokenType::INY, "JMP" => TokenType::JMP, "JSR" => TokenType::JSR, "LDA" => TokenType::LDA, "LDX" => TokenType::LDX, "LDY" => TokenType::LDY, "LSR" => TokenType::LSR, "NOP" => TokenType::NOP, "ORA" => TokenType::ORA, "PHA" => TokenType::PHA, "PHP" => TokenType::PHP, "PLA" => TokenType::PLA, "PLP" => TokenType::PLP, "ROL" => TokenType::ROL, "ROR" => TokenType::ROR, "RTI" => TokenType::RTI, "RTS" => TokenType::RTS, "SBC" => TokenType::SBC, "SEC" => TokenType::SEC, "SED" => TokenType::SED, "SEI" => TokenType::SEI, "STA" => TokenType::STA, "STX" => TokenType::STX, "STY" => TokenType::STY, "TAX" => TokenType::TAX, "TAY" => TokenType::TAY, "TSX" => TokenType::TSX, "TXA" => TokenType::TXA, "TXS" => TokenType::TXS, "TYA" => TokenType::TYA, "PRT" => TokenType::PRT, "LOG" => TokenType::LOG, "STP" => TokenType::STP}

The keywords for the tokens, used when parsing

RES_LOCATION = 65532_u16

Vector address for RESET

Constructors

Macro Summary

Instance Method Summary

Constructor Detail

def self.new(clock_cycle_mhz : Float = 1.79, reset : UInt16 = 0, brk : UInt16 = 0) #

Creates a 6502 CPU

The clock cycle is set in megahertz

reset is the value set at RES_LOCATION and is used to find where the #program_counter should start

#brk is the value set at BRK_LOCATION and is used to find where CPU#brk should goto


[View source]

Macro Detail

macro parse_address #

Parses the address of the line of code


[View source]
macro parse_address_mode #

Parses the address mode of the current line of code


[View source]

Instance Method Detail

def accumulator : UInt8 #

The 8-bit accumulator. Used in arithmetic operations


[View source]
def adc(m_value : UInt8) #

Add with Carry

ADC behavior depends on the state of the CPU::Flags::DecimalMode flag. In decimal mode, the values upon which the addition is performed are interpreted as packed BCD (Binary Coded Decimal).


[View source]
def add_instruction(hex : UInt8 | UInt16) #

Adds an instruction, given it's opcode, into the current location in memory of the CPU#program_counter and increments the CPU#program_counter by the byte length of the given hex


[View source]
def and(m_value : UInt8) #

Bitwise AND with Accumulator


[View source]
def asl(m_value : UInt8, m : UInt16 | UInt8, accumulator : Bool = false) #

Arithmetic Shift Left

ASL shifts all bits left one position. 0 is shifted into bit 0 and the original bit 7 is shifted into the Carry.


[View source]
def bcc(m_value : UInt8 | UInt16) #

Branch on Carry Clear


[View source]
def bcd(byte : UInt8 | Int8) #

Calculates a byte into a Binary Coded Decimal (BCD)

BCD is whereby the upper and lower nibbles (4-bits) of a byte (8-bits) are treated as two digits in a decimal number;

The upper nibble contains the number from the 'tens column'; and the lower nibble, the number from the 'units column'


[View source]
def bcs(m_value : UInt8 | UInt16) #

Branch on Carry Set


[View source]
def beq(m_value : UInt8 | UInt16) #

Branch on Equal


[View source]
def bit(m_value : UInt8) #

Test Bits

BIT sets the Z flag as though the value in the address tested were ANDed with the accumulator.

The N and V flags are set equal to bits 7 and 6 respectively of the value in the tested address.


[View source]
def bmi(m_value : UInt8 | UInt16) #

Branch on Minus


[View source]
def bne(m_value : UInt8 | UInt16) #

Branch on Not Equal


[View source]
def bpl(m_value : UInt8 | UInt16) #

Branch on Plus


[View source]
def brk #

Break

BRK sets the B flag, and then generates a forced interrupt. The Interrupt flag is ignored and the CPU goes through the normal interrupt process. In the interrupt service routine, the state of the B flag can be used to distinguish a BRK from a standard interrupt.

BRK causes a non-maskable interrupt and increments the program counter by one. Therefore an CPU#rti will go to the address of the BRK +2 so that BRK may be used to replace a two-byte instruction for debugging and the subsequent RTI will be correct.


[View source]
def bvc(m_value : UInt8 | UInt16) #

Branch on Overflow Clear


[View source]
def bvs(m_value : UInt8 | UInt16) #

Branch on Overflow Set


[View source]
def clc #

Clear Carry


[View source]
def cld #

Clear Decimal


[View source]
def cli #

Clear Interrupt


[View source]
def clock_cycle_mhz : Float64 #

The clock cycle in megahertz to run at.

Defaults to the NES's 6502 speed, 1.79mhz.

Set in CPU#initialize


[View source]
def clv #

Clear Overflow


[View source]
def cmp(m_value : UInt8) #

Compare Accumulator

Compare sets processor flags as if a subtraction had been carried out.

If the accumulator and the compared value are equal, the result of the subtraction is zero and the Zero (Z) flag is set. If the accumulator is equal or greater than the compared value, the Carry (C) flag is set.


[View source]
def cpx(m_value : UInt8) #

Compare X Register

Operation and flag results are identical to equivalent mode accumulator CPU#cmp operations.


[View source]
def cpy(m_value : UInt8) #

Compare Y Register

Operation and flag results are identical to equivalent mode accumulator CPU#cmp operations.


[View source]
def dec(m_value : UInt8, m : UInt16 | UInt8) #

Decrement Memory


[View source]
def dex #

Decrement X


[View source]
def dey #

Decrement Y


[View source]
def eor(m_value : UInt8) #

Bitwise Exclusive-OR with Accumulator


[View source]
def execute(end_on_tight_loop : Bool = true, reset : Bool = true) #

Runs all instructions

If end_on_tight_loop is true, it will not step if the current instruction sets the #program_counter to itself, creating a tight loop

NOTE A real 6502 does not end on tight loops, this is only used to ensure that a program doesn't run forever

If reset is true, it will set the CPU#program_counter to its original value. If reset is false, it simply continues the code from the last instruction. This is only really matters when end_on_tight_loop is true or when using CPU#stp


[View source]
def flags : UInt8 #

The CPU's 7 flag bits


[View source]
def get_flag(flag : Flags) : Bool #

Gets a the value of a bit in #flags


[View source]
def get_ind(address : Int) #

Gets the Indirect Address of a given address


[View source]
def get_indx(address : Int) #

Gets the Indirect X Address of a given address


[View source]
def get_indy(address : Int) #

Gets the Indirect Y Address of a given address


[View source]
def inc(m_value : UInt8, m : UInt16 | UInt8) #

Increment Memory


[View source]
def inx #

Increment X


[View source]
def iny #

Increment Y


[View source]
def jmp(m_value : UInt16) #

Jump

JMP loads the program counter with the absolute address, or the address stored at the memory location of the indirect address. Program execution proceeds from the new program counter value.


[View source]
def jsr(address : UInt16) #

Jump Saving Return

JSR pushes the address-1 of the next operation to the stack before transferring the value of the argument to the program counter. JSR behaves just like a JMP, but saves the return address to the stack first, thus creating a subroutine.

Subroutines are normally terminated by an CPU#rts instruction.


[View source]
def lda(m_value : UInt8) #

Load Accumulator


[View source]
def ldx(m_value : UInt8) #

Load X Register


[View source]
def ldy(m_value : UInt8) #

Load Y Register


[View source]
def load_asm(code : String, start_location : UInt16 = (peek(RES_LOCATION, true)).to_u16) #

Loads 6502 assembly instructions

Uses ; for comments

Works with labels label:

the resvec: label will set the value at RES_LOCATION to the label's memory location

the brkvec: label will set the value at BRK_LOCATION to the label's memory location

You can also manually set the starting location to write the instructions at. Useful for appending or editing code


[View source]
def log #

Prints out information about the CPU in its current state


[View source]
def lsr(m_value : UInt8, m : UInt16 | UInt8, accumulator : Bool = false) #

Logical Shift Right

LSR shifts all bits right one position. 0 is shifted into bit 7 and the original bit 0 is shifted into the Carry.


[View source]
def memory : IO::Memory #

The 64kb (65536 bytes) of accessible memory


[View source]
def nop #

No Operation

A NOP takes 2 machine cycles to execute, but it has no effect on any register, memory location, or processor flag. Thus, it takes up time and space but performs no operation.

NOP can be used to reserve space for future modifications or to remove existing code without changing the memory locations of code that follows it.

NOP can also be used in tightly timed code, to idly take up 2 cycles without having any other side effects.


[View source]
def ora(m_value : UInt8) #

Bitwise OR with Accumulator


[View source]
def peek(mem_location : Int, two_byte : Bool = false) #

Reads a value from memory. Will read as a UInt16 if two_byte is true


[View source]
def pha #
Push Accumulator

[View source]
def php #

Push Processor Status (CPU#flags)


[View source]
def pla #

Pull Accumulator


[View source]
def plp #

Pull Processor Status


[View source]
def poke(mem_location : Int, data : UInt8 | UInt16) #

Pokes a value into a location in memory


[View source]
def print_memory(start_pos : Int = 0, length : Int = 65536) #

Prints out the current memory.

Prints out 16 bytes per line. Therefore #print_memory(0x0F4, 1) will print 0x0F40 through 0x0F4F


[View source]
def program_counter : UInt16 #

The 16-bit program counter which points to the next instruction in memory to execute.

Gets set after a command is read, but before it is executed.

Meaning it points to the next instruction to execute, not the one that is currently executing


[View source]
def prt(value_to_print : UInt8 | UInt16) #

Print a 8-bit or 16-bit value. Mainly used with string interpolation

Example:

cpu = CPU.new

cpu.load_asm("
prt #{cpu.stack_pointer}
")

cpu.execute # => puts "Type: UInt8 | Hex: ff | Decimal 255 | Binary: 11111111"

[View source]
def rol(m_value : UInt8, m : UInt16 | UInt8, accumulator : Bool = false) #

Rotate Left

ROL shifts all bits left one position. The Carry is shifted into bit 0 and the original bit 7 is shifted into the Carry.


[View source]
def ror(m_value : UInt8, m : UInt16 | UInt8, accumulator : Bool = false) #

Rotate Right

ROR shifts all bits right one position. The Carry is shifted into bit 7 and the original bit 0 is shifted into the Carry.


[View source]
def rti #

Return from Interrupt

RTI retrieves the Processor Status byte and Program Counter from the stack in that order. Interrupts push the program counter first and then the processor status.

Unlike RTS, the return address on the stack is the actual address rather than the address-1.


[View source]
def rts #

Return to Saved

RTS pulls the top two bytes off the stack (low byte first) and transfers them to the program counter. The program counter is incremented by one and then execution proceeds from there.

RTS is typically used in combination with a CPU#jsr which saves the return address-1 to the stack.


[View source]
def run_instruction #

Runs the current value of CPU#program_counter's location in memory as an instruction


[View source]
def sbc(m_value : UInt8) #

Subtract with Carry

SBC behavior depends on the state of the CPU::Flags::DecimalMode flag. In decimal mode, the values upon which the subtraction is performed are interpreted as packed BCD (Binary Coded Decimal).


[View source]
def sec #

Set Carry


[View source]
def sed #

Set Decimal


[View source]
def sei #

Set Interrupt


[View source]
def set_flag(flag : Flags, set : Bool) #

Sets the value of a bit in #flags


[View source]
def sta(m : UInt16 | UInt8) #

Store Accumulator


[View source]
def stack_pointer : UInt8 #

The 8-bit stack pointer which points to the current position in the Stack.

The stack ranges from 0x100 to 0x1FF, starting at 0x1FF


[View source]
def step(end_on_tight_loop : Bool = true) #

Runs the next instruction

if end_on_tight_loop is true, it will not step if the current instruction sets the #program_counter to itself, creating a tight loop

NOTE A real 6502 does not end on tight loops, this is only used to ensure that a program doesn't run forever


[View source]
def stp #

Stops any active running CPU#execute

When used with CPU#execute(reset: false), it can act as a way to pause


[View source]
def stx(m : UInt16 | UInt8) #

Store X Register


[View source]
def sty(m : UInt16 | UInt8) #

Store Y Register


[View source]
def tax #

Transfer A to X


[View source]
def tay #
Transfer A to Y

[View source]
def tsx #

Transfer Stack Pointer to X


[View source]
def txa #
Tranfer X to A

[View source]
def txs #
Transfer X to Stack Pointer

[View source]
def tya #
Transfer Y to A

[View source]
def x_index : UInt8 #

The 8-bit x index register


[View source]
def y_index : UInt8 #

The 8-bit y index register


[View source]