在stm32f4xx(Arm Cortex-M4)上开发应用,需要在生成的firmware增加头部信息,以用于OTA升级和bootloader识别文件是否是有效的firmware。需要在这几个文件修改:
- 增加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.
- 增加独立的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
};
- 修改.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。
- 在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);
这样改后,程序完全运行正常了。