Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

STM32F411 Custom Bootloader with UART Firmware Updater

A custom bootloader for the STM32F411RE that receives a new application image over UART, verifies it with a CRC-32, and hands control to the application, together with a Python host tool that drives the update.

CI MCU Core License

Overview

This project implements a bare-metal bootloader that lives in its own region of flash and can reflash a separate application region over a serial cable, with no debugger required. It demonstrates the core skills a field-update path exercises at once: flash programming on the STM32F411, linker scripts and flash partitioning, Cortex-M vector-table relocation, and the design of a small framed UART protocol with CRC verification. The repository contains three cooperating pieces: the bootloader firmware, a demo application built to run from a separate flash region, and a Python host uploader.

Status: the full update path is implemented in source and the protocol, CRC-32, and flash-bounds logic are unit-tested against the Python host in CI. End-to-end flashing has not yet been validated on physical hardware, so the current release is tagged as a release candidate.

How it works

Flash partitioning

The STM32F411RE has 512 KB of flash in one continuous address space starting at 0x08000000. It is split into two independently linked programs:

Region Address range Size Sectors Contents
Bootloader 0x080000000x08007FFF 32 KB 0–1 this bootloader; runs first at reset
Application 0x080080000x0807FFFF 480 KB 2–7 the user application

Two linker scripts (bootloader/linker/bootloader.ld and app/linker/app.ld) set each program's flash origin so the bootloader keeps the reset vector and the application is built to run from 0x08008000.

Boot policy and jump to application

At reset the CPU always starts in the bootloader. On startup it briefly polls the UART. If the host is already sending data, the bootloader enters update mode and serves commands. Otherwise, if a valid application is present, it jumps to it. If neither is true, it stays in the update loop waiting for a host.

The jump reads the application's initial stack pointer from 0x08008000 and its reset handler address from 0x08008004. Before transferring control, the bootloader relocates the vector table by writing the application base to SCB->VTOR (0xE000ED08), so interrupts vector into the application rather than the bootloader. It validates the application first: the stack-pointer word must point into SRAM (0x200000000x20020000), which rejects an erased flash region that reads back as 0xFFFFFFFF.

UART update protocol

The host and bootloader speak a simple framed request/response protocol over USART2 at 115200 baud, 8N1. The full specification is in docs/protocol.md.

Each frame is:

SOF (0xA5) | CMD (1 byte) | LEN (uint16, little-endian) | PAYLOAD (LEN bytes) | CRC32 (4 bytes)

The CRC-32 covers CMD + LEN + PAYLOAD and is bit-compatible with Python's zlib.crc32, which is how the host and target agree that a frame arrived intact. Commands from host to bootloader:

CMD Name Payload Meaning
0x01 HELLO none probe; bootloader replies ACK plus one version byte
0x02 ERASE none erase the application region (sectors 2–7)
0x03 WRITE offset:u32 + data program data at app_base + offset
0x04 VERIFY crc32:u32 verify the CRC-32 of the whole written image
0x05 JUMP none relocate the vector table and boot the application

The bootloader answers every frame with a single byte, ACK (0x79) or NACK (0x1F), and the host waits for that byte before sending the next frame. WRITE offsets must be word-aligned and are range-checked (overflow-safe) so a write can never land outside the application region, and the bootloader never erases its own sectors 0–1. VERIFY re-reads the highest offset written since the last erase and compares its CRC-32 to the value the host supplies.

Target hardware

  • Board: STMicroelectronics NUCLEO-F411RE
  • MCU: STM32F411RE (ARM Cortex-M4F, 512 KB flash, 128 KB SRAM)
  • Serial link: USART2 on PA2/PA3, which is routed to the on-board ST-LINK USB virtual COM port, so plugging in the USB cable provides the update link without extra wiring
  • Demo application blinks the on-board user LED (LD2 on PA5) to prove the handover

Building

The firmware cross-compiles with arm-none-eabi-gcc and CMake. The toolchain file cmake/arm-none-eabi-gcc.cmake targets Cortex-M4 with hardware floating point.

sudo apt-get install -y gcc-arm-none-eabi cmake

# build the bootloader and the demo application (two separate CMake projects)
for t in bootloader app; do
  cmake -S $t -B build/$t \
    -DCMAKE_TOOLCHAIN_FILE="$PWD/cmake/arm-none-eabi-gcc.cmake" \
    -DCMAKE_BUILD_TYPE=Release
  cmake --build build/$t
done

The build produces build/bootloader/bootloader.bin and build/app/app.bin (each project also emits an .elf and a .map).

Flashing and updating firmware

Flash the bootloader once with an SWD programmer (for example st-flash from the open-source st-link tools):

st-flash write build/bootloader/bootloader.bin 0x08000000

After the bootloader is in place, application updates go over UART with no debugger. The host tool is host/uploader.py and depends on pyserial (pip install -r host/requirements.txt):

python3 host/uploader.py --port /dev/ttyACM0 --image build/app/app.bin

Arguments: --port (required, the serial device, for example /dev/ttyACM0), --image (required, the application .bin to flash), and --baud (optional, defaults to 115200). The uploader pads the image to a word boundary, then runs the full sequence: HELLO to read the bootloader version, ERASE, WRITE in 256-byte chunks with a progress indicator, VERIFY against the whole-image CRC-32, and JUMP to start the new application.

Tests

tests/host/ contains a host interop test that compiles the exact bl_proto.c and bl_crc32.c sources used in the firmware with the host compiler and checks three things:

  • CRC-32 matches the canonical check value 0xCBF43926, confirming compatibility with Python's zlib.crc32
  • the flash-region bounds check accepts valid ranges and rejects end-straddling and unsigned-overflow cases
  • frames generated by tests/host/gen_frames.py (byte-identical to the host uploader's framing) parse correctly, and a deliberately corrupted frame is rejected on CRC

Run it locally:

python3 tests/host/gen_frames.py frames.bin
gcc -Ibootloader/src tests/host/test_proto.c \
    bootloader/src/bl_proto.c bootloader/src/bl_crc32.c -o test_proto
./test_proto frames.bin

Continuous integration

GitHub Actions (.github/workflows/ci.yml) runs four jobs on every push and pull request:

  • firmware build: cross-compiles both images, asserts the bootloader links at 0x08000000 and the application at 0x08008000 by inspecting the .isr_vector section, and uploads the built binaries as artifacts
  • protocol interop: builds and runs the host interop test described above
  • host tool: byte-compiles host/uploader.py and lints it with flake8
  • static analysis: runs cppcheck over the firmware sources (advisory)

Project layout

bootloader/   bootloader firmware: UART driver, frame parser, CRC-32, flash driver, jump logic
app/          demo application linked at 0x08008000 (LED blink + VTOR relocation)
host/         uploader.py, the Python host tool that sends an image over serial
docs/         the UART protocol specification
tests/        host interop test for the protocol, CRC, and flash-bounds logic
cmake/        arm-none-eabi-gcc cross-compile toolchain file

License

MIT. See LICENSE.

About

Custom STM32F411 bootloader with a UART firmware updater — protocol, Python host tool, CMake build, and CI

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages