definitely-not-esp32 MOC
The project hub for definitely-not-esp32: a from-scratch RV32IMC system on a chip and a Rust microkernel to run on it — developed in Verilator simulation first, ported to a Tang Nano 20K FPGA, and benchmarked honestly against a real ESP32-C3. The name is the thesis: not a clone, not a toy, and not pretending the result will beat a shipping part. This MOC is the project view — what is being built, in what order, and what each stage forces you to understand. The concept view lives in Computer Architecture MOC, which holds 35 leaf notes on the ISA, pipeline, traps, memory protection, SoC fabric, and kernel primitives. The RTL, the kernel source, the spec, and the blog series live in the project’s own repository, outside this vault.
The organizing idea is the same one behind Building a Neural Network in C MOC, and the two projects are deliberate mirrors of each other. That one builds upward from a single neuron to a transformer, in C, refusing to hide behind a framework. This one builds downward from a running program to the gates that execute it, in Verilog and Rust, refusing to hide behind a CPU someone else designed. Between them they meet in the middle — at the instruction, the cache line, and the trap.
Build status — the roadmap in this MOC is a scaffold, not a record uncertain
This MOC was assembled from what the vault already contains, not from the project repository. Computer Architecture MOC states that the spec, roadmap, RTL and kernel live outside the vault, and its open threads reference a “Phase 5” without enumerating phases 1–4 anywhere that is readable from here.
So the stage breakdown below is reconstructed from the concept notes that exist and the dependencies between them — it is a plausible ordering, not a report of what has been done. Verify: the real phase numbering, which phases are complete, and what is currently in flight. To resolve: reconcile against the project repo’s roadmap and replace this callout with a dated status table. Until then, treat every “stage” heading here as a grouping of concepts, not a claim about progress.
The Two Views
flowchart LR subgraph PROJ["definitely-not-esp32 MOC — the project"] WHAT["what gets built<br/>in what order<br/>and why"] end subgraph CONC["Computer Architecture MOC — the concepts"] REF["35 leaf notes<br/>ISA · pipeline · traps<br/>PMP · fabric · kernel"] end subgraph REPO["the repository — outside this vault"] RTL["spec · RTL · kernel<br/>testbenches · blog"] end WHAT -->|"needs to understand"| REF WHAT -->|"is implemented in"| RTL REF -.->|"concept reference for"| RTL
How the three pieces divide. What it shows: this MOC answers “what am I building and what must I know to build it”; Computer Architecture MOC answers “how does this concept work”; the repo holds the artifact. The insight to take: the split exists so the concept notes stay reusable. Pipeline Hazards and Physical Memory Protection are true of any RV32 core, not just this one — keeping them out of the project MOC means they remain useful to a reader who never builds an SoC, and it keeps this hub short enough to actually navigate.
The System Being Built
flowchart TB subgraph SW["Software"] USER["User processes — U-mode"] KERN["Rust microkernel — M-mode"] USER -->|"ecall"| KERN end subgraph CORE["RV32IMC core"] PIPE["5-stage pipeline<br/>IF · ID · EX · MEM · WB"] HAZ["hazard unit<br/>forward · stall · flush"] BP["2-bit branch predictor"] CSR["CSRs · trap entry"] PMP["PMP"] end subgraph FAB["SoC fabric"] BUS{"Wishbone bus"} ROM["boot ROM"] RAM["RAM"] UART["UART"] CLINT["CLINT — timer, sw IRQ"] PLIC["PLIC — external IRQ"] end KERN -.->|"runs on"| CORE CORE --> BUS BUS <--> ROM BUS <--> RAM BUS <--> UART BUS <--> CLINT BUS <--> PLIC CLINT -->|"mtip"| CSR PLIC -->|"meip"| CSR
The target system. What it shows: a minimal but genuinely complete SoC — a pipelined core with hazard handling and branch prediction, memory protection, the two standard RISC-V interrupt controllers, and enough fabric to boot and print. The insight to take: the two dashed-to-solid interrupt paths at the bottom are where hardware becomes software. CLINT raises mtip and PLIC raises meip; the CSR block turns both into a trap; the microkernel’s handler turns that into a scheduling decision. Everything else on this diagram exists to make those two arrows work.
How to Build This
- Every rung is a program, and every rung has a known answer. Do not move on because the RTL elaborates — move on because a test you can check by hand passes. A core that runs your own test suite and nothing else is a core that is wrong in a way you have not noticed yet.
- Simulate before you synthesize. Verilator gives you
printf, waveforms, and a two-second edit loop. The FPGA gives you truth and almost no visibility. Every design decision should be made in the simulator and only confirmed on hardware. - The compressed extension is the last thing you add, not the first.
Cdoubles your decoder’s complexity for zero new capability. Build RV32I, pass the tests, then addM, thenC. - The spine is Stage 2 → 5 → 8: decode one instruction, boot and print, run a kernel. Stages 4, 7 and 9 are where the project earns the word engineering — tests, protection, and measurement — but the spine is what makes it a computer.
Stage 0 — The Tools You Need Before Any RTL
You cannot write Stage 2 without these. The abstraction you are writing in, the simulator you will live in, the compiler that produces something to run, and the ability to see a signal change — these are prerequisites, not a later optimization step.
The program: a Verilator testbench around a module that does nothing but count, dumping a VCD you open in a waveform viewer. Separately, compile int main(){return 42;} with the RISC-V cross-compiler and disassemble it. You now have both halves of every future debug session: a way to see the hardware, and a way to produce input for it.
- Register-Transfer Level — the abstraction the design is written in
- Verilator — RTL to C++ simulation; the fastest edit-test loop you will have
- Field-Programmable Gate Array — what actually happens to your Verilog
- The RISC-V Cross-Compilation Toolchain —
riscv32-unknown-elf-gcc,objdump, and why the triple matters - Testbenches and RTL Verification — driving a module and checking it, before there is a CPU to check
- Waveform Debugging — VCD/FST, and reading a signal trace as the primary debugging skill
Stage 1 — Decide What Machine You Are Building
Which ISA, which extensions, which privilege modes. RV32IMC is base integer plus multiply/divide plus compressed — the compressed extension is not decoration, it is what makes code fit in an FPGA’s block RAM. Zicsr is required the moment you want traps at all.
The program: none — this rung is reading and one decision, written down. But hand-decode one 32-bit instruction from a disassembly listing into its fields before you leave, because Stage 2 is that operation in Verilog.
- Instruction Set Architecture — what an ISA is, and the contract it fixes
- RISC-V Instruction Set Architecture — the base and its design choices
- RV32IMC — the exact target: base integer, M, C
- RISC-V Instruction Formats — R, I, S, B, U, J, and why the immediate bits are scrambled the way they are
- Zicsr Extension — control and status register access, the prerequisite for traps
- RISC-V Privilege Modes — M / S / U, and why a microkernel can start with M and U only
Stage 2 — Decode and Execute One Instruction
The first RTL that is recognisably a processor. A single-cycle core is not a stepping stone you throw away — it is the reference model you will compare the pipelined version against for the rest of the project.
The program: a single-cycle RV32I core that executes addi x1, x0, 42 and halts. Then grow the decoder one instruction at a time. The known answer is the register file: you can read x1 out of the simulator and check it by hand.
- Instruction Decode — turning 32 bits into control signals
- The Register File — 32 registers, two read ports, one write port, and why
x0is special - The Arithmetic Logic Unit — the operations the ISA actually requires
- Datapath and Control — the split that organizes every processor design
- The Single-Cycle Processor — correct, slow, and the reference model for everything after
Stage 3 — Make It Overlap
The classic five-stage pipeline, and the three things that go wrong in it. Every hazard mechanism is a direct consequence of overlapping instructions that were written as if they ran one at a time.
The program: pipeline the Stage 2 core. Then write the tests that break it — back-to-back dependent adds, a load followed immediately by a use, a taken branch after an arithmetic instruction. Each one should fail before you build the corresponding mechanism and pass after.
- Classic Five-Stage Pipeline — IF, ID, EX, MEM, WB
- Pipeline Hazards — structural, data, control
- Operand Forwarding — resolving data hazards without stalling
- Load-Use Hazard — the one case forwarding cannot fix, and the mandatory bubble
Stage 4 — Prove It Is Actually a RISC-V
The rung that separates a project from a demo. Your own tests encode your own misunderstandings; the official suite does not. Expect to fail it, and expect the failures to be in the instructions you were most confident about.
The program: build and run riscv-tests rv32ui-p-* against your core in Verilator, and do not proceed until every one passes. Then rv32um for the M extension, then rv32uc.
- The riscv-tests Suite — the official test programs, how they signal pass/fail, and how to run them under Verilator
- Cycles Per Instruction — the headline number, and what a bad CPI implicates
Stage 5 — Make It a System, and Make It Print
A core that executes but cannot talk to anything is untestable on hardware. The bus and the first peripheral come before the trap machinery for a practical reason: until the UART works, you have no way to see what a trap handler did.
The program: boot from a ROM at the reset vector, run code linked to your memory map, and print hello over the UART — first to the Verilator testbench’s stdout, then to a real serial cable from the FPGA. This is the moment the project stops being a simulation.
- System on a Chip — what makes a core a system
- Wishbone Bus — the fabric everything hangs off
- The SoC Memory Map — where ROM, RAM and each peripheral live, and why the decoder is just address bits
- Boot ROM and the Reset Vector — where the very first instruction comes from
- Linker Scripts and Memory Layout — placing
.text,.data,.bssand the stack into a map with no operating system - Universal Asynchronous Receiver-Transmitter — the first peripheral, and your only debugger for a while
Stage 6 — Traps, Interrupts, and the Kernel Boundary
Where the hardware starts talking to software. The trap machinery and the two interrupt controllers are the whole interface, and ecall is the doorway the kernel exposes upward.
The program: take an illegal-instruction trap and print mcause from the handler. Then make the CLINT’s timer fire and blink an LED from the interrupt handler. Then call ecall from a U-mode program and return cleanly. Those three are the entire kernel/hardware contract.
- Control and Status Registers —
mtvec,mepc,mcause,mstatus - RISC-V Trap Handling — what the hardware does on a trap, step by step
- ecall Instruction — the system-call primitive
- Core Local Interruptor — timer and software interrupts (
mtip,msip) - Platform-Level Interrupt Controller — external interrupt routing (
meip) - RISC-V Trap Delegation —
medeleg/mideleg, for when S-mode arrives - Supervisor Binary Interface — the SBI contract
Stage 7 — Protect Memory
A microkernel without memory protection is a monolith with extra steps. PMP is the cheap answer and is often enough for an M/U-mode system; a full MMU with Sv32 is the expensive one that buys real virtual memory. This follows Stage 6 because PMP is configured entirely through CSRs — pmpcfg* and pmpaddr* are CSRs, so the trap machinery has to exist first.
The program: configure a PMP region covering a U-mode task’s memory, have the task read one byte past it, and catch the access fault in your handler. A protection mechanism you have not seen fault is a protection mechanism you have not tested.
- Physical Memory Protection — region-based protection with no page tables
- Memory Management Unit — address translation in hardware
- Sv32 Virtual Memory — the RV32 page-table format
- Page Table Entry — the PTE bits and what hardware checks
- Translation Lookaside Buffer — why translation needs a cache to be viable
Stage 8 — The Microkernel
Rust in M-mode. The kernel is small on purpose: schedule, handle traps, pass messages. Everything before this rung existed so that this rung could be short.
The program: two tasks that alternate, each printing its own name — which requires a real context switch, not a loop. Then replace the alternation with a rendezvous: task A blocks sending, task B receives, both proceed. If the IPC round trip works, the microkernel works.
- Bare-Metal Rust —
no_std, the panic handler,#[no_mangle], and linking Rust into a bare SoC - Context Switching in a Microkernel — saving and restoring 31 registers plus
mepc, and where the trap frame lives - Microkernel — the architecture and what stays outside the kernel
- Round-Robin Scheduling — the simplest scheduler that is honestly a scheduler
- Synchronous IPC — rendezvous message passing, the microkernel’s core service
Stage 9 — Make It Fast, and Measure That It Is
Branch prediction is the first genuine microarchitecture decision — a two-bit saturating counter is a handful of gates and buys most of the available win. It comes after the kernel boots on purpose: an optimization you cannot measure end to end is a guess, and until Stage 8 runs there is nothing representative to measure. On the FPGA, the other half of “fast” is the clock you can actually close timing at.
The program: measure CPI on a fixed workload, add the predictor, measure again, and report both numbers with the workload named. Then read the synthesis report’s critical path and find out what is really limiting your Fmax — it is rarely what you assumed.
- Branch Prediction — why a taken branch costs a flush, and how prediction hides it
- Two-Bit Saturating Counter — the standard cheap predictor and its hysteresis
- Timing Closure and Fmax — critical path, slack, and why the synthesis report is a design document
Stage 10 — Benchmark Honestly
The project’s stated discipline, and the reason the name is what it is. An ESP32-C3 is a shipping RV32IMC part with a mature toolchain; a from-scratch core on a Tang Nano 20K will lose on clock speed, area, and peripheral count. Saying so precisely is the interesting result. What the comparison legitimately shows is CPI, trap latency, IPC round-trip, and interrupt response — the numbers a design choice actually moves.
The program: the comparison table itself, with the workload, the clock, and the toolchain flags stated for both sides. A benchmark whose conditions are not written down is an anecdote.
- ESP32-C3 — the reference part, RV32IMC, and what it ships with
- Tang Nano 20K — the FPGA target and its real constraints
Not Yet — Gated on Project Phase
These are deliberate ghost links, not a backlog. Computer Architecture MOC’s open threads gate them explicitly, and writing them before the corresponding hardware exists would document a design that has not been made.
- Stretch-goal concepts, once the project reaches that phase: Direct-Mapped Cache, Set-Associative Cache, Asynchronous IPC, Priority Preemptive Scheduling, Sv39 Virtual Memory
- SoC implementation notes, once Phase 5 lands: Wishbone Pipelined Mode, Bus Arbitration
A ready-made seam with the other project
Direct-Mapped Cache and Set-Associative Cache are gated here — and Cache Blocking and Loop Tiling in Building a Neural Network in C MOC already contains measured cache behaviour on real hardware (L1d 48 KiB/12-way/64 sets, L2 1 MiB/16-way, and a 47× spread from loop order alone at N=2048, collapsing to 1.6× when the working set fits L1). When the SoC gets a cache, those two notes should cite each other: one explains the structure you are building, the other shows what software does to it.
Cross-Cutting Themes
- This is the bottom of the vault’s stack. Linux MOC starts at the kernel/hardware boundary and looks up; this project starts below it and looks down. Linux System Call Interface MOC and Stage 6 here describe the same
ecallfrom opposite sides. - Two from-scratch projects, opposite directions. Building a Neural Network in C MOC refuses frameworks; this refuses vendor silicon. Both bet that the abstraction you built yourself is the one you actually understand.
- The simulator is the product, early on — which is why the tooling is Stage 0, not a mid-project concern. Verilator gives you a testbench, waveforms, and a debug loop measured in seconds. The FPGA gives you truth and almost no visibility. Most of the design work happens in the first and is only confirmed by the second.
- Traps are the whole interface. Stages 6, 7, and 8 are one story: hardware detects something, CSRs record it, the kernel decides. Control and Status Registers is the hinge note.
- Honest benchmarking is a design constraint, not a postscript. Committing up front to measuring against a real part is what stops the project drifting into a simulator that only runs its own tests.
Open Threads
- Rebuilt as a buildable ladder (2026-09-04). Each of the eleven stages now names the program you write and the hand-checkable answer that tells you it works, matching Building a Neural Network in C MOC and Games and Strategic Systems in C MOC. Sixteen missing implementation steps were added as ghost links — the MOC previously held 35 concept notes and not one step that turns a concept into RTL: no instruction formats, no register file or ALU, no single-cycle reference core, no cross-compiler, no
riscv-tests, no linker script or reset vector, no bare-metal Rust, no context switch. You could not get from “choose RV32IMC” to “five-stage pipeline” using only what was here. - Stages reordered into build order (2026-09-04). The previous ordering had three dependency inversions: the RTL/Verilator/FPGA tooling sat at Stage 3 although Stage 2 cannot be written without it (now Stage 0); Physical Memory Protection preceded Control and Status Registers although
pmpcfg*/pmpaddr*are CSRs (protection now follows traps); and the SoC fabric with Universal Asynchronous Receiver-Transmitter was bundled with the trap machinery although the UART is the only way to observe a trap handler (now its own Stage 3, before traps). Branch Prediction also moved after the microkernel, since an optimization is only measurable once something representative runs.Cycles Per Instructionwas listed twice and is now cited once, in Stage 7. No notes were added or removed — this is purely an ordering change. - Replace the reconstructed roadmap with the real one. The stage breakdown above is inferred from concept dependencies, not read from the project repo. Pull the actual phase list and completion status, then convert the build-status callout into a dated table. This is the single most valuable edit to this MOC.
- Record the benchmark results in the vault when they exist. CPI, trap latency, IPC round-trip and interrupt response against the ESP32-C3 belong here as a table, and are the project’s headline output. Nothing in the vault currently holds measured numbers from this project.
- Decide whether the repo gets a note.
definitely-not-esp32was previously a ghost link pointing at an external repository. It now resolves to this MOC. If the repo’s spec or blog series is worth summarizing in-vault, that is a separate leaf note, not this hub. - Alias cleanup done 2026-08-22: Computer Architecture MOC previously carried
definitely-not-esp32 MOCas an alias, which would now collide with this note’s title. Removed there; that MOC keepsComputer Architecture,RISC-V MOC, andCPU Microarchitecture MOC. -
#uncertainon every stage heading. See the build-status callout — the groupings are concept clusters, not verified project phases.
See Also
- Computer Architecture MOC — the concept companion: 35 leaf notes, all of it reusable beyond this project
- Building a Neural Network in C MOC — the mirror project, building upward instead of down
- Linux MOC — where this stack ends and a production kernel begins
- Linux System Call Interface MOC — the same
ecallboundary, from the software side - Concurrency and Parallelism MOC — the theory behind Stage 8’s scheduler and IPC
- Home — vault index