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

$0000 - $00FF

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.

$0100 - $01FF

Stack

Last-in first-out data structure. Grows backward from $01FF to $0100. Used by some transfer, stack, and subroutine instructions.

$0200 - $FFFF

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 $0100 + SP. SP is initially set to $FD

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 BRK instruction

3

D

Decimal

Decimal mode: mathematical instructions will treat the inputs and outputs as decimal numbers. E.g. $09 + $01 = $10

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 and stack_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

15

14

13

12

11

10

9

8

Bits

7

6

5

4

3

2

1

0

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

7

6

5

4

3

2

1

0

Bits

15

14

13

12

11

10

9

8

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.