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的第一道防线,它稳了,后面的状态机才能安心跑。下一节我们开始讲状态机设计,到时候你会看到,启动流程里的每一步,都和今天的固件校验息息相关。