4、固件版本管理:Git仓库初始化、分支策略、Tag管理、CHANGELOG编写规范

版本管理这件事,说白了就是给固件开发装一个「后悔药」和「记事本」。我见过太多团队,固件出了问题靠翻聊天记录找版本,或者用「_final_v3_真的不改了.hex」这种文件名。嗯,咱们今天就把这事彻底理清楚。

4.1 Git仓库初始化——别急着 git init

很多人拿到项目,上来就 git init,然后一股脑把所有文件都 git add .。我刚开始也这么干,结果仓库里塞满了编译中间文件、IDE 的临时配置、甚至还有 .o 文件。后来有一次同事拉代码编译不过,查了半天发现是 .o 文件冲突了。

正确的做法,是先想清楚哪些东西不该进仓库。

固件仓库里该放什么?

  • 源代码(.c、.h、.s、.ld 等)
  • 构建脚本(Makefile、CMakeLists.txt、scons 脚本等)
  • 配置文件(Kconfig、.config、board 定义等)
  • 文档(README、CHANGELOG、API 手册等)
  • 测试脚本与测试用例

绝对不要放什么?

  • 编译产物(.o、.elf、.hex、.bin、.map)
  • IDE 临时文件(.vscode、.idea、*.swp)
  • 第三方库的二进制包
  • 个人环境配置

我习惯在项目根目录先建一个 .gitignore,把上面这些东西全写进去。这里给一个嵌入式项目的通用模板:

# 编译产物
*.o
*.elf
*.hex
*.bin
*.map
build/
out/

# IDE 配置
.vscode/
.idea/
*.swp
*.swo
*~

# 系统文件
.DS_Store
Thumbs.db

# 个人配置
*.user
*.local

然后才是 git init,接着 git add .gitignore 先提交一次。为什么要先提交 .gitignore?因为后续再改 .gitignore 时,已经跟踪的文件不会自动被忽略,得用 git rm --cached 处理,很麻烦。

小技巧: 第一次提交只放 .gitignore 和 README.md,确认仓库干净了再逐步添加源码。这样每次提交的「原子性」更好,回滚也方便。

4.2 分支策略——别让 master 变成垃圾堆

固件开发的分支策略,跟纯软件不太一样。纯软件可以随便切分支,固件要考虑硬件依赖、烧录验证、产线发布等环节。我见过最惨的案例:有人在 master 上直接改代码,改到一半发现板子烧不进程序,想回退又跟别人的提交混在一起,最后只能重拉仓库。

我个人推荐嵌入式固件用这套分支模型:

分支名称 用途 谁可以合并 生命周期
master 已发布或准备发布的稳定版本 仅维护者 永久
develop 日常开发集成分支 所有开发者 永久
feature/xxx 新功能开发 个人 功能完成后删除
fix/xxx Bug 修复 个人 修复完成后删除
release/vx.x.x 发布前测试与微调 维护者 发布后合并到 master 和 develop 并删除
hotfix/xxx 紧急产线问题修复 维护者 修复后合并到 master 和 develop 并删除

为什么固件要单独搞一个 release/ 分支?因为固件发布前需要做硬件回归测试、产线烧录验证、EMC 测试等,这些测试可能要跑好几天。如果在 develop 上测,其他开发者的提交会不断干扰测试环境。release 分支就是给测试团队一个「冻结」的快照。

注意: 永远不要在 master 上直接提交代码。master 只接受来自 release/ 或 hotfix/ 的合并。这是底线,破了就乱套了。

4.3 Tag管理——给固件版本打上「坐标」

Tag 就是 Git 里的里程碑。每次发布固件,不管是给内部测试还是给产线量产,都应该打一个 Tag。我早期做项目时,觉得打 Tag 麻烦,结果客户反馈了一个问题,我找了半天才找到对应版本的代码。后来我养成了习惯:每次发布必打 Tag。

Tag 的命名规范,我建议跟固件版本号保持一致。比如固件版本是 v2.3.1,Tag 就是 v2.3.1。但要注意,Tag 分两种:

  • 轻量标签(lightweight tag):只是一个指针,指向某个 commit。适合临时标记。
  • 附注标签(annotated tag):包含作者、日期、消息,还可以用 GPG 签名。适合正式发布。

正式发布一定要用附注标签:

# 创建附注标签
git tag -a v2.3.1 -m "Release v2.3.1: 修复UART接收超时问题,优化低功耗模式"

# 推送标签到远程
git push origin v2.3.1

# 查看所有标签
git tag -l 'v2.*'

我习惯在 CHANGELOG 里记录每个 Tag 对应的变更,这样别人看 Tag 就能知道这个版本改了啥。另外,Tag 一旦推送,尽量不要删除或修改。如果发布错了,就打个新 Tag,比如 v2.3.2,而不是去改 v2.3.1。

经验之谈: 产线烧录的固件,一定要用 Tag 对应的 commit 来编译。不要用「最新的 develop」或者「某个分支的最新提交」。我吃过这个亏——产线烧了 1000 片板子,才发现代码里有个调试用的延时没去掉。

4.4 CHANGELOG编写规范——让历史可追溯

CHANGELOG 不是给机器看的,是给人看的。写得好,能省去无数沟通成本。我见过最差的 CHANGELOG 就一句话:「修复了一些 bug」。这跟没写有什么区别?

我推荐用 Keep a Changelog 规范,结合 Semantic Versioning(语义化版本)。每个版本按这个结构来:

# Changelog

## [v2.3.1] - 2025-03-15

### Added
- 新增低功耗模式下RTC唤醒功能
- 新增产线自检命令:AT+TEST

### Changed
- 优化UART中断处理逻辑,减少CPU占用率
- 升级FreeRTOS版本从v10.4.3到v10.5.1

### Fixed
- 修复UART接收缓冲区溢出导致死机的问题
- 修复看门狗超时时间配置不生效的问题
- 修复SPI通信在高速模式下数据错位的问题

### Removed
- 移除调试用的串口打印宏(DEBUG_PRINT)

### Security
- 修复OTA固件签名验证绕过漏洞

每个分类的含义:

分类 含义 示例
Added 新功能 新增、支持、实现
Changed 功能变更 优化、重构、升级
Deprecated 即将废弃 标记为废弃,后续版本会移除
Removed 移除功能 删除、移除
Fixed Bug 修复 修复、解决
Security 安全修复 修复漏洞、增强安全性

避坑指南: 我曾经在 CHANGELOG 里写「修复了若干问题」,结果客户追问「到底修复了哪些问题?」,我不得不去翻 commit log。从那以后,我要求团队每个版本必须写清楚具体修复了哪个 issue 或哪个现象。模糊的描述等于没写。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。从仓库初始化开始,到分支策略、Tag 管理、CHANGELOG 编写,每一步都有明确的输入输出。

固件版本管理知识体系 固件版本管理知识体系 仓库初始化 .gitignore → git init → 首次提交 分支策略 master / develop / feature / release / hotfix Tag 管理 附注标签 / 语义化版本 / 不可变 CHANGELOG 编写 Added / Changed / Fixed / Removed / Security 核心原则 1. master 只接受 release/hotfix 合并,禁止直接提交 2. 每次发布必须打附注 Tag,Tag 不可变 3. CHANGELOG 按分类写,模糊描述等于没写

你看,整个流程是环环相扣的。仓库初始化决定了「地基」干不干净;分支策略决定了「施工」乱不乱;Tag 管理决定了「坐标」清不清晰;CHANGELOG 决定了「历史」明不明白。哪一环偷懒,后面都要加倍还债。

最后说一句: 版本管理不是束缚,是保护。你花 10 分钟把仓库和分支搭好,后面可能省下 10 个小时的排查时间。这笔账,怎么算都划算。