CPU
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
Vector address for BRK
List of instructions sorted by its opcode
Format is {"InstructionName", opcode, cycle length, byte length}
The keywords for the tokens, used when parsing
Vector address for RESET
Constructors
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
The 8-bit accumulator. Used in arithmetic operations
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).
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
Bitwise AND with Accumulator
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.
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'
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.
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.
Clear Carry
Clear Decimal
Clear Interrupt
The clock cycle in megahertz to run at.
Defaults to the NES's 6502 speed, 1.79mhz.
Set in CPU#initialize
Clear Overflow
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.
Compare X Register
Operation and flag results are identical to equivalent mode accumulator CPU#cmp operations.
Compare Y Register
Operation and flag results are identical to equivalent mode accumulator CPU#cmp operations.
Decrement X
Decrement Y
Bitwise Exclusive-OR with Accumulator
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
The CPU's 7 flag bits
Gets the Indirect Address of a given address
Gets the Indirect X Address of a given address
Gets the Indirect Y Address of a given address
Increment X
Increment Y
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.
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.
Load Accumulator
Load X Register
Load Y Register
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
Prints out information about the CPU in its current state
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.
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.
Bitwise OR with Accumulator
Reads a value from the data bus
Push Accumulator
Push Processor Status (CPU#flags)
Pull Accumulator
Pull Processor Status
Pokes a value into the data bus
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
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"
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.
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.
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.
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.
Runs the current value of CPU#program_counter's location in memory as an instruction
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).
Set Carry
Set Decimal
Set Interrupt
The 8-bit stack pointer which points to the current position in the Stack.
The stack ranges from 0x100 to 0x1FF, starting at 0x1FF
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
Stops any active running CPU#execute
When used with CPU#execute(reset: false), it can act as a way to pause
Transfer A to X
Transfer A to Y
Transfer Stack Pointer to X
Tranfer X to A
Transfer X to Stack Pointer
Transfer Y to A
The 8-bit x index register
The 8-bit y index register
Macros
Parses the address of the line of code
Parses the address mode of the current line of code