Compact MBR Bootloader for STM32F407VE: Just 324 Bytes
This primary bootloader (MBR) for the STM32F407VE sits in the first Flash sector at 0x08000000 and hands off control to the secondary bootloader at 0x08060000. Before jumping, it validates the interrupt vector table: stack pointer in SRAM range (0x20000000–0x2001FFFF), ResetHandler address in Flash (0x08000000–0x080FFFFF) with the Thumb mode bit set.
If validation fails, it signals an error by blinking the LED on pin PE13 at 10 Hz (100 ms period, 50% duty cycle). The code is trimmed to 324 bytes by ditching the standard library, external interrupts, and using the DWT core timer for delays.
NOR Flash layout:
| Sector | Start Address | Size, KB | Contents |
|--------|---------------|----------|----------|
| 0 | 0x08000000 | 16 | Primary bootloader |
| 1–3 | 0x08004000–0x0800C000 | 16×3 | NVRAM |
| 4–6 | 0x08010000–0x08040000 | 64+128+128 | Application |
| 7 | 0x08060000 | 128 | Secondary bootloader |
Every byte shaved from the MBR expands NVRAM space.
Optimization Stages
Optimization was iterative using ARM-GCC and GNU Make:
- Naive HAL implementation: 25,192 bytes.
- -O0 no debug: 12,052 bytes.
- -Os -flto: 9,684 bytes.
- DeepSeek + -Os -flto: 2,124 bytes.
- -nostdlib, no libc_init: 652 bytes.
- Minimal vector table: 324 bytes.
Key compiler flags:
-nostdlib -nostartfiles -ffreestanding-ffunction-sections -fdata-sections -Wl,--gc-sections-Osfor size,-mcpu=cortex-m4 -mthumb
C Implementation
The core code in a single main.c file accesses registers directly, skipping HAL:
// main.c - Bootloader for STM32F407VE
#include <stdint.h>
#define APP_BASE 0x08060000UL
#define SRAM_BASE 0x20000000UL
#define SRAM_END 0x2001FFFFUL
#define FLASH_BASE 0x08000000UL
#define FLASH_END 0x080FFFFFUL
// Peripherals
#define RCC_BASE 0x40023800UL
#define GPIOE_BASE 0x40021000UL
#define DWT_BASE 0xE0001000UL
#define RCC_AHB1ENR (*((volatile uint32_t*)(RCC_BASE + 0x30)))
#define GPIOx_MODER (*((volatile uint32_t*)(GPIOE_BASE + 0x00)))
#define GPIOx_ODR (*((volatile uint32_t*)(GPIOE_BASE + 0x14)))
#define DWT_CYCCNT (*((volatile uint32_t*)(DWT_BASE + 0x04)))
#define DWT_CTRL (*((volatile uint32_t*)(DWT_BASE + 0x00)))
typedef struct {
uint32_t stack_ptr;
uint32_t reset_handler;
} VectorTable_t;
static inline int is_valid_sram(uint32_t addr) {
return ((SRAM_BASE <= addr) && (addr <= SRAM_END));
}
static inline int is_valid_flash(uint32_t addr) {
return ((FLASH_BASE <= addr) && (addr <= FLASH_END));
}
// Initialize DWT
static void dwt_init(void) {
*(volatile uint32_t*)0xE000EDF0 |= (1UL << 24);
DWT_CTRL |= 1;
}
// Delay ~ms at 168 MHz
static void delay_ms(uint32_t ms) {
uint32_t start = DWT_CYCCNT;
uint32_t cycles = ms * 16000;
while ((DWT_CYCCNT - start) < cycles);
}
// Blink LED PE13 at 10 Hz
static void blink_led(void) __attribute__((noreturn));
static void blink_led(void) {
RCC_AHB1ENR |= (1 << 4);
GPIOx_MODER &= ~(3 << 26);
GPIOx_MODER |= (1 << 26);
dwt_init();
while(1) {
GPIOx_ODR |= (1 << 13);
delay_ms(50);
GPIOx_ODR &= ~(1 << 13);
delay_ms(50);
}
}
static int is_valid_vector_table(const VectorTable_t *app_vec) {
if (!is_valid_sram(app_vec->stack_ptr)) return 0;
uint32_t reset_addr = app_vec->reset_handler;
if (!is_valid_flash(reset_addr & ~1)) return 0;
if ((reset_addr & 1) == 0) return 0;
return 1;
}
typedef void (*pFunction)(void);
int main(void) {
const VectorTable_t *app_vec = (const VectorTable_t*)APP_BASE;
if (is_valid_vector_table(app_vec)) {
pFunction jump = (pFunction)app_vec->reset_handler;
jump();
}
blink_led();
}
Project Build
Makefile with debug and packaging support:
CFLAGS += -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=softfp
CFLAGS += -Os -ffunction-sections -fdata-sections -nostdlib
CFLAGS += -nostartfiles -fno-builtin -ffreestanding
LDFLAGS += -T gcc_arm_mbr.ld -Wl,--gc-sections
SOURCES = main.c system_stm32f4xx.c
OBJECTS = $(SOURCES:.c=.o) startup_stm32f407xx.o
all: $(TARGET).bin
In startup_stm32f407xx.S, we removed bl __libc_init_array and trimmed the vector table to essentials (Stack, Reset, NMI, HardFault).
Key Takeaways
- 324-byte size frees up 15+ KB for NVRAM in the first sector.
- Vector table validation prevents hangs on corrupted code.
- DWT timer delivers precise delays without SysTick or PLL.
- Direct register access cuts overhead to the bone.
- Full build: ARM-GCC + custom linker script, no standard library.
— Editorial Team
No comments yet.