Skip to content

Commit 4654c91

Browse files
committed
Document the ST-LIB board contract
1 parent c9f3df2 commit 4654c91

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

docs/st-lib-board-contract.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# ST-LIB Board Contract
2+
3+
This document defines the contract of [`Inc/ST-LIB.hpp`](../Inc/ST-LIB.hpp), especially the
4+
`ST_LIB::BuildCtx` and `ST_LIB::Board` machinery.
5+
6+
If you change a domain, add a new domain, or add a cross-domain composition rule, read this first.
7+
8+
## 1. Mental Model
9+
10+
`Board<...>` is a compile-time build pipeline plus a runtime init pipeline.
11+
12+
- Compile time decides what exists and how it must be configured.
13+
- Runtime only materializes already-built configurations and links HAL handles.
14+
15+
`Board` is intentionally declarative. Request objects describe intent; domains convert that intent
16+
into concrete configs.
17+
18+
## 2. Domain Contract
19+
20+
Every domain used by `BuildCtx` and `Board` must provide:
21+
22+
- `static constexpr std::size_t max_instances`
23+
- `struct Entry`
24+
- `struct Config`
25+
- `template <size_t N> static consteval std::array<Config, N> build(std::span<const Entry>)`
26+
- `template <std::size_t N> struct Init`
27+
28+
`Init<N>` must provide:
29+
30+
- `static inline std::array<Instance, N> instances`
31+
- `static void init(std::span<const Config, N> ...)`
32+
33+
The exact runtime dependencies of `init(...)` are domain-specific, but they must be explicit in the
34+
signature.
35+
36+
## 3. Request Object Contract
37+
38+
A request object that can be used inside `Board<...>` must provide:
39+
40+
- `using domain = <DomainType>;`
41+
- `template <class Ctx> consteval std::size_t inscribe(Ctx&) const`
42+
43+
or any compatible return type if it naturally inscribes multiple dependent entries.
44+
45+
`inscribe(ctx)` must:
46+
47+
- be `consteval`
48+
- append the domain entries needed by the request
49+
- return indices only for later compile-time wiring
50+
- never depend on runtime state
51+
52+
## 4. BuildCtx Contract
53+
54+
`BuildCtx` is append-only.
55+
56+
Important consequences:
57+
58+
- `BuildCtx::add(...)` does not deduplicate.
59+
- If two requests emit the same physical resource twice, `BuildCtx` will keep both.
60+
- Preventing duplicates is the responsibility of request objects or the destination domain build
61+
logic.
62+
63+
This is a deliberate design choice. `BuildCtx` is a storage and ownership map, not a resolver.
64+
65+
## 5. Board Build Contract
66+
67+
`Board::build_ctx()`:
68+
69+
- creates a `DomainsCtx`
70+
- evaluates every request object's `inscribe(ctx)` in declaration order
71+
72+
`Board::build()`:
73+
74+
- computes domain sizes
75+
- runs each domain `build<N>(...)`
76+
- assembles a `ConfigBundle`
77+
78+
`Board::cfg` is the compile-time result of that process.
79+
80+
No runtime-only configuration logic belongs here.
81+
82+
## 6. Board Init Contract
83+
84+
`Board::init()`:
85+
86+
- must only consume `cfg`
87+
- must initialize domains in dependency order
88+
- must not invent new hardware resources
89+
- must not re-resolve compile-time relationships
90+
91+
Permitted runtime work:
92+
93+
- HAL init
94+
- clock enable
95+
- IRQ enable
96+
- handle linking
97+
- buffer allocation if the buffer is inherently runtime memory
98+
- starting peripherals using already-built configs
99+
100+
Forbidden runtime work:
101+
102+
- choosing a DMA request dynamically
103+
- choosing a stream dynamically
104+
- changing a domain topology
105+
- creating extra domain entries not represented in `cfg`
106+
107+
## 7. Owner Mapping Contract
108+
109+
`Board::instance_of<request>()` relies on owner pointers captured during `BuildCtx::add(...)`.
110+
111+
Therefore:
112+
113+
- each stored `Entry` must be associated with the request object that owns it
114+
- owner identity must remain stable across compilation
115+
- `instance_of` is only valid for request objects actually inscribed into `Board`
116+
117+
## 8. DMA Composition Contract
118+
119+
There are two valid ways for a domain to use `DMADomain`.
120+
121+
### 8.1 Direct inscription
122+
123+
Use this when one request object maps directly to one or more DMA resources.
124+
125+
Example: SPI.
126+
127+
The request object may hold `DMADomain::DMA<...>` and inscribe it directly.
128+
129+
### 8.2 Compile-time DMA contributions
130+
131+
Use this when multiple request objects share one physical DMA resource and direct inscription would
132+
duplicate it.
133+
134+
Example: ADC, where many channel requests can belong to one ADC peripheral and one DMA stream.
135+
136+
In this case, the domain must expose compile-time contribution helpers:
137+
138+
- `*_contribution_count(...)`
139+
- `build_*_contributions<...>(...)`
140+
141+
Those helpers must:
142+
143+
- be `consteval`
144+
- be additive only
145+
- preserve all pre-existing `DMADomain` entries
146+
- synthesize only the missing DMA entries for that domain
147+
148+
`Board` may then merge base DMA entries with these contributions through generic helpers such as
149+
`BuildUtils::merge_dma_entries(...)` and `BuildUtils::build_dma_configs(...)`.
150+
151+
## 9. ADC-Specific Rule
152+
153+
`ADCDomain` currently uses the DMA contribution pattern.
154+
155+
Reason:
156+
157+
- request granularity is one ADC channel request
158+
- physical DMA granularity is one DMA stream per ADC peripheral
159+
160+
If ADC ever changes to a request type that directly represents a full ADC peripheral, it could move
161+
to the direct inscription pattern used by SPI.
162+
163+
## 10. How To Add a New DMA-Using Domain
164+
165+
If the domain has a 1:1 request-to-DMA mapping:
166+
167+
1. Inscribe `DMADomain::DMA<...>` directly from the request object.
168+
2. Store DMA indices in the domain `Entry`.
169+
3. Use those indices during domain `build/init`.
170+
171+
If the domain has a many:1 request-to-DMA mapping:
172+
173+
1. Keep the domain request objects focused on their logical resource.
174+
2. Build the domain configs first.
175+
3. Expose compile-time DMA contribution helpers for the missing shared DMA entries.
176+
4. Merge those contributions into the base `DMADomain` entries in `Board::build()`.
177+
178+
## 11. Non-Negotiable Invariants
179+
180+
- Topology is compile-time.
181+
- `BuildCtx` is append-only and non-deduplicating.
182+
- `cfg` is the full source of truth for runtime init.
183+
- Runtime init may materialize resources, but not invent topology.
184+
- Cross-domain composition must be explicit and compile-time.
185+
186+
## 12. Practical Review Checklist
187+
188+
When reviewing a change to `Board`, a domain, or a DMA-using peripheral, verify:
189+
190+
- Does the request object inscribe only compile-time information?
191+
- Can duplicate physical resources be produced? If yes, where are they prevented?
192+
- Does runtime init consume only `cfg`?
193+
- Is any stream/request/allocation decision being made too late?
194+
- If the domain uses shared DMA, is it contributing only missing entries and preserving the rest?

0 commit comments

Comments
 (0)