版本号管理:语义化版本号、版本号存储位置、版本比较策略
版本号这东西,看着简单,但坑特别多。我见过不少团队,固件跑着跑着就乱了,最后查下来,版本号管理一塌糊涂。说白了,版本号就是固件的身份证,搞不清楚谁是谁,回滚就是瞎搞。
语义化版本号:MAJOR.MINOR.PATCH
我个人习惯用语义化版本号,格式是 MAJOR.MINOR.PATCH。比如 2.1.3,意思很明确:
- MAJOR:主版本号。API 不兼容、底层架构大改时加。比如换了 RTOS 内核,或者改了 Flash 分区表。
- MINOR:次版本号。新增功能,但向后兼容。比如加了个新的传感器驱动,或者新增了一个通信协议。
- PATCH:修订号。修 Bug、优化性能,不改变接口。比如修复了某个定时器溢出问题。
我在项目中遇到过一个问题:有个同事把 Bug 修复也加了 MINOR 号,结果回滚时以为功能没变,实际上一堆依赖关系全乱了。所以,规则一定要定死。
核心原则:版本号变更必须反映兼容性变化。MAJOR 变,意味着不能直接替换;MINOR 变,可以替换但需要测试;PATCH 变,放心替换。
版本号存储位置:放哪儿才靠谱?
版本号不能只写在代码里。你想想看,固件编译出来,版本号就固定了。但 MCU 跑起来之后,怎么知道当前跑的是哪个版本?
我建议至少存三个地方:
| 存储位置 | 用途 | 注意事项 |
|---|---|---|
| 固件头部(Firmware Header) | Bootloader 读取,用于校验和回滚判断 | 必须放在固定偏移,结构体对齐 |
| Flash 专用区域 | 运行时读取,用于 OTA 升级判断 | 防止被应用代码意外擦除 |
| 编译产物(.elf / .hex 注释) | 开发调试,追溯版本 | 不参与运行时逻辑 |
嗯,这里要注意:固件头部里的版本号,Bootloader 要能直接解析。我习惯用结构体来定义:
typedef struct {
uint32_t magic; // 魔数,用于校验有效性
uint32_t version_major; // 主版本号
uint32_t version_minor; // 次版本号
uint32_t version_patch; // 修订号
uint32_t crc32; // 固件 CRC 校验值
uint32_t size; // 固件大小
} firmware_header_t;
魔数很重要。我曾经遇到过 Flash 全空或者被意外擦除的情况,如果没有魔数校验,Bootloader 会把垃圾数据当成版本号,直接导致回滚逻辑崩溃。
避坑指南:我曾经在一个项目里把版本号只存在了应用代码的全局变量里。结果 Bootloader 根本读不到,回滚时只能靠猜。后来全部改成了固定偏移的头部结构体,问题才解决。
版本比较策略:怎么比才不出错?
版本比较不是简单的字符串比较。比如 2.10.0 和 2.9.0,按字符串比,2.10.0 反而比 2.9.0 小,因为 '1' < '9'。这显然不对。
我推荐用整数比较,把三个段拆开:
int compare_version(uint32_t maj1, uint32_t min1, uint32_t pat1,
uint32_t maj2, uint32_t min2, uint32_t pat2) {
if (maj1 != maj2) return (maj1 > maj2) ? 1 : -1;
if (min1 != min2) return (min1 > min2) ? 1 : -1;
if (pat1 != pat2) return (pat1 > pat2) ? 1 : -1;
return 0; // 完全相等
}
这样比较,逻辑清晰,也不容易出错。但有个细节:回滚时,我们比较的是「当前运行版本」和「待回滚版本」。如果待回滚版本比当前版本还新,那就不叫回滚,叫升级了。
小技巧:我习惯在 Bootloader 里维护一个「回滚版本号」变量。每次升级成功,就把当前版本号写入这个变量。回滚时,只允许回滚到比这个变量旧的版本,防止循环回滚。
版本号管理流程图
下面这张图,把版本号从编译到回滚的完整流程串起来了。你可以对照着看,理解每个环节的版本号是怎么流转的。
版本号管理的常见坑
- 版本号溢出:有些 MCU 用 uint8_t 存版本号,最大 255。产品迭代快的话,几年就溢出了。我建议用 uint32_t,省心。
- 版本号重复:同一个版本号编译了两次,但内容不同。这通常是因为编译时间戳没变,或者版本号脚本没更新。加个 Git Commit ID 到头部里,能有效避免。
- 版本号被篡改:如果 Flash 被意外写入,版本号可能变成非法值。Bootloader 一定要做魔数和 CRC 校验,发现非法就拒绝回滚。
我的习惯:每次发布固件,我都会在版本号后面加一个构建号(Build Number),比如 2.1.3.20250315。这样即使版本号没变,也能知道是哪个时间点编译的。调试时特别有用。
版本号管理,说白了就是给固件一个清晰的「身份标签」。规则定好了,存储位置选对了,比较策略写稳了,回滚机制才能跑得顺。别小看这几个数字,搞不好就是线上事故的源头。