STM32 Custom Bootloader v1.0
Minimal bootloader for STM32 — UART & USB
A small, self-contained bootloader for STM32 microcontrollers.
It handles flash layout, erase, write, validation, and protection
in one portable implementation.
The bootloader uses a simple communication protocol over UART or
USB CDC. Firmware updates are performed using the Web Serial API
directly from a web browser.
- Upgrade firmware over UART or USB
- CRC32 checksum validation for all flash writes
- Optional AES-128 encrypted binary support
- Reliable switch between bootloader and application mode
- Dedicated Settings flash region
- Automatic flash region size management
- User application validation on boot (prevents bricking)
Tested on: STM32F103, STM32F407, STM32G474,
STM32H743, STM32L476, STM32U575. Easily extended to other series.
Flashing Bootloader
Integrate into STM32CubeMX project
- Create STM32CubeMX project for your chip
- Configure HSE clocks, enable Serial Wire Debug
- Enable USART1 or USB depending on required flashing interface
- Enable USART1 DMA (TX → Normal Mode, RX → Circular Mode)
- Enable USART1 Global Interrupt
- Configure PC13 → GPIO output (for LED indication)
- Project Manager → Set toolchain to STM32CubeIDE
- Generate code
- Copy contents of STM32_TOOLKIT folder to Src folder of your project
Add examples.h into includes of main.c:
/* USER CODE BEGIN Includes */ #include "examples.h" /* USER CODE END Includes */
Insert APP_Boot(), APP_Init() and APP_Loop():
/* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ APP_Boot(); /* <-- HERE */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ APP_Init(); /* <-- HERE */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { APP_Loop(); /* <-- and HERE */ /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */
Build and run from STM32CubeIDE with ST-Link connected.
Rapid LED blinking confirms bootloader execution.
Compiling Application
Run from second half of flash
Switch examples by editing examples.h.
#define ACTIVE_EXAMPLE_NUM 2 #define FILE_1 "01_firmware_uploader.bin" #define FILE_2 "02_led_control.bin" /* SELECTED */ #define FILE_3 "03_device_configurator.bin" #define FILE_4 "04_temperature_monitor.bin" #define FILE_5 "05_fft_benchmark.bin"
Update STM32xxxx_FLASH.ld for second-half flash execution:
MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 32K /* 0x0000 → 0x8000 */ }
Update system_stm32fxxxx.c for second-half flash execution:
- Uncomment USER_VECT_TAB_ADDRESS
- Set VECT_TAB_OFFSET = 0x8000
- Project → Properties → C/C++ Build → Settings → MCU/MPU Post Build Outputs → Convert to binary file
- Project → Build project (do not Run to avoid flashing with ST-Link)
Output binary is located in the Debug folder.
Offset calculation: (64 × 1024 ÷ 2) = 32768 → 0x8000
Preparing Binary
Add CRC32 footer and encrypt using Web UI
- Rename compiled binary from the Debug folder (e.g. ledctrl_1.0.bin)
- Open 00_firmware_prepare.html from the HTML folder in your browser
- Click Select .bin File and choose ledctrl_1.0.bin
- Encrypted binary enc_ledctrl_1.0.bin will be generated and saved to your default downloads location
The encrypted file contains a CRC32 footer and AES encryption,
matching the format expected by the bootloader.
Firmware layout after processing: ┌── base firmware (STM32CubeIDE output) │ ├── ↓↓↓ Footer appended at end │ ├── version string (N bytes) ├── version string length N (1 byte) ├── CRC32 (4 bytes, little-endian) ├── 0x55 0x55 0x55 0x55 └── 0xFF ... (empty space)
Flashing Application
Upload firmware via Web UI
- Open 01_firmware_uploader.html in your browser
- Click Upload Firmware button
- Select enc_ledctrl_1.0.bin
- After update completes, open 02_led_control.html to control LED from the browser
The bootloader receives the encrypted firmware over UART, writes it to flash,
and validates the CRC32 checksum before executing the application.
USB Support
Flash over USB using CDC
- STM32CubeIDE → Close Project
- Open the project in STM32CubeMX
- Enable the USB peripheral
- Enable the Communication Device Class (CDC) middleware
- Generate code, open project in STM32CubeIDE
- For STM32F103C8T6, set compiler optimization to Optimize for size (-Os) to fit within flash
- In examples.h, set ACTIVE_EXAMPLE_NUM to 1 to compile the firmware uploader example
- Replace UART read/write calls with USB equivalents (e.g. USB_ReadNoWait(cmd))
- No need to change prefix of COM_Init()
- In system_stm32fxxxx.c, comment out USER_VECT_TAB_ADDRESS
Revert linker script ORIGIN to 0x800000 (beginning of flash memory):
/* Memories definition (STM32F103C8T6) */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K /* <-- when compiling as application */ /* FLASH (rx) : ORIGIN = 0x8008000, LENGTH = 32K */ /* <-- when compiling as bootloader */ }
Build and run from STM32CubeIDE with ST-Link connected. This will flash the USB-based bootloader.
You can convert application to use USB peripheral using the same approach.
With USB enabled, the device appears as a serial port over USB CDC.
Connect using a USB cable and use the same Web UI to flash firmware,
replacing the UART connection and USB-to-RS232 adapter.
Note: Flashing over USB is not supported on STM32U5 series yet.
Configuring Encryption
Set a secure AES key for production
The default AES-128 key is a simple test value. For production use,
replace it with a secure random key in both the firmware and the preparation tool.
/* In flash_helper.c: */ uint8_t key[16] = { 1,2,3,4,5,6,7,8, 9,10,11,12,13,14,15,16 };
/* In 00_firmware_prepare.html: */ // AES Encryption key const key = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ];
To flash unencrypted binaries, compile bootloader without
#define USE_ENCRYPTION in flash_helper.c.
Even when encryption is disabled, binaries must still be processed with
00_firmware_prepare.html to append the CRC32 checksum.
Production notes: call
FLASH_SetRDP(RDP_STATE_ENABLED) once to enable readout
protection before releasing devices. To prevent the bootloader itself from
being overwritten, enable write protection for REGION_BOOTLOADER
with FLASH_SetWRP(WRP_STATE_ENABLED).
Communication Protocol
How the bootloader talks to the host
The bootloader communicates with the host using a simple command–response
protocol over UART or USB CDC. All multi-byte values are sent little-endian.
Practical implementations of this protocol are included in the project.
A working firmware uploader using the Web Serial API can be found in
01_firmware_uploader.html. An example protocol parser
running on STM32 hardware is provided in
01_firmware_uploader.c.
0x10 – Get REGION_APPLICATION flash region information
Returns the size of the application region, the CRC32 stored in flash,
a live-calculated CRC32 of the current contents, and the length plus ASCII
string of the firmware version.
TX:0x10
RX:0x10 + <region_size:4> + <stored_crc:4> + <calc_crc:4> + <version_len:1> + <version_string:version_len>
0x20 – Erase REGION_APPLICATION and reset write pointer
Erases the entire REGION_APPLICATION flash space and sets the current write
position to the start of the region (offset 0). Subsequent 0x30 writes will
begin filling the region from this starting address.
TX:0x20
RX:0x20
0x30 – Write 32-byte data block and advance write pointer
Writes exactly 32 bytes of data to the current write position within the
selected flash region. After a successful write, the internal write pointer
is incremented by 32 bytes. The host must provide a CRC32 checksum calculated
over the 32 data bytes. If the checksum is invalid or the flash write
operation fails, the device returns 0xFF. On success, it returns 0x30.
TX:0x30 + <crc32:4> + <data:32>
RX:0x30 on success / 0xFF on error
0x40 – Jump to REGION_APPLICATION
Sends acknowledgment byte 0x40, then jumps to execute the application code
located in REGION_APPLICATION.
TX:0x40
RX:0x40
© HexFlashLab.com