refactor(arm): configure peripherals in their constructors - #41
Merged
Conversation
GpioPin and I2CBus now bring their hardware up as they are constructed. configured_/initialized_ and the eight kInvalidState guards they gated are gone, NucleoF767ZiBoard's member list is a description of the board rather than a set of promises Init() has to keep, and Init() is left with InitSysTick(), which needs the NVIC and so genuinely cannot run earlier. #37 assumed a constructor could not touch registers, because the board is a namespace-scope object built before the clock tree is up. That does not hold. Reset_Handler copies .data, zeroes .bss and calls SystemInit *before* __libc_init_array; SystemInit only enables the FPU and sets VTOR; no code here configures clocks at all, since the F767 runs on HSI at 16 MHz out of reset, which every timing constant already assumes; and RCC is live from reset, with each driver enabling its own peripheral clock first. Two comments stated that ordering backwards and are corrected here. That makes the fixes #37 proposed unnecessary. A factory with a private constructor is the right answer *given* the no-registers rule, but once the rule goes it guards a state that cannot occur -- and it forces std::optional members, which model a hardware absence that cannot happen on a fixed board. A friend declaration constrains who constructs, not who configures. A readiness gate is vacuous when declaring the member is the bring-up. Three invariants hold this up, documented on NucleoF767ZiBoard: bring-up must not fail, must not call mcu::Delay (SysTick is not up yet), and this board must stay the only object with a dynamic initializer. mcu::Usart violates the first and stays two-phase, which suits it -- only the application knows its UartConfig, and Init() validates it and can fail. Bus speed becomes a constructor argument with a derived TIMINGR table, deliberately undefaulted so a board must name the rate its wiring can carry. Both values are checked against the I2C-bus rev.03 limits rather than copied from ST's spreadsheet; the previous comment credited a table in AN4235 that does not exist, and that document covers F0/F3 rather than F7. arm_cm7 gains its first test. Its public headers name no vendor type, so test_peripheral_contract.cpp compiles on the host whichever backend is selected and asserts that a pin cannot be built without a direction, nor a bus without a speed. It proves the types, not the register writes. HostPin takes its direction at construction to match. HostBoard still builds its peripherals in Init(), which is a real difference and not an oversight: an emulated peripheral depends on a socket that can fail, a real one only on registers that are always there. board::Board and both applications are untouched. Flash drops 100-152 bytes per image and .bss by 4; .init_array still holds exactly crtbegin's entry plus the board. Not yet verified on hardware: blinky, the button, an I2C round trip and uart_echo on /dev/ttyACM0 all still need a physical boot. kFast400kHz is likewise unexercised -- nothing selects it today. Closes #37 Closes #38 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.
Closes #37. Closes #38.
The premise in #37 does not hold
#37 assumed a peripheral constructor could not touch registers, because the
board is a namespace-scope object built before the clock tree is up. Checking
the actual startup path:
startup.srunscopy .data → zero .bss → bl SystemInit → bl __libc_init_array → bl main. Static constructors run after.datais live andSystemInithas returned.
SystemInitis ours and touches onlyCPACRandVTOR. No code in thisproject configures clocks at all — the F767 runs on HSI at 16 MHz out of
reset, which
kTiming100kHzAt16MHzandkSystemCoreClockHzalready assume.first:
ConfigurePincallsEnablePortClock, which setsAHB1ENRand readsit back so the enable lands.
So there is no barrier to be after. Two comments in the tree stated that
ordering backwards and are corrected here.
What that buys
GpioPinandI2CBusconfigure their hardware as they are constructed.configured_/initialized_and the eightkInvalidStateguards they gated aregone.
NucleoF767ZiBoard's member list is now a description of the board ratherthan a set of promises
Init()has to keep, andInit()is left withInitSysTick()— which needs the NVIC and so genuinely cannot run beforemain().This makes every fix #37 proposed unnecessary rather than merely unchosen. A
factory with a private constructor is the right answer given the no-registers
rule, but once that rule goes it guards a state that cannot occur — and it
forces
std::optionalmembers, which model a hardware absence that cannothappen on a soldered-down board. A
frienddeclaration constrains whoconstructs, not who configures. A readiness gate is vacuous when declaring the
member is the bring-up.
Invariants this now rests on
Documented on
NucleoF767ZiBoard, where someone adding a peripheral will meetthem:
mcu::Delay.InitSysTick()runs fromInit(),after
main().within it is declaration order; order across translation units is not.
mcu::Usartviolates the first and stays two-phase — which suits it, since onlythe application knows its
UartConfigandInit()validates it and can fail.Also in here
TIMINGRtable,deliberately undefaulted so a board must name the rate its wiring can carry.
Both values are checked against the I2C-bus rev.03 limits rather than copied
from ST's spreadsheet. The old comment credited a table in AN4235 that does
not exist — that document has four tables, none of them per-frequency, and it
covers F0/F3 rather than F7.
arm_cm7's first test. Its public headers name no vendor type, sotest_peripheral_contract.cppcompiles on the host whichever backend isselected, and asserts that a pin cannot be built without a direction nor a bus
without a speed. It links no
arm_cm7library and no CMSIS. It proves thetypes, not the register writes.
HostPintakes its direction at construction.HostBoardstill builds peripherals in
Init(), which is a real difference and not anoversight: an emulated peripheral depends on a socket that can fail, a real
one only on registers that are always there.
Verification
nucleo-f767zi-debug/-releasehost-debug/host-releasehost_emulator_testtools/format.sh --check,uv run mypyboard.hpp,src/apps/Flash drops 100–152 bytes per image and
.bssby 4..init_arraystill holdsexactly
crtbegin.o's entry plus the board — invariant 3 intact.The contract test was checked negatively: reintroducing a two-argument
GpioPinconstructor fails the host build on the
static_assert.Not verified
uart_echoon/dev/ttyACM0all still need a physical board.kFast400kHzis unexercised. The arithmetic clears the fast-mode minimawith margin, but nothing in the tree selects it and it has never been on a
scope. Reasonable to drop it until something needs it.
🤖 Generated with Claude Code