ARTICLE DETAIL

资讯详情

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

FPGA中Carry4实现高精度TDC:延迟链、编码与校准全解析

FPGA中Carry4实现高精度TDC:延迟链、编码与校准全解析 简介本资源是一份面向FPGA工程师与数字电路设计学习者的高精度时间数字转换器TDC实现方案聚焦于利用Xilinx FPGA原生Carry4逻辑单元构建低延迟、高分辨率TDC适用于高速信号采样、精密时间测量及锁相环等对时序敏感的应用场景。压缩包共338个文件涵盖28个Verilog源码.v、27个综合数据库.sdb、25个文本说明.txt、20个XML配置、19个仿真脚本.do/.sh、12个报告文件.rpt、7个约束文件.xdc及1个可编程比特流.bit完整覆盖从Vivado工程搭建、Carry4计数器/比较器IP集成、时序约束到硬件验证的全流程。已有208人下载学习。资源包含多级仿真批处理脚本如compile.bat、simulate.bat、runme.bat、XSIM仿真配置xsim.ini.bak、调试辅助文件.wdb/.dbg及详细日志与报告便于快速复现、调试与性能分析特别适合深入理解Carry4底层进位链优化机制与TDC误差校正实践。 做高精度时间间隔测量的时候FPGA里的TDCTime-to-Digital Converter时间数字转换器一直是绕不开的话题。很多场景——比如激光测距、PET成像、原子物理实验、单光子探测——都需要把时间差量化到百皮秒甚至几十皮秒量级。专用TDC芯片比如ACAM的GPX、TI的TDC7200确实好用但价格高、通道数固定、集成度也受限。于是不少人把目光转向FPGA用芯片内部最底层的进位链逻辑来实现TDC而Xilinx 7系列及之后架构里的Carry4就是这条路上最常用的那块“砖”。这篇东西不是教科书式的原理堆砌是我在实际项目里用Carry4搭TDC、调分辨率、压非线性的过程记录。里面会讲清楚Carry4到底能做多细、延迟链怎么搭、编码器怎么设计、校准怎么处理也会把那些文档里不会写的坑——比如气泡问题、码宽不均、温度漂移——一个个拆开讲。适合正在做或者准备做FPGA-TDC的工程师也适合想了解高精度时间测量底层逻辑的同学。1. 整体设计思路拆解为什么选Carry4做精细测量1.1 TDC的核心问题与分层测量架构先说清楚一个基础问题TDC到底在测什么给定一个起始信号Start和一个结束信号Stop我们要量化两者之间的时间差Δt。最简单粗暴的办法是用一个高频计数器对基准时钟计数。比如500MHz时钟周期2ns计数器分辨率就是2ns。但2ns对很多应用来说太粗了激光测距要求亚厘米级精度对应的时间分辨率需要优于100ps普通计数器方案远远不够。要提高分辨率就得把“一个时钟周期内的时间余量”测出来。于是出现了经典的粗计数细计数的分层结构粗计数用基准时钟对整个时间段计数分辨率等于时钟周期T_clk细计数用延迟链测出Start信号边沿相对下一个时钟边沿的时间差、以及Stop信号边沿相对下一个时钟边沿的时间差两者相减得到精确的时间间隔。这里的核心逻辑是时钟周期是“标准尺子”延迟链是“游标卡尺的细分刻度”。Carry4就是用来制作这把游标卡尺刻度的。最终的时间测量结果可以写为Δt (N粗 × T_clk) T_fine_stop - T_fine_startN粗是粗计数器值T_fine是细计数测得的余量。这套架构几乎所有FPGA-TDC都在用区别只在于细计数的实现方式。1.2 为什么是Carry4而不是LUT延迟链或专用延迟单元FPGA内部可用的延迟资源有好几种LUT查找表、导线Routing、以及专用的进位链。理论上LUT也可以作为延迟单元——LUT配置成直通输入到输出有一定延迟大约40~60ps一级。但LUT延迟受配置和布线影响很大每一级的延迟波动也大而且LUT分散在不同的Slice里要把它们串起来只能走通用布线资源延迟无法精确控制。更麻烦的是布线延迟随布局布线结果变化稍微挪动位置延迟特性就完全变了设计不可复现。Carry4就不一样。它是Xilinx FPGA里专门为算术运算设计的进位链结构物理上把4个Slice内的进位逻辑串联在一起Cin到Cout的延迟非常短且固定。在7系列Artix-7、Kintex-7上单个进位级延迟大约在15~25ps量级具体值和工艺、温度、电压相关同一片芯片内一致性很好。更关键的是Carry4链是“硬连线”的物理结构不经过通用布线天然适合做延迟线。做个简单对比延迟实现方式单级延迟一致性受布线影响适用性LUT直通链60~100ps差大不推荐普通逻辑门布线不可控差极大不推荐Carry4进位链15~25ps较好极小推荐选Carry4还有一个实际考量它位于每个Slice内排列整齐用位置约束可以精确控制延迟链的物理位置保证多通道TDC的一致性。这是LUT链完全做不到的。1.3 系统架构的顶层规划在设计之初就要把整个系统框架定下来不能一上来就写代码。我习惯先画一个功能框图思路层面的不是文档要求的那种流程图输入信号经过IBUF进入FPGA分成两路——一路触发粗计数器另一路进入Carry4延迟链延迟链的每个抽头输出被一组D触发器寄存器快照锁存当前信号传播到的位置快照结果送编码器编码器输出“信号在第几个抽头”即细计数结果粗计数器结果和细计数结果合并通过FIFO送入后续数据处理模块。这个架构里需要注意几个设计参数延迟链级数决定测量范围细计数最大可测范围不超过1个基准时钟周期所以延迟链总延迟要覆盖一个时钟周期并留一定余量通常做到1.2~1.5倍T_clk抽头密度决定分辨率寄存器快照的时钟就是基准时钟编码器决定延迟测量的准确度。采用这种结构的好处是把模拟域的时间量转换成了数字域的“位置泊位”量后续处理全部在数字域完成稳定可靠。2. 核心细节解析与实操要点延迟链、快照与编码器的硬骨头2.1 延迟链的实现Carry4原语例化与进位链抽头先明确一件事直接用号写加法器综合器会自动推断出Carry4进位链但整个进位链的网络名、层级关系不可控而且综合器可能会做优化、重定时导致延迟链行为不稳定。做TDC时我强烈建议手动例化CARRY4原语把进位链完全控在自己手里。CARRY4原语在Xilinx 7系列里的端口包括CO[3:0]进位输出也是我们要用的抽头信号O[3:0]加法器求和输出CI进位输入CYINIT链初始值通常接0DI[3:0]加法器数据输入S[3:0]加法器功能选择输入。要把Carry4变成一条延迟线关键是让进位信号能够自由地穿透整条链。做法是把S全部固定为1DI全部固定为0这时Cout Cin进位信号从CI直接传到CO每一级CO都代表着相同的信号只是经过了不同的延迟级数。下面是延迟链的实例化代码片段8级Carry4共32个抽头实际使用通常需要几十到上百级module tdc_carry_chain #( parameter CHAIN_LEN 32 // 抽头数 )( input wire clk, input wire hit_signal, // 被测信号 output wire [CHAIN_LEN-1:0] taps ); wire [CHAIN_LEN:0] c; assign c[0] hit_signal; genvar i; generate for (i 0; i CHAIN_LEN; i i 4) begin : gen_chain CARRY4 carry4_inst ( .CO (c[i4] - 1? no, wait), // 注意CO是4位向量 .O (), .CI (c[i]), .CYINIT (1b0), .DI (4b0000), .S (4b1111) ); end endgenerate // 这里需要对CO的每一位抽头做寄存器快照 // 实际抽头连接方式第一级CARRY4的CO[0] c[1], CO[1] c[2], CO[2] c[3], CO[3] c[4] endmodule注意CARRY4是4位一组的单元一个原语提供4个进位级。抽头取的信号是每一级的进位输出CO也就是信号在进位链上传播到的位置。hit_signal从第一级的CI进入经过第一个进位级后出现在CO[0]再经过一级出现在第二个CARRY4的CO[0]以此类推。取抽头时要把每一级的CO[0]、CO[1]、CO[2]、CO[3]都引出来连到快照寄存器。有些设计会在输入加一个IBUF和两级同步寄存器减少输入信号亚稳态的影响。但如果信号本身和基准时钟不同步异步信号进入延迟链时抽头位置可能落在寄存器采样窗口内导致亚稳态。这就要靠后面的“气泡消除”逻辑来处理。2.2 快照寄存器的布局与亚稳态处理把模拟量定格在时钟沿延迟链输出的是一个瞬态信号在某个时钟沿到来时刻信号在链里传播到的位置决定了抽头输出。我们需要用基准时钟的边沿把这组抽头值锁存下来这一组锁存器就是“快照寄存器”。快照寄存器的位置约束非常关键。每个抽头信号必须就近锁存在同一Slice内的寄存器里不能跨Slice布线。原因是如果抽头信号走了一段通用布线再到寄存器布线延迟会叠加到测量路径上破坏延迟链的一致性和分辨率。Vivado里可以给抽头寄存器加位置约束让它们落在对应Carry4所在的Slice内或者依赖综合器自动布局但必须在综合属性中关闭对延迟链的优化。亚稳态问题在TDC里很典型。当信号边沿恰好落在时钟采样窗口内寄存器输出不是稳定的0或1而是介于两者之间的状态导致后续编码结果出现“气泡”——就是理论上的单调0→1跳变中间插入了不合逻辑的值比如01011而不是00111。这个问题没法完全避免只能缓解和纠正。常用手段包括在编码器前先用多级寄存器打拍但要注意会增加延迟影响时序对相邻抽头做表决处理相邻两路的值取多数消除单点亚稳态在编码结果中做合法性检查出现非法模式时按最近的有效边界修正。我对快照寄存器的启动顺序有一点心得先用Initial语句或复位信号把所有快照寄存器清零然后等延迟链稳定后再使能采样。这样能避免上电瞬间伪信号造成错误触发。2.3 编码器设计二分法、查找表与气泡修正延迟链抽头值“理应”是一串连续的1后面跟着一串连续的0或者反过来取决于信号初始电平编码器的任务就是找出0/1边界的位置。理论上这个位置编号就正比于时间间隔。最简单的编码器是Priority Encoder——用for循环找第一个0出现的位置always (*) begin position 0; for (i 0; i CHAIN_LEN; i i 1) begin if (taps[i] 1b0) position i; end end但直接在FPGA里写for循环会让综合器生成一条长长的比较器链组合逻辑延迟大时序容易崩。更常用的做法是二分法先判断链条后半段是否全0或全1如果是把搜索范围减半再在半个链内重复。这样形成一棵类似于比较器树的逻辑关键路径是log2(N)级而不是N级。实际工程里我更喜欢混合方案先用二分法定位到某一段比如32级一段段内再用小范围查找表LUT或者Case语句精确找边界。整套编码器的逻辑延迟可以控制在几纳秒内完全不影响流水线吞吐。气泡修正也很重要。常规做法是扫描整个抽头向量记录相邻抽头从1变0的位置如果出现多个边界例如01010就取最靠后的稳定边界——因为亚稳态通常发生在信号传播前沿后面的位置更可靠。更严谨的做法是做多拍采样用两拍的结果做“与”或者“或”运算剔除瞬态错误。2.4 为什么用Wave Union或码密度校准之前先要理解码宽不均很多初学者会问既然Carry4延迟很均匀为什么还要校准答案是Carry4静态延迟确实比较均匀但抽头到寄存器的布线延迟、寄存器本身的建立保持时间、以及不同Slice内的物理差异会导致每个bin码位的实际宽度并不相同。有的bin可能只有15ps有的可能达到40ps。如果直接用bin编号乘以平均延迟来算时间误差会很大。码宽不均的处理有两种层面。第一种是“码密度校准”也叫做直方图校准把一个随机时间分布信号大量输入TDC统计每个bin出现的次数。因为随机信号的时间分布是均匀的每个bin出现的次数就正比于它的宽度。统计足够多次通常几十万次就能得到每个bin的权重后续测量时先查表加权再累加。这样可以把TDC的有效分辨率提升到接近单bin的最低水平。第二种是“Wave Union”方法思路是让同一个信号在延迟链里产生多个传播边沿比如把脉冲信号处理后形成双沿或更复杂的波形一次测量得到多个boundary位置对它们做加权平均等效于把码宽误差随机化了。这个方法能显著降低微分非线性DNL但对资源消耗较大实际项目里按需选用。我个人的建议是基础测量用码密度校准表就够了先把DNL压下去如果追求极致精度再上Wave Union或游标卡尺法两个振荡器/两条延迟链的拍频测量。3. 实操过程与核心环节实现从代码到上板调试的完整流程3.1 工程搭建与延迟链长度计算开始写代码前先算清楚延迟链要多长。假设基准时钟是250MHz周期4ns。单片Artix-7在典型条件下Carry4单级延迟约20ps那么覆盖4ns需要200级。留出30%余量做到260级左右。级数太多也有问题信号在高延迟链上传播时受电源抖动和温度漂移影响更大且快照寄存器和编码器资源消耗增大。所以最好遵循“刚好覆盖1.2~1.5个时钟周期”的原则。延时估算示例参数数值说明基准时钟频率250 MHz周期4 ns单级Carry4延迟~20 ps典型值受温度电压影响理论所需级数200 级4ns / 20ps实际取级数260~280 级留余量覆盖极端温度电压抽头寄存器数量260~280 个每个抽头一个FF工程文件组织上我习惯把TDC做成独立模块端口只暴露start、stop输入和结果输出。顶层文件里例化两个TDC实例——一个测start余量一个测stop余量两个结果相减才是最终时间差。这里要特别强调的是start和stop对应的两条延迟链必须物理上靠近最好放在同一个时钟区域并且用位置约束让两条链平行排列这样两条链的温漂特性才能近似一致差模误差才小。3.2 代码实现延迟链、快照寄存器和编码器下面是一段简化的完整TDC核心模块代码。它包含延迟链、快照寄存器和简单的二分编码逻辑。module tdc_line #( parameter TAPS 256 )( input wire clk, // 基准时钟 input wire rst_n, input wire hit, // 待测信号异步 output reg [15:0] time_code, // 细计数结果 output reg valid ); // 进位链 wire [TAPS:0] c; assign c[0] hit; genvar i; generate for (i 0; i TAPS; i i 4) begin : gen_c4 CARRY4 u_c4 ( .CO (c[i4]), .O (), .CI (c[i]), .CYINIT (1b0), .DI (4b0000), .S (4b1111) ); end endgenerate // 快照寄存器 reg [TAPS-1:0] taps_r; always (posedge clk or negedge rst_n) begin if (!rst_n) taps_r {TAPS{1b0}}; else taps_r c[TAPS:1]; end // 编码找第一个0的位置简化版实际工程用二分/查找表 reg [15:0] pos; integer j; always (*) begin pos 16d0; for (j TAPS-1; j 0; j j - 1) begin if (taps_r[j] 1b0) pos j; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin time_code 16d0; valid 1b0; end else begin time_code pos; valid 1b1; end end endmodule这段代码能综合通过也能跑出基本结果但直接用于高精度测量是不够的。原因是for循环编码器的组合逻辑链太长261个比较器的级联会让时序收敛困难。实际工程中我先把taps_r按字节分段每8个抽头一组先看每组是否全1再用一个小LUT查组内的精确位置。这样两级查询就完成编码路径短时序好。生成CARRY4时还要加(* DONT_TOUCH TRUE *)属性防止综合器把进位链优化掉。如果用的是Vivado还需要在综合设置里关闭“register balancing”或者对该模块设置KEEP_HIERARCHY。3.3 约束与布局把延迟链钉在指定位置代码写完后时序约束是决定成败的关键。TDC的延迟链往往不满足普通同步设计的建立保持时间——它的信号路径长度本身就在一个时钟周期量级和时钟没有确定相位关系所以不能给它设常规的create_clock路径约束。我通常的做法是对抽头快照寄存器设置FALSE_PATH告诉时序工具不需要分析这条路径的时序对延迟链里每个CARRY4设置位置约束用set_property LOC SLICE_XxxxYxxx把链条锁定在指定位置用create_pblock把整个TDC区域圈起来防止其他逻辑侵占链周围的布线资源保证两个TDC通道的位置对称减少通道间偏斜。位置约束的数值取决于具体芯片。比如Artix-7 XC7A35TSlice坐标从SLICE_X0Y0到SLICE_X55Y32不等把延迟链放在芯片中间偏左或偏右的连续区域即可。注意看Device视图确保CARRY4在物理上是一条纵向直线这样延迟加成最均匀。如果不上位置约束Vivado会自动布局大概率会把CARRY4分散得到处都是抽头信号来回绕线延迟一致性完全失控。这一点务必重视。3.4 上板调试与数据采集上板后第一步不是急着看分辨率而是先看延迟链是否“铺满”了。用信号发生器给hit一个与基准时钟无固定相位关系的随机信号把taps_r引到ILA集成逻辑分析仪里观察。正常情况下taps_r应该呈现出一段连续的1和一段连续的0边界位置随时间随机变化。如果看到大面积的毛刺、随机跳变先检查三件事复位时序对不对、DONT_TOUCH设置有没有生效、快照寄存器和CARRY4是不是离得太远。我发现最常见的原因是综合器把S输入做了优化导致进位链没按预期级联。接下来是码密度统计。写一个简单的数据采集模块连续触发N次把每次的编码位置存入BRAM结束后从串口输出。用Python或MATLAB做直方图统计你会看到每个bin的计数理想情况是均匀的实际会有起伏。计数的起伏比例就是各码宽的粗略结果。统计后生成一个校准表存到ROM里后续每次测量根据bin编号查找时间权重累加得出精确间隔。4. 常见问题与排查技巧实录那些调试到凌晨的坑4.1 气泡问题为什么抽头出现多个0/1边界抽头向量在理想情况下是单调的比如0000...1111边界只有一个。但实际采样时由于信号在延迟链上传播是连续的某个抽头恰好处于建立保持时间窗口内导致该寄存器输出不确定产生类似000011011111的“气泡”。气泡会让编码器得到错误的边界位置对时间测量造成大误差。排查方法用ILA抓taps_r如果在同一时刻看到非单调的多处跳变说明有亚稳态或时钟偏斜问题。先用多拍聚合消除单点抖动比如把相邻三个抽头做多数表决输出稳定值。其次可以降低时钟频率扩大采样窗口减少亚稳态概率。还要检查时钟树确保快照寄存器的时钟偏斜尽量小。如果气泡出现在远离信号传播前沿的位置通常是布局布线引起的抽头间延迟差异过大需要检查位置约束是否生效。4.2 码宽严重不均直方图校准的正确姿势码密度校准要求输入信号是均匀随机分布的。实际调试时用信号发生器产生一个低频方波比如1kHz且不与基准时钟同步时间间隔相对于时钟周期是均匀分布的就可以认为是随机信号。采集几十万次编码结果统计每个bin的出现次数。设总次数为N_total总时间跨度为T_range等于一个时钟周期每个bin的理论时间宽度为bin_width(i) (count(i) / N_total) × T_range把每个bin的宽度存成查找表测量时累加0到position所有bin的宽度就是精确的细计数时间。这一表几乎解决所有静态DNL问题。如果某个bin的计数为0或特别大优先检查该bin对应的Carry4是否被其他逻辑干扰或者该抽头到寄存器的布线走得太远。4.3 温度漂移测量结果随时间变化Carry4的延迟会随温度升高而增大随电压升高而减小。温度漂移会让码宽整体膨胀或收缩如果校准表是固定不变的测量误差就会随时间累积。实际项目中我的做法是把校准表作为基准另做一个“实时码宽跟踪”模块——每隔一段时间测一次已知的时间间隔比如由FPGA内部产生的、长度固定的脉冲信号反推当前平均码宽对校准表做整体缩放。这样能补偿大部分温漂和电压漂移。对精度要求极高的场景可以加恒温或温度传感器补偿但多数FPGA-TDC应用用软件补偿就够了。4.4 常见问题速查表现象可能原因排查方法解决方法抽头全0或全1信号未传入链/CARRY4未生效查输入IBUF、查S/DI设置检查CARRY4配置加DONT_TOUCH抽头多边界亚稳态/时钟偏斜ILA抓波形看是否非单调多拍聚合/表决优化时钟约束分辨率远低于预期抽头太疏/布线延迟大查码密度直方图增加链级数加位置约束两通道测量不一致两条链布局不对称查看Device布局对称约束两条链位置温度变化后误差增大码宽漂移测量已知间隔实时码宽跟踪与软件补偿综合后链被优化掉未设置保持属性查看综合网表加DONT_TOUCH/KEEP_HIERARCHY编码结果跳变异常编码器组合逻辑太深/竞争查时序报告二分法/分段编码器4.5 我的调试顺序建议给刚开始做TDC的朋友一个可复现的调试路径第一步先用最简单的代码让延迟链工作起来只验证taps_r是否呈现单调波形第二步加编码器用ILA观察编码结果是否连续稳定第三步做码密度统计看直方图分布第四步做线性度测试用可调延迟线给已知时间间隔对比测量值和真实值最后一步才是优化温度特性和通道一致性。这个顺序能把你从“一个复杂的系统”里解放出来每次只解决一个问题。我第一次做的时候跳过第二步直接上全套逻辑结果出问题时根本不知道是延迟链坏了还是编码器坏了排查浪费了很多时间。5. 量化精度与系统级优化方向5.1 分辨率能做到多少理论值和实测值的差距很多文章会给出“分辨率达到XX ps”的结论但不同的人实测结果差异很大。原因在于“分辨率”有几种不同的定义单次测量的标准偏差jitter、多个bin加权后的有效分辨率、以及INL积分非线性修正后的精度。粗看码密度校准后一个260级的链在250MHz下能实现约15ps的平均量化步进但实际测量结果的抖动还包括输入信号的jitter、时钟抖动、以及链上的电源噪声通常综合下来单次测量标准差在20~40ps比较常见。理解这一点很重要不要被“Carry4有20ps分辨率”这种话迷惑。20ps只是量化步长不等于你的系统精度。真实精度是量化误差、时钟抖动、信号jitter、噪声和温度漂移的总和。设计TDC时除了关注Carry4本身还要注意输入信号调理——用高速比较器把模拟信号转成数字边沿避免慢沿信号在延迟链里引入额外抖动。5.2 多通道扩展与资源开销一个通道的TDC链路比如260级大约消耗260个CARRY4和260个FF。对7系列中规模芯片来说资源完全不是瓶颈。多通道设计时要注意的是多条延迟链不能共享同一条物理位置否则布局会互相挤占导致一致性恶化。一种实用的多通道方案是用“多个bank”的布局方式把通道1放在SLICE_X20Y0到Y50区域通道2放在X30Y0到Y50区域两条链保持相同的纵向跨度。再用一个共享编码器轮流处理各通道数据时间串行或者给每条链配独立编码器时间并行。多通道还有一个同步问题各通道的粗计数器必须严格对齐。可以在FPGA里用同一组粗计数器但每个通道单独采样或者在所有通道的输出端做一个统一的时刻标签处理保证最后结果的时间基准一致。5.3 从Carry4 TDC到系统级时间测量Carry4做的TDC模块只是一个“时间量化器”要真正解决一个测量问题还需要外围配合。以激光测距为例发射光脉冲时产生一个Start信号接收光电探测器输出Stop信号经过比较器整形TDC测出两者间隔再乘光速除以2就是距离。这时TDC的量程需要覆盖整个飞行时间范围比如300m测距对应2μs飞行时间粗计数器要能计到几百个周期。我建议把TDC封装成一个小IP对外只暴露start/stop和结果总线内部把延迟链、编码器、校准表、粗计数器都打包好。这样在上层应用切换时TDC模块可以复用只改量程参数即可。文章到这里核心技术点都讲完了。最后说一点自己的体会TDC设计入门容易、做精难。Carry4只是给你提供了一个约20ps的“刻度”真正决定精度的永远是对亚稳态、非线性、温漂这些细节的把控。别迷信某个“精确延迟值”用码密度校准和系统测试去标定才是正路。如果正在做类似项目建议先在开发板上把延迟链跑通用ILA观察抽头波形再逐步加编码、校准、应用逻辑。每一步都验证到位高精度TDC并没有那么神秘。本文还有配套的精品资源点击获取
返回列表