
1. 从原理到实现CARRY4进位链TDC的整体设计思路1.1 TDC是干嘛的为什么要用FPGA做时间数字转换器简称TDC干的事情就是用数字电路去测两个信号沿之间的时间差。这个需求在激光测距、PET断层扫描、核物理探测器读出、超声成像、粒子飞行时间谱仪这些场景里到处都是。比如测一束激光从发射到反射回来的时间光速是3×10^8米每秒1纳秒对应30厘米要达到毫米级的距离分辨率时间测量精度就得做到皮秒量级。这已经远超普通计数器加时钟能搞定的范围必须用专门的时间扩展、游标法或者延迟线插值技术。FPGA做TDC并不是新鲜事Xilinx和Altera的器件里早就有人这么干了。FPGA的优势在于一是片内资源充足可以同时做多通道TDC二是逻辑可重构需要改死时间、测程或者分辨率时不至于重新画板子三是和后续数字信号处理可以直接焊在一起不需要高速ADC再加一堆分立元件。我之前在一个激光测距项目里用过专用TDC芯片比如ams的TDC-GP22精度确实不错但价钱不便宜通道数也固定。后来转到FPGA方案用CARRY4进位链搭TDC实现在Kintex-7上用100多毫瓦的动态功耗换来了亚百皮秒级的单次精度而且通道数想扩就扩灵活性完全不在一个量级。标题里提到的CARRY4就是Xilinx 7系列FPGA里的专用进位链单元。Xilinx从Virtex-2开始就在每个Slice里放了专用的进位逻辑用来实现快速的加法器、计数器等算术运算。TDC的核心思路之一就是利用这种专用进位链的物理延迟作为“时间游标”配合同步采样寄存器把信号到达的时间量化成延迟线上的位置。用日常的话说进位链就像一把极细的尺子每一小格就是一个CARRY4单元的延迟信号沿走过多少格我们就知道它晚到了多长时间。Kintex-7上CARRY4的级间延迟大概在30到80皮秒之间温度电压工艺角不同会有浮动但足够我们做百皮秒以下的时间分辨。1.2 为什么选择CARRY4进位链而不是其他方案FPGA上做TDC还有几种常见思路。一种是用时钟相位插值比如用MMCM/PLL产生多相时钟对同一信号在多个相位下分别采样等效于把时钟周期分成若干份。这种方法实现难度低但分辨率受限于MMCM能输出的相位数量一般在几十到一百多皮秒且需要占用多个时钟网络资源测量死区也更大。另一种是用差分延迟线利用BUFG、IODELAY或LUT的布线延迟作为延迟单元这种方法布线难度高延迟值不单调校准麻烦。相比之下CARRY4进位链是FPGA内部专门为快速算术设计好的专用通路物理位置固定、延迟基本均匀、相邻单元耦合紧密在7系列器件里是最适合做抽头延迟线的资源。更关键的是CARRY4进位链和采样寄存器之间的布线有保障。FPGA的综合器能识别出进位链结构把它映射到专用的CARRY4单元里然后布线器会尽量保持进位链的物理连续性。这样我们不需要像手动布局布线那样去约束每一个LUT的位置只要在RTL里正确地写出结构再用合理的综合属性保护网络就能得到一条可用的延迟线。有人会问直接用LUT的布线延迟做延迟线行不行行是行但LUT的布线路径受布局影响极大两相邻寄存器之间的延迟可能非常不均匀想做到每个bin都稳定几十皮秒光约束就能把人逼疯。CARRY4是公共资源专用通道布线可控性要高得多。另外CARRY4进位链TDC还有一个我很看重的特点它在时间上的响应是单调的。信号进入进位链后沿着C0到O的输出逐级传播越靠后的抽头看到信号沿的时间越晚。只要进位链没有物理破损那么采样得到的温度计码thermometer code就是前面一串0后面一串1或者反过来边界位置直接映射时间。这就让编码器和校准器都变得非常简单。1.3 项目整体架构和关键指标规划在动手写代码之前先把这个项目的目标指标定下来。我这个设计的目标是这样的测量精度单次测量标准差single-shot precision小于100ps RMS多测几次平均后可以到20ps以下。测量范围单次测量不依赖外部计数器扩展的情况下约10ns加上粗计数器可以扩展到几十微秒。死时间这个直接决定系统最大事件率。延迟线加上编码器和FIFO的组合流水线处理下可以做到连续采样死时间很小。通道数一个通道占用一条CARRY4链加一小块编码逻辑Kintex-7 325T资源充足做8通道不成问题。架构上TDC模块分成三部分延迟线采样前端、编码器、以及校准FIFO。前端负责把输入信号沿映射成温度计码编码器把温度计码转换成二进制的时间戳校准FIFO配合软件完成逐bin校准和时间戳输出。这个思路和专用TDC芯片内部几乎一样只不过我们用FPGA把它搭出来逻辑完全透明哪里不对都能查。再说说为什么选Kintex-7而不是Artix-7或者Virtex-7。Artix-7的CARRY4结构和Kintex-7完全一致甚至延迟特性也接近区别主要在逻辑资源总量和高速收发器。如果只做单通道TDCArtix-7完全够用成本还低。但项目里同时还有LVDS接口采集、以太网回传、片上数据处理这些业务Artix-7的资源就显得紧张。Virtex-7的进位链延迟和Kintex-7差不多价格却高得多没必要。Kintex-7算是性能和成本之间的平衡点有足够的Slice放多通道TDC和业务逻辑CARRY4延迟特性在7系列里也比较成熟稳定网上可参考的资料多踩坑成本低。2. 硬件资源选型为什么是Kintex-7和CARRY42.1 CARRY4的内部结构与延迟特性想要用好CARRY4得先搞清楚它内部长什么样。Xilinx 7系列的每个Slice里有一个CARRY4原语它内部其实是4个级联的专用MUX进位单元各有输入端口S、DI、CYINIT以及输出端口O和CO。CARRY4被用来实现加法器、减法器、计数器时S端口接的是两个加数异或的结果DI接的是进位传播路径O是加法的和CO则逐级向下传递进位。TDC里我们不走传统加法器的用法而是把CARRY4当作纯粹的时间延迟元件来用。具体做法是把进位链的CIN端口当作信号输入端让被测信号从这个端口进入然后沿着进位链一级一级往下传在每级CARRY4的输出O端接入一个触发器的D端所有触发器共用同一个采样时钟。这样一来每个触发器捕获到的就是信号沿到达该级的时间。因为进位链级间延迟极小经过第N级的时间比经过第N-1级晚了大约一个bin所以所有触发器在同一时刻采样得到的值会形成一个边界清晰的温度计码。实现CARRY4延迟线有两种常见方法一种是在代码里直接例化CARRY4原语另一种是写一个大的加法器利用综合器自动推断进位链再用特定的网络名和KEEP属性保住结构。两种方法我都试过结论是例化原语的方式最可控适合对性能有严格要求的场景自动推断的方式代码简洁但容易因为综合优化剪掉中间节点还得费劲去加约束保住网络。本文后面给的例子是直接例化CARRY4因为这类项目里代码的可读性和可控性比省几行代码重要得多。在Kintex-7上一个CARRY4单元级联一个触发器的总延迟我实测下来通常在50皮秒左右具体值会随电压、温度、工艺批次变化。有人会以为这个bin越小越好其实未必。bin太小意味着抽头更多、延迟线长度更长编码器和校准的复杂度都会上升bin太大则分辨率下降。在Kintex-7上用默认布局单级延迟大概在30到80皮秒范围这个值对多数应用都够用。值得注意的是CARRY4的级间延迟并不完全均匀不同工艺角下差异会更明显所以校准是必须的后面会详细讲。2.2 理解测量范围、死时间和同步逻辑有了皮秒级的细刻度还得有个粗刻度来扩展测量范围。单独靠延迟线测时间量程就是延迟线的总长度。比如一条128级的进位链每级50皮秒总长度是6.4纳秒这在实际应用里太短了。所以通常的做法是把TDC和粗计数器结合粗计数器用系统时钟比如200MHz统计整周期数TDC负责在一个周期内做细分插值这样量程就不再受限于延迟线长度。这里的难点在于细时间戳和粗计数器必须引用同一个时基否则拼接时会出现周期边界不连续的问题。标准的做法是用一个自由运行的计数器它的时钟就是TDC的采样时钟。当START信号到达时我们锁存计数器的值和TDC的温度计码当STOP信号到达时再锁存一组。两个事件的粗计数值差值乘以周期加上TDC测得的周期内细相位差就是两个事件之间的总时间。只要采样时钟稳定这个拼接是严格线性的。死时间简单说就是TDC每处理完一个事件所需要的最小间隔。延迟线TDC的死时间主要取决于两个因素编码器从温度计码算出时间戳需要几个时钟周期以及FIFO是否足够深来缓冲突发事件。如果事件率不高比如每秒几万个脉冲直接用寄存器打拍再做编码死时间可以压到1到2个时钟周期。如果要做连续采样就得用流水线架构前一级还在编码的时候后一级已经在采新数据了死时间进一步压缩。我在设计里采用的是触发后立即锁存然后利用流水线做编码实测事件率到1MHz以上仍然稳定工作。这里有一个潜在问题必须提前说明采样时钟和输入信号之间的关系。TDC的采样时钟频率可以不高比如200MHz但采样时刻必须非常精确。采样寄存器在时钟沿到来时同时去采延迟线上所有抽头的电平值如果信号沿正好在采样沿附近到达某些触发器就会进入亚稳态采到的值不确定反映在输出上就是温度计码边界附近出现“气泡”比如不是严格的000111而是001011。处理亚稳态的标准思路是在编码器前面加一级同步寄存器再配合气泡消除逻辑这样可以把亚稳态的影响控制在极小的概率范围内。2.3 为什么延迟线TDC需要校准只要用过TDC的人都知道校准环节绕不开。原因很简单每个CARRY4的物理延迟不可能完全一致抽头之间的延迟也不是随温度线性变化的。不同批次甚至同一芯片不同区域的硅片特性都会有差异。直接按理论值去把温度计码换算成时间误差会累积非线性度可能达到好几个LSB。校准的本质是通过已知时间间隔的测量反推出每一个抽头的实际延迟宽度然后把这些宽度补偿进输出值里。校准的方法有很多简单一点的用已知频率的稳定信号源比如板上的晶振输出作为测试脉冲复杂一点的用码密度法code density test向TDC输入大量随机时间到达的脉冲统计每个bin被命中的次数因为随机到达时间在统计上均匀覆盖每个bin的统计次数应当近似相等偏差就反映了该bin宽度的不均匀。我两种都试过码密度法统计上更稳定但要保证输入脉冲确实均匀随机。工程上更常用的其实是“两步法”先用码密度法估计每个bin的宽度再用一个已知时间间隔做绝对校准确定整体时间基的偏移。校准结果可以存进BRAM里的查找表编码器查表输出最终时间戳速度完全跟得上。在Kintex-7的实际测试里不校准时同一时间间隔的重复测量结果标准差大概在100到200皮秒之间做成逐bin校准后能压到50到80皮秒。再结合多次平均精度可以进一步改善。这个提升幅度让我觉得校准逻辑虽然占点资源但绝对是值得的。3. 手把手搭建RTL代码、综合属性与延迟线实现3.1 直接例化CARRY4还是让综合器自动推断写RTL之前先定一个原则延迟线部分我建议直接例化CARRY4原语不要依赖综合器的推断。原因有两条。第一自动推断的进位链虽然是合法结构但综合器可能基于优化策略修改S、DI的输入连接有些中间节点会被合并或重命名导致我们无法准确地把抽头映射到预期的物理位置网络名也可能丢失。第二直接例化CARRY4时每个原语的端口连接完全由我们控制综合器只做映射不做优化这给后面的布局布线和调试都提供了最大的确定性。当然直接例化CARRY4也有缺点代码稍微啰嗦一点尤其是延迟线做得很长的时候200级就要写200个实例。这种场景下建议用generate语句批量生成代码既清晰又容易维护。下面这一段就是我在工程里用的简化版CARRY4延迟线生成代码// TDC delay-line front-end using CARRY4 primitives // N_TAPS number of CARRY4 cells, each cell provides 4 taps module tdc_delayline #( parameter N_TAPS 32 )( input wire i_clk, // sampling clock input wire i_signal,// signal under measurement output wire [4*N_TAPS-1:0] o_taps // thermometer code output ); wire [4*N_TAPS:0] carry_w; // carry chain wires assign carry_w[0] i_signal; genvar g; generate for (g 0; g N_TAPS; g g 1) begin : gen_carry CARRY4 #( .INIT(4b0) ) u_carry4 ( .CO(carry_w[4*g4 : 4*g1]), // carry out [3:0] .O(o_taps[4*g3 : 4*g]), // sum out [3:0] - to FF .CI(carry_w[4*g]), // carry in .CYINIT(1b0), .DI(4b0000), .S(4b1111) ); end endgenerate // sampling registers (* KEEP TRUE *) reg [4*N_TAPS-1:0] taps_q; always (posedge i_clk) begin taps_q o_taps; end assign o_taps_synced taps_q; endmodule这段代码里有几个细节值得注意。CARRY4的S端口接4b1111DI接4b0000这样每个进位单元的输出O就完全由进位输入CI决定O等于CI信号可以无阻碍地逐级传下去。CYINIT接地保证第一级的CIN是0被测信号从外部接到第一级的CI上。看起来有点绕但这是Xilinx推荐的做法让进位链成为一个纯延迟通路。另外CARRY4本身输出的O端口是组合逻辑必须经过触发器采样才能得到温度计码所以我用了一组寄存器在采样时钟沿打拍。这些寄存器的位置很关键综合器必须把它们放在紧邻CARRY4的Slice里否则布线延迟会把bin宽度搞得很不均匀。为了保险我加了KEEP属性把o_taps网络名固定住防止综合器重名或优化。3.2 把信号送进进位链输入缓冲与沿调整被测信号进入CARRY4之前必须经过合理的缓冲。直接用一个外部引脚信号去驱动进位链的CI端口在时序上可能有问题因为外部引脚到内部逻辑的路径很长且受IOB寄存器的摆放影响。更稳妥的做法是先把输入信号经过一个IBUF或IBUFDS送到片内再用BUFG或直接逻辑扇出到进位链。最高频的外部信号建议用IBUFDS加差分输入抗干扰能力好很多。还有一个容易被忽视的点TDC测的是“沿”的时间不是电平的时间。所以进入进位链的信号最好是边沿触发脉冲比如一个高电平持续几个纳秒的脉冲或者干脆就是把被测沿转换成一个极窄的脉冲。有些设计会先用逻辑把输入信号转化成符合进位链传播特性的脉冲比如用异或门把输入沿转换成单脉冲或者用D触发器产生一个宽度等于一个时钟周期的enable信号再去驱动进位链。这个转换逻辑放在进位链之前不会消耗进位链的抽头但会引入固定的附加延迟校准的时候需要把这部分减掉。Kintex-7的IO资源极其灵活输入延迟校准用IODELAY还能额外调整亚纳秒级别的相移。不过在我的项目里输入信号直接进FPGA引脚经过IBUF后送到CARRY4链固定延迟在几个纳秒量级对于最终时间差测量来说只要两个通道的输入路径对等这个固定延迟不会影响精度。真正需要注意的是两个被测信号之间的输入通路延时差比如START走IBUF左边、STOP走IBUF右边布线差异会造成固定偏差这个可以通过软件减去一个offset来补偿。3.3 采样时钟的选择与同步寄存器策略TDC的采样时钟我建议直接用板上低抖动晶振驱动的MMCM/PLL输出频率不需要太高100到250MHz都行。采样时钟的本质作用是为所有抽头寄存器提供一个同时刻的采样沿。时钟沿的抖动会直接影响单次测量的标准差所以时钟源的质量很关键一般板上集成晶振的抖动在几十皮秒已经能够满足百皮秒级的测量需求。如果有条件用超低抖动时钟芯片精度还能再往上走。时钟频率的选取影响的是粗计数器和延迟线重触发的节奏并不直接影响分辨率。分辨率由进位链bin宽度决定不是由采样时钟决定。这一点很多入门者会搞混总以为采样时钟越快精度越高其实不是。采样时钟只要保证在信号经过整个延迟线之前完成采样并且下一轮采样到来之前进位链里已经没有残余信号就行。如果采样时钟太高可能采样到同一个信号多次导致输出混乱。同步寄存器策略上我的做法是延迟线输出先经过一级寄存器打拍延迟线内部不开异步复位因为TDC的前端最关键的要求是面积小、延迟均匀异步复位反而给布局布线增加负担。打拍后的信号进入编码器时再做一级同步处理这级同步寄存器可以加复位因为此时信号已经远离模拟前端不再需要那么苛刻的物理约束。这里要说一个很多人踩过的坑直接拿延迟线的原始输出去做编码和判断非常容易采到亚稳态偶尔输出一个完全错乱的时间戳。正确的流程是延迟线 → 采样寄存器 → 同步寄存器 → 编码器 → FIFO。多出的一个时钟周期换来的是稳定得多的时间戳。4. 编码器设计、气泡消除与校准策略4.1 从温度计码到二进制时间戳延迟线上128个抽头采样出来的结果是一个128位的向量。理想情况下被测信号沿在进位链中传播到达的顺序是从第0级到第127级逐级变晚。采样时刻来临时已经传播到的级输出为1没传到的那部分输出为0于是我们得到一串形如“000...00111...111”的温度计码也有的设计方向相反。编码器的任务就是找出这个边界从哪一位开始0变成了1或者反过来这个位置就是信号沿在延迟线上的物理位置直接对应时间。最简单的编码方式是循环移位扫描但速度慢。实用的做法是用优先级编码器一行代码搞定always (*) begin boundary 0; for (int i 0; i WIDTH; i) begin if (thermo[i] 1b1) boundary i; end end这段Verilog综合出来就是一个带优先级的查找结构延迟大约在7到10个LUT级别几个纳秒内就能算出结果。如果通道数和事件率都很高还可以用树形编码器结构把整个编码时间压缩到一个时钟周期内。编码器输出的是抽头位置不是时间值。位置乘以平均bin宽度才是粗略时间。更精确的做法是查校准表把每个位置映射成一个校准后的时间值。编码器设计上有两个要点。第一是边界检测的单调性如果温度计码出现多个边界比如“0011011”说明有气泡需要先做气泡消除再编码。第二是编码器的输出时序对高速事件流编码器的组合逻辑延迟不能拖垮整个流水线的节奏必要时可以在编码器后加一级输出寄存器。4.2 气泡消除别让亚稳态毁掉你的测量为什么温度计码会出现气泡根源在于进位链内部的延迟并不严格相等个别bin的延迟可能比平均值小很多。当信号沿传播到这些特别快的bin时它可能在短时间内跨越两级甚至三级采样寄存器采到的结果就可能是“0、1、0”这种不连续的模式。另外亚稳态也会造成单个寄存器的输出在采样窗口内跳动。处理气泡的经典方案有两种。一种是把延迟线的输出按3抽头一组做少数服从多数的表决再把表决后的码流送进编码器。这种方法能消除单比特气泡但对双比特气泡无能为力。另一种是专门设计边界查找逻辑从延迟线的中点开始向两侧扫描找到第一个“1→0”跃迁忽略其他位置的跳动。这种方法天然对气泡不敏感实现也不复杂。我个人在实践中更倾向于在编码器内部做一个状态机先查找第一个边界再继续往前扫描一小段比如16个bin如果这段里没有第二个边界就接受第一个边界位置如果有说明延迟线内部有异常输出一个错误标志用软件剔除这一笔。虽然逻辑多一些但能明显提高多通道系统的鲁棒性。在单通道系统里简单表决器其实也够用。我这里给的简化代码里用的就是基本的表决加编码wire [WIDTH-1:0] clean_taps; genvar v; generate for (v 1; v WIDTH-1; v v 1) begin : gen_bubble assign clean_taps[v] (taps_synced[v-1] taps_synced[v]) | (taps_synced[v] taps_synced[v1]) | (taps_synced[v-1] taps_synced[v1]); end endgenerate这段代码把连续三个抽头做多数表决每个输出都是局部多数能有效消除单比特气泡。边界处因为两侧一边全1一边全0表决结果会自然地把边界位置磨平一级但误差不超过一个bin校准后可以接受。4.3 校准方法详解码密度法实操步骤码密度法是我做TDC校准的首选方案。它统计大量随机输入脉冲落到每个bin的次数用这些计数来推断每一个bin的宽度。比如总共打了100万个随机脉冲如果延迟线所有bin宽度完全一致每个bin应该被命中约100万除以bin总数次。实际统计到的次数与平均值的偏差就反映了这个bin比平均值宽或窄的比例。具体的操作流程是用一个信号源产生随机时间脉冲输入到TDC的START端或者我们关心的输入端TDC持续记录每个抽样位置的命中次数。在Kintex-7的工程里我在BRAM里开了一个数组大小等于抽头数每次编码器输出一个位置就把对应的计数加一。跑完足够多的样本后把计数数组读出计算每个bin的权重系数生成校准查找表。这里要注意随机脉冲的产生不能和采样时钟有任何相关性否则统计偏差会周期性出现。我实测下来用LFSR加比较器产生的伪随机时间信号配合一段不相干的时钟统计效果还不错。校准后的TDC输出时间戳等于t sum_{k0}^{m-1} bin_width[k] t_offset其中m是边界位置bin_width[k]是第k个bin的校准宽度t_offset是整体时间偏置。在实际实现中我先把bin_width放大2的16次方倍取整存进BRAM编码器出位置后做一次累加查表输出的是定标时间戳精度可以到几十飞秒量级。校准表需要在温度变化超过几摄氏度后重新生成因为温度直接影响CARRY4的延迟所以工程上通常软件定时校准比如每10秒用片上温度传感器检查一次温度变化超过阈值就触发重新校准。5. 约束文件与工程实现别让布局布线毁掉你的精度5.1 XDC约束不长但每条都不能少编写TDC的约束文件核心目标是保护进位链的物理结构保证采样时钟边沿不会同时碰上延迟线的多个抽头以及防止综合器把网络名改掉。下面是我用的一个精简版XDC可以直接套进Vivado工程# 1. Constrain the sampling clock create_clock -name clk200 -period 5.000 [get_ports {i_clk}] # 2. False path between the carry chain and the rest of the logic # The arrival time of the input signal is unknown by design set_false_path -from [get_ports {i_signal}] # 3. Keep the delay-line register names for post-route inspection set_property KEEP TRUE [get_nets {tdc_delayline/gen_carry[0].u_carry4/O}] # 4. Multi-cycle path on the sampler to avoid false timing violations set_multicycle_path 2 -setup -from [get_ports {i_signal}] -to [get_cells {tdc_delayline/taps_q_reg[*]}] set_multicycle_path 1 -hold -from [get_ports {i_signal}] -to [get_cells {tdc_delayline/taps_q_reg[*]}]第1条约束是常规操作。第2条把输入信号和其余逻辑之间的路径设为false path是因为我们根本不需要让Vivado去分析TDC前端的建立保持时间——它的时序是由物理延迟而非逻辑关系决定的工具分析也分析不出真实结果只会报一堆假违例。第3条KEEP属性保护延迟线网络名。第4条多周期约束尤其重要如果不加Vivado会按默认的单周期建立时间去检查延迟线和采样寄存器之间的路径结果会发现“延迟线输出变化时间”距离采样沿太近报出大量violation但实际上那是正常工作状态。5.2 布局约束把CARRY4链路锁在同一个时钟区域让Vivado把整条进位链布局在同一个时钟区域是保证时延均匀性最重要的一步。如果进位链跨越了不同的Slice列采样时钟到各抽头寄存器的路径长度会明显不同bin宽度会变得参差不齐校准难度大增。Xilinx的CARRY4进位链是有方向性的从下往上延伸为了让链尽量保持在同一个时钟区域内我们可以把链的级数控制在一个Column的范围内。在Kintex-7的实际器件里一个Slice里有两个CARRY4一个CLB里有4个Slice所以一个CLB能提供8级进位。如果你要建一条128级的延迟线就需要16个CLB垂直连在一起。Vivado通常能自动把这种纯generate生成的进位链布局成连续的列但保险起见可以用pblock把延迟线相关的cell锁进一个狭长的区域。我在工程里用了下面这个约束效果很稳定create_pblock pblock_tdc add_cells_to_pblock pblock_tdc [get_cells {tdc_delayline}] resize_pblock pblock_tdc -add CLOCKREGION_X0Y0:CLOCKREGION_X0Y2这个pblock把TDC延迟线约束在相邻的几个时钟区域内。这样做的好处是让所有抽头寄存器的时钟路径尽量短、尽量一致同时还能避免布局器把延迟线打散到芯片各处。使用pblock之后可能有个小缺点如果pblock太小放不下所有逻辑布局会报错所以建议先用Vivado自动布局一版看实际资源占用再根据实际占用来画pblock。5.3 时钟等长调整采样时钟和被测信号的相对关系TDC的测量本质上是测量“信号沿到达各抽头的时刻”和“采样时钟沿到来的时刻”二者的相对关系。时钟沿本身从时钟引脚到各抽头寄存器的传播延迟如果不一致就等于在各抽头上叠加了一个不均匀的偏移。好在FPGA内部时钟树设计得很均匀同步寄存器之间的时钟skew通常在几十皮秒量级这对我们的测量影响不大。需要注意的是外部输入信号的路径。被测信号从引脚到进位链的输入端口经过IBUF和一定的布线延迟。这个延迟不是问题因为它对信号沿到达时间的影响是固定的可以在校准中扣除。真正需要关注的是两个不同输入信号比如START和STOP之间的引脚到进位链延迟差。如果两个信号走的是同一个IBUF-GT的两种引脚延迟可能比较均衡如果一个是普通IO另一个是时钟专用引脚延迟差就可能有几百皮秒。所以多通道TDC设计时建议所有被测信号都用同一类型、相邻位置的普通IO引脚并保证PCB上的走线长度尽量一致。在Vivado里可以用set_input_delay约束把输入到寄存器的路径做个初步限定但TDC的输入信号不经过任何逻辑直接进进位链时序分析工具对这个路径的分析模型本身不太准确所以我在实践中干脆把这条路径设成伪路径不做时序分析一切凭实测校准结果说话。当你在实现后的Timing Summary里看到一堆关于i_signal的violation不要慌那是预期内的只要不影响其他正常逻辑就可以忽略。5.4 综合属性设置避免综合器把延迟线改坏Vivado综合器默认会做很多优化比如合并重复逻辑、重命名网络、调整寄存器位置等。对普通逻辑这是好事但对TDC延时线来说某些优化可能会把CARRY4的结构给改掉。最典型的问题是综合器把O端口的信号合并到其他组合逻辑里导致抽头数量变少或者延迟特性发生变化。我的经验是在RTL里给关键网络加上(* KEEP TRUE *)属性在综合选项中关闭和进位链相关的重定时retiming优化。Vivado里可以这样配置综合设置里把-retiming关掉或者在XDC里给TDC模块加DONT_TOUCH属性。DONT_TOUCH的作用是告诉综合器和布局布线器这个模块内部的所有逻辑都不许优化完全按原样实现。这个属性在最关键的延迟线、编码器后端都可以加上代价是可能降低一点资源利用率和布局自由度但对TDC来说稳定性优先。set_property DONT_TOUCH TRUE [get_cells {tdc_delayline}]加完这个之后综合时间略有增加但所有CARRY4的位置基本保持代码里的顺序。我建议即便加了DONT_TOUCH也还是保留KEEP属性双保险。很多刚开始做TDC的朋友怎么约束都不对最后发现是综合器把负责抽头输出的O信号当成普通组合逻辑优化掉了整个延迟线功能上还在但物理结构已经不是预期的那样了校准怎么都拉不回来。6. 实测问题与调试实录没有示波器寸步难行6.1 输出卡死先查约束再查时钟我在调TDC时遇到的第一个问题是温度计码输出若干个小时一直是0。前前后后查了很久发现问题出在采样时钟根本没有到达寄存器上。原因是MMCM的输出没有被正确约束成时钟导致Vivado认为i_clk是普通信号综合时把采样寄存器的时钟引脚也当成了普通数据信号。这个问题很隐蔽因为你看到RTL逻辑完全正常但实现后的网表里触发器根本没有接时钟。遇到这种“输出卡死、逻辑貌似正常”的情况我的排查顺序是固定的打开综合后的原理图确认CARRY4有没有被正确例化再确认采样寄存器的CE和R引脚是不是意外接了常量最后确认时钟路径有没有进BUFG。如果看到CARRY4的输出O端口悬空或者被综合成其他逻辑基本就是综合优化干的好事。如果寄存器时钟端什么都没有那就是约束文件里时钟没定义对。一次典型的排查流程我会在Vivado的Device视图里选中一个采样寄存器看它输入的路径是否真的从CARRY4的O端口来。如果路径中间冒出了一个LUT说明进位链的输出被综合器处理过了这基本可以断定是KEEP属性没生效或者综合设置问题。6.2 非线性度超标问题多半在校准数据不干净有段时间我测出的时间戳总是有规律地波动查阅校准表发现问题在于码密度校准时的输入脉冲数量不够。码密度法的一个统计规律是每个bin的命中次数相对误差大致等于命中次数的平方根分之一。如果个bin平均只命中1000次相对标准差约为3%校准后的每个bin宽度误差就高达3%这显然不够。解决方案很粗暴增加样本量。我把样本量从10万提高到100万误差压到了1%左右。后来再上升到1000万次校准精度进一步提高但消费也多时间接近十几分钟。实际应用中100万样本量是我认为性价比最好的平衡点。另一个问题是校准用的随机脉冲本身不够随机。如果脉冲生成逻辑和采样时钟共用同一个PLL它们之间就可能存在固定的相位关系统计分布就不是均匀的。想要解决可以脱离采样时钟域用另一个独立晶振产生的时钟驱动LFSR让LFSR的输出脉冲和主采样时钟保持非整数倍的关系统计上均匀性会好很多。6.3 在实测中验证精度的几个实用技巧验证TDC精度我通常用两种方法。第一种是用任意波形发生器产生两个频率相同但相位延迟可调的脉冲信号分别接START和STOP然后调整相位差对比TDC测出的结果和示波器测到的实际相位差。这种方法能检测出TDC的线性度和单次精度但受限于信号源和示波器本身的抖动验证精度大约到几十皮秒级。第二种方法更严谨一点叫“码密度直方图法”。把START信号固定在STOP端加入随机时间脉冲连续测几百次画出直方图。如果TDC是理想的直方图应该近似均匀。实际测量中直方图会显示出每个bin的高度偏差通过这个能直观地看到延迟线哪些地方偏宽、哪些地方偏窄。和校准表互相印证能快速定位是哪一段进位链问题。我觉得调试TDC项目示波器是必备的最好带宽在1GHz以上。尤其是查信号进入延迟线的波形质量时你会发现原本方正的脉冲经过IBUF和布线后边沿变缓了如果这个问题不解决输入端的判别点抖动就会直接影响TDC的测量精度。在调试阶段我习惯把被测信号也引到一个空闲的IOB上通过示波器同时观察输入波形和TDC的输出时间戳能很快判断是不是前端信号质量问题。6.4 温度漂移问题避开频繁校准的坑TDC的进位链延迟随温度变化非常明显这是硅工艺的固有特性。我曾经在实验室24小时跑同一个测试一开始精度很好几个小时后发现测量值整体偏移了几十皮秒。后来查了片内XADC的温度发现实验室空调下午关了芯片温度升了大概10度延迟也跟着变了。应对温度漂移一个做法是用片上XADC实时监测芯片温度每变化1度就重新做一次快速单点校准。快速校准不需要再做完整的码密度扫描只需要用一个已知时间间隔的脉冲测出当前平均bin宽度然后把整张校准表按比例缩放即可。虽然不够精细但能把温度引起的整体偏移拉回来。另一个做法是尽量控制芯片功耗平稳片上温度变化平缓校准频率可以降低。对于精度要求特别高的场景可以考虑恒温或者在板级做温度补偿电路。我个人的体会是温度漂移这种事不能等到系统上线才发现。设计阶段就要把XADC的温度监测和校准触发机制预留好软件上做定时校准这样才能保证长时间运行下的稳定输出。附完整源码结构与约束文件的组织方式源码我习惯这样组织顶层模块里例化TDC核和FIFOTDC核内部再分前端延迟线、编码器、校准表查找三个子模块FIFO负责跨时钟域把时间戳送到后面的UART或以太网接口。这种情况下TDC核的输入信号是干净的START/STOP脉冲输出是32位时间戳加一个valid信号。工程里其他业务逻辑和TDC之间用AXI-Stream接在一起方便后续扩展。XDC文件按模块拆分成两个文件比较清晰。一个文件放时钟和IO约束另一个放TDC特殊约束KEEP、DONT_TOUCH、pblock、multicycle_path。这样当工程规模变大时查找定位问题会方便很多。Vivado会把同一个pblock区域内的cell摆放得比较紧凑但如果工程里其他逻辑特别挤也可以给Pblock增加一些余量让布局器有足够空间安置编码器和其他逻辑。再强调一点不要在TDC延迟线附近放大的组合逻辑块。哪怕物理上不相连Power密度高了也会造成局部温度升高间接影响延迟线精度。我习惯使用set_property POWER_OPT_DESIGN TRUE把无关逻辑自动压低功耗同时人为地把TDC的pblock放在芯片角落或温度较低的区域给测量精度创造尽可能好的物理环境。这个设计做下来最让我印象深刻的一点是在FPGA上做皮秒级时间测量真正决定成败的往往不是RTL逻辑写得有多花哨而是你是否真的理解了底层物理结构、布局约束和校准策略。只要你把CARRY4看成一把可编程的精密尺子把校准看成给这把尺子做刻度修正很多看似玄学的问题其实都能用工程方法解决。