fix(arm): refuse bounded waits before the tick, and name the boot stages - #42
Open
nehalkpatel wants to merge 3 commits into
Open
fix(arm): refuse bounded waits before the tick, and name the boot stages#42nehalkpatel wants to merge 3 commits into
nehalkpatel wants to merge 3 commits into
Conversation
Millis() does not advance until InitSysTick() runs in Board::Init(), so
`(Millis() - start) > timeout` could never become true before that point.
I2CBus's flag wait and Usart::Receive both compared against that frozen
counter with no guard: a bus held low before the tick would have spun
forever, with the timeout silently doing nothing. Both now return
kInvalidState instead.
Unreachable today, since both are called only after Board::Init() has run.
It becomes reachable the moment anything talks to a device during bring-up,
which is exactly what documenting an off-chip bring-up stage invites.
docs/BOOT_FLOW.md is that documentation. The organising idea is that a
peripheral's stage is decided by what can fail, not by what is convenient:
- On-chip bring-up is register writes against hardware soldered into the
die. Nothing can be absent and nothing can time out, so it cannot fail,
and it lives in a constructor -- which is what makes an unconfigured
peripheral unreachable.
- Off-chip bring-up is I/O across a wire, where absence and silence are
normal outcomes. It needs somewhere that can report one: Board::Init().
That structure already existed; what was missing was the contract, which
board::Board now states where a reader meets it. The ADC is the case that
shows the split is not "sensor or not" -- the peripheral is stage 1, the
sensor wired to it is stage 2.
Also corrects an invariant from the previous commit. mcu::Delay *does* work
before the tick -- delay.cpp falls back to a DWT->CYCCNT spin -- so "bring-up
must not call mcu::Delay" was wrong. The real boundary is narrower: a stage 1
constructor may wait, but may not talk, because every real transfer needs a
timeout.
Two follow-ups are written down rather than taken. A timebase valid from
reset (Micros() on DWT->CYCCNT) would make timeouts work in every stage and
dissolve this boundary; it is worth doing when a stage 1 peripheral needs a
bounded wait. Explicit init levels in the style of Zephyr's PRE_KERNEL_1 /
POST_KERNEL would replace the two-phase split; the trigger is two off-chip
devices with an ordering constraint between them, and until then it would be
a framework guarding a state that has not occurred.
Known gaps recorded, not solved: common::Error has no payload, so
Board::Init() cannot name which device failed; there is nowhere to report a
bring-up failure to, since the console is a stage 3 resource; and an RTOS
will want SysTick for itself.
Costs 236 bytes of .text on i2c_demo for the guards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three commits overtook it. UART and I2C were still listed as placeholders that return an error, the architecture section said hardware boards were planned, and the technology stack named host emulation as the only target. The status table now distinguishes implemented from verified, which is a distinction this project should be making: GPIO/EXTI/SysTick and USART3 have been confirmed on the physical board, I2C has not. That matches the open task in PROJECT_PLAN.md's Milestone 2 rather than quietly claiming more. Also adds a Documentation section. docs/ has three files and the README linked none of them except in passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two gaps BOOT_FLOW.md recorded now have issues. Point at them, so the trigger for each is one click from the document that describes it -- the same treatment 4c141fd gave the peripheral-init decisions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-on to #41. Adds
docs/BOOT_FLOW.md, closes a latent hang, and correctsan invariant #41 stated wrongly.
The bug
Millis()does not advance untilInitSysTick()runs inBoard::Init(), so(Millis() - start) > timeoutcan never become true before that point.I2CBus's flag wait andUsart::Receiveboth compared against that frozencounter with no guard — a bus held low before the tick would spin forever,
with the timeout silently doing nothing. Both now return
kInvalidState.Unreachable today: both are called only after
Board::Init()has run. Itbecomes reachable the moment anything talks to a device during bring-up, which
is exactly what documenting an off-chip bring-up stage invites — so the guard
and the document belong together.
The design
A peripheral's stage is decided by what can fail, not by what is convenient:
die. Nothing can be absent, nothing can time out. It cannot fail, so it lives
in a constructor — which is what makes an unconfigured peripheral unreachable.
normal outcomes. It needs somewhere that can report one:
Board::Init().That structure already existed; the contract was what was missing.
board::Boardnow states it where a reader meets it, anddocs/BOOT_FLOW.mdcarries the verified reset sequence, the stage table, a decision tree for
placing a new peripheral, and worked examples for an I2C sensor, SPI flash, a
UART-attached radio and an ADC sensor.
The ADC is the case that shows the split is not "sensor or not": the ADC
peripheral is stage 1, the sensor wired to it is stage 2.
Correction to #41
mcu::Delaydoes work before the tick —delay.cppfalls back to aDWT->CYCCNTspin — so #41's "bring-up must not callmcu::Delay" was wrong.The real boundary is narrower and sharper: a stage 1 constructor may wait, but
may not talk, because every real transfer needs a timeout.
Deliberately not done
Both written down so the next step is a decision rather than a drift:
Micros()onDWT->CYCCNT) would maketimeouts work in every stage and dissolve this boundary entirely. Worth doing
when a stage 1 peripheral needs a bounded wait — not before.
PRE_KERNEL_1/POST_KERNEL) wouldreplace the two-phase split. Trigger: two off-chip devices with an ordering
constraint between them. Until then it is a framework guarding a state that
has not occurred.
Known gaps, recorded not solved
common::Errorcarries no payload, soBoard::Init()cannot name whichdevice failed. This lands before stage 2 has real occupants.
3 resource the application brings up.
SysTickfor itself, and will likely revisit this ordering.Verification
nucleo-f767zi-debug/-releasehost-debug/host-releasetools/format.sh --checkBOOT_FLOW.mdCosts 236 bytes of
.textoni2c_demofor the guards. No hardware boot —the guarded paths are unreachable without a device on the bus, so this is
build-and-host evidence only.
🤖 Generated with Claude Code