" 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
bl __libc_init_array
и добавил в ключи компилятора
-nostdlib

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

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/

Only registered users can participate in poll. Log in, please.
Вы делали первичный загрузчик MBR?
33.33%да7
66.67%нет14
21 users voted. 1 user abstained.