CAN-based bootloader for embassy-based firmwares, powered by embassy_boot. Power-fail-safe A/B updates with arbitrary health checks and auto-rollback.
Most of the heavy lifting is done by embassy_boot. That crate takes care of all of the hard stuff, like the actual flash handling, partition swapping, etc. This project adds a simple CAN protocol for using it, focussing on making including this capability in an application firmware as simple and painless as possible.
embassy_boot requires a flash partitioning scheme as follows (values are from STM32F105 example):
0x00000000 +----------------------+
| BOOTLOADER 24 KB |
0x00006000 +----------------------+
| STATE 2 KB |
0x00006800 +----------------------+
| ACTIVE 114 KB | <- your firmware lives here
0x00023000 +----------------------+
| DFU 116 KB |
0x00040000 +----------------------+
Firmware updates are streamed to the board while its regular firmware is running (out of ACTIVE), stored in the DFU partition. Only if the complete firmware has been written and CRC-verified, is the board actually rebooted. The bootloader will swap ACTIVE and DFU.
After the new firmware has started executing, it is responsible for calling a method that marks it as correctly booted. If the board resets before that state is reached (broken clock config, panic during boot, watchdog firing, etc.), the bootloader will rollback.
Using state stored in STATE, and exposed by cancan at runtime, the application firmware can tell in which of these states it is running (e.g. if the current firmware has been booted into as a result of a rollback from an attempted update).
Firmware is transmitted to the board via CAN using ISO-TP, reserving two CAN IDs (one for each direction), by default 0x7C0 and 0x7C1. Since flashing is intended to happen in situ, while the board itself and others are using the bus, you will have to ensure that these do not collide. You can change these IDs to whatever you want.
Nodes on the bus are addressed using a single byte, meaning that 256 boards can share a bus. The mechanism to assign each board a unique ID is out of scope, that's your problem.
Using cancan-build, a timestamp and pseudorandom id are embedded in the firmware binary during each build. This allows querying the last time a board was flashed using the CLI and it also allows correctly detecting if the newly flashed firmware is actually the one running. This breaks reproducibility, with every build producing a new build id. You can force constant values for build id and timestamp using environment variables. See that crate.
The cancan CLI is installed on the host (Linux) system, and uses a SocketCAN interface to perform the flashing. To install it:
$ cargo install --path cancan-cli
This repo features two examples, for STM32F105 and H743-based boards. A rough outline of the steps necessary to add CAN-based flashing to a firmware currently using something like probe-rs:
- Ensure your firmware is small enough that half your flash is enough.
- Disable any
memory-xEmbassy feature you are currently using. You will need to provide your ownmemory.xfor the layout including theDFUpartition. See examples. - Add a second, bootloader binary/crate to your project, mirroring the bootloader in the example. This one also needs a correct
memory.x(consider theINCLUDEmechanism as shown in the example to keep the layout in sync between app and bootloader). Flash this once using probe-rs. - Figure out a way to assign your board a node ID. Hardcode it, read it from an env var during build, whatever.
- Add
cancan-buildas a build dependency, callcancan_build::emitfrom your firmware's build script and include the generated metadata from your binary. - Build a
CanCanConfigin your firmware and run theCanCanservice next to your firmware, after callingCanCan::confirmto mark the running firmware as healthy. - Wire up
CanCanRxandCanCanTxto ensure update packets reachcancanand the responses reach the host.
See the examples for details.
To flash a board for the first time, first flash the bootloader using a runner like probe-rs:
$ cd examples/stm32f105/bootloader
$ cargo run --release
If this is the first time flashing this bootloader, getting errors after flashing is expected. Then, flash the application firmware once using probe-rs as well (ensure the correct runner is set in .cargo/config.toml).:
$ cd ../app
$ cargo run --release
From now on, you can flash new versions using cancan either manually:
$ cargo build --release
$ cancan flash 0x01 target/thumbv7m-none-eabi/release/cancan-example-stm32f105
... or by setting cancan to be the default runner using the firmware's .cargo/config.toml:
# .cargo/config.toml
[target.thumbv7m-none-eabi]
runner = "cancan flash 0x01"
Then you can just run cargo run [--release].
To query a single board (firmware name, chip, build id/timestamp, boot state, uptime):
$ cancan info 0x01
To discover which node ids are live on the bus, probe all 256 and list the boards that answer:
$ cancan scan
- There is no cryptography or firmware signing, firmware is only CRCed, anyone on the bus can flash your board.
- The MCU may lock up during active flash writes and real-time tasks may be disrupted. Obviously don't flash boards while they are doing something critical.