Thirty times I said no
Six hours with an AI on firmware. It produced a defensible answer roughly every twenty minutes, and the architecture only arrived because I could name what was wrong with each one.
I spent an evening with Claude on the capture path of my bench instrument — a CH32V305 reading two ADCs at 2 million pairs per second and pushing them over USB to a tablet. Six and a half hours, 110 typed prompts. What came out was an architecture I would not have specified at the start: DMA-completion-chained staging, raw four-byte pairs on the wire, envelope computation moved off the microcontroller entirely, ring overruns down from 0.233 per second to 0.017.
What did not happen is that I described that design and it got built. What happened is that it produced a good answer roughly every twenty minutes, and I declined about thirty of them.
That sounds like a story about persistence. It is not, quite. Persistence is cheap and mostly useless on its own — you can say “are you sure?” indefinitely and get nothing but rephrased confidence. Every refusal that actually moved something was a refusal I could justify, and the justification came from knowing the hardware.
What the refusals were made of
Pulled verbatim from the transcript, typos included. These are not encouragement.
5.83 cy/pair is too cheap
That one was arithmetic. It had measured a function — unpack two 12-bit fields from a 32-bit word, run eight min/max comparisons, write six bytes — at 5.83 cycles per pair, and built a whole CPU budget on top of it. But that is 45-odd instructions of work, and this is a single-issue RISC-V core. It retires at most one instruction per cycle. Eleven cycles for forty-five instructions is not unlikely, it is impossible.
The counter it was reading turned out to run at HCLK/8 rather than HCLK. Every cycle number that evening was eight times low. The corrected figure was 46.6 cycles per pair — 66% of the core, and the actual ceiling we were looking for.
memcpy is 32bit words are 8 bit at a time?
It had measured memcpy at 10 cycles per byte and, rather than disbelieving it,
had written it down as the floor for byte movement. Ten cycles per byte on a
word-aligned copy is not a floor, it is a bug. The disassembly showed the vendor
runtime’s memcpy is a naive byte loop — seven instructions per byte,
recomputing both pointers from the base on every iteration. An unrolled word
copy measured 0.67 cycles per byte. Fifteen times.
ISR should only fill pointers
This one is design, not measurement. It had written that a timer interrupt “fills a buffer”. An interrupt handler does not fill anything — it writes three DMA registers and returns, and the engine fills asynchronously afterwards. The distinction matters because it had also set the buffer-ready flag inside the handler, before the transfer completed, which meant the main loop then had to block waiting for it. That reintroduces exactly the coupling the design existed to remove.
and isr should first check if dma is free
Same category. Kicking a channel mid-transfer corrupts it. That is not a subtle point, but it is not one that surfaces unless you have written a DMA driver.
wait, you said earlier, that there is a DMA done interrupt, rather than timer brute forcing, use that to kick next buffer
The best single prompt of the evening, and it deleted a whole afternoon’s direction. It had built a timer to drive staging at the packet cadence, which required guessing the period — and guessing it wrong, so the staged rate quietly fell below the produced rate. Chaining off transfer completion needs no rate at all. Each transfer finishing starts the next. It is self-clocking.
is the overrun coutner wired properly?
The last one. It had just reported a four-times reduction in ring overruns from the new staging design. The counter producing that number had been compiled out of the build hours earlier when the code moved into a new branch. “Zero overruns” meant nothing was counting.
The thing that generalises
An AI assistant will stop at the first defensible answer. Not the first wrong answer — a wrong answer you can usually catch. The first defensible one: a real measurement, a genuine improvement, an internally consistent story. The fifteen-times word copy was defensible. It cleared the target. If I had accepted it we would have shipped a good result and never found that the DMA engine does the same transfer for free, or that the copy did not need to be on the critical path at all.
Nothing about the model’s output signals the difference between “this is the answer” and “this is an answer”. Both arrive with the same confidence, the same supporting numbers, the same tidy summary. The only thing that separates them is whether you know enough to say what is missing.
Which is the uncomfortable part. Every refusal above required specific knowledge: that a single-issue core cannot exceed one instruction per cycle, that vendor runtimes ship reference implementations rather than optimised ones, that a DMA channel has a completion interrupt and that using it is different from polling a flag. Take those away and I am left with “are you sure?”, which produces a rephrase and nothing else.
The tool did the work. It wrote the unrolled copy, the DMA path, the interrupt handler, and it found things I would not have — the eight-times counter error came from its own disassembly once I pushed. But it also proposed stopping at least three times at points that were good rather than best, and each time the next experiment was a single build away.
So the transferable version is not “persevere”. It is narrower and less comfortable: the quality of what you get out is bounded by your ability to tell a defensible answer from the right one, and that is not a prompting skill. It is the same domain knowledge you would have needed to do the work yourself — just spent on judging output instead of producing it.
You are not replaced. You are moved to the position of the person who has to know when to say no.