2、固件版本管理:语义化版本控制、Git标签管理、版本号自动生成策略
版本管理这事儿,我做了十几年嵌入式,踩过的坑真不少。
早期做项目,固件版本全靠手写。今天发个 v1.0,明天改两行代码就变成 v1.1。结果呢?产线烧录的版本和测试手里的版本对不上,客户反馈的 bug 根本没法定位。说白了,版本号乱成一锅粥,整个团队都在猜「这个固件到底改了啥」。
后来我痛定思痛,引入了语义化版本控制。今天就把这套方法论掰开揉碎讲给你听。
2.1 语义化版本控制:为什么是 X.Y.Z?
语义化版本,英文叫 SemVer。它的核心格式很简单:主版本号.次版本号.修订号,也就是 MAJOR.MINOR.PATCH。
你可能会问:为什么非得搞三个数字?两个不行吗?
嗯,我举个例子你就明白了。假设你有个智能灯固件,版本号是 1.2.3:
- 主版本号(MAJOR):做了不兼容的 API 改动。比如把控制灯的协议从 UDP 改成了 MQTT,旧版 App 连不上新固件了。这时候主版本号要加 1,变成 2.0.0。
- 次版本号(MINOR):增加了新功能,但向后兼容。比如新增了定时开关灯功能,旧版 App 依然能用。这时候次版本号加 1,变成 1.3.0。
- 修订号(PATCH):只修 bug,不新增功能。比如修复了灯偶尔闪烁的问题。这时候修订号加 1,变成 1.2.4。
核心原则:版本号必须能反映变更的「破坏性」程度。
我曾经见过一个团队,每次发版都只改修订号。结果客户升级后,发现所有外设驱动都不工作了。这就是典型的「版本号撒谎」——明明是不兼容的大改,却伪装成了小修小补。
另外,语义化版本还定义了预发布版本和构建元数据。比如 2.0.0-alpha.1 表示第一个内部测试版,2.0.0+build.20231001 表示构建日期。这些在嵌入式 OTA 灰度发布时特别有用。
2.2 Git 标签管理:把版本号刻在代码里
光有版本号规则还不够。你得把版本号和代码仓库绑定起来。Git 标签就是干这个的。
我个人习惯的做法是:每次发布正式固件时,打一个 annotated tag(附注标签)。为什么不用 lightweight tag(轻量标签)?因为轻量标签只是个指针,没有作者、日期、备注信息。万一出问题,你根本不知道这个标签是谁在什么时候打的。
来看一个典型的标签命令:
# 打一个附注标签
git tag -a v2.1.0 -m "Release v2.1.0: 新增低功耗模式,修复WiFi重连bug"
# 推送到远程仓库
git push origin v2.1.0
# 查看所有标签
git tag -l 'v2.*'
这里有个细节要注意:标签名最好和固件版本号完全一致。我见过有人标签写 v2.1.0,但固件里打印的版本号是 2.1.0。少了个 'v',排查问题时对不上,白白浪费半天时间。
我的小技巧:在 CI/CD 流水线中,自动从 Git 标签提取版本号。比如用 git describe --tags --always 命令,能拿到最近的标签名和 commit 数。这样固件里显示的版本号,永远和 Git 仓库里的标签一致。
2.3 版本号自动生成策略:别再手动改了
手动改版本号?我劝你趁早放弃这个念头。
有一次我熬夜发版,手一抖把 1.8.9 改成了 1.9.0,但实际只修了一个小 bug。结果客户看到版本号跳了次版本,以为有大功能更新,兴冲冲升级后大失所望。更糟糕的是,后续的版本号全乱了。
所以,版本号必须自动化生成。我推荐两种策略:
策略一:基于 Git 标签的自动递增
在 CI 脚本里,用 git tag 获取最新版本号,然后根据 commit 信息自动决定递增哪一位。
# 伪代码示例
latest_tag=$(git describe --tags --abbrev=0)
major=$(echo $latest_tag | cut -d. -f1)
minor=$(echo $latest_tag | cut -d. -f2)
patch=$(echo $latest_tag | cut -d. -f3)
# 检查 commit 信息,决定递增策略
if commit_message contains "BREAKING CHANGE"; then
major=$((major + 1))
minor=0
patch=0
elif commit_message contains "feat:"; then
minor=$((minor + 1))
patch=0
else
patch=$((patch + 1))
fi
new_version="${major}.${minor}.${patch}"
echo "新版本号: $new_version"
策略二:基于构建时间的版本号
有些场景下,比如内部测试版,用时间戳更直观。格式可以是 YYMMDD.HHMM。
# 生成时间戳版本号
build_version=$(date +"%y%m%d.%H%M")
echo "BUILD_VERSION=$build_version" >> $GITHUB_ENV
注意:时间戳版本号不适合正式发布。因为两个不同分支的同一天构建,版本号可能一样。正式版一定要用语义化版本。
2.4 嵌入式场景的特殊考量
嵌入式固件和普通软件不一样。固件烧进去就不好改了,所以版本管理要更谨慎。
我遇到过最头疼的问题:OTA 升级时,新固件版本号比旧固件低。结果设备判断「版本号没变高」,拒绝升级。后来我在固件头部固定位置写死了版本号,升级前先比较这个字段。
这里给出一段典型的固件版本结构体:
typedef struct {
uint8_t major;
uint8_t minor;
uint16_t patch;
char build_time[20]; // 如 "2023-10-01 14:30:00"
uint32_t crc32; // 固件校验
} firmware_version_t;
// 版本比较函数
int version_compare(firmware_version_t *a, firmware_version_t *b) {
if (a->major != b->major) return a->major - b->major;
if (a->minor != b->minor) return a->minor - b->minor;
return a->patch - b->patch;
}
另外,我建议在固件中保留完整的 Git commit hash。这样即使版本号相同,也能精确追溯到代码状态。做法很简单:在编译脚本里把 git rev-parse HEAD 的输出写入固件。
2.5 实战:一套完整的版本管理流程
说了这么多,咱们串起来看看实际怎么操作。
| 阶段 | 操作 | 版本号示例 |
|---|---|---|
| 日常开发 | 在 dev 分支提交代码,CI 自动构建测试版 | 1.2.3-dev.20231001 |
| 功能冻结 | 创建 release 分支,只修 bug | 1.3.0-beta.1 |
| 发布候选 | 内部测试通过,打 RC 标签 | 1.3.0-rc.1 |
| 正式发布 | 合并到 main 分支,打正式标签 | 1.3.0 |
| 紧急修复 | 从正式标签创建 hotfix 分支 | 1.3.1 |
你看,每个阶段都有明确的版本号规则。团队里任何人看到版本号,就知道固件处于什么状态。
避坑指南:我曾经在 release 分支上直接改了版本号,结果和 main 分支的标签冲突了。后来我规定:版本号只由 CI 自动生成,任何人不得手动修改。这个规矩救了我们很多次。
最后说一句:版本管理不是束缚,而是保护。它保护你的团队不被混乱的固件折磨,保护你的客户不被错误的升级坑害。花点时间把这套体系搭好,后面会省下无数麻烦。