FPGA中基于进位链的TDC实现:原理、Verilog代码与Quartus实战
简介:本资源是一套基于Quartus II平台、采用Verilog实现的高精度时间数字转换器(TDC)完整工程,面向FPGA开发初学者及数字电路进阶学习者,解决高速时间测量系统中核心模块——TDC的建模、综合与硬件验证难题,适用于量子计时、激光测距、TOF传感器等对时间分辨率敏感的应用场景。压缩包共145个文件,涵盖28个.cdb(编译数据库)、27个.hdb(仿真波形数据库)、16个.v(核心Verilog源码,含delay_line、Coarse_Counter、Fine_Counter、fifo及top顶层模块)、7个.tdf(时序约束文件)及配套.qsf工程配置、.qws工作区设置、.readme说明文档等,总大小1.49MB,结构规范,模块划分清晰,便于理解TDC粗细计数协同机制与延迟链校准逻辑。已有1181人学习下载,提供可直接编译运行的RTL代码、ModelSim仿真脚本(.do文件)、完整适配报告与硬件测试参考,助读者快速掌握TDC在FPGA上的落地实现路径。
1. 项目缘起:为什么要在FPGA里做TDC?
最近在做一个需要精确测量时间间隔的项目,手头正好有块Intel(Altera)的FPGA开发板,第一反应就是用Quartus Prime搭个数字系统。但需求有点特殊:要测的信号脉宽在几个纳秒到几十纳秒之间,用常规的计数器加高频时钟,比如200MHz的时钟周期是5ns,测几个纳秒的脉宽误差就太大了,根本没法用。这时候,TDC(Time-to-Digital Converter,时间数字转换器)就成了必选项。
简单说,TDC就是把一段物理时间长度,转换成数字量的电路。在FPGA里做TDC,核心思路就是利用其内部固有的门延迟、布线延迟,或者更高级的像进位链(Carry Chain)这种专用资源,来构造一个比系统时钟周期精细得多的“时间尺子”。用Verilog来描述这个“尺子”的行为,最后在Quartus里综合、布局布线,把时间测量功能给实现出来。
这活儿听起来挺“硬核”的,网上相关的博客和论文也不少,但很多要么讲得太理论,要么代码给得云里雾里,缺了最关键的一环:在Quartus这个具体工具链下的实战细节。比如,代码怎么写才能让综合器(Synthesis Tool)和布局布线器(Fitter)按你的意图去利用那些延迟资源?怎么校准这个“尺子”的刻度?实测中那些诡异的非线性误差是怎么来的,又该怎么处理?这些才是决定一个TDC能不能用的关键。所以,我想结合自己趟过的坑,把在Quartus下用Verilog实现一个实用TDC的完整过程,掰开揉碎了讲清楚。
2. TDC的核心原理与FPGA实现路径选择
在深入写代码之前,必须得先搞明白我们要在FPGA里造个什么东西,以及有哪几条路可以走。这决定了整个项目的架构和代码风格。
2.1 TDC的基本工作原理与关键指标
你可以把TDC想象成一把非常精密的“游标卡尺”,只不过它量的不是长度,而是时间。它的核心任务就两个:1. 捕捉一个“开始”信号和一个“停止”信号之间的时间间隔;2. 把这个间隔转换成一个数字值(比如,多少个“最小时间单位”)。
这里就引出了几个关键指标:
- 分辨率(Resolution) :这把“尺子”的最小刻度是多少?也就是TDC能分辨的最小时间差。在FPGA里,这通常由底层硬件资源(如一个逻辑门或一段布线的延迟)决定,理想情况下可以做到几十皮秒(ps)到几百皮秒。
- 量程(Range) :这把“尺子”总共有多长?即它能测量的最大时间间隔。单纯的延迟链TDC量程很短,通常需要结合粗计数(Coarse Counting)来扩展。
- 线性度(Linearity) :这把“尺子”的刻度均匀吗?如果实际延迟是10ps、20ps、30ps,但TDC输出对应的码值是1、2、3,那就是理想的线性。但现实中,由于工艺偏差、温度电压变化、布线差异,刻度可能是不均匀的(非线性),甚至有些刻度会“消失”(微分非线性DNL差)。
- 精度(Precision) :多次测量同一个时间间隔,结果的一致程度。这受到噪声、抖动等因素影响。
在FPGA里实现,我们追求的是在有限的资源下,获得尽可能高且稳定的分辨率、良好的线性度,并满足项目要求的量程。
2.2 FPGA内实现TDC的三种典型路径
根据利用的硬件资源不同,主要有三条技术路线:
-
基于逻辑单元(LE)延迟链的TDC :这是最直接、最“朴素”的方法。用Verilog写一串反相器(
not)或者缓冲器(buf),综合后映射到FPGA的逻辑单元上,依靠这些单元和它们之间连线的固有延迟来构成时间尺。这种方法设计简单,但 线性度通常很差 。因为Quartus的布局布线工具(Fitter)首要目标是优化时序、面积和功耗,它不会为了保持延迟均匀而去刻意排列这些单元。结果就是延迟链各段的延迟差异很大,刻度不均匀,实用性低。 -
基于进位链(Carry Chain)的TDC :这是目前主流且效果较好的方法。FPGA的Slice/ALM内部有专用的、为快速加法运算设计的进位链。这条链上的延迟相对稳定,且相邻位之间的延迟高度一致(即线性度好)。通过构造一个虚拟的加法器或计数器,让“开始”信号作为进位输入,在进位链中传播,“停止”信号来锁存进位传播到了哪一级,从而得到时间差。这种方法 分辨率高(可达几十ps)、线性度好 ,是Verilog编码实现的重点。
-
基于IODELAY/TIODELAY等专用延迟单元的TDC :一些高端的FPGA(如Xilinx的7系列及以上,Intel的部分型号)在IO模块中提供了可以精密调节的延迟单元。这种方法精度可能最高,且可调,但 严重依赖特定器件型号 ,通用性差,且通常需要通过原语(Primitive)或IP核调用,纯Verilog行为描述实现较复杂。
对于我们大多数使用Cyclone、Arria甚至部分Stratix系列FPGA的开发者来说, 基于进位链的TDC是实现精度、线性度和通用性平衡的最佳选择 。接下来的所有讨论和代码,都将围绕这种方法展开。
2.3 系统级架构:粗计数与细测量结合
单纯的进位链长度有限(比如128级),其总延迟可能也就十几纳秒。要测量更长时间(比如微秒、毫秒级),就需要结合一个运行在系统时钟下的普通计数器。这就构成了经典的“粗计数+细测量”两级架构:
- 粗计数器(Coarse Counter) :由系统时钟(如100MHz)驱动,计量“开始”到“停止”信号之间完整的时钟周期数。
- 细测量(Fine Measurement) :由进位链TDC完成,计量“开始”信号上升沿到下一个时钟沿(或“停止”信号)之间,不足一个时钟周期的时间碎片。
最终结果 = 粗计数值 × 时钟周期 + 细测量值 × TDC分辨率。
我们的Verilog代码需要清晰地构建这两部分,并处理好它们之间的同步和拼接逻辑。
3. 进位链TDC的Verilog实现与关键代码剖析
理解了原理和架构,我们就可以动手写代码了。这里我将呈现一个经过简化的、但核心逻辑完整的基于进位链的TDC模块,并逐段解释其设计意图和Quartus下的实现要点。
3.1 顶层模块与接口定义
首先,我们定义TDC模块的接口。一个实用的TDC模块通常需要校准接口,因此我们设计如下:
module carry_chain_tdc #(
parameter CARRY_CHAIN_LENGTH = 64, // 进位链长度,根据器件调整
parameter COARSE_COUNTER_WIDTH = 16 // 粗计数器位宽
)(
input wire clk, // 系统时钟,用于粗计数和同步
input wire rst_n, // 低电平异步复位
input wire start_i, // 开始测量脉冲,高有效
input wire stop_i, // 停止测量脉冲,高有效
input wire cal_en_i, // 校准使能,高电平时输出固定延迟链状态
output reg [COARSE_COUNTER_WIDTH-1:0] coarse_cnt_o, // 粗计数值
output reg [$clog2(CARRY_CHAIN_LENGTH)-1:0] fine_code_o, // 细测量编码
output reg valid_o // 数据有效标志
);
参数说明 :
-
CARRY_CHAIN_LENGTH:这是我们期望利用的进位链级数。并不是越大越好,需要综合布线后的时序报告来评估。通常从64或128开始尝试。 -
COARSE_COUNTER_WIDTH:根据最大测量时间需求计算。例如,100MHz时钟(10ns周期),测量1ms需要计数100,000次,至少需要17位(2^17=131072)。
端口说明 :
-
cal_en_i:这个端口非常关键。在真实测量前,我们需要先校准TDC。当cal_en_i=1时,我们通常给start_i一个与时钟同步的脉冲,然后观察fine_code_o的输出。在一个理想线性系统里,这个输出应该是一个稳定值(对应固定的时钟沿到start_i的延迟)。实际中,由于噪声和抖动,它会有一个分布。校准的目的就是找到这个分布的“中心值”或特征。 -
valid_o:用于指示一次完整测量(粗+细)完成,输出数据稳定有效。这是与后续数据处理模块(如CPU或逻辑)握手的关键信号。
3.2 进位链核心结构的实现
这是TDC的“心脏”。我们利用Verilog编写一个“伪”加法器,引导综合器使用进位链资源。
// 进位链核心寄存器
reg [CARRY_CHAIN_LENGTH:0] carry_chain_reg; // 多一位用于锁存溢出
wire [CARRY_CHAIN_LENGTH:0] carry_chain_d;
reg [CARRY_CHAIN_LENGTH:0] capture_reg;
// 构建依赖于start_i的进位链逻辑
// 关键技巧:使用按位操作和start_i作为进位源,迫使综合器使用快速进位链
genvar i;
generate
assign carry_chain_d[0] = start_i; // 进位链的起始端由start_i驱动
for (i = 0; i < CARRY_CHAIN_LENGTH; i = i + 1) begin : gen_carry_chain
// 核心代码:这是一个“始终为0”的加法,但进位路径被保留
// {carry_chain_d[i+1], dummy_sum} = carry_chain_d[i] + 1'b0 + 1'b1;
// 上述注释是逻辑思想,实际写法则要引导工具使用进位逻辑
// 更典型的写法是构造一个减法或加法,让start_i参与进位
// 这里使用一个常见的模式:carry_chain_d[i+1] = carry_chain_d[i] & some_constant[i];
// 但为了确保路径唯一且被使用,更直接的方法是例化底层原语(如`carry`),但为了代码可移植性,我们用行为描述引导。
// 使用一个虚拟的加法操作,重点在于“+ start_i”这个操作会形成进位链
// 注意:为了节省资源,我们只关心进位,不关心和。
// 下面的写法是一种引导方式,实际综合结果需要看网表确认
wire dummy_a = 1'b0; // 加数A固定为0
wire dummy_b = 1'b1; // 加数B固定为1,确保每次都有进位输入?
// 更准确的描述:让start_i作为第一级的进位输入,然后每一级都进行一个会产生进位的加法
// 但这样写综合后可能不理想。许多实践采用手动例化LCELL(逻辑单元)并添加约束。
// 鉴于篇幅,这里给出一个概念性代码,实战中需要根据器件手册和实验调整:
// 假设我们利用一个寄存器链的使能端或数据端,将start_i传播下去。
end
endgenerate
// 更可靠且通用的引导方法:使用一个多位加法器,并将start_i连接到最低位的进位输入
// 同时,让加数和被加数都是常数,以避免逻辑优化掉关键路径
reg [CARRY_CHAIN_LENGTH:0] operand_a;
reg [CARRY_CHAIN_LENGTH:0] operand_b;
wire [CARRY_CHAIN_LENGTH:0] chain_sum;
wire [CARRY_CHAIN_LENGTH:0] chain_carry_out;
always @(*) begin
operand_a = {(CARRY_CHAIN_LENGTH+1){1'b0}}; // 全0
operand_b = {(CARRY_CHAIN_LENGTH+1){1'b1}}; // 全1,这样每一位A+B+Carry_in都会产生进位
end
// 这是一个多位的加法器,我们期望综合器用进位链实现它
// start_i作为最低位的进位输入
assign {chain_carry_out, chain_sum} = operand_a + operand_b + {{CARRY_CHAIN_LENGTH{1'b0}}, start_i};
// 将进位链的输出寄存起来
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
capture_reg <= 0;
end else begin
// 在stop_i有效时,锁存当前进位链的状态(即进位传播的位置)
// 注意:这里需要根据stop_i和clk的关系做同步处理,避免亚稳态
// 简化模型中,假设stop_i已经与clk同步
if (stop_i) begin
capture_reg <= chain_carry_out; // 锁存的是进位输出,每一位代表进位是否传播到该级
end
end
end
代码剖析与实战要点 :
- 引导综合器 :上面代码中
operand_a + operand_b + start_i是关键。我们设置operand_a为全0,operand_b为全1。对于二进制加法,0+1+进位输入,结果和是1,并产生一个进位输出(如果进位输入是1)。这意味着,只要start_i(作为最低位进位输入)为1,这个“1”就会像波浪一样沿着进位链从低位向高位传播。stop_i信号到来时,chain_carry_out的数值就记录了“波浪”传播到了哪里。 这行代码强烈暗示综合器使用硬件进位链来实现这个多位加法 。 - 为什么不是简单的寄存器链 ?如果写
always @(posedge clk) carry_chain_reg <= {carry_chain_reg[CARRY_CHAIN_LENGTH-1:0], start_i},综合器大概率会将其优化成移位寄存器,使用的是寄存器之间的布线,而不是专用的、延迟均匀的进位链,分辨率会差很多。 - 查看综合结果 :写完代码后,在Quartus中综合(Synthesis),然后打开“Technology Map Viewer”(技术映射查看器)。在这里,你应该能看到你的加法器被映射到了多个“CARRY”原语上,这些CARRY模块首尾相连,形成了一条链。这才是成功的标志。如果看到是一堆分散的
LOGIC_CELL,那就需要调整代码或添加综合属性((* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER"*))来防止优化。
3.3 细测量编码(Thermometer-to-Binary转换)
锁存到的 capture_reg 是一个“温度计码”(Thermometer Code)。例如,如果进位传播到了第5级,那么 capture_reg 的低5位( [4:0] )都是1,第5位及以上都是0( [5] 是0)。我们需要将这个温度计码转换成一个二进制数,即 fine_code_o 。
// 温度计码转二进制码模块
// 这是一个组合逻辑,用于将capture_reg中的连续‘1’的个数转换为二进制码
// 方法:找到第一个从1到0下降沿的位置
integer j;
reg found;
always @(*) begin
fine_code_o = 0; // 默认值
found = 1'b0;
for (j = 0; j < CARRY_CHAIN_LENGTH; j = j + 1) begin
if (!found) begin
// 如果当前位是1,下一位是0(或当前是最高位),则找到了边界
if (capture_reg[j] == 1'b1 && (j == CARRY_CHAIN_LENGTH-1 || capture_reg[j+1] == 1'b0)) begin
fine_code_o = j + 1; // 编码为位置+1(因为从0开始计数)
found = 1'b1;
end
end
end
// 如果全为0,则fine_code_o为0(start_i尚未传播)
// 如果全为1,则fine_code_o为CARRY_CHAIN_LENGTH(溢出)
end
这个循环逻辑比较直观,但注意它在 CARRY_CHAIN_LENGTH 较大时可能会成为时序瓶颈。在实际高频系统里,可以考虑用并行前缀算法(如先行进位逻辑)或者多级流水来实现这个转换,或者将这个转换放到软核(如Nios II)中用软件完成,以节省逻辑资源并提高速度。
3.4 粗计数器与同步、控制逻辑
粗计数器的实现相对标准,但需要仔细处理 start_i 和 stop_i 的同步,这是避免亚稳态和测量错误的关键。
reg start_synced, stop_synced;
reg [COARSE_COUNTER_WIDTH-1:0] coarse_cnt;
reg measuring; // 测量状态标志
// 输入同步器:使用两级触发器消除亚稳态
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
start_synced <= 1'b0;
stop_synced <= 1'b0;
end else begin
start_synced <= start_i;
// stop_i也需要同步,但注意,如果stop_i是异步的,我们需要同步它
// 这里假设start_i和stop_i已经是与clk同源的信号,或者已被同步
stop_synced <= stop_i;
end
end
// 测量状态机与粗计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
measuring <= 1'b0;
coarse_cnt <= 0;
coarse_cnt_o <= 0;
valid_o <= 1'b0;
end else begin
valid_o <= 1'b0; // 默认无效,只在一个周期内有效
if (cal_en_i) begin
// 校准模式,粗计数器不工作,valid_o可能用于输出校准数据
measuring <= 1'b0;
coarse_cnt <= 0;
// 可以将fine_code_o直接连出来观察
end else begin
// 正常测量模式
if (!measuring && start_synced) begin
// 检测到start脉冲上升沿,开始测量
measuring <= 1'b1;
coarse_cnt <= 1; // 从1开始计数,因为start边沿本身消耗了第一个周期的一部分?
// 注意:这里取决于你的时间定义。一种常见定义是,start边沿触发后,到下一个时钟上升沿之间的时间由TDC测量,然后每个完整的时钟周期,粗计数器加1。
coarse_cnt_o <= 0; // 清零输出寄存器
end else if (measuring) begin
coarse_cnt <= coarse_cnt + 1; // 每个时钟周期加1
if (stop_synced) begin
// 检测到stop脉冲,结束测量
measuring <= 1'b0;
coarse_cnt_o <= coarse_cnt; // 锁存最终的粗计数值
// fine_code_o已经在stop_i有效时被锁存到capture_reg并转换
valid_o <= 1'b1; // 拉高有效信号一个周期
end
end
end
end
end
同步与亚稳态处理 :
注意 :上面的同步器是最简单的两级触发器同步。在实际系统中,
start_i和stop_i很可能来自异步时钟域(例如,来自外部传感器的信号)。你必须为它们设计专门的跨时钟域同步电路(如双触发器同步、握手协议、异步FIFO等),确保信号稳定后再送入测量逻辑。直接将异步信号接入会导致亚稳态,使测量结果完全不可信。这是一个极易出错的关键点。
粗计数逻辑的细节 : 粗计数从什么时候开始?这里有一个设计选择。假设 start_i 上升沿在某个时钟周期内到来。从该上升沿到下一个时钟上升沿的时间 t_fine1 由TDC测量。然后,从下一个时钟上升沿开始,直到 stop_i 上升沿所在的时钟周期,这期间的完整时钟周期数由粗计数器计量。 stop_i 上升沿到其所在时钟周期下一个时钟上升沿的时间 t_fine2 由另一个TDC测量(或者,如果 stop_i 也作为锁存信号,则锁存的是从 start_i 传播来的进位状态,这测量的是总间隔 t_fine_total )。我们的示例代码采用了简化模型,即 stop_i 信号直接锁存当前的进位链状态,这个状态对应的是从 start_i 到 stop_i 的总时间在进位链上的“刻度”。因此,粗计数器实际上计量的是 start_i 之后完整的时钟周期数。在 stop_i 有效时, coarse_cnt 的值就是粗计数值。你需要根据你的系统时序定义,仔细推敲这个关系。
4. Quartus工程设置、综合约束与校准实战
代码写完了,但工作才完成一半。在Quartus里如何设置,才能让我们的设计按照预期工作,并达到想要的性能,这才是真正的挑战。
4.1 创建工程与基本设置
- 器件选择 :选择你实际使用的FPGA型号。不同系列(Cyclone IV, Cyclone V, Cyclone 10 LP等)和等级的器件,其进位链的延迟特性可能有差异。最好选择速度等级较高的器件(如
-6比-8快),其内部延迟更小,但相对变化也可能更敏感。 - 分析综合设置 :
- 打开“Analysis & Synthesis Settings”。
- 在“Optimization Technique”中,对于TDC这类对延迟路径敏感的设计,可以尝试设置为
Speed而非Balanced或Area。这会让综合器更倾向于优化时序,可能对保持进位链的连贯性有好处。 - 关闭激进优化 :这是至关重要的一步!综合器会默认优化掉它认为冗余的逻辑。我们的进位链加法器(
operand_a + operand_b + start_i)中,operand_a和operand_b是常数,综合器可能会将其优化成一个简单的start_i传播,而不再使用进位链。为了防止这种情况,我们需要添加综合属性(Synthesis Attribute)或约束。- 方法一(推荐,使用Quartus属性) :在Verilog代码中,对需要保留的寄存器或网络添加
(* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER"*)或者更具体的(* preserve *)属性。例如:(* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER" *) reg [CARRY_CHAIN_LENGTH:0] operand_a; (* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER" *) reg [CARRY_CHAIN_LENGTH:0] operand_b; - 方法二(使用QSF约束文件) :在Quartus设置文件(.qsf)中添加:
但这会关闭整个设计的全局高级网表优化,可能影响其他部分。建议只对关键模块或寄存器使用。set_global_assignment -name ADV_NETLIST_OPT_ALLOWED NEVER
- 方法一(推荐,使用Quartus属性) :在Verilog代码中,对需要保留的寄存器或网络添加
4.2 布局布线约束与后仿真
- 布局约束(可选但重要) :为了获得更好的线性度,我们希望进位链上的各个单元在物理位置上尽量靠近,排成一条直线。这可以通过 位置约束(Location Constraints) 来实现。
- 在Quartus的“Assignment Editor”中,找到代表进位链的那些寄存器(例如
capture_reg的每一位),或者相关的CARRY原语。 - 为它们指定相邻的 逻辑阵列块(LAB) 或 逻辑单元(LE) 位置。例如,你可以将
capture_reg[0]锁定到某个LAB的某个LE,然后capture_reg[1]锁定到同LAB的下一个LE,以此类推。 - 这需要你对目标FPGA的底层架构有一定了解,操作较为复杂。对于初步实验,可以不添加位置约束,先看结果。如果线性度不佳,再考虑此方法。
- 在Quartus的“Assignment Editor”中,找到代表进位链的那些寄存器(例如
- 时序约束与忽略路径 :TDC的测量路径(
start_i-> 进位链 ->capture_reg)是纯组合逻辑延迟路径,它 不依赖于系统时钟 。因此,这条路径不应该被当作普通的寄存器到寄存器的时序路径来约束。我们需要告诉TimeQuest(Quartus的时序分析器) 忽略这条路径 ,否则它会报出巨大的建立/保持时间违例,导致布局布线工具徒劳地尝试“优化”它,反而可能破坏我们精心构造的延迟链。- 在SDC文件中添加如下约束:
# 假设 start_i 是输入端口,capture_reg 是寄存器 set_false_path -from [get_ports {start_i}] -to [get_registers {capture_reg[*]}] - 同样,也需要忽略从
stop_i到capture_reg的路径(如果stop_i是异步锁存使能):set_false_path -from [get_ports {stop_i}] -to [get_registers {capture_reg[*]}]
- 在SDC文件中添加如下约束:
- 后仿真(Post-Fitting Simulation) :前仿真(功能仿真)只能验证逻辑正确性。要评估TDC的实际性能(分辨率、线性度),必须进行 后仿真 。后仿真使用了布局布线后生成的精确延迟信息(.vo网表文件和.sdo延时文件)。
- 在Quartus中生成“Post-Fitting”网表文件和延时文件。
- 在ModelSim或QuestaSim中,加载这些文件和你的Testbench。
- 在Testbench中,你可以系统地改变
start_i和stop_i之间的延迟(以非常小的步进,如1ps),观察fine_code_o的变化。通过统计分析,你可以绘制出 码密度图(Code Density) 或 传递函数 ,从而评估微分非线性(DNL)和积分非线性(INL)。这是校准和评估TDC性能的黄金标准。
4.3 校准流程:从理论到可用的关键一步
没有经过校准的TDC只是一个玩具。校准的目标有两个:1. 测量分辨率(LSB) ;2. 构建查找表(LUT) ,修正非线性误差。
步骤一:测量平均分辨率(LSB)
- 将TDC置于校准模式(
cal_en_i=1)。 - 使用一个高精度的脉冲/延迟发生器(或者在FPGA内部用PLL产生相位精确偏移的时钟作为
start_i和stop_i),产生一个时间间隔t_known非常精确且已知的信号对。 - 用TDC测量这个间隔,得到原始码值
code_raw。 - 改变
t_known(例如,每次增加10ps),记录对应的code_raw。 - 在一段线性较好的范围内,对多组
(t_known, code_raw)数据进行线性拟合(最小二乘法)。拟合直线的斜率k的倒数,就是平均分辨率:LSB_avg = 1/k(单位:时间/码值)。拟合直线的截距可以看作系统固定延迟。
步骤二:构建非线性修正查找表(LUT)
- 使用“码密度测试(Code Density Test)”或“滑动脉冲法”。简单来说,就是让
start_i和stop_i之间的时间差在一个很大的范围内(覆盖整个TDC量程)缓慢、连续地变化,并记录每个原始码值code_raw出现的次数。 - 在理想线性TDC中,每个码值出现的概率应该相等(除了0和满量程码)。实际中,由于非线性,有些码值出现得多(DNL>0),有些出现得少甚至不出现(DNL<0,即“失码”)。
- 根据码密度直方图,可以计算出每个码值对应的 实际时间宽度 。具体方法涉及统计和计算,这里不展开。最终,你能得到一个长度为
2^N(N为fine_code_o位宽)的查找表LUT[code_raw],这个表存储了每个原始码值所代表的 实际时间值 (可能是相对于某个参考的皮秒数)。 - 在真实测量中,我们读到一个
code_raw,不是直接用code_raw * LSB_avg来计算时间,而是去查这个表:t_fine = LUT[code_raw]。这能极大地修正非线性误差,提高测量精度。
步骤三:系统集成与在线校准 校准LUT可以事先测好,固化在FPGA的ROM中。但温度和工作电压的变化会影响延迟。因此,一个高精度的系统可能需要:
- 内置校准电路 :在FPGA内部用PLL生成已知相位差的两个时钟,作为“内置标尺”,定期进行自校准,动态更新LUT。
- 温度电压补偿 :通过片内传感器监测温度和电压,建立它们与TDC延迟的模型,进行实时补偿。
5. 实测中的典型问题、调试技巧与进阶优化
即使按照上述步骤完成了设计和约束,第一次上板测试也常常会遇到各种问题。这里分享一些常见的坑和调试手段。
5.1 问题一:输出码值不稳定,跳动大
- 现象 :即使
start_i和stop_i是固定的时间间隔,fine_code_o也会在几个码值之间随机跳动。 - 可能原因与排查 :
- 亚稳态 :这是首要怀疑对象。检查
start_i和stop_i是否已经进行了充分的同步处理。确保同步触发器(两级或更多)的时钟是干净、稳定的系统时钟。用示波器或逻辑分析仪(如果FPGA有嵌入式逻辑分析仪SignalTap)观察同步后的信号是否干净。 - 电源噪声 :TDC对电源纹波极其敏感,几十毫伏的噪声就可能导致皮秒级的抖动。检查FPGA的供电电源,尤其是为内核(VCCINT)和逻辑阵列(VCCIO)供电的电源质量。确保电源去耦电容(decoupling capacitor)的容值和布局符合要求。在实验室环境下,使用线性稳压电源(LDO)比开关电源(DCDC)噪声更小。
- 时钟抖动 :用于粗计数的系统时钟
clk本身的抖动会直接影响测量结果。使用低抖动的时钟源,如晶体振荡器,并确保时钟布线远离高速数字信号线。 - 组合逻辑竞争冒险 :在
capture_reg锁存之前,如果stop_i信号或进位链信号存在毛刺,会导致锁存错误的值。确保stop_i信号本身是干净的(必要时进行滤波),并且stop_i到capture_reg的路径尽可能短。
- 亚稳态 :这是首要怀疑对象。检查
5.2 问题二:线性度差,DNL/INL过大
- 现象 :码密度测试显示,某些码值出现的概率远高于或低于平均值,甚至出现失码(某个码值永远不出现)。
- 可能原因与排查 :
- 布局布线不均匀 :综合布局布线工具没有把进位链的各个单元放在物理上均匀排列的位置。这是最常见的原因。 解决方法 :
- 添加位置约束 :如前所述,手动将关键寄存器或进位链单元约束到连续的LE位置。
- 使用LogicLock区域 :在Quartus中创建一个LogicLock区域,将整个TDC模块约束在这个区域内,并设置大小和形状,强制工具将相关逻辑集中布局。
- 尝试不同的“种子” :在Quartus的Fitter设置中,改变“Seed”值,然后重新编译。不同的初始种子会导致不同的布局布线结果,有时能偶然得到线性度更好的结果(但这方法不保证,且难以重现)。
- 进位链资源被其他逻辑占用 :FPGA中进位链是稀缺资源。如果设计中其他部分(比如真的有一个大的加法器)也使用了进位链,可能会与TDC模块产生冲突或干扰。检查“Resource Utilization”报告,确认进位链的使用情况。尽量将TDC模块放在一个独立的逻辑区域。
- 代码描述方式不优 :Verilog代码的描述方式可能没有“完美”地引导出理想的进位链结构。可以尝试不同的编码风格,例如,显式地使用
(* keep *)综合属性保留中间信号,或者尝试用if (start_i) ... else ...这样的条件语句来构造依赖于start_i的链式结构,看看综合结果是否有改善。
- 布局布线不均匀 :综合布局布线工具没有把进位链的各个单元放在物理上均匀排列的位置。这是最常见的原因。 解决方法 :
5.3 问题三:分辨率达不到预期
- 现象 :实测的平均LSB远大于单个逻辑单元的理论延迟(例如,理论几十ps,实测几百ps)。
- 可能原因与排查 :
- 工具优化 :综合器仍然优化了你的设计。再次确认是否已经添加了足够的
(* preserve *)或(* altera_attribute = "-name ADV_NETLIST_OPT_ALLOWED NEVER"*)属性。查看综合后的网表,确认进位链(CARRY原语)确实被使用并连接成链。 - 布线延迟占主导 :在FPGA中,门延迟(逻辑单元内部的延迟)通常很小,而互连布线延迟可能很大,尤其是当布局布线工具将逻辑单元放得很分散时。这再次凸显了 位置约束 的重要性。将逻辑约束在紧凑区域内,可以最小化布线延迟,使门延迟成为主导,从而提高分辨率。
- 器件速度等级 :使用速度等级更快的器件(如
-6代替-8),其内部的单元延迟和布线延迟都会更小。
- 工具优化 :综合器仍然优化了你的设计。再次确认是否已经添加了足够的
5.4 进阶优化思路
- 差分测量与游标卡尺(Vernier)法 :上述是简单的“直接链”TDC。更精密的方法是用两条延迟链:一条“慢链”,一条“快链”。
start_i在慢链中传播,stop_i在快链中传播(或者用两个相位不同的时钟采样)。通过比较两条链的状态,可以实现比单链分辨率更高的测量。这类似于游标卡尺的原理。 - 多相位时钟采样 :利用FPGA内部PLL生成多个同频但不同相位的时钟(例如,0°, 90°, 180°, 270°)。用这些时钟去采样同一个进位链的状态,相当于将时间分辨率提高了4倍。这需要更复杂的控制逻辑和数据处理。
- 波形数字化与内插 :对于高频周期性信号,可以使用TDC对其波形进行“采样”(本质上是用时间测量来重构电压波形),再通过数字信号处理算法(如内插)来提高有效分辨率。
- 系统级误差补偿 :除了TDC自身的非线性,整个测量系统还有“死时间”(两次测量之间需要复位的时间)、时间数字转换的“量化误差”等。需要在系统模型中进行建模和补偿。
在Quartus下用Verilog实现一个可用的TDC,是一个从理解原理、编写引导性代码、添加正确约束、到细致校准和调试的完整链条。它考验的不仅仅是编程能力,更是对FPGA底层硬件、EDA工具行为以及模拟/数字混合系统设计的综合理解。最开始的几次尝试可能会失败,线性度可能惨不忍睹,但每一次对网表的分析、每一次约束的调整、每一次校准数据的收集,都会让你对“时间”在数字世界中的度量有更深刻的体会。当你的TDC最终能稳定地分辨出几十皮秒的时间差时,那种成就感,远非调用一个现成IP核所能比拟。
更多推荐

所有评论(0)