mos-trap: A simple (external) 6502 debugger

For the holiday season of 2022, my partner gave me Ben Eater’s 6502 computer kit. It’s one of the greatest gifts I’ve ever received: I’ve been following Ben’s video series for a long while now, and am a big fan of older computers with much easier to understand architectures. Over the summer, as I found time, I painstakingly cut dozens and dozens of wires to length, routed them around the breadboard, cussed myself out as they were inevitably slightly too long or too short, and all around had a fantastic time.

Once I was fairly confident that I had everything correctly wired, I threw together a quick “Hello, world” binary, flashed the EEPROM using a Pi and a quick script I wrote (maybe another post on that in the future), popped it back into the main board, and… nothing.

That’s not to say I was disappointed, this was the most likely outcome and frankly exactly what I had expected to happen. I quickly double-checked all of the wiring, trying to ensure there weren’t any loose or simply wrong connections, but to my chagrin, nothing seemed to get that little 2-line display to spit out those annoying two words. I knew that the next reasonable step was to check the wires electrically, not visually, so I pulled out my little 1-channel portable oscilloscope and started probing around. Still, everything seemed sound, and I knew that realistically, I would need a better solution to view the full state of the data and address busses, in their entirety, at once. Additionally, a manual clock would be tremendously helpful, as I did not have the clock module Ben provides, and troubleshooting the state of a computer that is running at 1MHz is quite a challenge.

As part of his video series, Ben actually put together an arduino-based debugger, which was largely the inspiration for this project. I don’t have any of the larger arduinos around (all of my projects have used the micro, which doesn’t have enough pins for this project), but I do have a handful of raspberry pi zeros from a failed kubernetes cluster experiment I did several years ago, and figured that just might be perfect for this application.

This also proved to be the perfect excuse for me to try and learn a proper TUI framework. I’ve played with ncurses in the past, but mostly I try to avoid UI programming in favor of clean CLIs. I poked around several of the popular TUI crates for rust and settled on ratatui, a fork of the succinctly named (but now deprecated) tui crate. After skimming the docs, I felt there was nothing left but to get started!

Visualizing the 6502 Busses

At their core, the 6502’s address and data busses are simply 16 and 8 bit numbers shared between devices in the system. The 6502 asserts an address onto the bus, and depending on the instruction, either a device on the bus or the 6502 itself will assert data onto the bus. The biggest application of this system, at least for this project, is the loading of assembled code from an EEPROM to the 6502 to execute. This means that the majority of data on the data bus is going to be 6502 assembly, as well as the operands those instructions require to operate.

I wanted my debugger to be able to translate those opcodes to instruction names for simplicity; rather than reading EA, I wanted the debugger to also print out NOP, the corresponding instruction. This would make debugging much quicker, as I don’t have the 256 (minus a handful of “illegal” opcodes) instructions memorized offhan, and didn’t want to have to continually pull up a reference to translate them on-the-fly. So, I found a lovely concise table at https://www.masswerk.at/6502/6502_instruction_set.html, and started translating the information there to some const arrays. That way, I can simply pass in a byte and get the corresponding opcode information: names, width, legality, and cycle count.

The cycle count and width here are particularly important for the implementation. Each instruction has a varying “width” depending on how many operands follow it. The operands themselves are not instructions, but rather addresses or data required by the instruction. This means that if I simply translated every byte on the data bus into an instruction, I’d be incorrectly visualizing data as instructions. Confusing, to say the least. Cycle count, in a similar vein, is the number of CPU cycles that an instruction takes to execute. When the CPU is executing an instruction, the address and data bus typically remain unchanged between clock cycles. Thus, the number of cycles that a single instruction takes up depends on both cycle count and instruction width. Knowing this information is required to correctly determine which data bytes are addresses, which ones are operands, and which ones are simply unused while the CPU is internally performing some instruction.

At the time of writing, I still haven’t finished this part of the implementation actually- instruction width is taken into account, but cycle count is not. Thus, the translated instruction display actually desynchronizes after any multi-cycle instruction is encountered. I hope to get around to fixing this soon!

Reading Bus Data

I wanted this project to be as hardware-agnostic as possible. I’m still learning rust and am far from an expert, but I have used the embedded-hal crate before to write hardware-agnostic libraries. The part I’m less clear on, however, is the best way to support multiple HAL implementations in a single binary. Since all I have is a raspberry pi to test this with, I decided to do as much of the abstraction as I could initially, but focus more on the functionality of the application. This way, the codebase would be open-ended enough to enable adding support for other hardware in the future with minimal effort, but I wouldn’t spend too much time in the present adding support that has (currently) no practical demand. I settled on using feature gates to determine which HAL implementation to use at compile-time, which means that binaries are not hardware agnostic, but the codebase (more or less) is.

For the Pi implementation, it was simply a matter of grouping a bunch of GPIO pins together for the address and data lines, and using the rppal crate (which implements embedded-hal) to configure them for input. A helper function to iterate over the lines, read their state, and assemble a value by some simple bit math, and I had a succinct interface to fetch u8 and u16 values that represent the current state of the bus!

The TUI

Unfortunately, I am far from an experienced UI programmer, and I think it really shows with this project. I won’t spend much time talking about it here as I know that a lot can be improved upon. The short and sweet of it is that I spent a little time generating a palette to use for the UI, and building components to track the history of the bus state and allow some simple control and navigation.

I will say that I learned a lot, which was the goal, so next time I will hopefully have something I’m much more proud of!

Bringing it Together

I added the ability to manually trigger the clock (you thought I forgot, admit it!) from the TUI, which sends a clock pulse and automatically reads the new bus state, pushing it to the UI.

mos-trap screenshot

Picture here is the first several steps of the reset sequence for the 6502: we see some (seemingly) garbage data before the CPU asserts addresses 0xFFFC and 0xFFFD onto the bus. These addresses are meant to contain the “reset vector”, which tells the CPU which address to jump to to begin execution. In this case, we fetch 0x8000 from those addresses, and you can see that the CPU jumps to that address next. Sweet!

I won’t write up a whole analysis on the rest of the code the 6502 runs here, or why it wasn’t printing anything to the display, as I think it’s a bit off-topic for the rest of this post, but I will say that this tool has proven invaluable for debugging this little breadboard computer!

As always, this code is open source if you want to give it a spin yourself, you can check it out here! I’ve done my best to write a setup guide as well with the wiring details. I’d love some feedback!