第三章 版本控制策略:Git在固件开发中的分支模型、代码审查与合并策略

说到版本控制,很多做MCU开发的兄弟第一反应是:「Git?那玩意儿不是做上层软件用的吗?我们搞单片机的,一个人写几千行代码,要什么分支?」

嗯,我以前也这么想。直到有一次,一个客户现场反馈的bug,我需要同时维护三个不同版本的固件——量产版、测试版、还有给客户定制的魔改版。那时候我还在用文件夹备份,什么「v1.0_final」、「v1.0_final_真的最终版」、「v1.0_final_打死不改版」……你懂的,那场面简直是一场灾难。

从那以后,我彻底转向了Git。今天我就把这几年的经验,特别是针对MCU固件开发的分支策略、代码审查和合并技巧,跟你好好聊聊。

3.1 为什么MCU开发需要分支模型?

很多人觉得MCU项目小,一个人就能搞定,用Git纯属多余。但我想说,恰恰是因为资源紧张、硬件耦合度高,我们才更需要规范的分支管理。

我遇到过最典型的情况:硬件改版了,外设引脚变了,但软件还在用旧配置。如果没有分支隔离,你改着改着,连自己都分不清哪个版本对应哪块板子。

说白了,分支模型解决的是三个核心问题:

  • 并行开发:不同功能模块可以同时推进,互不干扰
  • 版本隔离:量产版本、测试版本、开发版本各走各的道
  • 风险控制:出问题了能快速回退,不至于全军覆没

核心观点:MCU固件的分支模型,不是为了炫技,而是为了在硬件和软件深度耦合的情况下,还能保持清晰的版本脉络。

3.2 推荐的分支模型:简化版Git Flow

Git Flow那套完整的分支模型,说实话,对MCU开发来说太重了。你想想看,一个只有两三个人的固件团队,搞什么release分支、hotfix分支、develop分支……光切换分支就够你喝一壶的。

我个人习惯用一套简化版Git Flow,只保留最核心的几个分支:

分支名称 用途 生命周期
main 生产发布版本,只接受经过完整测试的代码 永久
develop 日常开发主分支,所有功能分支从这里拉出 永久
feature/* 新功能开发,比如 feature/i2c-driver 短期,开发完成后合并回develop
fix/* bug修复,比如 fix/uart-overflow 短期,修复后合并回develop和main
release/* 发布候选版本,只做最后的回归测试和版本号修改 短期,测试通过后合并到main和develop

我的经验:对于MCU项目,我建议把main分支保护起来,不允许任何人直接推送。所有合并必须通过Pull Request。这个习惯救过我很多次——有一次同事不小心把调试用的死循环代码推到了main,幸亏有保护机制,才没影响到产线。

3.3 固件开发中的代码审查:不只是走形式

说到代码审查,很多MCU开发者觉得:「就那么几行寄存器配置,有什么好审的?」

我曾经也这么想,直到有一次,一个同事在配置SPI时钟极性时,把SPI_CPOL_High写成了SPI_CPOL_Low。就这一位之差,整个产线的设备通信全部异常,排查了整整两天。如果当时有人review一下,一眼就能看出来。

代码审查在固件开发中,重点看这几个方面:

  • 硬件配置的正确性:GPIO复用功能、时钟树配置、中断优先级——这些错了就是硬伤
  • 临界区保护:中断和主循环共享的变量,有没有加原子操作或关中断保护?
  • 资源泄漏:DMA缓冲区有没有及时释放?定时器有没有停止?
  • 可移植性:硬编码的地址、魔法数字,有没有用宏定义替代?

避坑指南:我曾经见过一个项目,代码审查只检查缩进和命名规范,真正的逻辑问题一个都没发现。结果固件在高温环境下频繁死机,最后定位到是一个全局变量在中断和主循环中同时被修改,没有加volatile关键字。所以,代码审查一定要关注实质性的问题,别只盯着表面功夫。

3.4 合并策略:如何避免「合并地狱」

合并冲突,是每个用Git的人都会遇到的噩梦。在MCU开发中,这个问题尤其突出——因为固件代码里充满了硬件配置头文件,两个人同时修改同一个芯片的引脚映射,冲突几乎是必然的。

我总结了几条实用的合并策略:

  1. 小步提交,频繁合并:不要攒了一周的代码才合并。每天至少合并一次,冲突范围小,容易解决。
  2. 功能分支尽量短命:一个功能分支的生命周期不要超过三天。超过三天,说明功能拆分得不够细。
  3. 使用rebase保持历史清晰:在合并到develop之前,先用git rebase把功能分支的提交整理成一条直线。这样合并后的历史记录干净整洁,方便后续回溯。
  4. 硬件配置文件的处理:把芯片相关的配置(引脚映射、时钟配置)单独放到一个文件中,并且尽量少修改。如果必须修改,提前和团队沟通。
# 我常用的合并流程
# 1. 切换到功能分支
git checkout feature/i2c-driver

# 2. 变基到最新的develop
git fetch origin
git rebase origin/develop

# 3. 解决冲突(如果有的话)
# 4. 强制推送(注意:只有自己的分支才能这样做)
git push --force-with-lease

# 5. 创建Pull Request,请求合并到develop

一个小技巧:在rebase之前,先用git stash保存当前工作区的修改。我曾经因为rebase过程中出现冲突,手忙脚乱地搞丢了半天的代码。从那以后,我每次rebase之前都会先stash一下,给自己留条后路。

3.5 固件版本号与Tag管理

版本号这件事,看似简单,但做不好真的很坑。我见过有人用日期当版本号,结果同一天发布了三个版本,全乱了。

我建议采用语义化版本号,格式为:主版本号.次版本号.修订号

  • 主版本号:硬件大改版,或者API不兼容的修改
  • 次版本号:新增功能,但向下兼容
  • 修订号:bug修复,功能不变

每次发布正式版本时,在Git中打上对应的Tag:

git tag -a v2.1.0 -m "Release version 2.1.0: 新增I2C驱动,修复UART溢出bug"
git push origin v2.1.0

这样,产线需要回退到某个版本时,直接根据Tag切换就行,不用再翻聊天记录找「上次那个能用的版本是哪个」。嗯,这个坑我踩过,所以特别强调一下。

3.6 总结:适合MCU团队的Git工作流

说了这么多,最后给你一个可以直接用的模板:

  1. 日常开发:从develop拉出feature/xxx分支,开发完成后提交Pull Request
  2. 代码审查:至少一个人review,重点关注硬件配置和临界区保护
  3. 合并:使用rebase保持历史清晰,合并到develop
  4. 发布:从develop拉出release/x.x.x分支,测试通过后合并到main并打Tag
  5. 紧急修复:从main拉出fix/xxx分支,修复后同时合并到maindevelop

这套流程我用了三年,团队从两个人扩展到八个人,代码库从几千行膨胀到十几万行,基本没出过大乱子。你刚开始可能会觉得麻烦,但坚持一个月,你就会发现——原来版本控制不是束缚,而是解放。

好了,关于Git在固件开发中的分支模型、代码审查和合并策略,我就聊到这里。下一章,我们会聊聊自动化构建和测试,那才是CI/CD真正发力的地方。