Skip to content

feat(firmware): CAN bootloader core and the 4 KB app layout (#363) - #376

Merged
mottihoresh merged 3 commits into
mainfrom
feat/363-bootloader-core
Oct 26, 2026
Merged

mottihoresh merged 3 commits into
mainfrom
feat/363-bootloader-core

Conversation

@mottihoresh

Copy link
Copy Markdown
Collaborator

Closes #363.

Every STM32 board now carries our own CAN bootloader in its first 4 KB, and every app starts at 0x08001000. The bootloader starts only an app whose identity block (#362) and CRC check out. Otherwise it waits and takes an image over CAN, writing block 0 last (commit last), so an interrupted update always leaves the board in its bootloader and flashable. It also copies the last valid app's NODE_ID and kind into option bytes Data0/Data1, so a board with a broken app still answers as its node. Decision D26.

What changed

  • Firmware/Bootloader/ (new): bare-metal CMSIS, 2,876 B of 4,096 (the build fails past 4 KB). One image serves both the 64 KB and 128 KB chips; it reads the flash size at run time.
    • Clock: HSE 72 MHz, falling back to HSI/2 × 9 if the crystal doesn't start. CAN runs from a 36 MHz clock either way, so the bit timing holds.
    • CAN by registers; flash erase/program; the hardware CRC unit.
    • Option-byte read-modify-write for the node ID copy.
    • The jump, done in one asm block as the spike required.
  • Protocol: CONNECT → BEGIN → BLOCK + 10×DATA per 64-byte block (CRC-16, acked) → END (image CRC-32, then commit block 0) → GO.
    • BEGIN erases the app's first page, so the old app is invalid from that point.
    • Pages are erased only as blocks reach them, so the PDU calibration page survives.
    • While waiting, the board announces itself every 1 s on BOOT_RSP_n. With no NODE_ID yet it announces on 0x7C1 instead.
  • Firmware/Libraries/BootProtocol/BootProtocol.h (new, header-only): the one source for every CAN ID, command, reply, status, state, the transfer shape and the backup-register entry flag. It uses #346 named values.
  • Firmware/Bootloader/tools/flash_can.py: bench host for a CANable. It reads BootProtocol.h and the image's identity block. --listen, --stop-after and --delay-ms support fault tests.
  • Layout switch: Firmware/shared/stm32.ini now sets board_build.flash_offset = 0x1000. The PDU still ends its app at the calibration page (0x0800FC00).
  • pio run -t bootstrap (in osh_image.py): builds and flashes the bootloader, then the app, for a blank board.
  • Release (firmware-release-assets.yml):
    • The bootloader is built, and its size goes in the summary table.
    • Every STM32 board folder gets <board>-<v>-full.hex: bootloader + app, objcopy -O ihex of both, joined. This file is new to the release; until now it shipped .elf + .uf2 only.
    • The standalone bootloader/bootloader-<v>.elf ships only when its sources changed since the last firmware-v* tag. manifest.json gains bootloader: {protocol, tag}, naming the release that holds it.
    • The validation workflow now also runs on bootloader changes, which enforces the 4 KB budget on PRs.
    • Release READMEs updated: -full.hex for a new board, .elf for a board that already has the bootloader.
  • Spec and docs:
    • New FirmwarePlan/12-bootloader.md and decision D26.
    • FirmwarePlan 02 (the frame table: the boot frames move from reserved to assigned; the free range is now 0x7C2–0x7FF), 08 (layout), 09 (the boot sequence now starts at the bootloader).
    • TechSpec Bootloader + BootProtocol (new) and FirmwareImage.
    • New page docs/firmware/bootloader.md, plus edits to the firmware index, platformio-setup.md, compatibility.md, guides/flashing.md, architecture/can-bus.md, hardware/pdu.md and the nav.
    • CANProtocol's reservation comment now points to BootProtocol.h. release-please bumps BootProtocol/library.json, and the impact map is regenerated.

Bench (Gateway board's bridge STM32 + CANable, 2026-10-25)

Test Result
Release -full.hex onto a mass-erased chip boots the app; Data0/Data1 copied (0 / BRIDGE), RDP untouched
pio run -t bootstrap bootloader + app; boots and relays
Truly blank chip (Data0/Data1 = 0xFF) + bootloader only announces unconfigured on 0x7C1 (a0 01 ff ff 03 40)
First app on that chip Data0/Data1 written from 0xFF → 0 / 3
ST-Link repurpose PDU Left ↔ bridge Data0/Data1 follow: 3/2, then 0/3
App through the bootloader same boot log as before (1.18 s); Gateway relay answers _NODE_STATUS
Entry flag (DR1 = 0xB007) + reset stays, flag cleared; announces REQUESTED
App page 0 erased stays, keeps node 0; announces APP_INVALID
Flash over CAN 32 KB in 3.2 s (9.7 KB/s), 0 retries; GO → app runs and relays
Corrupted image (1 byte flipped) END → BAD_CRC; stays, announces APP_INVALID; good image recovers
20 random ST-Link resets mid-transfer 20/20 end "app OK" or "in bootloader, flashable"; every bootloader case reflashed and ran
3 real power cuts (ST-Link unplugged) at blocks 29, 261 and 280 of 502 3/3 back in the bootloader (node kept, app invalid), reflashed over CAN, app runs

The board was restored to its original image afterwards. Findings about option-byte reload and OpenOCD unlock went into #364.

Notes

@mottihoresh mottihoresh added this to the Firmware v1.0.0 milestone Oct 26, 2026
@mottihoresh mottihoresh added enhancement New feature or request firmware Firmware implementation work priority: medium Wanted and scoped; not blocking today labels Oct 26, 2026
Comment thread Firmware/Bootloader/tools/flash_can.py Fixed
@mottihoresh
mottihoresh merged commit bfbffa1 into main Oct 26, 2026
12 checks passed
@mottihoresh
mottihoresh deleted the feat/363-bootloader-core branch October 26, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request firmware Firmware implementation work priority: medium Wanted and scoped; not blocking today

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(firmware): CAN bootloader — core

1 participant