基于FPGA进位链的皮秒级TDC设计:从原理到工程实践

发布时间:2026/10/7 19:02:18
基于FPGA进位链的皮秒级TDC设计:从原理到工程实践 1. TDC设计思路拆解为什么延时链能测出皮秒级时间差做高精度时间测量的人基本都绕不开TDCTime-to-Digital Converter时间数字转换器这个词。激光测距、PET扫描、高能物理粒子探测、示波器触发系统甚至手机上的激光对焦模块核心都在比谁的时间差小、谁测得更准。传统方案用ADC采样模拟电压来推算时间间隔分辨率受限于ADC的位数和带宽做到几百皮秒已经压力很大而TDC的思路完全不同它是直接把时间间隔量化成数字码不经过模拟电压这一层天然就比ADC路线更适用于“纯时间差”测量场景。那FPGA和TDC有什么关系关键在于FPGA内部有一种专用硬件结构——进位链Carry Chain。常规逻辑单元LUT的延迟不是为精密延时设计的受布局布线影响很大你不能指望把它串联起来做高一致性延时但进位链不一样它是FPGA里一条物理上已经固定好的快速传输路径从相邻逻辑单元一个接一个往下传每个进位单元本身的传播延迟非常稳定而且基本由硅片工艺决定。业界常说的“抽头延迟线法”Tapped Delay Line落在FPGA里就是把这串进位链当精密延时线信号进入延时链后每个进位节点用触发器打一拍记录信号走到了哪个位置从而反推出时间差。这篇博文要解决的就是用FPGA进位链把TDC做到几十皮秒级别的延时分辨率。适合正在做精密时间测量相关项目、但又不想直接用专用TDC芯片比如ACAM的GPX系列而被成本和供货卡脖子的朋友也适合对FPGA内部结构好奇、想把手里的开发板玩出更高价值的同学。后面所有内容都以Xilinx 7系列为例子展开我用的是Artix-7但原理和流程对其他厂商FPGA同样通用Altera/Intel的LE进位链、Lattice的Carry Chain大同小异稍微调整原语名称和布局约束就行。设计的第一步不是写代码而是把需求量化。TDC的几个核心指标分辨率LSB尺寸即一个延时单元多少皮秒、测量范围最大可测时间差、非线性DNL/INL、死区时间两次测量之间的最小间隔。这些指标会直接决定你选用多少级进位链、跑多高频率、编码逻辑怎么做。我当时的指标需求是分辨率优于50ps单次测量范围不小于50ns应对超声飞行时间测量的场景死区小于1μs。这里先插一个非常重要的认知FPGA进位链的延时并不是无限可分的它的最小单位是一个CARRY4模块内部MUX的传输延迟。在Xilinx 7系列里一个CARRY4由4个进位单元组成每个进位单元从CIN到COUT或从CIN到O的延迟大约在20~30ps之间不同速度等级、温度电压下有差异。也就是说如果你把300个进位单元串起来理想情况下可以得到约6~9ns的总延时——这刚好对应一个200~150MHz时钟周期。所以在设计测量范围时你要先确认延时链的总长度必须覆盖至少一个系统时钟周期否则会有测量盲区信号会在时钟采样前就已经跑丢。这背后还有一个更本质的问题为什么非要用进位链不用普通的查找表LUT级联或者直接用组合逻辑延迟做延时线答案是可重复性和温度一致性。LUT到LUT的布线延迟由布局布线决定你无法控制路径长短信号可能走长线也可能走短线跑出来的延时分布很乱而且温度一变化各路径的漂移还不一致进位链则是一条物理上等间距排列的专用路径单元之间距离固定、走线固定、每级延迟相对均匀这是做精密TDC的基础前提。没有这个前提后面所有校准和补偿技巧都无从谈起。2. 进位链延时原理与性能评估2.1 为什么CARRY4天然适合做精密延时Xilinx 7系列FPGA的每个CLB可配置逻辑块里有一个CARRY4模块它的结构比较复杂但我们只需要关注它的核心功能实现快速进位传播。它内部由若干MUX和专用路由组成信号从CIN引脚进入后经过每个进位单元产生COUT输出同时还有对应的O输出引脚可以接触发器采样。关键点在于这条CIN到COUT的路径是硬连线的不经过通用布线资源GRM所以延迟非常小且稳定。每个进位单元从CIN到COUT的延迟约20~30ps从CIN到O的延迟也差不多同一量级略大一点。这个数值不是由你在代码里控制而是由硅片工艺和电压温度环境决定的所以每颗FPGA芯片的绝对值会有差异这也是为什么后面必须做“码密度校准”的原因。使用进位链做延时链的基本拓扑是这样的信号从CIN进入经过N级进位单元后每一级的COUT/O引脚接D触发器FDRE的D端在同一个系统时钟沿对所有触发器采样得到一个N位的温度计码Thermometer Code。信号传到的位置越深说明信号相对采样时钟沿越早到达时间间隔也越大。用Verilog例化CARRY4原语时要明确每个端口的作用。CARRY4的端口包括CARRY4 #( .INIT(16h0000) ) carry_inst ( .CO (carry_co), // 4位进位输出接下一级CIN或触发器 .O (carry_o), // 4位求和输出通常接触发器D端 .CI (carry_ci), // 级联进位输入上一级CO[3]接这里 .CYINIT (1b0), // 第一级的初始化进位输入 .DI (4b0000), // 数据输入用于实现加法TDC可固定为0 .S (4b0000) // 选择输入TDC可固定为0 );这里有个经验TDC场景下CARRY4的DI和S都可以接0只让信号纯粹从CI往CO传播不要混入逻辑功能这样延时路径最干净。有些设计为了省触发器资源把O输出和CO输出都接触发器一起采这样同一个CARRY4可以得到8个采样点4个O 4个CO分辨率提升一倍但逻辑复杂度也翻倍。我的建议是先从单输入开始跑通整体架构后再考虑这种资源倍增的技巧否则编码逻辑会把你绕晕。2.2 延时单元一致性评估与码密度校准进位链的每一个单元延迟并不完全相等工艺偏差、电压梯度、温度分布在芯片内部都会造成码宽差异。如果把每个进位单元看作一个“量化桶”理想情况下每个桶宽度相等但实际做出来有的桶偏宽、有的桶偏窄这就是微分非线性DNL。你无法在物理上消除这种偏差但可以在逻辑上校准这就是“码密度测试”Code Density Test要做的事。码密度校准的原理很简单给延时链输入端一个与系统时钟完全异步的随机信号通常用外部噪声源或高速伪随机序列大量采样后统计每个进位单元被“命中”的次数。由于信号到达时刻在时间轴上近似均匀分布每个单元被命中的概率应该正比于它的实际延时宽度。被命中次数多的单元说明它实际占的时间宽度大命中少的说明它更窄。通过统计结果你可以给每个单元计算一个修正权重后续所有测量结果用它加权换算成实际时间。实测时需要注意采样次数至少要在10万次以上否则统计涨落带来的误差会大到让校准失去意义。我的做法是用FPGA产生一个计数器每采样10万次做一次统计并输出结果用串口或JTAG读回PC分析。第一次看到每个单元的命中次数分布时你会发现有的单元宽度是平均值的两倍有的只有平均值的50%——不用慌这是正常现象FPGA工艺就是这样不是你的设计坏了。单从性能评估来说除了码密度测试还要做“温度-延时漂移”的摸底。FPGA进位链的延时随温度升高而增大典型值在几十到上百ppm/℃的量级。如果一个延时单元标称25ps温度从25℃升到65℃40℃的温漂可能让整条链的延时变化几百ps这个漂移量足以毁掉你的测量精度。所以任何实用的TDC设计都必须有校准机制纯靠出厂标定一次用到底的方案在工业现场不现实后面第三章会详细说补偿策略。3. 高精度延时链的工程实现从原语例化到布局约束3.1 硬件平台与开发环境选择在动手写代码之前先确认手里的FPGA资源够不够这能帮你避免写到一半发现资源不够、推倒重来的尴尬。做TDC至少需要足够的进位链长度对应你要的测量范围、对应的触发器数量每个进位单元至少一个、以及后续编码逻辑使用的普通逻辑资源。拿我的Artix-7 XC7A35T举例它大约有5200个Slice每个Slice含一个CARRY4即4个进位单元。如果我要做200级进位链约4~6ns覆盖配合250MHz系统时钟刚好一个周期以上只占用50个Slice资源完全不是瓶颈。真正占资源的是数据采集和缓存逻辑每次采样得到一个200位的温度计码如果连续做平均或直方图统计会消耗大量Block RAM和DSP资源这里要提前规划好。开发环境我用的Vivado 2018.3版本不太重要新的都行关键是综合选项中要把“-keep_hierarchy”打开防止综合器把进位链结构优化掉。有些设计者直接在RTL里写加法器期望综合器推断出进位链但对TDC这种对结构有严格要求的场景我强烈建议直接例化原语不要依赖推断——综合器可能会插入额外的逻辑门或改变级联顺序你完全不可控。3.2 200级进位链的例化与级联约束代码层面最直接的方式是用generate语句把多个CARRY4级联起来每级的CO[3]接到下一级的CI。同时每一级的CO和O输出都要接到触发器上。下面给出一个我实际使用过的可参考代码框架module tdc_delay_line #( parameter NUM_TAPS 200, parameter CARRY4_NUM 50 )( input wire clk, input wire signal_in, output wire [NUM_TAPS-1:0] taps_out ); wire [CARRY4_NUM*4-1:0] carry_co; wire [CARRY4_NUM*4-1:0] carry_o; wire [CARRY4_NUM-1:0] carry_ci; assign carry_ci[0] 1b0; genvar i; generate for (i 0; i CARRY4_NUM; i i 1) begin : gen_carry CARRY4 #( .INIT(16h0000) ) u_carry ( .CO (carry_co[i*4 : 4]), .O (carry_o[i*4 : 4]), .CI (carry_ci[i]), .CYINIT (1b0), .DI (4b0000), .S (4b0000) ); if (i CARRY4_NUM-1) begin : gen_ci_next assign carry_ci[i1] carry_co[i*4 3]; end end endgenerate reg [NUM_TAPS-1:0] taps_reg; always (posedge clk) begin taps_reg[0] carry_o[0]; taps_reg[1] carry_o[1]; taps_reg[2] carry_o[2]; taps_reg[3] carry_co[0]; taps_reg[4] carry_o[4]; // ... 依此类推每个CARRY4的4个O加上CO的输出都采进来 end assign taps_out taps_reg; endmodule上面代码里我把O[0..2]和CO[0]都采了而跳过O[3]是因为在级联结构里O[3]和下一级的CI在时序上是重合的采样会冗余。实际操作中你可以只采CO输出也可以O和CO混合采关键是每一级采样点的相对位置要跟实际物理位置一一对应不能让编码逻辑猜。写完RTL后最关键的步骤是布局约束。你要让Vivado把整个进位链放在一片连续的Slice区域否则进位链可能在物理上被拆到距离较远的位置产生额外的路径延迟或布线拥塞。Vivado里可以通过set_property LOC约束把第一个CARRY4放在指定位置然后利用面积约束Pblock把整个generate块锁在一个矩形区域内。约束示例set_property LOC SLICE_X0Y0 [get_cells {tdc_delay_line/gen_carry[0].u_carry}] create_pblock pblock_tdc add_cells_to_pblock pblock_tdc [get_cells {tdc_delay_line/gen_carry[*].u_carry}] resize_pblock pblock_tdc -add {SLICE_X0Y0 SLICE_X0Y99 SLICE_X5Y0 SLICE_X5Y99}这里的X0Y0是你自己根据芯片布局选的起始位置别硬抄。放置完成后务必打开Vivado的Device视图检查一下进位链是否连续、有没有绕路、是否全部落在你设定的Pblock范围内。这一步查一次后面能省下大量调试时间。3.3 温度计码转二进制编码逻辑的正确打开方式采样得到的200位数据理论上是一个温度计码信号到达位置之前的触发器为1之后为0具体极性取决于设计这里假设信号到了就是1。你要做的核心编码工作是找到“最后一个1”或“第一个0”的位置即边沿位置。二十年前的做法是用比较器树或优先级编码器逐级找逻辑延迟大、资源消耗高。现代FPGA更推荐的思路是“分块降维”先把200位切成若干段比如每段16位找出段内边沿位置再通过段级优先级网络找到段号最后相加得到全局位置。这样做可以把组合逻辑路径控制在两级以内编码延迟小适合高速流水处理。还有一种经典的“气泡”修正技巧由于触发器存在建立保持时间温度计码的边沿附近可能出现一个或几个不合法的码型比如“...11101000...”这种中间有个0的“气泡”。直接找最后一个1会把位置卡在气泡处导致测量值偏小。常用的处理方法是找最后一个连续的1区间的起始位置或者利用查找表把非法码型映射到附近合法码型。我在工程中用的是“只找第一个0之前的最长连续1串”的思路用一个简单的移位比较逻辑把所有“1后面紧跟着0但不满足连续”的情况视为气泡统一修正到最后一个连续1的位置再进位。3.4 关键时序约束多比特采样与亚稳态的对抗TDC设计中有一对天然的矛盾信号到达时刻相对采样时钟沿是任意的所以当采样沿到来时信号可能正好处于进位单元输出变化的窗口内触发器就会进入亚稳态Metastability输出既不是稳定的0也不是稳定的1或者在不同触发器间表现为不一致的采样结果。亚稳态没法消除只能降低概率和降低影响。标准做法是每个采样点用两级触发器同步第一级采样原始信号第二级打一拍得到稳定值。FPGA的FDRE原语天然支持级联你只需要例化两个触发器即可。这里要注意两级触发器同步增加了一个时钟周期的延迟编码逻辑拿到的是延迟后的数据但这不影响测量原理只需要在换算时统一偏移即可。时序约束层面要给进链上的触发器设置合理的set_false_path或set_max_delay我的经验是不要对进位链的采样触发器设太严格的约束因为信号沿在整个链上是逐渐传播的如果你约束所有触发器都在同一个clock edge前稳定建立综合器会认为这是一个不可能满足的路径疯狂优化甚至报错。正确做法是只约束链上相邻节点的最大延时关系保证边沿传播的连续性并对采样触发器关闭时序检查设为set_false_path让布局布线自由安排。不过“关闭时序检查”不等于“不关心时序”这里真正要关心的是链上每个采样点触发器相对系统时钟的建立时间一致性。如果某个触发器的时钟偏斜特别大它采到的位置会系统性偏移给测量带来固定偏差。好在FPGA内部时钟网络BUFG/CR的偏斜非常小皮秒级远小于进位单元的延时所以这个问题影响有限不必过度焦虑。3.5 死区时间控制与连续测量策略死区时间指的是两次采样之间的最小间隔。由于同一根延时链在采样沿到来时会被“冻结”在某个状态你需要在下一次信号进来之前把链复位清空否则前一次残留的信号会污染下一次测量。实现上通常有两种方式串联复位在采样完成并把数据缓存到寄存器后用复位信号把所有进位单元清0再把CIN输入置为可接受新信号的状态。这种方式简单但复位信号本身要传播整个链如果链太长复位时间会拉高死区。双链交替两条完全相同的延时链交替工作一条在采样复位时另一条正好接受新信号。这种方式可以把死区几乎压到0适合需要连续高吞吐测量的场景但资源翻倍、校准也要做两份。我在实测中先使用了“单链快速复位”方案死区约30ns主要由编码逻辑和复位信号传播决定。如果后续要做高速率应用建议直接上双链架构工程复杂度上升不多但性能上限明显更高。4. 实测数据与调优过程实录4.1 实测分辨率和非线性表现把所有逻辑下板之后用信号发生器输出一个比系统时钟慢一点的方波相当于让信号沿在时间轴上均匀游走采集10万次码密度数据然后统计每个码元即每个进位单元的实际宽度。我这块XC7A35T-1速度等级的实测结果平均码宽约23ps最大码宽37ps最小码宽11psDNL的峰峰值超过±8ps。这个数据在文献里算中等偏上水平如果你的芯片速度等级是-2或-3平均码宽会再小一点DNL也会稍好一些。如果你要跟商用TDC芯片比差距主要在长稳和温漂上专用TDC芯片内部有复杂的非线性校准电路FPGA方案需要自己做。但从系统集成度、灵活性和成本看FPGA方案在需要同时做信号处理、控制逻辑和通信的场景里是完胜的。4.2 温漂补偿一种实用的动态校准框架前面提到温漂是FPGA-TDC最大的敌人。我的做法是构建一个“参考事件校准”机制在每次正常测量间隙自动插入一个已知时间间隔的参考信号通过测量这个已知间隔在延时链上的位置变化推算出当前温度下整个链的延时漂移系数再对所有测量值做比例修正。参考信号怎么产生最简单的方案是用FPGA内部的另一个计数器生成一个固定的时间脉冲比如每100μs产生一个间隔为10ns的参考脉冲对经过与主测量信号相同的路径进入延时链。因为10ns是已知值测量到的码位置差了可以反推出当前码宽的变化倍数。实测中这种动态校准可以把温漂从几百ps压低到30ps以内在20~60℃范围内完全满足常规超声测距和激光测距需求。4.3 布局调优的记录与心得体会第一版设计我直接让工具自由布局结果打开Device视图发现进位链被拆成了三段中间隔着别的逻辑块总延时直接比预期大了30%。解决办法是用Pblock把进位链锁到一列Slice中并且手动指定了起始位置。第二次布局后链的连续性就好了实测码宽分布也均匀了很多。还有一个容易忽略的点给进位链旁边留出“空地”。如果进位链周围塞满了其他逻辑布局工具可能会为了绕开拥塞而把链拆散。我的做法是在Pblock里只放进位链及其采样触发器把编码逻辑放到另一片区域用管线寄存器做缓冲。5. 常见问题排查与工程避坑指南5.1 为什么采样输出全是0或全是1这个现象最直接的原因通常是进位链没有形成有效的传播路径信号根本没有进到链里。检查顺序很明确先确认CIN输入是否有数据用ILA集成逻辑分析仪抓一下信号再确认CYINIT是不是接地了如果悬空会导致初始进位状态未知最后确认综合后CARRY4原语有没有被优化掉在综合报告里搜索CARRY4关键字看数量是否跟你例化的一致。如果综合报告里都正常就要检查布局。打开Device视图确认所有CARRY4都按你设想的方向排列。我曾经遇到过一次约束写错坐标导致整条链被放到了芯片角落远离了采集逻辑结果采样触发器全部采到无效值。5.2 温度计码出现大量气泡边沿定位不准气泡数量超过正常范围时优先排查时钟质量。如果系统时钟抖动太大每个触发器的有效采样时间点会在不同周期之间漂移码型就会变得混乱。用频谱仪或示波器看一下系统时钟的相噪确认抖动在几十皮秒以内。如果时钟没问题再检查触发器采样的数据路径上是否有额外的组合逻辑延迟——比如我不小心在O输出和触发器D端之间加了一个缓冲器导致不同采样点的路径延迟不一致气泡立刻变多。TDC设计里采样路径必须精简不允许有任何多余的逻辑门。5.3 校准后精度仍然不够理想这种情况最常见的原因是校准数据本身不干净。码密度测试要求输入信号在时间轴上均匀分布如果你用的信号源跟系统时钟有固定相位关系比如用同一个晶振分频出来的那么每个码元的命中次数就会带上信号的周期性统计结果严重失真。正确做法是使用独立的、频率略微偏离的异步信号源例如系统时钟125MHz校准信号用13.0001MHz保证信号沿在每个码元上均匀游走。5.4 常见问题速查表现象可能原因排查手段解决思路输出全0/全1CIN没接对或CYINIT悬空ILA抓CIN波形检查原语端口连接码型气泡严重采样路径有额外逻辑延迟查看网表中采样路径移除多余逻辑直接O接FDRE码宽分布极不均匀进位链被布局拆散打开Device视图查看用Pblock锁定连续Slice区域测量值温漂过大缺少动态校准记录不同温度下码密度统计增加参考事件校准机制校准信号无效校准信号与时钟相关检查信号源频率关系改用独立异步源做码密度测试死区时间不达标复位信号传播太慢时序报告查复位路径换双链交替或快速复位逻辑5.5 我踩过的几个坑你绝对可以避开第一个坑Vivado综合时默认会做“寄存器重定时”Register Retiming它可能会把采样触发器的位置挪动破坏采样点与进位单元的对应关系。解决方案是在综合选项里将重定时功能关掉-retiming off并约束为keep_equivalent_registers。第二个坑编码逻辑用了if-else级联导致组合逻辑链太长。500MHz下时序直接炸了。后来改成查表法加分段编码逻辑级数从十几级降到三级时序余量一下子充裕了。第三个坑一开始图省事直接用assign taps_out taps_reg没有做跨时钟域处理。后来编码逻辑跑在另一个时钟域时采样数据经常出错。TDC的采样结果是高频信号跨时钟域必须用异步FIFO或寄存器数组同步别偷懒。6. 用真实项目经验做更远的设计延伸如果你已经成功跑通了上面这套基础TDC不妨再往下想一步。先用它做单通道测量积累信心然后可以往两个方向扩展一个是多通道TDC阵列。在很多物理实验和成像设备里需要同时测量多个探测器信号的时间差这就要在FPGA里例化多条并行进位链每条链独立采样共享同一个编码和校准模块。这样对资源的规划、时钟的分配、布线的压力都会有新的挑战我第一次做8通道时因为布局不当通道间串扰明显最后是靠给每个通道的进位链之间插入隔离列才解决。另一个方向是把它跟示波器前端或者激光雷达结合。前面只说了延时链做TDC但实际工程里信号通常要先经过比较器或放大器整形才能变成干净的数字沿。这里面的噪声整形、阈值调节、延时匹配同样对整体精度影响巨大。FPGA-TDC不是孤立的它需要跟模拟前端联合设计才能发挥皮秒级的潜力。我个人做完这个项目后最大的体会是TDC调试跟普通数字逻辑不一样它更像模拟电路调试你盯着波形和统计数据让自己相信“这个结果是对的”。不能用“仿真过了就完事”的心态做它上板实测、统计数据、排查气泡、调整布局——每一步都得沉下心来把数据吃透。但一旦你把这条链路走通它带来的精度提升和设计空间会让你觉得之前所有的坑都踩得值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询