Yeah, baby! Austin Powers Writes Firmware
Yeah, Baby! Austin Powers Writes Firmware
10:00 AM. I have this screen ui running, a TUI. It’s pretty simple: four tabs, modals, breakpoints, ATP-by-URL loading wired in (paste a GitHub URL, do a SHA-cached fetch, ready to go). I have a firmware binary path locked and loaded: nxt-prod-qorvo-ae.bin. The plan is simple. Hit F. Watch a chip get programmed. Drink coffee.
So “Yeah baby!” hit F.
Nothing happens. Or something happens. Hard to tell, because for reasons I want to avoid relitigating in a code review later, every panel is doing runtime.invoke catch {} and quietly swallowing the error. My little TUI is too polite to mention that the bench just told us to fuck off.
First fix of the day: stop swallowing errors. Surface them uniformly. Now the failure is visible, which is the prerequisite for everything else.
It is going to be the prerequisite for a lot of things today. Today is going to be a long day.
I wrote a few weeks back about how the moats are mostly dead. Brand, network effects, capital, IP, first-mover: gone, gone, gone, gone, gone. The thing left standing is physics. Atoms. Things that fly, things that get implanted in humans, things that have to actually work in the cold rain when the cable is half-seated and the ground plane is slightly noisier than it was yesterday.
This essay is the documentation of an actual, real-life experience I had yesterday, which corroborates the thesis of my previous writing. Yesterday, I tried to cross one of the moats I wrote about in the abstract. The moat won. I want to talk about how it won, why AI helped right up to a wall and then fell over dead, and why embedded system development (in anno Domini 2026, with planet-scale computing in our pockets) is still doing things the way it did them in 1975.
So, basically, I want to talk about Austin Powers.
Remember? Powers gets cryogenically frozen in 1967, thawed in 1997, and spends the rest of the movie navigating a world that has evolved without him. The joke is that the world has moved and he hasn’t. The deeper punchline is that some of his stuff still works anyway, because some things never change. But! Then Dr. Evil steals his mojo! Powers trying to perform without the thing that made him work. No one really knows what the mojo is. But it’s gone.
Weirdly, this movie is fairly similar to the entire embedded experience.
The toolchain is frozen, like Powers. OpenOCD config files read like a flippin’ Latin Mass. I mean that literally; they have the cadence of liturgical text and the comprehensibility of one. STLink probes ship firmware that you have to upgrade by hand because nobody built an auto-updater? (Really?) The vendor SDK assumes you have hands and a printed datasheet. PAC55xx, the chip we are talking to today, does not expose an nRST pin to the debugger, which means there is no debugger-driven way to recover a bricked one. (Spoiler alert.) The cultural inheritance is older than the people maintaining it. Seriously, we might as well be chanting in latin.
But there’s something more disconcerting here: the priesthood likes being the priesthood. The forked OpenOCD that apparently no one maintains has a custom hack for VTREF/VAPP handling that is undocumented outside our codebase. The bootloader image my colleague keeps in a folder on her laptop “for exactly this” is somebody’s accumulated muscle memory in binary form. Nobody wrote it down. Nobody can.
This reveals the moat: part real water (alligators possibly lurking), but I now see: also part theological.
10:30 AM. Chip refuses to attach. Identity query fails. SWD speed sweep at 100, 250, 500, 1000, 2000, 4000 kHz. Every single one fails with the same error code: JtagGetIdcodeError. I build a tool called probe-doctor so I can iterate on this without keeping a human (me) in the loop. It is a beautiful little tool. It is completely useless because the problem isn’t probe-rs.
11:30 AM. I write a raw experiment that bypasses Probe::attach() and goes straight at the metal: attach_to_unspecified + try_into_arm_interface + raw DP register reads, the path OpenOCD effectively uses. Same failure, just lower in the stack. I commit the experiment binary because next time someone asks if probe-rs is the problem, the answer will be a Cargo run away.
12:30 PM. “Any openocd running on this box?” Yes. A stale instance is holding the probe. Kill it. Helpful. Not the root cause.
13:00. STLink firmware upgrade. V2J29 to V2J47. This involves rooting around on the web, downloading a completely circa-2009 updater. Now: modern firmware on the probe. But: still no change.
13:30. Bench power. I have a programmable supply on my desk. 24V CV, 1A CC limit. I have no idea if those are reasonable values. I’m writing this sentence and I still kind of don’t, in my bones. Writing it out: constant voltage, constant current, the thing acts like a voltage source until current draw exceeds the CC limit, at which point it pivots to a current source and lets voltage sag. Fine. I know that now. I did not know it at 13:30. What I said at 13:30, with my whole chest, was (using my best Dr. Evil voice): “Throw me a frickin’ bone here.”
I can write and debug software all day. I’ve been doing it since 1996, and it’s only gotten easier and easier. Looking at you, Claude. Software runs in a closed world made of symbols. Every state has a representation. Every transition has a cause. The whole weave and weft is, by construction, a symbol manipulator, which means everything that happens inside it has, in principle, a name. AI is brutally good in that world. AI compresses symbol-side work like a milspec trash compactor. The whole month I just spent learning embedded firmware? Wicked compressed. A month ago I didn’t know what a commutation angle was. I didn’t know what a stator was. I didn’t know what an adccal did, or why an ISR was a different kind of code than the rest of your code, or what a HardFault was, or why a UsageFault could escalate into one. Now I do. AI got me there in weeks instead of years. Yeah, baby, yeah.
But AI cannot tell you what a 1A CC limit feels like when the chip starts to misbehave. AI cannot tell you that the third pin on this cable is loose because the connector geometry is slightly off. AI cannot tell you that the reason it works on the laptop and not the desktop is that the USB stack on one of them does power negotiation differently and the probe needs a clean rail.
AI also cannot do what happened next.
13:45. My colleague Dawsyn comes over. (I am changing exactly zero details for the record. This happened.) She reads the situation and is politely skeptical that the way I’ve described my roadblock is in fact, the actual roadblock. She doesn’t really theorize about what I’ve said. She just starts… disconnecting shit.
Dawsyn unhooked every connection on the bench, hooking up the same physical setup to her laptop, plugged it in, and it worked. She reflashed a bootloader image she keeps on her drive (for exactly this situation?). Then she walked it back to my bench, plugged it back in, and handed it to me.
It worked.
She couldn’t name the variable that flipped. She implied it, in fact, may not be knowable. It could be that her USB stack negotiates power differently. It could be that the act of unseating and reseating the connectors did the work. It could be the bootloader reflash. It could be all three. It could be none of them and we got lucky on cable contact.
There is no test for the bench got reseated by someone else. There is no git log entry. There is no commit hash. There is nothing AI can read. There is nothing AI can replicate. Dawsyn does not, herself, fully understand which thing she did was the thing that mattered. Apparently, she has done it enough times that she does it without thinking, which is precisely what muscle memory is. Her hands know.
That’s the mojo, baby.
This is also the transmission mechanism for embodied knowledge that doesn’t have a symbolic representation. It is exactly the thing that AI, the most powerful symbol compressor we have ever built in the history of Mankind, cannot help you with, because there is nothing on the symbol manifold to compress. (I wrote about this too, in The Ground Beneath the Map)
I spent three frustrating hours upstairs doing structured software diagnosis. Dawsyn fixed it in five minutes downstairs by reseating cables and reflashing a binary she keeps in her back pocket. The line between “debug” and “ritual” is a real line. You learn to recognize when you’ve crossed it. You don’t learn to skip the ritual.
14:30. Bench is back. Probe attaches. Erase works. Program fails: flash write exceeds PAC55xx flash size.
I was uploading to the app start address (0x8000), but the production .bin is a full-flash image: 128KB starting from zero. 128KB at offset 0x8000 ends at 0x28760, which is, you may have noticed, off the end of a 128KB chip. I have a build-time size checker, but only for app-relative builds. This image isn’t one. Use flash_base (0x0). Done.
14:55. Run again. The test allocator complains about a leak. callBackend is doing _ = try backendMessage(allocator), which pulls the error message out and quietly discards the buffer. Remove the line. The message gets fetched separately later anyway.
15:15. IT WORKED! EUREKA!
End-to-end first in-house flash. Paste URL → fetch from GitHub → SHA cache → upload → SWD attach → erase → program → verify. No OpenOCD in the loop at all. Bridge crossed. (For about nineteen minutes. Stay tuned.)
I committed it. There was a brief moment of victorious joy. Brief.
15:30. “Now we can use openocd to see what my firmware is doing.” OpenOCD GDB server, attach with arm-none-eabi-gdb, drive it from there. Took maybe ten minutes. There is a HardFault in the running firmware. Decode the stacked exception frame. Path:
A NaN gets passed into interpolate_temp_from_voltage. The FPU raises invalid-op. UsageFault fires, escalates to HardFault because of how the config bits are set. The fault handler reads a health snapshot, which calls read_adc, which… feeds another NaN into the interpolation, which faults again, recursively. Stack is eating itself.
I patch it locally. New helper nxt_taurus_ae_isfinite_f32. !isfinite_f32(volts) guard at the top of interpolate_temp_from_voltage. Both sample voltage and Vcc guarded. Submit the patch.
Claude code review finds three things:
- The symmetric
taurus_400file got the helper butread_motor_temp_cuses linear math and bypasses the new guard. Incomplete. - NULL
outpointer toread_health_commonis still latent; the fault handler can call with NULL. - The new host-side test verifies the guard logic but doesn’t exercise actual FPU traps on Cortex-M, so it can’t catch a regression of the kind I just hit.
Four weeks ago I did not know what any of those words meant in this order. Yeah, baby. Yeah.
16:45. “Can I reflash with station?” Yes. pkill openocd to free the probe. (Station should auto-detect this; noted, fix-worthy.) Reflash patched firmware.
Now, on every attach: attach failed. Power cycle does nothing. Idle current draw ~26 mA. That’s suspicious. Way too low for active firmware. Unplug, replug. No change. Different probe. Same. Different host. Same. JTAG path. init mode failed. SWD path. JtagDbgPowerError: chip refuses the debug power-up handshake.
The AI assistant working with me proposed “firmware self-disabled debug power on boot.” I pushed back. I’d checked the source. There are no debug-disable register writes. Correct rebuke. The model updated.
Probe-rs CLI gives us the cleaner error directly. SWD: JtagDbgPowerError. JTAG: JtagNoDeviceConnected (board is likely SWD-only; TDO probably isn’t wired). My probe-doctor confirms across all speeds.
The AI suggests holding nRST low through the attach as a recovery move. I correct it again: PAC55xx doesn’t expose nRST to the debugger. There is no debugger-driven reset path. The only way back into this chip is the UART bootloader, or hardware-level intervention. Bench-side work, not symbol-side.
Achievement unlocked! Board is bricked! The afternoon’s good work bricked it. The morning’s tooling can’t bring it back.
This is the moat I wrote about.
So what did I actually learn?
I learned that the moats really are made of atoms. (I already knew this, but I didn’t feel it in my bones. Now I do…) I learned that AI is a screaming-fast accelerant inside symbol-world and gracelessly stops at the bench and barfs all over everything, because there is no symbolic representation of “the cables got reseated by someone with hands.” I learned that a thirty-year software veteran can compress a decent amount of an embedded firmware education into a few weeks with AI in the loop and still hit the wall the moment the problem space stops being symbols and starts being current draw and connector geometry and bootloader binaries kept in someone else’s home directory.
I learned - viscerally - after 30 years - what it feels like to be really, really n00b again. What it feels like to have no idea what you’re doing. The apprenticeship. I’m super grateful to Dawsyn, who is less than half my age, for being generous and considerate to me, the n00b. I experienced firsthand, that the apprenticeship - the gnostic experience - is the only known transmission mechanism for embodied knowledge that cannot be written down. It must be lived.
I have learned firsthand now that the entire field of embedded development has a cultural, almost theological response, here. That response, for about fifty years now, has apparently been to choose to remain in the past. To leave things illegible, unwritten… difficult by shared, silent consensus. The forked OpenOCD. The undocumented VTREF hack. The pkill-openocd-to-free-the-probe ritual. The bootloader-in-the-back-pocket. The PDF datasheet. The driver that installs from a CD-ROM ISO downloaded from a vendor portal. The error message that says JtagGetIdcodeError and assumes you know what an Idcode is and why getting it would error.
I’m not dismissing the difficulty of doing embedded development. That’s real. The collision with voltage, heat, EMI, timings… these are all real. What Rich Hickey calls inherent complexity.
The mojo is real and the mojo is irreducible. The 1975-ness is the cultural decision to use the existence of mojo as an excuse to leave everything else in the same condition the priesthood inherited it. The toolchains could be ergonomic. Errors could be legible. Probes could auto-update their own firmware. The stations could detect a stale OpenOCD process. The bootloaders could ship with the SDK.
On this day I was like a reverse Austin Powers, travelling to the past, bringing magic with me. Me, with thirty years and a working TUI rewrote lastErrorHint and added word-wrap in a few minutes. The field could have done that twenty years ago. It didn’t. Because the embedded “priesthood” likes being special, and the priesthood includes the part where the new person hits JtagGetIdcodeError and has no goddamn idea what it means… and must suffer. They have to earn that gnostic knowledge!
Anyhow the board on my bench is bricked, currently. I did make the tooling better. I found a real bug in my firmware and patched most of it. I discovered that the patch grew the binary past the partition on two AE-CX3 variants. I confirmed exhaustively across SWD, JTAG, and probe-rs that the chip is, in fact, bricked, and that the recovery path is hardware. That’s where I’m stuck now. Atoms are hard. Physics is hard. Recalcitrant PAC5556’s are apparently… hard.
Bridge crossed at 15:15. Bridge collapsed sometime after 16:45. Bridge possibly burned to the ground.
That’s the moat doing what moats do. That is exactly why it’s still a moat. And that is why, if you are looking for a place where the AI gold rush of 2026 has trouble buying a ticket, the embedded world (for all its self-inflicted Latin-Mass nonsense) is still where physics lives - and therefore remains defensible.
Yeah, baby.