V2 left a hole. The hardware worked, but firmware was the next problem, and it’s still where the project lives. This is a status post on the shell — what I picked, what was missing, where I got stuck, and what I think the bug is.
Picking STM_Shell#
I spent longer than I should have deciding what the user interface should look like. The end of that wandering was the obvious answer: a shell, like the Bus Pirate. Type a command, get a response. The model I’d been emulating from the start of the project was already the right answer; I just kept poking at alternatives.
For the shell itself I picked STM_Shell, a minimal C library that handles a prompt, a command table, and dispatch. The plan was to skip the scaffolding I’d written a dozen times in other contexts and focus on the parts specific to my board: protocol mode selection, IO level setting, the rest. That plan held for about as long as it took to start using the thing.
What it didn’t give me#
STM_Shell takes one byte at a time from a UART DMA callback, echoes it back, and dispatches the line when it sees a CR. That works fine for typing commands left to right. It does not work for arrow keys, mid-line backspace, or anything else a terminal user does without thinking about it.
Terminal line editing is multi-byte. Arrow keys come in as ANSI escape sequences — ESC [ A for up, three bytes that have to be recognized as a single event. A callback that fires once per byte and treats each byte in isolation has no way to tell it’s halfway through a sequence. On top of that, the shell doesn’t track a cursor position inside the line buffer. Backspace and delete don’t just append to a buffer, they edit it at the cursor. Without a notion of where the cursor is, neither command does the right thing.
Both are the kind of thing a full line-editing library handles. STM_Shell doesn’t, by design — it’s minimal — and adding them is not a one-line patch.
The ringbuffer approach#
The architectural fix is to decouple “byte arrived” from “shell consumes a character.” Stuff incoming bytes into a ringbuffer from the UART ISR. Let the shell pull bytes from the ringbuffer in the main loop, with whatever multi-byte parser state it needs to recognize escape sequences. The shell becomes a state machine over a byte stream rather than a function called once per byte.
That’s the right shape, and it’s also the point where I stopped being a library user and started being someone editing a library’s internals. The input path, the buffer ownership, and the parser state all became mine. STM_Shell’s role kept shrinking. I told myself I was adding a feature; what I was doing was rewriting the part of a shell that has any state in it.
I picked the library to avoid writing the boring scaffolding, which it did save me. It didn’t save me from any of the work I actually cared about.
The bug I didn’t track down#
The ringbuffer-fed shell mostly works. Normal commands type and dispatch. Arrow keys are inconsistent — most of the time the escape sequence is recognized, occasionally a byte goes missing and the rest of the sequence ends up in the line buffer as garbage. Slow human-typed input into an otherwise idle UART at 115200 baud [CHECK], dropping bytes somewhere between the ISR and the consumer.
The candidates are roughly: a race on the buffer indices, the UART interrupt being preempted long enough to lose a byte, or my parser state machine resetting wrong on certain input. None of those should be hard to provoke at this load, but I haven’t run it down.
What I did do before stopping was move from the V2 board to a Nucleo. The V2 board doesn’t break out SWD pins, so debugging there is printf-only. The Nucleo has an integrated ST-Link [CHECK] and gives me breakpoints, register inspection, all of it. The downside is I’m now debugging a slightly different system from the one I’m trying to ship — same MCU family, different board, different clocks, possibly different interrupt timing. If the bug is timing-sensitive, that matters.
Where it sits#
On hold. The shell takes commands. It does not edit lines the way I want. I have a hypothesis about the bug and a debug setup, but no fix yet, and I’ve picked up another project in the meantime. When I come back to this the first thing to do is figure out whether the bug reproduces on the Nucleo at all — that tells me whether I’m looking at logic or at timing.
Source is on Codeberg.