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 自动生成,任何人不得手动修改。这个规矩救了我们很多次。

最后说一句:版本管理不是束缚,而是保护。它保护你的团队不被混乱的固件折磨,保护你的客户不被错误的升级坑害。花点时间把这套体系搭好,后面会省下无数麻烦。