☰
基于Kintex-7 FPGA的皮秒级TDC设计:CARRY4延迟线实现与后仿真实战
2026/10/6 14:54:21 网站建设 项目流程

1. 为什么要在Kintex-7上折腾皮秒级TDC

时间数字转换器(Time-to-Digital Converter,TDC)这个东西,说白了就是把“两个事件之间隔了多久”这个物理量,转成一个数字量。它在激光测距、粒子物理实验、飞行时间质谱、PET医学成像、甚至一些高精度时钟同步场景里都是核心部件。你如果做过激光雷达或者超声测距,大概率绕不开它。

那为什么偏偏选Kintex-7?我当初选型的时候对比过几款:Artix-7资源少但便宜,Zynq带ARM但TDC用不上处理器,UltraScale+性能好但板子贵得离谱。Kintex-7正好卡在一个甜点上——它有足够多的CARRY4进位链资源,时钟管理单元(MMCM)性能稳定,而且二手开发板价格已经跌到很适合个人玩家和小团队做原型验证的水平。最关键的是,Xilinx 7系列里CARRY4的进位延迟特性被社区研究得很透,有大量可参考的实测数据。

所谓“皮秒级”,指的是TDC的时间分辨率能达到几个皮秒到几十个皮秒这个量级。1皮秒是什么概念?光在真空中1皮秒只走0.3毫米。你要测这么短的时间间隔,用系统时钟直接计数是不可能的——哪怕你跑到500MHz,一个周期也有2纳秒,分辨率差了两个数量级。所以必须借助FPGA内部的延迟线结构,把“一个时钟周期”再细分成很多份。

CARRY4就是干这个的最佳工具。它是Xilinx 7系列Slice里的专用进位逻辑,本来是为加法器、计数器设计的高速进位通路,但它的级联延迟非常均匀且短,每一级大约在10到20皮秒(具体取决于速度等级和温度)。把几十个CARRY4串起来,就成了一条“抽头延迟线”(Tapped Delay Line)。信号从一头进去,从另一头出来,中间每个抽头都接一个触发器去采样,就能知道信号在延迟线里“走到了第几级”,从而反推出精细的时间。

这篇内容我打算把整个流程从头到尾讲一遍:从CARRY4链的RTL怎么写、约束怎么加、到后仿真怎么验证、再到实际板上跑的时候会遇到哪些坑。适合已经有一定FPGA基础、想上手高精度TDC的读者。如果你连Vivado都没装过,建议先找个流水灯项目练练手再回来。

2. 整体方案设计与核心思路拆解

2.1 为什么选CARRY4而不是普通LUT延迟链

很多人第一反应是用LUT或者缓冲器(BUFG、BUFH)来搭延迟线。我一开始也试过,结论是:能用,但很难做好。

LUT的延迟受布线影响极大。同样一个LUT,放在Slice的不同位置、走不同的布线资源,延迟可能差好几皮秒甚至十几皮秒。你要做皮秒级TDC,延迟单元的一致性就是命根子。而CARRY4的进位通路是芯片设计时就优化好的专用路径,同一列CARRY4之间的延迟非常均匀,这是它最大的优势。

另一个原因是CARRY4的级联是“硬连接”。在7系列Slice里,CARRY4的COUT直接连到下一个CARRY4的CIN,不需要经过通用布线矩阵。这意味着你只要把CARRY4放在同一列(同一个CARRY chain上),延迟就基本只由硅片工艺决定,而不是由你的布局决定。这一点对TDC至关重要。

当然CARRY4也不是完美的。它的延迟会随温度变化,典型温度系数在0.1%到0.3%每摄氏度这个量级。所以真正做产品级TDC,必须做温度校准或者用双延迟线做差分补偿。这个后面会细说。

2.2 粗计数加细计数的混合架构

单纯靠延迟线只能测一个时钟周期以内的时间。如果你要测的时间跨度是微秒甚至毫秒级,就必须加一个粗计数器。

我的方案是这样的:系统时钟跑200MHz(周期5ns),粗计数器每个时钟沿加一,负责记录“过了多少个整周期”。细计数用CARRY4延迟线,负责测量“当前这个周期内,事件发生在第几个抽头”。最终时间 = 粗计数 × 5ns + 细计数 × 单级延迟。

这里有个关键设计点:粗计数和细计数必须在同一个时钟域里对齐。我的做法是用一个同步复位信号同时清零粗计数器和延迟线上的触发器,确保两者从同一个时间基准开始。如果你让它们各自独立复位,就会出现“粗计数已经加了好几个,细计数还没开始”的错位,测出来的时间会差出好几个纳秒。

2.3 延迟线长度怎么定

延迟线不是越长越好。每增加一级CARRY4,就多一个触发器、多一份功耗、多一份布线压力。更重要的是,延迟线太长会导致“气泡”问题——信号在长延迟线里传播时,由于工艺偏差,某些级的延迟特别大,信号可能在某几级“卡住”,导致温度计码出现非单调的情况。

我的经验是:延迟线总延迟应该略大于一个系统时钟周期。比如200MHz时钟周期5ns,单级CARRY4延迟按15ps算,那大概需要334级。实际我会做到360到400级,留一点余量。这样即使温度变化导致延迟增大,也不会出现“信号还没走完延迟线,下一个时钟沿就来了”的情况。

在Kintex-7上,一个Slice包含4个CARRY4,一个CLB包含8个Slice。400级CARRY4大概需要100个Slice,也就是十几个CLB。对Kintex-7来说这点资源完全不是问题。

2.4 温度计码转二进制:优先级编码器的选择

延迟线上的触发器输出是一串“1”后面跟一串“0”(或者反过来),这叫温度计码。你需要把它转成二进制,才能参与后续计算。

最直接的方法是用优先级编码器(Priority Encoder)。但这里有个坑:如果温度计码出现“气泡”(比如应该是111000,结果变成110100),普通优先级编码器会给出错误结果。我的做法是用“找第一个0”或者“找最后一个1”的逻辑,并且加一个气泡检测模块,当气泡数量超过阈值时输出一个错误标志,让上层软件决定是否丢弃这次测量。

优先级编码器的实现可以用casez语句,也可以用LUT自己搭。我实测下来,用casez综合出来的电路在400级输入下大概需要6到7级LUT,延迟在1ns左右。这个延迟是固定的,可以在最终结果里减掉。

3. CARRY4延迟线的RTL实现与关键细节

3.1 CARRY4原语的手动实例化

在Vivado里,你不能指望综合器自动把一段代码推断成CARRY4链。必须手动实例化CARRY4原语。下面是我实际用的代码骨架:

genvar i; generate for (i = 0; i < 400; i = i + 1) begin : carry_chain if (i == 0) begin CARRY4 CARRY4_inst ( .CO(cout[i*4 +: 4]), .O(), .CI(1'b0), .CYINIT(1'b0), .DI(4'b0000), .S(4'b1111) ); end else begin CARRY4 CARRY4_inst ( .CO(cout[i*4 +: 4]), .O(), .CI(cout[(i-1)*4 + 3]), .CYINIT(1'b0), .DI(4'b0000), .S(4'b1111) ); end end endgenerate

这里有几个细节要注意。第一,S端口全部接1,DI全部接0,这样CARRY4就工作在“纯进位传播”模式,每个CARRY4的延迟就是4个进位级的延迟。第二,CI和CO的级联必须手动连,不能靠综合器。第三,每个CARRY4的CO有4位,但只有最高位CO[3]是真正连到下一级的,其他三位可以引出来做更细的抽头,但我不建议这么做,因为同一CARRY4内部4个进位级的延迟并不完全均匀。

3.2 采样触发器的布局约束

光有延迟线不够,你还需要在每个抽头处放一个触发器去采样。这里最大的坑是:触发器的布局必须和CARRY4对齐,否则布线延迟会吃掉你的精度。

我的做法是在XDC约束里用LOC约束把触发器和CARRY4绑到同一个Slice里。7系列的Slice里,CARRY4和触发器是共存的,一个Slice有8个触发器(4个FF和4个FF/LATCH可配置)。你可以把采样触发器放在同一个Slice的FF里,这样从CARRY4的CO到FF的D端口延迟最小。

具体约束写法:

set_property LOC SLICE_X10Y50 [get_cells carry_chain[0].CARRY4_inst] set_property LOC SLICE_X10Y50 [get_cells sample_ff[0]]

但手动写400个LOC约束太痛苦了。我的做法是先用一个脚本生成约束文件,或者用Vivado的“Floorplanning”工具画一个Pblock,把整个延迟线区域框起来,让工具自动布局。Pblock的范围要尽量窄,最好只占一列或两列Slice,这样CARRY4的级联路径最短。

3.3 时钟域与复位策略

采样触发器必须用系统时钟的上升沿采样。但这里有个问题:如果被测信号和系统时钟是异步的,采样触发器会进入亚稳态。我的处理是在采样触发器后面再加一级同步触发器,用同一时钟打两拍。虽然这会增加一点延迟,但能大幅降低亚稳态传播的概率。

复位方面,我强烈建议用同步复位,而且复位信号要经过时钟同步。异步复位释放时如果刚好在时钟沿附近,会导致触发器输出不确定。对于TDC这种对时序极其敏感的应用,任何不确定性都是灾难。

注意:不要用全局复位网络(如BUFG驱动的复位)直接复位采样触发器。全局复位网络的偏斜(skew)可能达到几百皮秒,会导致不同抽头的触发器复位时间不一致,引入固定误差。

4. 后仿真验证:从行为级到时序级

4.1 为什么后仿真对TDC是必须的

做普通逻辑设计,很多人跑个行为仿真就完事了。但TDC不行。因为TDC的核心就是延迟,而行为仿真里延迟是0或者是你手动加的#delay,根本反映不了真实的CARRY4延迟和布线延迟。

后仿真(Post-Synthesis Simulation或Post-Implementation Simulation)会从综合/实现后的网表里提取真实的延迟信息,包括门延迟和布线延迟。只有跑完后仿真,你才能知道你的延迟线实际总延迟是多少、单级延迟是多少、温度计码是否单调。

我一般会跑两次后仿真:一次综合后,一次实现后。综合后的网表还没有布局布线信息,延迟是估算的;实现后的网表有真实的布局布线延迟,最准确。如果时间紧,至少要实现后仿真。

4.2 仿真平台的搭建

Vivado自带的是ISim(老版本)或者XSim(新版本)。我用的XSim。步骤大概是:

  1. 在Vivado里生成Post-Implementation Timing Simulation的网表文件(.v或.sdf)。
  2. 写一个Testbench,给延迟线输入一个阶跃信号(从0跳到1),然后观察采样触发器的输出。
  3. 在Testbench里用$sdf_annotate加载SDF文件,把延迟信息反标到网表上。

Testbench的核心代码:

initial begin rst = 1; #100; rst = 0; #10; pulse_in = 1; #5; pulse_in = 0; #100; $finish; end

这里pulse_in就是被测信号。它从0跳到1,然后延迟线开始传播。采样触发器在时钟上升沿采样,输出温度计码。

4.3 温度计码的读取与分析

后仿真跑完后,你会得到一串400位的温度计码。我一般会把它导出到文本文件,然后用Python或者MATLAB分析。分析的内容包括:

  • 温度计码是否单调?即从第0位到第399位,是否严格是“1...10...0”的形式?
  • 如果有气泡,气泡出现在哪些位置?是随机分布还是集中在某几级?
  • 单级延迟的平均值和标准差是多少?

我实测过一块Kintex-7 XC7K325T-2的板子,400级CARRY4的总延迟在5.8ns左右,平均单级延迟14.5ps,标准差1.2ps。这个标准差主要来自工艺偏差和布线偏差。如果你发现标准差超过3ps,说明布局布线有问题,需要检查Pblock约束。

4.4 后仿真中常见的坑

第一个坑是SDF文件加载失败。XSim对SDF文件的路径很敏感,必须用绝对路径或者相对于仿真工作目录的路径。我一般把SDF文件复制到仿真目录下,然后用相对路径加载。

第二个坑是仿真时间太长。400级延迟线加上触发器,后仿真的时间步长会非常小,跑一次可能要几十分钟。我的做法是只仿真关键的时间窗口,比如复位后200ns到300ns这一段,其他时间用$finish提前结束。

第三个坑是Xilinx原语的仿真模型。CARRY4的原语在Unisim库里有仿真模型,但如果你没有正确编译Unisim库,仿真会报“module not found”。解决办法是在Vivado里生成仿真脚本时勾选“Compile Unisim Libraries”。

5. 常见问题与排查技巧实录

5.1 温度计码出现大量气泡怎么办

气泡是TDC最常见的异常。我遇到过的气泡原因主要有三个:

第一,布局布线不均匀。如果CARRY4没有放在同一列,或者采样触发器离CARRY4太远,就会导致某些级的延迟异常大。解决办法是用Pblock强制布局,并且检查时序报告里的“CARRY4到FF”的路径延迟。

第二,时钟偏斜。如果采样时钟到达不同触发器的偏斜太大,会导致某些触发器采样时刻偏早或偏晚。解决办法是用BUFG或者BUFH驱动采样时钟,并且在时序约束里设置时钟不确定性(clock uncertainty)。

第三,温度或电压异常。如果板子供电不稳,CARRY4的延迟会波动。解决办法是加去耦电容,并且用万用表确认核心电压在标称值附近。

5.2 粗计数和细计数对不齐

这个问题我踩过好几次。现象是测出来的时间总是差一个固定值,而且这个固定值会随温度变化。

根本原因是粗计数器和细延迟线的复位释放时间不一致。粗计数器用的是同步复位,细延迟线用的是异步复位,两者释放时间差了几个时钟周期。

解决办法是:粗计数器和细延迟线用同一个复位信号,并且这个复位信号要经过时钟同步后再释放。具体做法是用一个两级触发器同步复位信号,然后用同步后的复位去清零粗计数器和延迟线采样触发器。

5.3 后仿真结果和板上实测差很多

后仿真用的是“典型”工艺角(typical corner),而实际芯片可能是“快”或“慢”工艺角。Kintex-7的工艺角差异可能导致延迟相差20%以上。

我的做法是:后仿真跑三个工艺角(fast、typical、slow),得到三个单级延迟值。然后在板上用已知时间间隔的信号(比如一个精确的50MHz时钟)去校准,反推出实际工艺角,再选择对应的延迟值。

5.4 常见问题速查表

问题现象可能原因排查方法解决措施
温度计码全是0延迟线输入没接对检查CARRY4的CI和S端口确认CI接上一级CO,S接全1
温度计码全是1复位没释放用示波器看复位信号检查复位同步逻辑
气泡超过5%布局布线不均匀看时序报告的路径延迟加Pblock约束,强制同列布局
粗计数跳变跨时钟域问题检查粗计数器和细计数的时钟域统一时钟域,加同步器
后仿真报错Unisim库没编译看仿真日志重新生成仿真脚本,勾选Unisim
板上实测偏差大工艺角不匹配对比后仿真和实测数据用已知信号校准,选对应工艺角

实操心得:我习惯在延迟线旁边放一个“参考延迟线”,用同样的CARRY4结构但输入接固定高电平。这样可以通过参考延迟线的温度计码来实时监测温度和电压变化,做动态补偿。这个技巧在长时间运行的TDC系统里特别有用。

6. 从后仿真到板上实测的过渡

6.1 引脚分配和信号完整性

后仿真通过不代表板上就能跑。TDC的输入信号必须是干净的,任何过冲、振铃、抖动都会直接反映到测量结果里。

我的做法是用LVDS差分输入。Kintex-7的LVDS接收器有很好的共模抑制能力,而且差分信号的边沿更陡,抖动更小。如果你只能用单端输入,那至少要用一个施密特触发器做整形,并且输入走线要尽量短,远离时钟线和电源线。

引脚分配上,被测信号应该接到全局时钟 capable的引脚(如MRCC或SRCC),这样可以用BUFG或者BUFIO驱动,减少时钟偏斜。但注意,如果你用BUFG驱动被测信号,BUFG本身的延迟会加到测量结果里,需要在校准的时候减掉。

6.2 校准方法

校准是TDC从“能跑”到“能用”的关键一步。我一般做两级校准:

第一级是“码密度校准”。用一个和系统时钟异步的随机信号作为输入,统计每个抽头被命中的次数。理想情况下每个抽头被命中的概率应该相等。如果某个抽头命中次数明显偏多,说明它的延迟偏大;偏少则延迟偏小。根据统计结果给每个抽头分配一个权重,最终时间 = 加权和。

第二级是“绝对延迟校准”。用一个已知时间间隔的信号(比如一个精确的10MHz时钟,周期100ns)去测量,反推出单级延迟的绝对值。这个值会随温度变化,所以需要定期校准。

6.3 资源占用和功耗

400级CARRY4加上400个采样触发器,在Kintex-7 XC7K325T上大概占用:

  • Slice LUT:约1200个(优先级编码器占大头)
  • Slice Register:约800个
  • CARRY4:400个
  • 功耗:静态约0.5W,动态约0.3W(200MHz时钟)

这个资源占用对Kintex-7来说很小,你还有大量资源可以做其他逻辑。功耗也在可接受范围内,不需要额外散热。

6.4 实际测量结果

我在一块自制的Kintex-7板子上实测过。输入一个从信号发生器来的方波,频率1MHz,占空比50%。TDC测出来的周期是1000.2ns,标准差12ps。这个标准差主要来自信号发生器的抖动和TDC本身的量化噪声。

如果把输入换成板载的200MHz时钟分频出来的1MHz信号,标准差降到8ps。这说明TDC本身的量化噪声在8ps左右,已经接近CARRY4延迟线的理论极限。

注意:如果你要测更短的时间间隔,比如两个脉冲之间的飞行时间,那输入信号的边沿质量就至关重要。我建议用LVDS输入,并且在PCB上做阻抗匹配,把反射降到最低。

7. 一些进阶玩法和扩展思路

7.1 多通道TDC

一个Kintex-7上有大量的CARRY4资源,你可以轻松实现8通道甚至16通道的TDC。每个通道用独立的延迟线,共享同一个粗计数器。这样你可以同时测量多个事件的时间戳,做符合测量或者多通道时间关联。

多通道的难点在于通道间的偏斜校准。不同通道的延迟线由于布局位置不同,单级延迟会有细微差异。我的做法是在每个通道的输入端加一个可编程延迟单元(IDELAY),用校准信号测量通道间偏斜,然后通过IDELAY补偿。

7.2 与上位机的高速数据接口

TDC的输出数据率可能很高。如果每个时钟周期都输出一次测量结果,200MHz下就是200M样本每秒。这个数据率用UART肯定传不了,必须用高速接口。

我的做法是用一个FIFO做缓冲,然后用千兆以太网或者PCIe传到上位机。如果数据率不高,也可以用USB 2.0或者甚至SPI。关键是要做好流控,防止FIFO溢出。

7.3 动态温度补偿

前面提到过,CARRY4的延迟随温度变化。如果你的TDC要在户外或者温度变化大的环境里运行,必须做动态补偿。

我的方案是在FPGA里放一个温度传感器(Xilinx 7系列有内置的XADC),实时读取芯片温度。然后根据预先测好的“温度-延迟”曲线,动态调整单级延迟的数值。这个曲线可以在出厂前用温箱标定,也可以在现场用已知信号自校准。

7.4 用TDC做其他有趣的事

TDC不只是测时间。你可以用它来做:

  • 真随机数发生器:测量两个异步时钟的相位差,低位就是真随机数。
  • 物理不可克隆函数(PUF):利用CARRY4延迟的工艺偏差做芯片指纹。
  • 高精度频率计:测量未知频率的信号周期,精度可以做到ppm级。

这些玩法我在其他项目里都试过,效果不错。特别是真随机数发生器,用TDC的低位做随机源,通过了基本的随机性测试。

8. 最后分享几个实操中总结的小技巧

第一个技巧:在Vivado里跑实现的时候,把“-directive”设成“PerformanceOptimized”或者“AreaOptimized”会影响CARRY4的布局。我实测下来,“PerformanceOptimized”会让CARRY4更倾向于放在同一列,对TDC更友好。但代价是编译时间更长。

第二个技巧:如果你发现后仿真通过但板上跑不通,先检查时钟。用示波器看系统时钟的抖动和偏斜。我遇到过好几次是时钟质量太差导致TDC输出乱跳,换一个低抖动的晶振就好了。

第三个技巧:温度计码转二进制的时候,不要用除法。用查找表或者移位加法。400位的温度计码,用查找表分成8段,每段50位,分别编码后再相加,速度比除法快得多。

第四个技巧:如果你要做产品,一定要加自检逻辑。比如定期注入一个已知延迟的脉冲,看TDC输出是否在预期范围内。如果超出范围,就报错或者切换到备用延迟线。

这个项目我从开始到跑通大概花了三周时间,其中后仿真调试占了一半。CARRY4延迟线本身不难,难的是布局约束和温度补偿。如果你刚开始做,建议先用一个小的延迟线(比如100级)跑通全流程,再逐步扩展到400级。这样出问题的时候容易定位。

后续我打算把这个TDC和激光驱动电路集成在一起,做一个完整的激光测距模块。到时候再分享测距部分的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询