Skip to content →

opcodes and addressing modes – the 6502

Of course our first action needs to be implementing all of the opcodes the 6502 offers. The first thing I noticed was the different addressing modes the 6502 has.

immediateThis is the address right after the opcode (PC+1)
zeropageThis is the address of the value at PC+1. (8 bit force it to be on zeropage, 0x00nn)
zeropage, x-indexedSame as zeropage, but offset with register X
zeropage, y-indexedSame as zeropage, but offset with register Y
indirect, x-indexedRead the value of the immediate byte. Use this value + X (low nibble), and this value + X + 1 (high nibble) as an address
indirect, y-indexed Read the value of the immediate byte. Use this value (low nibble), and this value + 1 (high nibble) as an address. Add Y to this address
absoluteThis gives a complete address with the next 2 bytes (little Endian, so low nibble comes first)
absolute, x-indexedSame as absolute, but offset with register X
absolute, y-indexedSame as absolute, but offset with register Y

For comfort, I just implemented each addressing mode as a function in the MMU, so whenever we need to address something, no matter if it is for a read or a write, we can just get the desired address by passing the values to our addressing functions.

unsigned char readFromMem(uint16_t adr);
uint16_t getImmediate(uint16_t adr);
uint16_t getZeropage(uint16_t adr);
uint16_t getZeropageXIndex(uint16_t adr, uint8_t X);
uint16_t getZeropageYIndex(uint16_t adr, uint8_t Y);
uint16_t getIndirectXIndex(uint16_t adr, uint8_t X);
uint16_t getIndirectYIndex(uint16_t adr, uint8_t Y);
uint16_t getAbsolute(uint16_t adr);
uint16_t getAbsoluteXIndex(uint16_t adr, uint8_t X);
uint16_t getAbsoluteYIndex(uint16_t adr, uint8_t Y);

With this done, we can now start implementing the opcodes of the 6502, which turn out to be quite a few less than on the Gameboy.

x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF
0x
1
BRK
imp
7
NV1BDIZC
BI
2
ORA
inx
6
NV1BDIZC
NZ
KIL SLO
inx
NOP
zp
2
ORA
zp
3
NV1BDIZC
NZ
2
ASL
zp
5
NV1BDIZC
CNZ
SLO
zp
1
PHP
imp
3
NV1BDIZC
2
ORA
imm
2
NV1BDIZC
NZ
1
ASL
akk
2
NV1BDIZC
CNZ
ANC
imm
NOP
abs
3
ORA
abs
4
NV1BDIZC
NZ
3
ASL
abs
6
NV1BDIZC
CNZ
SLO
abs
1x
3
BPL
rel
2*+
NV1BDIZC
2
ORA
iny
5+
NV1BDIZC
NZ
KIL SLO
iny
NOP
zpx
2
ORA
zpx
4
NV1BDIZC
NZ
2
ASL
zpx
6
NV1BDIZC
CNZ
SLO
zpx
1
CLC
imp
2
NV1BDIZC
C
3
ORA
aby
4+
NV1BDIZC
NZ
NOP
imp
SLO
aby
NOP
abx
3
ORA
abx
4+
NV1BDIZC
NZ
3
ASL
abx
7
NV1BDIZC
CNZ
SLO
abx
2x
3
JSR
abs
6
NV1BDIZC
2
AND
inx
6
NV1BDIZC
NZ
KIL RLA
inx
2
BIT
zp
3
NV1BDIZC
NOZ
2
AND
zp
3
NV1BDIZC
NZ
2
ROL
zp
5
NV1BDIZC
CNZ
RLA
zp
1
PLP
imp
4
NV1BDIZC
CNVZIBD
2
AND
imm
2
NV1BDIZC
NZ
1
ROL
akk
2
NV1BDIZC
CNZ
ANC
imm
3
BIT
abs
4
NV1BDIZC
NVZ
3
AND
abs
4
NV1BDIZC
NZ
3
ROL
abs
6
NV1BDIZC
CNZ
RLA
abs
3x
3
BMI
rel
2*+
NV1BDIZC
2
AND
iny
5+
NV1BDIZC
NZ
KIL RLA
iny
NOP
zpx
2
AND
zpx
4
NV1BDIZC
NZ
2
ROL
zpx
6
NV1BDIZC
CNZ
RLA
zpx
1
SEC
imp
2
NV1BDIZC
C
3
AND
aby
4+
NV1BDIZC
NZ
NOP
imp
RLA
aby
NOP
abx
3
AND
abx
4+
NV1BDIZC
NZ
3
ROL
abx
7
NV1BDIZC
CNZ
RLA
abx
4x
1
RTI
imp
6
NV1BDIZC
CNVZIBD
2
EOR
inx
6
NV1BDIZC
NZ
KIL SRE
inx
NOP
zp
1
EOR
zp
3
NV1BDIZC
NZ
2
LSR
zp
5
NV1BDIZC
CNZ
SRE
zp
1
PHA
imp
3
NV1BDIZC
2
EOR
imm
2
NV1BDIZC
NZ
1
LSR
akk
2
NV1BDIZC
CNZ
ALR
imm
3
JMP
abs
3
NV1BDIZC
3
EOR
abs
4
NV1BDIZC
NZ
3
LSR
abs
6
NV1BDIZC
CNZ
SRE
abs
5x
3
BVC
rel
2*+
NV1BDIZC
2
EOR
iny
5+
NV1BDIZC
NZ
KIL SRE
iny
NOP
zpx
2
EOR
zpx
4
NV1BDIZC
NZ
2
LSR
zpx
6
NV1BDIZC
CNZ
SRE
zpx
1
CLI
imp
2
NV1BDIZC
I
3
EOR
aby
4+
NV1BDIZC
NZ
NOP
imp
SRE
aby
NOP
abx
3
EOR
abx
4+
NV1BDIZC
NZ
3
LSR
abx
7
NV1BDIZC
CNZ
SRE
abx
6x
1
RTS
imp
6
NV1BDIZC
2
ADC
inx
6
NV1BDIZC
CNVZ
KIL RRA
inx
NOP
zp
1
ADC
zp
3
NV1BDIZC
CNVZ
2
ROR
zp
5
NV1BDIZC
CNZ
RRA
zp
1
PLA
imp
4
NV1BDIZC
NZ
2
ADC
imm
2
NV1BDIZC
CNVZ
1
ROR
akk
2
NV1BDIZC
CNZ
ARR
imm
3
JMP
ind
5
NV1BDIZC
3
ADC
abs
4
NV1BDIZC
CNVZ
3
ROR
abs
6
NV1BDIZC
CNZ
RRA
abs
7x
3
BVS
rel
2*+
NV1BDIZC
2
ADC
iny
5+
NV1BDIZC
CNVZ
KIL RRA
iny
NOP
zpx
2
ADC
zpx
4
NV1BDIZC
CNVZ
2
ROR
zpx
6
NV1BDIZC
CNZ
RRA
zpx
1
SEI
imp
2
NV1BDIZC
I
3
ADC
aby
4+
NV1BDIZC
CNVZ
NOP
imp
RRA
aby
NOP
abx
3
ADC
abx
4+
NV1BDIZC
CNVZ
3
ROR
abx
7
NV1BDIZC
CNZ
RRA
abx
8x NOP
imm
2
STA
inx
6
NV1BDIZC
NOP
imm
SAX
inx
2
STY
zp
3
NV1BDIZC
2
STA
zp
3
NV1BDIZC
2
STX
zp
3
NV1BDIZC
SAX
zp
1
DEY
imp
2
NV1BDIZC
NZ
NOP
imm
1
TXA
imp
2
NV1BDIZC
NZ
XAA
imm
3
STY
abs
4
NV1BDIZC
3
STA
abs
4
NV1BDIZC
3
STX
abs
4
NV1BDIZC
SAX
abs
9x
3
BCC
rel
2*+
NV1BDIZC
2
STA
iny
6
NV1BDIZC
KIL AHX
iny
2
STY
zpx
4
NV1BDIZC
2
STA
zpx
4
NV1BDIZC
2
STX
zpy
4
NV1BDIZC
SAX
zpy
1
TYA
imp
2
NV1BDIZC
NZ
3
STA
aby
5
NV1BDIZC
1
TXS
imp
2
NV1BDIZC
TAS
aby
SHY
abx
3
STA
abx
5
NV1BDIZC
SHX
aby
AHX
aby
Ax
2
LDY
imm
2
NV1BDIZC
NZ
2
LDA
inx
6
NV1BDIZC
NZ
2
LDX
imm
2
NV1BDIZC
NZ
LAX
inx
2
LDY
zp
3
NV1BDIZC
NZ
2
LDA
zp
3
NV1BDIZC
NZ
1
LDX
zp
3
NV1BDIZC
NZ
LAX
zp
1
TAY
imp
2
NV1BDIZC
NZ
2
LDA
imm
2
NV1BDIZC
NZ
1
TAX
imp
2
NV1BDIZC
NZ
LAX
imm
3
LDY
abs
4
NV1BDIZC
NZ
3
LDA
abs
4
NV1BDIZC
NZ
3
LDX
abs
4
NV1BDIZC
NZ
LAX
abs
Bx
3
BCS
rel
2*+
NV1BDIZC
2
LDA
iny
5+
NV1BDIZC
NZ
KIL LAX
iny
2
LDY
zpx
4
NV1BDIZC
CZ
2
LDA
zpx
4
NV1BDIZC
NZ
2
LDX
zpy
4
NV1BDIZC
NZ
LAX
zpy
1
CLV
imp
2
NV1BDIZC
V
3
LDA
aby
4+
NV1BDIZC
NZ
1
TSX
imp
2
NV1BDIZC
NZ
LAS
aby
3
LDY
abx
4+
NV1BDIZC
NZ
3
LDA
abx
4+
NV1BDIZC
NZ
3
LDX
aby
4+
NV1BDIZC
NZ
LAX
aby
Cx
2
CPY
imm
2
NV1BDIZC
CNZ
2
CMP
inx
6
NV1BDIZC
CNZ
NOP
imm
DCP
inx
2
CPY
zp
3
NV1BDIZC
CNZ
1
CMP
zp
3
NV1BDIZC
CNZ
2
DEC
zp
5
NV1BDIZC
NZ
DCP
zp
1
INY
imp
2
NV1BDIZC
NZ
2
CMP
imm
2
NV1BDIZC
CNZ
1
DEX
imp
2
NV1BDIZC
NZ
AXS
imm
3
CPY
abs
4
NV1BDIZC
CNZ
3
CMP
abs
4
NV1BDIZC
CNZ
3
DEC
abs
6
NV1BDIZC
NZ
DCP
abs
Dx
3
BNE
rel
2*+
NV1BDIZC
2
CMP
iny
5+
NV1BDIZC
CNZ
KIL DCP
iny
NOP
zpx
2
CMP
zpx
4
NV1BDIZC
CNZ
2
DEC
zpx
6
NV1BDIZC
NZ
DCP
zpx
1
CLD
imp
2
NV1BDIZC
D
3
CMP
aby
4+
NV1BDIZC
CNZ
NOP
imp
DCP
aby
NOP
abx
3
CMP
abx
4+
NV1BDIZC
CNZ
3
DEC
abx
7
NV1BDIZC
NZ
DCP
abx
Ex
2
CPX
imm
2
NV1BDIZC
CNZ
2
SBC
inx
6
NV1BDIZC
CNVZ
NOP
imm
ISC
inx
2
CPX
zp
3
NV1BDIZC
CNZ
2
SBC
zp
3
NV1BDIZC
CNVZ
2
INC
zp
5
NV1BDIZC
NZ
ISC
zp
1
INX
imp
2
NV1BDIZC
NZ
2
SBC
imm
2
NV1BDIZC
CNVZ
1
NOP
imp
2
NV1BDIZC
SBC
imm
3
CPX
abs
4
NV1BDIZC
CNZ
3
SBC
abs
4
NV1BDIZC
CNVZ
3
INC
abs
6
NV1BDIZC
NZ
ISC
abs
Fx
3
BEQ
rel
2*+
NV1BDIZC
2
SBC
iny
5+
NV1BDIZC
CNVZ
KIL ISC
iny
NOP
zpx
2
SBC
zpx
4
NV1BDIZC
CNVZ
2
INC
zpx
6
NV1BDIZC
NZ
ISC
zpx
1
SED
imp
2
NV1BDIZC
D
3
SBC
aby
4+
NV1BDIZC
CNVZ
NOP
imp
ISC
aby
NOP
abx
3
SBC
abx
4+
NV1BDIZC
CNVZ
3
INC
abx
7
NV1BDIZC
NZ
ISC
abx

The legal opcodes are linked to the description. The illegal opcodes are grey and not important for the moment. The address mode is in the bottom left, the timing in the bottom right. “+” indicates another cycle on a page boundary breach, “*” indicates another cycle, if the branch is taken. The affected flags are colored red at the bottom.

BRKSets a software interrupt with Break-Flag
ORABitwise OR to the accumulator
ASLShifts the value to the left, into Carry-Flag
PHPPuts the Status-Register (Flags) onto the stack
BPLBranches if negative flag is cleared
ASLRotates the value to the left, into Carry-Flag
CLCClears the Carry-Flag
ASLJumps to Sub-Routine
ANDBitwise AND to the accumulator
BITBit 7 of value to N-Flag, Bit 6 to V-Flag, and bitwise AND accumulator and value to Z-Flag
ROLRotates the value to the left, into Carry-Flag, old Carr-Flag value to bit 0
PLPLoads into Status-Register (Flags) from the stack
BMIBranches if negative flag is set
SECSets the Carry-Flag
RTIReturns from Interrupt
EORBitwise XOR to the accumulator
LSRShifts the value to the right, into Carry-Flag
int stepCPU() {
	switch (readFromMem(PC)) {
		case 0x29: { PC++; return AND(getImmediate(PC), 2); PC++; break; }
		case 0x2d: { PC++; return AND(getAbsolute(PC), 2); PC += 4; break; }
		case 0x39: { PC++; return AND(getAbsoluteYIndex(PC, registers.Y), 4); PC += 2; break; }		//	TODO +1 cyc if page boundary is crossed
		case 0x3d: { PC++; return AND(getAbsoluteXIndex(PC, registers.X), 4); PC += 2; break; }		//	TODO +1 cyc if page boundary is crossed
...

Most of the opcodes are fairly easy to implement, probably only ADC and SBC are the ones, that might give a headache.

ADC adds the value of the memory at the given address to the accumulator, plus the carry bit as well. Where SBC does the same, only subtracting the values from the accumulator. Therefore we can just write the ADC function, and use an inverted value as argument for it, to use it as SBC as well.

uint8_t ADD(uint8_t val, uint8_t cycles) {
	//	TODO handle bcd when decimal flag is set
	uint16_t sum = registers.A + val + status.carry;
	status.setOverflow((~(registers.A ^ val) & (registers.A ^ sum) & 0x80) > 0);
	status.setCarry(sum > 0xff);
	registers.A = sum;
	status.setZero(registers.A == 0);
	status.setNegative(registers.A >> 7);
	return cycles;
}
uint8_t ADC(uint16_t adr, uint8_t cycles) {
	return ADD(readFromMem(adr), cycles);
}
uint8_t SBC(uint16_t adr, uint8_t cycles) {
	return ADD(~readFromMem(adr), cycles);
}

The flags affected are zero, negative, carry and overflow (also decimal is important, but will be later added, when we use this CPU for the C64 emulation, as the NES’s CPU wasn’t capable of using this, probably just by a cut trace on the PCB).

zerois set when the result is zero
negativeis always bit 7 (0-based) of the result
carryis set when we have a carry, meaning that we can’t represent the result with 8 bits, therefore we check if the result is greater than 0xFF
overflowis set, when if two inputs with the same sign, produce a result with a different sign. So the flag actually represents that the sign of the outcome is wrong
Note: You can never have an overflow flag set, when the two inputs have different signs, as the range does not allow it.

For testing purposes I ran nestest, a testing ROM for almost all CPU behaviors. (Credits go to Kevin Horton, sadly I was unable to find a github or something else to link him to. Make sure to read his README)

Nestest provides a log file, with a proper output, that way, we can compare our output to it, and see how well it matches, and where our CPU might be going wrong.

Comparison between (modified) nestest.log and my output, mid-CPU-development

Comments

Leave a Reply