YA6S: Yet Another 6502 Simulator

YA6S is Yet Another 6502 Simulator, implementing a cycle-accurate WDC 65C02S microprocessor. It includes simulated RAM, ROM, and a 16550-compatible UART. It provides a set of interfaces that can be implemented to provide additional types of devices to the simulated system. All 65C02S instructions are supported, and YA6S successfully passes the widely-used functional tests provided by Klaus Dormann.

Motivation

YA6S is intended to be a prototyping platform for designing software for a physical 6502 based microcomputer. It has a pluggable device architecture and a configurable memory map, allowing for simple changes in hardware configuration to accommodate different hardware designs.

It is inspired by the SimH retrocomputing emulator, used to emulate vintage minicomputer systems such as the DEC PDP-11 and Data General Nova. SimH also provides a flexible configuration system for devices, and includes a monitor interface that allows users to suspend the operation of the emulated machine, check registers and memory, and set breakpoints.

As a cycle-level simulation, YA6S executes each instruction the way a real 65C02 does; each cycle has a memory read or write, and the order of these operations is the same as an actual 65C02. For example, an add instruction that uses absolute addressing (like ADC $1234) takes four cycles to execute:

  • Read at the program counter, which contains the ADC opcode, increment the program counter.
  • Read at the program counter, which contains the low byte of the address, increment the program counter.
  • Read at the program counter, which contains the high byte of the address, increment the program counter.
  • Read the data at the address, and add it to the accumulator.

All of these reads and writes (if any) are performed on the simulated backplane.

Example of Use

YA6S includes a shell script that starts the Java Virtual Machine with the libraries it depends on. Usually, you will want to start this with a configuration file that attaches devices like RAM, ROM, and UARTs.

Here is an exmple of running a small program that emits "Hello, world!" to a UART that is attached to the terminal:

$ bin/ya6s ya6s.config
A: $00,  X: $00,  Y: $00,  S: $00,  P: $20 (nv1bdizc) cycles: 0
0000:  00 00     BRK #$00
>>> cont
Hello, world!
(Ctrl-E to pause.)
Stopped.
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $66 (nV1bdIZc) cycles: 45425
F827:  40        RTI
>>> r f800 f83f
       0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F   01234567 89ABCDEF
F800: A9 83 8D 03 F0 A9 78 8D  00 F0 9C 01 F0 A9 03 8D  |©...ð©x. .ð..ð©..|
F810: 03 F0 A9 20 A2 00 BC 28  F8 F0 0B 2C 05 F0 F0 FB  |.ð© ¢.¼( øð.,.ððû|
F820: 8C 00 F0 E8 80 F0 DB 40  48 65 6C 6C 6F 2C 20 77  |..ðè.ðÛ@ Hello, w|
F830: 6F 72 6C 64 21 0D 0A 00  00 00 00 00 00 00 00 00  |orld!... ........|
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $66 (nV1bdIZc) cycles: 45425
F827:  40        RTI
>>> r fff0 ffff
       0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F   01234567 89ABCDEF
FFF0: 00 00 00 00 00 00 00 00  00 00 27 F8 00 F8 27 F8  |........ ..'ø.ø'ø|
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $66 (nV1bdIZc) cycles: 45425
F827:  40        RTI
>>> exit

Configuration

YA6S can take a configuration file at startup, which is a series of monitor commands. A configuration file will usually attach devices to the virtual backplane before the system is reset. When a 65C02 processor is reset, it reads from the memory address $FFFC, so it is common to configure a ROM device that covers that address range.

The configuraion file used in the example above attaches devices to the virtual backplane.

Devices

YA6S does not define a memory map. Devices are attached to virtual data, address, and control busses, and can react to changes of the values on those busses. At each clock tick, a typical device will check the address bus to see if it is within the expected range, and check the control bus to see if this is a read or a write.

YA6S currently ships with four device types. These can be configured to listen to any address, and multiple instances are allowed.

  • A static RAM which simply loads and stores data.
  • A ROM that can be initialized from bytes in a file.
  • A 16550 UART, which can be attached to the terminal.
  • A Counter that is decremented each clock tick, firing an interrupt when it reaches zero.

Devices use the attach command to register themselves with the backplane.

For example, these commands establish a memory map that puts 32kB of RAM at memory location $0000, a UART device at memory location $F000, and a ROM device initialized with the contents of "rom.bin" at location $F800. There is nothing registered between memory locations $8000 and $EFFF.

attach org.joev.ya6s.SRAM base=0000 size=8000
attach org.joev.ya6s.UART base=F000 path="tty"
attach org.joev.ya6s.ROM  base=F800 size=0800 file="rom.bin"

Monitor

The Monitor allows the user to control the activity of YA6S. It can be used to attach devices to the virtual backplane, inspect and modify registers and memory, and reset the system. It also allows for single-stepping through a program, and can disassemble instructions, which is very useful for debugging.

The monitor supports the following commands:

  • reset (alias r): Strobes the 65C02 RESET line, which causes the processor to jump to the location read from the RESET vector $FFFC/D.
  • attach {device class} [param*]: Attach a device to the backplane.
  • write {location} [byte+] (Alias w): Write bytes using the address and data busses.
  • read {start} [end] (Alias r): Read and display bytes using the address and data busses.
  • step (Alias s): Run the instruction at the program counter and stop.
  • cont (Alias c): Run the program from the current program counter. Use Ctrl-E to pause execution.
  • load {location} {path}: Load bytes from a file into the system using the address and data busses.
  • disassemble {location} [count]: Disassemble instructions at a location.
  • breakpoint {subcommand} [args] (Alias break): Manage breakpoints, see section below for subcommands.
  • profile {subcommand} [args]: Manage profiler, see section below for subcommands.
  • exit: Exit YA6S.

Breakpoints

The monitor can be used to set breakpoints, which pauses the simulator if the condition of a breakpoint is met. The conditions can include processor registers. The breakpoint command has subcommands:

  • list: Display the current set of breakpoints.
  • remove: Remove a breakpoint.
  • when {expression}: Add a breakpoint with the given expression.
  • at {address}: Add a breakpoint that pauses execution when the program counter reaches the given address.

The expression syntax allows comparisons with constants and registers, as well as the individual status register bits. For example, to set a breakpoint when Y is less than A, use break when Y < A. To set a breakpoint when the carry bit is set, use break when C = 1.

Profiler

The profiler keeps a count of how many times an instruction at each address is executed. This can be used to find "hot spots" in code, and provides guidance for where optimization may be helpful.

The subcommands:

  • on: Enable the profiler. Start keeping counts of instructions.
  • off: Disable the profiler.
  • reset: Reset the execution counts to zero.
  • show [count]: Show the counts for each instruction, in descending order.

For example:

A: $00,  X: $00,  Y: $00,  S: $00,  P: $20 (nv1bdizc) cycles: 0
0000:  00 00     BRK #$00
>>> profile on
A: $00,  X: $00,  Y: $00,  S: $00,  P: $20 (nv1bdizc) cycles: 0
0000:  00 00     BRK #$00
>>> cont
Hello, world!
(Ctrl-E to pause.)
Stopped.
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $26 (nv1bdIZc) cycles: 40889
F827:  40        RTI
>>> profile show 6
$F81B:             6773
$F81E:             6773
$F816:               16
$F819:               16
$F820:               15
$F823:               15
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $26 (nv1bdIZc) cycles: 40889
F827:  40        RTI
>>> disassemble F812
F812:  A9 20     LDA #$20
F814:  A2 00     LDX #$00
F816:  BC 28 F8  LDY $F828,X
F819:  F0 0B     BEQ 11
F81B:  2C 05 F0  BIT $F005
F81E:  F0 FB     BEQ -5
F820:  8C 00 F0  STY $F000
F823:  E8        INX
F824:  80 F0     BRA -16
F826:  DB        STP
A: $20,  X: $0F,  Y: $00,  S: $FD,  P: $66 (nV1bdIZc) cycles: 46577
F827:  40        RTI
>>>

This shows that the instructions at $F81B and $F81E are the most frequently executed instructions. The disassembly shows that this is the tight polling loop BIT $F005; BEQ -5. Perhaps it would be good to move to an interrupt-based design here.

Implementing New Devices

Devices are implemented as Java classes that have a public constructor that takes two arguments: a Backplane and a Map containing configuration parameters for the device, as well as a tick method that takes a Signal.EventType that gets called each time the clock edge rises or falls. The class does not need to extend from any particular superclass.

Typically, on a positive edge signal, the tick code will check the value of the address bus on the Backplane object, and if the value is not within the expected range, the function returns. Otherwise, it will typically check the read/write signal on the Backplane, and if the signal is true, place data on the data bus on the Backplane, otherwise, it will read data from the data bus. The device can assert the interrupt signal on the backplane as necessary.

The Counter device is a simple example of how a device is implemented. The UART is a more complex implementation that includes asynchronous activity using dedicated threads.

Conclusion

YA6S is a flexible prototyping platform for designing and debugging small computers built around the 65C02 microprocessor family. It goes beyond instruction fetch and execution and is intended to mimic the "microcode" executed for each processor cycle.