Firmware Build
Server habits break devices. No heap, bounded interrupt handlers, every wait has a
timeout, and nothing is verified without a number.
Standing reference: references/firmware-constraints.md.
Before writing code
- Read the datasheet section and the errata. Cite the section in a comment where the
required register order is not obvious. An erratum found after two days of debugging is
two days lost.
- Write the memory budget. RAM, stack per task, flash, and which section each large
object lands in. Check the map file rather than assuming; a
const table that lands in
RAM instead of flash is a common and expensive surprise.
- Draw the HAL boundary. Everything above it is host-testable logic. Everything below
it touches registers. Logic that reads a register directly cannot be tested off-target,
and off-target tests are orders of magnitude faster.
While writing
- No dynamic allocation after initialisation. Static pools and fixed-size buffers.
Fragmentation is unrecoverable on a device that runs for months.
- Interrupt handlers do four things: acknowledge the source, move data, signal, return.
No blocking, no allocation, no logging, no long loop, no floating point unless the
platform saves the FPU context.
- Guard shared state properly.
volatile stops the compiler caching a value. It is not
atomicity and not a memory barrier. Multi-byte state shared with an interrupt needs a
critical section, kept as short as possible.
- Bound every wait. Every peripheral operation gets a timeout and a defined failure
behavior. A bus that never asserts ready must not hang the device.
- Prefer a state machine over a task per feature. Fewer stacks, fewer races.
- Handle rollover. Tick arithmetic uses unsigned subtraction so a counter wrap behaves
correctly.
- Feed the watchdog from progress, not from a timer interrupt. A watchdog fed by a
timer keeps a wedged application alive, which defeats the purpose.
Testing
- Host-test all logic through the HAL seam with a fake peripheral layer, using
tdd-loop.
- On-target tests cover timing, interrupt behavior, and peripheral quirks only.
- Assert stack high-water mark and heap usage in the suite, not by eye.
- Build and test at the shipping optimisation level. A bug that disappears at
-O0 is a
timing or volatile bug, and its disappearance is the diagnosis.
On-target verification
Run before claiming the change works:
- **Build:** <toolchain, version, optimisation level>
- **Flash:** <used> / <total> (delta <n>)
- **RAM:** <used> / <total> (delta <n>)
- **Stack high-water:** <per task, with margin>
- **Worst-case ISR duration:** <measured with a pin toggle or cycle counter>
- **Host tests:** <quoted summary>
- **On-target tests:** <what ran on hardware, quoted>
- **Power:** <current per state, if battery powered>
- **Soak:** <duration, result>
A change that reports no numbers has not been verified. Prefer a pin toggle and a logic
analyser over print statements: printing changes timing and hides the bug you are chasing.
If the device does not boot, the peripheral never responds, or the fault is below the
register level, that is board-bringup-engineer, not more application debugging.
Update path
- Firmware update is atomic with a rollback slot. An update that can brick the device on
power loss is not shippable.
- The build embeds a version and a commit readable from the device.
- Images are signed if the device is reachable by anyone but you.
- Brownout behavior is defined for anything that writes to non-volatile storage. A
half-written record is a corrupted device.
Red flags
| Thought |
Reality |
| "One malloc at startup, so a bit more is fine" |
Fragmentation is unrecoverable over months. Static pools. |
| "I'll print from the ISR to debug" |
Printing changes timing and hides the bug. Toggle a pin. |
| "volatile makes it safe" |
It prevents caching. Not atomicity, not ordering. |
| "The stack is probably fine" |
Measure the high-water mark. Probably is how devices fault in the field. |
| "It works at -O0" |
That is the diagnosis, not a workaround. |
| "I'll retry until it responds" |
Bounded, with a timeout and a defined failure. Unbounded retries hang the device. |
| "It ran for ten minutes on the bench" |
Soak it. These bugs appear over hours. |
1---2name: firmware-build3description: Firmware Build4---56# Firmware Build78Server habits break devices. No heap, bounded interrupt handlers, every wait has a9timeout, and nothing is verified without a number.1011Standing reference: `references/firmware-constraints.md`.1213## Before writing code14151. **Read the datasheet section and the errata.** Cite the section in a comment where the16 required register order is not obvious. An erratum found after two days of debugging is17 two days lost.182. **Write the memory budget.** RAM, stack per task, flash, and which section each large19 object lands in. Check the map file rather than assuming; a `const` table that lands in20 RAM instead of flash is a common and expensive surprise.213. **Draw the HAL boundary.** Everything above it is host-testable logic. Everything below22 it touches registers. Logic that reads a register directly cannot be tested off-target,23 and off-target tests are orders of magnitude faster.2425## While writing2627- **No dynamic allocation after initialisation.** Static pools and fixed-size buffers.28 Fragmentation is unrecoverable on a device that runs for months.29- **Interrupt handlers do four things:** acknowledge the source, move data, signal, return.30 No blocking, no allocation, no logging, no long loop, no floating point unless the31 platform saves the FPU context.32- **Guard shared state properly.** `volatile` stops the compiler caching a value. It is not33 atomicity and not a memory barrier. Multi-byte state shared with an interrupt needs a34 critical section, kept as short as possible.35- **Bound every wait.** Every peripheral operation gets a timeout and a defined failure36 behavior. A bus that never asserts ready must not hang the device.37- **Prefer a state machine over a task per feature.** Fewer stacks, fewer races.38- **Handle rollover.** Tick arithmetic uses unsigned subtraction so a counter wrap behaves39 correctly.40- **Feed the watchdog from progress,** not from a timer interrupt. A watchdog fed by a41 timer keeps a wedged application alive, which defeats the purpose.4243## Testing4445- Host-test all logic through the HAL seam with a fake peripheral layer, using `tdd-loop`.46- On-target tests cover timing, interrupt behavior, and peripheral quirks only.47- Assert stack high-water mark and heap usage in the suite, not by eye.48- Build and test at the shipping optimisation level. A bug that disappears at `-O0` is a49 timing or `volatile` bug, and its disappearance is the diagnosis.5051## On-target verification5253Run before claiming the change works:5455```markdown56- **Build:** <toolchain, version, optimisation level>57- **Flash:** <used> / <total> (delta <n>)58- **RAM:** <used> / <total> (delta <n>)59- **Stack high-water:** <per task, with margin>60- **Worst-case ISR duration:** <measured with a pin toggle or cycle counter>61- **Host tests:** <quoted summary>62- **On-target tests:** <what ran on hardware, quoted>63- **Power:** <current per state, if battery powered>64- **Soak:** <duration, result>65```6667A change that reports no numbers has not been verified. Prefer a pin toggle and a logic68analyser over print statements: printing changes timing and hides the bug you are chasing.6970If the device does not boot, the peripheral never responds, or the fault is below the71register level, that is `board-bringup-engineer`, not more application debugging.7273## Update path7475- Firmware update is atomic with a rollback slot. An update that can brick the device on76 power loss is not shippable.77- The build embeds a version and a commit readable from the device.78- Images are signed if the device is reachable by anyone but you.79- Brownout behavior is defined for anything that writes to non-volatile storage. A80 half-written record is a corrupted device.8182## Red flags8384| Thought | Reality |85|---------|---------|86| "One malloc at startup, so a bit more is fine" | Fragmentation is unrecoverable over months. Static pools. |87| "I'll print from the ISR to debug" | Printing changes timing and hides the bug. Toggle a pin. |88| "volatile makes it safe" | It prevents caching. Not atomicity, not ordering. |89| "The stack is probably fine" | Measure the high-water mark. Probably is how devices fault in the field. |90| "It works at -O0" | That is the diagnosis, not a workaround. |91| "I'll retry until it responds" | Bounded, with a timeout and a defined failure. Unbounded retries hang the device. |92| "It ran for ten minutes on the bench" | Soak it. These bugs appear over hours. |