
" 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 |
убрал из startup_stm32f407xx.S | 652 |
Убрал из таблицы векторов все внешние прерывания | 324 |
Вот, что мне сгенерировал 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 type typedef struct { uint32_t stack_ptr; uint32_t reset_handler; } VectorTable_t; // Check if address is in SRAM range static inline int is_valid_sram(uint32_t addr) { return (( SRAM_BASE <= addr) && (addr <= SRAM_END)); } // Check if address is in Flash range static inline int is_valid_flash(uint32_t addr) { return ((FLASH_BASE <= addr) && (addr <= FLASH_END)); } // Initialize DWT cycle counter static 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 10Hz static 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 code int 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) { }; }
Это скрипт сборки программы. Важно добавить компилятору ключ -nostdlib
# Makefile include config.mk TARGET = jz_f407vet6_mbr_light_gcc_m MCU = cortex-m4 FPU = fpv4-sp-d16 FLOAT_ABI = softfp CC = arm-none-eabi-gcc OBJCOPY = arm-none-eabi-objcopy SIZE = arm-none-eabi-size CFLAGS += -mcpu=$(MCU) CFLAGS += -mthumb CFLAGS += -mfpu=$(FPU) CFLAGS += -mfloat-abi=$(FLOAT_ABI) ifeq ($(DEBUG),Y) CFLAGS += -O0 CFLAGS += -g3 endif ifeq ($(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 += -flto endif CFLAGS += -ffunction-sections CFLAGS += -fdata-sections CFLAGS += -nostdlib CFLAGS += -nostartfiles CFLAGS += -fno-builtin CFLAGS += -ffreestanding CFLAGS += -fno-exceptions CFLAGS += -fno-rtti CFLAGS += -Wl,-gc-sections -Wl,-s CFLAGS += -DSTM32F407xx CFLAGS += -DUSE_STDPERIPH_DRIVER # Размещаем код в начале Flash (0x08000000) LDFLAGS += -nostdlib LDFLAGS += -mcpu=$(MCU) LDFLAGS += -mthumb LDFLAGS += -mfpu=$(FPU) LDFLAGS += -mfloat-abi=$(FLOAT_ABI) LDFLAGS += -T gcc_arm_mbr.ld LDFLAGS += -Wl,--print-memory-usage LDFLAGS += -Wl,--gc-sections LDFLAGS += -Wl,--cref LDFLAGS += -Wl,-Map=$(TARGET).map #LDFLAGS += -Wl,-Ttext=0x08000000 #LDFLAGS += -nostartfiles #LDFLAGS += -Wl,--section-start=.text=0x08000000 SOURCES += main.c SOURCES += system_stm32f4xx.c OBJECTS += $(SOURCES:.c=.o) OBJECTS += startup_stm32f407xx.o all: $(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
В ассемблерном файле пришлось сократить таблицу векторов прерываний и исключить инициализацию стандартной библиотеки (убрать строку bl __libc_init_array).
/** ****************************************************************************** * @file startup_stm32f407xx.s * @author MCD Application Team * @brief STM32F407xx Devices vector table for GCC based toolchains. * This module performs: * - Set the initial SP * - Set the initial PC == Reset_Handler, * - Set the vector table entries with the exceptions ISR address * - Branches to main in the C library (which eventually * calls main()). * After Reset the Cortex-M4 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** */ .syntax unified .cpu cortex-m4 .fpu softvfp .thumb .global g_pfnVectors .global Default_Handler /* start address for the initialization values of the .data section. defined in linker script */ .word _sidata /* start address for the .data section. defined in linker script */ .word _sdata /* end address for the .data section. defined in linker script */ .word _edata /* start address for the .bss section. defined in linker script */ .word _sbss /* end address for the .bss section. defined in linker script */ .word _ebss /* stack used for SystemInit_ExtMemCtl; always internal RAM used */ /** * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application * supplied main() routine is called. * @param None * @retval : None */ .section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr sp, =_estack /* set stack pointer */ /* Copy the data segment initializers from flash to SRAM */ ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: adds r4, r0, r3 cmp r4, r1 bcc CopyDataInit /* Zero fill the bss segment. */ ldr r2, =_sbss ldr r4, =_ebss movs r3, #0 b LoopFillZerobss FillZerobss: str r3, [r2] adds r2, r2, #4 LoopFillZerobss: cmp r2, r4 bcc FillZerobss /* Call the clock system initialization function.*/ bl SystemInit /* Call static constructors bl __libc_init_array */ /* Call the application's entry point.*/ bl main bx lr .size Reset_Handler, .-Reset_Handler /** * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * @param None * @retval None */ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop .size Default_Handler, .-Default_Handler /****************************************************************************** * * The minimal vector table for a Cortex M3. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * *******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler /******************************************************************************* * Provide weak aliases for each Exception handler to the Default_Handler. * As they are weak aliases, any function with the same name will override * this definition. *******************************************************************************/ .weak NMI_Handler .thumb_set NMI_Handler,Default_Handler .weak HardFault_Handler .thumb_set HardFault_Handler,Default_Handler .weak MemManage_Handler .thumb_set MemManage_Handler,Default_Handler .weak BusFault_Handler .thumb_set BusFault_Handler,Default_Handler .weak UsageFault_Handler .thumb_set UsageFault_Handler,Default_Handler .weak SVC_Handler .thumb_set SVC_Handler,Default_Handler .weak DebugMon_Handler .thumb_set DebugMon_Handler,Default_Handler .weak PendSV_Handler .thumb_set PendSV_Handler,Default_Handler .weak SysTick_Handler .thumb_set SysTick_Handler,Default_Handler
Лог сборки этой прошивки тоже тривиальный
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.map arm-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.o cc1.exe: warning: command-line option '-fno-rtti' is valid for C++/D/ObjC++ but not for C arm-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.o cc1.exe: warning: command-line option '-fno-rtti' is valid for C++/D/ObjC++ but not for C arm-none-eabi-gcc -c -o startup_stm32f407xx.o startup_stm32f407xx.S arm-none-eabi-gcc -nostdlib -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.elf Memory region Used Size Region Size %age Used RAM: 16392 B 128 KB 12.51% CCMRAM: 0 GB 64 KB 0.00% FLASH: 324 B 3 KB 10.55% arm-none-eabi-size jz_f407vet6_mbr_light_gcc_m.elf text data bss dec hex filename 324 0 16392 16716 414c jz_f407vet6_mbr_light_gcc_m.elf arm-none-eabi-objcopy -O binary jz_f407vet6_mbr_light_gcc_m.elf jz_f407vet6_mbr_light_gcc_m.bin arm-none-eabi-objcopy -O ihex jz_f407vet6_mbr_light_gcc_m.elf jz_f407vet6_mbr_light_gcc_m.hex
Что можно улучшить?
++Можно попробовать переписать прошивку MBR на языке программирования assembler. Без Си -файлов.
++Можно добавить проверку пустых зарезервированных значений в таблице векторов прерываний
++Адрес прыжка можно задавать утилитой TunerPro. Для этого надо разместить глобальную константу по фиксированному адресу. Перед прошивкой MBR определить целеуказание на вторичный загрузчик, попатчить бинарь и прошить его. Так можно конфигурировать MBR прошивку без добавления нового кода и наращивания её размера.
Итог
Удалось написать для ARM Cortex-M4 прошивку первичного загрузчика размером 324 Byte! Исходники проекта тут.
Загрузка микроконтроллеров сродни работе многоступенчатых ракет.

Сначала запускается первичный загрузчик, затем вторичный загрузчик, потом, наконец, Generic приложение. Все это похоже на запуск на орбиты стутника многоступенчтой ракетой.
Если Вам удастся утрамбовать эту прошивку MBR загрузчика ещё меньше чем 324 байт, то пришлите пожалуйста свои исходники в комментариях.
Ссылки
Название | URL |
Типовая разметка памяти STM32F4 | |
Проект загрузчика jz_f407vet6_mbr_light_gcc_m | https://github.com/aabzel/trunk/tree/main/source/projects/jz_f407vet6_mbr_light_gcc_m |
https://github.com/aabzel/Artifacts/tree/main/jz_f407vet6_mbr_light_gcc_m | |
STM32. Процесс компиляции и сборки прошивки @andreyzaostrovnykh | |
Атрибуты Хорошего Загрузчика | |
NVRAM для микроконтроллеров | |
Обзор утилиты TunerPro (или const volatile) | |
Пуск DWT Таймера на ARM Cortex-M (или Ядерный Таймер) | |
Обзор учебно-тренировочной электронной платы JZ-F407VET6 | |
Размещение глобальных констант по фиксированным адресам | |
История одного байта @tasman |