Back to Home

MBR 324 bytes STM32F407VE: compact bootloader

Developed a minimal MBR bootloader for STM32F407VE sized 324 bytes. Checks vector table before passing control to secondary bootloader at address 0x08060000. On error, blinks LED on PE13. Full code, Makefile and optimization steps.

324-byte MBR bootloader STM32F407VE with validation
Advertisement 728x90

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:

Google AdInline article slot

| Sector | Start Address | Size, KB | Contents |

|--------|---------------|----------|----------|

| 0 | 0x08000000 | 16 | Primary bootloader |

Google AdInline article slot

| 1–3 | 0x08004000–0x0800C000 | 16×3 | NVRAM |

| 4–6 | 0x08010000–0x08040000 | 64+128+128 | Application |

| 7 | 0x08060000 | 128 | Secondary bootloader |

Google AdInline article slot

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
  • -Os for 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

Advertisement 728x90

Read Next