Vibe-Coding Safe Firmware

philhippus1 pts0 comments

Bare-Metal Vibecoding — lantra.nl

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

/* ESP32 GPIO peripheral base */<br>#define GPIO_BASE 0x3FF44000UL

#define GPIO_OUT_REG (*(volatile uint32_t *)(GPIO_BASE + 0x04))<br>#define GPIO_ENABLE_REG (*(volatile uint32_t *)(GPIO_BASE + 0x20))

#define GPIO5 (1U

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.<br>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.<br>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.<br>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.<br>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....

model code esp32 gpio registers clock

Related Articles