基于Carry4进位链的FPGA高精度TDC设计与校准

发布时间:2026/9/8 22:01:13
基于Carry4进位链的FPGA高精度TDC设计与校准 简介面向FPGA设计者这份资源是一套基于Xilinx Carry4进位链实现时间数字转换器TDC的完整工程方案适用于高速信号处理、精确计时、频率测量以及需要高分辨率时间间隔量化的场景既适合初学者从RTL代码入手理解进位链延迟线的构造也方便中高级开发者借鉴其精度优化思路。包内共337个文件压缩后约9.51MB按目录归类了Vivado工程与约束文件xpr/xci/xdc/dcp、Verilog/VHDL等RTL源码、仿真与编译脚本do/bat/tcl/vhd、综合实现与仿真日志rpt/log/wdb以及可烧写的比特流文件bit结构清晰便于按设计、验证、实现三个层次查阅。资源已有1684人学习下载实用性强。压缩包内还包含完整的Vivado仿真环境配置与自动编译脚本结合TDC_Carry4.bit可快速复现高精度进位链测量效果配套日志、报告和时序分析文件有助于理解误差校正与时钟约束处理方便迁移到自己的FPGA项目中。 做精密时间测量的工程师几乎都会遇到TDCTime-to-Digital Converter时间数字转换器这个词。它在激光测距、正电子发射断层成像、粒子物理实验、超声波流量计里都是核心模块干的事就是把两路信号的时间间隔量化成数字码。FPGA里实现TDC目前最常用的高精度方案就是用进位链Carry4搭延迟线。我最早接触这个方向是做多通道飞行时间测量当时需要达到几十皮秒量级的分辨率连续试了好几种方案最后发现Carry4加TDC这个组合在成本、分辨率、灵活性上都很能打。这篇文章把我踩过的坑、验证过的电路结构、校准流程都整理出来希望能帮你少走点弯路。方案本身不复杂但里面的细节非常多尤其是时序约束和校准这两块直接决定了最终性能。1. 为什么是Carry4延迟线的选型逻辑1.1 传统时间测量方案的痛点最直观的时间间隔测量方法是计数器法用高频时钟数周期。但这方法分辨率直接受时钟频率限制要测纳秒级的时间差得把时钟倍频到GHz以上FPGA里的时钟树资源和功耗都不允许。更常见的是时间戳法用锁相环倍频后对事件打时间戳但FPGA能稳定工作的时钟频率也就几百MHz分辨率天然卡在几个纳秒量级根本满足不了激光测距、高能物理这些场景几十皮秒的需求。于是出现了一个经典思路系统时钟只负责粗计数用一段延迟线把时钟周期内的小数部分细分出来这就是延迟线插值法。但早期很多设计直接用LUT或普通布线做延迟线效果很差。LUT内部路径延迟受综合工具影响大不同LUT位置之间还有布线延迟分散在芯片各处走线长短不一温度一波动延迟就乱套。普通布线更是不可控同一段net在不同批次器件上可能差出一倍。这条路我第一版就走过实测下来稳定性完全没法看。1.2 Carry4的结构优势在哪Xilinx 7系列FPGA里的CARRY4原本是为了实现加法器进位链设计的它包含4个MUX进位单元级与级之间有专用金属走线不经过通用布线资源。这意味着延迟链走的就是芯片里专门为进位传播预留的路径物理距离固定结构高度对称每一级CARRY4的延迟一致性远好于普通LUT路径。更关键的是CARRY4的进位输出CO可以直接连到同一个slice内的D触发器数据端走线极短不需要绕到通用布线网络上再绕回来。这意味着不仅延迟线本身可控抽头采集路径也同样可控。这两条加起来才让“几十皮秒级延迟单元”这件事变得靠谱。实际在Artix-7上单级CARRY4延迟大概在30~80ps左右不同芯片有差异但同一个芯片内部一致性不错。这个量级正好落在TDC感兴趣的区间太大会浪费分辨率太小会跟噪声和大触发器时钟偏斜可比反而难做。1.3 整体架构一句话说清整个TDC可以分成三块第一块是被测信号进入的一长串CARRY4进位链信号沿着链一级级传播每级CO输出拉出来一个抽头第二块是一排D触发器用同一个采样时钟沿把所有抽头的电平同时采下来得到一坨thermometer码也就是一段连续的0和一段连续的1边界位置就反映了信号到达时刻与采样时钟沿之间的剩余时间第三块是粗计数器负责记录整时钟周期数。把START和STOP两路各自转换出时间戳相减就能得到精确时间间隔。这方案不要求GHz时钟逻辑资源占用也不大做多通道的时候优势尤其明显。2. 延迟线与采样寄存器核心结构怎么搭2.1 CARRY4原语的手工例化我一直建议延迟线部分直接例化CARRY4原语而不是靠综合器去推断。原因是延迟线的物理结构太敏感综合工具一旦做了重组、优化或retiming整个时序关系就可能被改掉。下面是基本例化模板(* KEEP TRUE *) CARRY4 #( .CYINIT(1b0) ) carry_chain_inst ( .CO (co_bus), // 4位进位输出也是抽头信号 .O (o_bus), // 4位算数求和输出这里不使用 .CI (ci_bus), // 上一级CARRY4的进位输入 .CYINIT (hit_signal), // 被测信号从最前端注入 .DI (4b0000), .S (4b1111) );这里有个关键配置S端口全部拉高DI端口全部拉低。这样每个MUX进位单元等效于把输入信号原封不动向下传播唯一的影响就是经过了一级MUX的物理延迟。信号从CYINIT或前级CI进来经过4个MUX后从CO[3]输出再接下一级CARRY4的CI一级级串下去就形成了一条均匀的延迟链。抽头可以从每一级的CO[0]、CO[1]、CO[2]、CO[3]单独引出一个CARRY4就对应4个采样点。CARRY4本身没有复位端上电后链上状态是未知的。我的做法是上电后先让被测信号通路保持低电平等系统复位释放后再允许hit信号进入。另外采样用的FDRE要带复位端初始化全部置0避免在链空闲时采样到随机电平。低速调试时可以用一个简单的使能信号“守住”入口高速应用里加一点外部输入同步逻辑。2.2 抽头FF怎么接才稳定采样部分直接例化FDRE每个抽头一个FF代码结构如下genvar i; generate for (i 0; i TAP_WIDTH; i i 1) begin : tap_ff (* KEEP TRUE *) FDRE #(.INIT(1b0)) ff_i ( .Q (tap_out[i]), .C (sample_clk), // 统一采样时钟 .CE(1b1), .R (reset_n), .D (co_bus[i]) // 来自CARRY4的抽头输出 ); end endgenerate采样时钟就是系统里那个粗计数时钟比如200MHz。所有抽头FF共享同一个时钟在每一个上升沿把整条链的电平状态锁存下来。由于链上信号传播与采样时钟是异步的这本质上是一个异步采样过程必然存在亚稳态和时序违例风险。但这属于TDC的固有特性不是设计错误后面约束和编码阶段会专门处理。布局上要注意CARRY4和对应的采样FF尽量保持在同一个slice内。如果CARRY4的输出绕到远处FF再去布线抽头延迟就会被通用布线干扰整个延迟线的均匀性就毁了。Vivado在布局时通常会自动优先把CARRY4和同slice FF匹配起来但为了保险我习惯在XDC里把延迟线相关的slice区域环境做一次粗约束限定在某一列连续区域内。2.3 防止综合器和布局器“好心办坏事”延迟线的输出在综合器眼里可能是一堆“冗余”逻辑全0输入没有功能意义于是被优化掉。我在第一版就吃过这个亏综合完链还在布局后抽头信号没了。解决办法是老三样RTL里加(* KEEP TRUE *)属性约束文件里对相关net加DONT_TOUCH必要时在综合设置里对这条路径关闭retiming。如果使用较新版本的Vivado只加KEEP可能不够布线和优化仍可能把某些信号合并。更彻底的办法是写一个独立的延迟链模块专门用来生成CARRY4链和抽头FF模块输入输出端口都保留原始信号避免被跨模块优化。并把这整个模块在综合属性里标记为DONT_TOUCH。3. 从抽头码到时间戳编码器和粗计数的配合3.1 把thermometer码变成边界位置采样得到的tap_out是一个thermometer码。正常情况下它是类似“00011111”的形态一段0后跟着一段1唯一边界就是信号延迟链上传播到的地方。我们要做的就是从这堆0和1里找到第一个1的位置或者最后一个0的位置结果用二进制index表示。最简单粗暴的写法是for循环逐级判断但链长到64级以上就会形成很长的组合逻辑优先级链编码输出延迟大而且容易在后级采到毛刺。我实际工程中把链按8bit分组先并行判断哪些组有1再对命中的组内部找边界两级就出结果。示意逻辑如下wire has_grp0 |taps[7:0]; wire has_grp1 |taps[15:8]; wire has_grp2 |taps[23:16]; wire has_grp3 |taps[31:24]; always (posedge clk) begin if (has_grp3) group_sel 2d3; else if (has_grp2) group_sel 2d2; else if (has_grp1) group_sel 2d1; else if (has_grp0) group_sel 2d0; end找到组号后再在该组8位里做一次小范围优先编码得到低位4位和组号拼成完整边界index。整个组合逻辑深度基本只有两级LUT时序压力小很多。用casex或casez直接生成优先编码表也可以但位宽大了没法扩展建议还是分组。对于码型不合法的情况比如出现“1010”这种多个断点说明采样瞬间存在亚稳态或链上存在异常应该把该事件打上无效标记。我用一个简单的奇偶校验位和合法码型检查逻辑合法码型必须满足所有1都在所有0的同一侧不满足就丢弃事件或标记为无效。这个处理比强行编码有意义得多能大幅提高系统在噪声环境下的鲁棒性。3.2 粗计数器与细粒度的时间戳拼接细粒度边界只能覆盖一个采样时钟周期超出部分要靠粗计数器。用采样时钟对全局计数器CNT持续计数当hit信号到来时延迟线在下一个采样沿给出tap_out同时我们需要把当时的CNT锁存下来。这里要注意粗计数锁存和细粒度采样的“对齐”问题tap_out采样的是当前时钟沿下链上的电平CNT也是同一个时钟沿锁存二者天然对齐直接拼接即可。时间戳计算公式t_start CNT_start × T_clk idx_start × tau_avg - T_offsett_stop CNT_stop × T_clk idx_stop × tau_avg - T_offset其中tau_avg是延迟线平均每级延迟T_offset是包含信号入口、时钟偏斜、链初始化延迟在内的固定常数。这里的T_offset必须在整机校准时统一标定。两个时间戳相减后时间间隔的整周期部分由CNT差值决定小数部分由idx差值决定。之所以用tau_avg而不是每个bin的精细权重是因为先把数据存下来后续再做码密度矫正表处理实时路径上用平均延迟保证pipeline吞吐。3.3 双通道同时测量时别漏掉相位关系做START和STOP双通道TDC时两条延迟链分别针对各自信号但采样时钟是同一个。实际工作中START先到STOP后到两者各自的时间戳是独立产生的中间有任意时钟周期间隔都行。真正容易错的点是如果STOP信号在采样时钟边沿附近到达其细编码可能横跨时钟边界导致最终时间戳误差达到一个采样时钟周期。解决办法是在事件记录里同时保存“当前CNT”和“链上的余量”并设计一个简单的边界检测发现CNT_lock值和细编码方向矛盾时做加减修正。最初我觉得多此一举直到用ILA抓到过一次大误差才明白这个修正不是可选项是必要条件。4. 校准CARRY4的非线性是怎么被驯服的4.1 为什么校准不能省CARRY4单级延迟虽然比普通逻辑稳定但绝对做不到完全均匀。我一个工程里实测XC7A系列FPGA的延迟链相邻两bin宽度偏差可达好几倍有的bin只有20ps有的bin到80ps。直接用固定tau_avg去换算是要出问题的最终测量结果的差分非线性DNL非常难看积分非线性INL也会累积成一个明显的曲线。很多同学第一次上板看到测量值忽大忽小其实不是TDC坏掉了是没做校准。4.2 码密度测试原理与实际操作码密度法是目前最实用的标定手段。核心思路往TDC输入端灌大量时间位置完全随机的事件这些事件相对于采样时钟是均匀分布的因此延迟线上每个bin被命中的次数正比于该bin的实际宽度。统计一段时间后每个bin的宽度权重就出来了。具体操作上我通常在FPGA内部用LFSR产生伪随机数再在某个固定时钟沿触发一个内部hit事件。LFSR的随机性要让事件到来时刻相对采样时钟均匀分布如果事件频率过高会出现“频谱泄漏”某些bin永远采不到。经验是让事件间隔至少大于10个采样时钟周期且LFSR位宽不低于16位。统计样本量方面如果希望单bin宽度估计误差在1%左右每个bin至少要有1万次命中128级的链就需要至少128万个Hit事件一般在几十毫秒内就能跑完完全可以接受。统计过程最好在片上完成每个bin对应一个RAM计数器事件进入时对对应计数加一完成后通过串口或以太网将计数表上传。代码上用BRAM按idx寻址命中一次地址加1代码量不大。4.3 校准表怎么变成真实测量结果得到每个bin的hits计数后先把hits归一化为时间权重weight[i] hits[i] / total_hits × T_clk然后在测量时不再用固定tau_avg乘以边界下标而是把从链入口到此边界之间所有bin的权重累加。累加操作如果实时做会拖慢pipeline所以我的做法是把权重做前缀和存成一张查找表查表时直接按idx取出校准后时间。表项宽度根据精度需求定比如用32bit定点数单位取飞秒量级足够应付绝大多数应用。现场环境温度变化会改变CARRY4延迟校准表不是一劳永逸的。我遇到过从开机30度到稳定60度整体延迟漂移几个百分点的情况。解决思路分两种一是定期停止测量重新跑码密度校准二是插入已知时间间隔的参考事件在线估算当前平均延迟缩放校准表。第一种更简单适合数据采集类系统第二种适合连续测量需求。4.4 校准效果怎么验证才算数验证不能只看平均值要看标准差和残差。我的做法是用内部产生固定时间间隔的START和STOP脉冲对间隔设置为采样时钟周期的一半加一点偏移比如2.3个周期测量大量次数后统计均值和方差。均值应接近设定值标准差反映系统随机抖动残差反映非线性剩余误差。另一个更苛刻的测法是扫一组从0.1到1.9个时钟周期逐渐变化的输入间隔看测量结果是否单调这个测试能快速暴露编码器毛刺和bin宽度失配问题。校准后整体RMS能做到50ps以内经过滤波处理还能进一步下降。5. 常见问题排查与工程化建议5.1 时序违例和亚稳态问题延迟链抽头FF的采样路径天然是异步的Vivado里会报大量setup/hold违规。第一次见到满屏红色会吓一跳但其实不用慌。这些路径本质上是“跨时钟域采样”属于预期行为。我的做法是把延迟链输出先打拍一次得到同步后的tap_sync再只对tap_sync到编码器的路径做正常的时序约束。延迟链原语级到第一级FF之间的路径设置false path但不建议直接全路径false否则异常码型检测逻辑会失去时序依据。亚稳态本身无法根除只能抑制。当hit信号与采样时钟边沿间隔极小时某个抽头FF会进入亚稳态可能表现为码型出现毛刺。理论上这恰好对应了真实时间落在该bin边界附近但实际编码输出可能不稳定。处理方式在FF输出后加两级同步寄存器并设计合法性检查把非法码型丢弃或按邻近合法码替换。对于需要高计数率的场景可以在FPGA里实现Wave Union TDC结构一次触发记录多条边沿穿越信息既能提高分辨率又能通过冗余信息恢复部分亚稳态造成的坏码。不过这属于进阶玩法先把单边沿版本跑稳再说。5.2 延迟线资源与位置约束每条128级延迟链大约消耗128个FF和32个CARRY4资源开销非常小。但多通道展开时各通道的采样偏斜和延迟偏差会明显起来。推荐的做法是让所有通道放在同一列的连续slice区域利用CARRY4垂直级联的自然方向保持相对位置一致。这样通道间的失配主要来自器件工艺梯度而不是布局随机性。XDC里可以用方案把每条延迟链的第一个CARRY4粗略定位到指定slice比如set_property LOC SLICE_X32Y45 [get_cells delay_inst0/carry_chain[0].u_carry]然后让布局器自动向下排列。5.3 参数速查与选型参考参数典型值说明单级CARRY4延迟30~80 ps不同FPGA型号差异大必须实测校准校准后单bin分辨率10~30 ps依赖码密度校准质量系统RMS误差30~80 ps包含时钟抖动、电压纹波等影响链长128级细粒度量程约4~10 ns覆盖200MHz采样时钟一个周期足够粗计数器量程秒级可持续受计数器位宽限制可任意扩展每通道资源约128 FF 32 CARRY4多通道完全能接受选型时注意越先进的工艺单级延迟可能越小但不一定更好。bin宽到20ps以下FF建立保持时间、时钟偏斜、电源噪声的贡献反而可能压过延迟单元本身校准也会变得困难。不要一味追求低延迟稳定性优先。5.4 板上调试时的几个独门技巧调试TDC时ILA采样率如果和系统采样时钟一样看到的tap_out会是已经打拍后的同步信号其实已经丢失了亚稳态信息。我习惯在ILA里同时观察原始抽头、同步抽头、编码输出和CNT锁存值四组信号特别是要能抓到非法码型这样能快速分辨问题是出在延迟链、采样、编码还是后续处理。电源稳定性对TDC的影响远超预期。FPGA核心电压的一点纹波都会通过延迟单元传播变成时间测量误差。做高精度TDC时尽量在FPGA供电附近加足去耦电容避免和开关电源噪声耦合。我测试过同一套逻辑供电从普通DC-DC换到低纹波LDO后RMS误差直接下降了20%以上。这是最容易被忽视的优化点。另外事件触发率不能太高。延迟链在事件到达后需要一段时间让信号完全传播到链尾如果在上一次传播还没结束时就灌入下一次事件链上会同时出现多个边界码型完全乱掉。建议在入口处加一个简单的lockout机制限制最小事件间隔大于链长对应的传播时间否则后续处理再完善也白搭。这个项目做下来最大的心得是Carry4 TDC的硬件结构其实很简洁真正的技术含量全在细节控制上。延迟链的物理布局、时序约束的口径、校准表的更新策略、电源噪声抑制每个环节都会实实在在影响最终分辨率。做第一版时如果性能不达标优先检查延迟链是否被优化、校准样本量是否足够、供电纹波是否超标通常问题都出在这几个地方。先把这些基础打牢再去追求更小的bin宽和更复杂的编码算法方向才正确。本文还有配套的精品资源点击获取