3、固件升级基础:固件映像结构、Bootloader与Application分区、Flash布局与读写特性
蓝牙芯片的OTA(Over-The-Air)升级并非简单地将新固件写入Flash,其背后涉及固件映像的组织方式、启动流程的跳转逻辑以及Flash物理特性的约束。理解这三者之间的关系,是设计可靠升级方案的前提。本章将深入剖析固件映像的结构组成、Bootloader与Application的分区协作机制,以及Flash在读写擦除方面的物理限制,为后续章节的升级策略设计打下坚实基础。
3.1 固件映像结构:不仅仅是二进制数据的堆叠
固件映像(Firmware Image)是编译链接后生成的、可被芯片执行和烧录的二进制文件。对于蓝牙芯片而言,其映像结构通常比简单的裸机程序更为复杂,因为它需要支持OTA升级、安全启动和版本回退等高级功能。一个典型的蓝牙固件映像由映像头、向量表、代码段、只读数据段和可选的安全签名区组成。
核心原理:映像头(Image Header)是固件的第一部分,它包含了描述整个映像元数据的关键信息。这些信息包括:魔数(Magic Number,用于快速识别映像合法性)、版本号(Version,用于OTA版本比较)、映像总长度(Image Length)、校验和(Checksum,如CRC32)以及安全签名(Signature,用于验签)。Bootloader在启动时首先读取映像头,通过校验魔数和CRC来快速判断映像是否完整,再通过验签确认映像是否被篡改。向量表(Vector Table)则定义了芯片复位后第一条指令的地址以及各中断服务函数的入口地址,它必须位于Flash的起始地址(或链接脚本指定的地址),以保证CPU能正确启动。
| 映像组成 | 典型偏移 | 大小 | 关键内容 | 升级时的处理 |
|---|---|---|---|---|
| 映像头 | 0x0000 | 32~64B | 魔数、版本、长度、CRC、签名 | 必须重新计算并填充 |
| 向量表 | 0x0040 | 192B (48个中断) | 初始SP、复位向量、IRQ向量 | 随代码编译自动生成 |
| 代码段 | 0x0100 | 可变 | 程序指令 (.text) | 整体替换 |
| 只读数据 | 紧随代码 | 可变 | 常量、字符串 (.rodata) | 整体替换 |
| 签名区 | 末尾 | 256B (ECC) | 数字签名(可选) | 需私钥重新签名 |
判断方法:在工程实践中,可以通过readelf或objdump工具查看编译生成的.elf文件,确认各段的加载地址(LMA)和虚拟地址(VMA)。对于OTA升级,我们更关心的是最终烧录的.bin或.hex文件。使用objcopy -O binary生成的纯二进制文件不包含地址信息,因此需要依赖链接脚本(Linker Script)确保映像头位于偏移0处。
.bin文件。建议编写一个后处理脚本(如Python脚本),在原始.bin文件前添加自定义的映像头,并填充正确的CRC和版本号。这样可以避免Bootloader因映像头不合法而拒绝启动。
3.2 Bootloader与Application分区:职责分离与安全跳转
为了支持OTA升级,蓝牙芯片的Flash必须划分为至少两个逻辑区域:Bootloader区和Application区。这种物理上的分区是实现安全升级的基础。Bootloader是芯片上电后首先执行的代码,其职责是初始化硬件、校验Application的完整性,并决定是跳转到Application运行,还是进入固件下载模式。
核心原理:分区设计的关键在于地址隔离和权限控制。Bootloader区通常位于Flash的低地址段(如0x00000000),大小一般为16KB~64KB。Application区紧随其后,占据剩余的大部分空间。Bootloader在跳转前,会执行以下关键步骤:1) 读取Application映像头;2) 计算整个Application区的CRC并与映像头中的CRC比对;3) 若CRC校验通过,则设置MSP(主堆栈指针)为Application向量表的初始值,然后跳转到复位向量地址。若校验失败,则停留在Bootloader中,等待主机通过UART或BLE下发新固件。
bootloader_settings页,用于记录当前Application的起始地址、大小和CRC值。当执行OTA时,新固件先被写入Download区(如0x00078000),校验通过后,Bootloader会更新设置页中的地址指针,实现“原子性”的固件切换。
操作流程:设计分区时,需要遵循以下步骤:
- 评估资源:根据芯片Flash总大小,预留Bootloader空间(建议至少32KB,包含协议栈和OTA驱动)。
- 定义边界:在链接脚本中,为Bootloader和Application分别设置独立的
FLASH起始地址和长度。 - 设置中断向量偏移:Application代码中必须通过寄存器(如SCB->VTOR)将中断向量表重定位到Application区的起始地址。
- 实现跳转函数:Bootloader中通过函数指针跳转,跳转前需确保关闭全局中断、外设时钟,并重新设置栈指针。
SCB->VTOR = APP_START_ADDR;。
3.3 Flash布局与读写特性:OTA设计的物理约束
Flash存储器是OTA升级的物理载体,其独特的电气特性直接决定了升级策略的可行性。与RAM不同,Flash不能按字节直接覆写,且擦除操作有最小单位限制。理解这些特性,是避免升级过程中出现“变砖”事故的关键。
核心原理:Flash的读写特性可总结为“读随机、写清零、擦置一”。具体而言:读操作支持按字节或字随机访问,速度较快;写操作只能将位从1改为0,不能将0改为1;擦除操作必须按扇区(Sector,通常为4KB)或块(Block,通常为64KB)进行,擦除后所有位恢复为1。此外,每个扇区都有擦写寿命限制(通常为10万次),频繁擦写同一扇区会导致该扇区损坏。
| 操作类型 | 最小单位 | 操作时间(典型值) | 注意事项 |
|---|---|---|---|
| 读 (Read) | 字节/字 | ~50ns | 无限制,可随机访问 |
| 写 (Program) | 字 (32bit) | ~50µs | 只能1→0,写前需擦除 |
| 擦除 (Erase) | 扇区 (4KB) | ~100ms | 整扇区清零,耗时最长 |
| 擦写寿命 | 扇区 | 100,000次 | 磨损均衡需考虑 |
案例拆解:假设某蓝牙芯片Flash大小为512KB,扇区大小为4KB。若采用“双备份”升级策略,则需要将Flash划分为Bootloader(32KB)、App_A(192KB)、App_B(192KB)、Download(64KB)和NVDS(32KB)。当升级固件时,新固件先写入Download区,校验通过后,Bootloader将新固件从Download区复制到App_B区。复制过程中,Bootloader需要先擦除App_B区的48个扇区(192KB/4KB),然后逐字写入。整个擦写过程耗时约48*100ms + 192KB/4B*50µs ≈ 4.8秒 + 2.4秒 ≈ 7.2秒。在此期间,如果发生断电,App_B区可能处于不完整状态,但由于App_A区未受影响,设备仍可回退到旧版本运行。
常见误区:许多开发者误以为OTA升级就是简单的“下载-写入”两步。实际上,必须考虑以下问题:1) 写入过程中断电怎么办?——需要Bootloader具备回滚机制;2) 新固件本身有Bug怎么办?——需要保留旧固件备份;3) 频繁擦写导致Flash损坏怎么办?——需要引入磨损均衡算法,避免固定使用同一扇区存储OTA状态标志。
综上所述,固件映像结构、Bootloader/Application分区以及Flash物理特性三者构成了OTA升级的“铁三角”。映像结构决定了数据如何组织,分区策略决定了升级的安全边界,而Flash特性则划定了操作的物理底线。在实际项目开发中,建议在项目初期就完成分区规划,并编写详细的Flash映射表,作为后续驱动开发和升级策略设计的依据。