The cheapest of bootloaders for the cheapest of STM32 boards!
If you are shopping for the cheapest STM32 that can do CAN, you might just end up with the STM32F103C8T6 chip, which is also prominently featured on the famous Blue Pill boards. Once you start developing the firmware, you might quickly find that 64kB of flash code space is not that much. Then you realize that you still need a bootloader for firmware update, so you search around / try to put something with an LLM, and end up with a quarter of the flash reserved for the bootloader.
How about this instead: a CAN bootloader that only takes up 2kB of flash, and is effortlessly integrating into your zephyr RTOS application?
- 2 kB flash footprint; rest is available for the application.
- Simple CAN protocol using Bluetooth ACS packet segmentation scheme.
- Default CAN IDs: prefix
0x7E5(updater:prefix+0, loader:prefix+1); standard frames by default (IDE=0). - Application integrity ensured by flashing MSP and reset vector after all payload pages are written.
- Zephyr module with full sysbuild integration, see app example (try
image updateshell command).
- Bootloader: 0x0800_0000 – 0x0800_07FF (2 kB)
- Application slot: 0x0800_0800 – 0x0800_FFFF (62 kB)
Defined in zephyr/bluepill_partitions.overlay.
- IDs:
updater_id = CONFIG_BLUEPILL_CAN_ID_PREFIX + 0,loader_id = CONFIG_BLUEPILL_CAN_ID_PREFIX + 1. - Info frame (sent periodically by loader, DLC=8):
- Byte0: error and app present flags.
- Byte1: reserved.
- Bytes2-3: app flash size in kB.
- Bytes4-5: transfer chunk size (1 kB default).
- Bytes6-7: current chunk index.
- Payload segmentation (ACS-like): each PDU prepends a 1-byte header: bit0
first, bit1last, bits2-7 rollingcounter. Data follows the header. - Transfer rules:
- Chunk size equals the flash page (1 kB). Chunk 0 begins with the application header (MSP + reset vector) but those two words are written last once all chunks arrive intact.
- A zero-length CAN frame restarts the transfer (chunk index reset to 0).
- Short final chunk terminates the transfer; the loader then programs the first two words and boots the new image if valid.
- Error flags:
0x01invalid update (bad header/overflow),0x02flash failure,0x04RX timeout.
Requirements: CMake ≥3.22, gcc-arm-none-eabi, and a Ninja/Make generator. Provide the CAN bitrate via CONFIG_CAN_BITRATE (in bit/s).
cmake --preset=bluepill
cmake --build build/bluepill
# Output: build/bluepill/bluepill-can-bl.elfThe repository is a Zephyr module (zephyr/module.yml). The app/ directory is a minimal client showing how to request an update via the shell. Sysbuild pulls in and builds the bootloader automatically.
Example inside a Zephyr workspace with this module present:
west build -b stm32_min_dev@blue --sysbuild -d app/build appNotes:
- Sysbuild options live in app/sysbuild.conf (e.g.,
SB_CONFIG_BLUEPILL_CAN_BUS_BITRATE). - The sample enables shell and adds
image update(app/src/main.cpp) which sets the update request flag and reboots. - Partitioning comes from zephyr/bluepill_partitions.overlay.
When building a non-zephyr application, two things have to be adapted for bootloader support (this is true in general):
- Modify the linker script to ensure the application's flash area starts where the bootloader's ends.
- Write the flash start address to the VTOR register (update vector table address).
- Application sets the update request flag (see
can_bl::set_update_request()or runimage updatein the sample shell), then resets. - Bootloader stays if no valid app or an update was requested; otherwise it jumps to the app.
- Loader advertises status via the periodic info frame; updater streams segmented CAN PDUs in 1 kB chunks.
- Loader erases/writes each flash page; on the last (short) chunk it writes the MSP/reset vector and boots the new image if valid.
CONFIG_CAN_BITRATE: CAN bus bitrate (required when building standalone).CONFIG_BLUEPILL_CAN_ID_PREFIX: CAN ID base (default 0x7E5), defined in the interface target.CONFIG_BLUEPILL_CAN_IDE: 0 for standard, 1 for extended CAN IDs.
A host-side CAN sender is not included; any CAN tool that can emit the framed/segmented PDUs will work. Follow the protocol above to push 1 kB pages (chunk 0 starts with the app header, but those first two words are flashed last by the loader).
The bootloader defers programming the first two words (MSP and reset vector) until after all other pages succeed. This prevents the boot vector from pointing to incomplete images.