Zynq 实战 25|版本控制与 CI/CD:Git 管理 Vivado 工程 + GitHub Actions 自动验证
Zynq 实战 25|版本控制与 CI/CD:Git 管理 Vivado 工程 + GitHub Actions 自动验证
这是《Zynq FPGA 嵌入式系统设计实战》系列第 25 篇。 板子:Pynq-Z2(XC7Z020-1CLG400C)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 24|软硬件协同仿真》
0. 这一篇要解决什么问题
FPGA 项目 Git 管理有两种常见失误:
失误 A:什么都 commit。.xpr 文件 5MB、.runs/ 目录 3GB、编译缓存 1GB,仓库第一周就膨胀到无法克隆,git clone 要跑半小时。
失误 B:什么都不 commit。“我的 Vivado 工程在本地,你想用就来找我拷”——没有版本记录,无法协作,硬盘一坏全没了。
正确做法是只 commit 能重建工程的最小集合:RTL 源码、约束文件、TCL 脚本。任何人拿到这个仓库,一条命令就能从零重建 Vivado 工程,然后一键综合。
本文覆盖:
- Vivado 工程的 Git 文件分类
- 完整可用的
.gitignore - 从零重建工程的
build.tcl - GitHub Actions 用 Yosys 做 RTL lint(无需 Vivado License)
- 自建 runner 跑 Vivado 时序检查
- Git LFS 管理比特流文件
1. Vivado 工程文件分类:什么该提交,什么不该
2. 推荐仓库目录结构
zynq-myproject/
├── .github/
│ └── workflows/
│ ├── fpga-lint.yml # Yosys RTL lint(无 License)
│ └── vivado-timing.yml # Vivado 时序检查(自建 runner)
├── hdl/
│ ├── top/
│ │ └── top_design.v # 顶层
│ ├── ip/
│ │ ├── pwm_ctrl/
│ │ │ ├── pwm_ctrl.v
│ │ │ └── pwm_ctrl_tb.v
│ │ └── adc_spi/
│ │ └── adc_spi.v
│ └── sim/
│ └── tb_top.v # 顶层 testbench
├── constraints/
│ ├── timing.xdc # 时序约束
│ └── pins.xdc # 引脚约束(Pynq-Z2 Master XDC)
├── ip_repo/ # 自定义 IP 核(Vivado IP Packager 产出)
│ └── pwm_ctrl_1.0/
│ ├── component.xml
│ └── hdl/
├── scripts/
│ ├── build.tcl # 重建 Vivado 工程(最重要的文件)
│ ├── bd_design.tcl # Block Design 重建脚本
│ └── synth_check.tcl # CI 用合成+时序检查
├── software/
│ ├── drivers/
│ └── app/
├── docs/
│ └── design_spec.md
├── .gitignore # 关键,见下节
├── .gitattributes # Git LFS 配置
└── README.md
3. 完整 .gitignore(可直接使用)
# ============================================================
# Vivado / Vitis / PetaLinux .gitignore
# 适用:Vivado 2023.2,Vitis 2023.2,PetaLinux 2023.2
# ============================================================
# ── Vivado 工程文件(从 build.tcl 重建,不提交)──
*.xpr
*.xsa
# ── Vivado 生成目录(综合/实现结果,体积极大)──
.runs/
.cache/
.hw/
.sim/
.ip_user_files/
.hbs/
.gen/
# ── IP 输出文件(从 IP catalog 生成)──
*.xci.bak
# 注意:.xci 文件本身要提交(IP 配置),但生成的 HDL/网表不提交
ip/*/hdl/
ip/*/simulation/
ip/*/doc/
ip/*/example_designs/
# ── 综合/实现报告(可重新生成)──
*.rpt
*.log
*.jou
vivado_pid*.str
vivado*.log
vivado*.jou
webtalk*.log
webtalk*.jou
# ── 比特流(用 Git LFS 单独管理,见 .gitattributes)──
# *.bit ← 注释掉,改为 LFS 追踪
# *.bin ← 注释掉,改为 LFS 追踪
# *.ltx ← 注释掉,改为 LFS 追踪
# ── Vitis / SDK 工作区 ──
.metadata/
.sdk/
RemoteSystemsTempFiles/
*/Debug/
*/Release/
*_bsp/
*.elf
*.map
*.size
# ── PetaLinux 构建产物 ──
build/
images/
components/plnx-linux*/
pre-built/linux/
*.dtb
*.cpio
*.cpio.gz
*.jffs2
*.ub
# ── SDK / 工具自动生成 ──
xparameters.h
xuartps.h
*.mss
# ── 编辑器 / OS 垃圾文件 ──
.DS_Store
Thumbs.db
*.swp
*.swo
*~
.idea/
.vscode/settings.json
# ── Python 缓存 ──
__pycache__/
*.pyc
*.pyo
# ── 临时文件 ──
*.tmp
*.bak
*.orig
NA/
# .gitattributes — Git LFS 配置
# 需要先安装 Git LFS:git lfs install
# 比特流和二进制固件 → LFS
*.bit filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.ltx filter=lfs diff=lfs merge=lfs -text
*.mcs filter=lfs diff=lfs merge=lfs -text
# 文档 PDF → LFS
*.pdf filter=lfs diff=lfs merge=lfs -text
# 确保 TCL/Verilog/XDC 始终是文本(跨平台换行符)
*.tcl text eol=lf
*.v text eol=lf
*.sv text eol=lf
*.vhd text eol=lf
*.xdc text eol=lf
*.md text eol=lf
4. 从 TCL 重建工程:build.tcl 完整版
这是整套方案里最重要的文件。任何拿到仓库的人只要运行这个脚本,就能从零重建完整的 Vivado 工程。
第一步:从已有工程导出 TCL
# 在 Vivado Tcl Console 里运行(在现有工程打开的情况下):
# 导出 Block Design 重建脚本
write_bd_tcl -force ./scripts/bd_design.tcl
# 导出整个工程重建脚本(包含 IP 配置、综合设置等)
write_project_tcl -force -no_copy_sources ./scripts/build.tcl
第二步:修改生成的 build.tcl,去掉绝对路径
Vivado 生成的 TCL 里通常有硬编码的绝对路径,必须改成相对路径:
# build.tcl — Vivado 工程重建脚本(手动清理版)
# 用法:vivado -mode batch -source scripts/build.tcl
# 在仓库根目录下运行
# ── 设置工作目录为脚本所在目录的上一级(仓库根)──
set script_dir [file dirname [file normalize [info script]]]
set repo_root [file dirname $script_dir]
# 切换到仓库根目录
cd $repo_root
# ── 工程基本信息 ──
set project_name "zynq_myproject"
set project_dir "./vivado_project" # 工程目录(在 .gitignore 里)
set part_name "xc7z020clg400-1" # Pynq-Z2 芯片型号
# 创建工程
create_project ${project_name} ${project_dir} -part ${part_name} -force
# 设置 IP Repository(自定义 IP 核)
set_property ip_repo_paths [list \
[file normalize "${repo_root}/ip_repo"] \
] [current_project]
update_ip_catalog -rebuild
# ── 添加 RTL 源码 ──
add_files -norecurse [glob -nocomplain ${repo_root}/hdl/top/*.v]
add_files -norecurse [glob -nocomplain ${repo_root}/hdl/ip/**/*.v]
# ── 添加仿真文件(只到 sim_1)──
add_files -fileset sim_1 -norecurse \
[glob -nocomplain ${repo_root}/hdl/sim/*.v]
# ── 添加约束文件 ──
add_files -fileset constrs_1 -norecurse \
${repo_root}/constraints/timing.xdc
add_files -fileset constrs_1 -norecurse \
${repo_root}/constraints/pins.xdc
# ── 重建 Block Design ──
source ${script_dir}/bd_design.tcl
# bd_design.tcl 里的 create_bd_design 会生成 .bd 文件
# make_wrapper 和 add_files 封装器
make_wrapper -files [get_files *.bd] -top
add_files -norecurse [glob ${project_dir}/${project_name}.srcs/sources_1/bd/*/hdl/*_wrapper.v]
set_property top ${project_name}_wrapper [current_fileset]
# ── 综合策略设置 ──
set_property strategy "Vivado Synthesis Defaults" [get_runs synth_1]
set_property strategy "Performance_ExplorePostRoutePhysOpt" [get_runs impl_1]
# ── (可选)直接跑综合 ──
# launch_runs synth_1 -jobs 8
# wait_on_run synth_1
puts "✅ 工程重建完成:${project_dir}/${project_name}.xpr"
puts " 在 Vivado GUI 里打开:vivado ${project_dir}/${project_name}.xpr"
运行重建:
# 从仓库根目录运行
vivado -mode batch -source scripts/build.tcl
# 预期输出(最后几行):
# INFO: [IP_Flow 19-4965] Updating IP Catalog
# INFO: [BD 41-1306] Created BD 'bd_design'
# ✅ 工程重建完成:./vivado_project/zynq_myproject.xpr
# 在 Vivado GUI 里打开:vivado ./vivado_project/zynq_myproject.xpr
重建一个中等复杂度工程(~20 个 IP 核,Block Design)通常需要 2-5 分钟。
5. GitHub Actions:Yosys RTL Lint(无需 Vivado License)
Yosys 是开源 RTL 综合工具,虽然不能替代 Vivado 做实现,但做语法检查和基本 lint 完全够用,而且运行在标准 GitHub Actions runner 上,不需要 Vivado License。
# .github/workflows/fpga-lint.yml
# 触发:任何 PR 或推送到 main/develop 分支,且修改了 hdl/ 下的文件
name: FPGA RTL Lint (Yosys)
on:
push:
branches: [main, develop]
paths:
- 'hdl/**'
- 'constraints/**'
pull_request:
branches: [main, develop]
paths:
- 'hdl/**'
jobs:
rtl-lint:
name: Yosys Synthesis Check
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Yosys and tools
run: |
sudo apt-get update -qq
sudo apt-get install -y yosys iverilog verilator
yosys --version
iverilog -V
- name: Run Yosys lint on all RTL files
run: |
set -e
echo "=== 扫描 hdl/ 下的 Verilog 文件 ==="
find hdl/ -name "*.v" -not -path "*/sim/*" -not -name "*_tb.v" | sort
echo "=== 运行 Yosys 语法检查 ==="
find hdl/ -name "*.v" \
-not -path "*/sim/*" \
-not -name "*_tb.v" | while read f; do
echo "-- Checking: $f"
yosys -p "read_verilog -sv $f; check" 2>&1 | \
tee /tmp/yosys_out.txt
if grep -q "ERROR\|error:" /tmp/yosys_out.txt; then
echo "❌ Yosys 报错:$f"
exit 1
fi
done
echo "✅ 所有文件 Yosys lint 通过"
- name: Run iverilog syntax check
run: |
set -e
echo "=== 运行 iverilog 语法检查 ==="
find hdl/ -name "*.v" \
-not -path "*/sim/*" \
-not -name "*_tb.v" | while read f; do
echo "-- iverilog check: $f"
iverilog -tnull -Wall -Wno-implicit \
-I hdl/ip/pwm_ctrl/ \
-I hdl/ip/adc_spi/ \
"$f" || exit 1
done
echo "✅ 所有文件 iverilog 语法检查通过"
- name: Run Verilator lint
run: |
set -e
echo "=== 运行 Verilator lint(最严格)==="
find hdl/ -name "*.v" \
-not -path "*/sim/*" \
-not -name "*_tb.v" | while read f; do
echo "-- verilator --lint-only: $f"
verilator --lint-only \
-Wall \
--error-limit 20 \
"$f" 2>&1 | grep -v "^%Warning-DECLFILENAME" || true
done
echo "✅ Verilator lint 完成"
- name: Check XDC syntax (basic)
run: |
echo "=== 检查 XDC 约束文件格式 ==="
find constraints/ -name "*.xdc" | while read f; do
echo "-- 检查: $f"
# 检查是否有明显的语法错误(非注释行,但不是有效的 Tcl 命令)
grep -v "^#" "$f" | grep -v "^$" | \
grep -v "^set_\|^create_\|^get_\|^place_" && \
echo "⚠️ 可能有无效行: $f" || true
done
echo "✅ XDC 格式检查完成"
Lint 流程耗时:
| 工具 | 用途 | 典型耗时(20个 .v 文件) |
|---|---|---|
| Yosys | 解析 + 基本语义检查 | ~30 秒 |
| iverilog | 严格语法检查 | ~10 秒 |
| Verilator | 最严格的 lint(未定义信号、位宽不匹配等) | ~60 秒 |
| 合计 | ~2 分钟 |
6. 有 Vivado License 时:自建 Runner 跑完整检查
如果你的团队有 Vivado 授权服务器,可以在内网自建 GitHub Actions self-hosted runner,跑真正的综合+时序检查:
# .github/workflows/vivado-timing.yml
# 需要:自建 runner,标签 vivado-runner,已安装 Vivado 2023.2
name: Vivado Synthesis & Timing Check
on:
push:
branches: [main]
workflow_dispatch: # 手动触发
jobs:
vivado-synth-check:
name: Vivado Synthesis + Timing Report
runs-on: [self-hosted, vivado-runner]
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Rebuild project from TCL
run: |
source /opt/Xilinx/Vivado/2023.2/settings64.sh
vivado -mode batch -source scripts/build.tcl \
-log /tmp/build_project.log \
-journal /tmp/build_project.jou
- name: Run synthesis check
run: |
source /opt/Xilinx/Vivado/2023.2/settings64.sh
vivado -mode batch \
-source scripts/synth_check.tcl \
-log /tmp/synth_check.log \
-journal /tmp/synth_check.jou
- name: Parse timing results
run: |
python3 scripts/parse_timing.py \
--report /tmp/timing_summary.rpt \
--max-wns -0.5 \
--max-whs -0.1 \
--max-lut-util 80
- name: Upload reports
uses: actions/upload-artifact@v4
if: always()
with:
name: vivado-reports-${{ github.sha }}
path: |
/tmp/*.rpt
/tmp/*.log
retention-days: 14
对应的综合检查脚本:
# scripts/synth_check.tcl — Vivado 综合 + 时序检查
# 输出:/tmp/timing_summary.rpt,供 parse_timing.py 解析
set script_dir [file dirname [file normalize [info script]]]
set repo_root [file dirname $script_dir]
# 打开已重建的工程
open_project ${repo_root}/vivado_project/zynq_myproject.xpr
# 运行综合(如果还没跑)
if {[get_property STATUS [get_runs synth_1]] ne "synth_design Complete!"} {
puts "INFO: 运行综合..."
launch_runs synth_1 -jobs 8
wait_on_run synth_1
}
# 检查综合是否成功
if {[get_property PROGRESS [get_runs synth_1]] ne "100%"} {
puts "ERROR: 综合失败!"
exit 1
}
# 运行实现(布局布线)
launch_runs impl_1 -jobs 8
wait_on_run impl_1
# 生成时序报告
open_run impl_1
report_timing_summary \
-delay_type min_max \
-report_unconstrained \
-check_timing_verbose \
-max_paths 10 \
-input_pins \
-file /tmp/timing_summary.rpt
# 生成资源利用率报告
report_utilization -file /tmp/utilization.rpt
# 读取 WNS(Worst Negative Slack)
set wns [get_property SLACK [get_timing_paths -max_paths 1 -nworst 1 -setup]]
set whs [get_property SLACK [get_timing_paths -max_paths 1 -nworst 1 -hold]]
set lut_util [expr {[get_property USED [get_cells -hier -filter PRIMITIVE_TYPE=~LUT*]] * 100 / 53200}]
puts "INFO: WNS = ${wns} ns"
puts "INFO: WHS = ${whs} ns"
puts "INFO: LUT 利用率 = ${lut_util}%"
# 检查时序违例
if {$wns < -0.5} {
puts "ERROR: WNS = ${wns} ns,超过阈值 -0.5 ns!"
exit 2
}
if {$whs < -0.1} {
puts "ERROR: WHS = ${whs} ns,超过阈值 -0.1 ns!"
exit 3
}
if {$lut_util > 80} {
puts "WARNING: LUT 利用率 = ${lut_util}%,超过 80%,请检查"
}
puts "✅ 时序检查通过:WNS=${wns}ns WHS=${whs}ns LUT=${lut_util}%"
7. 分支策略与 PR 流程
main ──●──────────────────────●── (稳定比特流,每个 commit 对应一个已验证的 .bit)
\ /
develop ───●──●──●──●──●──●──● (集成分支,通过 lint CI 才能合入)
\ /
feature/adc-spi ─────●──●──● (功能分支,包含 RTL + 驱动 + 测试)
分支说明:
| 分支 | 说明 | 合入条件 |
|---|---|---|
main | 已验证的稳定版本,有对应比特流 | 人工审核 + 完整 timing check 通过 |
develop | 集成分支,日常开发 merge 到这里 | Yosys lint CI 通过 + Code Review |
feature/* | 功能开发分支,一个 feature 一个分支 | 完成后 PR 到 develop |
release/* | 发版分支,最后合入 main | timing check + 板级验证 |
hotfix/* | 紧急修复,从 main 分出 | 直接 PR 到 main,同步 cherry-pick 到 develop |
硬件修改的 PR checklist:
## PR Description
修改内容:[描述]
## Hardware Changes Checklist
- [ ] Vivado behavioral simulation 通过(附截图或 log)
- [ ] `write_bd_tcl` 已重新导出 `scripts/bd_design.tcl`
- [ ] 约束文件已更新(如果改了引脚或时序约束)
- [ ] `build.tcl` 中没有硬编码绝对路径
- [ ] Yosys lint CI 通过(自动检查)
## Timing Report(如果跑了 Vivado 综合)
- WNS: __ ns
- WHS: __ ns
- LUT 利用率: __%
- BRAM 利用率: __%
## Test Results
- [ ] QEMU 功能验证通过(如果适用)
- [ ] 板级验证通过(如果有硬件)
8. 三处避坑
🚧 避坑 1:TCL 脚本里的绝对路径
write_project_tcl生成的脚本里通常包含绝对路径,例如:add_files /home/kevin/projects/zynq_proj/hdl/pwm_ctrl.v这个路径在别人的机器上或 CI 环境里完全失效。
解决方法:在脚本开头用
[info script]获取脚本位置,计算出$repo_root,然后所有路径改为相对路径:set repo_root [file dirname [file dirname [file normalize [info script]]]] add_files [file normalize "${repo_root}/hdl/pwm_ctrl.v"]提交前在另一台机器上 clean clone 验证能跑通,这是最可靠的检验方法。
🚧 避坑 2:IP 核版本升级会让 bd_design.tcl 失效
当你升级 Vivado 版本(比如从 2022.2 到 2023.2),
bd_design.tcl里的 IP 版本号会过期。create_bd_cell -type ip -vlnv xilinx.com:ip:axi_dma:7.1这类命令里的版本号(7.1)在新 Vivado 里可能变成了7.1.2,导致重建失败。两个解决方案:
- 在
create_bd_cell后面加upgrade_ip [get_ips *],让 Vivado 自动升级 IP 版本- 升级 Vivado 后,在新版里打开工程、升级所有 IP,然后重新
write_bd_tcl版本升级时一定要重新验证
build.tcl从零跑通,不能假设旧脚本在新工具链上还能用。
🚧 避坑 3:多人同时修改 Block Design 必然产生 merge conflict
Block Design 导出的
bd_design.tcl虽然是文本,但两个人同时改同一个 Block Design,合并时会产生大量冲突,而且 TCL 冲突几乎无法手动合并(不像代码冲突那样有语义)。唯一可靠的解法是人员分工:每次只有一个人修改 Block Design。硬件架构要在团队内先对齐,确定后再实现,不要让 Block Design 成为多人同时修改的热点文件。
如果必须并行修改,可以把 Block Design 拆成多个层次化 BD(Hierarchical Block Design),每人负责一个子 BD——但这对工程组织要求更高,适合中大型团队。
9. 本篇 checklist
- 仓库里没有
.runs/、.cache/、.xpr等生成物(du -sh .git应该 < 10MB) -
.gitignore配置完整,git status在 Vivado 跑完后不会出现大量未跟踪文件 -
build.tcl里没有绝对路径,在 clean clone 后能从零重建工程 -
write_bd_tcl已导出bd_design.tcl,Block Design 能从脚本重建 - GitHub Actions Yosys lint 已配置,每次 PR 自动运行
- 分支策略已确定:
main/develop/feature/* -
.gitattributes配置了.bit用 Git LFS - PR 模板包含 timing report 字段
10. 下一篇预告
下一篇 《Zynq 实战 26|项目实战一:智能数据采集系统(ADC + FIR + DMA + Web 监控)》 是一个完整的综合实战项目:
- 8 路模拟输入,AD7606B,100 ksps/路
- 自定义 SPI 控制器 IP + AXI-Stream + FIR 滤波 + DMA
- Linux 驱动 + TCP 推送 + Web 实时波形显示
- 从 Block Design 到跑通的完整代码
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG892 | Vivado Design Suite User Guide: Design Flows Overview | write_project_tcl 和 write_bd_tcl 命令说明 |
| UG835 | Vivado Design Suite Tcl Command Reference | 所有 Tcl 命令的完整参数说明 |
| Yosys Manual | Yosys Open SYnthesis Suite | Yosys -p "check" 用法 |
| GitHub Docs | Self-hosted runners | 自建 runner 配置指南 |
| Git LFS | git-lfs.com | Git LFS 安装和 .gitattributes 配置 |
| AMD Forum | Vivado Project TCL Export Best Practices | AMD 官方 TCL 导出最佳实践 |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 25 篇。 Vivado TCL 重建脚本遇到问题,或者 CI 配置有疑问,欢迎留言。