CAN-bus firmware update system for STM32G4 microcontrollers.
Update firmware on STM32G4 CAN nodes in the field using a standard USB-CAN adapter -- no SWD access needed after initial deployment. Flash the bootloader once, then push updates over the CAN bus forever.
USB-CAN adapter
PC βββββββββββββ βββββββββββ βββββββββββ
can_flasher β βββββ‘ Node 1 ββββββ€ Node 2 β ...
(Python CLI) β βββββββββββ βββββββββββ
250 kbps CAN bus
- The Python tool discovers nodes on the CAN bus
- Sends the target node into DFU (Device Firmware Update) mode
- Streams the new firmware in 1024-byte blocks with CRC verification
- Verifies the full image CRC on the MCU, then activates and reboots
Transfer speed: ~5.5 KB/s at 250 kbps.
| Directory | What it is |
|---|---|
bootloader/ |
Standalone 16 KB bootloader firmware (C, Makefile) |
can_bootloader_lib/ |
Reusable C library -- drop into any STM32G4 project |
can_flasher/ |
Python CLI host tool for firmware updates |
drivers/ |
STM32G4 HAL and CMSIS vendor SDK |
tools/ |
CAN bus monitor/sniffer with CANopen decoding |
0x08000000 ββββββββββββββββββββββββ
β Bootloader (16 KB) β Fixed -- flashed via SWD once
0x08004000 ββββββββββββββββββββββββ€
β Application (240 KB)β Updated over CAN bus
0x0803FFFF ββββββββββββββββββββββββ€
β Reserved (254 KB) β For future use
0x0807F800 ββββββββββββββββββββββββ€
β Metadata (2 KB) β Active slot, CRCs, versions
0x0807FFFF ββββββββββββββββββββββββ
- ARM toolchain:
arm-none-eabi-gcc(download from ARM, orsudo apt install gcc-arm-none-eabion Ubuntu/Debian) - Python 3.8+ with
pip(Windows, Linux, or macOS) - USB-CAN adapter: IXXAT or Kvaser USB-to-CAN (with vendor driver installed)
- STM32 programmer: ST-Link or J-Link (one-time initial flash only)
cd bootloader
makeOutput: build/bootloader.bin (~10 KB). If the toolchain isn't on PATH:
make PREFIX=/path/to/arm-none-eabi-Your application needs three things to work with the bootloader:
- Link at
0x08004000-- use the reference linker script incan_bootloader_lib/STM32G491KEUX_APP.ld - Relocate the vector table -- add
SCB->VTOR = 0x08004000;early inmain() - Integrate the CAN update library -- see
can_bootloader_lib/README.mdfor the complete step-by-step guide
After building, generate a .bin file:
arm-none-eabi-objcopy -O binary your_app.elf your_app.binSTM32_Programmer_CLI -c port=SWD -e all
STM32_Programmer_CLI -c port=SWD -w bootloader/build/bootloader.bin 0x08000000
STM32_Programmer_CLI -c port=SWD -w your_app.bin 0x08004000On first boot with no metadata, the bootloader enters DFU mode automatically.
cd can_flasher
pip install -r requirements.txt
python -m can_flasherThe tool is fully interactive -- it will:
- Detect your adapter -- auto-probes IXXAT and Kvaser USB-CAN interfaces
- Scan the bus -- lists all nodes with their ID, firmware version, and state
- Select a target -- pick which node to update
- Select a file -- enter the path to your
.binfile (supports.hextoo) - Transfer -- streams firmware with a progress bar and per-block CRC checks
- Verify -- triggers a full-image CRC-32 check on the MCU
- Activate -- writes metadata and reboots the node into the new firmware
No command-line flags needed -- the tool prompts for everything. Just run it and follow the prompts.
| Parameter | Value |
|---|---|
| MCU | STM32G491KEU6 (Cortex-M4F, 160 MHz, 512 KB flash, 112 KB SRAM) |
| CAN | FDCAN1 on PA11 (RX) / PA12 (TX), 250 kbps |
| Clock | 24 MHz HSE, PLL to 160 MHz |
| LEDs | PA6 (Status), PA7 (Heartbeat) |
| CAN termination | PB0 (120 ohm via ADG801BRTZ, GPIO-controlled) |
Standard 11-bit CAN IDs 0x7E0--0x7E3, chosen to avoid CANopen and J1939 conflicts.
| CAN ID | Direction | Purpose |
|---|---|---|
0x7E0 |
Host -> Node | Commands |
0x7E1 |
Node -> Host | Responses |
0x7E2 |
Host -> Node | Firmware data (6 bytes/frame) |
0x7E3 |
Node -> Host | Data ACK |
Frame format:
- Commands:
[cmd_code, target_node_id, payload...] - Responses:
[source_node_id, response_code, payload...] - Data:
[seq_lo, seq_hi, byte0..byte5] - Broadcast target:
0xFF
Host Node
| |
|--- SCAN_REQUEST (0x01) -------->|
|<--- SCAN_RESPONSE (0x01) -------|
| |
|--- ENTER_DFU (0x02) ----------->|
|<--- DFU_READY (0x02) -----------| Node reboots into bootloader
| |
|--- FW_HEADER (0x03) ----------->| Size + CRC upper 16 bits
|--- FW_HEADER_EXT (0x04) ------->| CRC lower 16 bits + version
|<--- FW_HEADER_ACK (0x03) -------|
| |
|--- FW_DATA frames ------------->| 1024-byte blocks, 171 frames each
|--- FW_BLOCK_DONE (0x11) ------->| Block number + CRC-32
|<--- FW_BLOCK_ACK (0x11) --------| Per-block verification
| ... repeat ... |
| |
|--- FW_VERIFY (0x20) ----------->|
|<--- FW_VERIFY_RESULT (0x20) ----| Full-image CRC-32 check
| |
|--- FW_ACTIVATE (0x21) --------->|
|<--- FW_ACTIVATE_ACK (0x21) -----| Metadata updated, node reboots
| |
Three ways to enter DFU mode:
- CAN command -- send
ENTER_DFUto the running application (normal path) - Boot window -- bootloader listens on CAN for 100 ms on every boot
- No valid app -- missing or corrupt metadata triggers DFU automatically
See can_bootloader_lib/README.md for the full integration guide.
In short:
- Copy
can_bootloader.candcan_bootloader.hinto your project - Add the FDCAN RX filter for IDs
0x7E0--0x7E3 - Call
can_bootloader_init()after FDCAN starts - Call
can_bootloader_process()in your main loop - Link at
0x08004000(see reference linker script incan_bootloader_lib/)
Standalone bus sniffer with CANopen frame decoding:
python tools/can_monitor.py --interface kvaser --bitrate 250000Supports --interface ixxat or --interface kvaser, and --fd for CAN-FD.
AxxCanFlash/
βββ bootloader/ Standalone 16 KB bootloader
β βββ Makefile Build with arm-none-eabi-gcc
β βββ Src/main.c Boot logic, CAN, flash, clock init
β βββ Inc/ HAL configuration headers
β βββ Startup/ Cortex-M4 startup assembly
β βββ STM32G491KEUX_BOOTLOADER.ld
β
βββ can_bootloader_lib/ Reusable C library
β βββ can_bootloader.c Protocol state machine, flash ops
β βββ can_bootloader.h Public API, constants, memory map
β βββ STM32G491KEUX_APP.ld Reference application linker script
β βββ README.md Integration guide
β
βββ can_flasher/ Python host tool
β βββ can_flasher.py Interactive update workflow
β βββ can_protocol.py CAN frame builders and parsers
β βββ firmware_utils.py Binary/hex loading, CRC, validation
β βββ requirements.txt python-can, colorama
β
βββ drivers/ STM32G4 HAL + CMSIS vendor SDK
βββ tools/can_monitor.py CAN bus sniffer
βββ LICENSE GPL-3.0
βββ README.md
- FDCAN clock -- defaults to HSE on STM32G4. Both bootloader and application set it to PCLK1 via
__HAL_RCC_FDCAN_CONFIG(RCC_FDCANCLKSOURCE_PCLK1). - FDCAN filter -- the RX filter for
0x7E0--0x7E3must be inMX_FDCAN1_Init(), not after FDCAN start. Required when a CAN stack (e.g. CANopen) re-initializes the peripheral. - Deferred flash operations -- flash erase, write, and CRC verification run in the main loop (
can_bootloader_process()), not in the CAN RX interrupt. The ISR only buffers data and sets flags. - Single-slot operation -- firmware always stages to Slot A (
0x08004000) since the binary is position-dependent. - CRC-32 -- standard zlib-compatible (polynomial
0x04C11DB7). The STM32 hardware CRC is configured with byte-reversal to match Python'szlib.crc32().
| Problem | Solution |
|---|---|
| No nodes found | Check CAN wiring, 120 ohm termination at each end, baud rate (250 kbps) |
| Transfer fails mid-way | MCU times out after 60s and reboots. Retry the transfer. |
| Bootloader too large | Run make size. Compiled with -Os, must be under 16 KB. |
| DFU mode stuck | 60-second timeout auto-resets and tries booting the app. |
| CRC verification fails | Ensure .bin is built for 0x08004000. Don't use .elf directly. |
| Adapter not detected | Install IXXAT VCI V4 or Kvaser CANlib driver. |
This project is licensed under the GNU General Public License v3.0.