Show HN: Arduino 6502 Controller

Show HN: Arduino 6502 Controller

The 6502ctl project is an Arduino controller for the 6502 CPU. The controller controls all 6502 pins, including the clock signal and interrupts, and simulates an address and data bus with attached memory and an output peripheral. The controller includes a clock-cycle debugger with disassembler. An assembler is also included with the project.



This project requires the following hardware components:


Refer to the W65C02S datasheet Figure 3-1 that describes the W65C02S pins.

  • Address bus: connect W65C02S pins A0-A15 to Arduino pins A0-A15 (ATmega2560 ports F,K).
  • Data bus: connect W65C02S pins D0-D7 to Arduino pins 49-42 (ATmega2560 port L):
    • D0 → 49
    • D1 → 48
    • D7 → 42
  • Input control: connect W65C02S input control pins to Arduino pins 22-28 (ATmega2560 port A):
    • RDY → 22
    • IRQB → 23
    • NMIB → 24
    • RESB → 25
    • SOB → 26
    • PHI2 → 27
    • BE → 28
  • Output control: connect W65C02S output control pins to Arduino pins 35-30 (ATmega2560 port C):
    • RWB → 35
    • PHI2O → 34
    • SYNC → 33
    • MLB → 32
    • PHI1O → 31
    • VPB → 30
  • Power: connect W65C02S VCC to Arduino 5V and W65C02S VDD to Arduino GND.
  • NC Pin: DO NOT connect the W65C02S NC pin.


This software consists of the following components:

  • Controller: runs on the Arduino MEGA 2560 and fully controls the 6502.
    • Provides a clock signal to the 6502. The clock signal is variable and can be fully stopped. Under normal operation the Arduino MEGA 2560 is able to produce a clock signal every 5 μs, which gives it a maximum speed of 200KHz.
    • Simulates an address bus and data bus so that the 6502 can access memory and peripherals.
    • Simulates a 16K ROM and a 4K RAM memory.
    • Simulates a memory mapped I/O range to interface with peripherals and a single character output peripheral.
    • Simulates interrupts from peripherals.
    • Includes a simple debugger that is able to single-step clock cycles. The debugger shows the address and data bus values and whether the address space is read or written. In addition the debugger will disassemble the instruction when the 6502 fetches an instruction opcode.
  • Assembler: runs on the host computer and is used to produce ROM images for the 6502.

How to use

After connecting all hardware components and the Arduino MEGA 2560 to the host computer, open this project in the Arduino IDE. Upload the sketch to the Arduino and open the Serial Monitor and set it to 1000000 baud.

The following will appear in the Arduino Serial Monitor (there may also be some garbage characters before this message from the serial interface – we can ignore these):

    s to step
    c to continue
    b to break
    r to reset
r--V fffc 00

The 6502ctl debugger informs us of the few commands that it supports:

  • s to step a single clock cycle
  • c to continue (i.e. run without stepping)
  • b to break into the debugger after continuing
  • r to reset the 6502

The debugger also follows up with a line that describes the operation that the 6502 just performed. Every clock signal the 6502 performs an operation and the debugger (if active) will output a line describing that operation. The line contains information about some of the 6502 control pins, the address bus and data bus values and possibly a disassembled instruction.

The line has the general format:


The 4 first characters encode values from the 6502 control pins:

  • r or W: RWB pin; determines whether the 6502 is trying to read or Write.
  • S or -: SYNC pin; determines whether the 6502 is fetching an instruction opcode.
  • - or M: MLB pin; the 6502 sets this pin for some “Read-Modify-Write” instructions.
  • - or V: VPB pin; determines whether the 6502 is accessing a vector location (e.g. RESET).

The ADDR is the hex address that the 6502 is accessing. The DATA is the hex value that the 6502 is reading or writing. The INSTR is the disassembled instruction and only appears when the debugger determines that the 6502 is fetching an instruction opcode (when the SYNC pin is high).

We can now interpret the line r--V fffc 00. This means that the 6502 is reading from hex address $fffc the value $00 and is accessing a Vector location (RESET). This is part of the 6502’s startup/reset sequence.

In the Serial Monitor enter s and press ENTER:

The 6502 is reading from hex address $fffd the value $c0 and is accessing a Vector location (RESET).

What we are seeing is the 6502 reset sequence. After a reset the 6502 expects to find the start of the program at location $fffc. Because this is a 16-bit address this is stored in locations $fffc and $fffd. The 6502 read the values $00 and $c0 which together comprise the address $c000 (in little-endian format). This is where our program starts.

Enter s again and press ENTER:

The 6502 starts executing instructions at address $c000. It fetches the first instruction opcode and signals this via the SYNC pin. The debugger notices this and disassembles the instruction: LDA #$ff (load the accumulator with the immediate value #$ff). Note that the 6502 has not fully fetched the instruction yet; it is just that the 6502ctl debugger has looked ahead in memory to find all instruction bytes so that it could disassemble it.

Enter s again and press ENTER:

The 6502 is now reading the additional instruction bytes. In this case this is the value #$ff that is the immediate value to be loaded into the accumulator.

Enter s again and press ENTER:

The 6502 is now reading another instruction: TAX (transfer the accumulator to register X).

Enter s again and press ENTER:

The 6502 is taking some time to execute the TAX instruction.

Enter s again and press ENTER:

The 6502 is now reading another instruction: TXS (transfer register X to the stack pointer).

At this point we can just continue execution by entering c and pressing ENTER:

The debugger allows the clock to run at full speed and the 6502 executes a program that sends the string hello world from 6502 to the Serial Monitor.

We can break again into the debugger by pressing b and ENTER:

We can now reset the 6502 by pressing r and ENTER:

The 6502 is executing its reset sequence again.

Serial Monitor

NOTE: Most modern debuggers show the next statement/instruction that is going to be executed. The 6502ctl debugger always shows the last operation (not instruction) that was executed.

How it works

The main loop of 6502ctl is as follows (some detail removed for this discussion):

void loop()
    uint16_t addr;
    uint8_t data;
    uint8_t octl;

    cli();                              // (1) Disable Arduino interrupts

    for (;;)
        clock_rise();                   // (2) Clock goes high

        octl=read_octl();             // (3) Read 6502 output control pins
        addr=read_abus();             // (4) Read address from 6502 address bus
        if (octl & P6502_OCTL_PIN(RWB)) // (5) Test the RWB pin
        {                               // (5T) If RWB pin is high the 6502 is reading
            data=read_data(addr);     // (5T.1) Read data from simulated ROM, RAM or MMIO
            write_dbus(data);           // (5T.2) Write data to data bus
        {                               // (5F) If RWB pin is low the 6502 is writing
            data=read_dbus();         // (5F.1) Read data from data bus
            write_data(addr, data);     // (5F.2) Write data to simulated ROM, RAM or MMIO

        clock_fall();                   // (6) Clock goes low

        if (debug_available())          // (7) Fast check if debugger is active
            debug(addr, data, octl);    // (7T.1) Debugger implementation

A few things to note:

  • Arduino interrupts are disabled in order to maximize performance in the core loop. Arduino interrupts are enabled only for serial communications and the debugger.
  • All communication with the 6502 happens while the clock is high.
  • Communication with the 6502 does not happen via Arduino’s digitalRead / digitalWrite, because this would make the loop very slow. Instead the ATmega 2560 ports are accessed directly. For example to read from the address bus we read ports F and K directly (some detail removed):
    static inline uint16_t read_abus()
        return PINF | (PINK 8);
  • We want maximum performance w

Read More

Charlie Layers

Charlie Layers

Fill your life with experiences so you always have a great story to tell