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

48. A custom bootloader and firmware updates

While the device is on your desk, the debugger changes the program. As soon as it has gone to the customer, you need a way to update without a debugger, without opening the enclosure and without the risk of turning the unit into a brick halfway through. This is the job of a bootloader: a small program that starts first, decides what to run, and can accept new firmware.

This section is about how it is built: the memory layout, handing over control, the image format, the integrity check, rollback to the previous version and over-the-air updates. The mechanics of writing to Flash are covered in section 11, the factory bootloader and BOOT0 in section 4; here all of it is put together into a working scheme.

Terms used in the documentation

Term Meaning
bootloader the program that starts first and loads the application
application, image the firmware image
slot, bank a region that holds an image
vector table offset (VTOR) where the vector table is located
main stack pointer (MSP) the main stack pointer
reset handler the reset handler, the entry point
Thumb bit the lowest address bit: marks the instruction set
Intel HEX a text image format with addresses
firmware over the air (FOTA, OTA) updating over a wireless link
rollback returning to the previous version
confirm / revert confirming that it works / going back
in-application programming (IAP) writing Flash from your own program
read-out protection (RDP) protecting Flash from being read
write protection (WRP) protecting pages from being written

48.1 When you need one and when you do not

The honest answer: more often you do not. A sensor that counts visitors is agreed with the customer at the prototype stage and the program is flashed in production; an improvement ships with the next batch, and old units are simply replaced.

A bootloader is needed where the value of the device is in the program and where physical access is expensive:

And a sober note right away: your own bootloader is a separate program that cannot be updated the same way. It must be simple, debugged and unchanged for years. Everything that can be moved into the application is moved into the application.


48.2 Memory layout

Flash layout and handing over control

The bootloader sits at the start of Flash — it is what starts from the reset address. The rest is divided into slots for images. A scheme with two slots — active and receiving — costs twice the memory but removes the main fear: an update can be interrupted at any moment, and the running program stays intact.

One subtlety shapes the whole scheme: the application is built for its own address (48.5) and runs only from there — the vector table and the code contain absolute addresses. An image built for slot A will not run from slot B; the check from 48.4 will not even let it through — the entry point will be outside the slot. There are three ways out:

#define BOOT_SIZE      (16u * 1024u)
#define SLOT_SIZE      ((FLASH_BYTES - BOOT_SIZE) / 2u)
#define SLOT_A_ORIGIN  (FLASH_ORIGIN + BOOT_SIZE)
#define SLOT_B_ORIGIN  (SLOT_A_ORIGIN + SLOT_SIZE)
Chip Flash Erase unit Bootloader Slot
F103 128 KB 1 KB page 16 KB 56 KB
G474 512 KB 2 KB page 16 KB 248 KB
F407 1 MB 16…128 KB sector 128 KB 384 KB

⚠️ On the F4 the formula does not work. The sectors differ in size: four of 16 KB, one of 64 KB, then 128 KB each. Splitting "in half after the bootloader" gives 504 KB — not a multiple of any sector, and you cannot erase half a sector. The layout is set from the map: sectors 0…4 are the bootloader (128 KB), 5…7 and 8…10 are two slots of 384 KB, sector 11 is left for settings.

This is a case where _Static_assert pays off at once: slot boundaries are checked for being a multiple of the erase unit at compile time, not at the first update in the field.


48.3 The image header and alignment

In front of the firmware in the slot there is a header: everything needed to decide whether to run it or not.

typedef struct {
    uint32_t magic;         /* образ вообще есть */
    uint32_t size;          /* длина прошивки за заголовком, байт */
    uint32_t crc;           /* CRC-32 этих size байт */
    uint32_t version;       /* чтобы не откатиться назад молча */
    uint32_t entry;         /* адрес входа — проверяется на попадание в область */
    uint32_t flags;         /* признак «принята, но ещё не подтверждена» */
    uint32_t reserved[2];
    uint8_t  pad[IMG_HDR_SIZE - 8u * 4u];
} img_hdr_t;

32 bytes would be enough for the header, but it is padded to 512. The reason is not room for the future but alignment: right after the header the application's vector table begins, and its address must be a multiple of the size of the table rounded up to a power of two. The vector table offset register simply does not store the lower seven bits — you cannot write 0x08004020 there, and the application will fault on its very first interrupt.

The second requirement: on the G4 Flash is written in double words, so the size of the header must be a multiple of eight. Both conditions are checked at build time.


48.4 Checking the image before running it

The first two words of any Cortex-M firmware are the initial value of the stack pointer and the address of the reset handler. The bootloader looks at them before jumping: if they do not look plausible, the application will crash before its first instruction, and finding the cause will be hard.

static int image_valid(uint32_t slot)
{
    const img_hdr_t *h = (const img_hdr_t *)slot;
    const uint32_t *vec = (const uint32_t *)(slot + IMG_HDR_SIZE);
    uint32_t sp, pc;

    if (h->magic != IMG_MAGIC)
        return 0;
    if (h->size == 0u || h->size > SLOT_SIZE - IMG_HDR_SIZE)
        return 0;

    sp = vec[0];
    pc = vec[1];
    /* Указатель стека должен смотреть в ОЗУ. */
    if (sp < RAM_ORIGIN || sp > RAM_ORIGIN + RAM_BYTES)
        return 0;
    /* Адрес входа — внутрь этой же области и с признаком Thumb. */
    if ((pc & 1u) == 0u)
        return 0;
    if (pc < slot + IMG_HDR_SIZE || pc >= slot + SLOT_SIZE)
        return 0;

    return crc32((const void *)(slot + IMG_HDR_SIZE), h->size) == h->crc;
}

The checks go from cheap to expensive: magic number, length, plausibility of the stack and the entry point — and only then the CRC over the whole image, which takes milliseconds.

What a CRC gives and what it does not. It catches corruption: an interrupted transfer, a write failure, cell degradation. It does not protect against substitution: anyone can compute the CRC of their own firmware. If it matters that the device accepts only your updates, you need a signature — an HMAC on a shared secret or, better, a digital signature with a key pair (section 43). Signature verification lives in the bootloader, and the verification key in its own immutable part.


48.5 Handing over control

The order here matters more than the code.

static void jump_to_app(uint32_t slot)
{
    uint32_t sp = *(const uint32_t *)(slot + IMG_HDR_SIZE);
    uint32_t pc = *(const uint32_t *)(slot + IMG_HDR_SIZE + 4u);
    uint32_t i;

    __disable_irq();

    /* Снять все разрешения и все висящие запросы: приложение начинает с чистого. */
    for (i = 0u; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++) {
        NVIC->ICER[i] = 0xFFFFFFFFu;
        NVIC->ICPR[i] = 0xFFFFFFFFu;
    }
    SysTick->CTRL = 0u;
    SysTick->LOAD = 0u;
    SysTick->VAL  = 0u;

    SCB->VTOR = slot + IMG_HDR_SIZE;    /* где искать векторы теперь */
    __DSB();
    __ISB();

    /* Стек, разрешение прерываний и переход — одной вставкой: после смены MSP
     * компилятору нечего читать из кадра старого стека. Сюда уже не вернёмся. */
    __asm volatile ("msr msp, %0\n cpsie i\n bx %1" :: "r"(sp), "r"(pc));
}

Why exactly this way:


48.6 How the application requests reflashing

The bootloader needs to know whether this is a normal start or the user requested an update. The options, from simple to reliable:

Where the flag is kept Survives Cost
a RAM variable in a section that is not zeroed software reset nothing; lost when power is removed
backup registers (VBAT domain) power removal with a battery needs the backup domain
a Flash page everything an erase cycle for every request
a pin/button at start-up everything needs physical access

The most common is the first, and it is enough: an update always starts from a running application, and it reboots in software.

#define REBOOT_MAGIC   0xB00710ADu
uint32_t reboot_flag __attribute__((section(".noinit")));

static void request_update(void)
{
    reboot_flag = REBOOT_MAGIC;
    NVIC_SystemReset();                  /* сброс без снятия питания */
}

static int update_requested(void)
{
    int yes = (reboot_flag == REBOOT_MAGIC);
    reboot_flag = 0u;                    /* прочитали — сбросили, иначе зациклимся */
    return yes;
}

The section must be excluded from zeroing in the startup code and in the linker script; otherwise the flag is lost before main. And it must be read exactly once, with a reset: otherwise the device will stay in the bootloader forever.

And the key condition: the flag lives at the same address in both programs. The bootloader and the application are each built with their own linker script, and the .noinit section of each ends up wherever its own variables ended: in the test build for the F103 it is 0x20000800 in the bootloader and 0x20000258 in the application. The bootloader would read a different cell from the one the application wrote. So both scripts get the same separate region at the end of RAM — for 20 KB it is NOINIT (rwx) : ORIGIN = 0x20004FF0, LENGTH = 16 — and the section is placed there, while RAM is shortened by the same 16 bytes so that the top of the stack lies below it. You can also access the address directly: #define REBOOT_FLAG (*(volatile uint32_t *)0x20004FF0u).


48.7 Image format: binary or Intel HEX

A binary file is exactly the contents of Flash: no addresses, no markup. Intel HEX is a text format in which every line carries a piece of data and the address where to put it.

:020000040800F2
:1000000000000120110A0008190900081F09000852
:00000001FF

A line: a colon, the data length, the address (two bytes), the record type, the data, the checksum. Types: 00 — data, 01 — end of file, 04 — extended linear address (sets the upper half of the address). A file from the linker usually also has 05 — the start address: objcopy puts it before the end of file. The entry point is in the second word of the vector table anyway, so 05 is skipped — as is 03, the same kind of start address for the segmented memory of old x86. Record 02, the segment address, is used by some tools, and it must not be skipped: its value multiplied by 16 is added to the addresses of the following lines. There are six types in total, and the parser must know all of them: one that understands only 00, 01 and 04 and rejects the rest will refuse a real firmware file.

The checksum is the two's complement of the sum of all other bytes, so the sum of all bytes of a line, including it, gives zero in the low byte — and the check is built on that:

for (i = 0u; i < n; i++)
    sum += raw[i];
if ((sum & 0xFFu) != 0u)
    return 0;                        /* сумма всех байт, включая контрольную, — ноль */

Parsing the example above gives: base address 0x08000000, the first word 0x20010000 — the top of RAM, the second 0x08000A11 — the entry point with the low bit set. Exactly what the bootloader checks.

The two Intel HEX lines from the example, field by field: length, address, type, data, checksum

Which to choose. HEX is convenient because it is self-checking and carries addresses — but 16 bytes of data turn into 45 bytes of file: three times more. A 60 KB firmware over UART at 115200 takes 5.3 seconds as a binary file and 15 seconds as HEX. For a link paid per megabyte the difference is decisive. A practical compromise: HEX for the desktop tool, a binary image with a header for the device.


48.8 Receiving, writing, confirming

How long it takes. Numbers for the G474 (typical Flash characteristics):

Action Time
erasing a 2 KB page 22.0 ms
writing a 2 KB page 20.9 ms
erasing a whole 248 KB slot 2.73 s
writing a whole slot 2.59 s
full slot update 5.3 s
erasing and writing only 60 KB of firmware 1.29 s

Two conclusions follow. First: an update is a process of seconds during which the device is busy; you need to show this to the user and take it into account in the watchdog (section 12). Second: erasing the whole slot when the image is four times smaller wastes four seconds for nothing.

How to receive. In page-sized chunks, acknowledging each chunk, with the ability to resume from where it stopped: the link is unreliable, and starting over from the middle is annoying. A received chunk is written at once — the whole image will not fit in the microcontroller's RAM.

Confirming that it works. New firmware can pass the CRC and still not work: the wrong hardware, a bug in a new branch. The protection scheme has three steps:

  1. The bootloader marks the new image with a "trial" flag and runs it.
  2. The application, once it is sure it is alive (the link is up, the sensors respond), sets the "confirmed" flag.
  3. If after a reset the image is still on "trial", the bootloader treats the attempt as failed and goes back to the previous slot.

How to store these flags. In Flash they are not rewritten but appended: a word can be written only from the erased state, and erasing the page with the header means erasing the start of the image too. On the G4 writes go in double words with an error-correcting code, and into a double word already written you can at most write zeros. So each step has its own cell in the header (on the G4 a double word each; there is room in the pad padding): "ran on trial" and "confirmed". An erased cell (all ones) means "not yet", a written one means "yes"; these cells are skipped when the image is written. The bootloader writes the first before the trial run, the application writes the second; if after a reset the first is written and the second is not — rollback.

The watchdog is an ally here: an application that hangs before confirming reboots by itself and rolls back.

The life of an image: the bootloader checks the CRC and writes the "trial" cell, the application writes the "confirmed" cell; a reset before confirmation leads to rollback to the previous slot, and an image left unfinished because power was lost is rejected

What is stored where so that rollback is possible. The previous image stays in place — that is the whole point of two slots. The bootloader remembers which one is active from the flags in the headers and the version number; it runs an image only from the slot whose address the image was built for (48.2).


48.9 Protection

The bootloader is responsible not only for the update going through but also for only the right one going through.

Measure Against what Where it is configured
CRC-32 of the image corruption in transfer and writing image header
image signature firmware substitution header + key in the bootloader
image encryption reading the firmware in the link and on disk key in the bootloader (section 43)
read-out protection (RDP) reading Flash with a debugger option bytes (section 11.6)
page write protection (WRP) corruption of the bootloader itself option bytes
version check rollback to an old version with a known hole image header

About keys: an array in the sources is not protection. The key must be in a region closed to reading, and preferably unique to each unit. A key shared by the whole batch means that opening one unit compromises them all.

⚠️ Read-out protection is removed only together with erasing the whole Flash. Once you enable it, you can no longer read the firmware — including your own during debugging. The second level is irreversible on some families.


48.10 Over-the-air updates

The link may be a wireless module, a cellular modem, USB, an SD card, a wired interface; the bootloader does not care, it needs a byte stream. The difference is where the image is kept before it is written:

What the protocol must have regardless of the link: the length and checksum of the whole image up front, numbered chunks, acknowledgement of each, resumption from the point of interruption, a timeout for the whole session. And a log: what version, when it arrived, how it ended — otherwise there will be nothing to analyze a failure in the field by.


48.11 Ready-made paths if you do not need your own

Before writing a bootloader, check whether a ready-made one is enough:

You write your own bootloader when you need your own link, your own format or your own verification rules — that is, almost always if updates go over the air.


48.12 Pitfalls

A fault right after the jump. VTOR was not moved, or the application was built for address 0x08000000 but sits at another one.

Works until there are interrupts. The same VTOR: the vectors are looked up in the bootloader.

The image from the second slot fails the check or crashes. It was built for the address of the first: you need a separate image per slot, copying into the active slot, or a bank swap (48.2).

The vector table "will not align". The image header is not a multiple of the required alignment. Pad it to 512 bytes.

The device is stuck in the bootloader forever. The request flag is not cleared after reading, or the no-zeroing section is zeroed by the startup code after all.

The application requests reflashing, but the bootloader does not see it. The flag in .noinit is at different addresses in the two programs: you need a common region in both linker scripts.

The firmware was received but does not start. Check the order: was a byte lost in transfer, does the length in the header match what was actually written, is the CRC computed over the same boundaries as at build time.

An erase fails. A slot boundary does not match a page or sector boundary; on the F4 — an attempt to use the formula instead of the sector map.

The watchdog interrupts the update. A page erase takes tens of milliseconds; either refresh the watchdog inside the write loop or use a longer interval for the duration of the update.

Power was lost in the middle of a write. This is exactly why there are two slots: the active one is intact, the bootloader sees the unfinished image by its flag and runs the previous one.

New firmware "bricks" the device. There is no confirmation that it works and no rollback.

The Flash endurance ran out. 10000 cycles is 27 years with an update once a day, but only a week if the application writes settings to the same page once a minute. Put settings in a separate page, with rotation.

The bootloader turned out to have a bug. It cannot be updated by the same mechanism. Hence the rule: minimal code, no experiments, thorough testing before a batch is released.


48.13 Summary

Next: section 48а "Firmware signing: the bootloader checks who built it".

Open in the interactive handbook