如何将header信息嵌入到stm32f4xx生成的firmware里

在stm32f4xx(Arm Cortex-M4)上开发应用,需要在生成的firmware增加头部信息,以用于OTA升级和bootloader识别文件是否是有效的firmware。需要在这几个文件修改:

  1. 增加header定义

在头文件中增加

cpp 复制代码
typedef struct {

  uint32_t magic;       // 4 bytes

  char hardware_id[16]; // 16 bytes

  uint8_t major;        // 1 byte

  uint8_t minor;        // 1 byte

  uint8_t patch;        // 1 byte

  uint8_t reserved[5];  // 5 bytes (Comment says 1 byte, but array is 5)

  char git_hash[8];     // 8 bytes

} __attribute__((packed)) fw_header_t;

结构的长度为0x24,36.注意,最好把结构长度定义为4 aligned.

  1. 增加独立的C文件来定义header数据
cpp 复制代码
// For armclang: Place in a custom section named "fw_metadata"

__attribute__((used, section("fw_metadata")))

const fw_header_t firmware_header = {

    .magic       = FIRMWARE_MAGIC_EXAMPLE,

    .hardware_id = EXM_MODEL,            

    .major       = FW_VERSION_MAJOR,  

    .minor       = FW_VERSION_MINOR,

    .patch       = FW_VERSION_PATCH,

    .reserved    = {0, 0, 0,0,0},                          

    .git_hash    = GIT_COMMIT_HASH    

};
  1. 修改.sct文件, 它是ARM Linker(链接器 armlink)用的内存布局配置文件,用来精确指定编译出的代码和数据(RO/RW/ZI 段)在 MCU 物理内存(Flash、RAM、外部存储器等)中的加载地址和运行地址。
cpp 复制代码
LR_IROM1 0x08008000 0x00020000  {

  ER_FW_METADATA 0x08008000 0x00000024 {

    * (fw_metadata, +FIRST)

  }

  ER_IROM1 +0 0x0001FFDC {  

    *.o (RESET, +FIRST)            

    .ANY (+RO)

  }

  ; SRAM1 (112KB)

  RW_IRAM1 0x20000000 0x0001C000 {

    .ANY (+RW +ZI)

  }

  ; SRAM2 (16KB)

  RW_IRAM2 0x2001C000 0x00004000 {

    .ANY (+RW +ZI)

  }

}

如上所示,fw_metadata section放在头部,即增加ER_FW_METADATA flash区,长度0x24,压缩ER_IROM1区,紧挨着METADATA区,长度减0x24,成为0x0001FFDC。注意,该App的起始地址是0x08008000,不是缺省的0x08000000,因为这个地址给了bootloader。

  1. 在system_stm32f4xx.c中修改VECT_TAB_OFFSET

#define VECT_TAB_OFFSET (APP_FW_ADDR + FW_METADATA_SIZE)

这个值会被用来计算vector table的起始地址

#if defined(USER_VECT_TAB_ADDRESS)

SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */

#endi

VECT_TAB_BASE_ADDRESS一般是0x08000000, 现在要指向正确的Vector table,必须要首先位移app的偏移,然后加上header的偏移。

另外,为了让keil5中start debug session也跳转到正确的地址,需要增加Debug.ini 文件,(可选,仅在app project下单独start debug session跳转到app时选用)。

cpp 复制代码
RESET                                // 1. Hard reset the hardware first

_sleep_(50);                         // 2. Wait for the hardware reset to settle



LOAD %L INCREMENTAL                  // 3. Load the debug symbols

SP = _RWORD(0x08008024);             // 4. Force Stack Pointer to your App's SP

PC = _RWORD(0x08008028);             // 5. Force Program Counter to your App's Reset Vector

_WDWORD(0xE000ED08, 0x08008000);     // 6. Force VTOR to your App's Vector Table



// Do NOT put 'g' or 'g, main' here.

在Keil5 Options Box, Debug->Initialization File选择它。

编译后,start debug session, 可以顺利到main()并在入口处pause。但是点击run后, 马上就不知道跑到哪里了。我发现这个和VECT_TAB_OFFSET的值相关,当设置为0x00008028U,stuck在SystemClock_Config(),设置为0x28U,stuck在 Hao_I2C_Init(). 把这个现象问Gemini,Gemini回答VTOR寄存器必须divisible by 128(0x80),甚至256(0x100)。建议我把ER_FW_METADATA长度改为0x00000100。保守起见,问了豆包,豆包的回答是:

Cortex‑M4 VTOR 向量表地址完整要求

一、硬件强制最小对齐(内核寄存器硬约束)

VTOR 寄存器位定义(0xE000ED0C,SCB->VTOR):

Bits31:30:保留,必须写 0

Bits29:7:TBLOFF,存储向量表基地址高 23 位

Bits6:0:硬件保留、强制为 0,写入会被忽略

结论 1:最小必须 128 字节(0x80)对齐

向量表基地址低 7 位必须全 0,即地址 & 0x7F == 0:

合法地址示例:0x08000000、0x08000800、0x20000100

非法地址示例:0x08000040、0x20000010(低 7 位非 0,中断查表偏移计算错乱,直接 HardFault)

代码保证对齐写法:

c

运行

// 清除低7位强制128字节对齐再赋值

SCB->VTOR = (uint32_t)VectorTable & ~0x7FU;

二、实际使用更高对齐(ARM 官方附加规则)

ARM 手册明确:地址对齐边界必须 ≥ 向量表总大小,且边界为 2 的整数次幂

向量表总条目 = 16 个系统异常 + N 个外设中断

每条 4 字节,总字节数 = (16 + N) * 4

将总字节向上取最近 2 的幂,地址必须对齐到该值

举例

只用≤16 个外设中断:总条目 32,总字节 128 → 128 字节对齐(刚好最小要求)

用到 21 个外设中断:总条目 37,总字节 148,向上取 256(0x100)→ 必须 256 字节对齐

用到 128 个外设中断:总条目 144,总字节 576,向上取 1024(0x400)→ 1KB 对齐

STM32F4 实测场景

STM32F4 最多 100 + 中断,向量表超过 128 字节,工程里一般直接按 512 字节 / 1KB 对齐向量表,避免溢出。

和Gemini的说法基本一致。

于是我把ER_FW_METADATA长度改为0x00000100,即256。运行Debug session,发现它比以前能通过main()中更多的初始化,确还是在某个硬件的初始化时stuck住了。直到我改成0x00000400,才运行完全正常。因为我的应用里包含的中断很多。这就意味着,(1)flash空间占用多1K,考虑到flash空间有1M,这个占用忽略不计。(2)生成的firmware多1K,因为生成的firmware原本有60K多,增加1K也就是1个百分点多,也是可以接收的。考虑到OTA的过程中,从接收第一个包开始检验包是否正确,所以header必须放在文件开头。综合其它方式的利弊,还是用这种方案。

修改后.sct文件如下:

cpp 复制代码
; --- LOAD REGION 1: Just for the 36-byte Metadata ---

LR_METADATA 0x08008000 0x00000400 {

  ER_FW_METADATA 0x08008000 0x00000024 {  ; Sits at 0x08008000

    * (fw_metadata, +FIRST)

  }

}



; --- LOAD REGION 2: For the Application (Starts perfectly aligned) ---

LR_IROM1 0x08008400 0x0001FC00 {

  ER_IROM1 0x08008400 0x0001FC00 {        ; Load Addr == Exec Addr (No shifting!)

    *.o (RESET, +First)      

    *(InRoot$$Sections)

    .ANY (+RO)

    .ANY (+XO)

  }

FW_METADATA_SIZE定义从0x24改为0x400U。

Debug.ini文件也做相应修改:

SP = _RWORD(0x08008400); // 4. Force Stack Pointer to your App's SP

PC = _RWORD(0x08008404);

这样改后,程序完全运行正常了。