1. 项目概述:FPGA TDC中的细时间测量挑战
在时间数字转换器(TDC)的设计中,细时间(Fine Time)的测量精度直接决定了整个系统的性能上限。对于基于FPGA实现的TDC而言,其核心在于利用芯片内部可编程逻辑单元构建高精度延时链,以此将一个系统时钟周期“细分”成数百甚至上千个更小的时间单元。听起来很美好,对吧?但真正动手做过的人都知道,从原理图到稳定、精确的测量结果,中间隔着一道名为“非理想性”的鸿沟。本文要聊的,就是如何跨越这道鸿沟,特别是如何处理延时链起始部分那些“不听话”的延时单元,确保我们得到的细时间编码是纯粹、可靠且可重复的。
为什么这个问题如此关键?因为FPGA内部的布线资源和逻辑单元延时并非像ASIC那样经过精心设计和严格控制。当你用LUT(查找表)和走线搭建一条延时链时,第一个接收到外部“Hit”信号的单元,往往处在一个非常特殊且“脆弱”的位置。它的延时可能剧烈波动,远大于链中其他“兄弟”单元。如果忽略这一点,直接将所有单元的输出用于编码,那么你的TDC在测量靠近时钟沿的时间间隔时,会引入巨大的系统误差和不确定性,导致测量结果出现难以校准的毛刺和跳变。因此,理解细时间的构成,并学会“修剪”延时链,是每个FPGA TDC设计者必须掌握的实战技能。
2. 核心原理:延时链内插法与FPGA的实现困境
2.1 内插法的基本思想与理想模型
TDC测量时间的本质,是测量一个“事件”(如光子到达、脉冲边沿)与一个参考时钟沿之间的时间差。当这个时间差小于一个时钟周期时,就需要细时间测量。内插法的核心思想非常直观:在一个时钟周期内,插入一条由多个微小延时单元首尾相连组成的链,即延时链。当“Hit”信号到来时,它像波浪一样沿着这条链传播。在下一个参考时钟沿采样时刻,我们锁存整条链上每个单元的输出状态。理论上,“Hit”信号传播经过的单元数量,乘以每个单元的固定延时τ,就是细时间值。
在理想模型中,我们假设这条链上的每一个延时单元(通常由一个反相器或一个缓冲器构成)的延时τ1、τ2……τN都完全相等,且数值极小(例如几个皮秒)。这样,测量分辨率就是τ,测量范围就是N*τ(通常略小于一个时钟周期)。编码逻辑只需要简单地统计从链头开始,连续为‘1’(或‘0’,取决于设计)的单元数量即可。
2.2 FPGA实现带来的非理想性挑战
然而,当我们将这个理想模型映射到FPGA上时,问题接踵而至。FPGA的逻辑资源(如ALTERA的LAB、Xilinx的CLB)是为通用数字逻辑设计的,其内部的LCELL(逻辑单元)或布线开关的延时,首要目标是保证功能正确和时序收敛,而非提供精确、稳定的皮秒级延时。
- 基础延时过大且不一致:一个最基础的FPGA逻辑单元(如通过一个LUT实现的缓冲器),其延时通常在几百皮秒量级。例如,在ALTERA Cyclone IV系列的一个速度等级下,一个LCELL的延时可能高达300ps以上。直接用它们构建延时链,分辨率太低,毫无实用价值。
- 布线延时占主导且不可控:信号在FPGA内部从引脚到逻辑单元,再到单元之间的走线,延时(称为布线延时)往往比逻辑单元本身的固有延时(称为单元延时)大得多,且受布局布线工具算法的巨大影响。每次编译,路径都可能不同,导致延时值变化。
- 位置依赖性与工艺偏差:即使我们通过特殊方法(如利用进位链Carry Chain)构建了相对稳定的延时链,链上不同位置的单元,由于其物理位置、供电电压、温度微小的差异,其延时也会有差异,这就是微分非线性(DNL)。更棘手的是链首和链尾的单元,由于处在边界,其电气环境与中间单元不同,行为可能更加异常。
因此,基于FPGA设计TDC,与其说是“设计一条延时链”,不如说是“在FPGA的非理想海洋中,小心翼翼地识别并隔离出一段相对稳定、可用的延时片段”。下文将深入这个“识别与隔离”的过程。
3. 延时链的实战结构分析与“斩首”操作
3.1 实际可用的延时链结构剖析
图1展示了一个典型的基于FPGA内插法的TDC结构。关键在于,图中的“延时单元”并非普通的LUT,而是利用了FPGA底层硬件中具有快速、专用连接关系的资源。例如,在ALTERA FPGA中,通常利用LAB中的进位链(Carry Chain)。进位链原本用于实现快速加法器,其进位信号从上一个位到下一个位的路径是经过优化设计的专用线路,延时小且一致性好。通过巧妙配置,我们可以让“Hit”信号沿着这条进位链传播,从而获得一系列延时微小(可达几十皮秒)且相对稳定的延时单元。
即使使用了进位链,我们得到的延时链DNL测试结果也可能如图2所示。它并非一条平坦的直线,而是呈现出一定的波动。但重要的是,除了最开始的几个单元,后面大部分单元的延时值在一个可接受的范围内(例如±10%以内)波动。通过大量的统计学校准(如码密度测试),我们可以测量出每个单元的实际延时,从而将“单元个数”转换为“精确时间”。这是后处理的内容,但前提是原始数据必须可靠。
3.2 “斩首”的必要性:头几个单元为何不可用?
仔细观察图2或进行实际测试,你会发现一个关键现象:延时链的头4个(具体数量因器件和布局而异)单元的延时行为极其反常。如图3所示,第一个单元的延时可能飙升至700ps以上,是正常单元延时(如51ps)的十几倍。
这背后的根本原因在于“捕获”过程的模糊地带。
当“Hit”信号从FPGA引脚进入,到它被我们所谓的“第一个延时单元”真正采样,这中间存在一段无法用延时链本身度量的“前导路径”。这段路径包括:
- 全局布线路径:从IOE(输入输出单元)到目标LAB的输入端口。这段路径使用FPGA的全局或行列布线资源,延时大(纳秒级)且每次编译变化大。
- LAB内部输入路径:信号进入目标LAB后,需要从LAB的输入连线箱(Interconnect)分配到具体承载延时链的那个逻辑单元(如一个特定LUT的A输入端口)。这段延时也有数百皮秒。
问题在于,我们构建的延时链的“起点”,在物理上是从这个LUT的输出开始算起的。而信号从LUT的输入端口传播到其输出端口(即第一个延时单元的输入),这段延时被计入了“第一个延时单元”的延时中。但实际上,这段路径(图7中黑圈到紫圈)与后面纯粹的进位链延时(紫圈之后)性质完全不同。前者是常规的组合逻辑路径,延时大且不稳定;后者是专用的进位链路径,延时小且稳定。
更糟糕的是,这个“前导路径”与第一个进位链单元的边界是模糊的,无法在电路上精确切割。因此,处于这个临界位置的第一个(乃至前几个)单元,其表现是“前导路径延时”和“真实链延时”的混合体,且受布局布线影响巨大,导致其延时值巨大且充满不确定性(Uncertainty)。如果使用它们的输出进行编码,会在细时间码的低端(对应时间值很小的情况)引入巨大的、非线性的误差。
3.3 解决方案:生成稳定的“HitOK”信号
既然头几个单元不可信,我们如何知道“Hit”信号已经确实进入了稳定的延时链区域并被成功捕获呢?解决方案是引入一个**“HitOK”生成逻辑**。
具体做法是:监视延时链上第5个和第6个单元(避开头部不稳定区域)的输出。当“Hit”信号沿链传播时,这两个单元的状态会从“00”变为“01”,再变为“11”。我们可以设计一个小的状态机或逻辑电路,当检测到第5个单元为‘1’且第6个单元为‘0’(或类似的稳定模式)时,产生一个“HitOK”脉冲。这个脉冲标志着“Hit”信号已经成功穿越了不稳定的头部区域,进入了稳定测量区。
如图5所示,“HitOK”信号相对于原始“Hit”信号有一个固定的延迟(Tdelay + 4.5 * τ avg)。这个延迟是已知的,或者可以通过校准确定。在后续处理中,我们以“HitOK”信号的上升沿作为虚拟的“有效Hit到达时刻”,并只使用从第5个或第6个单元开始的延时链输出进行细时间编码。这就相当于把不稳定的头部连同那段模糊的“前导路径”一起从测量系统中“切割”掉了,我们只使用中间那段纯粹的、稳定的进位链延时段落。
实操心得:确定“斩首”数量“斩首”几个单元不是固定的。它需要通过时序分析工具(如TimeQuest/Timing Analyzer)结合硬件实测来确定。在时序报告中,找到从“Hit”输入引脚到延时链第一个寄存器(即图7红圈处)的路径。分析这条路径的分解,找到从LAB输入到第一个进位链单元输出之间的累积延时。将这个延时除以链中一个典型单元的延时(如50ps),得到的商(向上取整)就是你需要跳过的单元数量。通常这个数字在3到6之间。保险起见,可以多跳过1-2个。
4. 从理论到实现:关键步骤与设计要点
4.1 步骤一:专用延时链的构建
以Intel(Altera) FPGA为例,构建稳定延时链的推荐方法是使用进位链逻辑。
// 示例:使用Verilog描述一个利用进位链的延时链核心 module delay_chain_core ( input wire hit_in, // 原始Hit信号,需要先同步到目标LAB input wire clk, // 系统采样时钟 output reg [63:0] fine_code // 细时间编码输出 ); // 声明一个足够宽的wire用于进位链传播 wire [63:0] chain_out; // 第一个逻辑单元:将输入信号接入进位链 // 使用“+”操作迫使综合器使用进位链 assign chain_out[0] = hit_in + 1'b0; // 这个加法操作仅用于占用进位链资源 // 生成进位链:核心技巧是进行一连串的加法/累加操作 genvar i; generate for (i = 1; i < 64; i = i + 1) begin : gen_delay_chain // 关键:每一位都加上前一位的进位结果(即chain_out[i-1]) // 实际综合后,hit_in信号会沿着进位链从第0位传播到第63位 // 这里用了一个常数0作为加数,目的是让综合器保留进位路径 assign chain_out[i] = chain_out[i-1] + 1'b0; end endgenerate // 在采样时钟沿锁存整个链的状态 always @(posedge clk) begin fine_code <= chain_out; end endmodule需要注意的是,上述代码是一个概念模型。实际中,为了获得最佳性能和避免优化,可能需要使用原语(Primitive)或特定属性(Attribute)来直接例化进位链资源,并禁止优化工具合并或重排这些逻辑。例如,在Quartus中,可能需要使用(* preserve *)属性,并仔细约束布局位置,将整个延时链锁定在同一个LAB甚至同一个半LAB(Half-LAB)内。
4.2 步骤二:“HitOK”信号生成与同步处理
“HitOK”信号需要在异步的“Hit”域中生成,但最终要用于控制细时间编码的读取,因此需要小心地同步到采样时钟域。
module hit_ok_generator ( input wire [63:0] chain_raw, // 来自延时链的原始数据 output reg hit_ok_async // 异步的HitOK信号 ); // 假设我们决定使用第5个和第6个单元作为检测点(索引从0开始) localparam DETECT_HIGH = 5; // 第6个单元 localparam DETECT_LOW = 4; // 第5个单元 wire detection_point; // 当第6个单元为1,且第5个单元为0时,表明Hit波前刚好位于此区间 // 这是一个稳定的状态,不易受毛刺影响 assign detection_point = chain_raw[DETECT_HIGH] & (~chain_raw[DETECT_LOW]); // 用一个小型脉冲生成器产生HitOK脉冲 reg detection_point_dly; always @(posedge clk_async) begin // 注意:这里使用一个高速的、与延时链同源的自由运行时钟,或直接用链上信号作触发 detection_point_dly <= detection_point; end // 在detection_point的上升沿产生一个单周期脉冲 assign hit_ok_async = detection_point & (~detection_point_dly); endmodule // 将HitOK信号同步到系统时钟域,并生成编码使能信号 module sync_and_control ( input wire sys_clk, input wire hit_ok_async, input wire [63:0] fine_code_raw, output reg [63:0] fine_code_valid, output reg code_valid ); reg [2:0] sync_reg; // 两级同步器加一级边沿检测 always @(posedge sys_clk) begin sync_reg <= {sync_reg[1:0], hit_ok_async}; end wire hit_ok_synced_pulse; assign hit_ok_synced_pulse = sync_reg[1] & (~sync_reg[2]); // 检测上升沿 always @(posedge sys_clk) begin if (hit_ok_synced_pulse) begin // 当同步后的HitOK脉冲到来时,锁存此时的细时间编码 // 注意:这里锁存的是原始编码,后续处理需要丢弃头部无效位 fine_code_valid <= fine_code_raw; code_valid <= 1'b1; end else begin code_valid <= 1'b0; end end endmodule4.3 步骤三:细时间编码的提取与校准预处理
得到稳定的fine_code_valid后,需要提取有效的细时间计数。
module fine_time_encoder ( input wire clk, input wire code_valid, input wire [63:0] fine_code_valid, output reg [7:0] fine_bin, // 例如,输出0-255的精细时间区间 output reg fine_valid ); // 参数:头部需要跳过的单元数 localparam SKIP_HEAD = 5; // 跳过前5个单元(索引0-4) // 找到第一个'0'的位置(假设Hit信号为高电平传播,链初始为全0) // 只从第SKIP_HEAD位开始搜索 integer i; reg [7:0] count; always @(*) begin count = 8'd0; // 从SKIP_HEAD开始,向下查找直到遇到第一个'0',或者到达最大搜索深度 for (i = SKIP_HEAD; i < SKIP_HEAD + 256 && i < 64; i = i + 1) begin if (fine_code_valid[i] == 1'b1) begin count = count + 1; end else begin disable; // 在Verilog中,遇到0即跳出循环(实际需用其他方式,此为示意) // 实际实现可用优先级编码器或循环后break end end end // 寄存器输出 always @(posedge clk) begin if (code_valid) begin fine_bin <= count; fine_valid <= 1'b1; end else begin fine_valid <= 1'b0; end end endmodule这个模块输出的fine_bin是一个粗略的“温度计码到二进制码”的转换结果,它代表了从第SKIP_HEAD个单元开始,连续‘1’的个数。这个数值(称为“粗码”)还需要结合每个单元的实际延时值进行校准,才能转换为精确的皮秒级时间。
5. 时序约束、布局与实测调试要点
5.1 关键时序约束
为了让设计可靠工作,必须施加正确的时序约束,特别是对于异步的“Hit”信号路径。
# 在SDC文件(如TimeQuest)中 # 1. 将Hit输入引脚设置为虚假路径(False Path),因为我们不关心它到任何寄存器建立/保持时间 set_false_path -from [get_ports {hit_in}] # 2. 但是,我们需要约束从Hit引脚到延时链第一个寄存器(即捕获寄存器)的**最大延时**(Max Delay)。 # 这个约束不是为了时序收敛,而是为了控制“前导路径”的延时上限,确保它不会过长导致HitOK生成过晚,甚至错过一个时钟周期的采样窗口。 # 假设你的系统时钟周期为10ns,你需要保证Tdelay + 头部不稳定延时 < 时钟周期 - 建立时间 - 裕量。 # 例如,约束其最大延时为8ns。 set_max_delay -from [get_ports {hit_in}] -to [get_registers {delay_chain_reg[*]}] 8.000 # 3. 对延时链本身的路径,可以设置为多周期路径或虚假路径,因为链内传播是组合逻辑,其延时我们通过编码来测量,而不是通过时钟来同步。 set_false_path -through [get_cells {gen_delay_chain[*]}]5.2 布局布线约束
这是提升TDC性能最关键的步骤之一。目标是将整个延时链及其捕获寄存器紧密地布局在一起,以最小化布线差异。
- 锁定到同一LAB:使用
LOCATE或ALTERA_ATTRIBUTE约束,将构成延时链的所有逻辑单元(LUT)和捕获寄存器强制布局在同一个LAB内。这能最大程度保证单元间延时的一致性。 - 使用进位链资源:确保综合器确实使用了进位链。查看综合报告,确认关键路径使用了
CARRY_SUM之类的原语。可以通过RTL编码风格(如上述加法链)或直接例化原语来引导。 - 固定输入路径:如果可能,将“Hit”信号输入的引脚固定到离目标LAB最近的IO Bank,并使用
set_location_assignment约束。在LAB内部,尝试将“Hit”信号连接到该LAB某个特定的输入端口,并在RTL中通过(* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER"*)等属性防止优化工具移动网络。
5.3 实测调试与性能评估
设计完成后,需要用实际信号进行测试。
码密度测试(单次命中法):这是校准TDC的核心方法。将一个与系统时钟无关的、周期性(但频率远低于时钟频率)的“Hit”信号输入TDC,收集大量(例如数百万次)的细时间编码
fine_bin。统计每个编码值出现的次数。在理想情况下,每个编码出现的概率应该相等,形成一条平坦的直方图。实际中,由于DNL,直方图会有起伏。通过这个直方图,可以:- 验证“斩首”效果:观察编码值在0附近(对应头部单元)的计数是否异常(如严重偏低或出现毛刺)。如果异常,说明需要调整
SKIP_HEAD参数。 - 测量每个单元的宽度:直方图中每个“槽”的宽度代表了该编码区间对应的时间宽度,即相应延时单元的延时。
- 计算DNL和INL:根据直方图数据计算微分非线性和积分非线性,评估TDC的精度。
- 验证“斩首”效果:观察编码值在0附近(对应头部单元)的计数是否异常(如严重偏低或出现毛刺)。如果异常,说明需要调整
测量分辨率与RMS精度:通过分析码密度测试数据,可以计算平均最小可分辨时间(LSB的平均值)。通过测量一个固定延时的时间间隔多次,统计其测量结果的分布,可以计算出单次测量的RMS精度(单次触发精度)。
温度与电压敏感性测试:FPGA的延时对温度和供电电压敏感。需要评估在预期工作环境范围内,TDC的校准参数(单元延时表)变化有多大。对于高精度应用,可能需要进行实时在线校准。
6. 常见问题、误区与进阶优化
6.1 常见问题排查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 细时间编码大量集中在某几个值 | 延时链未能正常工作,信号没有逐级传播。 | 1. 检查综合报告,确认进位链被正确推断或例化。 2. 检查布局约束是否生效,确保链上所有元件在同一个LAB。 3. 使用SignalTap抓取 chain_raw信号,看是否呈现预期的“温度计码”波形。 |
| 测量结果随机跳变严重 | 1. 亚稳态。 2. 头部不稳定区域未正确处理。 3. “HitOK”信号同步或生成不可靠。 | 1. 确保“Hit”信号和“HitOK”信号到系统时钟域有足够级数的同步器(至少2级)。 2. 增加 SKIP_HEAD的值,确保完全跳过了不稳定区域。3. 检查“HitOK”检测逻辑,确保检测的是稳定状态(如 [5]=1 & [4]=0),而不是瞬态。 |
| 码密度测试直方图在0附近有深谷或尖峰 | 头部单元切除不干净或切除过多。 | 1. 用时序分析工具仔细分析从引脚到链首寄存器的路径延时,精确计算需要跳过的单元数。 2. 微调 SKIP_HEAD参数,观察直方图变化,找到最平坦的起始点。 |
| 不同编译结果性能差异大 | 布局布线随机性导致关键路径延时变化。 | 1. 加强布局约束,将关键路径完全锁定。 2. 使用“物理综合”选项,并尝试不同的布局布线种子(Seed)。 3. 考虑使用更底层的设计方法,如Quartus的LogicLock或Xilinx的Pblock进行严格区域约束。 |
| 高计数率下测量误差增大 | 死时间影响或脉冲堆积。 | 1. 检查TDC的死时间(从一次测量完成到准备好下一次测量所需时间)。确保系统时钟频率足够高,能及时清空捕获寄存器。 2. 对于高频率Hit,考虑使用多通道交替测量或更复杂的“乒乓”结构。 |
6.2 误区澄清
- 误区一:追求绝对均匀的延时链。在FPGA中这是不可能的。我们的目标是获得一段相对稳定、可重复、可校准的延时链。接受DNL的存在,然后用精密的校准算法去补偿它。
- 误区二:忽略“Hit”路径的时序约束。虽然我们将其设为
false_path,但set_max_delay约束至关重要。无约束的路径可能导致延时过长,使“HitOK”错过时钟窗口,造成整个周期测量错误。 - 误区三:认为校准是一劳永逸的。FPGA的延时随温度、电压漂移。对于实验室环境下的静态测量,一次校准可能够用。但对于野外或长期运行设备,可能需要集成温度传感器,并建立延时-温度查找表进行实时补偿。
6.3 进阶优化方向
- 多相位时钟内插:结合使用FPGA内部PLL产生的多相时钟,可以进一步细分时间。例如,使用4个相位差90度的时钟去采样同一条延时链,理论上可以将分辨率提高4倍,或者用更短的链达到相同的分辨率,降低DNL影响。
- 游标法(Vernier Method):使用两条不同单位延时的链(一条快链,一条慢链)。Hit信号同时启动两条链,当慢链追上快链时停止。通过计算两条链的步数差来得到时间。这种方法可以突破单个逻辑单元延时极限,获得更高的分辨率,但设计更复杂。
- 波形数字化与后处理:对于高速、连续的信号,可以考虑用高速ADC对信号进行采样,然后通过数字信号处理算法(如插值、曲线拟合)来提取更精确的时间信息。这需要消耗大量的DSP和存储器资源,但潜力巨大。
FPGA TDC的设计是一场与芯片内部非理想特性的精细博弈。理解“细时间”不仅仅是几个皮秒的数字,而是深刻洞察从IO引脚到进位链深处那一连串物理事件的真实过程。“斩首”操作看似简单,却是区分理论可行性与工程实用性的关键一步。每一次布局布线的微调,每一行约束文件的编写,都是为了从混沌中梳理出秩序,从不确定性中榨取出确定性。当你看到码密度测试的直方图从崎岖不平变得相对平坦,当你的TDC能够稳定地分辨出几十皮秒的时间间隔时,你会觉得这一切繁琐的工作都是值得的。记住,好的TDC设计,三分靠电路,七分靠约束和校准。