Vivado Tcl 脚本自动化

Tcl 脚本 vs GUI 工作流

Vivado 的原生脚本语言是 Tcl(Tool Command Language)。每个 GUI 操作都有等效的 Tcl 命令,会显示在 Tcl Console 中。这意味着你在 Vivado GUI 中执行的任何工作流都可以被捕获、脚本化并自动化,以实现可重复的构建。

为什么用脚本而非 GUI

因素 GUI Tcl 脚本
可重复性 手动步骤,容易遗漏 每次执行完全相同的命令
速度(重复构建) 每次通过向导点击 一条命令:vivado -source build.tcl
版本控制 项目 .xpr 文件是二进制格式 .tcl 文件是纯文本,可 diff
CI/CD 集成 不可能 简单:批处理模式运行
团队协作 "在我机器上没问题" 通过脚本获得完全一致的环境
参数扫描 手动修改后重新运行 在 Tcl 中循环遍历参数值

从 GUI 操作中捕获 Tcl

每个 Vivado 项目都有一个 .jou(日志)文件,记录了执行的每条 Tcl 命令。文件位置:


<project_dir>/<project_name>.jou

从 GUI 会话提取干净的脚本:


# 去除时间戳和交互式专用命令
grep -v '^#' vivado.jou | grep -v '^$' | \
    grep -v 'create_gui_project' | \
    grep -v 'update_compile_order' > build_clean.tcl

Vivado 批处理模式启动

基本调用


# 批处理模式运行 Tcl 脚本(无 GUI)
vivado -mode batch -source build.tcl

# 带日志记录运行
vivado -mode batch -source build.tcl -log build/vivado.log -journal build/vivado.jou

# 终端模式运行(Tcl 交互 shell,无 GUI)
vivado -mode tcl

退出码处理

Vivado 批处理模式在遇到严重错误时返回非零值,但许多"警告"不会影响退出码。要捕获所有问题:


# 在 build.tcl 末尾添加:
if {[catch {write_bitstream -force build/fpga_top.bit} errmsg]} {
    puts "错误: 比特流生成失败: $errmsg"
    exit 1
}
exit 0

检查日志中关键短语的包装脚本:


#!/bin/bash
# build_fpga.sh
vivado -mode batch -source build.tcl -log build/vivado.log
VIVADO_EXIT=$?

# 检查不会触发非零退出的错误字符串
if grep -qi "ERROR:\[DRC\|ERROR:\[Vivado 12-\|implementation ERROR" build/vivado.log; then
echo "致命错误: Vivado 日志中发现严重错误"
exit 1
fi

if [ $VIVADO_EXIT -ne 0 ]; then
echo "致命错误: Vivado 退出码为 $VIVADO_EXIT"
exit $VIVADO_EXIT
fi

echo "构建成功"

关键 Tcl 命令参考

项目创建与文件管理


# 创建新项目(仅 RTL,无开发板)
create_project fpga_top /home/user/work/FPGA_Prj/Project/WebServer/build/vivado \
    -part xc7k325tffg900-2 -force

# 或创建内存项目(更快,无磁盘 I/O)
create_project -in_memory -part xc7k325tffg900-2

# 添加源文件
add_files -norecurse rtl/fpga_top.v
add_files -norecurse rtl/wb_interconnect.v
add_files -norecurse rtl/uart_core.v

# 从文件列表添加文件
set fp [open "verible.filelist" r]
while {[gets $fp line] >= 0} {
if {[string match "#*" $line]} {continue}
if {[string length $line] == 0} {continue}
if {[string match "sim/*" $line]} {continue}
add_files -norecurse $line
}
close $fp

# 添加约束
add_files -fileset constrs_1 -norecurse xdc/pinout.xdc
add_files -fileset constrs_1 -norecurse xdc/timing.xdc

# 设置顶层模块
set_property top fpga_top [current_fileset]

综合


# 基本综合
launch_runs synth_1 -jobs 16
wait_on_run synth_1

# 检查严重警告
set crit_warn [get_property STATS.CRITICAL_WARNINGS [get_runs synth_1]]
if {$crit_warn > 0} {
puts "警告: 综合过程中产生 $crit_warn 个严重警告"
# 可选:报告它们
open_run synth_1
report_critical_warnings -file build/synth_critical_warnings.txt
}

# 使用策略综合
set_property strategy Flow_AreaOptimized_high [get_runs synth_1] ;# 面积优化
# set_property strategy Flow_PerfOptimized_high [get_runs synth_1] ;# 速度优化
# set_property strategy Flow_AlternateRoutability [get_runs synth_1] ;# 拥塞优化

# 综合选项
set_property -name {STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS} -value \
{-fsm_extraction one_hot -resource_sharing off -no_lc} \
-objects [get_runs synth_1]

# 综合后报告
open_run synth_1
report_utilization -file build/synth_utilization.rpt
report_timing_summary -file build/synth_timing.rpt

实现


# 启动实现
launch_runs impl_1 -jobs 16
wait_on_run impl_1

# 实现策略
set_property strategy Performance_ExplorePostRoutePhysOpt [get_runs impl_1]

# 实现后报告
open_run impl_1
report_utilization -file build/impl_utilization.rpt
report_timing_summary -file build/impl_timing.rpt
report_clock_utilization -file build/impl_clocks.rpt
report_power -file build/impl_power.rpt

比特流生成


# 写比特流
launch_runs impl_1 -to_step write_bitstream -jobs 16
wait_on_run impl_1

# 或直接使用 write_bitstream 命令
write_bitstream -force build/fpga_top.bit

# 额外输出格式用于调试
write_debug_probes -force build/fpga_top.ltx ;# ILA 调试探针

# Flash 烧录用的二进制格式
write_cfgmem -format bin -interface spix4 -size 128 \
-loadbit "up 0x0 build/fpga_top.bit" \
-file build/fpga_top.bin

# 生成用于 SPI Flash 的 MCS
write_cfgmem -format mcs -interface spix4 -size 128 \
-loadbit "up 0x0 build/fpga_top.bit" \
-file build/fpga_top.mcs

完整一键构建脚本

该脚本通过单条命令实现从源文件到比特流的完整构建。


# /home/user/work/FPGA_Prj/Project/WebServer/build.tcl
# 一键综合 + 实现 + 比特流
# 用法: vivado -mode batch -source build.tcl

set project_name "fpga_top"
set part_name "xc7k325tffg900-2"
set top_module "fpga_top"
set output_dir "build"
set jobs 16

# ── 创建输出目录 ──
file mkdir ${output_dir}

# ── 创建项目 ──
puts "=== 创建项目: ${project_name} ==="
create_project -in_memory -part ${part_name}

# ── 从 verible.filelist 读取源文件 ──
puts "=== 添加源文件 ==="
if {[file exists "verible.filelist"]} {
set fp [open "verible.filelist" r]
while {[gets $fp line] >= 0} {
set line [string trim $line]
if {$line eq ""} {continue}
if {[string match "#*" $line]} {continue}
if {[string match "sim/*" $line]} {continue}
puts " 添加: $line"
add_files -norecurse $line
}
close $fp
} else {
puts "错误: 未找到 verible.filelist"
exit 1
}

# ── 添加约束 ──
puts "=== 添加约束 ==="
if {[file exists "xdc/pinout.xdc"]} {
add_files -fileset constrs_1 -norecurse xdc/pinout.xdc
}
if {[file exists "xdc/timing.xdc"]} {
add_files -fileset constrs_1 -norecurse xdc/timing.xdc
}

# ── 设置顶层模块 ──
set_property top ${top_module} [current_fileset]
puts " 顶层模块: ${top_module}"

# ── 综合 ──
puts "\n=== 运行综合 (${jobs} 个并行任务) ==="
set_property strategy Flow_AreaOptimized_high [get_runs synth_1]
launch_runs synth_1 -jobs ${jobs}
wait_on_run synth_1

set synth_status [get_property STATUS [get_runs synth_1]]
if {$synth_status ne "synth_design Complete!"} {
puts "错误: 综合失败,状态: $synth_status"
exit 1
}
puts " 综合完成"

# ── 综合后检查 ──
open_run synth_1
set synth_crit [get_property STATS.CRITICAL_WARNINGS [get_runs synth_1]]
puts " 严重警告: $synth_crit"

report_utilization -file ${output_dir}/synth_utilization.rpt
report_timing_summary -file ${output_dir}/synth_timing.rpt

# ── 实现 ──
puts "\n=== 运行实现 (${jobs} 个并行任务) ==="
launch_runs impl_1 -jobs ${jobs}
wait_on_run impl_1

set impl_status [get_property STATUS [get_runs impl_1]]
if {$impl_status ne "route_design Complete!"} {
puts "错误: 实现失败,状态: $impl_status"
exit 1
}
puts " 实现完成"

# ── 实现后检查 ──
open_run impl_1
report_timing_summary -file ${output_dir}/impl_timing.rpt
report_utilization -file ${output_dir}/impl_utilization.rpt
report_power -file ${output_dir}/impl_power.rpt

# 检查时序收敛
set wns [get_property SLACK [get_timing_paths -max_paths 1 -setup]]
set ths [get_property SLACK [get_timing_paths -max_paths 1 -hold]]
puts " 建立时间 WNS: ${wns}"
puts " 保持时间 WNS: ${ths}"

if {$wns < -0.100} {
puts "错误: 建立时间违规 (WNS = ${wns} ns)"
exit 1
}

# ── 比特流生成 ──
puts "\n=== 生成比特流 ==="
launch_runs impl_1 -to_step write_bitstream -jobs ${jobs}
wait_on_run impl_1

set bit_status [get_property STATUS [get_runs impl_1]]
if {$bit_status ne "write_bitstream Complete!"} {
puts "错误: 比特流生成失败,状态: $bit_status"
exit 1
}

# 将比特流复制到输出目录
set bit_src [get_property DIRECTORY [get_runs impl_1]]
set bit_src "${bit_src}/${top_module}.bit"
file copy -force ${bit_src} ${output_dir}/${top_module}.bit
puts " 比特流: ${output_dir}/${top_module}.bit"

# ── 生成烧录文件 ──
puts "\n=== 生成 MCS Flash 镜像 ==="
set bin_file "${output_dir}/${top_module}.bin"
set mcs_file "${output_dir}/${top_module}.mcs"

write_cfgmem -format bin -interface spix4 -size 128 \
-loadbit "up 0x0 ${bit_src}" \
-file ${bin_file} -force

write_cfgmem -format mcs -interface spix4 -size 128 \
-loadbit "up 0x0 ${bit_src}" \
-file ${mcs_file} -force

puts " 二进制镜像: ${bin_file}"
puts " MCS 镜像: ${mcs_file}"

# ── 汇总 ──
puts "\n=== 构建完成 ==="
puts " 器件: ${part_name}"
puts " 资源利用率: [exec grep 'Slice LUTs' ${output_dir}/impl_utilization.rpt | head -1]"
puts " 建立 WNS: ${wns} ns"
puts " 保持 WNS: ${ths} ns"
puts " 比特流: ${output_dir}/${top_module}.bit"

close_project
exit 0

Makefile + Shell 混合编排

对于混合使用 Tcl、Verilator 和 Shell 工具的项目,将 Vivado 包装在 Makefile 中以实现一致的调用。


# /home/user/work/FPGA_Prj/Project/WebServer/Makefile
# 组合 Makefile: 综合、仿真、Lint、标签

PART := xc7k325tffg900-2
TOP := fpga_top
JOBS := 16
BITSTREAM := build/$(TOP).bit

# ── 默认目标 ──
.PHONY: all
all: lint synth

# ── Lint ──
.PHONY: lint
lint:
@echo "=== Verilator Lint ==="
verilator --lint-only -Wall -I rtl/ -I ../../ip_common/rtl/ \
-f verible.filelist --top-module $(TOP)

# ── 格式检查 ──
.PHONY: format-check
format-check:
verible-verilog-format --verify -f verible.filelist

# ── 就地格式化 ──
.PHONY: format
format:
verible-verilog-format --inplace -f verible.filelist

# ── 综合 (Vivado Tcl) ──
.PHONY: synth
synth:
@echo "=== Vivado 综合 ==="
source /home/user/tools/scripts/vivado_env.sh && \
vivado -mode batch -source build.tcl -log build/vivado.log

# ── 仅生成比特流(假设综合已完成) ──
.PHONY: bit
bit: $(BITSTREAM)

$(BITSTREAM): build.tcl
@echo "=== Vivado 比特流 ==="
source /home/user/tools/scripts/vivado_env.sh && \
vivado -mode batch -source build.tcl -log build/vivado.log

# ── 仿真 (Icarus) ──
.PHONY: sim
sim:
@echo "=== Icarus 仿真 ==="
@mkdir -p build
SOURCES=$$(grep -v '^#' verible.filelist | grep -v '^$$' | grep -v 'sim/'); \
iverilog -g2012 -I rtl/ -I ../../ip_common/rtl/ -o build/sim.vvp \
$$SOURCES sim/tb_$(TOP).v
vvp build/sim.vvp

# ── 仿真含波形 ──
.PHONY: wave
wave: sim
gtkwave build/sim.fst &

# ── 打快照标签 ──
.PHONY: tag
tag:
@TAG=snapshot-$$(date +%Y%m%d%H%M%S); \
echo "创建标签: $$TAG"; \
git tag -a "$$TAG" -m "Build snapshot: $$(date -Iseconds)"

# ── 清理 ──
.PHONY: clean
clean:
rm -rf build/
rm -rf *.jou *.log .Xil/

# ── 完整自检 (lint + synth + sim) ──
.PHONY: check
check:
@bash run_all.sh

# ── 帮助 ──
.PHONY: help
help:
@echo "目标:"
@echo " all — lint + 综合"
@echo " lint — Verilator lint 检查"
@echo " format — Verible 就地格式化"
@echo " format-check — 验证格式 (CI)"
@echo " synth — Vivado 综合 + 实现 + 比特流"
@echo " sim — Icarus 仿真"
@echo " wave — Icarus 仿真 + GTKWave"
@echo " tag — Git 快照标签"
@echo " clean — 删除构建产物"
@echo " check — run_all.sh 完整自检"

跨版本 Tcl 兼容性

Vivado Tcl 命令在不同版本间会发生变化。编写版本自适应脚本。

检测 Vivado 版本


set vivado_version [version -short]
puts "Vivado 版本: $vivado_version"

# 解析主版本号.次版本号
regexp {(\d+)\.(\d+)} $vivado_version -> major minor

if {$major >= 2024} {
# Vivado 2024.x 特有命令
set_param general.maxThreads 32
} elseif {$major >= 2022} {
# Vivado 2022.x / 2023.x
set_param general.maxThreads 16
} else {
puts "错误: Vivado 版本 $vivado_version 过旧 (需要 2022+)"
exit 1
}

常见兼容性问题

问题 2023 之前 2023+
get_cells -hier 行为 返回层次化 Cell 过滤语法变更;使用 -hierarchical
report_timing 默认值 1 条路径 2024.1 中改为 10 条路径
write_bitstream 格式 .bit 可输出压缩 .bit
create_project -in_memory 不可用 可用 (2022.2+)
wait_on_run 超时 无超时参数 2023.2 新增 -timeout

版本抽象辅助过程


# vivado_helpers.tcl — 版本无关的包装过程

proc safe_add_files {filelist} {
# 从 filelist 读取文件,跳过注释和空行
set fp [open $filelist r]
set files []
while {[gets $fp line] >= 0} {
set line [string trim $line]
if {$line eq "" || [string match "#*" $line]} {continue}
lappend files $line
}
close $fp

# 添加文件(跨 Vivado 版本工作)
foreach f $files {
if {[file exists $f]} {
add_files -norecurse $f
} else {
puts "警告: 文件未找到: $f"
}
}
}

proc safe_wait {run_name {timeout 0}} {
# 带可选超时的等待(向后兼容)
set v [version -short]
if {[string compare $v "2023.2"] >= 0} {
if {$timeout > 0} {
wait_on_run $run_name -timeout $timeout
} else {
wait_on_run $run_name
}
} else {
wait_on_run $run_name
}
}

proc report_timing_safe {args} {
# 安全的时序报告,带一致的路径数量
set num_paths 100
foreach {flag val} $args {
if {$flag eq "-max_paths"} {set num_paths $val}
}
report_timing_summary -max_paths $num_paths {*}$args
}

FPGA 烧录自动化


# program.tcl — 自动 FPGA 烧录
# 用法: vivado -mode batch -source program.tcl

set bitstream_file "build/fpga_top.bit"
set probe_file "build/fpga_top.ltx"

# 打开硬件管理器
open_hw_manager
connect_hw_server -url TCP:localhost:3121

# 查找第一个可用设备
set hw_targets [get_hw_targets]
if {[llength $hw_targets] == 0} {
puts "错误: 未找到硬件目标"
puts " JTAG 线缆是否已连接?"
puts " 检查: lsusb | grep -i xilinx"
exit 1
}

set hw_target [lindex $hw_targets 0]
puts "使用目标: $hw_target"
open_hw_target $hw_target

set hw_devices [get_hw_devices]
if {[llength $hw_devices] == 0} {
puts "错误: JTAG 链中未找到设备"
exit 1
}
set hw_device [lindex $hw_devices 0]

# 烧录设备
puts "正在烧录设备: $bitstream_file"
set_property PROGRAM.FILE $bitstream_file $hw_device
program_hw_devices $hw_device

# 可选:设置 ILA 调试探针
if {[file exists $probe_file]} {
puts "正在从 $probe_file 设置调试探针"
set_property PROBES.FILE $probe_file $hw_device
}

puts "烧录完成"
close_hw_manager

日志文件解析工具


#!/bin/bash
# parse_vivado_log.sh — 从 Vivado 日志中提取关键指标
# 用法: ./parse_vivado_log.sh build/vivado.log

LOG="$1"

echo "=== Vivado 构建汇总 ==="
echo ""

# 时序
echo "时序:"
grep -A1 "Worst Negative Slack" "$LOG" | tail -1
grep -A1 "Worst Hold Slack" "$LOG" | tail -1
echo ""

# 资源利用率
echo "资源利用率:"
grep -E "Slice LUTs|Slice Registers|Block RAM|DSP" "$LOG" | head -20
echo ""

# 时钟频率
echo "时钟:"
grep -E "clock.*period|Clk.*frequency" "$LOG" | head -10
echo ""

# 严重警告
echo "严重警告:"
grep -c "CRITICAL WARNING" "$LOG"
echo ""

# 构建耗时
echo "构建耗时:"
grep -E "INFO.*Total.*time|INFO.*Time spent" "$LOG" | head -5

总结

任务 命令
从文件列表创建项目 vivado -mode batch -source build.tcl
构建前检查格式 verible-verilog-format --verify -f verible.filelist
完整构建流程 make synthmake bit
运行自检 make check (调用 run_all.sh)
烧录 FPGA vivado -mode batch -source program.tcl
清理构建产物 make clean
捕获 GUI 工作流到脚本 <project>.jou 日志文件复制