Writing a 6502 emulator in Python#
Computers seem to be magical devices that can be programmed to do anything, but how do they work in the first place? To understand how they work the easiest way is to write a simple 6502 emulator. The 6502 is a microprocessor that is used in the Nintendo Entertainment System (NES) and the Sega Genesis (and many other systems) for example, but also in the Commodore 64.
The 6502 is a very simple processor, it has only two registers, the accumulator and the program counter. The accumulator is used to store the result of arithmetic and logical operations, and the program counter is used to keep track of the current location in the program. It also can address 64 kilobytes of memory, which is also used to address the ROM, RAM, and I/O devices. This makes it ideal for writing an emulator and learning how to use it. The Z80 and Intel 8080 are more complex and require a lot of concepts to understand that are irrelevant for this tutorial series.
Most emulators are written in C, but this is not a requirement. You can write 6502 emulators in any language you like and this tutorial will show you how to do it in Python 3. Most computer science students will have access to Python 3 on their computers and a Raspberry Pi is powerful enough to complete this tutorial.
An introduction to the 6502#
A MOS Technology 6502 processor is designed by the former team that created the Motorola 6800 and it is a simplified and less expensive version of that design. The characterizing features of the 6502 are that it is an 8-bit processor, can address 16 bits of memory, and has 56 instructions. While the processor was designed in 1973 it is still being sold today as 65C02. Other versions were also created like the 6507 for the Atari 2600 with only 13 bits (8 kilobytes) to address memory or the 6509 that could do bank switching and address 1 megabyte of memory. Or the 6510 that had additional I/O-port was used in the Commodore 64.
While the registers are 8 bits wide it stores 16 bits values in memory following the little-endian convention. Meaning that it stores first the low byte and then the high byte, and both need to be swapped before using the values. The program counter is 16 bits wide and stores the address of the current instruction. It also limits the program to 64 kilobytes of memory that is used to address the ROM, RAM, and I/O devices.
With the limitations, the 6502 instruction set allows for multiple addressing options to reduce the number of processor cycles and speed up the program. The 6502 has two addressing modes, immediate and indirect. The immediate mode is used when the instruction is followed by a value that is used as an address. The indirect mode is used when the instruction is followed by a value that is used as an address, but the value is not used directly as an address, but instead, the address of the value is used.
Creating memory#
Platforms like Commodore 64 and Atari 2600, but also the Nintendo Entertainment System have different memory models. They all are based on the same three main memory regions with a Zero Page and Stack regions and a General-purpose region. The Zero Page region is used to store variables and constants, and the Stack region is used to store the return addresses of subroutines. The General-purpose region is used to store the ROM, RAM, and I/O devices.
Region |
Contents |
Description |
---|---|---|
|
Zero page |
The first page of memory, which is faster to access than other pages. Instructions can specify addresses within the zero page with a single byte as opposed to two, so instructions that use the zero page instead of any other page require one less CPU cycle to execute. |
|
Stack |
Last-in first-out data structure. Grows backward from |
|
General-purpose |
Memory that can be used for whatever purpose is needed. Devices that use the 6502 processor may choose to reserve sub-regions for other purposes, such as memory-mapped I/O. |
The Memory class by default creates an array of bytes with a size of 65536. This is the maximum size of the 6502, but if desired it can be reduced to a smaller size. The memory can be accessed using the []
operator as magic methods __getitem__
and __setitem__
are implemented. When memory is created the size is validated to at least two pages and a maximum size of 65536 bytes, and an exception is raised if the size is not valid. The address is also validated when accessing memory to make sure we stay within boundaries. And finally, the __setitem__ method also verifies that only 8-bit values are written.
"""Emulation of the MOT-6502 memory."""
class Memory:
"""Memory bank for MOT-6502 systems."""
def __init__(self, size: int = 65536) -> None:
"""Initialize the memory.
:param size: The size of the memory
:return: None
"""
if 0x0200 < (size - 1) > 0xFFFF:
raise ValueError("Memory size is not valid")
self.size = size
self.memory = [0] * self.size
print(len(self.memory))
def __getitem__(self, address: int) -> int:
"""Get the value at the specified address.
:param address: The address to read from
:return: The value at the specified address
"""
if 0x0000 < address > self.size:
raise ValueError("Memory address is not valid")
return self.memory[address]
def __setitem__(self, address: int, value: int) -> int:
"""Set the value at the specified address.
:param address: The address to write to
:param value: The value to write to the address
:return: None
"""
if 0x0000 < address > self.size:
raise ValueError("Memory address is not valid")
if value.bit_length() > 8:
raise ValueError("Value too large")
self.memory[address] = value
return self.memory[address]
Note
The class Memory currently presents all available memory as RAM and in later version support for both protected ROM and I/O devices needs to be added.
None of the memory is initialized to zero as this is part of the Post-Reset cycle and done by code present in the ROM at the vector address.
Creating the processor#
As stated the 6502 is an 8-bit processor, so we need to create a class that will represent the processor. The class will have the following attributes:
Register |
Size (bits) |
Purpose |
---|---|---|
Accumulator (A) |
8 |
Used to perform calculations on data. Instructions can operate directly on the accumulator instead of spending CPU cycles to access memory |
X register (X) |
8 |
Used as an index in some addressing modes |
Y register (Y) |
8 |
Used as an index in some addressing modes |
Program Counter (PC) |
16 |
Points to the address of the next instruction to be executed |
Stack Pointer (SP) |
8 |
Stores the stack index into which the next stack element will be inserted.
The address of this position is |
Status (SR) |
8 |
Each bit represents a status flag. Flags indicate the state of the CPU or information about the result of the previous instruction. See the table below for a description of each flag |
The Status Register is an 8-bit register that contains the following flags:
Bit |
Symbol |
Name |
Description |
---|---|---|---|
7 |
N |
Negative |
Compare: Set if the register’s value is less than the input value Otherwise: Set if the result was negative, i.e. bit 7 of the result was set |
6 |
V |
Overflow |
Arithmetic: Set if a signed overflow occurred during addition or subtraction, i.e. the sign of the result differs from the sign of both the input and the accumulator BIT: Set to bit 6 of the input |
5 |
- |
Unused |
Always set |
4 |
B |
Break |
Set if an interrupt request has been triggered by a |
3 |
D |
Decimal |
Decimal mode: mathematical instructions will treat the inputs and outputs as decimal numbers.
E.g. |
2 |
I |
Interrupt Disable |
Disables interrupts while set |
1 |
Z |
Zero |
Compare: Set if the register’s value is equal to the input value BIT: Set if the result of logically ANDing the accumulator with the input results in 0 Otherwise: Set if the result was zero |
0 |
C |
Carry |
Carry/Borrow flag used in math and rotate operations Arithmetic: Set if an unsigned overflow occurred during addition or subtraction, i.e. the result is less than the initial value Compare: Set if register’s value is greater than or equal to the input value Shifting: Set to the value of the eliminated bit of the input, i.e. bit 7 when shifting left, or bit 0 when shifting right |
The Processor class implements the attributes and two methods for initializing and resetting the processor. To reduce code complexity we also create the memory when the processor is created. The specification of the processor doesn’t specify the state of the processor at the start of the program, but it does specify the state after a reset.
"""Emulation of the MOT-6502 Processor."""
import m6502
class Processor:
"""MOT-6502 Processor."""
def __init__(self, memory: m6502.memory) -> None:
"""Initialize the processor.
:param memory: The memory to use
:return: None
"""
self.memory = memory
self.reg_a = 0 # Accumlator A
self.reg_y = 0 # Incex Register Y
self.reg_x = 0 # Incex Register X
self.program_counter = 0 # Program Counter PC
self.stack_pointer = 0 # Stack Pointer S
self.cycles = 0 # Cycles used
self.flag_c = True # Status flag - Carry Flag
self.flag_z = True # Status flag - Zero Flag
self.flag_i = True # Status flag - Interrupt Disable
self.flag_d = True # Status flag - Decimal Mode Flag
self.flag_b = True # Status flag - Break Command
self.flag_v = True # Status flag - Overflow Flag
self.flag_n = True # Status flag - Negative Flag
def reset(self) -> None:
"""Reset processor to initial state.
:return: None
"""
self.program_counter = 0xFCE2 # Hardcoded start vector post-reset
self.stack_pointer = 0x01FD # Hardcoded stack pointer post-reset
self.cycles = 0
self.flag_i = True
self.flag_d = False
self.flag_b = True
Note
The reset method has in its current form a hardcoded value for both the
program_counter
andstack_pointer
based on the C64 ROM and this will be correct in a later version.The attribute
cycles
are used to track the number of cycles used by the processor but are only part of the emulator to validate the correctness of the implementation.
Testing the memory and a processor#
Building and using a test suite makes it easier to test and debug the implementation. The easiest way to start is with the processor after it has been reset to the state specified in the design.
"""Verifies that the processor class works as expected."""
import m6502
def test_cpu_reset() -> None:
"""Verify CPU state after CPU Reset.
:return: None
"""
memory = m6502.Memory()
cpu = m6502.Processor(memory)
cpu.reset()
assert (
cpu.program_counter,
cpu.stack_pointer,
cpu.cycles,
cpu.flag_b,
cpu.flag_d,
cpu.flag_i
) == (0xFCE2, 0x01FD, 0, True, False, True)
Validating if the memory implementation is correct is currently not fully possible as the memory implementation is not fully specified. So let us assume that the memory implementation is correct and only test the memory implementation currently known. The means we can test the Zero Page and the Stack implementation, and the Start Vector addresses.
"""Verifies that the memory class works as expected."""
import pytest
import m6502
@pytest.mark.parametrize("i", range(0x0000, 0x0100))
def test_write_zero_page(i: int) -> None:
"""Verify that the Zero Page memory can be written to and
read from.
:param i: The address to write to
:return: None
"""
memory = m6502.Memory()
memory[i] = 0xA5
assert memory[i] == 0xA5
@pytest.mark.parametrize("i", range(0x0100, 0x0200))
def test_write_stack(i: int) -> None:
"""Verify that the Stack memory can be written to and
read from.
:param i: The address to write to
:return: None
"""
memory = m6502.Memory()
memory[i] = 0xA5
assert memory[i] == 0xA5
@pytest.mark.parametrize("i", [0xFFFC, 0xFFFD])
def test_write_vector(i: int) -> None:
"""Verify that the C64 vector memory can be written to and
read from.
:param i: The address to write to
:return: None
"""
memory = m6502.Memory()
memory[i] = 0xA5
assert memory[i] == 0xA5
Reading and writing to memory#
A processor needs to be able to read and write to memory. The 6502 has a 16-bit address space, so we need to be able to read and write to any address. Every memory location can store an 8-bit value and costs one clock cycle per read or write action.
class Processor:
"""MOT-6502 Processor."""
...
def read_byte(self, address: int) -> int:
"""Read a byte from memory.
:param address: The address to read from
:return: int
"""
data = self.memory[address]
self.cycles += 1
return data
def write_byte(self, address: int, value: int) -> None:
"""Write a byte to memory.
:param address: The address to write to
:param value: The value to write
:return: None
"""
self.memory[address] = value
self.cycles += 1
Now that we have a way to read and write to memory, we need to validate the functionality. We can use the read_byte()
and write_byte()
methods in the same test. In the repository, we have a test that does this per a separate function as well. The state of the CPU should not be changed and the number of cycles used should be 1 cycle each.
def test_cpu_read_write_byte() -> None:
"""Verify CPU can read and write a byte from memory.
The cost of the read and write operation is 1 cycle each, and
the state of the CPU is not changed.
:return: None
"""
memory = m6502.Memory()
cpu = m6502.Processor(memory)
cpu.reset()
cpu.write_byte(0x0001, 0xA5)
value = cpu.read_byte(0x0001)
assert (
cpu.program_counter,
cpu.stack_pointer,
cpu.cycles,
cpu.flag_b,
cpu.flag_d,
cpu.flag_i,
value,
) == (0xFCE2, 0x01FD, 2, True, False, True, 0xA5)
Working with words#
Besides reading and writing bytes, the 6502 can also read and write words that are two bytes long. This is useful for reading and writing to memory locations that are not byte aligned. In processor design, two ways are different on how to read and write words and are called little and big-endian. The 6502 is a little-endian processor, so we need to make sure to use the little-endian layout when reading and writing to memory. Most processors are little-endian, but we need to make sure that the emulator is working with the same endianness.
The Theory#
Endianness is the way that the bytes in a word are ordered when stored in memory. A big-endian word is stored as the most significant byte first, and a little-endian word is stored as the least significant byte first. If we visualize a word as a series of bits as seen in the diagram below, the most significant bit is on the left and the least significant bit is on the right. This is also how they are stored in memory and matches how they are stored in the processor.
High byte |
Low byte |
||||||||||||||||
Bits
|
Bits
|
With little-endian, the first byte is the least significant byte and the second byte is the most significant byte stored in memory. We have to make sure that the emulator converts the bytes in the correct order when reading and writing to memory. Later on, we will see how to do this.
Low byte |
High byte |
||||||||||||||||
Bits
|
Bits
|
The code#
Python has via the sys
module away to expose the endianness of a processor. The sys.byteorder
variable will return little
or big
depending on the endianness of the machine. With this information, we can create a function that will return the correct endianness of the word. The endianness will determine on which memory address we need to apply a bitshift operation to split the word into two bytes or add two bytes together.
For reading a word on a little-endian machine we only have to read the low byte at the initial address and then read the high byte at the next address and combine them into a word by shifting the high byte to the left by 8 bits. For big-endian processors, we need to read the high byte first and then the low byte and combine them into a word by shifting the high byte to the left by 8 bits. For writing a word we can mostly reuse how we handled the reading of a word. The only difference is to shift the high byte to the right by 8 bits and make sure it is only 8 bits long.
class Processor:
"""MOT-6502 Processor."""
...
def read_word(self, address: int) -> int:
"""Read a word from memory.
:param address: The address to read from
:return: int
"""
if sys.byteorder == "little":
data = self.read_byte(address) | (self.read_byte(address + 1) << 8)
else:
data = (self.read_byte(address) << 8) | self.read_byte(address + 1)
return data
def write_word(self, address: int, value: int) -> None:
"""Split a word to two bytes and write to memory.
:param address: The address to write to
:param value: The value to write
:return: None
"""
if sys.byteorder == "little":
self.write_byte(address, value & 0xFF)
self.write_byte(address + 1, (value >> 8) & 0xFF)
else:
self.write_byte(address, (value >> 8) & 0xFF)
self.write_byte(address + 1, value & 0xFF)
The test case test_cpu_read_write_word()
is similar to test_cpu_read_write_byte()
as described in the previous section, but it uses the read_word()
and write_word()
methods instead of read_byte()
and write_byte()
. Secondly, the values used are 0xA5
(1010 0101
) and 0x5A
(0101 1010
) so we can test if the values are stored and retrieved correctly. The only drawback is that the tests only validate the working for the processor architecture that is running the tests on.
def test_cpu_read_write_word() -> None:
"""Verify CPU can read and write a byte from memory.
The cost of the read and write operation is 2 cycles each, and
the state of the CPU is not changed.
:return: None
"""
memory = m6502.Memory()
cpu = m6502.Processor(memory)
cpu.reset()
cpu.write_word(0x0001, 0x5AA5)
value = cpu.read_word(0x0001)
assert (
cpu.program_counter,
cpu.stack_pointer,
cpu.cycles,
cpu.flag_b,
cpu.flag_d,
cpu.flag_i,
value,
) == (0xFCE2, 0x01FD, 4, True, False, True, 0x5AA5)
Fetching opcodes#
We now can implement the fetching of instructions or more precisely the opcode which is the numeric value of the instruction and also any arguments that the instruction might need. The processor has a register for the program counter which is used to keep track of the current address in memory. The opcode is then read from memory at the current address and the program counter is incremented by one. The same goes for the arguments which can be both a byte or a word.
We have to implement a function to fetch a byte or word from memory. The function will take the current address from the program_counter
register and return the byte or word at that address. Here we can reuse the read_byte()
and read_word()
functions from the previous section and only increase the program_counter
.
class Processor:
"""MOT-6502 Processor."""
...
def fetch_byte(self) -> int:
"""Fetch a byte from memory.
:param address: The address to read from
:return: int
"""
data = self.read_byte(self.program_counter)
self.program_counter += 1
return data
def fetch_word(self) -> int:
"""Fetch a word from memory.
:param address: The address to read from
:return: int
"""
data = self.read_word(self.program_counter)
self.program_counter += 2
return data
Validating these methods is very simlar as the one we used for reading a byte or word. We can reuse the same tests and make sure the program counter is incremented by the correct amount. For a byte we need an increment of one and for a word we need an increment of two.
1def test_cpu_fetch_byte() -> None:
2 """Verify CPU can fetch a byte from memory.
3
4 The cost of the fetch operation is 1 cycle, and increases the
5 program counter by 1. The state of the CPU is not changed
6 further.
7
8 :return: None
9 """
10 memory = m6502.Memory()
11 cpu = m6502.Processor(memory)
12 cpu.reset()
13 memory[0xFCE2] = 0xA5
14 value = cpu.fetch_byte()
15 assert (
16 cpu.program_counter,
17 cpu.stack_pointer,
18 cpu.cycles,
19 cpu.flag_b,
20 cpu.flag_d,
21 cpu.flag_i,
22 value,
23 ) == (0xFCE3, 0x01FD, 1, True, False, True, 0xA5)
24
25def test_cpu_fetch_word() -> None:
26 """Verify CPU can fetch a word from memory.
27
28 The cost of the fetch operation is 2 cycle, and increases the
29 program counter by 2. The state of the CPU is not changed
30 further.
31
32 :return: None
33 """
34 memory = m6502.Memory()
35 cpu = m6502.Processor(memory)
36 cpu.reset()
37 memory[0xFCE2] = 0xA5
38 memory[0xFCE3] = 0x5A
39 value = cpu.fetch_word()
40 assert (
41 cpu.program_counter,
42 cpu.stack_pointer,
43 cpu.cycles,
44 cpu.flag_b,
45 cpu.flag_d,
46 cpu.flag_i,
47 value,
48 ) == (0xFCE4, 0x01FD, 2, True, False, True, 0x5AA5)
Executing instructions#
class Processor:
"""MOT-6502 Processor."""
...
def execute(self, cycles: int = 0) -> None:
"""
Execute code for X amount of cycles. Or until a breakpoint is reached.
:param cycles: The number of cycles to execute
:return: None
"""
while (self.cycles < cycles) or (cycles == 0):
opcode = self.fetch_byte()
if opcode == 0x18:
self.ins_nop_imp()
elif opcode == 0xEA:
self.ins_clc_imp()
...
def ins_nop_imp(self) -> None:
"""
NOP - No Operation.
:return: None
"""
self.cycles += 1
def ins_clc_imp(self) -> None:
"""
CLC - Clear Carry Flag.
:return: None
"""
self.flag_c = False
self.cycles += 1
Optimizing the code#
class Processor:
"""MOT-6502 Processor."""
...
ADDRESSING = [
# 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
"imp", "inx", "imp", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "acc", "imm", "abs", "abs", "abs", "abs", # 0
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # 1
"abs", "inx", "imp", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "acc", "imm", "abs", "abs", "abs", "abs", # 2
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # 3
"imp", "inx", "imp", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "acc", "imm", "abs", "abs", "abs", "abs", # 4
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # 5
"imp", "inx", "imp", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "acc", "imm", "ind", "abs", "abs", "abs", # 6
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # 7
"imm", "inx", "imm", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "imp", "imm", "abs", "abs", "abs", "abs", # 8
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpy", "zpy", "imp", "aby", "imp", "aby", "abx", "abx", "aby", "aby", # 9
"imm", "inx", "imm", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "imp", "imm", "abs", "abs", "abs", "abs", # A
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpy", "zpy", "imp", "aby", "imp", "aby", "abx", "abx", "aby", "aby", # B
"imm", "inx", "imm", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "imp", "imm", "abs", "abs", "abs", "abs", # C
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # D
"imm", "inx", "imm", "inx", "zp", "zp", "zp", "zp", "imp", "imm", "imp", "imm", "abs", "abs", "abs", "abs", # E
"rel", "iny", "imp", "iny", "zpx", "zpx", "zpx", "zpx", "imp", "aby", "imp", "aby", "abx", "abx", "abx", "abx", # F
]
OPCODES = [
# 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
"brk", "ora", "nop", "slo", "nop", "ora", "asl", "slo", "php", "ora", "asl", "nop", "nop", "ora", "asl", "slo", # 0
"bpl", "ora", "nop", "slo", "nop", "ora", "asl", "slo", "clc", "ora", "nop", "slo", "nop", "ora", "asl", "slo", # 1
"jsr", "and", "nop", "rla", "bit", "and", "rol", "rla", "plp", "and", "rol", "nop", "bit", "and", "rol", "rla", # 2
"bmi", "and", "nop", "rla", "nop", "and", "rol", "rla", "sec", "and", "nop", "rla", "nop", "and", "rol", "rla", # 3
"rti", "eor", "nop", "sre", "nop", "eor", "lsr", "sre", "pha", "eor", "lsr", "nop", "jmp", "eor", "lsr", "sre", # 4
"bvc", "eor", "nop", "sre", "nop", "eor", "lsr", "sre", "cli", "eor", "nop", "sre", "nop", "eor", "lsr", "sre", # 5
"rts", "adc", "nop", "rra", "nop", "adc", "ror", "rra", "pla", "adc", "ror", "nop", "jmp", "adc", "ror", "rra", # 6
"bvs", "adc", "nop", "rra", "nop", "adc", "ror", "rra", "sei", "adc", "nop", "rra", "nop", "adc", "ror", "rra", # 7
"nop", "sta", "nop", "sax", "sty", "sta", "stx", "sax", "dey", "nop", "txa", "nop", "sty", "sta", "stx", "sax", # 8
"bcc", "sta", "nop", "nop", "sty", "sta", "stx", "sax", "tya", "sta", "txs", "nop", "nop", "sta", "nop", "nop", # 9
"ldy", "lda", "ldx", "lax", "ldy", "lda", "ldx", "lax", "tay", "lda", "tax", "nop", "ldy", "lda", "ldx", "lax", # A
"bcs", "lda", "nop", "lax", "ldy", "lda", "ldx", "lax", "clv", "lda", "tsx", "lax", "ldy", "lda", "ldx", "lax", # B
"cpy", "cmp", "nop", "dcp", "cpy", "cmp", "dec", "dcp", "iny", "cmp", "dex", "nop", "cpy", "cmp", "dec", "dcp", # C
"bne", "cmp", "nop", "dcp", "nop", "cmp", "dec", "dcp", "cld", "cmp", "nop", "dcp", "nop", "cmp", "dec", "dcp", # D
"cpx", "sbc", "nop", "isb", "cpx", "sbc", "inc", "isb", "inx", "sbc", "nop", "sbc", "cpx", "sbc", "inc", "isb", # E
"beq", "sbc", "nop", "isb", "nop", "sbc", "inc", "isb", "sed", "sbc", "nop", "isb", "nop", "sbc", "inc", "isb", # F
]
def execute(self, cycles: int = 0) -> None:
"""
Execute code for X amount of cycles. Or until a breakpoint is reached.
:param cycles: The number of cycles to execute
:return: None
"""
while (self.cycles < cycles) or (cycles == 0):
opcode = self.fetch_byte()
eval("self.ins_" + self.OPCODES[opcode] + "_" + self.ADDRESSING[opcode] + "()") # noqa: PLW0123
Verifying instructions#
"""
NOP - No Operation.
The NOP instruction causes no changes to the processor other than the normal
incrementing of the program counter to the next instruction.
Processor Status after use:
+------+-------------------+--------------+
| Flag | Description | State |
+======+===================+==============+
| C | Carry Flag | Not affected |
+------+-------------------+--------------+
| Z | Zero Flag | Not affected |
+------+-------------------+--------------+
| I | Interrupt Disable | Not affected |
+------+-------------------+--------------+
| D | Decimal Mode Flag | Not affected |
+------+-------------------+--------------+
| B | Break Command | Not affected |
+------+-------------------+--------------+
| V | Overflow Flag | Not affected |
+------+-------------------+--------------+
| N | Negative Flag | Not affected |
+------+-------------------+--------------+
+-----------------+--------+-------+--------+
| Addressing Mode | Opcode | Bytes | Cycles |
+=================+========+=======+========+
| Implied | 0xEA | 1 | 2 |
+-----------------+--------+-------+--------+
"""
import m6502
def test_cpu_ins_nop() -> None:
"""
Do nothing for 1 computer cycle.
return: None
"""
memory = m6502.Memory()
cpu = m6502.Processor(memory)
cpu.reset()
memory[0xFCE2] = 0xEA
cpu.execute(2)
assert (
cpu.program_counter,
cpu.stack_pointer,
cpu.cycles,
) == (0xFCE3, 0x01FD, 2)
Note
The repository contains more information about the project.