Repository navigation
A timebase valid from reset would remove the boot-stage boundary #44
Description
Problem
mcu::Millis() is driven by the SysTick interrupt, which InitSysTick() starts
from Board::Init() — i.e. after main(). Before that the counter is frozen,
so any bounded wait written the obvious way can never end:
if ((Millis() - start) > timeout_ms) { /* unreachable before the tick */ }That was a live hang in I2CBus's flag wait and Usart::Receive until #42
added guards; both now refuse with kInvalidState when the tick is not running.
The guards are correct but they are a fence, not a fix: they make the boundary
safe, not absent.
The boundary is what forces the two-stage split in docs/BOOT_FLOW.md. A stage 1
constructor may wait — mcu::Delay falls back to a DWT->CYCCNT spin — but may
not talk, because every real transfer needs a timeout.
Proposal
Build a timeout source on DWT->CYCCNT, which is free-running from reset and
needs no interrupt:
/// Microseconds since reset, from the cycle counter. Valid at any point after
/// reset, including from a static constructor -- unlike Millis(), which does
/// not advance until the board starts the tick.
[[nodiscard]] auto Micros() -> std::uint32_t;src/libs/mcu/arm_cm7/delay.cpp already does exactly this for sub-millisecond
waits (SpinCycles, EnableCycleCounter) — the machinery exists and is being
used for half the job.
With it:
WaitForFlagandUsart::Receiveget a real timeout at every point in boot,
and theSysTickRunning()guards can go.- Stage 1 and stage 2 stop differing in when they may run and differ only in
whether they can fail — which is the distinction that actually matters. - An off-chip device could be brought up from a constructor if it never fails,
rather than being barred on a technicality about the clock.
What it does not solve
- It is a timeout source, not an uptime clock.
CYCCNTis 32-bit and wraps
in ~268 s at 16 MHz. Fine for bounding a transfer; useless forMillis()'s
49-day range, so it does not replace SysTick. - It does not make bring-up fallible. A constructor still cannot report an
error, so off-chip bring-up still belongs inBoard::Init()for that reason
alone. This removes one of the two arguments for the split, not both.
Unverified
EnableCycleCounter sets CoreDebug->DEMCR |= TRCENA then DWT->CTRL |= CYCCNTENA.
On some Cortex-M parts TRCENA only sticks with a debugger attached. This needs
measuring on the F767 running standalone before anything depends on it — and the
existing mcu::Delay fallback depends on it today, silently, so that is worth
checking regardless of whether this issue is taken.
Raising the core clock off HSI also invalidates kSystemCoreClockHz here, the
same dependency delay.cpp and the I2C TIMINGR table already carry.
Trigger
Not worth doing speculatively. Do it when a stage 1 peripheral genuinely needs a
bounded wait — a PLL lock poll, or an on-chip block with a status bit that can
hang.
Anchored in the code
src/libs/mcu/arm_cm7/delay.cpp—SpinCycles,EnableCycleCounter, the existingSysTickRunning()fallbacksrc/libs/mcu/arm_cm7/systick.hpp—Millis(),SysTickRunning()src/libs/mcu/arm_cm7/i2c.cpp—WaitForFlag's guardsrc/libs/mcu/arm_cm7/usart.cpp—Receive's guarddocs/BOOT_FLOW.md— "What would change this design"
Activity
docs: anchor the boot-flow follow-ups to #43 and #44