Самый маленький загрузчик (MBR)

от автора

» 3кБ — это ж очень много. «

Пролог

В программировании на STM32 бывает нужно сделать так, чтобы загрузчик оказался не в начале Flash памяти, а в самом конце. Это объясняется тем, что между загрузчиком и приложением надо как-то вклинить NVRAM (Non-Volatile Random-Access Memory).

Перед вами разметка NOR FLASH памяти для STM32F407VE.

Sector 

Start Address

size, kByte

содержимое

0

0x0800 0000

16

первичный загрузчик

1

0x0800 4000

16

NVRAM

2

0x0800 8000

16

NVRAM

3

0x0800 C000

16

NVRAM

4

0x0801 0000

64

Generic прошивка

5

0x0802 0000

128

Generic прошивка

6

0x0804 0000

128

Generic прошивка

7

0x0806 0000

128

Вторичный загрузчик

Поэтому надо написать отдельную крохотную прошивку первичного загрузчика, которая просто при старте передает управление на другой адрес в физической памяти (0x0806_0000). Такие прошивки я называю MBR (Master Boot record).

В случае с STM32 первичный загрузчик всегда стартует с начального адреса начала FLASH памяти: 0x0800_0000. Всё, что по-большому счету требуется от mbr — это прыгнуть на константный адрес. Как правило, на вторичный загрузчик, который у меня обычно прописан в самом последнем секторе flash памяти (0x0806_0000). Это тот который 128k Byte. Причем прыгать надо с осторожностью. Надо сперва проверить, что там в самом деле лежит валидная таблица векторов прерываний. Иначе же просто зависнем! Если таблица содержит абсурдные значения, то не прыгаем туда, а просто крутимся в суперцикле и даем на LEDы какую-н индикацию об ошибке. Если бы все секторы FLASH памяти были одного размера (как у всех нормальных производителей микроконтроллеров), например по 8kByte, то не было бы и необходимости в отдельной сборке под названием mbr. Мы бы просто прописали начиная с 0x08000000 полноценный загрузчик любого размера, который бы стартовал при подаче питания и запускал приложение.

Постановка задачи:

Написать на языке программирования Си для микроконтроллера STM32F407VE прошивку (для платы JZ-F407VET6), которая при старте с адреса 0x0800_0000 сразу прыгает исполнять код по адресу 0x0806_0000. Перед прыжком в новый адрес следует проверить, что в 0x0806_0000 в самом деле прописана адекватная таблица векторов прерываний. Убедиться, что адрес указателя на вершину стека в самом деле принадлежит диапазону RAM памяти, что адрес ResetHandler в самом деле принадлежит интервалам Flash памяти, а сами инструкции собраны в режиме Thumb. Если по адресу 0x0806_0000 нет корректной таблицы векторов прерываний, то не прыгать туда, а просто мигать LEDом на пине PE13 с частотой 10 Hz и скважностью 50%.

Постараться утрамбовать эту прошивку как можно более компактно! Прерывания, SysTick, PLL даже включать не надо. Временные отметки в случае необходимости брать от ядерного таймера DWT.
Можно весь основной код для наглядности разместить в одном лишь файле main.c.
Использовать файлы startup_stm32f407xx.S, system_stm32f4xx.c, STM32F407VETx_FLASH.ld.
Собирать проект компилятором ARM-GCC из самостоятельно написанного GNU Make скрипта.

Прошивка MBR нужна того лишь для того, чтобы запустить другую прошивку, которая прописана где-то по другому адресу в физической памяти микроконтроллера.

Зачем уменьшать размер загрузчика?

Вы можете спросить:

Зачем в 2026 году вообще нужно уменьшать размер прошивки?

Как известно памяти с микроконтроллерах сейчас много. Вот на том же STM32F407VET6 заложено 512 kByte Flash памяти программ. Утрамбовывать прошивки по моей памяти, вообще говоря, уже редко приходится. Однако первичный загрузчик на STM32 — это особый случай. Дело в том, что сразу за MBR следует NVRAM. И каждый байт высвобожденный из первичного загрузчика увеличивает вместительность вашей on-chip NVRAM. Вот такие дела.

Реализация
При наивной реализации у меня получилась прошивка размером 25192 bytes. Далее я предпринял еще некоторые паллиативные меры и довел прошивку до размера 9684. Что делать дальше мне было уже и не ясно. В итоге я обратился к deepseek.

Версии прошивки

размер bin файла, byte

Наивная реализация на основе STM32 HAL

25192

Отключены отладочные символы -g3 -O0

12052

Собрана с ключами -Os -flto

9684

Сгенерировал DeepSeek (c флагами -O0 -g3)

2360

Сгенерировал DeepSeek (c флагами -Os -flto)

2124

вот что мне сгенерировал DeepSeek

// main.c - Bootloader for STM32F407VE// Jumps to 0x080E0000 after validating vector table#include <stdint.h>// Memory addresses#define APP_BASE        0x08060000UL#define SRAM_BASE       0x20000000UL#define SRAM_END        0x2001FFFFUL  // 128KB for STM32F407VE#define FLASH_BASE      0x08000000UL#define FLASH_END       0x080FFFFFUL  // 1MB// Peripherals#define RCC_BASE        0x40023800UL#define GPIOE_BASE      0x40021000UL#define DWT_BASE        0xE0001000UL#define CoreDebug_BASE      (0xE000EDF0UL)#define CoreDebug_DEMCR_TRCENA_Pos         24U                                            /*!< CoreDebug DEMCR: TRCENA Position */#define CoreDebug_DEMCR_TRCENA_Msk         (1UL << CoreDebug_DEMCR_TRCENA_Pos)typedef struct {  volatile uint32_t DHCSR;                  /*!< Offset: 0x000 (R/W)  Debug Halting Control and Status Register */  volatile uint32_t DCRSR;                  /*!< Offset: 0x004 ( /W)  Debug Core Register Selector Register */  volatile uint32_t DCRDR;                  /*!< Offset: 0x008 (R/W)  Debug Core Register Data Register */  volatile uint32_t DEMCR;                  /*!< Offset: 0x00C (R/W)  Debug Exception and Monitor Control Register */} CoreDebug_Type;#define CoreDebug           ((CoreDebug_Type *)     CoreDebug_BASE)// Register offsets#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)))// Vector table entry typetypedef struct {    uint32_t stack_ptr;    uint32_t reset_handler;} VectorTable_t;// Check if address is in SRAM rangestatic inline int is_valid_sram(uint32_t addr) {    return (( SRAM_BASE <= addr) && (addr <= SRAM_END));}// Check if address is in Flash rangestatic inline int is_valid_flash(uint32_t addr) {    return ((FLASH_BASE <= addr) && (addr <= FLASH_END));}// Initialize DWT cycle counterstatic void dwt_init(void) {    // Enable DWT in debug component (optional)    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // set bit 24    DWT_CTRL |= 1;}// Delay using DWT (approximate for 168MHz)static void delay_ms(uint32_t ms) {    uint32_t start = DWT_CYCCNT;    uint32_t cycles = ms * 16000;  // 16MHz * 0.001    while ((DWT_CYCCNT - start) < cycles);}// Blink LED on PE13 at 10Hzstatic void blink_led(void) __attribute__((noreturn));static void blink_led(void) {    // Enable GPIOE clock    RCC_AHB1ENR |= (1 << 4);        // Configure PE13 as output    GPIOx_MODER &= ~(3 << 26);    GPIOx_MODER |= (1 << 26);        dwt_init();        while(1) {        GPIOx_ODR |= (1 << 13);   // High        delay_ms(50);        GPIOx_ODR &= ~(1 << 13);  // Low        delay_ms(50);    }}static int32_t is_valid_vector_table(const VectorTable_t * const app_vec){    int32_t res = 1 ;    // Validate stack pointer    if (!is_valid_sram(app_vec->stack_ptr)) {        res = 0 ;    }    // Validate reset handler address    uint32_t reset_addr = app_vec->reset_handler;    if (!is_valid_flash(reset_addr & ~1)) {        res = 0 ;    }    // Check Thumb mode bit    if ((reset_addr & 1) == 0) {        res = 0 ;    }    return res;}typedef void (*pFunction)(void);pFunction Jump_To_Code = 0; /*Must not be in stack*/// Main function - called from startup codeint main(void) {    const VectorTable_t *app_vec = (const VectorTable_t*) APP_BASE;    int32_t res = is_valid_vector_table(app_vec);    if (res) {        uint32_t reset_addr = app_vec->reset_handler;        Jump_To_Code = (pFunction)reset_addr;        // Jump to application        Jump_To_Code();    }    blink_led();    // Should never reach here    while (1) {    };}

Это скрипт сборки программы

include config.mkTARGET = jz_f407vet6_mbr_light_gcc_mMCU = cortex-m4FPU = fpv4-sp-d16FLOAT_ABI = softfpCC = arm-none-eabi-gccOBJCOPY = arm-none-eabi-objcopySIZE = arm-none-eabi-sizeCFLAGS += -mcpu=$(MCU) CFLAGS += -mthumb CFLAGS += -mfpu=$(FPU) CFLAGS += -mfloat-abi=$(FLOAT_ABI)ifeq ($(DEBUG),Y)    CFLAGS += -O0    CFLAGS += -g3 endififeq ($(PACK_PROGRAM),Y)    # $(error PACK_PROGRAM=$(PACK_PROGRAM))    CFLAGS += -Os    #When compiling with -flto, no callgraph information is output along with    #the object file.    #This option runs the standard link-time optimizer.    #When invoked with source code, it generates GIMPLE (one of GCCs internal representations) and writes    #it to special ELF sections in the object file.    # When the object files are linked together, all the function bodies are read     # from these ELF sections and instantiated as if they had been part of the same translation unit.    COMPILE_GCC_OPT += -fltoendifCFLAGS += -ffunction-sectionsCFLAGS += -fdata-sections CFLAGS += -nostdlib CFLAGS += -nostartfilesCFLAGS += -fno-builtin CFLAGS += -ffreestanding CFLAGS += -fno-exceptions CFLAGS += -fno-rttiCFLAGS += -Wl,-gc-sections -Wl,-sCFLAGS += -DSTM32F407xx CFLAGS += -DUSE_STDPERIPH_DRIVER# Размещаем код в начале Flash (0x08000000)LDFLAGS += -mcpu=$(MCU) LDFLAGS += -mthumb LDFLAGS += -mfpu=$(FPU) LDFLAGS += -mfloat-abi=$(FLOAT_ABI)LDFLAGS += -T gcc_arm_mbr.ldLDFLAGS += -Wl,--print-memory-usage LDFLAGS += -Wl,--gc-sectionsLDFLAGS += -Wl,--cref LDFLAGS += -Wl,-Map=$(TARGET).mapSOURCES += main.cSOURCES += system_stm32f4xx.cOBJECTS += $(SOURCES:.c=.o)OBJECTS += startup_stm32f407xx.oall: $(TARGET).bin $(TARGET).hex $(TARGET).elf$(STARTUP): startup_stm32f407xx.S$(CC) $(CFLAGS) -c $< -o $@%.o: %.c$(CC) $(CFLAGS) -c $< -o $@$(TARGET).elf: $(OBJECTS)$(CC) $(LDFLAGS) $^ -o $@$(SIZE) $@%.bin: %.elf$(OBJCOPY) -O binary $< $@%.hex: %.elf$(OBJCOPY) -O ihex $< $@clean:rm -f $(OBJECTS) $(TARGET).elf $(TARGET).bin $(TARGET).hex $(TARGET).map.PHONY: all clean  

Лог сборки этой прошивки тоже тривиальный

21:46:09 **** Build of configuration Release for project jz_f407vet6_mbr_light_gcc_m ****build_from_make.bat rm -f main.o system_stm32f4xx.o startup_stm32f407xx.o jz_f407vet6_mbr_light_gcc_m.elf jz_f407vet6_mbr_light_gcc_m.bin jz_f407vet6_mbr_light_gcc_m.hex jz_f407vet6_mbr_light_gcc_m.maparm-none-eabi-gcc -mcpu=cortex-m4  -mthumb  -mfpu=fpv4-sp-d16  -mfloat-abi=softfp -Os -ffunction-sections -fdata-sections  -nostdlib  -nostartfiles -fno-builtin  -ffreestanding  -fno-exceptions  -fno-rtti -Wl,-gc-sections -Wl,-s -DSTM32F407xx  -DUSE_STDPERIPH_DRIVER -c main.c -o main.occ1.exe: warning: command-line option '-fno-rtti' is valid for C++/D/ObjC++ but not for Carm-none-eabi-gcc -mcpu=cortex-m4  -mthumb  -mfpu=fpv4-sp-d16  -mfloat-abi=softfp -Os -ffunction-sections -fdata-sections  -nostdlib  -nostartfiles -fno-builtin  -ffreestanding  -fno-exceptions  -fno-rtti -Wl,-gc-sections -Wl,-s -DSTM32F407xx  -DUSE_STDPERIPH_DRIVER -c system_stm32f4xx.c -o system_stm32f4xx.occ1.exe: warning: command-line option '-fno-rtti' is valid for C++/D/ObjC++ but not for Carm-none-eabi-gcc    -c -o startup_stm32f407xx.o startup_stm32f407xx.Sarm-none-eabi-gcc -mcpu=cortex-m4  -mthumb  -mfpu=fpv4-sp-d16  -mfloat-abi=softfp -T gcc_arm_mbr.ld -Wl,--print-memory-usage  -Wl,--gc-sections -Wl,--cref  -Wl,-Map=jz_f407vet6_mbr_light_gcc_m.map main.o system_stm32f4xx.o startup_stm32f407xx.o -o jz_f407vet6_mbr_light_gcc_m.elfMemory region         Used Size  Region Size  %age Used             RAM:       17488 B       128 KB     13.34%          CCMRAM:          0 GB        64 KB      0.00%           FLASH:        2124 B         3 KB     69.14%arm-none-eabi-size jz_f407vet6_mbr_light_gcc_m.elf   text   data    bss    dec    hexfilename   1044   1080  16420  18544   4870jz_f407vet6_mbr_light_gcc_m.elfarm-none-eabi-objcopy -O binary jz_f407vet6_mbr_light_gcc_m.elf jz_f407vet6_mbr_light_gcc_m.binarm-none-eabi-objcopy -O ihex jz_f407vet6_mbr_light_gcc_m.elf jz_f407vet6_mbr_light_gcc_m.hex21:46:10 Build Finished. 0 errors, 0 warnings. (took 1s.99ms)

Что можно улучшить?
++Можно попробовать переписать прошивку MBR на языке программирования assembler.
++Можно добавить проверку пустых зарезервированных значений в таблице векторов прерываний
++Адрес прыжка можно задавать утилитой TunerPro. Для этого надо разместить глобальную константу по фиксированному адресу. Перед прошивкой MBR определить целеуказание на вторичный загрузчик, попатчить бинарь и прошить его. Так можно конфигурировать MBR прошивку без добавления нового кода и наращивания её размера.

Итог
Удалось написать для ARM Cortex-M4 прошивку первичного загрузчика размером 2124 Byte! Исходники проекта тут. Когда я показал приятелю, что у меня загрузчик уместился в 3kByte, он мне ответил.

» 3кБ — это ж очень много. «

Загрузка микроконтроллеров сродни работе многоступенчатых ракет. Сначала запускается первичный загрузчик, затем вторичный загрузчик, затем, наконец, Generic приложение. Все это похоже на запуск на орбиты стутника многоступенчтой ракетой.

Если Вам удастся утрамбовать эту прошивку MBR загрузчика ещё меньше чем 2124 байт, то пришлите пожалуйста свои исходники в комментариях.

Ссылки

Название

URL

Типовая разметка памяти STM32F4

https://habr.com/ru/articles/1001268/

Проект загрузчика jz_f407vet6_mbr_light_gcc_m

https://github.com/aabzel/trunk/tree/main/source/projects/jz_f407vet6_mbr_light_gcc_m

Готовые артефакты jz_f407vet6_mbr_light_gcc_m

https://github.com/aabzel/Artifacts/tree/main/jz_f407vet6_mbr_light_gcc_m

STM32. Процесс компиляции и сборки прошивки @andreyzaostrovnykh

https://habr.com/ru/companies/timeweb/articles/793152/

Атрибуты Хорошего Загрузчика

https://habr.com/ru/articles/754216/

NVRAM для микроконтроллеров

https://habr.com/ru/articles/706972/

Обзор утилиты TunerPro (или const volatile)

https://habr.com/ru/articles/965828/

Пуск DWT Таймера на ARM Cortex-M (или Ядерный Таймер)

https://habr.com/ru/articles/1005622/

Обзор учебно-тренировочной электронной платы JZ-F407VET6

https://habr.com/ru/articles/988494/

Размещение глобальных констант по фиксированным адресам

https://habr.com/ru/articles/966862/

История одного байта @tasman

https://habr.com/ru/articles/27055/

ссылка на оригинал статьи https://habr.com/ru/articles/1022264/