← 返回博客
FPGAZynqGitCI/CDGitHub ActionsVivadoYosys版本控制DevOps

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 工程文件分类:什么该提交,什么不该

Vivado 工程文件分类:✅ 提交 vs ❌ 忽略 ✅ 提交这些(小 + 可重建) 源码 / 约束 hdl/*.v *.vhd *.sv ← RTL 源码 constraints/*.xdc ← 时序 + 引脚约束 ip_repo/**/*.v *.vhd ← 自定义 IP 核源码 脚本(重建工程的关键) scripts/build.tcl ← 重建 Vivado 工程 scripts/bd_design.tcl ← Block Design(write_bd_tcl 导出) scripts/synth_check.tcl ← CI 用合成检查脚本 软件 / 文档 software/src/**/*.c *.h ← PS 端应用 / 驱动 docs/*.md *.pdf ← 设计文档 .github/workflows/*.yml ← CI 配置 .gitignore .gitattributes ← Git 配置 总大小:通常 < 5 MB(纯文本) 克隆时间:< 10 秒 ❌ 忽略这些(大 + 可再生) Vivado 生成物 *.xpr ← 工程文件(从 TCL 重建) .runs/ ← 综合/实现结果,几 GB .cache/ ← IP 编译缓存 .hw/ ← 硬件描述文件 .sim/ ← 仿真输出 .ip_user_files/ ← IP 用户文件缓存 *.bit *.bin ← 比特流(用 LFS 单独管) Vitis / PetaLinux 生成物 workspace/.metadata/ ← Eclipse 工作区 */Debug/ */Release/ ← 编译输出 *.xsa ← 平台文件(由 .xpr 生成) images/linux/ ← PetaLinux 构建产物 总大小:通常 500 MB ~ 10 GB 含这些会让仓库彻底无法使用
图 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/*发版分支,最后合入 maintiming 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,导致重建失败。

两个解决方案:

  1. create_bd_cell 后面加 upgrade_ip [get_ips *],让 Vivado 自动升级 IP 版本
  2. 升级 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 到跑通的完整代码

参考资料

文档号名称用途
UG892Vivado Design Suite User Guide: Design Flows Overviewwrite_project_tclwrite_bd_tcl 命令说明
UG835Vivado Design Suite Tcl Command Reference所有 Tcl 命令的完整参数说明
Yosys ManualYosys Open SYnthesis SuiteYosys -p "check" 用法
GitHub DocsSelf-hosted runners自建 runner 配置指南
Git LFSgit-lfs.comGit LFS 安装和 .gitattributes 配置
AMD ForumVivado Project TCL Export Best PracticesAMD 官方 TCL 导出最佳实践

这是《Zynq FPGA 嵌入式系统设计实战》系列第 25 篇。 Vivado TCL 重建脚本遇到问题,或者 CI 配置有疑问,欢迎留言。