4、固件加载与校验:Bootloader的作用、固件镜像的存储位置(Flash分区)、CRC校验与签名验证、固件版本管理
各位同学,今天我们聊一个非常核心的话题——固件加载与校验。
说实话,TCU(牵引控制单元)这玩意儿,一旦上了车,你就不能随便把它拆下来刷程序。你想想看,一列高铁或者一台电力机车,运行在半路上,TCU突然因为固件损坏死机了,那可不是闹着玩的。所以,固件怎么加载、怎么校验、怎么保证它是对的,就成了整个启动流程里最关键的一环。
我个人习惯把这一块叫做「固件的守门人」。没有它,后面的状态机跑得再溜也没用。
4.1 Bootloader的作用
Bootloader,说白了就是TCU上电后第一个跑的程序。它不干别的,就干三件事:
- 初始化硬件:把CPU、内存、Flash控制器这些基础外设先点亮。
- 加载固件:从Flash里把真正的应用程序(也就是咱们的TCU控制算法)搬到RAM里。
- 校验固件:确保搬过来的东西没被篡改、没损坏。
我在项目中遇到过一件事:有次调试,Bootloader跑完了,但应用程序死活起不来。查了半天,发现是Bootloader里忘了初始化FPGA的配置接口。结果应用程序一上来就访问外设,直接挂了。嗯,这里要注意——Bootloader的初始化动作,一定要覆盖到应用程序会用到的所有关键外设。
核心原则:Bootloader要足够小、足够稳。它不负责复杂的控制逻辑,只负责把应用程序安全地请进内存。
4.2 固件镜像的存储位置(Flash分区)
TCU的Flash空间怎么划分?我一般这么干:
| 分区名称 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader区 | 0x08000000 | 64KB | Bootloader程序本身 |
| 参数区 | 0x08010000 | 16KB | 校准参数、序列号等 |
| 应用程序A区 | 0x08014000 | 512KB | 主固件镜像(当前运行) |
| 应用程序B区 | 0x08094000 | 512KB | 备份固件镜像(升级备用) |
| 日志区 | 0x08114000 | 128KB | 运行日志、故障记录 |
为什么要分A区和B区?你想想看,如果升级过程中突然断电了,A区写到一半坏了怎么办?这时候B区就是救命稻草。Bootloader会检查A区是否完整,如果不完整,就自动从B区启动。这叫「双备份启动」,在轨道交通领域几乎是标配。
我的习惯:参数区我一般单独放,不和固件混在一起。因为参数可能需要在现场调整,而固件升级时不应该覆盖参数。我曾经见过一个项目,参数和固件放在同一个分区,结果升级一次参数就丢一次,现场工程师差点骂娘。
4.3 CRC校验与签名验证
固件加载进来,你怎么知道它没被改过?两个手段:CRC和签名。
CRC校验
CRC(循环冗余校验)是检查数据完整性的。Bootloader在烧录固件时,会计算整个固件镜像的CRC值,存到镜像末尾。启动时再算一遍,对比一下。不一样?说明数据坏了,启动B区或者报错。
代码示例(伪代码):
uint32_t calculate_crc(uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc = crc_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
bool verify_firmware(uint8_t *firmware, uint32_t size) {
uint32_t stored_crc = *(uint32_t*)(firmware + size - 4);
uint32_t calc_crc = calculate_crc(firmware, size - 4);
return (stored_crc == calc_crc);
}
这里有个坑:CRC只能检测随机错误,不能防恶意篡改。如果有人故意改了你的固件,然后重新算了个CRC填进去,CRC校验是发现不了的。
签名验证
签名验证就是为了防篡改。用非对称加密算法(比如RSA或ECDSA),固件发布前用私钥签名,Bootloader里烧录公钥。启动时验证签名,签名不对,直接拒绝启动。
我曾经在项目里遇到过一个问题:签名验证通过了,但CRC校验失败了。查了半天,发现是签名验证只验证了固件头部,没验证整个镜像。后来我把签名范围改成了整个固件镜像,问题才解决。所以,签名验证一定要覆盖全部数据,别偷懒。
重要提醒:公钥一定要烧死在Bootloader里,不能存在可擦写的Flash区。否则攻击者可以替换公钥,然后用自己的私钥签名恶意固件。这个坑,我见过不止一个团队踩过。
4.4 固件版本管理
版本管理看起来简单,但做不好会出大问题。我建议在固件镜像头部定义一个版本信息结构体:
typedef struct {
uint32_t magic; // 魔数,用于识别是否为有效固件
uint32_t version_major; // 主版本号
uint32_t version_minor; // 次版本号
uint32_t version_patch; // 补丁版本号
char build_date[16]; // 编译日期
char build_time[16]; // 编译时间
uint32_t firmware_size; // 固件大小
uint32_t crc32; // 固件CRC
uint32_t reserved[8]; // 保留
} firmware_header_t;
Bootloader启动时,先读这个头部,检查魔数是否正确,版本号是否兼容。我一般会做版本兼容性检查:
- 主版本号不同:不兼容,不能升级(可能涉及硬件变更)。
- 次版本号不同:兼容,可以升级(功能增减)。
- 补丁版本号不同:完全兼容,放心升级(bug修复)。
另外,我建议在Bootloader里保留一个「版本回退」功能。万一新固件有问题,现场工程师可以通过特定操作(比如按住某个按键上电)强制从B区启动旧版本。这个功能在紧急情况下能救命。
总结一下:固件加载与校验,说白了就是三件事——放对地方、验明正身、管好版本。Bootloader是TCU的第一道防线,它稳了,后面的状态机才能安心跑。下一节我们开始讲状态机设计,到时候你会看到,启动流程里的每一步,都和今天的固件校验息息相关。