| 编辑推荐: |
本文主要系统性地介绍了嵌入式软件研发中的版本管理与分支管理方法论,希望对你的学习有帮助。
本文来自于一口吃懂硬件,由火龙果软件Alice编辑、推荐。 |
|
开门见山
很多嵌入式项目一开始只有几个人,代码放在 Git 里,谁需要改就直接改。项目小的时候没什么问题;一旦产品进入量产、多人并行开发、客户现场又不断出现问题,就会马上遇到一串熟悉的问题:
“昨天还能烧,今天怎么又不一样了?”
“客户现场的版本到底是哪一版?”
“这个 Bug 是谁改进去的?改之前是什么状态?”
“新功能还没做好,为什么又把量产版本改坏了?”
“同一个产品有十几个分支,哪个才是正式版本?”
版本管理解决的是“我们现在到底是什么版本”;分支管理解决的是“大家怎样同时开发,又不互相踩代码”。两者放在一起,才构成一套真正能让嵌入式研发跑起来的代码管理方法。
重要前提 本文以 Git 为基础,用嵌入式产品研发最常见的场景来讲解版本号、Tag、Commit、分支、发布、回滚和量产管理。重点不是 Git 命令本身,而是让研发团队形成一套简单、可执行、能追溯的规则。 |
一、 先把几个概念讲明白
第一次接触 Git 的工程师,最容易把“版本、分支、Tag、Commit”混在一起。其实可以把它们想象成研发现场的四样东西:
| 概念 |
通俗理解 |
嵌入式项目中的例子 |
| Commit(提交) |
一次“存档” |
修复按键偶发死机,提交一次代码 |
| Branch(分支) |
从主路上分出去的一条施工道路 |
开发 Wi-Fi 配网功能时单独开 feature/wifi |
| Tag(标签) |
给某个确定的存档贴上正式标签 |
v2.3.0 = 已验证、可发布的固件版本 |
| Release(发布) |
把经过验证的版本正式交付 |
输出 BIN/HEX、版本说明、校验值 |
| Version(版本号) |
告诉大家“这套软件到底是哪一版” |
产品固件版本:v2.3.1 |
最关键的一句话:
Commit 是过程,Branch 是并行开发,Tag 是确定版本,Release 是对外发布。
图1 Commit → Branch → Tag → Release:从代码变化到可交付固件
一个最直观的例子
假设智能门锁当前量产版本是 v1.8.0。研发准备增加指纹功能,于是从稳定代码上开出一条分支。开发过程中提交了 20 次 Commit,但这 20 次提交都只是研发过程,并不意味着出现了 20 个对外版本。
当指纹功能开发完成、回归测试通过,团队决定发布,这时才给最终代码打上 Tag:v1.9.0。之后测试又发现一个低风险 Bug,修复后发布 v1.9.1。
| 时间 |
动作 |
代码状态 |
| 周一 |
从 v1.8.0 开 feature/fingerprint |
开始开发 |
| 周二~周四 |
连续 Commit |
开发中,不对外承诺 |
| 周五 |
合入 release/1.9 |
准备发布 |
| 周五 |
测试通过,打 Tag v1.9.0 |
正式版本 |
| 下周 |
修复一个小 Bug |
v1.9.1 |
二、 为什么嵌入式软件特别需要版本管理
普通应用软件出了问题,重新下载一个程序往往就能解决;嵌入式产品则不同。软件和硬件、Bootloader、Flash 分区、通信协议、生产烧录工具经常是绑在一起的。版本混乱,最终可能直接变成现场返修。
1. 量产版本必须“能找回来”
客户反馈“设备重启”,第一件事不是马上改代码,而是先回答:客户手里的到底是什么固件?如果没有版本号、Tag、构建记录和发布包,研发只能拿“当前代码”去猜。
2. 同一时间往往有三件事并行
| 场景 |
典型情况 |
如果没有分支 |
| 量产维护 |
修复线上严重 Bug |
开发人员直接改主干,可能影响新功能 |
| 新功能开发 |
增加蓝牙、指纹、Matter 等 |
新功能未完成却混进量产版本 |
| 下一代产品 |
硬件已经变化 |
不同硬件代码互相污染 |
3. 嵌入式还有一个“硬件版本”问题
同一个软件可能对应多个 PCB 版本。例如 HW1.0 使用旧电源芯片,HW1.1 换了新芯片;如果软件没有把兼容关系管理好,烧错固件就可能出现无法启动、外设异常甚至现场变砖。
所以嵌入式版本管理,最好同时记录:产品型号 + 硬件版本 + 软件版本 + Bootloader 版本 + 编译时间/提交号。
三、 版本号到底怎么定?——用最简单的规则
推荐团队采用“主版本.次版本.修订版本”的形式,例如:v2.4.3。核心不是数字本身,而是团队必须约定:什么变化对应什么数字。
| 版本号 |
什么时候增加 |
通俗理解 |
例子 |
| Major 主版本 |
出现较大的架构/协议/产品变化,可能不兼容旧版本 |
换了一代产品 |
v2 → v3 |
| Minor 次版本 |
增加功能,但总体保持兼容 |
多了一个功能 |
v2.3 → v2.4 |
| Patch 修订版本 |
Bug 修复、稳定性优化、小改动 |
把问题修掉 |
v2.4.2 → v2.4.3 |
推荐规则:
新增功能:Minor +1,例如 v2.3.0 → v2.4.0。
Bug 修复:Patch +1,例如 v2.4.0 → v2.4.1。
重大不兼容变化:Major +1,例如 v2.x.x → v3.0.0。
图3 版本号解剖:v2.4.3 的每一段都有明确含义
升一位的时候,后面的位要不要归零?
建议归零,这是业界最通用的约定。升级 Minor 时,Patch 归零(v2.3.5 → v2.4.0,而不是 v2.4.5);升级 Major 时,Minor 和 Patch 都归零(v2.7.3 → v3.0.0)。原因很简单:数字越靠后,说明离“当前这次改动”越近;一旦前面的位变了,后面的位就该重新计数,否则版本号会给人错误的信息——好像 v2.4.5 比 v2.4.0 经过了更长时间打磨,但其实它们对应的是两条完全不同的开发线。
预发布版本怎么标?
除了正式版本号,团队常常还需要标注“这是给谁看的”。推荐用后缀区分,不占用正式的三段式版本号:
| 后缀 |
含义 |
示例 |
| -alpha |
内部早期验证,可能还有明显问题 |
v2.5.0-alpha.1 |
| -beta |
功能基本完整,邀请小范围用户/客户测试 |
v2.5.0-beta.1 |
| -rc |
候选发布版本,只等最后确认 |
v2.5.0-rc1 |
| (无后缀) |
正式发布版本 |
v2.5.0 |
嵌入式项目可以再增加两个维度
对于产品线较多的团队,单靠一个三段式版本号可能不够。可以把产品型号和硬件版本放在版本信息之外管理,而不是把所有东西都塞进一个超长版本号。
| 推荐记录 |
示例 |
| 产品型号 |
SmartDoor-2601 |
| HW 版本 |
HW1.2 |
| FW 版本 |
v2.4.3 |
| Boot 版本 |
BL1.1.0 |
| Git Commit |
8f3a21c |
| Build 时间 |
2026-08-21 09:20 |
| 固件校验 |
SHA-256 / CRC(按项目要求) |
这样做的好处是:现场拿到一台设备,只要读出版本信息,就能反查到“哪套代码、哪个硬件、什么时候编译出来的”。
四、 分支到底是什么?——把它理解成“不同施工队”
可以把代码仓库想象成一条主干道路。所有人都直接在主干上施工,很快就会堵车。分支就是从主干旁边开出施工道路:每个团队在自己的道路上施工,完成后再把结果合回主路。
| 分支 | 职责 |
是否允许长期存在 |
例子 |
| main / master | 稳定代码、正式版本 |
是 |
生产可用代码 |
| develop(可选) | 下一版本集成开发 |
视团队而定 |
v2.5 开发中 |
| feature/* | 开发一个具体功能 |
否,完成即合并 |
feature/fingerprint |
| release/* | 版本冻结、测试和发布 |
短期 |
release/v2.5.0 |
| hotfix/* | 线上紧急问题修复 |
短期 |
hotfix/v2.4.1 |
为什么不建议每个人一个永久分支?
因为分支存在时间越长,与主干的差异越大。差异越大,最后合并时越痛苦。嵌入式项目经常有大量硬件驱动、宏定义、板级配置文件,长期分支特别容易产生冲突。
所以一个很实用的原则是:
| “短分支、早合并;主干保持可用;正式版本用 Tag固定。” |
推荐的分支关系
图2 推荐的简单分支关系:功能开发最终回到稳定主干
五、 一个嵌入式团队到底需要多少分支?
不要一上来就照搬复杂的 Git Flow。对于大多数嵌入式研发团队,建议从“四类分支”开始:main、feature、release、hotfix。
| 分支 |
什么时候开 |
谁负责 |
什么时候关 |
| main |
长期存在 |
团队共同维护 |
不关闭 |
| feature/功能名 |
开始新功能 |
功能开发工程师 |
合入 release/main 后删除 |
| release/版本号 |
功能冻结、进入测试 |
版本负责人 |
正式发布后删除 |
| hotfix/版本号 |
线上严重 Bug |
指定开发人员 |
修复并发布后删除 |
实际开发流程
| 步骤 |
研发动作 |
结果 |
| ① 开功能 |
从 main 拉 feature/fingerprint |
独立开发,不影响稳定代码 |
| ② 日常提交 |
小步 Commit |
每天都有可追溯记录 |
| ③ 功能完成 |
提交 Merge Request / Pull Request |
代码评审 |
| ④ 版本冻结 |
建立 release/v2.5.0 |
只修 Bug,不再随意加功能 |
| ⑤ 测试 |
测试、回归、硬件验证 |
记录问题和结果 |
| ⑥ 发布 |
合并 main + 打 Tag v2.5.0 |
生成正式固件 |
| ⑦ 线上问题 |
从 Tag v2.5.0 拉 hotfix |
修复并发布 v2.5.1 |
注意 release 分支的核心意义不是“又多了一条分支”,而是给团队一个明确的信号——从今天开始,这个版本进入冻结期。 |
六、 Commit 怎么写?——不要让 Git 变成“垃圾堆”
很多团队 Git 历史里充满了“修改”“测试一下”“111”“再改改”。几年后出了问题,谁都看不懂。Commit 信息应该让一个没有参与当天开发的人,也能看懂你改了什么。
| 差的写法 |
好的写法 |
| 修改一下 |
fix: 修复门锁低电量时误报故障 |
| 测试 |
test: 增加低温环境下电机堵转测试日志 |
| 再改 |
fix: 修复 RS485 接收超时未退出问题 |
| 蓝牙优化 |
feat: 增加蓝牙配网失败自动重试 |
| 改参数 |
perf: 优化电机启动电流限幅参数 |
推荐一个简单的 Commit 前缀
不需要发明团队专属的复杂规则,业界已经有一套被广泛使用的前缀约定(Conventional Commits),拿来直接用就好:
图5 Commit 前缀速查卡:一眼看出这次提交在做什么
Commit 的粒度:多小算太小,多大算太大?
新手常见的两种极端:一种是一天写一个几千行的大 Commit,把好几件事混在一起;另一种是每敲几行代码就提交一次,导致历史里全是噪声。一个实用的判断标准是——
能不能用一句话,说清楚这次提交做了什么?如果要用“并且”连接两件不相关的事,通常就该拆成两次提交。
如果这次提交单独 revert(撤销),会不会连带撤掉不该撤的东西?如果会,粒度就太粗了。
嵌入式项目还有一条额外建议:涉及寄存器配置、中断优先级、时序参数这类“改错了会很难排查”的改动,尽量单独提交,并在 Commit 信息里写清楚改动前后的数值和原因,方便日后靠 git blame 直接定位。
七、 什么时候必须打 Tag?
Tag 不是可有可无的装饰,而是版本管理里“法律文件”级别的动作。以下几种情况必须打 Tag:
| 场景 |
是否必须打 Tag |
示例 |
| 合入 release 分支准备测试 |
建议 |
v2.5.0-rc1 |
| 测试通过,准备正式发布 |
必须 |
v2.5.0 |
| 正式量产 |
必须 |
固定量产版本 |
| 现场紧急修复 |
必须 |
例如 v2.5.1 |
Release Candidate(候选版本)有什么用?
对于需要测试的嵌入式项目,可以在正式版本前增加 RC,例如 v2.5.0-rc1。测试发现问题后继续修复,再出 v2.5.0-rc2。最终测试通过,再打正式 Tag:v2.5.0。
这样测试团队不会把“正在修改的代码”和“正式发布代码”混在一起。
正式发布包建议包含什么?
| 文件 |
作用 |
| firmware.bin / hex |
实际烧录文件 |
| version.txt |
记录 FW/HW/BL/Commit 等信息 |
| release_notes.md / xlsx |
本版本改了什么、修了什么 |
| checksum.txt |
校验文件完整性 |
| 测试报告 |
证明这个版本经过哪些测试 |
| 升级说明 |
说明升级方式、限制和回滚方式 |
八、 嵌入式最重要的一点:版本必须“可追溯”
真正成熟的版本管理,不是 Git 仓库里有很多 Tag,而是能做到:拿到一台现场设备,就能追溯到它是怎么编出来的。
| 设备现场 |
研发仓库 |
发布资料 |
| FW v2.5.1 |
Tag v2.5.1 |
正式发布包 |
| HW1.2 |
硬件兼容配置 |
硬件版本说明 |
| BL1.1.0 |
Bootloader Tag |
Boot 版本记录 |
| Commit 8f3a21c |
精确代码状态 |
构建记录 |
| Build 2026-08-21 |
CI / 编译环境 |
编译日志 |
图4 现场固件 → Git Tag → Commit → 构建记录:形成完整追溯链
建议把版本号编进设备
例如设备启动后通过串口、调试菜单、Web 页面或 App 查询版本,输出:
| Product: SmartDoor-2601 HW: 1.2 FW: 2.5.1 BL: 1.1.0 Git: 8f3a21c |
这样售后拍一张版本信息截图,研发就能快速判断问题属于哪个软件版本。
为什么不能只记录“v2.5.1”?
因为同一个 v2.5.1 如果被工程师重新编译过,二进制文件可能已经不同。正式发布版本应该固定 Tag,并保留构建产物;如果项目允许重复构建,还应尽量固定编译环境、工具链和依赖版本。
九、 最容易踩的 8 个版本管理坑
这 8 个坑不是理论推演,而是几乎每个从“随便改改”走向规范化的团队,都会真实踩到的问题。提前认识它们,能少走很多弯路:
图7 最容易踩的 8 个版本管理坑
特别提醒:不要用“最终版、最终版2、最终版3”管理固件
文件名例如:door_final.bin、door_final2.bin、door_final_new.bin、door_final_new2.bin,看起来很熟悉,但它实际上等于没有版本管理。
正确做法是:产品型号 + 固件版本 + 硬件版本 + 发布日期/构建信息,例如:SmartDoor-2601_FW2.5.1_HW1.2.bin。
十、 遇到量产 Bug,正确的处理路径是什么?
假设当前量产版本是 v2.5.0,研发正在开发下一代 v2.6.0。客户突然反馈:某批设备在特定场景下会死机。
| 错误做法 |
正确做法 |
| 直接切到当前开发分支修 |
从 v2.5.0 Tag 拉 hotfix/v2.5.1 |
| 修完直接发给客户 |
先复现 → 修复 → 回归 → 发布 |
| 只把新代码复制过去 |
保留 Commit 和 Tag,保证可追溯 |
| 修完不告诉 main |
hotfix 合入正式维护线,并同步到后续开发线 |
| 直接覆盖旧 bin |
生成新的 v2.5.1 发布包,旧版本保留 |
推荐流程
图6 量产 Bug 的标准处理路径
为什么要“同步到后续版本”?
因为 Bug 很可能不是只存在于 v2.5.0。假如 v2.6.0 的代码是从旧代码继续开发的,而修复没有同步进去,那么你可能刚修完量产版本,下一个版本又把同一个 Bug 带回来了。
十一、 嵌入式软件版本管理,还要管好 OTA
如果产品支持 OTA,版本管理的重要性会再提高一个等级。因为此时“版本”不仅是研发内部的事情,还决定设备能不能升级、能不能回滚。
| 要管理的内容 |
示例 |
为什么重要 |
| 当前版本 |
v2.4.0 |
判断设备处于什么状态 |
| 目标版本 |
v2.5.0 |
判断能否升级 |
| 最低允许版本 |
v2.3.0 |
防止非法降级/跨版本升级 |
| 硬件兼容范围 |
HW1.1~HW1.3 |
避免刷错硬件 |
| Bootloader 兼容 |
BL1.1.x |
确保升级后能启动 |
| 升级包完整性 |
签名/Hash/CRC |
防止包损坏或被篡改 |
| 失败回滚 |
回到 v2.4.0 |
防止升级变砖 |
版本号不能只是“显示给用户看”
OTA 系统应该真正理解版本。例如设备当前 v2.4.0,服务器下发 v2.5.0;Bootloader 或升级管理模块要检查产品型号、硬件版本、升级包版本、完整性和必要的兼容条件。
特别是 Bootloader、App、分区表、协议版本之间存在依赖关系时,必须把兼容矩阵纳入版本管理。
十二、 工具怎么选:Git 平台、CI 和分支保护
规则定好之后,还需要工具把规则“落地”,而不是全靠工程师自觉。选型不用追求最新最全,够用、团队会用、能长期维护,比什么都重要。
图8 Git 平台怎么选:私有部署 vs 云端托管
分支保护:把规则写进工具里,而不是写进文档里
文档里写“不要直接改 main”,工程师赶时间的时候照样会忘。更可靠的做法是在 Git 平台上开启分支保护,让工具帮团队守住底线:
| 保护项 |
作用 |
| 禁止直接推送 main/release |
所有改动必须通过 Merge Request 进入 |
| 至少 1~2 人评审通过才能合并 |
降低单人失误进入主干的概率 |
| 必须通过 CI 检查才能合并 |
编译失败、单元测试不过,直接挡在门外 |
| 禁止强制推送(force push) |
防止历史被意外覆盖,Tag 更安全 |
| Tag 权限单独收紧 |
避免正式版本被随意打或误删 |
CI 能帮嵌入式团队做到什么程度?
很多嵌入式团队会觉得“CI 是软件互联网公司的事”,其实哪怕只是最基础的自动化,也能省下大量重复劳动和低级错误:
每次提交自动编译,第一时间发现“提交了编不过的代码”。
自动跑单元测试和静态检查(如 MISRA 规则),把问题挡在合并之前。
release 分支自动生成带版本号、带 Commit 信息的固件包,减少人工复制粘贴出错。
打 Tag 时自动触发正式构建,产出物和构建日志自动归档,天然形成追溯记录。
提示 不需要一步到位。先从“自动编译 + 自动跑单元测试”开始,跑顺了再逐步加静态检查、自动打包、自动归档。 |
十三、 给嵌入式团队的一套“够用就好”的管理规则
如果团队现在还没有统一规则,不建议一次性上几十条制度。先执行下面 10 条,已经可以解决大部分问题。
主干 main 始终保持可编译、可集成,不能把半成品随意提交进去。
新功能必须开 feature 分支,功能完成后再合并。
进入测试阶段建立 release 分支,冻结新增功能。
正式版本必须打 Tag,Tag 一旦发布原则上不修改。
量产 Bug 从对应的正式 Tag 拉 hotfix,不要从最新开发代码修。
每个正式版本必须保留固件包、版本说明、测试结果和构建信息。
Commit 不写“修改一下”,至少说明改了什么。
关键代码合入主干前必须 Code Review。
设备内部必须能够查询 FW/HW/BL 版本,最好还能查询 Git Commit。
版本升级、回滚、硬件兼容关系必须纳入版本管理,不能只靠工程师记忆。
推荐的最小落地方案
| 项目 |
建议 |
| 代码平台 |
GitLab / GitHub Enterprise / Gitea 等 Git 平台 |
| 主干 |
main |
| 功能分支 |
feature/功能名 |
| 发布分支 |
release/vx.y.z |
| 紧急修复 |
hotfix/vx.y.z |
| 正式版本 |
Tag:vx.y.z |
| 评审 |
Merge Request / Pull Request |
| 构建 |
统一编译脚本,最好逐步接入 CI |
| 发布物 |
BIN/HEX + 版本说明 + 测试记录 + 校验值 |
| 追溯 |
设备版本 ↔ Tag ↔ Commit ↔ 构建产物 |
十四、 团队最常问的几个问题
Q1:一两个人的小项目,也要搞这一整套吗?
不需要照搬全部规则,但建议至少保留三件事:main 分支保持可用、正式发布必须打 Tag、Commit 信息说清楚改了什么。这三条几乎不增加负担,却能在项目变大、加入新人的那一天,帮你省下大量“考古”时间。
Q2:要不要直接照搬 Git Flow?
不建议。经典 Git Flow 定义了 develop、feature、release、hotfix、support 五类分支,规则详尽,但对大多数嵌入式团队偏重。本文推荐的 main + feature + release + hotfix 四分支模型,是 Git Flow 的简化版,覆盖了嵌入式研发最核心的场景;如果团队后续确实需要更细的流程(例如多个版本长期并行维护),再逐步引入 develop 或 support 分支也不迟。
Q3:分支和 Tag 的命名有没有推荐规范?
| 类型 |
推荐格式 |
示例 |
| 功能分支 |
feature/简短描述 |
feature/ble-reconnect |
| 发布分支 |
release/vX.Y.0 |
release/v2.5.0 |
| 紧急修复分支 |
hotfix/vX.Y.Z |
hotfix/v2.5.1 |
| 正式版本 Tag |
vX.Y.Z |
v2.5.0 |
| 候选版本 Tag |
vX.Y.Z-rc序号 |
v2.5.0-rc1 |
Q4:老项目历史 Git 记录一团糟,还有必要补救吗?
有必要,而且不用推倒重来。可以从“今天这一刻”开始执行新规则:老代码保持原状,从下一个正式版本开始统一打 Tag、统一 Commit 格式。几个版本之后,团队会发现新的问题基本都能追溯,老问题的比例也在自然下降。
Q5:多个产品共用一套底层代码,版本号怎么处理?
常见做法是底层库和产品固件分开维护版本号:底层库(比如通信协议栈、驱动库)作为独立仓库,有自己的版本号;产品固件仓库通过“引用某个底层库版本”的方式集成(例如 Git Submodule,或直接在版本记录里写明依赖的底层库 Tag)。这样底层库升级不会强迫所有产品固件同步改版本号,出问题时也能明确是底层库的问题还是产品层的问题。
十五、 一页纸记住:嵌入式版本与分支管理
如果只记住一张图,可以记住下面这条主线:
图9 最推荐嵌入式团队掌握的版本与分支主流程
一句话记忆 分支解决“多人怎么一起开发”,版本解决“我们现在到底是哪一版”,Tag 解决“正式版本怎么固定”,发布记录解决“出了问题怎么追溯”。 |
再简单一点:
“开发走分支,发布打 Tag,量产留包,现场可追溯。”
小结
对于嵌入式研发团队,版本管理不是为了让 Git 看起来专业,而是为了降低研发协作和量产维护的风险。团队规模越大、产品生命周期越长、硬件版本越多、OTA 越复杂,越需要把代码、版本、硬件、测试和发布物连成一条完整的链路。
真正成熟的管理,不是“分支越多越高级”,而是规则足够简单,所有工程师都愿意遵守,而且出了问题能够在几分钟内回答:设备是什么版本、代码在哪里、谁改的、为什么改、哪个版本验证过、怎么回滚。
|