This document defines the contract of Inc/ST-LIB.hpp, especially the
ST_LIB::BuildCtx and ST_LIB::Board machinery.
If you change a domain, add a new domain, or add a cross-domain composition rule, read this first.
Board<Policy, ...> is a compile-time build pipeline plus a runtime init pipeline.
- Compile time decides what exists and how it must be configured.
- Runtime only materializes already-built configurations and links HAL handles.
Board is intentionally declarative. Request objects describe intent; domains convert that intent
into concrete configs. The first template argument is not a request object: it is the global fault
runtime policy type used by FaultController.
Protection declarations are also request objects. They do not inscribe hardware into BuildCtx;
instead, Board filters them into a board-specific ProtectionEngine type.
The first template argument of Board must be a fault policy type.
That type must expose:
static constexpr bool has_operational_machinestatic constexpr Callback on_fault_enterstatic constexpr auto& operational_machinewhenhas_operational_machine == true
FaultPolicy<...>, FaultPolicyNoMachine<...>, and DefaultFaultPolicy are the intended public
helpers for this contract.
Every domain used by BuildCtx and Board must provide:
static constexpr std::size_t max_instancesstruct Entrystruct Configtemplate <size_t N> static consteval std::array<Config, N> build(std::span<const Entry>)template <std::size_t N> struct Init
Init<N> must provide:
static inline std::array<Instance, N> instancesstatic void init(std::span<const Config, N> ...)
The exact runtime dependencies of init(...) are domain-specific, but they must be explicit in the
signature.
A request object that can be used after the first Policy argument inside Board<Policy, ...> must
provide:
template <class Ctx> consteval ... inscribe(Ctx&) const
or any compatible return type if it naturally inscribes multiple dependent entries.
inscribe(ctx) must:
- be
consteval - append the domain entries needed by the request
- return indices only for later compile-time wiring
- never depend on runtime state
Hardware requests also expose using domain = <DomainType>; for Board::instance_of<request>().
Protection requests intentionally do not expose a hardware domain and are accessed through
Board::ProtectionEngine.
BuildCtx is append-only.
Important consequences:
BuildCtx::add(...)does not deduplicate.- If two requests emit the same physical resource twice,
BuildCtxwill keep both. - Preventing duplicates is the responsibility of request objects or the destination domain build logic.
This is a deliberate design choice. BuildCtx is a storage and ownership map, not a resolver.
Board::build_ctx():
- creates a
DomainsCtx - evaluates every request object's
inscribe(ctx)in declaration order - does not inspect or route the
PolicythroughBuildCtx
Board::build():
- computes domain sizes
- runs each domain
build<N>(...) - assembles a
ConfigBundle
Board::cfg is the compile-time result of that process.
Board::ProtectionEngine is the compile-time protection engine assembled from the protection
request objects in the same Board<Policy, ...> declaration.
No runtime-only configuration logic belongs here.
Board::init():
- must only consume
cfg - must initialize domains in dependency order
- must not invent new hardware resources
- must not re-resolve compile-time relationships
Permitted runtime work:
- HAL init
- clock enable
- IRQ enable
- handle linking
- initializing the board-specific protection engine
- starting the global fault runtime
- buffer allocation if the buffer is inherently runtime memory
- starting peripherals using already-built configs
Forbidden runtime work:
- choosing a DMA request dynamically
- choosing a stream dynamically
- changing a domain topology
- creating extra domain entries not represented in
cfg
Board::instance_of<request>() relies on owner pointers captured during BuildCtx::add(...).
Therefore:
- each stored
Entrymust be associated with the request object that owns it - owner identity must remain stable across compilation
instance_ofis only valid for request objects actually inscribed intoBoard
There are two valid ways for a domain to use DMADomain.
Use this when one request object maps directly to one or more DMA resources.
Example: SPI.
The request object may hold DMADomain::DMA<...> and inscribe it directly.
Use this when multiple request objects share one physical DMA resource and direct inscription would duplicate it.
Example: ADC, where many channel requests can belong to one ADC peripheral and one DMA stream.
In this case, the domain must expose compile-time contribution helpers:
*_contribution_count(...)build_*_contributions<...>(...)
Those helpers must:
- be
consteval - be additive only
- preserve all pre-existing
DMADomainentries - synthesize only the missing DMA entries for that domain
Board may then merge base DMA entries with these contributions through generic helpers such as
BuildUtils::merge_dma_entries(...) and BuildUtils::build_dma_configs(...).
ADCDomain currently uses the DMA contribution pattern.
Reason:
- request granularity is one ADC channel request
- physical DMA granularity is one DMA stream per ADC peripheral
If ADC ever changes to a request type that directly represents a full ADC peripheral, it could move to the direct inscription pattern used by SPI.
If the domain has a 1:1 request-to-DMA mapping:
- Inscribe
DMADomain::DMA<...>directly from the request object. - Store DMA indices in the domain
Entry. - Use those indices during domain
build/init.
If the domain has a many:1 request-to-DMA mapping:
- Keep the domain request objects focused on their logical resource.
- Build the domain configs first.
- Expose compile-time DMA contribution helpers for the missing shared DMA entries.
- Merge those contributions into the base
DMADomainentries inBoard::build().
- Topology is compile-time.
BuildCtxis append-only and non-deduplicating.cfgis the full source of truth for runtime init.- Runtime init may materialize resources, but not invent topology.
- Cross-domain composition must be explicit and compile-time.
When reviewing a change to Board, a domain, or a DMA-using peripheral, verify:
- Does the request object inscribe only compile-time information?
- Can duplicate physical resources be produced? If yes, where are they prevented?
- Does runtime init consume only
cfg? - Are protection requests passed through the board and evaluated through
Board::ProtectionEngineorBoard::evaluate_protections()? - Is any stream/request/allocation decision being made too late?
- If the domain uses shared DMA, is it contributing only missing entries and preserving the rest?