1. 先搞清楚这套TCL脚本到底在干什么
1.1 为什么ADI不直接给一个现成的.xpr工程文件
我第一次接触AD9361的HDL参考设计时,下意识去找zc706_fmcomms2.xpr或者vcu118_fmcomms2.xpr这种现成工程文件,结果翻遍整个仓库都没找到。后来才明白,ADI维护的不是“某个板卡对应的单个工程”,而是一个覆盖多款FPGA开发板、多款射频模块的大型HDL仓库。如果每发布一个版本都要手工维护几十个.xpr文件,不仅工作量爆炸,而且很容易因为IP版本、器件型号的细微差别导致工程打不开。
所以ADI选择了TCL脚本这套方案。TCL脚本本质上是一串Vivado能直接执行的命令,脚本运行时会根据你指定的板卡、器件、参考设计名,在内存中动态创建工程、添加IP、生成Block Design、写入约束文件。用一句话概括:TCL脚本生成工程的过程,像是用代码“画”了一个工程出来,而不是给你一张画好的图。
对于开发者来说,这有很实际的好处。你可以通过改参数瞬间换一块FPGA器件,或者把fmcomms2换成fmcomms4,不需要手动删除旧工程重新建。版本管理也友好,Git里只存文本类型的TCL脚本和源文件,不会出现二进制项目文件冲突。
1.2 脚本的工作流程拆解
我建议在跑脚本之前,先打开仓库里的projects/fmcomms2/zc706目录,看清这里的文件结构。通常你会看到:
Makefile:封装了TCL脚本的调用方式,加上参数后一条make命令就能生成工程。system_project.tcl:核心脚本,负责创建顶层工程、添加源文件、调用ADI公共IP库。system_top.v:顶层HDL文件,包含处理系统、射频接口、DMA等模块的例化。system_bd.tcl:用来生成Block Design的脚本,会在Vivado里创建system_bd。system_constr.xdc:引脚约束和时序约束。
这些TCL脚本内部会先加载library/scripts/adi_ip.tcl这类公共函数,然后注册ADI的自定义IP核,比如axi_ad9361、axi_dmac、util_pack等。之后脚本会调用create_project创建工程,调用add_files添加HDL源文件,再执行system_bd.tcl把Block Design搭建出来,最后生成约束并更新编译顺序。
理解了这条链路,再去执行脚本心里就有底了。如果中间某一步报错,你能很快判断是IP库路径问题、约束文件问题还是Block Design脚本问题,而不是对着整个Vivado日志发呆。
2. 运行脚本前必须做好的环境准备
2.1 Vivado版本与分支的匹配问题
这一步看着不起眼,但影响最大。ADI的HDL仓库会按Vivado主版本拆分分支,常见的有hdl_2021_r2、hdl_2022_r1、hdl_2023_r1等。如果你在Vivado 2022.2里硬跑hdl_2021_r2分支的脚本,大概率会遇到IP版本不支持或者某个TCL函数被移除的报错。反过来,用Vivado 2021.2跑2023分支也可能出问题。
我的做法是:先确定自己装的Vivado版本,再去仓库里git checkout对应的分支。比如本机是Vivado 2022.2,那就切换到hdl_2022_r2分支。如果遇到几个版本之间API差异的问题,通常README里会写明推荐版本。ADI官方也会在分支名上直接标注,基本不需要猜。
除了Vivado版本,还要确认安装了对应的许可证。生成工程本身不需要特别License,但后续综合和实现Vivado的IP核时,可能需要有效的Vivado License。我遇到过一次“IP Evaluation License”弹出的情况,那是没有激活正式License导致的,需要检查本机许可状态。
2.2 获取ADI HDL参考设计的正确方式
获取源码这步,我建议直接使用Git克隆。仓库地址是https://github.com/analogdevicesinc/hdl。如果你只是临时用,也可以下载zip压缩包,但后续想切换版本、看提交记录就很麻烦。克隆完成后,进入仓库根目录:
git checkout hdl_2022_r2然后确认一下子模块是否完整。ADI仓库有时候会引用一些外部IP或库,不过大多数情况下你需要的HDL源码都直接包含在仓库里。如果后续脚本报“找不到某个文件”,优先怀疑当前分支没有拉完整。
还有一个容易忽略的点:仓库路径不能有中文、空格,Windows下尤其注意。Vivado自身对路径空格的支持时好时坏,而TCL脚本里经常用相对路径组合字符串,一旦遇到空格就会出现文件找不到或者路径解析错误。我一般把仓库放在D:\projects\hdl这种纯英文路径下,省去一堆麻烦。
2.3 检查Vivado环境变量,尤其Linux下要source settings
在Windows下,最直接的方式是打开“Vivado 2022.2 Tcl Shell”或“Vivado Tcl Console”,它会自动把工具路径配置好。但在Linux服务器环境下,如果你直接打开一个普通终端执行vivado -mode batch -source xxx.tcl,很可能会提示vivado: command not found。这时候需要先source Vivado的环境变量脚本:
source /tools/Xilinx/Vivado/2022.2/settings64.sh然后再执行which vivado确认路径。如果你是Windows下用命令行批处理,也要注意安装目录里的settings64.bat,或者干脆使用Vivado自带的Tcl Shell,避免自己配置PATH。
此外,建议提前安装git和make。Windows下如果要用make命令,需要装MSYS2或者使用Windows Subsystem for Linux。如果你不想折腾,直接从Tcl Console执行脚本也能生成工程,make不是必须的。但我自己更推荐用make,因为它能自动设置ADI_HDL_DIR等环境变量,减少手动拼参数的错误概率。
3. 手把手运行TCL脚本生成HDL工程
3.1 从“一条make命令”到“手动执行TCL脚本”
进入对应工程目录是第一步。以ADI官方参考设计fmcomms2搭配Zynq开发板为例:
cd projects/fmcomms2/zc706 make这个命令会读取Makefile,设置几个关键变量后调用Vivado批处理模式执行TCL脚本。整个过程中,终端会打印大量日志,最后在当前目录下出现zc706_fmcomms2文件夹,里面就是生成好的Vivado工程。
但如果你像我一样想尽量理解每一步在做什么,可以抛开Makefile,在Vivado Tcl Console里手动执行。先启动Tcl Console并进入仓库目录:
cd D:/projects/hdl/projects/fmcomms2/zc706 source ./system_project.tcl脚本里会用set adc_dac_cores 1这类变量控制射频链路数量,还会引用公共库中的函数来添加IP核。手动执行的好处是,出错了你能在Console里看到是哪一行TCL报错,能用set直接查看当前变量值,方便调试。
3.2 关键参数的含义与典型配置
在运行TCL脚本之前,建议先扫一眼脚本顶部的变量。不同的reference design参数差异很大,但有几个是通用的:
| 变量名 | 含义 | 常见取值 |
|---|---|---|
adc_dac_cores | 射频收发链路数量,决定例化几个AD9361接口 | 1、2 |
rx_dma/tx_dma | 是否使能RX/TX的DMA通道 | 1、0 |
fpga_part | 目标FPGA型号 | xc7z045ffg900-2 |
board_name | 板卡名称,用于约束和顶层选择 | zc706 |
adi_mm2s_enable | 是否启用AXI MM2S接口 | 1、0 |
如果不需要两条射频链路,只做单收单发,那就把adc_dac_cores设为1,可以节省大量逻辑资源。若你的处理逻辑只要接收不要发送,可以把tx_dma关掉,生成的Block Design里会减少对应的DMA通道。
3.3 自定义板卡目录的操作方法
实际项目中很少有人直接拿官方开发板做产品,多半是自研板卡。这时候不建议直接修改官方目录里已有的工程,而是把整个zc706目录复制一份,改成自己的板卡名。比如:
cp -r projects/fmcomms2/zc706 projects/fmcomms2/myboard然后修改myboard目录下的system_project.tcl,把工程名、器件型号、板卡名改成自己的。同时要把system_constr.xdc里的引脚约束全部替换成自己板卡上AD9361和FPGA的实际连接。TCL脚本里通常会读取板卡目录下的约束文件,你只要保证myboard目录里约束文件名字和脚本里引用的名字一致就行。
需要特别提醒的是,如果自研板卡的FPGA型号和官方板卡不同,还要检查IP核版本的可用性。比如某些IP核不支持新的器件,脚本虽然能生成工程,但综合时可能报“IP not supported on target part”。遇到这种情况,优先去ADI公共IP库中查看IP核的支持列表,换一个支持的器件。
4. 生成工程后的常规处理与验证
4.1 生成XPR后先检查Block Design,不要急着综合
脚本跑完,生成了.xpr工程文件。很多人一激动就直接打开工程点“Generate Bitstream”,结果烧到板上没反应。我的习惯是先用Vivado打开system_bd,检查Block Design里的几个核心模块是否都连对了。以AD9361工程为例,最重要的几个IP是:
axi_ad9361:AD9361的数字接口控制器,负责LVDS/CMOS数据接收、发送、时钟同步。axi_ad9361_adc_pack:把采样数据打包成AXI-Stream。axi_dmac:DMA控制器,把数据从PS内存搬到PL或反向搬。util_wfifo:跨时钟域FIFO,缓存数据流。axi_gpio:用于配置AD9361的复位、使能等控制信号。
TCL脚本生成的Block Design一般已经很完整,但你要确认DMA的中断连接是否到了PS端的pl_ps_irq0或pl_ps_irq1,因为Linux驱动通常依赖中断来搬运数据。如果发现中断没连,需要手动连上后重新生成Wrapper。
在Block Design里修改后,记得右键设计源文件,选择“Create HDL Wrapper”,让Vivado重新生成顶层的HDL例化代码。如果你不生成Wrapper,后续综合时可能会用旧版本的顶层文件,导致修改不生效。
4.2 综合、实现和生成比特流的完整流程
确认Block Design没问题后,就可以开始综合了。这里我建议按顺序执行:
- 在Sources面板选中顶层文件,右键“Generate Output Products”。
- 等待所有IP核的输出产物生成完毕。
- 点击“Run Synthesis”,综合完成且无错误后,再“Run Implementation”。
- 最后“Generate Bitstream”。
如果你的工程文件多、器件大,建议用批处理模式来跑,避免GUI卡死。比如:
vivado -mode batch -source run_bitstream.tcl在TCL脚本里依次执行synth_design、opt_design、place_design、route_design和write_bitstream。这种方式不光快,而且方便记录日志。
综合后经常会出现时序违例,尤其是AD9361数据接口的时序约束。这类接口直接连到FPGA的引脚上,受PCB走线长度影响很大。如果时序不过,先看是不是约束文件里set_input_delay和set_output_delay设置得偏差太大,再考虑调整综合策略,比如把phys_opt_design打开,或者对数据通路加一列寄存器。AD9361的LVDS数据速率不算非常高,正常布局下时序是可以收敛的。
4.3 如何把AD9361接口和自定义逻辑对接
生成好的工程默认是把AD9361数据通过DMA搬到PS端,靠Linux驱动或裸机程序读取。如果你不想走DMA,而是想在PL里直接对数据进行处理,比如做数字下变频、滤波,就得打断原来的数据通路。
最常用的方式是在Block Design中,把axi_ad9361_adc_pack输出的AXI-Stream接口不接到DMA,而是接到你自己的自定义IP上。你可以在Vivado里用“Add Module”添加一个HDL文件,然后手动连线。AXI-Stream的接口比较简单,核心信号是axis_tdata、axis_tvalid、axis_tready、axis_tlast。数据位宽一般是32位或64位,取决于你配置的采样位数和通道数。
我自己习惯的做法是先用ILA抓一下AD9361输出的波形,确认数据有效信号和数据内容没问题,再接后续算法模块。ILA的采样深度不用太大,深度1024足够观察链路是否正常。抓到的数据如果全是0,先检查AD9361的SPI配置是否完成,而不是怀疑数据通路。
5. 我在实际操作中踩过的坑和排查思路
5.1 TCL脚本跑到一半报“file not found”的根因分析
我曾在一次脚本执行中遇到一个特别诡异的报错,提示某个.xci文件找不到。单独检查文件确实存在,路径也正确。后来发现是TCL脚本内部用相对路径解析文件,而执行脚本时当前工作目录和我项目目录不一致。因为我是打开了Vivado GUI,在默认的Tcl Console路径下直接source了system_project.tcl,脚本里的相对路径自然就找不到了。
排查方法很简单:执行脚本前先pwd看一下当前目录,然后cd到工程目录。另外有些脚本会检查环境变量ADI_HDL_DIR,指向仓库根目录。如果没设这个变量,公共函数就找不到library下的IP脚本。这个问题在Linux下尤其常见,因为你在普通终端里source了settings后,ADI_HDL_DIR并没有设置。
建议手动设置一下:
export ADI_HDL_DIR=/path/to/hdl或者在Windows环境变量里添加ADI_HDL_DIR。这样TCL脚本在加载library目录时就能正确定位。
5.2 脚本运行时间过长,卡在“Generating IP”
生成工程本身不算慢,但后续“Generate Output Products”会花很长时间。如果不开批处理,GUI界面还可能一直转圈,看起来像卡死了。其实Vivado是在逐个生成IP核的输出,尤其是axi_ad9361这种包含大量实例的IP,需要几分钟到十几分钟不等。
如果你的机器内存比较小,可以尝试关闭多余软件,或者使用批处理模式。另外,Vivado的IP缓存目录可能在系统盘,如果磁盘空间不足,生成IP会异常变慢。可以在Tcl Console里执行:
set_param general.maxThreads 8这个参数可以调整Vivado使用的线程数,对于多核CPU有明显效果。但也不要无脑调太大,我试过在16核机器上设置16线程,反而因为内存不足导致编译失败,后来稳定在8线程。
5.3 明明选的AD9361,生成出来的顶层却是AD9380
这个问题看起来离谱,但我在自定义板卡时遇到过。原因是目录结构选错了。projects目录下有很多参考设计,有些是ADI视频接口,有些是射频收发。TCL脚本生成顶层时,会根据system_top.v里的宏定义或者文件列表来判断例化哪个IP。如果你把fmcomms2目录里的脚本和adv7511目录里的system_constr.xdc混在一起,工程自然就乱套了。
所以我的建议是:不要跨目录混用文件。每块板卡目录自成一个完整系统,复制后要检查所有文件是否都来自同一套参考设计。特别是有了自定义板卡目录后,要确保system_project.tcl引用的system_top.v还在这个目录下,而不是意外引用到了官方路径。
5.4 综合阶段出现“MMCM/PLL cannot lock”或时序崩溃
这个现象不是每次必现,但我遇到过一个比较隐晦的原因:工程里某个时钟管理模块的频率约束和实际引脚输入时钟不一致。AD9361参考设计通常会从板载晶振或者AD9361的时钟输出获取参考时钟,如果TCL脚本里默认的时钟频率和你开发板实际晶振频率不同,就会导致MMCM配置失败。
遇到这类问题,先打开Clocking Wizard的配置,确认输入时钟频率是否和板卡原理图一致。比如原理图上写着100MHz,但脚本里默认是50MHz,那就需要手动改一下IP配置。改完之后重新生成输出产物,再跑一遍综合,大部分问题都能解决。
6. 以这套工程为基础做二次开发的小技巧
6.1 改工程名和顶层模块后如何保持脚本可控
很多人拿到生成好的工程后,第一件事就是改工程名。直接在Vivado里“Save Project As”当然可以,但如果以后你想再次用TCL脚本重新生成工程,改过的名字和路径就被覆盖了。正确的做法是修改system_project.tcl里的工程名变量,然后删除旧工程目录,重新执行脚本生成。
我有一次把工程名从zc706_fmcomms2改成了myradio_top,结果发现TCL脚本生成后,Block Design的名字还是system_bd,顶层Wrapper也是system_wrapper。如果希望名字统一,需要在system_bd.tcl里也同步修改。建议只改工程名,保留system_bd和system_wrapper这些内部名称,改动最小,脚本也不容易坏。
6.2 加入自定义AXI-Lite寄存器逻辑
很多场景下,你需要通过PS端控制PL里的参数,比如滤波器系数、增益表。这时最方便的做法是在Block Design里加入一个自定义AXI-Lite Slave IP。Vivado自带的“Create and Package New IP”向导能帮你生成AXI-Lite接口框架,然后在Verilog里写寄存器逻辑。
以一个8路寄存器为例:
always @(posedge s_axi_aclk) begin if (s_axi_aresetn == 1'b0) begin reg0 <= 16'h0; end else if (s_axi_awvalid && s_axi_wvalid && s_axi_awready && s_axi_wready) begin if (s_axi_awaddr[3:0] == 4'd0) reg0 <= s_axi_wdata[15:0]; end end添加完成后,需要在Block Design里手动连接这个IP的S_AXI到PS的M_AXI_GP口。记得给PS的接口打开“Enable”状态,并分配地址。地址分配时避开已有的GPIO、SPI等外设地址区间即可。这样Linux端只需要通过/dev/mem或UIO框架就能读写这些寄存器。
6.3 把整个流程纳入版本管理,用批处理模式跑工程生成
TCL脚本最大的优势是确定性。只要仓库版本固定、环境变量固定,任何人在任何机器上都能生成一模一样的工程。我现在的做法是把仓库克隆下来后,写一个build.sh脚本,里面设置好ADI_HDL_DIR、Vivado版本、目标工程目录,然后一键生成并编译。
source /tools/Xilinx/Vivado/2022.2/settings64.sh export ADI_HDL_DIR=/home/user/hdl cd /home/user/hdl/projects/fmcomms2/myboard make这样团队成员之间只需要同步HDL仓库和脚本,不用同步动辄几个GB的Vivado工程目录。如果遇到IP版本升级,直接切分支重新生成即可,旧工程不会污染新的开发流程。
我个人建议把生成过程中所有的警告日志都打印出来固化到仓库里,方便后续对比。虽然这样会占用一点存储空间,但排查问题的时候比对日志比比对截图高效得多。尤其是TCL脚本这种“一步错步步错”的流程,一份干净的历史日志能帮你快速定位是哪次修改引入了回归。
以上就是我从零跑通ADI AD9361 TCL脚本生成HDL工程的完整记录。整套流程并不复杂,关键是理解每一步在做什么,以及准备好版本匹配的Vivado环境。如果你也在折腾AD9361和Vivado,建议先在官方板卡上跑通默认脚本,再逐步改成自定义板卡。遇到问题时,先完整读一遍TCL日志,绝大多数答案都在里面。