STM32 System Memory Bootloader:
Flashing Firmware Without a Debugger
Every STM32 ships with a factory-programmed bootloader in a dedicated ROM area called System Memory. You cannot erase it, you cannot corrupt it, and it is always there โ even on a blank chip straight from the reel. This bootloader can program the internal Flash via USART, I2C, SPI, CAN (on selected families), and USB DFU, using nothing more than a serial adapter or a USB cable.
In over a decade of embedded consulting, I have seen teams spend days debugging a prototype only to discover their debugger had a broken pin, or waste hours swapping microcontrollers because a field unit would not boot after a failed update. The system memory bootloader is the universal fallback: it works when nothing else does. In this article, I will cover everything you need to know to use it confidently in production, field updates, and board bring-up.
How to Enter the System Memory Bootloader
There are two ways to force an STM32 to execute the system memory bootloader after reset:
1. BOOT Pin Configuration
This is the classic method. On most STM32 families, the boot pins are sampled on the rising edge of RESET to determine where the CPU starts fetching code:
| BOOT0 | BOOT1 (nBOOT1 / PH2) | Boot Area |
|---|---|---|
| 0 | X (don't care) | Main Flash memory (user application) |
| 1 | 0 | System Memory (factory bootloader) |
| 1 | 1 | Embedded SRAM (debug) |
To enter the bootloader: pull BOOT0 HIGH, set BOOT1 LOW (or leave the pin floating if internally pulled low), toggle the RESET pin, and the MCU starts executing the ROM bootloader. Many Nucleo and Discovery boards expose BOOT0 on a dedicated header pin for exactly this purpose.
2. Option Byte: nBOOT0 and nBOOT_SEL
On newer STM32 families (STM32G0, G4, L4+, L5, U5, H5, H7), you can avoid the external BOOT pin by configuring the option bytes. Setting nBOOT0 = 0 and nBOOT_SEL = 0 forces the MCU to boot from System Memory without any external hardware. This is especially useful on space-constrained PCBs where you cannot afford a BOOT0 pin header:
/* STM32G4 example โ set option bytes to force boot from System Memory */
HAL_FLASH_OB_Unlock();
/* nBOOT0 = 0 (BOOT0 value from option byte, not pin) */
/* nBOOT_SEL = 0 (use nBOOT0 option byte) */
OB->USER &= ~(OB_USER_nBOOT0 | OB_USER_nBOOT_SEL);
HAL_FLASH_OB_Launch(); /* causes a system reset โ MCU boots into bootloader */
After programming, restore the option bytes to boot from Flash. If you forget, the chip boots into the bootloader every time โ which is actually a valid production strategy if you use the bootloader for application updates (more on this below).
Supported Peripherals by Family
The bootloader peripheral availability varies significantly between STM32 families. Here is a quick reference table for the most common families I encounter in the field:
| Family | USART | I2C | SPI | CAN/FDCAN | USB DFU |
|---|---|---|---|---|---|
| STM32F0 | USART1 (PA9/PA10) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | โ | โ |
| STM32F1 | USART1 (PA9/PA10) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | โ | โ |
| STM32F4 | USART1 (PA9/PA10) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | CAN2 (PB5/PB6) | OTG_FS (DP/DM) |
| STM32G0 | USART1 (PA9/PA10 / PA2/PA3) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | โ | โ |
| STM32G4 | USART1 (PA9/PA10) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | FDCAN1 (PB8/PB9) | โ |
| STM32L0 | USART1 (PA9/PA10 / PA2/PA3) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | โ | โ |
| STM32L4/L4+ | USART1 (PA9/PA10) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | โ | OTG_FS (DP/DM) |
| STM32H7 | USART1 (PA9/PA10 / PB14/PB15) | I2C1 (PB6/PB7 / PB8/PB9) | SPI1 (PA5/PA6/PA7 / PB3/PB4/PB5) | FDCAN1 (PD0/PD1) | OTG_HS / OTG_FS |
| STM32U5 | USART1 (PA9/PA10 / PA2/PA3) | I2C1 (PB6/PB7) | SPI1 (PA5/PA6/PA7) | FDCAN1 | OTG_FS |
The exact pin assignments depend on the package. Always consult the Application Note AN2606 (the definitive reference) for your specific device. The bootloader uses fixed pins โ you cannot remap them.
Practical Example: Flashing via USART with STM32CubeProgrammer CLI
USART is the most universally available bootloader interface โ every STM32 family supports it. Here is the complete workflow:
Hardware Setup
Connect a USB-to-UART adapter (e.g. FT232, CP2102) to the STM32 USART1 pins:
- TX (adapter) โ PA10 (USART1_RX on the STM32)
- RX (adapter) โ PA9 (USART1_TX on the STM32)
- GND โ GND (common ground)
Set BOOT0 = HIGH (3.3 V), BOOT1 = LOW (GND). Apply power and toggle RESET.
Detect and Flash
# 1. Detect the bootloader device on /dev/ttyUSB0 (Linux)
STM32_Programmer_CLI -c port=USART1 baudrate=115200
# 2. Erase the main Flash (optional but recommended before flashing)
STM32_Programmer_CLI -c port=USART1 -e all
# 3. Write the firmware binary
STM32_Programmer_CLI -c port=USART1 -w firmware.bin 0x08000000
# 4. Verify and start the application
STM32_Programmer_CLI -c port=USART1 -v firmware.bin 0x08000000 -g 0x08000000
The bootloader defaults to 115200 baud on most families, but you can change it by sending a special command after the synchronisation handshake. If the device does not respond, try 9600 baud โ some older bootloader revisions start at 9600 and switch to 115200 after synchronisation.
Manual Handshake (for Custom Host Tools)
If you are writing your own programmer (for production test jigs, for example), the bootloader uses a simple two-way handshake over USART:
/* Host sends 0x7F (the synchronisation byte) */
/* Bootloader replies with 0x79 (ACK) or 0x1F (NACK) */
/* Then host sends the command byte + inverted byte (checksum) */
/* Minimal C host-side snippet (POSIX) */
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
int sync_bootloader(int fd) {
uint8_t sync = 0x7F;
write(fd, &sync, 1);
usleep(100000); /* 100 ms โ bootloader needs time */
uint8_t resp;
if (read(fd, &resp, 1) != 1 || resp != 0x79) {
return -1; /* NACK or timeout */
}
return 0;
}
/* Send a GET command (0x00) to read bootloader version */
int get_version(int fd) {
uint8_t cmd[] = {0x00, 0xFF}; /* cmd + inverted cmd */
write(fd, cmd, 2);
uint8_t resp, ver;
if (read(fd, &resp, 1) != 1 || resp != 0x79) return -1;
read(fd, &ver, 1);
printf("Bootloader version: 0x%02X\n", ver);
return 0;
}
USB DFU โ Flashing Without Any Extra Hardware
On STM32F4, L4, U5, H7, and other families with USB OTG, the bootloader also supports USB DFU (Device Firmware Update). When the MCU boots into the system memory bootloader and the USB DP/DM lines are connected to a host, it enumerates as a DFU device. On Linux, you see it with lsusb as "STMicroelectronics STM Device in DFU Mode".
Flashing is straightforward with dfu-util:
# List DFU devices
dfu-util -l
# Download firmware.bin to main Flash (address 0x08000000)
dfu-util -d 0483:df11 -a 0 -s 0x08000000:leave -D firmware.bin
The :leave suffix tells the bootloader to exit DFU mode and jump to the user application after the transfer completes. Without it, the chip stays in the bootloader and you need to toggle RESET manually.
USB DFU is my preferred method for field updates during prototyping: no debugger, no serial adapter, just a USB cable. On production boards without a USB connector, I fall back to USART.
Production Strategy: Bootloader-Based Updates Without BOOT Pins
On single-board products where adding a BOOT0 header is not acceptable, you can implement a software-triggered jump to the system memory bootloader. The application checks for an update request (a GPIO level, a CAN message, a button press at startup) and, if detected, reconfigures the system clock back to HSI, disables all peripherals, and jumps to the System Memory base address:
/* Jump to System Memory bootloader from user application */
/* Works on STM32F4, G4, L4, H7 and most Cortex-M4/M7 parts */
void jump_to_bootloader(void) {
/* 1. Disable global interrupts */
__disable_irq();
/* 2. Disable all peripheral clocks */
RCC->CIR = 0;
RCC->AHB1ENR = 0;
RCC->APB1ENR = 0;
RCC->APB2ENR = 0;
/* 3. Reset the SysTick timer to prevent early interrupt */
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* 4. Remap System Memory at address 0x00000000 */
SYSCFG->MEMRMP = 0x01; /* remap System Flash to 0x0000 */
/* 5. Set the main stack pointer from bootloader's vector table */
uint32_t boot_addr = 0x1FFF0000; /* System Memory base (F4) */
/* ^^ This address varies by family โ check AN2606! */
uint32_t msp = *(volatile uint32_t *)boot_addr;
__set_MSP(msp);
/* 6. Jump to the reset vector in bootloader */
void (*bootloader)(void) = (void (*)(void))
*(volatile uint32_t *)(boot_addr + 4);
bootloader();
/* Never returns */
}
Critical: the System Memory base address (0x1FFF0000 in the example) is different for every STM32 family. On STM32G0 it is 0x1FFF0000, on STM32F1 it is 0x1FFFB800, on STM32H7 it is 0x1FF09800. Look up the correct address in AN2606 Table 5 โ using the wrong address causes a HardFault every time.
Practical Checklist
- Confirm the bootloader pins for your exact device: Open AN2606, find your part number, note the USART/I2C/SPI pins. There are no alternatives โ the bootloader uses fixed pins.
- Test USART in both 9600 and 115200 baud: Some devices default to 9600 for the initial handshake and switch to 115200 after synchronisation. If STM32_Programmer_CLI cannot detect the device, try the
baudrate=9600option. - Verify the BOOT0 voltage level: BOOT0 needs to be sampled HIGH at the moment RESET is released. A weak pull-up (10 kฮฉ to 3.3 V) works, but a jumper to VDD is more reliable during development.
- Disable the watchdog before jumping to the bootloader: If the IWDG is running, the bootloader does not service it. The chip resets before the USART synchronisation completes. Disable the watchdog (or configure it for the longest possible timeout) before calling
jump_to_bootloader(). - Check the option bytes on G0/G4/L5/U5: These parts read
nBOOT0andnBOOT_SELfrom the option bytes. A stray value can make the chip boot into System Memory even with BOOT0 = 0. - USB DFU on Windows needs the proper driver: STM32 DFU devices use the STTub30 driver or libusb/WinUSB. Without the driver, Windows enumerates the device as an unknown peripheral.
- Verify Flash content after programming: Always read back and verify (
-vflag in STM32_Programmer_CLI). A baud rate mismatch can produce corrupted data that still passes the bootloader's CRC check (rare but possible with marginal timing).
How I Would Approach This on a Client Project
On every production design I consult on, I insist on including at least one bootloader-capable USART or CAN interface accessible on the final product's connector, even if the budget is tight. The cost of three PCB traces and a 3-pin header is negligible compared to the cost of a single field return that requires opening the enclosure and connecting a debugger.
My standard production bootloader architecture:
- The factory programs the initial firmware via the USART bootloader on the production test jig (using STM32_Programmer_CLI in a Python test script).
- The application implements a field-update mechanism: a GPIO (or CAN message) triggers the software jump to the system memory bootloader.
- A field technician connects a ruggedised serial adapter (FT232-based, with screw terminals) and reflashes using a simple batch script. No debugger, no IDE, no CubeProgrammer GUI.
- For products with USB, I use DFU as the primary update path and USART as the fallback if the USB PHY is damaged.
In automotive and industrial environments, I route CAN instead of USART to the field connector โ CAN is more robust against ESD and long cable runs, and the bootloader on STM32G4/F4/H7 supports FDCAN natively. One less protocol to manage.
Sources and References
- ST Application Note AN2606 โ STM32 microcontroller system memory boot mode (the definitive reference for all bootloader commands, pins, and addresses by device)
- ST Application Note AN3155 โ USART protocol used in the STM32 bootloader
- ST Application Note AN3154 โ I2C protocol used in the STM32 bootloader
- ST Application Note AN4286 โ SPI protocol used in the STM32 bootloader
- ST Application Note AN5362 โ USB DFU protocol used in the STM32 bootloader
- STM32CubeProgrammer User Manual (UM2237)
- dfu-util man page and documentation at dfu-util.sourceforge.net
- Cortex-M3/M4/M7 Generic User Guide โ vector table and stack pointer initialisation
๐ฌ Comment by email
If you have questions, corrections, or want to share your experience with the system memory bootloader on STM32, drop me a line at blog-comments@carrese.eu. Include the article slug in the subject line.