
1. TDC延迟链到底在测什么做激光雷达、ToF相机或者精密时间测量仪器的人基本都碰到过同一个问题如何把一个时间间隔量到足够准。买个专用TDC芯片当然省事但成本、通道数、接口灵活性未必合适。FPGA里用延迟链搭TDC是很成熟的路线配合码密度校准能把几十皮秒量级的时间测量做得很稳定。这篇文章就是我基于自己实际折腾过的延迟链TDC经验重点讲讲延迟链怎么优化、码密度校准怎么做以及校准过程中那些容易被忽略的坑。适合想在自己项目里实现高精度时间测量的FPGA工程师参考。1.1 一次测量拆成“粗计数细量化”我习惯把TDC理解成一把游标卡尺。参考时钟的整数倍计数是主尺延迟链是游标。用公式表示就是t_measured N_count × T_ref t_fine其中N_count是粗计数器在参考时钟下数出的周期数T_ref是参考时钟周期t_fine是延迟链测量出来的相位残差。粗计数器负责长量程延迟链负责高分辨率。这样链长只需要覆盖一个T_ref比如参考时钟200MHz、周期5ns链上的平均bin宽在50ps左右时链长大约100级实现起来很轻松。如果试图用纯延迟链去覆盖几十微秒的测量范围那链长会爆炸而且温度漂移会让标定变得非常不稳定。1.2 真正该关注的是DNL和INL我见过不少方案把分辨率写成“延迟链单级延迟xx ps”好像这样就能代表测量精度其实差别很大。TDC性能更关键的指标是DNL微分非线性和INL积分非线性。打个比方你把一条100米的跑道当成测量尺子误差源不只是每一米刻度是否准确还有每个刻度间长度差异累积起来造成的总偏差。DNL描述相邻bin宽度的波动INL描述累积到某个位置时的总偏差。对码密度校准来说最重要的产物就是把每段bin的真实宽度统计出来从而同时改善DNL和INL而不是靠把电路做到“平均”来假装精度。校准前后的数据对比往往非常直观校准前DNL可能达到±0.5 LSB甚至更大校准后能压到±0.1 LSB以内INL改善则更明显尤其对长延迟链来说累积误差会直接影响测距精度。1.3 延迟链的基本形态在FPGA里延迟链通常不是用一批LUT串联出来的而是用专用进位链实现。拿Xilinx 7系列举例每个CARRY4单元内部有四级进位逻辑级与级之间通过专用布线连接延迟小且一致性好。每级进位输出再接一个D触发器FDRE形成一个抽头。被测信号沿进位链传播在停止时钟沿到来时锁存整个链的状态。理论上链上会留下一个“信号已走到哪”的温度计码后续再用编码器换算成时间码。这条链的物理实现质量直接决定后续标定能修正多少误差。如果链本身乱得一塌糊涂靠算法去修也很难修到理想状态。所以先得把物理层做扎实然后再谈码密度校准。2. 延迟链优化决定测量质量的物理层功夫2.1 为什么进位链优于LUT级联设计延迟链时最大的坑就是用LUT来搭延迟线。LUT本身可以配置成缓冲器级联后也能产生几十到上百皮秒的延迟但问题在于每级LUT之间的布线路径并不固定工具在布局布线时经常会把两条相邻路径拉到不同长度的线网导致bin宽严重不均匀。更麻烦的是LUT级联的延迟受输入扇出、布线资源竞争影响很大综合结果不可控。进位链的好处是结构天然规整。CARRY4之间的CI到CO直接通过专用短连线衔接路径非常短进位链在芯片上呈列状分布只要把抽头触发器紧挨着对应进位输出放置就能在很大程度上消除线网延迟的不确定性。实际综合时我会尽量直接用原语例化进位链而不是写一段通用加法/比较逻辑让工具“碰运气”优化。2.2 布局与相对位置约束逻辑定了布局跟不上照样白搭。延迟链最重要的是保持“进位链相邻抽头寄存器相邻”的物理关系。如果你只是用RTL描述链行为然后让EDA工具自动布局大概率会得到一条扭曲的链有些抽头会因为布线绕远而让bin宽偏移几十甚至上百ps。工程上常用的做法是先用原语把进位链和抽头寄存器例化出来然后做区域约束把整条链限定在某一列或几列CLB内如果工具支持相对位置宏RPM把每个抽头寄存器的相对位置固定下来再给链上所有进位逻辑和寄存器打上综合属性防止它们被优化、合并或重定时。以Xilinx环境为例可以写这样的约束示意# 把链上所有例化单元划入同一个Pblock区域 create_pblock tdc_chain add_cells_to_pblock tdc_chain [get_cells -hier -filter {NAME ~ *chain_gen*}] # 或者直接对关键原语指定位置 set_property LOC CARRY4_X2Y10 [get_cells chain_gen[0].carry4_inst]写完约束后最好在布局后打开器件视图看一眼链上的CARRY4是否真的排成一列每个抽头FDRE是否紧贴在各自进位输出旁边。肉眼检查虽然土但能发现不少工具在约束不当时留下的奇怪布局。2.3 锁存时钟和复位处理延迟链的抽头锁存是时间测量的关键时刻任何时钟偏斜都会直接变成测量误差。锁存时钟最好走全局时钟网络保证在链的每一端相位一致。我曾经试过用普通本地信号做锁存时钟结果链前段和链末端的时钟偏差有几十皮秒实测直方图呈现出明显的周期性鼓包。复位信号也要小心。延迟链的锁存器我建议只在系统初始化时复位一次平时不要反复异步复位。异步复位对时序收敛不友好而且复位释放时刻如果不一致会造成某次测量正好卡在翻转点附近产生错误码。更稳妥的做法是采用同步复位或者在数据输出阶段做滤波。另一个容易忽略的点是延迟链抽头寄存器的复位值必须全部统一为0否则一旦复位温度计码会从链中间出现不连续的1编码器会直接懵。2.4 链长和余量的选择链长不是“预估需要多少级就精确到多少级”建议至少留出10%~20%余量。温度升高时延迟会增大极端情况下链上传播速度变慢原本刚好覆盖一个T_ref的链可能覆盖不满反过来低温下延迟变小信号可能提前走到头。可设计成让信号在链上继续传播末尾多出几级用于“吞掉”多余传播同时编码时只截取有效窗口。这样在实际应用中的温度适应性会好很多。我再补充一个经验链的输入级通常要加一级缓冲或整形逻辑避免被测信号经过封装引脚、IO逻辑之后变成边沿过缓、噪声很大的信号。很多第一次做TDC的人忽略输入信号调理直接拿外部脉冲怼到进位链入口结果链前几级老是误触发直方图前几个bin异常偏高。严格来说这部分属于“延迟链之前的功夫”但对整体测量质量的影响不亚于链本身。3. 码密度校准把每个bin的真实宽度标出来3.1 码密度法的出发点延迟链的物理特性再好bin宽也不可能完全一样。进位链内部结构、抽头布线、触发器时钟偏斜、工艺偏差都会让每个bin的真实宽度偏离平均值。既然无法在物理上做到绝对均匀那就用统计的方式把它测出来。码密度校准的原理归纳成一句话让大量均匀分布的时间事件通过TDC统计落在每个bin里的次数次数比例就等于宽度比例。假设参考时钟周期为T_ref延迟链共有N个bin。校准模式下发出一系列与参考时钟异步、均匀分布的启动脉冲每个脉冲沿延迟链传播在下一次参考时钟上升沿被锁存。这个启动脉冲与锁存时钟沿之间的时间差在[0, T_ref)范围内大致均匀。记第i个bin被命中的次数为h[i]总样本数为S那么第i个bin的真实宽度近似为w[i] h[i] / S × T_ref3.2 为什么需要大量样本样本量决定校准统计精度。这里的统计噪声大致正比于sqrt(N/S)。举个具体例子链长N200采样S20万时平均每个bin得到1000次命中统计相对误差约3%提高到S100万误差能压到1%左右。码密度校准本身是把固有的非线性测出来去修正统计误差越小修正得越干净。我一般会采100万次以上。这个数据量在FPGA端其实不大因为只需要计数直方图N200时一张直方图才几百字节完全可以在片上RAM里累计结束后一次性通过串口或网口读到PC。如果采集速度不快校准过程也就几秒钟可接受。3.3 校准激励怎么产生实际校准激励不一定需要外部精密仪器。最简单的方法是让自由振荡器产生启动脉冲它和参考时钟没有整数倍关系相位在[0, T_ref)内近似均匀。也可以用FPGA内部两个不同频的PLL输出产生异步关系比如参考时钟200MHz启动时钟用37MHz之类只要两个频率没有简单倍数关系且测量点数足够多等效来看覆盖也比较均匀。这里要提醒一句校准用的时间事件必须完整覆盖整个周期不能只在某个相位区域聚集。如果只是某个逻辑条件触发启动很可能造成直方图严重偏置标出来的bin宽也是错的。我见过有人直接用测量对象的业务信号做校准结果业务信号集中在几个固定相位直方图出现大片零计数算出来的查找表完全不能用。3.4 查找表的生成与定点化拿到直方图之后校准流程可以离线完成。下面是Python处理的核心思路import numpy as np def calibrate(hist, T_ref_ps): hist hist.astype(np.float64) total hist.sum() width hist / total * T_ref_ps # 每个bin的宽度单位ps lut np.concatenate(([0], np.cumsum(width))) return width, lut # 使用示例 # width, lut calibrate(hist, 5000) # 若原始码为m取bin中心时间估值(lut[m] lut[m1]) / 2LUT每一项记录的是“信号走到第i个bin边界时已经过去的累计时间”。考虑到FPGA内浮点资源有限建议把LUT转成定点。比如5ns周期、要0.5ps的分辨率可以用14bit定点表示0~5000ps再保留若干比特小数位整体用32bit足够。定点化时注意舍入误差如果校准表后续还要做插值建议多保留几位小数避免插值结果来回抖动。3.5 校准结果的验证校准之后一定要验证不能只看直方图拟合得好就觉得完事。我的做法是校准完成后再采一组独立样本对每个bin用校准后的时间累加次数重建直方图与理想均匀分布对比或者做一个固定延迟回环测量比如把同一信号经过已知长度走线后再触发TDC检查测量值是否落在预期范围。另一种验证方式是扫频测量输入一个频率可变的周期信号测量其周期值观察不同频率下测量结果是否平滑。如果校准表在某些bin边界存在跳变扫频图上会出现周期性毛刺这时需要回头检查直方图对应区域是不是有异常bin。4. 工程落地从RTL到上位机的完整实现4.1 模块划分与信号连接一个典型TDC通道可以拆成这几块延迟链与抽头锁存负责将时间转换为温度计码粗计数器负责记录参考时钟周期数编码器把温度计码翻译成二进制原始码校准统计模块校准时统计每个原始码的命中次数FIFO/接口把原始时间数据或直方图数据送出。校准模式和工作模式共用同一套延迟链只是数据流向不同。工作模式下测量结果粗计数×T_ref细时间校准模式下只统计原始码序号不做时间换算。这样设计的好处是硬件复用率高逻辑开销小。多通道应用时每条TDC通道可以独立做延迟链但校准统计模块可以共用只要做好通道号和码值打包就行。4.2 延迟链捕获部分的关键写法延迟链抽头用简单寄存器即可。下面是示意性的Verilog片段reg [N-1:0] tap_reg; wire [N-1:0] tap_in; // 来自进位链各CO输出 always (posedge clk_ref) begin if (rst) tap_reg 0; else tap_reg tap_in; end这里的clk_ref就是与粗计数器共用的参考时钟。锁存时刻确定之后整条链的状态就被冻结。实际综合时需要对tap_in这条路径设置保持时间约束或使用固定布局否则工具可能把它优化成不可预测的时序路径。我曾经因为没设dont touch属性综合器把部分进位链优化成了查找表逻辑延迟特性直接变了校准表也就失效了。4.3 温度计码编码与泡泡消除延迟链锁存下来的温度计码理论上应从某一位开始由0变1但受亚稳态和布局影响偶尔会出现“泡泡”比如0 0 1 0 1 1。直接用优先编码器会把错误位置当成翻转点。最简单的泡泡消除方法是把相邻两位做AND生成新的温度计码也可以用“找最长连续1”的算法代价稍高但在FPGA上仍然很快。如果出错概率很低也可以用查表法把有限模式直接映射到正确码。小技巧是编码器输出不要直接作为最终时间码先做一次“原始码有效性检查”。如果输入模式明显不符合温度计码结构标记这个样本为坏点后续统计或输出时剔除。这样既能防止错误传播也能在调试阶段通过统计坏点率监控延迟链的健康状态。4.4 数据采集与校准流程整个校准流程分四步跑通配置参考时钟和自由振荡器让延迟链进入校准模式大量启动脉冲输入每产生一次锁存编码器输出原始码统计模块对该码值计数并存入RAM采满目标次数后把直方图通过串口/网口读出到PCPC脚本计算w[i]和累积LUT再把LUT回传到FPGA的RAM中切换工作模式开始正常测量。流程不复杂但每一环节的时序都值得盯紧。特别是RAM写入和读出时的地址对齐我曾因为计数时少加了1导致整个LUT偏了一个bin排查了很久才定位。校准表回传之后还可以把工作模式下的直方图再抓一遍确认修正后数据分布接近均匀这算是最直观的“有没有修正对”的验证。5. 常见问题与排查记录5.1 直方图不是单调的出现大鼓包如果校准直方图中某个区域bin计数畸高往往说明该区域bin宽特别大或者有额外固定延迟。首先查布局是不是抽头寄存器没有紧挨进位链摆放其次查时钟锁存时钟在网络上的偏斜是否会形成周期性尖刺。必要时把部分区域约束改动后重新跑。这类问题靠纯算法是修不干净的必须在物理层解决。5.2 编码结果频繁跳变泡泡问题泡泡主要来自亚稳态。当启动脉冲到达抽头锁存器时正好处于建立/保持边界附近D触发器输出可能不确定。改善方法一是适当增加链长让信号传播时刻避开大多数触发器的敏感窗口二是在编码器前增加泡泡消除逻辑三是如果允许做多次采样取多数。这些措施能显著降低异常码概率但不能完全消除保留坏点剔除机制更稳妥。5.3 校准后精度还是不够如果校准后INL仍然很大多半是样本量不足或者校准数据与工作环境不一致。温度变了10℃以上延迟链的延迟分布会明显偏移离线校准结果跟不上。建议在系统里保留在线校准入口关键测量任务前自动跑几万次校准。另外供电电压波动同样会影响延迟必要时对核心供电做滤波。码密度校准修正的是“一段时间的统计特性”它代替不了稳定的电源和稳定的温度。5.4 常见问题速查表现象可能原因检查与解决直方图出现连续零计数链断线、约束错误、复位异常检查进位链例化与布局确认链上每个寄存器都被正确驱动某个bin宽度异常巨大触发器到进位输出布线绕远查看布局对异常bin周边增加区域约束编码结果出现泡泡亚稳态、建立保持违规加泡泡消除逻辑改善锁存时钟质量温度变化后精度下降延迟尺寸随PVT漂移在线重校准使用温度补偿或工作模式前自动校准数据流中粗计数与细时间错位粗计数器与锁存时钟不同源用同一时钟域触发确保粗计数在锁存沿更新5.5 几个独家经验我最后分享几个自己折腾后的心得。第一延迟链的优化永远比校准更重要。码密度校正是修正手段不是用来背锅的物理层做得越规整校准后才越接近理想。第二一定要把原始码和校准后的时间分开看待。调试早期先看原始直方图能直接暴露布局和时钟问题直接看校准后结果反而会掩盖一些问题。第三针对温度敏感场景可以把校准LUT按温度区间做多份运行时根据片内温度传感器选择对应表这样比频繁在线校准更省时间响应也更及时。还有一个容易忽略的小技巧在链的末端多放几级冗余抽头并在编码时做窗口截取。这样在校准时不会因为链覆盖不满T_ref或链头链尾边界效应导致数据分布出现截断变形。多留出来的那几级平时不参与测量计算但在温度变化剧烈时能明显提升边界稳定性算是最低成本的安全余量。