Writing a 6502 emulator, part 1

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

Next step

As we have a bare minimum implementation of the processor and memory, we can now start to build the emulator. In the next step we will add the instructions to the processor and test the implementation.

Note

  • The repository contains more information about the project.

  • All parts 1, 2