This project is still in the early design stages, so this page serves both as an intro to the project and as a living design document.
Goal
Currently, the only way to get firmware onto a controller board’s microcontroller is to plug in a USB programmer and connect it to the controller board to be flashed. This works, but if we want to flash multiple boards, it’s very slow, and programmatically updating firmware is very difficult.
Instead, what we’d like to do is write a bootloader which will allow us to replace the firmware on any board on the CAN network by sending the data over CAN. The bootloader will take up the first bit of the MCU’s flash and will take care of responding to any commands, replacing or updating its application code or its config, and jumping to the application firmware.
As an example of the interface, to update the MCI board’s firmware, you might attach your laptop to the CAN bus, then type
make boot-update PROJECT=mci
Or to update all the firmware in the car at once, you might just type
make boot-update-all
There are many other exciting applications of this beyond the primary application. For example, if the x86-side bootloader interface is encapsulated in a Python script, we could put that script on an internet-connected raspberry pi to enable over-the-air updates. We could also write automatic multi-board smoke tests by flashing a smoke test to multiple boards at once, in effect allowing automatic hardware integration tests.
Background
Our controller boards have STM32F072 microcontrollers with 128KB of flash memory (persistent) and 16KB of RAM.
Our current flashing practice is as follows. We use a CMSIS-DAP USB programmer which connects to the controller board via Serial Wire Debug (SWD) pins, which is a protocol with direct access to the MCU’s memory. We use OpenOCD (open on-chip-debug) scripts to overwrite flash through SWD with a binary file, which is created via linking all the object files produced from compilation into an ELF file and then converting that ELF file into a .bin file. This linking process is controlled by a linker script which determines the addresses of each section - text (code), data (global variables), bss (zero-initialized global variables), and others.
We want our existing flashing process to still be usable as a backup - if the bootloader fails, we still need to be able to flash our other projects onto the boards the old-fashioned way. Thus, any changes made to existing projects as part of the bootloader must be backwards-compatible: projects flashed normally must be unaffected.
Prior Art
STM32 devices include bootloaders over various peripherals in ROM: see this application note. Unfortunately, the STM32F072 devices we use only include USB, I2C, and USART bootloaders, not CAN, and we’d have to upgrade to STM32F1xx to use a native CAN bootloader as investigated here:https://uwmidsun.atlassian.net/l/c/avCveAoH . This isn’t really practical since it would be a big pain to change MCUs.
Many non-native CAN bootloaders have been written before. This design takes heavy inspiration from https://github.com/cvra/can-bootloader . A frontend for the native STM32 CAN bootloader is https://github.com/marcinbor85/can-prog . This bootloader tutorial is also useful: https://interrupt.memfault.com/blog/how-to-write-a-bootloader-from-scratch.
Architecture
Addressing controller boards
One major issue is how to address a certain MCU on the CAN network. Unlike the systems for which many other bootloaders were designed, Midnight Sun uses controller boards to hold microcontrollers for each board, rather than having an MCU built into each board. Thus, a certain controller board can’t be guaranteed to stay with one board forever, and we can’t just call one MCU “power_distribution”.
Therefore, the bootloader system should be able to identify a controller board by any of the following:
A numeric ID, set when flashing the bootloader and stored in its config. We can then physically label each controller board with its ID.
(Optional) A human-friendly string name, set when flashing the bootloader and stored in its config. Each controller board can also be labelled with its name. This is optional, but I think “centre console is on Gemini” sounds more fun than “centre console is on controller board 5”.
The name of the current project on the board, as a string. This will be set whenever the bootloader loads a new application project. This way, we can just say “update steering” and whichever controller board currently has the steering project will be updated, rather than having to find which board has steering.
Optionally, additional identifying information set by the application project. For example, we could differentiate between front and rear power distribution this way, and then we could selectively update rear power distribution.
The user should also be able to specify multiple boards to load the same firmware onto (e.g. boards 2,5,7), and if multiple boards are running the same project, we should be able to update all of them by saying something like “update power_distribution”.
Memory Layout and Config
This part is *heavily* inspired by https://github.com/cvra/can-bootloader .
The STM32F072 has 128KB of flash and 16KB of RAM. A flash page is 2KB, and a flash sector is 4KB. (This matters because we can’t just write to flash normally, we have to erase a whole flash page at a time; as well, write protection works per sector.)
We will partition the flash into four sections:
the bootloader code
two identical bootloader config pages
the application code
The two config pages are for redundancy. We will use the persist
module to manage storing the config blob in those pages. A CRC (cyclic redundancy check - a quick hash function) will be stored along with the blob to ensure its integrity; if one page has an invalid blob, we overwrite it with the other one. This way we always have a valid config page, since fixing invalid config would otherwise require manually reflashing the bootloader onto the board.
In the config, we will also store a CRC of the application code. Before jumping to the application code, the bootloader will compute its CRC; if it doesn’t match the config, something must have been corrupted and it will refuse to boot.
A preliminary list of what we might want to store in the config:
config CRC (4 bytes) - CRC32 of the config blob
controller board ID (1 byte?) - numeric ID of the controller board
controller board name (64-byte C-string) - human-friendly name of the controller board
project name (64-byte C-string) - name of the current project, e.g.
power_distribution
project info? (64-byte C-string?) - possible extra string set by the project to differentiate different boards, like
rear
for rear power distrogit version info (32-byte (?) C-string) - commit hash of the branch we flashed from, like
0bdfdd8-dirty
; this is what’s printed by git_version.capplication CRC (4 bytes) - CRC32 of the application code
application size (4 bytes) - needed for the CRC
We might consider write-protecting the bootloader code and possibly the config (when not intentionally writing to it) to prevent the bootloader or application code from overwriting those sections. (See section 3.3.2 of the stm32f0xx manual.)
Linker Scripts
To support this new memory layout, we will need a new linker script for building the bootloader and projects to be loaded via the bootloader. We will maintain a second set of linker scripts for the bootloader and switch to them when building the bootloader / its associated projects.
A consequence of the memory layout is that size requirements for the bootloader are very strict: if the bootloader grows too large, updates in the linker scripts are required and all the controller boards will have to be reflashed.
Operations and CAN Protocol
Like Babydriver, the bootloader will use a command-based architecture.
TODO
Client and API
TODO
Task List
TODO