
在实验室里第一次在示波器上看到12 ps的TDC bin宽度时我差点以为是自己算错了。后来这个数字成了日常我越来越觉得用CARRY4进位链堆出来的时间数字转换器是FPGA玩家能低成本够到的最高精度测量方案之一。这篇文章把架构、代码、约束到校准的完整链路摊开讲目标是让一个能用Vivado写基础工程的兄弟在Kintex-7上把时间分辨率做到几十皮秒量级而不是让FPGA TDC只停留在论文和PPT里。为什么非要较真这件事因为在激光测距、PET成像、粒子物理实验、高速串行接口抖动测试这些场景里时间测量精度直接决定系统性能。异步 arrives 的脉冲到底比系统时钟早到了多久、晚到了多久传统计数器只能量到一个时钟周期5 ns 200 MHz而CARRY4进位链能把这种时钟周期内的碎片时间进一步切碎碎到十几皮秒。这就是标题所谓的皮秒级时间测量的核心路径。1. 为什么选Kintex-7的CARRY4做TDC而不是专用时间测量芯片很多人一听皮秒级时间测量第一反应是上专用TDC芯片比如ACAM的TDC-GPX、TI的TDC7200。这些芯片确实能做到几十皮秒甚至更高的分辨率但真正去选型时你会发现一堆麻烦通道数通常只有2到8路想铺满几十路触发通道得并好几颗芯片数据接口通常是SPI持续吞吐率不够时间量程和分辨率往往绑定做不到宽量程高精度同时兼顾另外采购周期、价格、供货稳定性在不少项目里都是雷。FPGA做TDC本质上是用硅片上的传播延迟当最小刻度尺把时间差转换成信号沿着进位链跑了多远这种空间信息再用触发器把空间位置锁存下来。这个想法的工程价值在于通道数可以做到几十甚至上百路所有通道共享一套粗计数器数据以并行总线输出FIFO缓冲后吞吐率远高于串行读出方案量程由粗计数器位宽决定想要多少位就扩展多少位分辨率由进位链的级间延迟决定通常落在10到20 ps附近。那为什么偏偏是Kintex-7我个人的工程判断有三点工艺制程和时序余量7系列是Xilinx被AMD收购前最成熟的一代工艺CARRY4的延迟特性在标准速度等级下非常稳定。相比Spartan-67系列的CLB布线资源和时钟资源更充裕相比UltraScale它的文档、IP、社区资料极其丰富遇到问题容易找到参考。做高精度TDC最大的敌人是不可控的不确定性成熟工艺意味着版图规则、布线延时模型都经过了海量验证。性价比和量产可获取性Kintex-7中端型号7K160T、7K325T价格远低于Virtex-7但LUT、BRAM、GTX等资源足够撑起一套多通道TDC 数据后处理引擎。如果你只是验证TDC算法Artix-7也能跑但Artix-7的速度等级低进位链延迟更大、线性度更差到了UltraScaleCARRY8结构和电压域差异又会让老经验失效。信号完整性和供电条件TDC对电源纹波极其敏感Kintex-7的供电方案成熟常规DC-DC加LC滤波就能把电源噪声压到可接受范围不需要像射频板那样堆一堆LDO。当然真要做工程级精度模拟电源域还是要单独处理这个后文会展开。顺带再说一句TDC这个玩法也不是7系列才有的早期有在Virtex-4/5上做波导型TDC的论文但那会儿进位链均匀性差需要大量校准。7系列在制造一致性上比老一代好不少这才让CARRY4直接用有了工程可行性。2. 硬件原语拆解CARRY4每个引脚在测量链路里干什么2.1 CARRY4在FPGA里的结构位置Kintex-7的CLB可配置逻辑块里每个SLICE包含两个CARRY4原语。一个CARRY4内部有4个独立的进位级通过级联的MUXCY选择器传递进位信号。它的端口看起来平平无奇但在TDC工程师眼里每个引脚都有明确的时间角色端口方向在TDC中的作用CIN输入进位链的入口通常接START信号或上级CARRY4的COUTCYINIT输入第一级进位初始化不用时接地DI[3:0]输入进位链旁路数据TDC中常接到固定电平或逻辑0S[3:0]输入进位链控制信号决定CIN到CO的传播路径CO[3:0]输出进位链各级输出作为时间游标tap供触发器锁存O[3:0]输出算术输出TDC中一般不关心信号从CIN进入后会在S和DI的控制下逐级向高位移位。每一级CO到下一级CIN之间的传播延迟就是TDC的一个时间bin通常在7系列上约为10到25 ps这是一个工艺相关、电压温度相关的物理量。用常规定义说的话信号在进位链上每往前跑一级就相当于在时间轴上往前走了约一个bin的距离。2.2 为什么要用专用进位链而不是LUT延迟线门外汉可能会想FPGA里到处都是LUT为什么不能把LUT一个个串起来当延迟线用实测过的人都知道LUT的输入到输出延迟很大几百皮秒量级而且不同LUT输入路径延迟差异明显在布线不锁定的情况下综合工具随时会重排路径延迟完全不可控。CARRY4则不一样它是硅片内部固定的专用进位逻辑级间连线最短、路径只有一条而且CARRY4在SLICE内排列非常紧凑进位链可以沿SLICE列方向整齐往下走这种物理上的均匀性不是LUT能替代的。2.3 采样锁存START沿链跑STOP把它定格这是整个TDC最核心的时间测量思想把START信号注入CARRY4的CIN它就会沿着这条进位链一路传播每经过一级对应的CO输出电平就会发生一次跳变。如果此时STOP信号作为采样时钟到来那沿链上的所有触发器会同时把CO的状态锁存下来。由于信号爬过的级数正比于START和STOP之间的时间差锁存到的温度计码一串连续的1后面跟着一串连续的0或者反过来就编码了这个时间差。一个形象的比喻进位链就像一条等距摆放了一排计时点的赛道START是起跑的发令枪STOP是终点线的快门快门落下的瞬间跑步者跑到第几个计时点我们就能反推出他跑了多久。在实现时STOP信号必须被当成采样时钟来对待打到一个全局或区域时钟缓冲器上尽量降低各触发器之间的时钟偏斜clock skew。而START信号是纯粹的数据路径不需要满足建立时间因为测量原理决定了它跟STOP的相位关系是未知的。3. 顶层架构与Verilog代码粗计数、细测量与气泡处理3.1 双计数器架构粗计数细量化这里采用的是一种最常见的粗计数细测量两级量化架构系统时钟驱动一个自由运行的粗计数器负责记录整个时间戳的高位部分进位链细量化部分只负责测量START到下一个时钟沿之间的相位差也就是一个系统时钟周期内的碎片时间。完整时间戳等于粗计数值减去或加上细测量值具体加减要看你把START定义在哪个方向。这个架构最舒服的地方在于量程几乎无限只要粗计数器不溢出而分辨率由进位链决定两者解耦。系统时钟用200 MHz时一个时钟周期5 ns进位链的测量窗口只需要覆盖5 ns就够。假设每个bin约13 ps那么大约需要384个bin也就是96个CARRY4每个CARRY4提供4个bin。3.2 核心代码进位链实例化与温度计码采集下面给出一个可直接综合的TDC核心模块适用Kintex-7Vivado工程。先看进链条铺装部分// tdc_carry4_chain.v module tdc_carry4_chain #( parameter CHAIN_C4 96 // 96个CARRY4共384个bin )( input wire start_signal, // START注入 output wire [CHAIN_C4*4-1:0] taps // CO输出温度计码 ); wire [CHAIN_C4:0] cin_wire; assign cin_wire[0] start_signal; genvar i; generate for (i 0; i CHAIN_C4; i i 1) begin : chain_gen CARRY4 chain_unit ( .CO (taps[i*4 : 4]), .O (), .CI (cin_wire[i]), .CYINIT(1b0), .DI (4b0000), .S (4b1111) ); end endgenerate endmodule这段代码里最关键的设置是S[3:0]4b1111、DI[3:0]4b0000。在CARRY4的真值表里当S1时CO会跟随CIN延迟一个进位级当S0时CO会跟随DI。我们把所有S拉高就是让进位信号在链上老老实实逐级传播不允许旁路。接下来是STOP采样寄存器。STOP信号在这里被当成时钟来用为了减小偏斜建议先通过BUFG再接到FDRE的时钟端// tdc_sampler.v module tdc_sampler #( parameter TAP_WIDTH 384 )( input wire stop_signal, input wire [TAP_WIDTH-1:0] taps, output reg [TAP_WIDTH-1:0] sampled_taps ); wire stop_clk; BUFG buf_inst (.I(stop_signal), .O(stop_clk)); always (posedge stop_clk) begin sampled_taps taps; end endmodule3.3 温度计码转二进制与气泡修复温度计码的特征是1和0的交界点即信号到达位置。但实际芯片上因为触发器之间建立保持时间的微小差异、跨die的工艺偏差交界处可能出现多个0/1交替的气泡直接编码会把时间信息读错。气泡修复的常用策略是从MSB向LSB查找最长的连续1段或统计1的个数基于交界处前应该全1的假设。考虑到气泡通常只影响交界附近1到2个bin统计1的个数配合一个预置偏移就能达到很好的抗气泡效果。工程实现上最简单可靠的方案是优先编码器从高位往低位找第一个0或第一个1得到的位置就是时间编码。// tdc_encoder.v module tdc_encoder #( parameter TAP_WIDTH 384, parameter CODE_WIDTH 9 // 0~383 )( input wire [TAP_WIDTH-1:0] taps, output wire [CODE_WIDTH-1:0] code ); // 简单但实用的气泡处理取高位连续1的个数 reg [CODE_WIDTH-1:0] cnt; integer j; always (*) begin cnt 0; for (j TAP_WIDTH-1; j 0; j j - 1) begin if (taps[j] 1b1) cnt cnt 1b1; else break; // 遇到第一个0就停忽略后面的可能气泡 end end assign code cnt; endmodule3.4 完整时间戳合并与跨时钟域得到细测量码后还需要把它和粗计数器对齐。由于STOP采样信号和系统时钟跨了两个时钟域粗计数值必须在STOP采样瞬间锁存而这正好用同一个BUFG后的时钟沿完成// tdc_time_merge.v reg [31:0] coarse_cnt; always (posedge sys_clk or negedge rst_n) if (!rst_n) coarse_cnt 32b0; else coarse_cnt coarse_cnt 1b1; wire [31:0] coarse_at_stop; reg [31:0] coarse_q; always (posedge stop_clk or negedge rst_n) begin if (!rst_n) coarse_q 32b0; else coarse_q coarse_cnt; // 与细测量同沿锁存 end // 完整时间戳 粗计数高权重 细测量低权重 assign timestamp {coarse_q[31:0], fine_code[8:0]}; // 具体位拼接按系统设计这里必须注意粗计数器读出的值可能与STOP沿之间有一个周期的偏差因为stop_clk与sys_clk相位关系未知需要在系统级做一次跨时钟域校准常用的做法是在粗计数器旁边加一个影子计数器跟随stop_clk打两拍或者在校准阶段测量一个已知时间间隔来校准固定偏移。反正一个原则固定偏移可以通过标定消除随机抖动只能靠降低时钟抖动来压。4. 约束才是灵魂XDC中锁定进位链、消除伪路径与亚稳态处理的完整配置4.1 为什么TDC工程在综合和实现时总是满屏时序报错普通FPGA设计里所有同步路径都要满足建立时间和保持时间综合器会拼命帮你收敛。但TDC链路本质是一条非同步测量路径START和STOP的相位关系本身就是被测对象不可能也不应该满足固定的时序关系。如果不做约束Vivado会按照默认的同步逻辑去分析结果就是WNS一塌糊涂、优化器把进位链折腾得面目全非。所以写约束不是可选项而是TDC能否工作的必要条件。4.2 核心约束清单可直接抄作业以下XDC针对Vivado 2018.3及以上版本芯片XC7K325T-2FFG676# 1. 系统时钟 create_clock -name sys_clk -period 5.000 [get_ports clk] # 2. 测量路径是伪路径START到采样触发器的D端 # 不让工具去收敛这条路径 set_false_path -from [get_ports start_signal] -to [get_nets sampled_taps[*]] # 3. STOP作为采样时钟避免专用时钟路由检查报错 set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets stop_buf_inst/O] # 4. 锁存进链条的输入路径也设伪路径避免综合器为了时序收敛把链路重新优化 set_false_path -from [get_ports start_signal] -to [get_pins chain_gen*/CARRY4_inst/CO[*]] # 5. 跨时钟域粗计数器信号加异步FIFO或同步器后设为伪路径 set_false_path -from [get_cells coarse_cnt_reg*] -to [get_cells coarse_q_reg*] # 6. 锁定进位链位置保证链在物理上连续、不弯折 # 下方Pblock将96个CARRY4限在相邻SLICE列 create_pblock pblock_tdc_chain add_cells_to_pblock pblock_tdc_chain [get_cells -hierarchical {tdc_inst/chain_gen*}] resize_pblock pblock_tdc_chain -add {SLICE_X100Y150 SLICE_X103Y157}CLOCK_DEDICATED_ROUTE FALSE这一句尤其关键。STOP信号如果不加这个Vivado会坚持要求它必须走专用时钟网络否则直接报错。但TDC里STOP确实是从外部进来的异步脉冲走BUFG到全局时钟网络已经能满足绝大多数场景这个属性就是告诉工具我知道我接的是时钟网络不用你再检查规范路径。Pblock的作用则是把96个CARRY4锁进4列SLICE范围内每列24个SLICE刚好容纳96个CARRY4因为每个SLICE有2个CARRY4每列SLICE数24的话一个SLICE里放了2个共48个CARRY4这里需要更细致的设计。实际一个SLICE列上同一个SLICE里的两个CARRY4是纵向排列的而不同SLICE也是纵向排列所以一个SLICE列就能放很多CARRY4。Pblock主要目的是限制链条在一个连续街区不要被其他逻辑打断。若要精确到每个CARRY4的位置建议更小的Pblock甚至手动R LOC但手工R LOC的工作量很大项目早期不推荐。4.3 亚稳态TDC的职业病温度计码信号在STOP沿到来时START可能正处于某个进位级中间所以每个采样触发器都有概率进入亚稳态。两级同步器只能降低亚稳态传播概率却不能修复已经采错的数据。这就是气泡修复之所以重要的原因——它属于逻辑层面的纠错。实际工程中我会在采样寄存器后加两级同步reg [TAP_WIDTH-1:0] sync1, sync2; always (posedge stop_clk) begin sync1 sampled_taps; sync2 sync1; end第一级sync1可能亚稳态第二级sync2基本稳定。虽然sync2相比真实相位可能差一个周期但时间戳本来就是相对量系统校准可以吸收这个固定偏差。真正要注意的是不要指望同步器后还能保留皮秒级的时间信息时间信息只存在于锁存瞬间的温度计码里同步器只负责消除亚稳态影响。4.4 约束后的实现检查写完约束跑完implementationVivado时序报告一定会有很多violation。我的经验是重点关注以下几类时钟树偏斜是否太大STOP BUFG到各采样触发器的skew应控制在几十ps以内过大说明局部时钟布线不合理需要调整Pblock位置或换BUFG。进位链是否被综合工具拆散打开Device视图确认96个CARRY4在物理上是连续排列的没有中间插入其他模块的逻辑。异步路径是否被Ungroup破坏如果设计中加了某些OPT_DESIGN指令可能把链打散这时要用DONT_TOUCH属性保护(* DONT_TOUCH true *) wire [TAP_WIDTH-1:0] taps_raw;DONT_TOUCH是TDC设计的保命符忘加它综合器极有可能觉得这个纯走线的进位链没用而把它优化掉或抽走一部分逻辑去凑利用率。5. 码密度校准实操用随机事件测出每个bin的真实宽度5.1 为什么平均值骗人假设你觉得384个bin覆盖5ns所以每个bin约13ps那就掉进坑里了。CARRY4每一级的延迟并不相同甚至相邻两级之间的差异都可能达到好几ps更别提不同芯片、不同电压温度下的整体漂移。平均值只适合写汇报PPT工程上做时间测量必须知道每个bin的真实宽度所以要做码密度校准。码密度校准的原理非常朴素如果你输入大量均匀随机分布的时间间隔理论上每个bin拿到的命中次数正比于它的时间宽度。统计直方图后用系统总时间窗口去除总命中数得到每个bin的平均宽度再用单个bin命中数除以平均命中数就得到该bin相对宽度的比例。5.2 校准平台怎么搭要在Kintex-7上跑码密度校准建议按这个思路搭测试环境用信号源产生与系统时钟非相关的周期性脉冲作为START源STOP由另一个延迟可调的脉冲源提供或者直接用信号源的两个通道输出已知时间差。关键START源和stop_clk不能有固定相位关系。如果两者同源采样点会周期性地落在一两个bin上统计结果完全没用。系统连续采集大量时间戳几十万到几百万个在FPGA里先累加直方图存到BRAM后通过UART或以太网上传到PC分析。伪代码如下Python侧分析# analysis.py 简化版本 import numpy as np # fine_code 为 FPGA 上传的温度计码编码值数组 hist np.bincount(fine_code, minlength384) total hist.sum() # 系统时钟周期5ns链长384bin平均bin宽度 bin_mean_width_ps 5000.0 / 384 # 每个bin相对宽度的实际值 bin_width_ps hist / total * 5000.0 # DNL: 以LSB为单位的差分非线性度 DNL_LSB (bin_width_ps - bin_mean_width_ps) / bin_mean_width_ps # INL: 积分非线性度累计加和 INL_LSB np.cumsum(DNL_LSB)5.3 校准查找表的构建拿到每个bin的实际宽度后在做高精度测量时就不能再把bin序号直接当时间用了。最简单的校正是构建一个累积查找表第k个bin的实际时间位置等于前k个bin宽度之和。测量时温度计码找到交界bin序号去查表得到精确时间。更精细的办法是拟合一条二阶或三阶曲线消除INL的整体趋势但残留的逐bin抖动还是只能用查找表。实践下来查表法最稳、最快FPGA里用一块BRAM就能存下384个16 bit的校正值每次测量做一次查表加法即可不会成为吞吐瓶颈。5.4 温度多点校正是躲不掉的CARRY4的延迟受电压和温度影响很大实测数据里从25°C升到85°Cbin宽度的平均漂移可能在5%到15%之间单bin抖动更大。所以只做一次25°C校准是不够的。工程上我一般做两组基础校准出厂25°C常温校准标定绝对偏移系统内嵌温度传感器如XADC监温在不同温度区间切换不同的校正查找表。实在不想存多张表也可以用线性外推因为温度漂移在常见工作范围内近似线性。这里给个建议如果你做的是实验室样机每次上电做一次短时码密度校准用时几秒能显著提升测量重复性。如果是量产设备必须把校准参数固化每种温度等级单独标定。6. 实测数据复盘分辨率、死时间与温度漂移的工程代价6.1 同一块板子上的实测结果我用的是一块XC7K325T-2FFG676开发板系统时钟200 MHz96个CARRY4构成的384 bin进位链外部信号源产生的脉冲对作为START/STOP。在恒定25°C、电源干净的条件下采集200万事件做码密度统计结果如下指标实测值备注平均bin宽度13.1 ps不同板卡间有±1.5 ps差异单bin最小/最大宽度7.2 ps / 19.8 ps制造偏差明显DNL范围-0.72 ~ 0.9 LSBRMS约0.3 LSBINL范围±3.5 LSB对应约±46 ps ±单次测量分辨率RMS~18 ps包含量化噪声与时钟抖动死时间~12 ns受输出FIFO深度和编码延迟限制单次测量分辨率18 ps这里要说明不是每个bin平均宽度13 ps分辨率就一定到13 ps。因为DNL较大时相邻两级的宽度差异会造成较大的单点误差。实际统计的RMS包括bin宽度的非均匀性、STOP时钟抖动约5到8 ps RMS、粗计数器跨时钟域引入的不确定性。工程上有效分辨率用多次重复测量同一时间间隔的标准差来评估而不是用bin宽度平均值。6.2 温度漂移的实测曲线把板子放进温箱从-10°C到85°C每隔15度做一次码密度校准。观察到平均bin宽度从13.1 ps缓慢增大到14.8 ps变化幅度约13%。这直接导致如果只做一次常温校准时间戳在85°C环境下可能产生数百ps的累计误差。所以前面强调的温度补偿不是学术洁癖而是实测逼出来的需求。6.3 调试中踩过的几个大坑给所有想做TDC的兄弟提个醒这些坑我全踩过电源纹波是隐形杀手TDC对电源电压变化极其敏感。FPGA内核电压上哪怕几十mV的纹波都会直接体现在bin宽度抖动上。建议TDC相关逻辑所在区域的供电单独走LDO磁珠滤波测量期间不要有大电流外设切换。进位链附近别塞高速串行逻辑GTX或者LVDS接口高速翻转时地层噪声会通过衬底耦合进位链导致测量结果周期性抖动。实测中离GTX收发器较近的进链条DNL会明显恶化。start_signal输入管脚布局很关键外部脉冲从IOB到CARRY4的CIN这段路径如果绕了远额外走线延迟会进测量值。给START分配靠近进位链位置的IOB或者至少保证它在每次实现后延迟保持一致。综合实现版本别乱升级同一个工程在Vivado 2018.3和2020.2两个版本下实现进位链布局可能完全不同。项目开始后锁定Vivado版本否则你辛辛苦苦校准的查找表全得重来。复位信号要放在进位链采集逻辑外面千万别用系统复位去复位采样触发器复位网络本身就是大噪声源。TDC采集逻辑应该只依赖stop_clk沿工作。6.4 和专用TDC芯片的最终对比如果你问做完这个方案后我会不会在量产项目里用FPGA TDC替代专用芯片我的答案是看场景。FPGA TDC的优势是通道数多、读写带宽高、实现灵活适合做多通道数据采集系统劣势是绝对精度受温漂、电源、制程影响更大需要投入精力做校准。面对单通道、对价格敏感的消费级产品TDC-GPX这类芯片确实省心但面对几十通道并行触发、要求ms级数据吞吐的科研装置FPGA TDC几乎是唯一现实的选择。写在最后的经验如果你打算从零开始复现我建议按这个顺序走先把CARRY4链条、采样和编码跑通用示波器看温度计码是否稳定然后加约束、上Pblock这一步能明显改善DNL等波形稳了再写码密度校准和上位机分析最后才去考虑多通道和温度补偿。别想一口吃成胖子因为每多一级处理排查时序诡异问题的难度就会翻倍。我个人在实际操作中最深刻的体会是FPGA TDC真正难的不是写代码而是让硅片的物理行为变得可预测。那96个CARRY4摆在那里十几皮秒的差异就在毫厘之间调试时与其盯着仿真波形不如多看看真的在芯片上发生的事。反复的校准和验证看起来是最笨的功夫恰恰是精度最好的朋友。