Expand description
Async buffered serial console for both targets — byte-level ring buffer
with overwrite-on-full semantics. The producer (log!() / send_line)
is O(1) and NEVER blocks the caller: never spins, never awaits, never
creates back-pressure. On full, the oldest bytes are overwritten so the
lines fired just before a failure survive (those are the informative ones).
A single drain_task pulls contiguous slices from the ring and writes
them via the target’s async TX sink (write_all().await parks on the
TX-done IRQ when the FIFO is full — no busy-spin). Producers signal the
drain after every write; the drain awaits the signal when the ring is empty.
Per-target seam (hardware): the sink types + setup (build + split the
peripheral) + [imp::boot_panic_write]. fire27 = UART0 @ 1 Mbaud; cores3 =
USB-Serial-JTAG CDC. setup does NOT make the fire27 TX async — into_async()
binds the IRQ to the calling core, so the binary does it from main (PRO).
The firmware’s alternator_regulator::logger::cat_line calls send_line
for the :cat dump. Unlike log!(), send_line is back-pressuring:
it awaits ring space before writing, so a fast read-back self-paces to the TX
drain rate and is lossless (a plain overwrite-on-full write dropped lines and
made log_interval’s read-back show false gaps). alternator-regulator depends
on this crate ONLY for that — optional + esp-hal-gated, so host builds never
pull it.
Modules§
- markers
- Stable, greppable log markers that the HIL
detect_crashcontract keys on. Treat these as part of the public contract — do not reword without updating the test harness.
Structs§
- Config
- Console install configuration.
serial: Noneis the production backstop — the log backend is registered (solog!is a cheap no-op into the ring) but no transport is brought up and no drain task runs, leaving zero serial surface.Some(_)brings up the chip’s native transport (UART0 on Fire27, USB-Serial-JTAG CDC on CoreS3) and spawns the drain. - Console
- What
installhands back: the console RX half, when a transport was brought up, to be handed toserial_cmd(bidirectional on one port, R7). In a serial-free build (console-serialoff, R9) it carries no transport. - Panic
Crumb - A panic breadcrumb recovered from the previous run:
locationis the panic file,reasona 32-bit digest of the panic message. - Serial
Resources - The peripheral bundle
super::installneeds: just the USB-Serial-JTAG device (the CoreS3 console is the native CDC port — no probe needed, R7).
Functions§
- drain_
task - The single console writer: pulls contiguous slices from the ring and
writes them via the target’s async TX sink.
write_all().awaitparks on the TX-done IRQ when the FIFO is full — no spin, no interrupts-off, no cross-core contention. When the ring is empty, awaits [DRAIN_SIGNAL] (woken by every producer write). Spawn once from the binary’s main (fire27: passtx.into_async(); cores3: the split TX is already async). - enable_
async - Compatibility no-op. The prior design switched producers from a blocking writer to an async queue here; with the ring buffer the producer path is the same in every phase (boot and steady-state both write to the ring), so there’s nothing to switch. Kept so binaries don’t have to change in this commit — can be removed once both binaries stop calling it.
- init
- Register the console as the global
logbackend. Call once, early. The ring is statically initialised, so producers can write immediately — pre-drain_taskwrites simply accumulate in the ring and are flushed once the drain runs. - install
- One-call console bring-up: register the
logbackend atcfg.leveland, whencfg.serialisSome, build the chip’s transport + spawn the singledrain_task. Replaces the hand-wiredinit()+setup()+ drain-spawn sequence in every binary. Call once from main (the core that owns the drain; Fire27’s TXinto_async()binds the IRQ to the calling core). - on_
panic - Shared message-only panic handler for both targets. Pushes the panic info
into the ring (alongside the pre-panic context that’s already there), then
synchronously drains the ring via the raw FIFO poker — the async drain
task is gone (or never started) so it can’t service the ring for us. After
the drain, halts so the fault stays visible. No stack walk: message-only
on both targets (deliberate, symmetric), which is why neither
esp-backtracenoresp-printlnis pulled in. The binary’s#[panic_handler]is a one-line wrapper around this. - send_
line - Back-pressuring line sink — the R10 injection point. A control crate must
not depend on this BSP directly; instead it defines its own
LineSink-style trait and the binary injects a ~5-line newtype whosesend_lineforwards here (the trait stays consumer-side, so neither BSP nor control crate depends on the other). This is also the bulk-dump path for the HIL:catCSV. - setup
- Build the USB-Serial-JTAG console and split.
into_async()here binds the IRQ to whatever core calls this — the binary calls it from main. - take_
panic_ breadcrumb - Read and clear the previous run’s panic breadcrumb, if any. Call once at
boot, before
install, and log the result (themarkers::PREV_PANICline) — that read-back is the cross-transport fault contract (R8), identical on both targets. ReturnsNoneon a clean boot or once the crumb is taken.
Type Aliases§
- Console
Rx - RX half →
serial_cmd(async poller); TX half → the drain task. - Console
Tx - Console
TxAsync - The drain task’s sink — the split TX is already async on cores3.