☰
FPGA进位链实现高精度TDC:从原理到后仿真全解析
2026/9/29 15:53:17 网站建设 项目流程

从第一次接触TDC这个词,到真正在Xilinx FPGA上把进位链方案跑通,我自己前后折腾了快两个月。这个项目的起因其实很简单:一个激光测距的需求,要求时间分辨率做到几十皮秒量级,同时要能把数据稳定读回到上位机。一开始我也考虑过专用TDC芯片,比如TI的TDC7200或者ACAM的芯片,但当时手头已经有一块Kintex-7的板子,而且后续还要做多通道扩展,专用芯片的通道数和接口灵活性都不太够。于是决定在FPGA内部用进位链直接搭一个高精度TDC。

先说结论:如果方案设计合理、约束正确、后仿真验证充分,Xilinx 7系列FPGA的进位链TDC做到等效10ps级别的有效分辨率是完全可行的。但这条路绝不是把LUT和寄存器连起来就行,里面全是细节。这篇博文我就从原理到后仿真,把完整链路和踩过的坑一起整理出来,希望对准备用FPGA做精密时间测量的朋友有帮助。

1. 为什么用FPGA进位链做TDC,这个方案到底赢在哪

1.1 TDC是什么,什么场景需要它

TDC(Time-to-Digital Converter,时间数字转换器)的核心任务就是把两个事件之间的时间间隔转换成一个数字量。常见应用包括激光测距、正电子发射断层扫描(PET)、高能物理实验中的粒子飞行时间测量、核医学仪器、高精度频率计等。

以激光测距为例:激光发出到收到回波之间的时间差大约是纳秒到微秒量级,每1ns对应约15cm的飞行距离。如果要求测距精度达到毫米级,时间分辨率就必须做到几皮秒级别。这样的精度要求下,直接用计数器数时钟周期显然是做不到的——就算是1GHz的时钟,周期也还有1ns。所以就需要在计数器粗测的基础上,加一个细时间测量模块,用来测量待测信号与时钟沿之间的微小相位差。TDC干的就是这件事。

1.2 FPGA内部有什么可以做延迟线

那为什么选进位链而不选别的资源?在Xilinx 7系列FPGA中,可用来做精细延迟的有几个选择:LUT的传播延迟、布线延迟、进位链延迟、甚至DSP内部的专用路径。但在实际工程中,绝大部分高精度TDC设计都会选择CARRY4进位链,原因有几点。

第一,进位链是物理上紧挨着的专用资源。Xilinx 7系列FPGA中,CARRY4是专门为高效实现加法器进位传播而设计的基本单元,每个CARRY4包含4个级联的进位位,并且CARRY4与CARRY4之间的连接走的是固定且极短的专用布线资源。这种物理特性决定了进位链各级之间的延迟相对均匀、确定性高,同时环境温度电压变化时,整条链的延迟变化趋势也保持一致。换用普通LUT和布线来实现延迟线,时延会严重受到布局布线结果影响,每次重新实现延迟都不一样,根本无法控制。

第二,进位链延迟足够小。在7系列FPGA中,单个进位位的延迟通常在20ps到50ps之间,视具体器件型号、电压、温度而定。这个量级决定了单通道TDC的粒度足够做到皮秒级别。相比之下,普通布线单位延迟动辄几百皮秒,LUT输入到输出的延迟也在100ps左右波动,做粗校准还行,想做到精密测量就很难了。

第三,进位链级联非常方便。CARRY4可以直接把前一级的CARRYOUT连接到下一级的CARRYIN,不需要额外的布线资源。这样一来,一条长延迟线可以由数十个甚至数百个CARRY4首尾相连组成,每一级都有一个可供采样的抽头。在FPGA内部做这样一条延迟线,占用的资源是非常可控的,而且不会对周围逻辑产生明显的布局负担。

不过有一点要提前说清楚:进位链的延迟虽然相对均匀,但绝不是均匀的。实际制造工艺决定了每一级的延迟都有偏差,有的级偏大,有的级偏小。因此,基于进位链的TDC并不是直接读“1的个数”就能得到准确时间,必须做码密度校准(Code Density Calibration)来标定每级延迟的真实宽度。后面我会专门讲。

1.3 与专用TDC芯片方案的对比

很多人在做精密时间测量时会在“FPGA内实现TDC”和“外挂专用TDC芯片”之间纠结。我的建议是,如果只是做一个通道、对成本不敏感、接口要求简单,那么专用芯片可以直接用,开发周期短。但如果需要多通道并行测量、通道间延迟差要一致、板级面积受限,或者需要把时间测量和大量逻辑处理(比如符合判断、直方图统计)集成到同一颗芯片里,那么FPGA内实现TDC就是更有吸引力的方案。

另外,FPGA方案在数据传输上有天然优势。专用TDC芯片通常通过SPI或并行FIFO接口输出数据,数据率有限。而FPGA内部TDC的测量结果可以直接接入自己的FIFO、DMA、以太网或PCIe链路,数据吞吐能力完全不是同一个量级。高能物理实验里常见的几千通道TDC阵列,目前绝大多数都是靠FPGA的进位链方案实现的。

还有一点容易被忽略:FPGA方案的起始时间和恢复时间完全由FPGA内部逻辑控制,可以做非常灵活的触发和自触发测量。这在某些精密测量应用中非常重要,例如一边发送激励脉冲一边回读反射波,时间基准可以完全统一在同一个FPGA内,不必担心外部芯片间的同步问题。

2. 用Carry Chain搭TDC的整体设计与核心编码方案

2.1 基本架构:粗计数与细时间测量结合

一个完整的TDC系统,从整体架构上看,可以分成三大块:粗计数器、细时间测量模块、数据编码与读出模块。

粗计数器负责数整周期。假设系统时钟是250MHz,那么一个时钟周期是4ns。粗计数器记录被测事件发生在第几个时钟周期,能覆盖的最大时间范围就是计数器位宽乘以4ns。比如用一个24位计数器,可以覆盖约67ms,完全足够绝大多数测量场景。

细时间测量模块就是进位链TDC的核心,要测量的是被测信号边沿与系统时钟边沿之间的时间差。这个时间差是小于一个时钟周期的,也就是0到4ns之间。用进位链去测量这个时间段,再把粗计数器和细时间结果合并,就得到了一个高精度的时间戳。

用公式表示就是:

T_measure = (N_coarse * T_clk) + T_fine

其中,N_coarse是粗计数器的值,T_clk是系统时钟周期,T_fine是进位链测量得到的亚时钟周期时间差。

还有一个同样重要的思路是“起始-停止”式测量。激光测距这类场景中,有时会同时存在起始信号和停止信号,需要测量这两个信号之间的间隔,而不是测量相对于时钟的绝对时间。这种情况下,可以设计成两个TDC通道分别测起始信号和停止信号相对时钟的时间差,再做差值即可。这种做法在同步多通道系统中非常常见,因为每个通道都只做相对同一个时钟的时间测量,不存在跨时钟域同步问题。

2.2 进位链延迟线如何工作

进位链TDC的核心是一条延迟线和一组采样寄存器。

具体做法是,把一串CARRY4的进位输出逐级引出,接到一组D触发器的数据输入端,然后用系统时钟作为这组触发器的采样时钟。当待测信号的边沿在进位链上传播时,系统时钟的上升沿到来,所有D触发器同时采样链上各点的状态,从而得到一条反映信号传播位置的温度计码。

这里需要特别注意一个关键点:进位链本质上是一个多路选择器的链,而不是一根普通的导线。为了把CARRY4用于延迟线而非加法器,需要把CARRY4的S输入和DI输入设置成合适电平。常规做法是把CARRY4的S输入和DI输入都接为固定的01组合,使得进位信号在CARRY4内部从进位输入传播到进位输出时,每经过一级CARRY4都会产生一个固定的传播延迟。

实际的RTL实现中,通常不直接例化CARRY4原语,而是用加法器逻辑让综合工具自己推断出进位链结构。例如:

wire [N-1:0] add_w; reg [N-1:0] add_s = 0; always @(*) begin add_w = add_s + signal_pulse; end

当signal_pulse发生变化时,从最低位产生的进位会逐级向上传播,经过N级进位链,从而形成一个延迟线。综合工具会自动推断出CARRY4链,并在合适位置插入采样寄存器。但是,为了让综合工具不把这条链拆掉,必须加一堆综合约束和物理属性,这些我在第3节会详细给出。

2.3 温度计码转二进制的两种落地方式

采样寄存器输出的是温度计码。以单边沿为例,如果被测信号从低到高跳变,那么链上从起点开始的一段会被置成1,后面还是0,于是寄存器捕获到的状态是连续的多个1后面跟着连续的0。这个“1到0”的跳变位置就对应信号边沿到达的位置。温度计码转二进制的常见方法有查找表(LUT)和优先级编码器两种。

查找表方式适合链长比较短的情况,比如128级以内。直接把温度计码送入一块BRAM或者分布式RAM,地址端口接温度计码,数据端口输出对应的二进制编码。优点是速度快、延迟固定,缺点是资源占用随链长指数增长,不适合长链。

优先级编码器方式适合长链。用二分法逐级判断:先看后半段是否全部为1,如果是,说明跳变在更后面;否则把查找范围压缩到前半段。这个逻辑可以用组合逻辑树实现。延迟略大但可控,而且资源消耗只和链长成对数关系。

实际工程中还有一个更常用的技巧:把进位链分成若干个bin,每个bin包含固定级数(比如16级或32级),先用寄存器捕获温度计码,每个bin内部用一个小的优先级编码器,bin之间再用一个多级MUX网络选择出第一个不是全1的bin。这样可以把编码延迟控制在几个纳秒以内,不会成为系统瓶颈。

我在项目里采用的是“bin=24级”的分组方式,加两级流水线实现编码。第一级流水对每个bin生成局部编码,第二级流水根据bin编号拼接出全局编码。整个编码延迟大约是3个时钟周期,完全可以接受。

2.4 链长怎么定:测量范围、分辨率与资源换算

链长的确定需要综合考虑两个因素:最大可测时间范围和每级实际延迟。假设系统时钟250MHz,周期4ns,进位链每级延迟约30ps,那么大约需要133级进位位才能覆盖一个完整时钟周期。而一个CARRY4包含4个进位位,所以大约需要34个CARRY4。听起来不多,但实际设计时会留一定余量,因为温度电压变化可能让延迟变大,尤其是温度从常温升到85℃时,链路延迟可能增加10%以上。

如果测量范围需要超过一个时钟周期,通常不靠延长进位链实现,而是通过粗计数器扩展。进位链只需要覆盖一个时钟周期即可,其余时间由粗计数器处理。这个设计思路可以显著节省资源。但要注意,待测信号与系统时钟之间的相位差必须被正确采样,如果待测信号在时钟上升沿附近到达,很可能落到亚稳态区域,这是后仿真和实测中最难处理的点之一,后面会专门讲。

另一个容易踩的坑是链路延迟不均匀造成的“泡”(Bubble)。实际FPGA的进位链各级延迟并不完全一致,当信号沿链传播时,如果某几级延迟特别大或特别小,采样得到的温度计码中可能出现0/1交替的毛刺,而不是非常干净的一个跳变沿。这时如果直接找第一个0的位置,结果会不稳定。处理办法是采用“气泡容忍”编码算法,比如在优先级编码器前加一个消除连续跳变的小逻辑,只识别最后一个1到第一个0的稳定过渡点。

3. 动手实现:核心RTL代码与关键约束

3.1 进位链例化与寄存器连接的时序细节

下面是一个简化但完整可用的进位链TDC核心模块示例。它以系统时钟作为采样时钟,将待测信号送入进位链的输入端。

module tdc_carry_chain #( parameter CHAIN_LEN = 128 // 进位位数量,需覆盖一个时钟周期 )( input wire clk, input wire rst_n, input wire hit_signal, // 待测信号 output wire [$clog2(CHAIN_LEN)-1:0] code_out ); // 1. 进位链加法器自动推断 reg [CHAIN_LEN:0] add_a; reg [CHAIN_LEN:0] add_b; wire [CHAIN_LEN:0] sum_w; always @(*) begin add_a = {CHAIN_LEN{1'b0}}; add_a[0] = hit_signal; add_b = {CHAIN_LEN+1{1'b1}}; end assign sum_w = add_a + add_b; // 2. 采样寄存器组:用系统时钟沿捕获链上状态 reg [CHAIN_LEN:0] tap_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) tap_reg <= 'b0; else tap_reg <= sum_w; end // 3. 温度计码转二进制(此处用最简单的循环比较实现,适合小链长) reg [$clog2(CHAIN_LEN)-1:0] code_r; integer i; always @(*) begin code_r = 0; for (i = 0; i <= CHAIN_LEN; i = i + 1) begin if (tap_reg[i] == 1'b1) code_r = i; end end assign code_out = code_r; endmodule

这里核心是求和操作add_a + add_b。由于add_a只有bit0可能为1,其余为0,而add_b全部为1,因此当hit_signal为1时,最低位的加法会产生一个进位,并且这个进位一路向上传播。每经过一级进位位,就相当于产生一个延迟。采样寄存器tap_reg捕获的是这个进位传播过程中的中间状态。为了确保综合工具不把这条链拆散,sum_w必须被完整采样且不能优化。

在实际工程中,我更推荐直接例化CARRY4原语并进行手动连接,这样物理结构更可控。尤其在芯片型号比较老、综合工具版本又比较激进的情况下,自动推断链条容易被优化得面目全非。CARRY4原语例化示例:

// 每个CARRY4提供4个进位位 CARRY4 #() u_carry_0 ( .CO({chain_next[3], chain_next[2], chain_next[1], chain_next[0]}), // 4-bit carry out .O(), // XOR sum output,本设计不用 .CI(carry_in), // 前级进位输入 .CYINIT(1'b0), // 第一级时用0 .DI({hit_signal, 3'b000}), // 数据输入,关键影响传播路径 .S({4{1'b1}}) // 选择输入固定为1,使进位传播生效 );

不过CARRY4的物理内部工作原理非常微妙,DI、S信号的不同接法会导致进位传播路径不同,从而影响每级延迟。建议在动手写代码前,先翻阅所选器件的数据手册,重点看CARRY4原语的内部逻辑图,搞清楚进位信号实际经过的逻辑门级数。我最初就是随便接了S和DI,结果后仿真与实测的延迟特性相差很多。

3.2 综合约束与物理属性:如何防止进位链被工具优化

这是整个项目里最容易翻车的环节。如果不对进位链和采样寄存器添加物理保护,综合工具很可能基于各种“优化”策略,把链条拆散、打乱或插入延迟均衡逻辑,导致采样结果完全混乱。

给节点加的多米诺式约束包括:

(* KEEP = "TRUE" *) wire [CHAIN_LEN:0] sum_w; (* KEEP = "TRUE" *) wire [CHAIN_LEN:0] tap_reg;

综合属性KEEP会告诉综合工具和实现工具:不要删掉这条线或寄存器,也不要尝试重定时。除了KEEP,还可以用DONT_TOUCH属性,它比KEEP更严格,在综合和布局布线阶段都会被保留。

(* DONT_TOUCH = "TRUE" *) reg [CHAIN_LEN:0] tap_reg;

另外一个很重要的属性是MAX_FANOUT。采样寄存器阵列中,待测信号通常需要接到大量触发器的时钟端或数据端。如果这个扇出过大会导致布线延迟巨大、时钟偏差严重加剧,甚至影响进位链入口的建立时间。建议对待测信号设定合理的MAX_FANOUT,或者在扇出前先插入一个BUFG或者局部时钟缓冲。

采样时钟的处理同样关键。TDC的采样时钟必须保证极高的时钟质量,不能有大的时钟偏斜。通常的做法是把输入时钟接入全局时钟缓冲BUFG,再用同一时钟驱动所有采样触发器。为了进一步减少时钟偏斜,可以把采样寄存器限定在固定的时钟区域(clock region)内。在Vivado中可以使用Pblock约束,将进位链和采样寄存器放在同一个时钟区域内,这是后仿真通过、上板实测也能复现的重要保障。

3.3 输入信号调理与跨时钟域处理

待测信号进入进位链之前,必须经过仔细的时序处理。很多第一次做的人会把外部的异步信号直接送进FPGA引脚再连到进位链,这样做的问题在于:异步信号与系统时钟之间没有任何相位关系,到达进位链的时间点是完全随机的,亚稳态几乎不可避免。

一个典型的做法是先把待测信号通过IDELAY或IODELAY进行总相位粗调,让信号边沿落入测量范围内的有效区域,然后送到TDC链路。如果系统支持差分输入,建议优先使用IBUFDS差分输入缓冲,因为其共模抑制特性可以减少外部噪声对时间测量的干扰。

另外,TDC的测量结果在进入后端数据处理逻辑之前,也要做跨时钟域处理。进位链采样用的是系统时钟,但后续的粗计数器可能是同一个时钟,也可能是另一个不同频率的时钟。如果两者频率不同,就必须用异步FIFO或者握手脉冲做数据同步。不然采到的数据在高位和低位之间可能不是同一个时序关系,产生“错位”就是一个隐藏很深的bug。

4. 后仿真如何彻底验证TDC设计

4.1 三种仿真模式怎么选

Vivado中的仿真分为行为级仿真、综合后仿真和实现后仿真三个层次。对TDC这种极度依赖硬件物理结构的设计来说,行为级仿真的意义非常有限,因为它不包含任何延迟信息,进位链的表现不会体现在仿真波形中。综合后仿真包含逻辑门延迟,但布线延迟并未回标,效果也不够贴近实际。实现后仿真(也就是常说的后仿真)则是在布线完成后回标了完整的SDF时序信息,包括所有逻辑延迟和布线延迟,是最接近真实芯片表现的一种仿真模式。

我的建议是,TDC项目至少要做实现后仿真。为了带宽裕度,实现后仿真可以选两种模型:功能仿真和时序仿真。功能仿真使用布线后的网表但不带SDF时序,主要验证逻辑连接是否正确;时序仿真则带完整时序延迟。对于TDC,必须跑时序仿真才能验证进位链的真实延迟和触发器的建立保持时间裕度。功能仿真只能用来验证无时序条件下的数据链路连通性。

在Vivado中切换到实现后仿真的步骤如下:在Flow Navigator里面选择SIMULATION,点开之后有一个“Simulation Settings”,在“Simulation Mode”下拉框中选“Post-Implementation Timing Simulation”。同时确认勾选了“Enable SDF Back-annotation”。Vivado会自动为每个相关的FT构建仿真库,把布线后的网表和SDF文件传给仿真器。

很多初学者会卡在仿真库里出问题:编译报错一堆“Failed to open SDF file”或者“Cannot find instance”,这种问题通常是因为工程中没有正确设置仿真库搜索路径,或者SDF文件的结构与网表实例名不匹配。Vivado一般会自动完成这些配置,但如果你在外部仿真器(比如单独的ModelSim)里手动编译,就需要格外关注库文件和SDF路径。

4.2 后仿真波形中应该重点关注的几个信号

实现后仿真的运行时间通常比行为级仿真长很多,但波形信息量也大得多。对于TDC,我一般会重点关注这么几个信号:

第一,sum_w和tap_reg的数据变化。观察进位传播过程中各中间节点的电位变化是否符合预期:信号应该依次从低到高传播,每级变化之间有明显的时间差。如果某几级跳变几乎同时发生,说明那些级延迟太小,意味着进位链的连接可能有问题。

第二,采样时钟沿附近的tap_reg值。如果某个触发器在时钟沿附近出现X态或者震荡,说明建立时间或保持时间裕度不足,这就是亚稳态的表现。在后仿真中,亚稳态不会像在真实芯片中那样自动稳定到一个确定值,而是以X态呈现,这正好可以用来定位触发时间冲突的位置。

第三,输出code_out在连续相同输入情况下是否保持一致。给待测信号加入固定延迟(例如通过testbench中的#延迟),观察两次完全相同的输入测量得到的输出编码是否一致。如果编码不稳,大概率是链上存在“气泡”或亚稳态,需要回到编码算法上做处理。

下面给出一个testbench片段,示意如何注入固定延迟的信号并检查输出:

reg hit_signal; initial begin clk = 0; forever #2 clk = ~clk; // 4ns周期,250MHz end initial begin hit_signal = 0; #100; // 第一次测量:边沿在时钟上升沿前700ps处到达 @(negedge clk); #1.3; hit_signal = 1; #10; hit_signal = 0; // 第二次完全相同的测量 #100; @(negedge clk); #1.3; hit_signal = 1; #10; hit_signal = 0; end

在这个testbench中,我们刻意让hit_signal的跳变发生在时钟沿前约700ps的位置。通过后仿真可以观察两个问题:一是code_out的编码值是否一致,二是采样触发器的建立时间是否满足要求。

4.3 SDF回标对仿真精度的影响

谈到后仿真,就绕不开SDF(Standard Delay Format)文件。SDF文件记录了布局布线后每个逻辑单元和网络的实际延迟。在Vivado中,布线完成后会自动生成SDF文件,并在后仿真时自动加载。SDF里包含的信息包括每个CARRY4的单元延迟、每个网络上的布线延迟、各触发器内部的建立/保持时间等。

这里分享一个排查经验:如果后仿真波形中进位链的每级延迟显示为零或者完全不符合芯片手册中的典型值,八成是SDF没有正常回标。验证方法是在仿真日志里搜索“SDF”或“Timing”,确认是否出现类似“SDF Backannotation Successful”的提示。如果没有,需要检查是否选错了仿真模式,或者SDF文件的路径包含中文/空格字符,导致解析失败。

另一个常见的问题是仿真器的精度设置不够。ModelSim和QuestaSim默认的仿真时间精度可能是1ns,这显然不足以观察几十ps级的进位链延迟。建议把simulate的time resolution设置为1ps,命令是:

vsim -t 1ps work.tb_tdc

在Vivado的仿真设置里,也可以直接在“Simulation”->“Options”里把仿真分辨率设为1ps。这一步不做的话,SDF里几十ps的延迟会被舍入成0,后仿真结果和真实芯片表现会有严重偏差。

4.4 后仿真到底能验证什么、不能验证什么

这里必须把后仿真的能力边界说清楚,不然会白费很多时间。

后仿真能验证的是:逻辑连接是否正确、进位链传播是否连续、触发器的建立/保持时间是否有边沿冲突、编码器逻辑是否在低延迟下出错。它也能帮我们定位进位链上是否存在由于布局导致的异常大延迟跳变。

后仿真不能验证的是:真实芯片的工艺偏差、温度漂移、电压波动带来的延迟变化;引脚到内部逻辑之间的真实走线延迟随PCB布局的变化;进位链延迟在不同die之间的散度。这些只能靠上板后用实际信号做码密度校准和外部精密延迟扫描来标定。

所以后仿真的定位应该是“查大错、验逻辑”,真正决定TDC精度的还是上板后的校准。不要指望后仿真通过就万事大吉,那样上板后大概率还是会掉进坑里。

5. 实测调试与常见坑解决实录

5.1 进位链非线性严重:码密度校准的真实案例

上板之后遇到的第一个大坑,就是进位链的各级延迟严重不均匀。我在最开始的实测中,用固定延迟线扫描了整条链,发现相邻级延迟的波动范围从不到5ps到超过80ps都有。如果直接读编码来算时间,非线性误差能到几百ps,完全够不上“高精度”三个字。

解决办法是码密度校准。思路很简单:向TDC输入大量相位完全随机的信号,统计每个延迟bin被命中的次数。由于随机信号在时间轴上均匀分布,每个bin的命中次数与它的实际宽度成正比。命中次数多的bin,说明它实际延迟宽;命中次数少的,说明它实际延迟窄。用统计数据算出每个bin的校正系数,做成查找表,测量时用查表的方式还原真实时间。

实际实现时,我用的激励源是板上的可编程时钟发生器和PN码发生器。PN码产生的随机脉冲经过一段较长延迟后送入TDC,相当于把时间戳搬到了随机位置。总共采集了数百万个样本,统计后生成每个bin的宽度表。校准后,TDC的微分非线性(DNL)从原来的几十ps降到了10ps以内。这里所谓的“有效分辨率达到10ps级别”,指的就是校准后的综合测量精度,而不是不校准时的原始分辨率。

码密度校准还有一点要注意:它必须在实际工作温度、电压条件下做。进位链延迟随核心电压和结温变化非常敏感,温度从室温到85℃会让各级延迟整体变化10%以上。如果校准是在常温下做的,而工作时芯片温度大幅升高,测量精度依然无法保证。工程上要么为TDC模块增加温度补偿,要么在系统初始化时定期做一次快速校准。

5.2 亚稳态问题:如何让TDC输出稳定

TDC的输入信号和采样时钟是异步的,因此亚稳态是本质性的问题,不可能完全消除,只能把发生的概率降到可接受范围,并把亚稳态的影响限制在局部。

对于TDC来说,最凶险的是待测信号边沿正好落在采样时钟的建立/保持窗口内。这时采样寄存器可能进入亚稳态,输出既不是稳定的0也不是稳定的1,可能在某个不确定的电平上振荡。这个现象会在后仿真中表现为X态,在真实芯片中则表现为随机的0或1跳变。

处理亚稳态的标准做法是在采样寄存器之后再加几级同步寄存器。以双触发器同步器为例:

reg [CHAIN_LEN:0] tap_reg; reg [CHAIN_LEN:0] tap_reg_meta; reg [CHAIN_LEN:0] tap_reg_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tap_reg <= 'b0; tap_reg_meta <= 'b0; tap_reg_sync <= 'b0; end else begin tap_reg <= sum_w; // 采样进位链状态 tap_reg_meta <= tap_reg; // 第一级同步 tap_reg_sync <= tap_reg_meta; // 第二级同步 end end

第二级同步寄存器输出基本可以进入稳定状态。但要注意,加入同步器会引入一个时钟周期的流水延迟,这个延迟对编码逻辑没有影响,因为它只是一个常数偏移,可以在校准阶段扣除。

更彻底的一种方式是采用“边沿检测+Pulse Splitter”结构,在待测信号进入TDC之前,先用一个快逻辑把信号边沿转化为一个宽度足够宽、且不会落在时钟采样窗口内的脉冲。这个做法在文献里叫“双链TDC”或“环形TDC”的变体,实现复杂度更高,但对亚稳态的抑制效果更好。如果项目对长期稳定运行要求很高,值得投入时间去做。

5.3 布局布线导致进位链不连续的问题排查

我遇到过一种非常诡异的现象:同一个RTL,在某一次布局布线后TDC功能完全异常,但有时候重跑一遍又恢复正常。后来排查发现,Vivado在布局时偶尔会把某些CARRY4移到不希望的位置,导致进位链沿线散布了走线延迟较大的逻辑,采样寄存器也被打散到不同时钟区域,结果TDC的性能剧烈恶化。

解决方案是把进位链和采样寄存器放进一个Pblock,并限定它们的位置。在Vivado中创建Pblock约束:

create_pblock pblock_tdc add_cells_to_pblock pblock_tdc [get_cells {u_tdc/*}] resize_pblock pblock_tdc -add {SLICE_X0Y0 SLICE_X30Y100}

把Pblock限定在相邻的SLICE列范围内,并确保所有CARRY4和采样触发器都落在这些SLICE中。这样Vivado在布局时就不会把关键资源随机打散了。

Pblock设置好之后,要检查一下进位链是否真的连续。最简单的方法是在Vivado的Device视图中打开布局结果,选中整条进位链高亮显示。正常情况下,你会看到一条从下到上或从左到右的连续CARRY4链,中间没有跳变或跨越很远距离的连接。如果发现链路中间有长线互联,就要回头检查是不是某个中间寄存器被工具挪走了。

另外一个常被忽略的选项是综合设置里的“-keep_equivalent_registers”。有些版本的综合器会自动合并同名的寄存器,导致采样环节的数量减少。关闭这个优化选项,可以避免寄存器被不合理合并。

5.4 多通道扩展时的一致性校正

项目后期需要扩展到多个TDC通道。这时候发现不同通道的进位链延迟特性差异很大,即使是同一片FPGA、同一条链长的进位链,平均每级延迟也各有不同。这很正常,因为芯片制造过程中的局部工艺偏差是客观存在的。

多通道校正的一个实用思路是:选定一个参考通道,用参考通道做码密度校准,得到参考通道的bin宽度表。其余通道通过对同一随机信号进行测量,计算出与参考通道的偏差曲线,再叠加到各自的bin宽度表中。这样不需要对每个通道都做几十万次统计,用相对少的样本就能完成对齐。

同步问题也需要特别处理。多个通道的待测信号可能来自不同引脚,到达进位链的总延迟各不相同。在PCB层面就要尽量保证通道间走线等长,在FPGA内部也尽量保证输入到各TDC入口的路径长度一致。做不到完全一致也没关系,可以通过相位校准系数在逻辑中补偿。

多通道的另一个坑是物理资源竞争。如果多个TDC链路靠近得太近,强大的进位传播可能会通过共享的电源网络互相干扰,导致通道间出现可测的串扰。在布局时给不同通道之间留出至少一个SLICE列的空隙,能显著降低干扰。

5.5 数据读出速率与FIFO深度设计

最后是数据读出。TDC的测量速率可能很高,比如激光测距应用中每秒测量几千次,PET系统则需要处理每秒几百万次的事件。每个TDC输出一个32位时间戳,如果不在FPGA内做缓冲,很容易因为后端处理不及时而丢数据。

我的设计是在编码器后接一个异步FIFO,深度根据峰值事件速率来定。例如峰值事件率是2M个事件/秒,怕突发时峰值是平均的5倍,那么每个事件需要4个时钟周期的处理时间,在250MHz时钟下相当于FIFO写入带宽为62.5M事件/秒,FIFO深度选4096足够扛住突发。如果FIFO写满,保持在FPGA内输出一个溢出标志,供上位机判断数据完整性。

同步FIFO还是异步FIFO取决于TDC时钟域和处理逻辑时钟域是否相同。如果TDC模块与数据处理逻辑在同一个时钟域,同步FIFO即可。如果跨时钟,就必须用异步FIFO,并且在读侧注意格雷码跨域与空满标志的同步。

6. 沉淀下来的心得与进一步改进方向

从最初对进位链TDC停留在理论认识,到最后上板跑通并完成码密度校准,整个过程最大的感悟是:FPGA内部做高精度时间测量,本质上是一个物理设计问题,而不是单纯的逻辑设计问题。RTL代码只是起点,真正决定成功与否的是约束、布局、时序分析和校准。很多看起来“莫名其妙”的现象,深挖到最后都是物理层面的问题,比如局部工艺偏差、电源噪声、温度漂移。

如果项目还有进一步提升分辨率的需求,可以从几个方向入手。一是采用Wave Union TDC结构,让信号边沿在进位链中形成多个翻转,用多个跳变沿的平均值来降低测量误差。二是加入在线温度补偿和自动校准,让TDC在环境变化的场景下依然保持高精度。三是尝试环形进位链结构,把测量时间范围扩展到两个时钟周期以上。

另外一个很实用的建议是:把整个TDC模块做成可参数化的IP核,加到自己的IP库中。这样后续新项目可以快速复用,只要根据时钟频率、测量精度、通道数调节参数即可。做TDC的过程中总结出的经验,也沉淀成一个完整的验证testbench,方便每次改动后快速回归。实测下来,这套流程能帮我把新项目的TDC开发周期压缩到原来的三分之一左右。

最后分享一个小技巧,也是踩过几次坑之后养成的习惯:TDC版本的每次改动,都要固定综合种子(seed),否则布局布线结果变化会造成链路差异,导致对比数据时误判为代码异常。在Vivado中设置固定seed的方法是在综合和实现设置中指定一个固定的随机种子值,例如“synthesis seed”“placement seed”全部手动固定。测试时始终保持seed一致,再对代码做增量修改,每次改动的影响才能被清晰分离。这个习惯让我在后期的调试中少走了很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询