ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA TDC延迟链优化与码密度校准方法详解

FPGA TDC延迟链优化与码密度校准方法详解 1. 从一次“测不准”的崩溃说起TDC延迟链到底难在哪做时间数字转换Time-to-Digital ConverterTDC这行的朋友大概都有过类似的经历辛辛苦苦在FPGA里搭好了一条延迟链综合、布局布线、上板满心期待地拿信号源打一个已知脉宽进去结果读回来的码值跟理论值差了十几个LSB而且换个温度、换块板子偏差又变了。你开始怀疑是信号源不准怀疑是示波器探头接地不好怀疑是电源纹波太大最后折腾一圈才发现——问题出在延迟链本身的不均匀性上。这就是我今天想聊的核心话题基于FPGA的TDC延迟链优化与码密度校准方法。TDC说白了就是把一个很短的时间间隔转换成数字码值输出。它在激光测距、飞行时间测量、粒子物理实验、PET医学成像、高速串行链路抖动分析里都是关键模块。而FPGA因为内部有丰富的可编程延迟单元和进位链资源成了实现TDC最方便的载体之一。但方便归方便FPGA的延迟链有个天生的毛病每一级延迟单元的传播延时并不相等。有的级快有的级慢甚至同一级在不同芯片、不同温度下都不一样。这就导致你直接拿“码值乘以标称LSB”去算时间误差能大到没法看。码密度校准Code Density Calibration就是来解决这个问题的。它的思路很朴素如果你往延迟链里灌大量随机分布的脉冲信号理论上每个延迟单元被“命中”的概率应该正比于它的实际延时。统计一段时间内每一级被触发的次数就能反推出每一级的真实延时进而建立一张“码值—时间”的查找表。听起来简单但实操里坑非常多随机信号怎么产生、统计样本要多少、bin宽怎么划分、温漂怎么补偿、资源怎么省每一步都有讲究。这篇文章我打算分两大块讲清楚。第一块讲原理和实现也就是延迟链怎么搭、码密度校准的数学模型怎么推、FPGA里具体怎么落地第二块会涉及优化技巧和实测数据但那部分内容较多我会放在后续单独展开。今天先把地基打牢适合正在做TDC项目、被非线性误差折磨、或者刚入门想搞清楚“为什么我的TDC精度上不去”的朋友。不管你是用Xilinx的Carry4、Intel的进位链还是高云、易灵思这些国产FPGA底层逻辑是相通的。2. 延迟链的硬件原理与FPGA实现选型2.1 为什么FPGA里最适合做延迟链的是进位链在FPGA里实现延迟链最直接的想法是用一串反相器或者缓冲器buffer。但你真去写代码综合一下就会发现工具会把你的反相器链优化掉或者布局得乱七八糟每一级延时完全不可控。所以有经验的人都会盯上FPGA内部的专用进位逻辑资源比如Xilinx 7系列里的CARRY4、UltraScale里的CARRY8Intel里的进位链单元。为什么是进位链因为进位链在FPGA内部是硬核布线它的路径是固定的、专用的综合工具不会随意打乱。每一级进位单元carry cell的延时虽然不完全相等但相对稳定而且延时量级很小通常在10到50皮秒之间正好适合做高精度TDC。以Xilinx Artix-7为例一个CARRY4包含4个进位复用器MUXCY每个MUXCY的传播延时在20ps左右一条链串几百级就能覆盖几纳秒的量程。具体怎么搭核心思路是把进位链配置成“抽头延迟线”Tap Delay Line。信号从链的头部输入沿着进位链一级一级往下传每一级的输出都引出一个抽头tap接到触发器上采样。当被测脉冲到来时它会在链中传播触发器在时钟沿把当前链上“传播到哪一级”的状态锁存下来用温度计码thermometer code表示。比如链上第1到第37级是1后面全是0就说明脉冲传播到了第37级对应的时间就是前37级延时之和。在Verilog里通常用(* keep *)或者(* dont_touch *)属性来防止工具优化用CARRY4原语例化来保证映射到进位链。下面是一个简化的Xilinx 7系列延迟链例化片段genvar i; generate for (i 0; i 128; i i 1) begin : delay_chain (* keep true *) CARRY4 CARRY4_inst ( .CO(co[i*43:i*4]), .O(o[i*43:i*4]), .CI(ci[i]), .CYINIT(1b0), .DI(4b0000), .S(4b1111) ); end endgenerate这里把S端全接1DI全接0进位链就变成了一个纯粹的延迟线CO输出就是每一级的抽头。实际项目中我会把抽头接到IDDR或者直接接FDRE用系统时钟同步采样。2.2 延迟链的级数怎么定量程、精度与资源的三角平衡延迟链级数不是拍脑袋定的它直接决定了TDC的两个核心指标量程和分辨率。量程等于总级数乘以平均单级延时分辨率约等于单级延时。比如你要测一个最大5ns的时间间隔单级延时按25ps算那至少需要200级。但级数越多占用的进位资源、触发器资源、布线资源就越多功耗和面积都上去了。我一般会按这个流程来定级数先明确被测时间间隔的最大值T_max再根据目标分辨率LSB_target算出最少级数N_min T_max / LSB_target然后留20%到30%的余量因为链尾的延时可能偏大、温漂也会让量程缩水。比如T_max4ns目标LSB20psN_min200实际取256级正好是2的幂方便后续编码。但这里有个容易被忽略的点链尾的延时通常比链头大。因为进位链走到后面布线拥塞和负载效应会让单级延时增加。我实测过一条256级的链头部单级延时约18ps尾部能到28ps差了50%以上。所以量程计算不能简单用平均值乘级数最好在码密度校准之后用实际累积延时来标定。2.3 抽头采样方式触发器阵列与温度计码转二进制抽头采样的核心是用一组触发器在时钟沿同时锁存所有抽头的状态。这里有个关键细节触发器的建立保持时间会影响采样结果。如果脉冲在时钟沿附近到达可能采到亚稳态导致温度计码出现“气泡”bubble比如本该是11111000结果采成11101000。这种气泡如果不处理后续转二进制会出错。处理气泡的常规做法是“冒泡排序”或者“多数表决”。简单说就是扫描温度计码遇到0后面又出现1的情况就把那个孤立的1抹掉或者把孤立的0填上。更稳妥的方式是用优先级编码器加气泡抑制逻辑。我在实际项目里会先用一个简单的“前向扫描”模块从低位往高位扫一旦遇到第一个0后面全部强制为0。这样虽然会损失一点精度但能保证码值单调不会出现跳变。温度计码转二进制最直接的是用优先编码器priority encoder找到最高位的1的位置输出对应的二进制索引。但这样只用了“最后一个1”的位置前面所有抽头的状态都浪费了。更好的做法是统计1的个数也就是popcount这样能利用所有抽头的平均效应降低气泡影响。不过popcount的资源消耗大高速场景下不一定划算。我一般会在低速高精度场景用popcount高速场景用优先编码器加气泡抑制。3. 码密度校准的数学模型与实操推导3.1 码密度校准到底在校准什么先把这个概念说透。理想情况下延迟链每一级的延时相等那么一个随机脉冲落在每一级的概率也相等直方图应该是平的。但实际直方图是高低起伏的有的bin高有的bin低。码密度校准的本质就是用这个直方图的形状去反推每一级的真实延时。数学上怎么描述假设延迟链有N级第i级的延时是τ_i总延时T_total Στ_i。现在往链里灌M个随机脉冲第i级被命中的次数是h_i。那么第i级的延时估计值就是τ_i ≈ (h_i / M) × T_total这个公式的直觉是如果某一级延时大脉冲停留在这一级的时间就长被采样到的概率就高h_i就大。所以h_i正比于τ_i。把所有级的h_i归一化再乘以总延时就得到了每一级的实际延时。但这里有个循环依赖T_total本身是未知的它等于Στ_i。所以实际操作中我们通常用外部已知的时间间隔来标定T_total比如用一个已知脉宽的信号源打进去测出对应的总码值反推出T_total。或者用两个不同脉宽做两点标定同时解出增益和偏移。3.2 随机信号的产生为什么不能用周期信号码密度校准要求输入脉冲在时间上随机均匀分布。如果你用周期信号比如固定频率的方波那脉冲总是落在链的固定位置直方图只会出现几个尖峰根本没法覆盖所有bin。所以必须用随机信号。在FPGA里产生随机信号有几种办法。最简单的是用LFSR线性反馈移位寄存器产生伪随机序列然后跟一个高频时钟做比较产生随机抖动的脉冲。但LFSR的随机性有限周期长了之后会有规律性影响校准精度。更好的办法是用两个不同频率的时钟做异步采样利用时钟域之间的相位漂移产生天然的非均匀分布。比如用一个100MHz时钟和一个100.1MHz时钟两者的相位差会缓慢扫过整个周期等效于在时间轴上均匀扫描。我实测下来双时钟异步采样法效果最稳而且不需要额外的模拟电路。具体做法是被测信号用时钟A同步延迟链采样用时钟BA和B同源但频率略有差异。这样每次采样时脉冲相对于采样时钟的相位都在变化长时间统计下来就能覆盖所有bin。频率差一般取0.1%到1%差太小校准时间太长差太大相邻样本相关性太强。3.3 样本数量要多少才够从统计误差反推样本数M直接决定了校准精度。根据统计理论每个bin的计数h_i服从二项分布其相对标准差约为1/sqrt(h_i)。如果某个bin的h_i只有10那相对误差就有30%以上根本没法用。一般要求每个bin的计数至少几百到几千才能把相对误差压到几个百分点以内。假设链有256级每级平均计数要达到1000那总样本数M至少是256×1000256000。如果采样率是100MHz那需要2.56毫秒。听起来不长但如果你要做温度补偿每隔几秒就要重新校准一次这个开销就不能忽略了。所以实际项目中我会做权衡精度要求高的场景用大样本精度要求低的场景用小样本加插值。还有一个技巧非均匀分bin。因为链头链尾的延时差异大如果按等码值分bin有的bin样本多有的少。可以按预估延时来分bin让每个bin的样本数大致相等。不过这需要先做一次粗校准稍微麻烦一点。3.4 从直方图到查找表校准数据的存储与使用校准完成后你会得到一张表码值0对应时间0码值1对应时间τ_1码值2对应时间τ_1τ_2以此类推。这张表就是查找表LUT。实际测量时读到一个码值直接查表得到时间。存储这张表有几种方式。如果链级数少比如128级可以直接用分布式RAM或者寄存器阵列存。如果级数多用Block RAM更省资源。Xilinx的BRAM一个18Kb能存1024个18位数据256级的表只占一小块。但要注意查找表的位宽要够不然量化误差会限制精度。比如量程4ns要求1ps精度那至少需要12位2^1240964ns/4096≈1ps。实际我会用16位留足余量。查表本身可以用流水线做一个时钟读地址下一个时钟出数据不会成为速度瓶颈。如果要做实时校准还可以把LUT做成双端口一个端口读一个端口写校准和测量并行。4. 完整实现流程从RTL到上板实测4.1 工程搭建与关键约束先说工程结构。一个典型的TDC工程包含这几个模块延迟链本体、抽头采样触发器阵列、温度计码转二进制、码密度校准统计模块、查找表存储与查表模块、以及一个校准控制状态机。时钟方面至少需要两个时钟采样时钟和校准用的参考时钟如果做双时钟异步采样还需要第三个慢速时钟来产生相位漂移。约束文件是重中之重。延迟链的布局必须固定否则每次综合结果都不一样校准表就失效了。在Xilinx Vivado里我会用set_property LOC把CARRY4原语锁定到具体的SLICE位置用set_property BEL锁定到CARRY4站点。同时要加set_false_path或者set_max_delay约束防止工具对延迟链做时序优化。下面是一个约束示例# 锁定延迟链到指定SLICE set_property LOC SLICE_X10Y50 [get_cells delay_chain[0].CARRY4_inst] set_property BEL CARRY4 [get_cells delay_chain[0].CARRY4_inst] # 禁止对延迟链做时序优化 set_false_path -through [get_pins delay_chain[*].CARRY4_inst/CO[*]]Intel FPGAQuartus里对应的是set_location_assignment和set_instance_assignment逻辑类似。国产FPGA工具链的约束语法各有不同但核心思想一致固定位置、禁止优化。4.2 温度计码转二进制的Verilog实现细节温度计码转二进制我一般用两级流水线。第一级做气泡抑制第二级做优先编码。气泡抑制的逻辑是从低位往高位扫描维护一个“已经遇到0”的标志一旦标志置位后面所有位强制为0。这样保证输出是单调的。// 气泡抑制从低位到高位遇到第一个0后全部清零 always (posedge clk) begin bubble_free[0] thermo[0]; for (int i 1; i N; i i 1) begin bubble_free[i] thermo[i] bubble_free[i-1]; end end优先编码器可以用casez或者for循环实现。for循环综合出来是一串MUX延时较大高速场景下建议用树形结构。我实测过256级的优先编码器用树形结构能把关键路径压到3ns以内跑300MHz没问题。4.3 码密度校准状态机的设计校准状态机是整个模块的“大脑”。它的流程是进入校准模式打开随机信号源清零直方图计数器等待M个样本关闭信号源计算累积延时写入查找表退出校准模式。整个过程可以用一个简单的三段式状态机实现。这里有个细节校准期间不能进行正常测量否则测量脉冲会污染直方图。所以要么分时复用要么用两套延迟链一套测量一套校准。分时复用省资源但会有死区时间双链并行无死区但资源翻倍。我一般在对实时性要求不高的场景用分时复用比如每秒校准一次死区几毫秒可以接受。直方图计数器的位宽要算好。如果M256000每个bin最多可能计到几万那至少需要16位。256个bin就是256×164096位用分布式RAM或者BRAM都行。我习惯用BRAM一个36Kb的BRAM能存2048个18位数据256个bin只占八分之一。4.4 上板实测从“码值乱跳”到“稳定输出”第一次上板大概率会遇到码值乱跳的问题。别慌按这个顺序排查先看时钟是否稳定用示波器测采样时钟的抖动如果抖动超过LSB那码值肯定跳。再看延迟链的约束是否生效在Vivado里打开Implemented Design看CARRY4是不是真的锁在了指定位置。然后看温度计码有没有气泡如果气泡多说明采样时刻太靠近脉冲边沿需要调整采样时钟的相位。我踩过最坑的一次是延迟链约束没问题时钟也稳但码值就是每隔几秒跳一次。查了半天发现是电源纹波导致的。FPGA的进位链延时对电源电压非常敏感电压波动10mV延时可能变1%。后来在电源引脚旁边加了几个低ESR的陶瓷电容问题就解决了。所以做高精度TDC电源完整性比信号完整性还重要。实测数据方面我用一条256级的链在Artix-7上跑未校准前非线性误差INL能到±8LSB校准后压到±0.5LSB以内。分辨率约22ps量程约5.6ns。温度从25度升到60度未补偿时码值漂移约3LSB加了温度补偿后压到0.5LSB以内。5. 常见问题与排查技巧实录5.1 码密度直方图不平怎么办直方图不平是常态关键看起伏有多大。如果某个bin的计数只有平均值的十分之一那这个bin对应的延迟单元可能有问题比如被工具优化掉了或者布线异常。排查方法是先看综合报告确认所有CARRY4都例化了再看布局图确认链是连续的没有断点。如果链是连续的但某个bin特别低可能是那个SLICE的负载特别重可以尝试手动调整布局把负载均衡一下。另一个常见原因是随机信号不够随机。如果直方图出现周期性起伏那多半是随机源有规律性。换成双时钟异步采样或者把LFSR的抽头换一下通常能改善。5.2 校准后精度还是上不去校准后精度上不去先确认查找表的位宽够不够。如果量程5nsLUT只有8位那量化误差就有20ps比LSB还大。其次看校准样本数够不够样本太少每个bin的统计误差就大。最后看温度漂移如果校准后过了几分钟精度变差那就是温漂没补偿。温漂补偿的做法是在芯片里放一个温度传感器Xilinx有XADCIntel有TSDC定期读温度根据预先测好的温度-延时曲线做修正。或者更简单粗暴每隔固定时间重新校准一次用时间换精度。5.3 资源不够怎么省资源紧张的时候可以砍这几块第一减少链级数量程够用就行第二用优先编码器代替popcount省掉大量加法器第三直方图用分布式RAM代替BRAM虽然容量小但更灵活第四校准状态机用低速时钟省功耗。我做过一个极简版128级链只用了不到200个LUT和128个FF跑在200MHz分辨率30ps够很多场景用了。5.4 常见问题速查表现象可能原因排查方法解决措施码值乱跳时钟抖动大示波器测时钟换低抖动时钟源码值乱跳电源纹波大示波器测电源加去耦电容直方图有尖峰随机源不随机检查LFSR或时钟换双时钟异步采样直方图某bin极低延迟单元被优化看综合报告加keep属性校准后精度差LUT位宽不够算量化误差增加LUT位宽校准后精度差样本数不够算统计误差增加校准时间温漂明显未做温度补偿读温度传感器加温度修正或定期重校资源不够链级数太多算量程需求减少级数资源不够popcount太耗资源看资源报告换优先编码器提示延迟链的布局一旦固定就不要轻易改约束。每次改约束都要重新校准而且旧校准表作废。建议把约束文件和校准表一起版本管理。5.5 几个我踩过的坑和私房技巧第一个坑别用工具自动布局。我早期偷懒没锁CARRY4位置结果每次综合出来的链延时都不一样校准表完全没法复用。后来老老实实锁位置虽然麻烦但一次校准能用很久。第二个坑采样触发器的时钟要用全局时钟。我试过用局部时钟结果时钟偏斜skew导致不同抽头的采样时刻不一致温度计码全是气泡。换成BUFG之后问题消失。第三个技巧校准的时候多灌一点样本但别太多。样本太多校准时间长而且收益递减。我一般取每个bin平均1000到2000个样本再多了精度提升不明显。第四个技巧查找表可以做插值。如果链级数少LUT点稀疏可以在两个码值之间做线性插值等效于提高分辨率。比如256级的链插值后等效512级分辨率翻倍。当然插值会引入一点误差但比直接量化好得多。第五个技巧用ILA抓实时波形。Vivado的ILA是调试TDC的神器。把温度计码、二进制码、查找表输出都接到ILA上触发条件设成码值跳变一眼就能看出问题在哪。我很多bug都是靠ILA定位的。6. 写在最后一些个人体会做TDC这几年我最大的感受是原理不难难在细节。延迟链的搭建、码密度校准的数学看几篇论文就能懂但真正上板跑起来电源、时钟、布局、温度每一个都能让你前功尽弃。我见过太多人卡在“校准后精度还是不行”这一步最后放弃。我的建议是先把一条最简单的链跑通哪怕只有64级、分辨率50ps先看到码值随脉宽线性变化再逐步加校准、加补偿、加优化。不要一上来就追求1ps分辨率那只会让你在细节里迷失。另外码密度校准不是万能的。它只能修正延迟链的静态非线性对动态非线性比如电源噪声引起的抖动无能为力。如果你的应用对动态精度要求高那还得从电源和时钟入手校准只是最后一道防线。后续我打算再写一篇专门讲延迟链的优化技巧包括怎么用IDELAY、怎么混合进位链和布线资源、怎么做多链拼接扩展量程、以及温度补偿的具体实现。那部分内容更偏工程实战感兴趣的朋友可以关注一下。
返回列表