The importance of pancakes
An emulator inside an emulator inside an emulator
A few weeks ago I was having lunch with some friends. Lunch was delicious - pancakes with marmalade and maple syrup. But even more, err, delicious was learning more about what Piers had been up to with his OneROM EPROM emulator.
EPROMS - erasable programmable read only memories - used to make the world go round. Before the invention of flash chips they were how firmware - the code that gets computers up and running - was shipped. They were the staple of hardware in the 1970s, 1980s and early 1990s.
The early ones were erased using UV light - there’s a little quartz window that lets you see the silicon chip. As a five year old I thought this was incredibly cool.
These days EPROMs have long been replaced by flash memories. But they live on in legacy industrial equipment - CNC machines, industrial controllers. Kit that can last decades without needing replaced. Except that when an EPROM fails, it can’t be replaced - no one makes them anymore. So kit worth hundreds of thousands of dollars is rendered worthless due to the failure of a small part.
Which is why Piers’ OneROM project is so interesting - a modern replacement for a legacy part. Easily programmable. Using a Raspberry Pi Pico - a RP2350 on a custom board. Very neat. I knew Piers had hit the big time when the OneROM appeared on Adrian’s Digital Basement.
A daft idea takes shape
But as we talked my mind began to wander. I’ve got a 386/DOS emulator. Could I build a RP2350 emulator, run the OneROM firmware, program a OneROM with a PC BIOS image, integrate the whole lot with my emulator and boot from that?
I suspect describing this raises questions about my sanity. But let’s ignore that for now and focus on the far more important question: how could I get this working?
Several big pieces came to mind:
First I needed to add support to the emulator for booting from BIOS images. It’s a high-level emulator at the core - when it starts, DOS is already "booted", so it doesn’t go through the typical BIOS startup flow of enumerating devices, configuring hardware, booting from a hard-disk etc. It’s just instantly on. This is a biggish change. Oh, and I need to find a BIOS image.
Then we needed a RP2350 emulator. This was the really big bit. The RP2350 is complex and fast (it runs at 150MHz). Could we even emulate it at anywhere near a reasonable speed on a PC?
Then we’d need to build a OneROM image with the BIOS in place.
And then integrate the whole lot into the emulator.
I got back from lunch around 7.30pm. And got underway.
Oracles
These days, building follows an increasingly familiar pattern. First up: work out the oracles. The RP2350 has dual ARM (or RISC-V) cores, plus a smorgasbord of peripherals. QEMU has great support for the ARM (and RISC-V cores) - so it made an obvious first oracle. Another oracle was the official data sheet.
But an even better oracle is the real hardware - here is a OneROM (the red board) connected to a RP2040 debug probe. This was to be the first of several OneROMs who’d do sterling service running fuzzing through many nights.
Finally I had a project oracle. I wanted to be able to successfully run Piers’ OneROM in the emulator.
Architecture
With the oracles in place it’s time to think about the architecture. This isn’t very fancy - just a rough idea of the key components, how they map to crates and how they are phased. In the past I’d have written a Word doc. These days I use a post-it note (I was on the bus). Which Claude tidied up and turned into a phase table for me.
The RP2350 is an interesting chip - it has dual Arm cores. And dual RISC-V cores. But you can only choose to use one pair at a time. For now RISC-V cores were out of scope.
And then we were onto building…
Building
The build loop is the key bit. The goal is to give Claude (or Codex) a framework to enable it to work autonomously. Get this right and it’ll run for hours. As I’m writing this I’ve got a Codex session that has been debugging a Windows app problem for ~10 hours.
Design matters. The oracles are the external quality check, but the design is all about getting the innards directionally correct. These days I no longer care how the functions are named or split. I don’t care what algorithms are used. I don’t care about whether Claude uses a vec or an array or whatever. But I do care that I can draw the high-level block diagram. I need to be able to reason about the code from the outside. I need to know there is compartmentalisation. Defined APIs.
We started with the ARM M33 core, with differential testing against QEMU. QEMU is functionally correct. But not, it turns out, cycle accurate for the RP2350 (i.e. correctly report how many cycles each instruction took). So the hardware oracle joined in to provide that information.
And as we went we added UTs. These days I view UTs as a kind of "lock". Build the code, get it to work (via the oracles) and then lock in the function with UTs. They are your watchers, guards if you like, spotting when refactors or changes break the function.
Over the next two weeks we iterated in the background. We added multi-thread support (run each core, PIO & DMA on separate cores). I learnt a lot about the trade-offs involved in multi-threaded emulation (spoilers multi-threading is hard to exploit - as I discovered it’s trivial to create something slower and more complex). I discovered how long it takes Windows to wake up a sleeping thread. We eventually managed to get ~150MHz performance - but only for the right workloads. Emulating a speedy microcontroller on a PC is hard. There’s a whole set of posts here (albeit for a niche audience).
Did it work? You betcha! Here’s the emulator booting using SeaBIOS (an open source BIOS), with the BIOS bytes all served from a OneROM running on a RP2350 emulator. It’s ~20x slower than real-time, but still… It’s quite bonkers. And cool :).
If you want to play with the emulator it’s here. And also as a Rust crate here.
The RP2040 sidequest
Before the RP2350 there was the RP2040. I’ve used a lot of those in retro computing projects over the years - BlueSCSI, PicoGUS. Which got me thinking. If we can emulate the RP2350, surely we can emulate the RP2040 too? So we did. And then we went further and emulated the whole of the PicoGUS - an ISA add in card with RAM, and a D2A converter. The input to the PicoGUS is via the ISA bus. So we hacked Dosbox-x to generate the necessary ISA bus trace of the audio from the startup of Monkey Island. And then fed that into our emulated PicoGUS. And here’s the result:
It took ~20 minutes to generate this clip - so ~20x slower than realtime. But still. This is an emulated RP2040 running an emulation of a Yamaha YMF262 FM sound generation chip. That’s kinda amazing to me.
And so?
Little more than 3 weeks after that lunch, I present to the world a cycle accurate, albeit slow, RP2350 emulator. You can integrate it with your PC emulator to boot a 1990s OS from a BIOS image served by emulated firmware running on an emulated microcontroller. Turtles anyone?
This shouldn’t be possible. Six months ago it wasn’t. But here we are. I don’t have deep ARM expertise. This is my first time using an RP2350. I’m not an expert in PC BIOS. Or Rust. Or really anything. Without Claude I couldn’t have done this. But, equally, I’d like to think (believe?) that I’m a critical part of the process. Claude can’t (yet) do this on it’s own. It needs the framework, the guidance, the focus on quality, the steering. Plus I’ve learnt a lot. And had a lot of fun.
My conclusion? I need to have more pancakes :).



