ARTICLE DETAIL

资讯详情

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

FPGA中基于进位链的TDC实现:原理、Verilog代码与Quartus实战

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最近在做一个需要精确测量时间间隔的项目手头正好有块IntelAltera的FPGA开发板第一反应就是用Quartus Prime搭个数字系统。但需求有点特殊要测的信号脉宽在几个纳秒到几十纳秒之间用常规的计数器加高频时钟比如200MHz的时钟周期是5ns测几个纳秒的脉宽误差就太大了根本没法用。这时候TDCTime-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^17131072。端口说明cal_en_i这个端口非常关键。在真实测量前我们需要先校准TDC。当cal_en_i1时我们通常给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[i1], dummy_sum} carry_chain_d[i] 1b0 1b1; // 上述注释是逻辑思想实际写法则要引导工具使用进位逻辑 // 更典型的写法是构造一个减法或加法让start_i参与进位 // 这里使用一个常见的模式carry_chain_d[i1] carry_chain_d[i] some_constant[i]; // 但为了确保路径唯一且被使用更直接的方法是例化底层原语如carry但为了代码可移植性我们用行为描述引导。 // 使用一个虚拟的加法操作重点在于“ start_i”这个操作会形成进位链 // 注意为了节省资源我们只关心进位不关心和。 // 下面的写法是一种引导方式实际综合结果需要看网表确认 wire dummy_a 1b0; // 加数A固定为0 wire dummy_b 1b1; // 加数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_LENGTH1){1b0}}; // 全0 operand_b {(CARRY_CHAIN_LENGTH1){1b1}}; // 全1这样每一位ABCarry_in都会产生进位 end // 这是一个多位的加法器我们期望综合器用进位链实现它 // start_i作为最低位的进位输入 assign {chain_carry_out, chain_sum} operand_a operand_b {{CARRY_CHAIN_LENGTH{1b0}}, 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为全0operand_b为全1。对于二进制加法01进位输入结果和是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 1b0; for (j 0; j CARRY_CHAIN_LENGTH; j j 1) begin if (!found) begin // 如果当前位是1下一位是0或当前是最高位则找到了边界 if (capture_reg[j] 1b1 (j CARRY_CHAIN_LENGTH-1 || capture_reg[j1] 1b0)) begin fine_code_o j 1; // 编码为位置1因为从0开始计数 found 1b1; end end end // 如果全为0则fine_code_o为0start_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 1b0; stop_synced 1b0; 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 1b0; coarse_cnt 0; coarse_cnt_o 0; valid_o 1b0; end else begin valid_o 1b0; // 默认无效只在一个周期内有效 if (cal_en_i) begin // 校准模式粗计数器不工作valid_o可能用于输出校准数据 measuring 1b0; coarse_cnt 0; // 可以将fine_code_o直接连出来观察 end else begin // 正常测量模式 if (!measuring start_synced) begin // 检测到start脉冲上升沿开始测量 measuring 1b1; 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 1b0; coarse_cnt_o coarse_cnt; // 锁存最终的粗计数值 // fine_code_o已经在stop_i有效时被锁存到capture_reg并转换 valid_o 1b1; // 拉高有效信号一个周期 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但这会关闭整个设计的全局高级网表优化可能影响其他部分。建议只对关键模块或寄存器使用。4.2 布局布线约束与后仿真布局约束可选但重要为了获得更好的线性度我们希望进位链上的各个单元在物理位置上尽量靠近排成一条直线。这可以通过位置约束Location Constraints来实现。在Quartus的“Assignment Editor”中找到代表进位链的那些寄存器例如capture_reg的每一位或者相关的CARRY原语。为它们指定相邻的逻辑阵列块LAB或逻辑单元LE位置。例如你可以将capture_reg[0]锁定到某个LAB的某个LE然后capture_reg[1]锁定到同LAB的下一个LE以此类推。这需要你对目标FPGA的底层架构有一定了解操作较为复杂。对于初步实验可以不添加位置约束先看结果。如果线性度不佳再考虑此方法。时序约束与忽略路径TDC的测量路径start_i- 进位链 -capture_reg是纯组合逻辑延迟路径它不依赖于系统时钟。因此这条路径不应该被当作普通的寄存器到寄存器的时序路径来约束。我们需要告诉TimeQuestQuartus的时序分析器忽略这条路径否则它会报出巨大的建立/保持时间违例导致布局布线工具徒劳地尝试“优化”它反而可能破坏我们精心构造的延迟链。在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[*]}]后仿真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.测量分辨率LSB2.构建查找表LUT修正非线性误差。步骤一测量平均分辨率LSB将TDC置于校准模式cal_en_i1。使用一个高精度的脉冲/延迟发生器或者在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和满量程码。实际中由于非线性有些码值出现得多DNL0有些出现得少甚至不出现DNL0即“失码”。根据码密度直方图可以计算出每个码值对应的实际时间宽度。具体方法涉及统计和计算这里不展开。最终你能得到一个长度为2^NN为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核所能比拟。本文还有配套的精品资源点击获取
返回列表