2024 - 2026 Custom Bootloader Notes

@Ishan Deshpande

Initial Thoughts

Trying to get a bootloader up and running for our suite of STM32 microcontrollers.

Bootloader Design Details

After countless hours of messing with linker scripts and ARM assembly, I’ve successfully crafted a bootloader structure that seems to make sense.

Linker Script Generator

A linker script is a file that tells the linker where to place all of the object code in memory. It expects a bunch of input sections which are essentially “tagged” pieces of code or functions or data that are passed in by the compiler. Depending on the rules laid out in the linker script, it reorganizes these input sections and places them at discrete memory addresses on the chip.

image-20241105-160302.png

stm/generate_ld.sh is the linker script generator script I’ve written for this project. It takes in a .cfg file for a processor, which includes details specific to the STM32 processor you are using. As I write this, the stm32f446.cfg file looks like this:

MCU_NAME=stm32f446ret RAM_SIZE=128 TOTAL_FLASH_SIZE=512 VEC_TAB_OFS=0x1d0

The linker script generator fixes the BOOT_SIZE (bootloader size) at 128KiB. It uses the LINKER_TEMPLATE.ld file as a baseline, in which it replaces discrete sections or variable for both BOOT and FLASH linker scripts.

The linker script generator then spits out two files, STM32x4xx_APP.ld and STM32x4xx_BOOT.ld. We have two separate linker scripts because even though it’s all going on one processor, I’ve constructed two separate build processes for the application and for the bootloader.

Build Process

 

image-20241105-161115.png

The bootloader is set up as a separate project. The rationale behind this was that developers should not have to reflash the bootloader every time they want to update their application. It incurs extra memory costs (flash has a limited number of erase cycles) and could potentially destroy the bootloader if interrupted or if flashed incorrectly, meaning that developers would need to JTAG into their board to fix it (destroying the point of this entirely).

We pass a separate set of sources/includes, linker script (STM32*_BOOT.ld) and a separate flash address for the bootloader, but use the same base Makefile as our application as the flags and compilation process for gcc stays mostly the same.

The application has the typical startup flow. Note that the flash address is not passed in for the application, as the Makefile sets the default FLASH_ADDRESS to 0x08020000.

Memory Layout

Address Range

Section

Address Range

Section

0x20000000 - 0x20020000 (128KiB)

RAM: The bootloader and the application use a shared stack here.

0x08000000 - 0x08020000 (128KiB)

BOOT: The bootloader code & data and the boot vector table is stored here. Our STM32 processors expect the vector table to be at 0x08000000 on reset so that it knows where it can find the Reset_Handler.

0x08020000 - (0x08020000 + APP_FLASH_SIZE) (APP_FLASH_SIZE)

FLASH: We call this FLASH in the linker script even though BOOT is technically also stored in flash memory. This includes the application code & data and the application vector table. The bootloader and the application have two separate vector tables, which will make a bit more sense in context with the program startup flow flowchart.

Program Startup Flow

A few notes on this:

  • We reset the stack pointer so that we don’t accidentally leave application data on the stack when executing the bootloader, and so that we don’t accidentally leave bootloader data on the stack when executing the application

  • The nice thing about this structure is that the user can still define a Reset_Handler that runs on reset, but it will only run after the bootloader checks for a flash message. Thus the user will still have the impression that their reset handler runs immediately on reset and they do not have to deal with any logic for “jumping to the bootloader”.