Skip to content

A timebase valid from reset would remove the boot-stage boundary #44

Description

@nehalkpatel

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.cppSpinCycles, EnableCycleCounter, the existing SysTickRunning() fallback
  • src/libs/mcu/arm_cm7/systick.hppMillis(), SysTickRunning()
  • src/libs/mcu/arm_cm7/i2c.cppWaitForFlag's guard
  • src/libs/mcu/arm_cm7/usart.cppReceive's guard
  • docs/BOOT_FLOW.md — "What would change this design"

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions