Embedded HandbookApps
All sections › VI. Practice
STM32F103, STM32F407, STM32G474 · level intermediate · 37 min read
Open in the interactive handbook

48д. The ST factory bootloader in system memory: entry via BOOT0 and from firmware, the USART protocol, your own flashing utility

In section 4.9 the factory bootloader appeared as a way to rescue a board: BOOT0 to one, a reset — and the chip waits for firmware over USART1. In 48.11 it comes first among the ready-made update paths. Sections 48а–48г and 198щ built our own bootloader: signature, MCUboot, delta update, encryption. Here we look at the bootloader that already sits in every chip and that you do not have to write.

The section covers: what system memory is and when the factory bootloader is enough; how to get into it with the BOOT0 pin on F1 and F4 and with the nBOOT0 and nSWBOOT0 bits on G4; how to jump into it from running firmware — our own C code for three families; which interfaces and pins the F103C8, F407 and G474 have; the USART protocol per AN3155 — the frame of every command; our own flashing utility in Python and the bootloader model it was tested against; pitfalls.

Sources: AN2606 Rev 21, DocID13801 (the factory bootloader of all families: table 2 — entry conditions, 3.1 — jump from firmware, table 3 — interfaces and versions, tables 6, 7 — F10xxx, tables 22–25 — F40xxx/41xxx, table 68 — PID and addresses, tables 69, 70 — startup time), public address — st.com, AN2606; AN3155 Rev 6, DocID17066 (USART protocol: commands, checksums, versions) — st.com, AN3155; RM0008 Rev 17 (F1: table 9 — boot modes, table 5 — pages), RM0090 Rev 19 (F4: table 2 — boot modes, table 5 — sectors, 3.7.3 — RDP levels), RM0440 Rev 6 and datasheet DS12288 Rev 5 (G4: table 5 — boot modes, 2.4 — RAM, 47.6.1 — device ID). The AN2606 revision this section was checked against came out before G4; newer revisions also describe G4, but the section was not checked against them — so on G4 this section says only what is in RM0440 and DS12288. License: the section's own code — free to use.

Terms used in the documentation

Term Meaning
system memory boot mode booting from system memory
bootloader activation pattern the set of conditions for entering the bootloader
automatic baud rate detection detecting the baud rate from the first byte
even parity (8E1) 8 data bits, even parity bit, 1 stop bit
XOR checksum byte checksum byte: exclusive OR of all bytes of the block
product ID (PID), Get ID the chip family number, the command that reads it
bootloader ID the version number of the bootloader in system memory
Erase / Extended Erase erasing with one-byte / two-byte page numbers
Readout Protect / Readout Unprotect enable / remove readout protection
Go command the "go" command: stack and entry point from the vector table
physical remap (MEM_MODE) what is visible at address 0: Flash, system memory or RAM
software BOOT0 (nSWBOOT0, nBOOT0) BOOT0 from the option bytes instead of the pin

48д.1 System memory, and when the factory bootloader beats your own

Besides the Flash for the program, an STM32 has an information block: the option bytes and system memory with ST code written at the factory. You cannot erase or rewrite it: per AN2606 (table 5) the Write and Erase commands do not work on system memory, only Read. On F103 it is 2 KB from address 0x1FFFF000, on F407 — 0x1FFF0000…0x1FFF77FF (AN2606, table 68), on G474 — 28 KB from 0x1FFF0000 (RM0440, 2.6.1 and the Flash layout).

A computer with the utility, through a USB–UART adapter, is connected to USART1 of the chip on PA9 and PA10; the DTR and RTS lines of the adapter go to BOOT0 and NRST; on the right the choice of boot area: BOOT0 equal to 1 and BOOT1 equal to 0 — system memory at 0x1FFFF000 on F103 and 0x1FFF0000 on F407, BOOT0 equal to 0 — Flash at 0x08000000, entry from firmware with a flag in RAM and a reset leads to system memory; at the bottom the Write Memory frame: code 31 CE, ACK, address with XOR checksum, ACK, length and 256 bytes of data with checksum, ACK; 265 bytes out, 3 back, at 115200 bits per second 25.6 ms per block

What the bootloader can do, the same for all families: receive an image over one of the interfaces, erase and write Flash, read it, enable and remove read and write protection, jump to an address. What it cannot do: check whose image it is (there is no signature), roll back to the previous version, receive an image over radio.

When Factory Your own (48, 198щ)
flash a board on the desk without a programmer yes —
revive a board where the program took over PA13/PA14 (4.9) yes —
programming on a production line over UART yes possible
update at the customer's site over a UART or USB brought outside yes, if no signature is needed yes
signature, encryption, rollback, log (48а–48г, 198щ) no yes
update over Bluetooth, radio, network no yes
Flash space used 0 bytes 4528 bytes for 198щ

There is one more reason to keep the factory bootloader in mind even when you have your own: it always stays in the chip. Whoever can set BOOT0 to one and connect to USART1 can read the Flash — if readout protection is not enabled (11в), and can erase it and write their own — always, except on F4 and G4 with RDP level 2: there entry into system memory is closed (RM0090, 3.7.3; RM0440, p. 123).


48д.2 Entry via pins: BOOT0 and BOOT1, nBOOT0 and nSWBOOT0

On F1 and F4 the boot area is selected by two pins (RM0008, table 9; RM0090, table 2): BOOT0 = 0 — Flash, BOOT0 = 1 and BOOT1 = 0 — system memory, both at one — RAM. The values are latched on the 4th rising edge of the SYSCLK clock after reset and again on exit from Standby, so you have to hold them all that time. In AN2606 this is condition "pattern1" (table 2); on F10xxx and F40xxx/41xxx the bootloader is entered exactly this way (4.1 and 11.1.1).

G4 has no BOOT1 pin; instead there is the nBOOT1 bit in FLASH_OPTR, and BOOT0 can be taken not from the PB8 pin but from the nBOOT0 bit (RM0440, table 5):

nSWBOOT0 BOOT0 source System memory when
1 pin PB8 PB8 = 1 and nBOOT1 = 1
0 bit nBOOT0 nBOOT0 = 0 and nBOOT1 = 1

The BOOT_LOCK bit = 1 overrides everything: boot from Flash only. With nSWBOOT0 = 0 the PB8 pin is freed as an ordinary pin — but then you can no longer enter the bootloader with a pin: only by rewriting the option bytes or by a jump from firmware (48д.3). The DS12288 datasheet calls the same bit nBOOT_SEL, RM0440 calls it nSWBOOT0; they are one and the same.

A handy trick for BluePill and F4 with a USB–UART adapter: DTR drives BOOT0, RTS drives NRST. The utility sets BOOT0, pulses reset, waits for the bootloader to start and begins the exchange (enter_bootloader in 48д.6). How long to wait — AN2606, table 69: on F10xxx at least 1.227 ms, on F40xxx/41xxx — 84.59 ms for version V3.x and 74 ms for V9.x; the utility waits 0.1 s.


48д.3 Entry from running firmware

In a device inside an enclosure you cannot reach BOOT0. Then the application itself goes into system memory — on a menu command, on a frame from the computer. AN2606 (3.1) lists what to do before the jump: turn off the peripheral clocks and the PLL, disable interrupts and clear pending requests. The bootloader expects to find the chip in the state a reset leaves it in — the hand-over of control in 48.5 relies on the same condition.

The most reliable way is to restore nothing by hand but to really reset the chip: the application puts a flag in RAM (as in 48.6 and 198щ.4 — a magic value and its complement) and does a software reset; the first line of Reset_Handler, before .data, .bss and clock setup, checks the flag — and jumps. The reset restored everything by itself. The second way is a jump without a reset, when you cannot insert a check at the start of Reset_Handler (the startup code is someone else's): then the code stops SysTick, NVIC, the clocks and the peripherals itself.

All the code is one file for three families; register addresses are our own #defines (checked against the CMSIS headers by the section's check):

/* sysboot48d.h — раздел 48д: вход в заводской загрузчик ST из работающей прошивки.
 * Собирать с -DSB_F1 (STM32F103), -DSB_F4 (STM32F407) или -DSB_G4 (STM32G474). */
#ifndef SYSBOOT48D_H
#define SYSBOOT48D_H
#include <stdint.h>

#if defined(SB_F1)
#define SB_SYSMEM_ADDR 0x1FFFF000u   /* AN2606, таблица 68 */
#define SB_FLAG_ADDR   0x20004FF0u   /* F103C8: 20 КБ ОЗУ, последние 16 байт (48.6) */
#elif defined(SB_F4)
#define SB_SYSMEM_ADDR 0x1FFF0000u   /* AN2606, таблица 68 */
#define SB_FLAG_ADDR   0x2001FFF0u   /* F407: 128 КБ основного ОЗУ */
#elif defined(SB_G4)
#define SB_SYSMEM_ADDR 0x1FFF0000u   /* RM0440, 2.6.1 */
#define SB_FLAG_ADDR   0x20017FF0u   /* G474: SRAM1 80 КБ + SRAM2 16 КБ (RM0440, 2.4) */
#else
#error "SB_F1, SB_F4 или SB_G4"
#endif

#ifndef SB_SYSMEM
#define SB_SYSMEM SB_SYSMEM_ADDR     /* опыт в QEMU подставляет свой адрес */
#endif

#define SB_MAGIC 0x5B007ED0u

void sysboot_request(void);    /* из приложения: признак в ОЗУ и программный сброс */
void sysboot_early(void);      /* первой строкой Reset_Handler, до .data, .bss и тактирования */
void sysboot_jump_now(void);   /* без сброса: вернуть камень к состоянию после сброса и прыгнуть */

#endif
/* sysboot48d.c — раздел 48д: вход в заводской загрузчик ST из работающей прошивки, без вывода BOOT0.
 * Два пути. Надёжный: признак в ОЗУ + программный сброс, а после сброса — прыжок первой строкой
 * Reset_Handler, пока ничего не включено. Быстрый: прыжок сразу, вернув камень к состоянию после сброса
 * так, как велит AN2606, 3.1: такты периферии выключить, PLL выключить, прерывания запретить и снять.
 * Адреса регистров — из RM0008, RM0090, RM0440; сверены с заголовками CMSIS проверкой раздела. */
#include "sysboot48d.h"

#define REG(a) (*(volatile uint32_t *)(a))

/* Ядро Cortex-M3/M4 — одинаково у всех трёх семейств */
#define SYST_CSR     REG(0xE000E010u)
#define SYST_RVR     REG(0xE000E014u)
#define SYST_CVR     REG(0xE000E018u)
#define NVIC_ICER(i) REG(0xE000E180u + 4u * (i))
#define NVIC_ICPR(i) REG(0xE000E280u + 4u * (i))
#define SCB_ICSR     REG(0xE000ED04u)
#define SCB_VTOR     REG(0xE000ED08u)
#define SCB_AIRCR    REG(0xE000ED0Cu)
#define ICSR_PENDSTCLR (1u << 25)
#define AIRCR_RESET  (0x05FA0000u | (1u << 2))   /* ключ VECTKEY и SYSRESETREQ */

#if defined(SB_F1)
#define RCC          0x40021000u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x04u)
#define RCC_CIR      REG(RCC + 0x08u)
#define RCC_APB2RSTR REG(RCC + 0x0Cu)
#define RCC_APB1RSTR REG(RCC + 0x10u)
#define RCC_AHBENR   REG(RCC + 0x14u)
#define RCC_APB2ENR  REG(RCC + 0x18u)
#define RCC_APB1ENR  REG(RCC + 0x1Cu)
#define DMA1_CCR(ch) REG(0x40020008u + 20u * (ch))   /* каналы 1…7: ch = 0…6 */
#define CR_HSION     (1u << 0)
#define CR_HSIRDY    (1u << 1)
#define CFGR_SW_HSI  0x0u
#define CFGR_RESET   0x00000000u
#elif defined(SB_F4)
#define RCC          0x40023800u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x08u)
#define RCC_CIR      REG(RCC + 0x0Cu)
#define RCC_AHB1RSTR REG(RCC + 0x10u)
#define RCC_AHB2RSTR REG(RCC + 0x14u)
#define RCC_AHB3RSTR REG(RCC + 0x18u)
#define RCC_APB1RSTR REG(RCC + 0x20u)
#define RCC_APB2RSTR REG(RCC + 0x24u)
#define RCC_AHB1ENR  REG(RCC + 0x30u)
#define RCC_AHB2ENR  REG(RCC + 0x34u)
#define RCC_AHB3ENR  REG(RCC + 0x38u)
#define RCC_APB1ENR  REG(RCC + 0x40u)
#define RCC_APB2ENR  REG(RCC + 0x44u)
#define SYSCFG_MEMRMP REG(0x40013800u)
#define APB2ENR_SYSCFGEN (1u << 14)
#define CR_HSION     (1u << 0)
#define CR_HSIRDY    (1u << 1)
#define CFGR_SW_HSI  0x0u
#define CFGR_RESET   0x00000000u
#elif defined(SB_G4)
#define RCC          0x40021000u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x08u)
#define RCC_CIER     REG(RCC + 0x18u)
#define RCC_AHB1RSTR REG(RCC + 0x28u)
#define RCC_AHB2RSTR REG(RCC + 0x2Cu)
#define RCC_AHB3RSTR REG(RCC + 0x30u)
#define RCC_APB1RSTR1 REG(RCC + 0x38u)
#define RCC_APB1RSTR2 REG(RCC + 0x3Cu)
#define RCC_APB2RSTR REG(RCC + 0x40u)
#define RCC_AHB1ENR  REG(RCC + 0x48u)
#define RCC_AHB2ENR  REG(RCC + 0x4Cu)
#define RCC_AHB3ENR  REG(RCC + 0x50u)
#define RCC_APB1ENR1 REG(RCC + 0x58u)
#define RCC_APB1ENR2 REG(RCC + 0x5Cu)
#define RCC_APB2ENR  REG(RCC + 0x60u)
#define SYSCFG_MEMRMP REG(0x40010000u)
#define APB2ENR_SYSCFGEN (1u << 0)
#define AHB1RSTR_FLASHRST (1u << 8)      /* флеш-память не сбрасываем: из неё идёт этот код */
#define CR_HSION     (1u << 8)
#define CR_HSIRDY    (1u << 10)
#define CFGR_SW_HSI  0x1u                /* у G4 HSI16 — это 01 */
#define CFGR_RESET   0x00000005u
#endif

#define CR_HSEON     (1u << 16)
#define CR_CSSON     (1u << 19)
#define CR_PLLON     (1u << 24)
#define CFGR_SW      0x3u
#define CFGR_SWS     (0x3u << 2)
#define SB_WAIT      100000u             /* ожидания с пределом: зависший генератор не вешает прыжок */

#define SB_FLAG ((volatile uint32_t *)SB_FLAG_ADDR)

__attribute__((noreturn)) static void sb_jump(uint32_t base)
{
    uint32_t sp = REG(base);
    uint32_t pc = REG(base + 4u);

    __asm volatile("msr msp, %0\n"
                   "msr control, %1\n"   /* стек MSP, привилегированный режим, без следа FPU */
                   "isb\n"
                   "cpsie i\n"           /* после сброса PRIMASK = 0 — загрузчик ждёт именно этого */
                   "bx %2\n"
                   :
                   : "r"(sp), "r"(0u), "r"(pc)
                   : "memory");
    for (;;) {
    }
}

static void sb_remap(void)
{
#if defined(SB_F4) || defined(SB_G4)
    RCC_APB2ENR |= APB2ENR_SYSCFGEN;
    (void)RCC_APB2ENR;
    SYSCFG_MEMRMP = 0x1u;                /* MEM_MODE = 01: по адресу 0 — системная память */
#endif
    SCB_VTOR = SB_SYSMEM;
    __asm volatile("dsb\n"
                   "isb\n" ::: "memory");
}

void sysboot_request(void)
{
    SB_FLAG[0] = SB_MAGIC;
    SB_FLAG[1] = ~SB_MAGIC;
    __asm volatile("dsb" ::: "memory");
    SCB_AIRCR = AIRCR_RESET;
    __asm volatile("dsb" ::: "memory");
    for (;;) {
    }
}

void sysboot_early(void)
{
    if (SB_FLAG[0] != SB_MAGIC || SB_FLAG[1] != ~SB_MAGIC)
        return;
    SB_FLAG[0] = 0u;                     /* прочитали — стёрли, иначе загрузчик навсегда */
    SB_FLAG[1] = 0u;
    sb_remap();
    sb_jump(SB_SYSMEM);
}

static void sb_clocks_reset(void)
{
    uint32_t n;

    RCC_CR |= CR_HSION;
    for (n = 0u; n < SB_WAIT && !(RCC_CR & CR_HSIRDY); n++) {
    }
    RCC_CFGR = (RCC_CFGR & ~CFGR_SW) | CFGR_SW_HSI;
    for (n = 0u; n < SB_WAIT && (RCC_CFGR & CFGR_SWS) != (CFGR_SW_HSI << 2); n++) {
    }
    RCC_CFGR = CFGR_RESET;               /* делители и MCO — как после сброса */
    RCC_CR &= ~(CR_PLLON | CR_CSSON);
    RCC_CR &= ~CR_HSEON;
#if defined(SB_G4)
    RCC_CIER = 0u;                       /* прерывания RCC выключены */
#else
    RCC_CIR = 0u;
#endif
}

static void sb_periph_reset(void)
{
#if defined(SB_F1)
    uint32_t ch;
    RCC_AHBENR |= 1u;                    /* DMA1EN: у F103 сброса DMA через RCC нет — гасим каналы сами */
    for (ch = 0u; ch < 7u; ch++)
        DMA1_CCR(ch) = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_APB1RSTR = 0xFFFFFFFFu;
    RCC_APB1RSTR = 0u;
    RCC_AHBENR = 0x00000014u;            /* значения после сброса (RM0008, 7.3.6–7.3.8) */
    RCC_APB2ENR = 0u;
    RCC_APB1ENR = 0u;
#elif defined(SB_F4)
    RCC_AHB1RSTR = 0xFFFFFFFFu;
    RCC_AHB1RSTR = 0u;
    RCC_AHB2RSTR = 0xFFFFFFFFu;
    RCC_AHB2RSTR = 0u;
    RCC_AHB3RSTR = 0xFFFFFFFFu;
    RCC_AHB3RSTR = 0u;
    RCC_APB1RSTR = 0xFFFFFFFFu;
    RCC_APB1RSTR = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = 0x00100000u;           /* значения после сброса (RM0090, 6.3.10–6.3.14) */
    RCC_AHB2ENR = 0u;
    RCC_AHB3ENR = 0u;
    RCC_APB1ENR = 0u;
    RCC_APB2ENR = 0u;
#elif defined(SB_G4)
    RCC_AHB1RSTR = ~AHB1RSTR_FLASHRST;
    RCC_AHB1RSTR = 0u;
    RCC_AHB2RSTR = 0xFFFFFFFFu;
    RCC_AHB2RSTR = 0u;
    RCC_AHB3RSTR = 0xFFFFFFFFu;
    RCC_AHB3RSTR = 0u;
    RCC_APB1RSTR1 = 0xFFFFFFFFu;
    RCC_APB1RSTR1 = 0u;
    RCC_APB1RSTR2 = 0xFFFFFFFFu;
    RCC_APB1RSTR2 = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = 0x00000100u;           /* значения после сброса (RM0440, 7.4.14–7.4.19) */
    RCC_AHB2ENR = 0u;
    RCC_AHB3ENR = 0u;
    RCC_APB1ENR1 = 0x00000400u;
    RCC_APB1ENR2 = 0u;
    RCC_APB2ENR = 0u;
#endif
}

void sysboot_jump_now(void)
{
    uint32_t i;

    __asm volatile("cpsid i" ::: "memory");
    SYST_CSR = 0u;                       /* SysTick стоп: загрузчик меряет им скорость сам */
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    SCB_ICSR = ICSR_PENDSTCLR;
    for (i = 0u; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;      /* запретить все прерывания */
        NVIC_ICPR(i) = 0xFFFFFFFFu;      /* и снять висящие запросы */
    }
    sb_clocks_reset();
    sb_periph_reset();
    for (i = 0u; i < 8u; i++)
        NVIC_ICPR(i) = 0xFFFFFFFFu;      /* сброс периферии мог поднять запросы ещё раз */
    sb_remap();
    sb_jump(SB_SYSMEM);
}

Why it is done this way:

Sizes (arm-none-eabi-gcc 12.2.1, -Os): the whole file is 436 bytes of code on F1, 504 on F4 and 464 on G4, no data and no .bss; of these sysboot_jump_now is 276, 304 and 272, sysboot_early is 76, 56 and 56 (on F4 and G4 it is accompanied by sb_remap — 60 and 52 bytes, on F1 it is inlined), sysboot_request is 52, sb_jump is 32 bytes. In QEMU 7.2.22 (netduinoplus2 — that is an STM32F405) instead of ST system memory there is our own vector table at 0x08008000; its "bootloader" prints the state in which it received the chip. The jump without a reset — from a mode with PSP, with SysTick running, with the USART1 interrupt enabled and pending:

APP before: systick=irq iser1=0x00000020 ispr1=0x00000020
BOOT msp=0x2000eff8 control=0x00000000 primask=0x00000000 ipsr=0x00000000 syst_csr=0x00000000 iser1=0x00000000 ispr1=0x00000000 vtor=0x08008000 memrmp=0x00000000 flag0=0x00000000

CONTROL, PRIMASK, IPSR are zeros: thread mode, MSP stack, interrupts enabled. SysTick is stopped, the USART1 interrupt is both disabled and cleared (it was 0x20 in both registers). The stack pointer is 0x2000F000 from the table minus 8 bytes pushed by the "bootloader" function itself. The path through a reset gave the same result, and the flag was erased after the jump. SYSCFG_MEMRMP reads as zero in QEMU — the QEMU model does not store this register, so the memory remap was not tested here.


48д.4 Interfaces by family and automatic baud rate detection

STM32F103C8 STM32F407 STM32G474
USART protocol version V2.2 V3.1 —
USART USART1: PA9 TX, PA10 RX USART1: PA9/PA10; USART3: PB10/PB11 or PC10/PC11 USART (DS12288)
USB DFU no PA11/PA12, OTG FS yes (DS12288)
CAN no CAN2: PB5 RX, PB13 TX, 125 kbit/s —
I²C no on V9.x: I2C1 PB6/PB7, I2C2 PF1/PF0, I2C3 PA8/PC9 yes (DS12288)
SPI no on V9.x: SPI1 PA4–PA7, SPI2 PI0–PI3 yes (DS12288)
PID 0x410 0x413 0x469 (DEV_ID, RM0440, 47.6.1)
system memory 0x1FFFF000, 2 KB 0x1FFF0000…0x1FFF77FF 0x1FFF0000, 28 KB
bootloader RAM 512 bytes 8 KB (V3.x), 12 KB (V9.x) —
bootloader clock HSI, PLL at 24 MHz HSI, PLL at 24 MHz (V3.x) or 60 MHz (V9.x); CAN and DFU — from a 4–26 MHz crystal —

The source for F103 and F407 is AN2606: table 3 (interfaces and versions), tables 6, 22, 24 (pins, clock, RAM), table 68 (PID and addresses). On F407 the bootloader comes in two generations — V3.x with ID 0x31 and V9.x with ID 0x90; the ID is stored in system memory at address 0x1FFF77DE and is read with the Read Memory command. For G474 — only what is in DS12288 (3.7: USART, I²C, SPI, USB DFU) and RM0440; the G474 in all memory sizes is category 3 (RM0440, table 1), with DEV_ID 0x469. The pins of the G4 interfaces and their versions are in the newer AN2606 revisions, which this section was not checked against.

On F407, CAN and DFU need a crystal that is a multiple of 1 MHz, from 4 to 26 MHz; the bootloader measures its frequency itself with timer TIM11 (AN2606, table 22). USART runs from the internal HSI, no crystal is needed.

Automatic baud rate detection. The bootloader does not know the baud rate in advance. It waits on RX for the frame 0x7F — start bit, 0x7F, parity bit, stop bit — and measures its duration with the SysTick timer; from that it configures the USART and replies 0x79 (AN3155, 1). 0x7F was not chosen by chance: the low 7 bits are ones, the top one is zero; between the falling edge of the start bit and the falling edge of bit 7 there are exactly 8 bit periods. Baud rates from 1200 to 115200 bit/s have been tested; the mismatch between the baud rates must be less than 2.5% (AN3155, 2). Which edges exactly the bootloader measures, AN3155 does not specify; the section's model measures from falling edge to falling edge.


48д.5 The USART protocol per AN3155

The UART frame is 8 bits, even parity, 1 stop bit (AN2606, tables 6 and 22). Replies are one byte: ACK 0x79 or NACK 0x1F (AN3155, 3). Three protections against errors: parity of every byte; a command is sent as a pair "code and its complement" (XOR of the pair = 0xFF); address and data blocks end with a checksum byte — the XOR of all bytes of the block, so the XOR of the block together with the checksum = 0. The address is 4 bytes, most significant first.

Command Code From the computer after ACK on the code Chip reply
Get 00 FF — N = 11, version, 11 command codes, ACK
Get Version 01 FE — version, 00, 00, ACK
Get ID 02 FD — N = 1, PID (2 bytes, most significant first), ACK
Read Memory 11 EE address + checksum; N − 1 and its complement ACK, ACK, N bytes
Go 21 DE address + checksum ACK, jump
Write Memory 31 CE address + checksum; N − 1, N bytes, XOR ACK, ACK
Erase 43 BC N − 1, N numbers of one byte, XOR; or FF 00 ACK
Extended Erase 44 BB N − 1 (2 bytes), N numbers of 2 bytes, XOR; or FF FF 00 ACK
Write Protect 63 9C N − 1, N sector numbers, XOR ACK, reset
Write Unprotect 73 8C — ACK, reset
Readout Protect 82 7D — ACK, reset
Readout Unprotect 92 6D — erase of the whole memory, ACK, reset

Limitations and details people trip over:


48д.6 Your own flashing utility and a bootloader model

The utility is not tied to a serial port: the Stm32Boot class talks to any object that has write, read (returns less on timeout) and flush_input. The pyserial port is the SerialPort class; the bootloader model is ModelPort. This way the same code works with a board and is tested on the computer. In full:

# -*- coding: utf-8 -*-
"""stm32boot48d.py — раздел 48д: своя утилита прошивки через заводской загрузчик STM32 (протокол USART, AN3155).

Протокол не знает, что под ним: класс Stm32Boot работает с любым «портом» — объектом с методами
write(data), read(n) (вернёт меньше n байт по тайм-ауту), flush_input() и, по желанию, set_boot0(v),
set_reset(v). Порт на pyserial — SerialPort ниже; модель загрузчика — в model48d.py.
Запуск с настоящей платой (нужен pyserial):  python stm32boot48d.py COM5 firmware.bin [0x08000000]"""
import sys, time

ACK, NACK, SYNC = 0x79, 0x1F, 0x7F
GET, GV, GID, READ, GO, WRITE = 0x00, 0x01, 0x02, 0x11, 0x21, 0x31
ERASE, EXT_ERASE, WP, WUP, RP, RUP = 0x43, 0x44, 0x63, 0x73, 0x82, 0x92
# Раскладка флеш-памяти по номеру кристалла (PID из Get ID): F103 medium-density — страницы по 1 КБ
# (RM0008, таблица 5), F407 — секторы 16/16/16/16/64/128 × 7 КБ (RM0090, таблица 5).
LAYOUT = {
    0x410: ("страницы", [1024] * 128),
    0x413: ("секторы", [16384] * 4 + [65536] + [131072] * 7),
}


class BootError(Exception):
    pass


class Nack(BootError):
    pass


def xor(data):
    x = 0
    for b in data:
        x ^= b
    return x


class SerialPort:
    """Порт на pyserial: 8 бит, чётность even, 1 стоп (AN2606: так настроен USART загрузчика).
    DTR ведёт BOOT0, RTS — NRST; полярность зависит от переходника, поэтому её можно перевернуть."""

    def __init__(self, name, baud=115200, timeout=1.0, inv_boot0=False, inv_reset=False):
        import serial  # pyserial
        self.s = serial.Serial(name, baud, bytesize=8, parity=serial.PARITY_EVEN, stopbits=1, timeout=timeout)
        self.inv_boot0, self.inv_reset = inv_boot0, inv_reset

    def write(self, data):
        self.s.write(bytes(data))

    def read(self, n):
        return self.s.read(n)

    def flush_input(self):
        time.sleep(0.05)
        self.s.reset_input_buffer()

    def set_boot0(self, level):
        self.s.dtr = bool(level) != self.inv_boot0

    def set_reset(self, active):
        self.s.rts = bool(active) != self.inv_reset


class Stm32Boot:
    def __init__(self, port, echo=False):
        self.port = port
        self.echo = echo          # переходник возвращает каждый посланный байт (полудуплекс, один провод)
        self.tx = self.rx = 0     # байт в линии туда и обратно — для оценки времени
        self.version = None
        self.cmds = b""

    def _send(self, data):
        data = bytes(data)
        self.port.write(data)
        self.tx += len(data)
        if self.echo:
            back = self.port.read(len(data))
            if back != data:
                raise BootError("эхо не совпало: %s" % back.hex())

    def _recv(self, n):
        data = self.port.read(n)
        self.rx += len(data)
        if len(data) != n:
            raise BootError("тайм-аут: ждали %d байт, пришло %d" % (n, len(data)))
        return data

    def _ack(self, what):
        r = self._recv(1)[0]
        if r == ACK:
            return
        if r == NACK:
            self.port.flush_input()   # F407 V3.1 при защите чтения шлёт два NACK (AN2606, таблица 23)
            raise Nack("NACK: " + what)
        raise BootError("%s: ответ 0x%02X вместо 0x79" % (what, r))

    def _cmd(self, code):
        self._send((code, code ^ 0xFF))
        self._ack("команда 0x%02X" % code)

    def _addr(self, addr):
        a = addr.to_bytes(4, "big")
        self._send(a + bytes((xor(a),)))
        self._ack("адрес 0x%08X" % addr)

    def sync(self, tries=2):
        """0x7F — по нему загрузчик меряет скорость. NACK на повторный 0x7F значит: связь уже была."""
        for _ in range(tries):
            self.port.write(b"\x7f")
            self.tx += 1
            if self.echo:
                self.port.read(1)
            r = self.port.read(1)
            self.rx += len(r)
            if r == bytes((ACK,)):
                return "ack"
            if r == bytes((NACK,)):
                return "уже"
            if r == b"\x7f":
                raise BootError("эхо: вернулся свой 0x7F — включите echo=True")
            if r:
                raise BootError("ответ 0x%02X вместо 0x79: скорость или формат кадра" % r[0])
        raise BootError("нет ответа на 0x7F: BOOT0, сброс, TX/RX, питание")

    def get(self):
        self._cmd(GET)
        n = self._recv(1)[0]
        data = self._recv(n + 1)
        self._ack("Get")
        self.version, self.cmds = data[0], data[1:]
        return self.version, self.cmds

    def get_version(self):
        self._cmd(GV)
        data = self._recv(3)
        self._ack("Get Version")
        return data[0], data[1], data[2]

    def get_id(self):
        self._cmd(GID)
        n = self._recv(1)[0]
        pid = self._recv(n + 1)
        self._ack("Get ID")
        return int.from_bytes(pid, "big")

    def read_memory(self, addr, n):
        if not 1 <= n <= 256:
            raise ValueError("Read Memory: от 1 до 256 байт")
        self._cmd(READ)
        self._addr(addr)
        self._send((n - 1, (n - 1) ^ 0xFF))
        self._ack("длина %d" % n)
        return self._recv(n)

    def write_memory(self, addr, data):
        data = bytes(data)
        if not 1 <= len(data) <= 256 or len(data) % 4 or addr % 4:
            raise ValueError("Write Memory: 4…256 байт, кратно 4, адрес кратен 4")
        self._cmd(WRITE)
        self._addr(addr)
        block = bytes((len(data) - 1,)) + data
        self._send(block + bytes((xor(block),)))
        self._ack("запись 0x%08X" % addr)

    def go(self, addr):
        self._cmd(GO)
        self._addr(addr)

    def erase(self, pages):
        """Erase 0x43 (версии до 3.0): номера страниц по байту, не больше 255 за раз."""
        pages = list(pages)
        if not 1 <= len(pages) <= 255 or max(pages) > 254:
            raise ValueError("Erase: от 1 до 255 страниц с номерами до 254")
        self._cmd(ERASE)
        block = bytes([len(pages) - 1] + pages)
        self._send(block + bytes((xor(block),)))
        self._ack("стирание %d стр." % len(pages))

    def extended_erase(self, pages):
        """Extended Erase 0x44 (версии 3.0 и новее): номера по два байта, старший первым."""
        pages = list(pages)
        block = (len(pages) - 1).to_bytes(2, "big") + b"".join(p.to_bytes(2, "big") for p in pages)
        self._cmd(EXT_ERASE)
        self._send(block + bytes((xor(block),)))
        self._ack("стирание %d стр." % len(pages))

    def mass_erase(self):
        if EXT_ERASE in self.cmds:
            self._cmd(EXT_ERASE)
            self._send(b"\xff\xff\x00")
        else:
            self._cmd(ERASE)
            self._send(b"\xff\x00")
        self._ack("стирание всей памяти")

    def write_protect(self, sectors):
        sectors = list(sectors)
        self._cmd(WP)
        block = bytes([len(sectors) - 1] + sectors)
        self._send(block + bytes((xor(block),)))
        self._ack("защита записи")      # потом загрузчик сбрасывает кристалл: снова sync()

    def write_unprotect(self):
        self._cmd(WUP)
        self._ack("снятие защиты записи")

    def readout_protect(self):
        self._cmd(RP)
        self._ack("защита чтения")

    def readout_unprotect(self):
        self._cmd(RUP)
        self._ack("снятие защиты чтения")  # перед этим ACK загрузчик стирает всю флеш-память


def plan_erase(pid, base, size, flash=0x08000000):
    """Какие страницы или секторы задевает образ [base, base + size)."""
    if pid not in LAYOUT:
        raise BootError("PID 0x%03X: раскладка флеш-памяти неизвестна" % pid)
    kind, sizes = LAYOUT[pid]
    out, start = [], flash
    for i, sz in enumerate(sizes):
        if start < base + size and base < start + sz:
            out.append(i)
        start += sz
    return kind, out


def program(boot, image, base=0x08000000, verify=True, run=True, log=print):
    """Целиком: связь, что за кристалл, стирание нужного, запись по 256 байт, сверка, запуск."""
    image = bytes(image) + b"\xff" * (-len(image) % 4)
    boot.sync()
    ver, cmds = boot.get()
    pid = boot.get_id()
    log("загрузчик %d.%d, PID 0x%03X, команд %d" % (ver >> 4, ver & 15, pid, len(cmds)))
    kind, pages = plan_erase(pid, base, len(image))
    if EXT_ERASE in cmds:
        boot.extended_erase(pages)
    elif ERASE in cmds:
        boot.erase(pages)
    else:
        raise BootError("нет команды стирания")
    log("стёрто: %s %d…%d (%d)" % (kind, pages[0], pages[-1], len(pages)))
    t0 = boot.tx + boot.rx
    for off in range(0, len(image), 256):
        boot.write_memory(base + off, image[off:off + 256])
    log("записано %d байт блоками по 256: %d блоков, %d байт в линии" % (len(image), (len(image) + 255) // 256, boot.tx + boot.rx - t0))
    if verify:
        t0 = boot.tx + boot.rx
        for off in range(0, len(image), 256):
            n = min(256, len(image) - off)
            if boot.read_memory(base + off, n) != image[off:off + n]:
                raise BootError("сверка: расхождение в блоке 0x%08X" % (base + off))
        log("сверка совпала, %d байт в линии" % (boot.tx + boot.rx - t0))
    if run:
        boot.go(base)
        log("Go 0x%08X" % base)


def enter_bootloader(port, wait=0.1):
    """BOOT0 = 1 и импульс NRST: кристалл стартует из системной памяти. wait — время запуска загрузчика
    (AN2606, таблица 69: от 1,227 мс у F10xxx до 84,59 мс у F40xxx V3.x)."""
    port.set_boot0(1)
    port.set_reset(1)
    time.sleep(0.01)
    port.set_reset(0)
    time.sleep(wait)


def leave_bootloader(port):
    port.set_boot0(0)
    port.set_reset(1)
    time.sleep(0.01)
    port.set_reset(0)


def main(argv):
    if len(argv) < 3:
        print(__doc__)
        return 2
    base = int(argv[3], 0) if len(argv) > 3 else 0x08000000
    port = SerialPort(argv[1])
    image = open(argv[2], "rb").read()
    enter_bootloader(port)
    program(Stm32Boot(port), image, base)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

The bootloader model (model48d.py) follows AN3155 command by command and takes from AN2606 what depends on the chip: PID, version, erase command, RAM occupied by the bootloader, system memory addresses, ID 0x31 on F407 and the double NACK on F407 V3.1. Each command is a Python generator: a byte arrives — one step forward. Parsing the code and the write — in full:

    def _session(self):
        while True:
            c = yield from self._rx()
            x = yield from self._rx()
            if c ^ x != 0xFF:
                self._nack()
                continue
            h = {0x00: self._get, 0x01: self._gv, 0x02: self._gid, 0x11: self._read, 0x21: self._go,
                 0x31: self._write, self.c["erase"]: self._erase if self.c["erase"] == 0x43 else self._ext_erase,
                 0x63: self._wp, 0x73: self._wup, 0x82: self._rp, 0x92: self._rup}.get(c)
            if h is None:
                self._nack()
                continue
            if self.rdp and c not in RDP_ALLOWED:
                self._nack(double=self.c["double_nack"] and c in (0x11, 0x31))
                continue
            self.out.append(ACK)
            done = yield from h()
            if done == "reset":
                self.reset(clear=False)   # ответ ушёл, потом кристалл сбрасывается
                return
            if done == "go":
                return
    def _write(self):
        addr = yield from self._addr()
        mem, off = self._region(addr, 4, "write") if addr is not None else (None, None)
        if mem is None or addr % 4:
            self._nack()
            return
        self.out.append(ACK)
        n = yield from self._rx()
        data = yield from self._rxn(n + 1)
        cks = yield from self._rx()
        mem, off = self._region(addr, n + 1, "write")
        if xor(bytes([n]) + data) != cks or (n + 1) % 4 or mem is None:
            self._nack()            # модель: длина не кратна 4 — NACK (AN3155: N+1 должно быть кратно 4)
            return
        if mem is self.vrange:
            if data[0] > 4 or data[1:] != b"\xff\xff\xff":
                self._nack()
                return
        if mem is self.flash:
            for i, b in enumerate(data):
                if self._page_of(off + i) in self.wp:
                    continue        # AN3155: запись в защищённое — без ошибки, но и без записи
                old = mem[off + i]
                if old != 0xFF and old != b:
                    self._nack()    # модель: поверх незатёртого — ошибка записи
                    return
                mem[off + i] = b
        else:
            mem[off:off + n + 1] = data
        self.out.append(ACK)

Where the documents are silent, the model makes its own choice and marks it "model:": a byte with a parity error is silently dropped; a write with a length not a multiple of 4 and a write over unerased data get NACK; erasing a bank on the single-bank F407 gets NACK.

The run. The utility against the model, without a board:

GET chip=F103C8 sync=ack version=0x22 n=11 cmds=0001021121314363738292 gv=22-00-00 pid=0x410
GET chip=F407 sync=ack version=0x31 n=11 cmds=0001021121314463738292 gv=31-00-00 pid=0x413
F407 blid=0x31 vrange=3
Image Erased Blocks of 256 Bytes on the line: sync, erase, write Line time Bytes for verify Time
F103C8, 20000 bytes pages 0–19 79 21000 2.01 s 20948 2.00 s
F103C8, 65536 bytes pages 0–63 256 68704 6.56 s 68608 6.55 s
F407, 70000 bytes sectors 0–4 274 73331 7.00 s 73288 7.00 s

Line time is the bytes out and back × 11 bits / 115200 bit/s: this is how long the bytes themselves take, without Flash write and erase time. One write block is 265 bytes out and 3 back, 268 × 11 / 115200 = 25.6 ms, that is about 10000 bytes of data per second; verifying a block is 9 bytes out and 259 back, the same amount. The whole F103C8 (64 KB) goes over the wire in 6.56 s for writing and 6.55 s for verifying. In every run Go found the stack and the entry point in the image's vector table: sp=0x20005000, pc=0x08000101.

Failures and subtleties — the raw reply bytes:

ERR bad_complement=1f
ERR addr_bad_cks=791f
ERR write_ram_bootloader_area=791f
ERR write_ram_0x20000200=797979
ERR write_len6=79791f
ERR util_len6=Write Memory: 4…256 байт, кратно 4, адрес кратен 4
ERR go_sysmem=791f
ERR read_sysmem=00000000
ERR overwrite=NACK
ERR mass_erase_ff_01=7979 still_data=True
ERR mass_erase_ff_00=7979 erased=True

A wrong code complement — NACK at once; a wrong address checksum — ACK on the code, NACK on the address; the first 512 bytes of F103 RAM are occupied by the bootloader — NACK, from 0x20000200 the write goes through; Go into system memory — NACK, but you can read it. The utility itself does not allow a length of 6. FF 01 instead of FF 00 after Erase — ACK, but nothing is erased (AN3155, note to 3.7).

Readout protection, write protection, synchronization:

RDP chip=F103C8 after_rp_state=sync read_reply=1f util=Nack gv_rdp=22-00-00 pid=0x410 unprotect_blank=True mass_erase
RDP chip=F407 after_rp_state=sync read_reply=1f1f util=Nack gv_rdp=31-00-00 pid=0x413 unprotect_blank=True mass_erase
WP chip=F407 sectors=0 program=сверка:_расхождение_в_блоке_0x08000000 flash_first=ffffffff
WP after_unprotect ok=True
SYNC first=ack second=уже
SYNC echo_plain=эхо: вернулся свой 0x7F — включите echo=True
SYNC echo_mode sync=ack pid=0x410
SYNC boot0_low=нет ответа на 0x7F: BOOT0, сброс, TX/RX, питание
SYNC after_enter=ack

After Readout Protect the chip reset (after_rp_state=sync — it is waiting for 0x7F). Read on F103 gets one NACK, on F407 — two (1f1f): this is a known limitation of F40xxx V3.1 and V9.0 (AN2606, tables 23 and 25). After a NACK the utility flushes the receive buffer, and the next command starts with a clean slate. Get Version and Get ID work under protection too. After removing the protection the Flash is blank. With write protection on sector 0, erase and write on F407 passed with ACK, but the Flash holds ffffffff: only the verify caught the error.

The same code through real pyserial. In stm-pio:1 (pyserial 3.5, Python 3.12.14) the model sits on one end of a Linux pseudo-terminal, and the utility opens the other end as an ordinary serial port — SerialPort with even parity and a 1 s timeout:

PTY chip=F103C8 pyserial=3.5 parity=E dtr=OSError ok=True go=0x08000000_sp=0x20005000_pc=0x08000101
PTY   загрузчик 2.2, PID 0x410, команд 11
PTY   стёрто: страницы 0…19 (20)
PTY   записано 20000 байт блоками по 256: 79 блоков, 20948 байт в линии
PTY   сверка совпала, 20948 байт в линии
PTY   Go 0x08000000
PTY chip=F407 pyserial=3.5 parity=E dtr=OSError ok=True go=0x08000000_sp=0x20020000_pc=0x08000101
PTY   загрузчик 3.1, PID 0x413, команд 11
PTY   стёрто: секторы 0…4 (5)
PTY   записано 70000 байт блоками по 256: 274 блоков, 73288 байт в линии
PTY   сверка совпала, 73288 байт в линии
PTY   Go 0x08000000

dtr=OSError: a pseudo-terminal has no DTR and RTS lines; an adapter does have them — control of BOOT0 and NRST was tested only on the model.


48д.7 Pitfalls

Sync succeeded, but the first command gets silence. The port is opened as 8N1. The line model shows why: 0x7F has an odd number of ones, so the parity bit is a one, and the 8N1 stop bit passes for it; the chip replies 0x79. But for 00 FF (Get) the parity position again holds a stop bit equal to one, while a zero is needed — a parity error, the byte is dropped (parity_errors=1), and the utility waits for a reply in vain:

LINE host=115200/8E1 sync=ack dev_baud=115384.6 err=0.160% get=0x22 parity_errors=0
LINE host=9600/8E1 sync=ack dev_baud=9600.0 err=0.000% get=0x22 parity_errors=0
LINE host=115200/8N1 sync=ack dev_baud=115384.6 err=0.160% get=тайм-аут:_ждали_1_байт,_пришло_0 parity_errors=1
LINE garbage_first dev_baud=7303.7 sync=ответ_0x00_вместо_0x79:_скорость_или_формат_кадра

The cure is 8E1 in the port settings (parity=PARITY_EVEN).

The first byte is not 0x7F. When the port is opened or the adapter is powered up, garbage slips onto the line, and the bootloader measures the baud rate from it. In the model a 0x00 byte comes 1 ms before 0x7F: from its falling edge to the falling edge of 0x7F there are not 8 bits but more than a hundred; the chip tuned itself to 7303.7 bit/s instead of 115200 and replies with garbage (ответ 0x00 вместо 0x79). Only a reset and a clean 0x7F help. That is why AN2606 (3.3) requires holding the RX of the bootloader interfaces that are not used at a known level during interface selection: a floating line can activate the wrong one.

NACK in reply to 0x7F. The connection already existed — the utility was started a second time without a reset, and the bootloader took 0x7F for a command code. The utility sends 0x7F once more: the pair 7F 7F is not a code with its complement, so NACK, which means the channel is alive (second=уже in the run).

Received its own 0x7F. Echo: a half-duplex line, RS-485 or a single wire for TX and RX — the adapter returns every byte sent. The utility with echo=True reads back the echo after every transmission and compares it.

No reply at all. BOOT0 is not at one or there was no reset: the chip is in the application (boot0_low in the run). Also — TX and RX swapped, no common ground, or the bootloader did not have time to start (AN2606, table 69 — up to 84.59 ms on F40xxx V3.x).

DTR and RTS are "inverted". The polarity of DTR and RTS on the adapter pins depends on the chip and the board circuit, and when the port is opened the lines may toggle by themselves — the board resets at the moment the port opens. In SerialPort the polarity is inverted with flags; check it with a multimeter on your own adapter.

After Go the application behaves strangely. AN3155 (3.5) says that before the jump the bootloader's peripheral registers are returned to their reset values, and AN2606 (note to 3.1) says they are not; the documents disagree. The reliable way is not to rely on it: the application configures USART1, the clocks and everything of its own itself. If the hardware watchdog is enabled, the bootloader has set it to the largest prescaler (AN2606, table 6) — the application has to reconfigure it.

Written with ACK, but the Flash is empty. The sector is write-protected: the bootloader does not return an error (AN3155, 3.6). A read-back check after writing is not a luxury.

F407 writes slowly. By default the F2 and F4 bootloader writes Flash byte by byte (AN2606, 3.4 and table 22); writing 03 FF FF FF to address 0xFFFF0000 switches to words with a supply of 2.7–3.6 V.

Known limitations per AN2606. For F10xxx, table 7 lists no known limitations. On F40xxx V3.0, a Read or Write with an unsupported address and a correct checksum gets no NACK, and the next 2 bytes are read as a new command (table 23; fixed in V3.1); on V3.1 and V9.0 — two NACKs under readout protection, and over CAN Write Unprotect does not work — instead you write the option bytes with the Write Memory command (tables 23 and 25).


48д.8 Summary

Next: section 39 "How to read ST documentation".

Open in the interactive handbook