
简介面向FPGA时间测量需求的TDC时间数字转换Vivado工程文件基于Xilinx Artix-7 XC7A35T器件构建完整设计流程适合学习高精度时间间隔测量、高速数据采集与FPGA时序分析的学生和开发人员。压缩包共369个文件约9.87MB包含工程入口.xpr、Verilog/VHDL源码、仿真波形配置.wcfg、约束文件.xdc、IP核配置.xci以及综合与实现报告.rpt等目录结构按源码、仿真、IP核与运行记录分类便于直接恢复工程、编译仿真与硬件部署。工程覆盖行为级与实施级仿真配置并提供PLL等用户IP核相关文件可完整展示从RTL设计、时序约束到比特流生成的全流程。目前已有1495人学习对于理解TDC在高频操作下的计数、比较器结构及低延迟、抗噪声设计策略具有较强参考价值也可作为Artix-7平台时间测量项目的开发模板能够显著缩短项目环境搭建时间。 最近在整理手头一套时间数字转换TDC的Vivado工程文件发现网上的资料要么是纯理论推导要么贴一段Verilog代码就算交代了能直接拿来跑、文件结构又清楚的参考其实不多。我手上这份工程是从一个激光测距项目里抽出来的TDC核心模块裁掉外围业务逻辑后保留了一整套可综合、可仿真的结构延迟链、温度计码编码器、粗计数器、码密度校准和串口回读逻辑都在。这套方案适合正在做高精度时间间隔测量、超声流量计、粒子物理或者单纯想学FPGA高速电路设计的朋友拿来当起点非常合适。先把结论放前面FPGA做TDC核心思路就是“粗计数加细量化”。用系统时钟数出整数周期再用进位链把小于一个周期的部分细分到几十皮秒级别。Vivado工程里最难处理的往往不是逻辑本身而是延迟链的综合约束和工程文件的管理方式。这篇文章就沿着“原理→电路→校准→工程管理→排错”这条线把每一步都摊开讲尽量少说废话。1. TDC在FPGA上怎么落地整体思路与选型考量1.1 先搞明白TDC到底在测什么TDC全称Time-to-Digital Converter时间数字转换器做的事情一句话概括把两个事件之间的时间间隔变成一串数字码输出。比如激光雷达里从发射光脉冲到接收回波中间的时间差直接决定目标距离粒子物理里两个探测器信号的到达时间差用来区分粒子飞行路径。精密时间测量在很多场景下等价于高精度的空间或速度测量系统性能的上限往往就卡在时间分辨率上。用生活里能理解的东西打比方普通秒表只能读到百分之一秒而TDC就是时间的“游标卡尺”。一个典型FPGA TDC系统测量范围从几纳秒到几毫秒分辨率能做到几十皮秒甚至更低。皮秒是什么概念光在空气中1皮秒大约走0.3毫米所以激光测距想做到毫米级时间测量精度就得做到皮秒级。普通数字计数器用时钟直接数最高分辨率就是时钟周期本身想打破这个限制必须用专门的细量化结构。FPGA上的主流做法是用进位链作为延迟线。信号进入链后以极小的步进逐级传播每级延迟就是时间量化精度。这样做的好处是延迟单元经过芯片布线资源的特殊设计路径固定、延迟小且相对均匀不需要额外占用可编程布线资源。用7系列FPGA的CARRY4搭延迟链每级延迟大约在10到20皮秒这个量级已经能满足绝大多数精密测量需求。1.2 为什么选用FPGA而不是专用TDC芯片市面上有专门的TDC芯片比如一些厂商出品的单通道或多通道时间测量芯片分辨率和稳定性都很好接口也简单。但这类芯片有它的局限通道数固定不方便扩展到几十上百通道内置逻辑不开放想在上面做数据预处理、符合判选或者自定义触发策略很麻烦最后集成到系统里要多带一颗芯片和一套外围电路成本和面积都不占优。FPGA方案的核心优势是“通道密度与逻辑灵活性兼得”。一个中等规模的7系列FPGA做上二三十个TDC通道完全没问题每个通道的延迟链和编码器只占几百个LUT和FF剩下的逻辑资源还能跑以太网、USB、自定义协议栈甚至软核处理器。一套系统往往同时需要时间测量、数据处理、上位机通信FPGA可以一肩挑。我实测过在中端Kintex-7上做32通道TDC整片资源占用不到30%还能塞下一套千兆网传输逻辑。当然这么做的代价是开发周期长一些尤其是温度和电压变化引起的延迟漂移必须在校准环节解决。但如果你需要的是灵活、可控、可扩展的时间测量平台用Vivado加FPGA做TDC是值得投入的路线。2. Vivado工程结构拆解一整套TDC工程里都有什么2.1 从工程目录看起xpr、srcs、runs各管什么很多人拿到TDC工程第一反应是双击.xpr文件打开看到界面上一堆文件就懵了。其实Vivado工程结构很清晰只是界面默认帮你隐藏了很多目录细节。我习惯用文件管理器直接看工程根目录下面几个东西是核心.xpr文件工程入口保存了工程配置、器件型号、源文件列表和约束文件引用关系。注意它保存的很多路径是相对路径所以工程整体拷贝到别的机器上只要目录层级不变就能打开。srcs目录源文件所在地。其中sources_1/新添加或新创建的RTL文件一个bd对应Block Designconstraints_1放约束文件sim_1放仿真文件。Vivado对文件类型有严格分类同一个目录下的.v文件有可能被分成Design Sources和Simulation Sources。runs目录综合和实现结果。synth_1是综合产物impl_1是实现产物里面能看到时序报告、资源利用率报表以及最终生成的bit文件。.cache和.hw目录一些缓存和硬件调试信息不是核心文件但误删可能导致工程打开后需要重新索引。把整个工程复制给别人时最好连这些一起打包省得对方打开后各种重新加载。理解目录结构有个实际用途很多人遇到“Vivado里看到的文件和磁盘上不一致”的问题基本都是没有理解Vivado对文件的管理方式。Vivado的工程文件列表只是一个“引用索引”磁盘上文件有没有被同步修改、有没有被复制进工程目录Vivado并不强制检查需要手动刷新或者脚本同步。2.2 顶层模块与数据流划分一套能跑的TDC工程顶层逻辑大致可以分成下面几个大块我在项目里通常按这个模块划分组织源码模块名职责关键接口tdc_top顶层连接所有子模块、处理时钟复位sys_clk, rst_n, hit_in, tdc_valtdc_channel单通道TDC包含延迟链锁存编码hit_in, clk, thermo, bin_cntdelay_lineCARRY4延迟链hit_in, clk, dly_tapencoder温度计码转二进制thermo, bin_cntcoarse_counter粗计数器输出时钟周期整数部分clk, cnt_en, coarse_valcalibration码密度统计/校准查找表en, bin_cnt, lut_wr_enuart_engine串口回读调试用txd, rxd, data_in数据流是这样的hit信号进入延迟链经过约十几纳秒的传播延迟后在系统时钟边沿被锁存得到一长串温度计码温度计码经过编码器计算出“该时钟沿时刻信号在链中传播到的位置”也就是细时间同一时刻粗计数器给出整时钟周期计数值细时间和粗时间拼接后再通过校准查表输出最终时间码。理解这个数据流以后调试TDC基本就是沿着hit_in→dly_tap→thermo→bin_cnt→time_code这条路逐级查。3. 核心电路实现延迟链、编码与粗细拼接3.1 用CARRY4搭延迟链千万别被综合工具“优化”掉7系列FPGA的CARRY4是一个4位超前进位结构。把输入DI全部置0、S全部置1时进位信号会沿CO引脚直接传递下去形成近似等延时的传播路径。每个CARRY4能提供4个抽头抽头与抽头之间的延迟就是TDC的分辨率。延迟链代码本身不复杂麻烦在于让综合器知道“这串东西不能动”。下面是一段我在工程里实际使用的延迟链代码用了DONT_TOUCH属性来防止被综合优化parameter TAPS 512; (* DONT_TOUCH TRUE *) wire [TAPS-1:0] dly; genvar i; generate for (i 0; i TAPS/4; i i 1) begin : g_carry CARRY4 u_carry4 ( .CI (i 0 ? hit_in : dly[i*4-1]), .CYINIT (1b0), .DI (4b0000), .S (4b1111), .O (), .CO (dly[i*43 : i*4]) ); end endgenerate这里最关键的是每级延迟单元输出必须保留否则Vivado综合时会把整条链识别成“等价于直接连线”然后全部折叠掉。除了代码里的DONT_TOUCH我还会在综合属性面板里给延迟链单独设置keep hierarchy双保险。但注意DONT_TOUCH和KEEP属性会阻止跨层级优化如果链很长又分散在多个模块可能出现资源位置不佳的问题所以链最好集中在一个子模块内。链长怎么确定这个需要算一下。假设系统时钟125MHz周期8ns目标让链覆盖一个整时钟周期再留大约20%裕量以防温度和电压变化导致链延迟变短那么需要的总延迟是8ns × 1.2 9.6ns。若实测平均每级延迟约15ps需要的抽头数量为9.6ns / 15ps 640级。每个CARRY4提供4级则需要约160个CARRY4。实际用ILA实测过一次该配置在室温下延迟链刚好能覆盖约1.3个时钟周期留了足够裕量。如果你的时钟更高或目标分辨率更细链长按这个公式线性折算就行。3.2 温度计码编码器把链上状态变成二进制数延迟链上的信号在时钟边沿被锁存后得到的是一串类似温度计的东西从起点到某个位置全部是1再往后全部是0。这个“0/1分界点”所在的位置就是hit信号在这一拍钟内传播到了第几级。编码器的任务就是找到这个边界。always (*) begin bin_cnt 0; for (k 0; k TAPS-1; k k 1) begin if (thermo[k] 1b1 thermo[k1] 1b0) bin_cnt k; end end这是一段很暴力的循环Vivado综合时扩展成查找表的优先级逻辑代码量小、时序好。要注意的是如果温度计码出现“冒泡”——比如序列10111中间多了个0说明链上某两级延迟不均匀或者锁存时钟倾斜边界就变成了两个这个for循环会取最后一个满足条件的k作为近似结果。实测下来冒泡问题在小规模链上不严重但如果链比较长建议在编码前加一级多数判决或冒泡消除逻辑把每三个相邻抽头做多数表决后再进编码器付出的代价是细时间会多一级组合逻辑延迟。对于分辨率要求不是极致苛刻的场合这方法简单可靠。更激进的做法是直接用进位链末端的少数高延迟抽头配合多个采样时钟相位但复杂度会翻倍一般项目没必要。3.3 粗计数器与细时间拼接游标卡尺的数字化表达细时间只是“在一个时钟周期内信号走到哪了”真正要输出的时间值等于整数时钟周期加上小数部分。硬件上实现起来就是两个数拼接粗计数器在hit信号到来时锁存count值编码器输出锁存bin_cnt然后按下面的公式计算time_code coarse_cnt × T_clk bin_cnt × (T_clk / TAPS)听起来简单工程上有个容易出错的地方粗计数器的值和细时间必须取自同一个时钟沿。如果hit在时钟沿附近到达粗计数器可能已经翻转而细时间对应的还是翻转前的周期两者会错位一个周期。解决方法是让粗计数器和锁存链都用同一个采样时钟并且把粗计数器的寄存器位置和延迟链锁存寄存器尽量靠近同时把跨时钟域信号用格雷码同步。我习惯把粗细拼接做成查表而不是直接乘除法因为FPGA里乘除法代价高。预先算好每个bin对应的皮秒数存到BRAM输出时直接查表相加校准也一起合进这张表。这样时间分辨率不再均衡但测量精度反而更高因为校准表本身就是非均匀的分布。3.4 约束文件里必须写清楚的几条路径TDC工程的约束文件和普通逻辑不太一样不能只写个时钟约束就交差。延迟链采样路径非常特殊有必要向时序分析工具说明。下面是XDC文件里几个关键片段create_clock -period 8.000 -name sys_clk [get_ports sys_clk] # 延迟链上的路径由硬件原理保证不做标准时序检查 set_false_path -from [get_cells g_chain/g_carry*] -to [get_cells g_latch*]第一句是基本时钟约束第二句把延迟链到锁存寄存器的路径设置为伪路径。原因在于这条路径的天然延迟就是TDC的工作基础工具默认的建立时间检查会认为它严重违规报出一堆时序错误。伪路径只是告诉分析器不要在这里卡报错。但有几个细节要注意如果链太长极端温度下延迟减小hit可能在一个周期内穿透整条链再冲出去导致温度计码全部为1、边界丢失。这种情况下伪路径掩盖了问题。所以我实际项目里更建议用set_max_delay代替set_false_path比如设成时钟周期的1.5倍这样既能通过时序检查报告里还能看到这条路径的实际延迟估算分辨率很方便。真到流片阶段再去纠结怎么让时序分析更精确做板级实验时够用就行。4. 校准把非理想延迟“拉直”4.1 码密度校准的基本原理和实现步骤延迟链有一个绕不开的问题每个抽头之间的延迟并不相等芯片制造工艺、电压、温度都会让延迟发生漂移。如果直接按理想bin宽度计算时间测量结果会出现周期性非线性码密度分布明显不均匀有些bin宽度大、有些bin宽度小严重时DNL能达到一个LSB以上。码密度校准Code Density Test是解决这个问题最常用的方法。原理很简单向TDC输入大量时间上均匀分布的事件理论上每个bin接收到的事件数与它的实际宽度成正比。统计足够多的事件后根据每个bin的计数比值反推出bin的真实宽度生成一张非均匀查找表。校准步骤我整理成这样的流程输入大量随机时间事件至少每bin几百个以上统计每个bin出现的次数hist[i]。计算平均计数avg total_count / TAPS。计算每个bin的权重系数w[i] hist[i] / avg。生成校准表w[i]后续测量时把细时间bin_cnt对应的系数累加进时间码。做这一步时我建议先把校准数据通过串口回PC端处理成查找表再把查找表固化到FPGA的BRAM里。完全在FPGA内做统计也可以但需要足够的RAM和复杂的控制逻辑对初学不友好。校准完成后我实测的DNL从约0.9 LSB降到了0.2 LSB以内精度提升非常明显。4.2 在线校准与温度漂移处理码密度校准解决的是“不均匀”问题但温度变化引起的是“整体延迟变慢或变快”的问题。FPGA的进位链延迟受温度影响比较大温度升高时延迟增大温度降低时延迟缩短。如果校准表是固定的一套环境温度变化后测量结果会出现整体偏移。工程里常用的在线校准方案是在芯片里放一个已知周期的事件源比如用专用逻辑产生一个周期稳定的信号把它同时送入几个预留的TDC通道实时统计这几个通道的测量结果与真实周期之间的偏差用这个偏差来修正主通道的校准表。说得直白点就是给TDC系统加一个“尺子”随时校正刻度。也可以参考其他思路利用FPGA的PLL或MMCM工作状态估算温度和电压变化然后反过来调整校准表。但这个方法精度有限且依赖具体工艺。我试过几次后发现预留给校准通道才是性价比最高的做法——资源占用小修正实时性也好。最后要提醒一点校准事件源必须与系统时钟有确定相位关系否则校准通道本身的抖动会被当成误差适得其反。5. 工程文件管理实操增删文件不踩坑5.1 “加入工程”和“复制到工程”是两码事TDC工程维护过程中绕不开的一个操作是往工程里加文件。Vivado里Add Sources时弹窗里有个选项Copy sources into project。这个选项让很多人踩过坑。如果勾选文件会从原路径复制一份到工程目录下的srcs/sources_1/imports里如果不勾选Vivado只记录一个绝对路径引用源文件还留在原来的位置。表面上两个都能编译区别要到工程迁移、打包或换电脑时才会暴露。我的习惯是凡是正式项目的源文件一律勾选Copy。这样整个工程目录就是完整的从版本管理或归档角度看非常干净拿到另一台电脑上打开不会报找不到文件。如果是调试阶段临时挂载一个仿真模型或外部给的参考文件可以不复制但必须在工程文档里记录外部路径否则过两周自己都找不回来。很多教程只说“添加文件”很少讲清楚这个复制行为。实际上Vivado在磁盘上管理文件有一套自己的逻辑理解清楚“引用索引”和“物理文件”这两个概念以后很多文件管理问题都能想明白。5.2 删除文件后Vivado报错怎么处理玩TDC工程时经常随手删几个调试用文件但Vivado里“删除”这个词有歧义。在Sources面板右键文件弹出的菜单里有Remove File from Project和Delete File两个选项前者只是把文件从工程索引里摘掉磁盘文件完好后者会直接把磁盘上的源文件也删了。我见过不止一个同事把Delete当成Remove结果辛辛苦苦写的Verilog文件直接进了回收站。更常见的问题是反过来在磁盘上手动删除了文件但Vivado工程索引里还指向它。打开工程后Vivado会标记这些文件为Missing编译时报错有时候连打开工程都会卡在恢复索引的状态。处理办法是在Sources面板右键选择Refresh或直接选中缺失文件后右键Remove。如果缺失的是IP核相关文件最好用IP Catalog重新生成一次比手动重建靠谱得多。这里有个经验分享我的TDC工程里仿真文件和综合源文件经常有同名或同目录混杂的情况。用脚本从版本管理工具更新代码时如果新旧文件有增有删尽量先刷新工程列表再编译不然Vivado可能一直在用缓存里的旧文件出问题排查起来极度痛苦。5.3 用TCL命令管理源文件多人协作更省心如果TDC工程只在自己电脑上开发图形界面操作没问题。但多人协作时每台机器的工程绝对路径不同直接把.xpr和工程目录提交到Git几乎每次合并都会产生冲突因为.xpr里记录了大量绝对路径和窗口状态。更稳的做法是只用TCL脚本管理源文件。我建议在工程根目录放一个top.tcl脚本把所有源文件、约束文件、编译选项都写成TCL命令新机器上只需打开Vivado执行source top.tcl就能重建整个工程。几个最常用的命令create_project tdc_proj ./tdc_proj -part xc7a75tfgg484-2 add_files -norecurse [glob ./src/*.v] add_files -fileset constrs_1 ./constraints/tdc_top.xdc add_files -fileset sim_1 ./sim/tb_tdc.v set_property top tdc_top [current_fileset]这样版本管理里只保留源码和脚本不追踪工程目录Git冲突基本消失换电脑重新搭建环境也只要几分钟。TDC这类高速设计对开发工具版本和IP版本敏感脚本里最好把Vivado版本和器件型号写进注释防止队友用错版本打开后一堆诡异报错。6. 常见问题与排查技巧实录6.1 综合后延迟链被优化没了这个问题我几乎在每次带新人做TDC时都会遇到。现象是综合后的资源报告里只有很少的LUTILA采样到的链上信号全是X或常量测量结果完全不变化。原因就是综合工具把纯组合传播结构当作冗余逻辑优化掉了。解决办法就是前面提到的DONT_TOUCH属性加综合keep hierarchy还不行就手动检查一下综合日志里有没有输出“deleted”或“trimmed”相关的警告。但注意属性别滥用链上所有节点都强制保留会让布局压力变大链太长时可能无法布成一条直线。6.2 输出始终卡在某个bin或出现“死区”现象是输入任意时刻的hit测量结果总是集中在一两个bin附近或者在某个范围内根本没有结果。通常有三类原因延迟链长度不够hit在半个周期内就穿出链末端锁存时链上全部置成1编码器无法区分或者是输入信号与采样时钟不同步hit本身正好在时钟沿附近反复抖动导致锁存值为亚稳态再就是编码器循环条件写错了边界搜索方向反了。排查时先看温度计码原始值用ILA抓thermo信号确认是不是标准的1/0分布。如果是全0或全1先加长链或改变hit输入时序如果是正常边界但输出异常问题大概率在编码器和后续计算逻辑。有个技巧强制给输入一个固定的、和采样时钟相位差可调的信号扫描相位差观察输出线性度能快速定位“死区”在哪个环节产生。6.3 资源占用比预期大TDC本身的资源开销其实很低一个512级的链加编码器不到两百个LUT。如果整个工程资源异常偏高多半是编码器被综合成了超大优先编码器或者链上锁存寄存器被重复复制导致寄存器数量爆炸。我遇到过一种情况在for循环里把链输出在多个always块中使用综合器出于时序考虑复制了大量寄存器副本。解决方法是统一在顶层把thermo信号打一拍寄存后再分发避免综合器自己做冗余推断。资源利用率报表如果显示FF数量是预期的好几倍优先检查这里其次再怀疑是不是IP核配置了过大的FIFO或存储器。如果让我重新做一遍这套TDC我会先把校准通路做好再做延迟链本身。很多同学一上来就追求几十皮秒分辨率结果先把延迟链调通就花了两周。实际项目里码密度校准和温度补偿才是决定最终精度的关键延迟链本体的均匀性反而可以通过校准弥补。最后再分享一个小技巧Vivado工程里删除或加入文件时务必先确认文件属于哪个filesetDesign Sources和Simulation Sources混在一起是很多“编译报错找不到文件”的根源。希望这篇工程笔记能帮你少走点弯路一次跑通。本文还有配套的精品资源点击获取