A Minimal Operating System Kernel with Runtime Instruction Translation for Low-Cost RISC-V Microcontrollers · part 8 of 8
Restructuring (Again): From Side Project to Dissertation
The JIT is growing up. Here's how I'm restructuring the whole project around my university dissertation, and what to expect over the next few months.
Keith Gangarahwe 10 min readSo… it’s happening again. If you’ve been following along, you’ll know this project has already had a couple of fresh starts. First there was the JIT on my laptop, which gave me a false sense of performance. Then came Zig running on the ESP32-P4 through ESP-IDF. This time, though, the restart has a much better reason behind it: this project is becoming my university dissertation.
My proposal for the BSc Honours in Computer Science gives it a new (and admittedly long) title:
A Minimal Operating System Kernel with Runtime Instruction Translation for Low-Cost RISC-V Microcontrollers
It’s a mouthful, I know. But it describes exactly what I’m building, and it’s the name every post in this series now lives under. The earlier posts are still part of the story; this one is just the point where the plot twists.
Why I’m Doing This
Before the technical bits, a quick word on the why, because the title doesn’t really say it.
What I like. The kind of question I enjoy working on, the one I keep coming back to, is this:
How do we make machines and the software running on them behave predictably when we control the system all the way down to the architectural boundary?
Looking back, that’s what this series has quietly been about from the start. Learning the ARM7TDMI register layout, stepping through GDB one instruction at a time, wrestling the build system until Zig ran on bare hardware: all of it was about knowing exactly what the machine is doing, and why.
What got me interested in this project. Somewhere along the way, I became interested in what happens when systems techniques normally designed for relatively capable machines are forced onto a constrained processor where the assumptions underneath them disappear. Process isolation, binary translation, scheduling: all of them were worked out on computers with virtual memory, megabytes of RAM and cores to spare. Take those away, and what’s left? Does the technique still hold up? What does it cost? What has to change?
This project is where those two meet, and that’s the lens for everything below.
What Changed?
The original idea was simple: run ARM7TDMI code on RISC-V. That’s still very much in there. But writing the proposal forced me to ask a harder question: what does the translated program actually run on?
Up to now, the answer was ESP-IDF, and with it Espressif’s FreeRTOS. That works, but in a system like FreeRTOS the application code, the driver code and the protocol handling all share a single trust domain. And a translated binary from a vendor that might not even exist anymore is exactly the kind of code you don’t want having unrestricted access to the whole machine.
So the project is now two things stacked on top of each other:
- A microkernel. A tiny kernel that keeps only task scheduling, memory protection and inter-process communication in privileged mode. Everything else (device drivers, the file system, the display) runs as an ordinary, isolated program.
- A dynamic binary translator that runs under that kernel as just another isolated task, turning ARM7TDMI machine code into RISC-V as it executes.
And yes, that means ESP-IDF is out. The manufacturer’s RTOS won’t be in the image at all. The only thing I’m keeping from Espressif is the second-stage bootloader; everything after it, starting from the very first assembly instruction, is ours. The build pipeline from the last post taught me a lot, but it’s about to be replaced by something much leaner.
Why This Is Actually Interesting
Here’s the part that got me excited while writing the proposal. The ESP32-P4 (and its cheaper cousin, the ESP32-C3) has no memory management unit. There’s no virtual memory giving every program its own private view of memory. Instead there’s Physical Memory Protection, or PMP: a small, fixed set of registers, each describing one range of addresses and what’s allowed there. The C3 has 16 of them, the P4 has 32, and the kernel has to rewrite them every single time it switches from one task to another.
Now look at what the existing research assumes:
- Microkernel research (Liedtke’s work, the L4 family, seL4) assumes an MMU, where adding another isolation boundary is cheap.
- Microcontroller isolation work (Tock, ACES, EPOXY) gets by without an MMU, but doesn’t build a message-passing microkernel and doesn’t run foreign machine code.
- Binary translators (QEMU and friends) assume a host operating system, virtual memory and megabytes of RAM. The more recent ones hide translation delays by pushing the work onto spare CPU cores.
Nobody has built something where all three constraints apply at once. That’s the gap, and that’s what the dissertation is really about. Not just can it work, but what does it cost when it does?
One thing I want to be clear about up front, because an earlier draft of the proposal got misread on exactly this point: I am not building a tool for debugging solar inverters. No commercial device gets opened, probed or monitored. The motivation comes from equipment like solar charge controllers, prepaid electricity meters and borehole telemetry units, devices whose firmware exists only as a compiled binary for a processor that may not even be manufactured anymore. When that firmware misbehaves, working hardware gets thrown away. The actual work, though, is pure systems software, measured on a development board.
The New Architecture
Here’s the rough shape of the system:
┌────────────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐
│ ARM7TDMI guest │ │ FAT32 fs │ │ block │ │ display │
│ (translated) │ │ service │ │ service │ │ service │
└────────────────┘ └────────────┘ └─────────┘ └─────────┘
↕ messages pass through the kernel ↕
┌───────────────────────────────────────────────────────┐
│ kernel: traps · scheduler · IPC · PMP switching │
└───────────────────────────────────────────────────────┘
ESP32-P4 · RISC-V · no MMU · 32 PMP entriesEvery box on the top row is an isolated task with its own PMP regions. If one of them does something stupid, the hardware raises a fault, the kernel stops that task and reports it, and everything else keeps running.
Booting
On reset, the processor starts in its most privileged mode with no stack and no initialised memory. Our own startup code, written in assembly, sets up a stack, clears the uninitialised data section, disables the four hardware watchdog timers that would otherwise reset the chip, installs the kernel’s trap handler and jumps into Zig.
Traps, Scheduling and Isolation
- Traps and system calls: the trap handler saves the interrupted task’s full register state into its control block, works out the cause and dispatches it. A task that faults gets stopped and reported instead of taking the whole system down with it.
- Scheduling: preemptive and priority-based. A hardware timer interrupts at a fixed interval, and a bitmap lets the scheduler pick the highest-priority ready task in constant time.
- Isolation: each task gets a region for its code, one for its data and stack, and one for any buffer it shares. Those get written into the PMP registers on every switch. The catch: PMP regions must be a power of two in size and aligned to their own size, so memory gets lost to padding. How much? That’s one of the things I’ll be measuring.
Message Passing and Services
Tasks never call each other directly. Short messages travel in registers and never touch memory; bigger transfers go through a shared buffer, so data is never copied. A task can also poke another one without blocking by setting a bit in its notification word.
On top of that sit three services, all running as ordinary isolated tasks:
- a block service that owns the storage hardware and does exactly one thing: read a numbered block,
- a read-only FAT32 file system service that knows nothing about the hardware and just asks the block service for blocks,
- and a text-mode display service that can render either over a serial terminal or into a screen buffer.
The Translator (Finally!)
This is where the earlier ARM posts come back into play. An ARM7TDMI program is loaded from the SD card into a protected region and started as its own task. Then:
- Interpret first. Every ARM instruction is decoded and carried out by equivalent RISC-V code. Correctness gets checked by running the same program on an independent reference simulator and comparing the CPU state after every instruction.
- Then translate. Short sequences of ARM instructions are translated into RISC-V and written into a fixed-size code cache, so hot code runs natively instead of being interpreted again.
- Chain blocks. When one block reliably jumps to another, its exit is patched into a direct jump, skipping the trip back to the translator.
- Be lazy with flags. RISC-V doesn’t have ARM’s condition flags, so the translated code only records what it needs and computes the flags when something actually tests them.
- Flush, don’t fiddle. When the cache fills up, the whole thing gets thrown away and translation starts again. Surprisingly, that’s cheaper than tracking and evicting individual blocks.
The Part I’m Most Curious About
Translation takes time, and while it’s happening, the guest program isn’t running. Existing systems hide that by translating on a second CPU core. I’m deliberately using one core, so instead translation gets chopped into small units of work, each with a fixed budget, and one unit runs every time the kernel already has control anyway: a message send, a system call, a timer interrupt. In the literature this is called an anytime algorithm: it can be paused at any point and still leave you with something useful. The upshot is that the translator can never hold the processor for an unbounded amount of time, which, if you remember the question at the top of this post, is exactly the kind of predictability I’m chasing.
Does that actually reduce the longest pause the guest program experiences? And what does it cost in total speed? Honestly, I don’t know yet. That’s the fun part.
What I’ll Be Measuring
The kernel measures itself, using the processor’s cycle counters and a sampling profiler that lives in the timer interrupt. The big questions:
- How many cycles does it cost to switch tasks, send a message and rewrite the PMP registers?
- How does that cost grow as the PMP budget changes between 8, 12, 16, 24 and 32 regions? Is there a point where splitting the system into more isolated services costs more than it gives back?
- How much memory is lost to power-of-two alignment, and is alignment or register count the real limit?
- How does this compare against a FreeRTOS baseline on the exact same hardware? FreeRTOS will almost certainly be faster, and the point is to measure the price of isolation rather than pretend it’s free.
- How do you reconcile “no memory is ever writable and executable at the same time” with a translator that needs to patch its own generated code?
And whatever the numbers are, they get reported as they come out, including the ones that don’t go my way.
The Timeline
The assessed part of the dissertation runs for one semester, from March to June 2027, which is about 16 weeks. Everything between now and then is preparation: reading the literature, studying the RISC-V privileged architecture, setting up the build and debugging environment, and bringing up the board. None of that is formally assessed, but it’s what makes a four-month schedule realistic, and it’s exactly what the next few posts will cover.
| Milestone | Target |
|---|---|
| Kernel running multiple tasks on the ESP32-P4 | 31 March 2027 |
| Reading a file through the block and FAT32 services | 30 April 2027 |
| ARM7TDMI interpreter and translator, verified against a reference | 31 May 2027 |
| Translation scheduling experiment | 15 June 2027 |
| Open-source release and dissertation submitted | 30 June 2027 |
The order is deliberate. The kernel with its isolation, services and measurements is a complete project on its own. The interpreter is stage two and the translator is stage three. If the translator runs late, it gets reported as partially demonstrated, and everything before it still stands.
Just as important is what’s out of scope: running across both P4 cores (apart from one optional experiment), ARM’s Thumb instruction set, coprocessors, writing to storage, graphics beyond a text display, and on-chip Wi-Fi. That last one is because Espressif’s Wi-Fi stack ships as a closed binary that depends on their RTOS, which is exactly what I’m getting rid of. If networking is needed, a second controller handles it over serial.
What This Means for the Blog
- Posts will follow the build: boot code, traps, the scheduler, message passing, PMP isolation, the services, and then the interpreter and translator.
- Everything lives in one series. The earlier posts on ARM7TDMI, debugging with GDB and QEMU, and cross-compiling with Zig aren’t wasted; they’re the groundwork, and they stay right where they are on the project page.
- No proprietary firmware, ever. Every test program is either compiled from source by me for the ARM7TDMI (much like the inline assembly setup from the early posts) or comes from an openly licensed test suite.
- It’s all going open source under a permissive licence, with documentation.
For the hardware nerds: the main board is the ESP32-P4 dev board I already have (with a display and SD card slot), a couple of ESP32-C3 boards join in to prove the hardware-specific code is properly separated, and Espressif’s emulator, which even emulates the PMP registers, will save me from bricking the P4 too often.
Next up: booting the P4 with nothing but our own startup code. No ESP-IDF, no safety net. What could possibly go wrong?