RTL 工程 Git 流程:分支策略、版本标签与入库边界
更新时间:2026-11-04。单个
.v文件可以随手存盘,但一个要交付、要流片、要接第三方 IP 的硬件工程,必须回答三个工程问题:改动在哪个分支上发生、某次交付对应哪个版本、哪些文件进仓库哪些不进。本篇用一个最小计数器工程,把 RTL 工程 Git 流程从头走一遍:建仓与分支、回归通过才合并、tag 固定交付版本、.gitignore划清入库边界、submodule 管 vendor IP。单文件仿真的四步闭环见 仿真是什么,Git 操作本身的高频踩坑见 IC 设计中的 Git 常见坑。
本文要回答的问题
- 硬件仓库的分支怎么分?
main、develop、feature/*各放什么状态的代码? - 一次功能改动从开工到进主线,标准的 Git 命令序列是什么?
.vvp、.vcd、obj_dir/、sim_build/、比特流文件为什么不入库,规则怎么写?- 第三方 IP 有哪几种入库方式,什么时候用 submodule,什么时候直接放 vendor 目录?
- commit 切多细、Code Review 在合并前具体检查什么?
先给一张流程总览卡:
读这张卡抓住一条主线:分支隔离改动,回归守门质量,合并与 tag 固定历史。任何一步被跳过——不回归就合并、在 main 上直接改、产物随手 git add . 进库——都会在两周后的交付复盘里变成事故。
一、版本管理在硬件流程里的位置
Git 不产生新的设计或验证结果,它位于「人」与「工具产物」之间:设计与验证工程师把 rtl/、tb/、约束与脚本的每一次改动作为提交存入历史;iverilog、Verilator、yosys 等工具从被提交的源码出发生成产物;产物不进历史,任何历史版本都能靠同一份源码重新构建。
读这张图注意两条边界。横向是一条代码自己的生命周期:工作区里的改动先上 feature 分支,回归通过后合并进 main,交付时在 main 上打 tag;纵向是第三方代码的进入方式——vendor IP 不复制进主仓历史,而是以 gitlink(一个指向 IP 仓某提交号的引用)挂在 vendor/ 下,主仓记录「这个工程用的是 IP 的哪个版本」,却不承载 IP 的代码演进。多人长期协作时可以在 feature 与 main 之间加一条 develop 集成分支:feature 先合 develop 做夜间全量回归,稳定后再进 main;三五个人的小团队直接 feature 对 main 也完全成立,关键是 main 的状态纪律不变。
二、操作序列逐段解读:从 git init 到 tag
完整序列在第五节真跑,这里先把每条命令的参数和「不加会怎样」讲清。
建仓用 git init -b main,-b main 直接把初始分支命名为 main,省去旧版 git 默认 master 后再改分支名的麻烦。建仓后第一件事不是提交代码,而是写 .gitignore——规则先于产物存在,工具生成物从第一次编译起就不会出现在 git status 里。
开分支用 git checkout -b feature/count-enable:-b 表示创建并切换,分支名带类型前缀(feature/、fix/、exp/)加简短英文描述,一眼能看出用途。一个分支只做一件事:给计数器加使能是一件事,顺手重命名时钟信号是另一件事,混在一起评审时无法只回滚其中一半。
提交前看改动面用 git diff --stat(暂存后用 git diff --cached --stat main)。--stat 只给文件名与增删行数,是评审前的自检:本次改动应当只落在预期的 rtl/、tb/ 文件上;如果输出里出现约束文件、综合脚本或无关模块,说明分支里混入了别的事。
合并用 git merge --no-ff feature/count-enable。--no-ff(no fast-forward)强制生成一个合并提交,即使主线没有新提交也保留「这串改动属于同一个 feature」的拓扑信息;默认的快进合并会把 feature 上的提交平铺进 main,事后无法按功能整组回滚,也画不出分支图。合入后要在 main 上再跑一遍回归——合并结果可能与分支头不同(主线期间进过别的改动)。
发布用 git tag -a v1.0 -m "release v1.0: ..."。-a 创建注解标签:它有作者、日期、说明文字,并且可以事后 git tag -n、git verify-tag,是给交付件用的标签;轻量标签(不带 -a)只是一个指针,只适合本地临时标记。版本号语义化:不兼容的端口改动升大版本,新增功能向后兼容升小版本,修错升补丁号。流片回来的工程、交付客户的快照、进回归基线的版本,各打一个 tag,日后任何「那个版本有问题」的对话都有唯一指代。
三、入库边界:什么进 Git,什么不进
判据只有一条:能否由入库的源码与脚本重建。能重建的不入库,不能重建的必须入库。
| 文件 | 入库? | 理由 |
|---|---|---|
rtl/*.v、tb/*.v、filelist、约束 .sdc/.xdc | 入库 | 设计源头,不可重建 |
Makefile、综合脚本、.gitmodules | 入库 | 重建过程本身 |
| IP 的 vendor 快照或 submodule gitlink | 入库 | 别人的代码也要固定版本 |
*.vvp(iverilog 字节码)、build/ | 不入库 | make 一条命令重建,且绑定本机工具版本 |
*.vcd、*.fst 波形 | 不入库 | 仿真产物,二进制大文件,diff 无意义 |
obj_dir/(Verilator)、sim_build/ | 不入库 | 编译目录,整体可删 |
比特流 .bit/.bin、网表中间件 | 默认不入库 | 大二进制;需要存档时走制品仓或 release 附件,不进源码历史 |
对应的 .gitignore 写法:
*.vvp
*.vcd
*.fst
build/
obj_dir/
sim_build/
*.log
.DS_Store规则是否生效不要靠猜,用 git check-ignore -v build/sim.vvp 验证:输出会指出是第几行规则命中了这个文件,是排查「为什么这个产物没被忽略」的直接手段。比特流是否入库有团队差异:有的组在 tag 处把最终 .bit 作为 release 附件存档(源码仓仍干净),有的组全交制品服务器;共同底线是不要让几百 MB 的二进制在每次提交里膨胀 .git 体积。
vendor IP 的三种管法按团队规模选:
| 方式 | 做法 | 适合场景 |
|---|---|---|
| vendor 目录直接拷贝 | IP 文件拷进 vendor/uart_ip/ 随主仓提交 | IP 不再改动、小团队,想让 clone 即完整 |
| submodule | IP 独立建仓,主仓用 gitlink 固定到某个提交 | IP 多工程共享、要跟随 IP 版本升级 |
| subtree | git subtree add 把 IP 历史并入主仓 | 用 submodule 但 clone 者常忘 --recursive 的团队 |
submodule 的代价是操作复杂:clone 要带 --recurse-submodules,升级要在子模块里提交后主仓再记一次新指针;它换来的好处是主仓历史里每个版本都精确对应 IP 的某个 commit,IP 自己的版本演进不污染主仓。小工程从 vendor 目录起步,遇到「两个工程要共享同一份 IP」时再迁 submodule 不迟。
四、commit 粒度与 Code Review 检查点
好的提交粒度标准是「一个提交讲一件完整的事,且这件事单独拿出来回归是过的」。给计数器加使能,RTL 与对应 testbench 应当在同一个提交里——只提 RTL 不提 TB,中间状态编译不过;把使能、格式清理、新 Makefile 目标塞进一个提交,评审者只能整体接受或整体拒绝。提交信息用一行祈使句说清「做了什么」(feat(rtl): add count-enable, tb covers hold and resume),必要时空一行补「为什么」。
合并前的评审检查点对硬件工程比软件更关键,因为缺陷成本是重新综合乃至重新流片:
| 检查点 | 具体看什么 |
|---|---|
| 功能正确 | 新行为有对应 testbench 断言,TEST PASS 是自校验结论不是「波形看着对」 |
| 改动面干净 | git diff --stat 只含本功能的文件,没有顺手改的无关行 |
| 可综合性 | RTL 没有 $display、#延迟、初始化依赖等仿真专用写法混进设计 |
| 复位与时钟 | 新逻辑挂在既有时钟域,复位策略与同模块一致 |
| 入库边界 | diff 里没有任何产物文件、大二进制或本地路径 |
| 回归基线 | 不仅新用例过,旧用例全过;批量跑法见 iverilog 回归脚本流程 |
评审通过后合并、合入后主线再跑一次回归,然后才允许打 tag。把这套检查接到 CI 上(如 Verilator 接入 CI 的流程),机器能拦的就不靠人记;回归该多久全量跑一次的取舍见 数字设计回归该多久跑一次。
五、真实跑一遍:最小工程的完整序列
在 Git 2.42.1、Icarus Verilog 13.0、GNU Make 3.81 下,用一个 4 位计数器工程实跑:建仓与初始提交 → 开 feature/count-enable 分支加使能端口、TB 增加保持 4 拍与恢复计数检查 → git diff --stat 自检改动面 → make sim 回归 → --no-ff 合并 → 主线再回归 → 打 v1.0 → 验证忽略规则 → 本地 IP 仓演示 submodule 挂载。分支开发到打标签这段实录如下:
读这份实录抓三个点。分支上 make sim 输出 TEST PASS tb_counter4,使能新增的「保持 4 拍 q 不变、恢复后继续数到 15 再回卷」全部由 testbench 断言,不是人眼看波形;合并提交后 git log --oneline --graph 出现分叉再汇合的菱形拓扑,tag: v1.0 挂在合并提交上;合入后主线直接 vvp 重跑仍是 PASS,证明 tag 指向的主线状态确实可回归。
.gitignore 的效果不用形容词,用 git status 与 git check-ignore -v 的原样输出说话:
===== [5] .gitignore 证据:产物满目录,git 视野干净 =====
sim.vvp
wave.vcd
--- git status --short(应无输出)---
--- git check-ignore -v(逐条给出命中规则)---
.gitignore:5:build/ build/sim.vvp
.gitignore:5:build/ build/wave.vcd
.gitignore:6:obj_dir/ obj_dir/
.gitignore:7:sim_build/ sim_build/x.vvp
--- git status --ignored --short(!! 开头即被忽略)---
!! build/build/ 里 .vvp 与 .vcd 都在,git status --short 却一片空白;check-ignore 逐条指出命中规则所在行。同一份实录尾部还有 submodule 段:git submodule add 之后 status 只显示 .gitmodules 与 vendor/uart_ip 两项,后者在仓库里是模式 160000 的 gitlink——一个提交号引用,IP 的源码文件并不进入主仓历史。
实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon)
- 工具版本:git version 2.42.1;Icarus Verilog version 13.0 (stable) (v13_0);GNU Make 3.81
- 日期:2026-11-04
- 完整命令:
git init -b main demo-repo→ 首批提交(.gitignore/Makefile/rtl/tb)→git checkout -b feature/count-enable→ 改 RTL 与 TB →git diff --stat→make sim(TEST PASS)→ 提交 → 回 main 后git merge --no-ff→ 主线make sim→git tag -a v1.0→git status/git check-ignore -v验证忽略 → 本地建vendor-ip-repo后git submodule add ../vendor-ip-repo vendor/uart_ip并提交。
代码获取
完整 RTL、自校验 testbench、Makefile、.gitignore 与一键复现脚本已同步到公开仓库:geek-doc.cn/experiments/flow-git-rtl-versioning,克隆后进入该目录执行 make sim 可直接看到 TEST PASS;执行 bash demo-git-flow.sh 可离线复现建仓、分支、回归、合并、打 tag 与本地 submodule 挂载的完整序列,不依赖任何网络地址。
这一篇在知识体系里的位置
- Git 操作本身的高频坑(换行符、大文件误提交、submodule 断链):读 IC 设计中的 Git 常见坑;
- 入库的 Makefile 怎么写 sim/synth/clean 三个目标:读 硬件工程 Makefile:sim 仿真、synth 综合与 clean 清理;
- 多文件 RTL 的 filelist 与目录组织:读 iverilog 多文件工程组织;
- 合并前守门的批量回归怎么搭:读 iverilog 回归脚本流程,想接到服务器上定时跑读 Verilator 接入 CI 的流程;
- 仿真工程 Makefile 的双后端模板:读 仿真工程 Makefile 通用模板。
总结
RTL 工程 Git 流程的核心是给主线状态定纪律:feature 分支隔离每个功能,提交前 git diff --stat 自检改动面,自校验 testbench 回归通过才提交,回主线一律 --no-ff 合并并在合入后再跑一次,交付状态用 -a 注解 tag 固定;入库边界按「能否重建」划分,vvp、vcd、obj_dir/、sim_build/ 与比特流全部排除,规则用 git check-ignore -v 验证;第三方 IP 小团队放 vendor 目录、需要跨工程共享版本时用 submodule 只记提交号;评审盯功能、改动面、可综合性、复位时钟与入库边界五件事。分支、标签、忽略规则都不复杂,真正的门槛是让 main 上每个提交都回归通过,做到这点,历史版本才配称为版本。