2. 固件版本管理:版本号规范、存储结构与兼容性检查
版本管理这事儿,看着简单,做起来全是坑。我早年吃过不少亏——有一次现场升级,就因为版本号没对齐,整批设备变砖,差点被客户拉黑。从那以后,我对版本管理就特别较真。
说白了,固件版本管理就三件事:怎么编号、怎么存、怎么判断能不能升。咱们一个一个说。
2.1 语义化版本号规范
版本号不是随便写的。我个人习惯用语义化版本(Semantic Versioning),格式是:
主版本号.次版本号.修订号
例如:2.1.3
规则其实很简单:
- 主版本号:不兼容的API变更。比如协议改了、数据结构变了,老固件和新固件没法互通。
- 次版本号:向下兼容的功能新增。加了新功能,但老功能照常工作。
- 修订号:向下兼容的问题修复。修了个bug,功能没变。
我在项目中遇到过一种情况:研发团队把版本号写成 V20250301,看着像日期,但根本看不出兼容关系。结果现场升级时,A版本能升到B版本,B版本却升不回A版本,乱成一锅粥。
核心原则:版本号必须能表达兼容性信息。日期、流水号都不行。
实际项目中,我还会加一个构建号或内部版本号,比如 2.1.3.456。最后一位是构建计数,方便定位具体是哪次编译的固件。
2.2 版本存储结构
版本信息存在哪儿?怎么存?我见过最糙的做法——把版本号写死在代码里。每次改版本都要重新编译,万一编译环境不一样,版本号对不上,调试起来想死的心都有。
我建议的做法是:把版本信息放在固件镜像的固定偏移位置,用一个结构体存起来。
// 版本信息结构体
typedef struct {
uint8_t magic[4]; // 魔数,用于校验,比如 "RMCV"
uint8_t major; // 主版本号
uint8_t minor; // 次版本号
uint16_t patch; // 修订号
uint32_t build; // 构建号
uint8_t reserved[8]; // 保留字段,以后扩展用
uint32_t crc32; // 整个结构体的CRC校验
} FirmwareVersion_t;
这个结构体放在固件镜像的头部,比如偏移0x100处。这样,不管固件有多大,我只要读前几百字节就能拿到版本信息,不用解压、不用解析整个镜像。
小技巧:魔数用ASCII字符,比如 "RMCV",用十六进制查看器一眼就能认出来。调试时特别方便。
在Flash里,我通常会预留两个区域存版本信息:
- 当前运行版本区:存当前正在跑的固件版本。每次启动时读这个区,确认固件是否正常。
- 备份版本区:存上一次成功启动的固件版本。回滚时用。
嗯,这里要注意:Flash写入次数有限,不能每次启动都写版本区。我一般只在升级成功并验证通过后才更新版本区。
2.3 版本兼容性检查
兼容性检查,说白了就是判断「新固件能不能覆盖老固件」。我见过最惨的案例:现场升级时,新固件改了Flash分区表,老固件不认新分区,升级失败后连回滚都做不了。
兼容性检查的核心逻辑是这样的:
// 兼容性检查伪代码
bool check_compatibility(FirmwareVersion_t *old_ver, FirmwareVersion_t *new_ver) {
// 1. 魔数校验
if (memcmp(old_ver->magic, "RMCV", 4) != 0) {
return false; // 老固件版本信息损坏
}
// 2. 主版本号检查
if (new_ver->major != old_ver->major) {
// 主版本不同,属于不兼容升级
// 需要特殊处理,比如擦除全部配置参数
return false; // 或者返回 true 但标记为"需全量升级"
}
// 3. 次版本号检查
if (new_ver->minor < old_ver->minor) {
// 新固件的次版本号比老固件还低
// 这属于降级,需要确认是否允许
return is_downgrade_allowed();
}
// 4. 修订号检查
if (new_ver->patch < old_ver->patch) {
// 修订号回退,一般不允许
return false;
}
return true;
}
我曾经踩过的坑:兼容性检查不能只看版本号大小。比如从 2.0.0 升到 3.0.0,主版本变了,但实际协议没改。反过来,从 2.0.0 升到 2.1.0,次版本只加了一位,但Flash布局全改了。所以,版本号只是参考,真正的兼容性信息要放在固件头部的兼容性掩码里。
我现在的做法是加一个兼容性掩码字段:
typedef struct {
uint8_t magic[4];
uint8_t major;
uint8_t minor;
uint16_t patch;
uint32_t build;
uint32_t compat_mask; // 兼容性掩码
// ... 其他字段
} FirmwareVersion_t;
兼容性掩码的每一位代表一个特性:
- bit0:Flash分区布局是否变化
- bit1:通信协议是否变化
- bit2:配置参数结构是否变化
- bit3:加密算法是否变化
升级时,新固件和老固件的掩码做按位与,如果结果不等于老固件的掩码,说明有特性不兼容,需要特殊处理。
你想想看,有了这个掩码,兼容性检查就不再是简单的数字比较了,而是特性级别的精确匹配。这才是工程上靠谱的做法。
2.4 实战建议
最后,给几个我踩过坑后总结的建议:
- 版本号从 1.0.0 开始,不要从 0.1.0 开始。0.x 版本容易让人误解为测试版。
- 每次发布都更新版本号,哪怕只改了一行注释。不更新版本号,你永远不知道现场跑的是哪个版本。
- 版本信息结构体要预留扩展空间。我吃过亏——结构体定死了,后来想加字段,老固件不认新结构,升级兼容性检查直接崩了。
- 兼容性检查失败时,要有明确的错误码。不要只返回 true/false,要告诉现场工程师「是主版本不兼容」还是「Flash分区变了」。
一句话总结:版本管理不是写个数字就完事了。它是固件升级的第一道安全防线。版本号规范、存储结构合理、兼容性检查严谨,这三件事做好了,升级事故能减少八成。