class

CPU

Inherits Reference < Object

The 6502 CPU

NOTE: THESE EXAMPLES ASSUME A MEMORY BUS AND ARE NO LONGER VALID. PLEASE VIEW THE MULTIPLICATION EXAMPLE TO SHOW HOW TO CREATE A SYSTEM BUS

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 address location.
brkvec: will set the value at BRK_LOCATION to the label's address 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 #

Constants

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

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

Instance methods

accumulator

The 8-bit accumulator. Used in arithmetic operations

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).

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

and(m_value : UInt8)

Bitwise AND with Accumulator

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.

bcc(m_value : UInt8 | UInt16)

Branch on Carry Clear

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'

bcs(m_value : UInt8 | UInt16)

Branch on Carry Set

beq(m_value : UInt8 | UInt16)

Branch on Equal

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.

bmi(m_value : UInt8 | UInt16)

Branch on Minus

bne(m_value : UInt8 | UInt16)

Branch on Not Equal

bpl(m_value : UInt8 | UInt16)

Branch on Plus

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.

bvc(m_value : UInt8 | UInt16)

Branch on Overflow Clear

bvs(m_value : UInt8 | UInt16)

Branch on Overflow Set

clc

Clear Carry

cld

Clear Decimal

cli

Clear Interrupt

clock_cycle_mhz

The clock cycle in megahertz to run at.

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

Set in CPU#initialize

clv

Clear Overflow

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.

cpx(m_value : UInt8)

Compare X Register

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

cpy(m_value : UInt8)

Compare Y Register

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

dec(m_value : UInt8, m : UInt16 | UInt8)

Decrement Memory

dex

Decrement X

dey

Decrement Y

eor(m_value : UInt8)

Bitwise Exclusive-OR with Accumulator

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

flags

The CPU's 7 flag bits

get_flag(flag : Flags) : Bool

Gets a the value of a bit in flags

get_ind(address : Int)

Gets the Indirect Address of a given address

get_indx(address : Int)

Gets the Indirect X Address of a given address

get_indy(address : Int)

Gets the Indirect Y Address of a given address

inc(m_value : UInt8, m : UInt16 | UInt8)

Increment Memory

inx

Increment X

iny

Increment Y

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.

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.

lda(m_value : UInt8)

Load Accumulator

ldx(m_value : UInt8)

Load X Register

ldy(m_value : UInt8)

Load Y Register

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

log

Prints out information about the CPU in its current state

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.

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.

ora(m_value : UInt8)

Bitwise OR with Accumulator

peek(mem_location : UInt32 | Int32 | UInt16 | UInt8, two_byte : Bool = false) : UInt8 | UInt16

Reads a value from the data bus

pha

Push Accumulator

php

Push Processor Status (CPU#flags)

pla

Pull Accumulator

plp

Pull Processor Status

poke(mem_location : UInt32 | Int32 | UInt16 | UInt8, data : UInt8 | UInt16)

Pokes a value into the data bus

program_counter

The 16-bit program counter which points to the next instruction in the data bus 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

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"
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.

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.

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.

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.

run_instruction

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

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).

sec

Set Carry

sed

Set Decimal

sei

Set Interrupt

set_flag(flag : Flags, set : Bool)

Sets the value of a bit in flags

sta(m : UInt16 | UInt8)

Store Accumulator

stack_pointer

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

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

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

stp

Stops any active running CPU#execute

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

stx(m : UInt16 | UInt8)

Store X Register

sty(m : UInt16 | UInt8)

Store Y Register

tax

Transfer A to X

tay

Transfer A to Y

tsx

Transfer Stack Pointer to X

txa

Tranfer X to A

txs

Transfer X to Stack Pointer

tya

Transfer Y to A

x_index

The 8-bit x index register

y_index

The 8-bit y index register

Macros

parse_address

Parses the address of the line of code

parse_address_mode

Parses the address mode of the current line of code

Nested types