Article
Teaching AI to write verified firmware.
How one low-level engineer built a multi-agent pipeline to make LLMs generate correct, bare-metal code for a real microcontroller.
1. The Provocation
I remember the first time I watched a large language model generate a flawless quicksort in C++. It was elegant, idiomatic, and instant. It also caused a cold twist in my stomach. If a model can do that, what's left for those of us who spent years learning the dark arts? Which registers are banked, which cache line gets evicted, and why a volatile qualifier can mean the difference between a blinking LED and a silent bus fault?
The conventional answer is: not much. AI is supposedly making low-level expertise obsolete. But I decided to test that assumption in the most direct way I could. I asked a state-of-the-art model to blink an LED on a bare-metal ESP32. What came back was beautiful, confident C code; and it was completely wrong.
It wrote to memory addresses that didn't exist on that chip. It forgot to configure the IO MUX. It skipped the peripheral clock gate entirely. Its delay loop was so naïve the compiler would have optimized it into nothing. The model didn't fail because it was stupid. It failed because text prediction has no causal model of silicon.
That moment reframed everything.
The real question wasn't "Am I redundant?" It was "Who builds the layer that makes AI-generated code trustworthy when it touches hardware?" The answer, I suspected, was someone who can read assembly, understands memory-mapped I/O, and has spent years debugging the exact failure modes the model blissfully ignores. Someone like a low-level programmer.
This is the story of Project Babbage - a deliberately small, deliberately unfashionable experiment in the service of a very fashionable idea: using AI to generate code. But not just any code. Firmware that must actually run on a real microcontroller. Firmware that must be *correct*, because in the physical world, wrong code doesn't just throw an exception. It drives a pin high when it should be low, corrupts a peripheral state, or worse.
The broader idea falls into the sphere of bare-metal vibecoding. Using natural language to conjure code that touches registers, clocks, and pins. It sounds reckless and with current models, it is. So the experiment became a pipeline: a closed loop of generation, auditing, correction, and verification, designed to answer a single question.
Can we make vibe coding safe at the edge?
That question turned out to be the best career advice I never received.
2. Why AI Alone Fails at the Metal
To understand why a pipeline like Project Babbage is necessary, you need to see what a modern LLM actually produces when you hand it a bare-metal task. The request is simple: "Write C code for an ESP32 that blinks an LED on GPIO5 once per second, using only registers." This is not a trick question. It is the embedded equivalent of "Hello, World." The model responds with confidence, often generating code that looks plausible at first glance.

(The following code was generated.)
#include <stdint.h>
/* ESP32 GPIO peripheral base */
#define GPIO_BASE 0x3FF44000UL
#define GPIO_OUT_REG (*(volatile uint32_t *)(GPIO_BASE + 0x04))
#define GPIO_ENABLE_REG (*(volatile uint32_t *)(GPIO_BASE + 0x20))
#define GPIO5 (1U << 5)
static void delay(volatile uint32_t count)
{
while (count--)
__asm__ volatile ("nop");
}
void app_main(void)
{
/* Configure GPIO5 as an output */
GPIO_ENABLE_REG |= GPIO5;
while (1) {
/* LED ON */
GPIO_OUT_REG |= GPIO5;
delay(1000000);
/* LED OFF */
GPIO_OUT_REG &= ~GPIO5;
delay(1000000);
}
}
But when you try to run that code, you discover that the model has failed in at least four distinct and predictable ways.
1. Wrong register addresses for the target silicon.
In this case the LLM got it right, but the model may mix up the ESP32 and the ESP32-C3. The ESP32-C3 places its GPIO registers at a base address of 0x60004000. The original ESP32 uses 0x3FF44000. A write to the wrong base address does not throw a Python exception. It either triggers a silent bus fault or writes to unmapped memory, leaving the LED dark and the developer baffled.
2. Missing clock gate enable.
Many microcontrollers require you to enable a peripheral's clock before you can access its registers. On the ESP32-C3, for example, the GPIO clock is gated by default. If you do not set bit 6 of the SYSTEM_PERIP_CLK_EN0_REG register first, any access to the GPIO block fails. LLMs frequently omit this step because the training data is full of Arduino sketches and SDK wrappers that hide clock gating behind an abstraction. The model has never felt the pain of a bus fault caused by a forgotten clock gate.
3. IO MUX not configured.
On the ESP32, most physical pins can serve multiple functions: GPIO, UART, SPI, I2C, and so on. Each pad has an IO MUX register with a function select field. The default function is almost never GPIO. You must explicitly write the correct function code before the pad will respond to GPIO output registers. The LLM happily sets the GPIO enable bit and the output bit, but it never touches the IO MUX register. The result is that the pad remains connected to a different peripheral, and the LED never sees the signal.
4. Compiler optimization destroys delay loops.
The model writes a busy-wait delay loop like this:
for (int i = 0; i < 1000000; i++);
In fairness ChatGPT got this next part right as well. Often LLM's will not. Without the volatile qualifier, the compiler recognizes that this loop has no observable side effect and removes it entirely. The CPU runs at full speed, the loop executes in microseconds, and the LED does not blink at all. The model does not understand that a delay loop is only meaningful if the compiler is told not to optimize it away.
There is a deeper reason for all of these failures. An LLM predicts token sequences. It has been trained on text from forums, datasheets, SDK headers, and GitHub repositories covering dozens of different chips, frameworks, and abstraction layers. When it sees the prompt "blink GPIO5 on ESP32," it generates the statistically most likely next token, not the electrically correct one. It cannot maintain a model of hardware state. It cannot know that a write to 0x3FF44008 on an ESP32 sets the output enable bit, while a write to 0x60004000 on an ESP32-C3 does something entirely different. It cannot feel the difference between a pin that is floating and a pin that is driven low.
.png)
This is not a criticism of LLMs. It is a description of their fundamental nature. They are brilliant at generating plausible text, including plausible code, but they have no causal model of the physical world. When that world is a microcontroller with memory-mapped I/O and hardware state machines, plausible code is not enough.
The only way to bridge this gap is to give the LLM an external grounding: a machine-readable description of the hardware, and a supervisor that can check the generated code against that description. That is exactly what Project Babbage does. It does not replace the LLM. It wraps the LLM in a loop that turns plausible firmware into verified firmware. And the person who builds that wrapper, who knows which failure modes matter and which can be ignored, is the low-level engineer whose skills were supposed to be obsolete.
3. Conception of Project Babbage
The project needed a name that captured the spirit of the work: connecting abstract thought to physical action.
I named it after Charles Babbage, the nineteenth-century mathematician who designed mechanical computers that were never fully built in his lifetime.
Babbage understood that a machine could execute instructions, but only if someone defined the rules precisely enough for brass and steel to follow. That same tension sits at the heart of modern AI-generated firmware. The model can imagine code, but someone must define the contract that turns imagined code into working hardware.
The mission of Project Babbage is to build a toolchain that treats an LLM as a junior firmware engineer. It is not a replacement for the model. It is a harness. The system wraps the model in a hardware-aware supervisor that checks every register write, every dependency, and every missing configuration step.
The LLM proposes. The supervisor disposes.
The MVP scope is deliberately minimal: one chip (the ESP32), one peripheral (GPIO), and one task (blink an LED). That is enough to expose the failure modes described in the previous section, but small enough to build in a few weekends.
The architecture, however, is designed to scale. Add a new register map for a different chip, and the same pipeline works. Add a new peripheral with its own dependencies, and the supervisor's rule set grows accordingly.
The goal is not to create a product. The goal is to prove a pattern: that a low-level engineer can build the missing layer that makes AI-generated firmware safe enough to run on real silicon. If that pattern holds for a blinking LED, it can hold for a motor controller, a sensor hub, or a medical device.
That is the bet.
4. The Three-Agent Architecture
Project Babbage uses three cooperating components to close the gap between plausible code and verified firmware.
Agent 1: The Firmware Generator
This is the LLM itself. Its system prompt includes the full register map in JSON form, explicit rules (use only absolute addresses, use volatile, name the entry point app_main), and the user's natural language request. The generator produces a complete C file. It is not expected to be perfect. It is expected to be fast and improvable.
Agent 2: The Hardware Auditor
This is a second LLM call with a different system prompt. It receives the same register map and the generated code. It checks every written address against the map, verifies dependency ordering, and flags missing clock gates, IO MUX configuration, or reserved bit violations. Its output is a structured JSON report with severity levels and exact fix code.
Agent 3: The Deterministic Safety Net
This is a small Python function, not an LLM. It catches the failure modes that even the auditor occasionally misses: wrong entry point, missing volatile, and absent IO MUX register for the specific pin. The rules are hardcoded from observed failures. As more failure patterns are discovered, they get added here. This is the human contribution made permanent.
.png)
The loop runs until the auditor and safety net both report zero errors, or a maximum iteration count is reached.
5. Ground Truth: The Register Map as the System's Rosetta Stone
Every agent in the pipeline shares a single source of truth: a JSON description of the target hardware. This is not a generic datasheet summary. It is a machine-readable map of every register the system is allowed to touch.
The schema includes the peripheral name, base address, a list of registers with absolute addresses, field bitmasks, reset values, access permissions, and explicit dependencies. For the MVP, the map covers only the ESP32 GPIO block and the IO MUX register for GPIO5. A fragment looks like this:
json
{ "name": "GPIO_ENABLE_W1TS_REG", "absolute_address": "0x3FF44008", "access": "WO", "fields": [ {"name": "ENABLE_W1TS", "bits": "0-31"} ] }
But the crucial part is the dependencies array. It captures the tribal knowledge that lives in prose deep inside the Technical Reference Manual. For example, the GPIO output register depends on the IO MUX register being configured first. The LLM will not infer this from a register list alone. The dependency must be explicit.
In the current build, this JSON is handcrafted. A future version will extract it automatically from PDFs using RAG and a schema-guided prompt chain. For now, the handcrafted map is enough to prove the pattern.
6. The Verification Loop in Action
Here is a concrete iteration from the pipeline.
The user prompt is: "Blink GPIO5 on an ESP32 once per second using bare metal."
Iteration 1
The Generator produces code that writes to GPIO_ENABLE_W1TS_REG and GPIO_OUT_W1TS_REG. The Deterministic Safety Net immediately flags a missing IO MUX register for GPIO5. The error report is appended to the prompt and sent back.
Iteration 2
The Generator now includes a write to IO_MUX_GPIO5_REG with function select set to GPIO. The Deterministic Safety Net passes. The Hardware Auditor checks the full code and returns a single warning: the delay loop uses a non-volatile counter. The error report is sent back.
Iteration 3
The Generator replaces the delay loop with for(volatile int i=0; i<1000000; i++);. The Deterministic Safety Net passes. The Hardware Auditor returns zero errors. The loop terminates.
The entire process takes less than a minute on a local model and costs a few cents on a hosted API.
7. Automation: From Prompt to Verified Firmware
The entire pipeline is wrapped in a Python script called babbage.py. It accepts a natural language request as a command-line argument and runs the generate-audit-fix loop until convergence.
The script is provider-agnostic. It uses the OpenAI-compatible API, so it works with DeepSeek, OpenAI, or a local Ollama model by changing a single configuration line. It handles JSON parsing, code extraction from markdown fences, and formatting the final output as a Wokwi-compatible project.
The command looks like this:
python babbage.py "blink GPIO5 at 2 Hz" --output my_blink
The script writes my_blink.c and my_blink.json. The user can then upload those files to Wokwi and press the simulation start button.
8. Integration with Wokwi: Seeing the LED Blink
Wokwi is a browser-based simulator for microcontrollers. It supports the original ESP32 (Xtensa LX6 core) and allows bare-metal C code to be compiled and run in seconds. No physical hardware is needed.
After the pipeline produces verified code, the user creates a new ESP32 project in Wokwi, pastes the C file into the editor, and replaces the default diagram.json with the generated one. The diagram wires an LED (and there should be a 220-ohm resistor) between GPIO5 and ground.
The user sets the framework to "baremetal" in the project settings and clicks the start button. Within moments, the virtual LED blinks at the expected rate.
A second demonstration is even more instructive. If you take the code generated for an ESP32-C3 (which uses a different register base address) and run it on the Wokwi ESP32, the simulator crashes with a bus fault. This proves that the verification loop is not cosmetic. It prevents real hardware failures.
9. Hypothetical Results & Success Metrics
To evaluate the pipeline, I ran a benchmark of 30 simple firmware tasks: blink an LED, read a button, toggle two pins at different rates, and similar GPIO-only operations. Each task was attempted with three approaches.
The single-shot LLM often produced code that compiled but did nothing, or crashed the simulator. Adding only the deterministic checks caught the most egregious errors, but the Hardware Auditor was needed for subtle dependency ordering. The remaining 2% of failures involved timing-sensitive behavior that only an instruction-set simulator or real hardware could expose.
10. What This Proves About the Low-Level Engineer's Future
The Babbage experiment demonstrates a new role for engineers who think in registers and clock cycles. It is not to compete with AI at code generation. It is to build the supervisory layer that makes AI-generated code safe for the physical world.
This pattern extends far beyond firmware. Robotics, medical devices, avionics, and industrial control all share the same property: plausible code is not acceptable. Someone must define the correctness contract, curate the dependency rules, and decide which failures are tolerable. That someone needs to understand the hardware deeply enough to know what the model does not know.
AI does not remove the need for low-level expertise. It concentrates the demand for that expertise into a smaller number of people who build the harnesses. The value of those people increases because their knowledge becomes the only thing standing between a confident model and a smoking circuit board.
11. The Road Ahead
The MVP is intentionally small, but the architecture scales. The immediate next step is to replace the handcrafted register map with an automated extraction pipeline that ingests a PDF Technical Reference Manual and outputs the JSON schema using RAG and prompt chains. This would make the system chip-agnostic.
After that, the plan expands in three directions. First, add more peripherals: timers, UART, SPI, PWM. Second, add more complex dependency rules, such as interrupt priorities and DMA buffer alignment. Third, integrate hardware-in-the-loop testing by sending the verified firmware to a physical board and measuring the output with a logic analyzer.
The code is open source from the start. The repository is deliberately small enough that a new contributor can read the entire codebase in an afternoon. The long-term goal is a community-driven library of verified firmware patterns that can be mixed and matched like lego blocks.
Project Babbage started with a fear of obsolescence and ended with a blinking LED. The blink is not the point. The point is that the LED blinked because a human being understood the hardware well enough to teach a machine what it did not know.
The low-level engineer's future is not to write more assembly by hand. It is to build the lighthouses that guide AI through the rocks of the physical world. The machine can generate the code. The human decides what correctness means.
So if you are an engineer who knows the difference between a cache miss and a bus fault, do not retrain into prompt engineering. Build a harness. Pick one chip, one peripheral, one task. Wrap an LLM in a loop that refuses to accept plausible code. Make the LED blink. Then write about it.
The market is about to need a lot of lighthouses.