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:
WaitForFlag and Usart::Receive get a real timeout at every point in boot,
and the SysTickRunning() 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.
CYCCNT is 32-bit and wraps
in ~268 s at 16 MHz. Fine for bounding a transfer; useless for Millis()'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 in Board::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 existing SysTickRunning() fallback
src/libs/mcu/arm_cm7/systick.hpp — Millis(), SysTickRunning()
src/libs/mcu/arm_cm7/i2c.cpp — WaitForFlag's guard
src/libs/mcu/arm_cm7/usart.cpp — Receive's guard
docs/BOOT_FLOW.md — "What would change this design"
Problem
mcu::Millis()is driven by the SysTick interrupt, whichInitSysTick()startsfrom
Board::Init()— i.e. aftermain(). Before that the counter is frozen,so any bounded wait written the obvious way can never end:
That was a live hang in
I2CBus's flag wait andUsart::Receiveuntil #42added guards; both now refuse with
kInvalidStatewhen 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 1constructor may wait —
mcu::Delayfalls back to aDWT->CYCCNTspin — but maynot talk, because every real transfer needs a timeout.
Proposal
Build a timeout source on
DWT->CYCCNT, which is free-running from reset andneeds no interrupt:
src/libs/mcu/arm_cm7/delay.cppalready does exactly this for sub-millisecondwaits (
SpinCycles,EnableCycleCounter) — the machinery exists and is beingused for half the job.
With it:
WaitForFlagandUsart::Receiveget a real timeout at every point in boot,and the
SysTickRunning()guards can go.whether they can fail — which is the distinction that actually matters.
rather than being barred on a technicality about the clock.
What it does not solve
CYCCNTis 32-bit and wrapsin ~268 s at 16 MHz. Fine for bounding a transfer; useless for
Millis()'s49-day range, so it does not replace SysTick.
error, so off-chip bring-up still belongs in
Board::Init()for that reasonalone. This removes one of the two arguments for the split, not both.
Unverified
EnableCycleCountersetsCoreDebug->DEMCR |= TRCENAthenDWT->CTRL |= CYCCNTENA.On some Cortex-M parts
TRCENAonly sticks with a debugger attached. This needsmeasuring on the F767 running standalone before anything depends on it — and the
existing
mcu::Delayfallback depends on it today, silently, so that is worthchecking regardless of whether this issue is taken.
Raising the core clock off HSI also invalidates
kSystemCoreClockHzhere, thesame dependency
delay.cppand the I2CTIMINGRtable 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"