1. 项目缘起:从“测不准”到“测得准”的FPGA实践
在FPGA开发的路上,测量一个未知数字信号的频率,听起来是个基础需求,但真动手做起来,坑可不少。你可能试过用单片机配合定时器和外部中断来数脉冲,精度受限于系统时钟和中断响应时间,高频信号一上来就抓瞎。或者,你用过一些现成的频率计模块,但对里面的门道总感觉隔了一层。这次,我们就用Verilog在Quartus II的环境里,从零开始搭一个数字频率计,并且通过仿真把它彻底跑通。这不仅仅是写几行代码,更是理解如何用纯数字逻辑,在时间与计数的博弈中,实现精确测量。整个过程会涉及核心计数逻辑、基准时钟产生、测量控制状态机,以及最重要的——如何用ModelSim这样的仿真工具验证设计的正确性,确保它不光在纸上谈兵,更能经得起实践的考验。无论你是刚接触FPGA的新手,还是想巩固数字系统设计基础的老兵,这个从设计到仿真验证的完整闭环,都能给你带来实实在在的收获。
2. 数字频率计的核心原理与架构设计
在开始写代码之前,我们必须先搞清楚我们要建造的究竟是个什么东西,以及为什么选择这样的架构。一个数字频率计,本质是一个“数数”和“计时”的结合体。它的核心思想是:在一个已知的、非常精确的时间窗口内,统计未知信号发生了多少个完整的周期。用公式表示就是:F_measure = N / T_gate。其中,F_measure是待测频率,N是在闸门时间T_gate内计得的脉冲数。
2.1 直接测频法及其FPGA实现权衡
基于上述原理,最直接的方法就是“直接测频法”。我们用一个高稳定度的晶体振荡器产生基准时钟,例如50MHz。然后通过分频器,从这个基准时钟分出一个标准的闸门信号,比如1秒宽的脉冲。用这个1秒的闸门信号去控制一个计数器:闸门开启时,计数器对待测信号的每个上升沿进行计数;闸门关闭时,停止计数并锁存计数值,这个值就是信号在1秒内的周期数,即频率(Hz)。
听起来很简单,但魔鬼在细节里。对于FPGA实现,我们需要权衡几个关键点:
- 精度与速度的矛盾:闸门时间越长,测量低频信号精度越高(因为计数值N大,±1的计数误差影响小)。但测量一次所需时间也越长,响应慢。通常我们会设计多个闸门时间档位(如1s,0.1s,10ms)来适应不同频率范围。
- 高频信号的挑战:当待测频率接近甚至超过FPGA内部计数器的时钟(即系统时钟)时,直接用系统时钟去采样待测信号会面临亚稳态和漏计数的风险。这时需要用到“同步器”和“脉冲检测”电路来可靠地捕捉边沿。
- 测量盲区:直接测频法在闸门开关时刻与待测信号边沿不同步时,会引入±1个周期的误差。这是原理性误差,但我们可以通过多次测量取平均、或使用更复杂的等精度测频法来改善。
考虑到首次实现的清晰度和学习目的,我们将采用直接测频法,并设计一个1秒的固定闸门。我们的目标是先建立一个正确、可仿真验证的模型。
2.2 系统顶层模块划分
根据原理,我们将系统划分为几个协同工作的模块,这是写出清晰、可维护Verilog代码的关键。
- 基准时钟分频模块 (
clk_gen):负责将系统主时钟(如50MHz)分频,产生一个周期为2秒(高电平1秒,低电平1秒)的闸门信号。这1秒的高电平就是我们的测量窗口。 - 测频控制模块 (
freq_ctrl):这是整个系统的大脑,一个状态机。它监测闸门信号的上升沿和下降沿。在闸门上升沿,它清零计数器并启动计数使能;在闸门下升沿,它锁存当前计数器的值到输出寄存器,并产生一个“数据有效”信号。 - 同步与计数模块 (
sync_counter):对待测输入信号进行同步化处理(防止亚稳态),并包含一个使能可控的二进制计数器。只有在测频控制模块使能时,它才对同步后的信号上升沿进行计数。 - 顶层封装模块 (
freq_meter_top):将以上所有模块实例化连接起来,并定义对外的输入输出接口。
这样的划分使得每个模块功能单一,便于单独测试和调试。接下来,我们就进入具体的代码实现环节。
3. Verilog代码实现:从模块到系统
这里,我们采用自底向上的方式,先实现核心功能模块,最后进行顶层集成。所有代码均考虑可综合性和仿真验证。
3.1 基准闸门信号生成 (clk_gen)
这个模块的任务很简单:输入系统时钟clk和复位信号rst_n,输出一个周期为2秒的方波gate。假设系统时钟clk为50MHz(周期20ns)。要产生1秒高、1秒低的闸门信号,我们需要计数50,000,000个时钟周期。
module clk_gen ( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低电平复位 output reg gate // 闸门信号, 1秒高, 1秒低 ); // 参数定义:计数50M次, 因为从0开始计数,所以最大值是50M-1 parameter CLK_FREQ = 50_000_000; // 50MHz parameter GATE_HALF_PERIOD = CLK_FREQ; // 半周期计数(1秒) reg [25:0] cnt; // 需要26位宽来计数50M (2^26 > 50M) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; gate <= 1'b0; end else begin if (cnt == GATE_HALF_PERIOD - 1) begin cnt <= 0; gate <= ~gate; // 每计数满1秒,闸门信号翻转 end else begin cnt <= cnt + 1; end end end endmodule注意:这里
gate信号的高电平持续时间就是我们的有效测量闸门时间。代码中使用了参数CLK_FREQ,方便适配不同的系统时钟频率。
3.2 同步与边沿检测 (sync_counter)
待测信号sig_in来自FPGA外部引脚,与系统时钟clk异步。直接用它来驱动计数器是危险的,会引发亚稳态。标准的做法是使用两级D触发器进行同步。
module sync_counter ( input wire clk, input wire rst_n, input wire sig_in, // 异步待测信号 input wire count_en, // 计数使能,来自控制模块 output reg [31:0] freq_out // 频率计数值输出 ); reg sig_sync_r0, sig_sync_r1; // 两级同步寄存器 reg sig_sync_pulse; // 同步后信号的边沿检测脉冲 reg [31:0] counter; // 主计数器 // 1. 两级同步器,消除亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sig_sync_r0 <= 1'b0; sig_sync_r1 <= 1'b0; end else begin sig_sync_r0 <= sig_in; sig_sync_r1 <= sig_sync_r0; // sig_sync_r1 是同步化后的稳定信号 end end // 2. 边沿检测:检测同步后信号的上升沿 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sig_sync_pulse <= 1'b0; end else begin // 检测上升沿:上一拍为0,当前拍为1 sig_sync_pulse <= (~sig_sync_r1_dly) & sig_sync_r1; end end // 一个小技巧:需要缓存一拍sig_sync_r1用于边沿检测 reg sig_sync_r1_dly; always @(posedge clk or negedge rst_n) begin if (!rst_n) sig_sync_r1_dly <= 1'b0; else sig_sync_r1_dly <= sig_sync_r1; end // 3. 使能控制下的计数器 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 0; freq_out <= 0; end else begin if (count_en) begin // 使能有效时,在同步信号上升沿计数 if (sig_sync_pulse) begin counter <= counter + 1; end end else begin // 使能无效时,保持计数值(或者也可以清零,由控制模块决定) // 这里我们不清零,等待控制模块锁存 end // freq_out 通常由控制模块在闸门结束时锁存,这里先直接连线 // 实际中,锁存动作在控制模块完成更清晰 end end // 将内部计数器值持续输出,供控制模块锁存 assign freq_out = counter; endmodule关键点解析:
- 两级同步器(
sig_sync_r0,sig_sync_r1):这是处理异步信号的黄金标准。它不能消除亚稳态,但能将亚稳态发生的概率降低到可接受的水平,并确保后续电路使用的是稳定后的信号。- 边沿检测:我们使用一个D触发器缓存上一拍的值(
sig_sync_r1_dly),然后通过组合逻辑(~上一拍 & 当前拍)来产生一个时钟周期宽度的脉冲(sig_sync_pulse)。这个脉冲才是计数器真正应该响应的“事件”。- 计数使能
count_en:这个信号至关重要。它由控制模块产生,只在有效的闸门时间内为高。这确保了计数器只在测量窗口内工作,避免了闸门边沿处的误计数。
3.3 测频控制状态机 (freq_ctrl)
控制模块是协调工作的核心。它检测闸门信号gate的边沿,产生相应的控制信号。
module freq_ctrl ( input wire clk, input wire rst_n, input wire gate, // 来自 clk_gen 的闸门信号 input wire [31:0] count_val_in, // 来自 sync_counter 的实时计数值 output reg count_en, // 输出给 sync_counter 的计数使能 output reg [31:0] freq_out, // 锁存后的频率输出 output reg data_valid // 数据有效标志,锁存完成后拉高一个周期 ); // 状态定义 localparam S_IDLE = 2'b00; localparam S_COUNT = 2'b01; localparam S_LATCH = 2'b10; reg [1:0] current_state, next_state; reg gate_dly; // 用于检测gate的边沿 // 状态寄存器更新 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= S_IDLE; else current_state <= next_state; end // 延迟一拍gate,用于边沿检测 always @(posedge clk or negedge rst_n) begin if (!rst_n) gate_dly <= 1'b0; else gate_dly <= gate; end // 状态转移逻辑 always @(*) begin next_state = current_state; count_en = 1'b0; data_valid = 1'b0; case (current_state) S_IDLE: begin if ((gate == 1'b1) && (gate_dly == 1'b0)) begin // 检测gate上升沿 next_state = S_COUNT; end end S_COUNT: begin count_en = 1'b1; // 在计数状态,使能计数器 if ((gate == 1'b0) && (gate_dly == 1'b1)) begin // 检测gate下降沿 next_state = S_LATCH; end end S_LATCH: begin // 锁存数据,并通知输出有效 freq_out = count_val_in; // 锁存计数值 data_valid = 1'b1; // 产生一个周期的有效脉冲 next_state = S_IDLE; // 回到空闲,等待下一次测量 end default: next_state = S_IDLE; endcase end endmodule这个状态机清晰地描述了测量流程:空闲等待闸门开启 -> 开启后使能计数 -> 闸门关闭时锁存数据并输出有效信号。data_valid信号对于后续电路(如数码管显示、串口发送)非常有用,告知它们此时freq_out的数据是新鲜的、可读取的。
3.4 顶层模块集成 (freq_meter_top)
最后,我们将所有模块像搭积木一样连接起来。
module freq_meter_top ( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低电平复位 input wire sig_in, // 待测频率信号输入 output wire [31:0] frequency, // 测量得到的频率值 output wire data_valid // 频率数据有效标志 ); // 内部连线声明 wire gate_from_gen; wire count_en_to_sync; wire [31:0] count_val_to_ctrl; // 实例化闸门生成模块 clk_gen u_clk_gen ( .clk (clk), .rst_n (rst_n), .gate (gate_from_gen) ); // 实例化同步计数模块 sync_counter u_sync_counter ( .clk (clk), .rst_n (rst_n), .sig_in (sig_in), .count_en (count_en_to_sync), .freq_out (count_val_to_ctrl) // 实时计数值输出给控制模块 ); // 实例化控制模块 freq_ctrl u_freq_ctrl ( .clk (clk), .rst_n (rst_n), .gate (gate_from_gen), .count_val_in (count_val_to_ctrl), .count_en (count_en_to_sync), .freq_out (frequency), // 连接到顶层输出 .data_valid (data_valid) // 连接到顶层输出 ); endmodule至此,一个完整的、可综合的数字频率计Verilog代码就完成了。但代码写完了不等于工作做完了,尤其是数字设计,仿真验证是保证设计正确的生命线。接下来,我们就要在ModelSim中,为这个设计创造一个“虚拟实验室”。
4. 仿真测试平台搭建与关键测试场景设计
仿真(Simulation)是FPGA开发中成本最低、效率最高的验证手段。我们通过编写Testbench(测试平台),给设计模块施加各种激励(输入信号),并观察其输出是否符合预期。对于这个频率计,我们需要精心设计测试场景。
4.1 基础Testbench框架
我们创建一个名为freq_meter_tb.v的文件。Testbench本身也是一个Verilog模块,但它不可综合,只能用于仿真。
`timescale 1ns / 1ps // 时间单位/精度 module freq_meter_tb(); // 1. 定义连接到被测对象的信号 reg clk_tb; reg rst_n_tb; reg sig_in_tb; wire [31:0] freq_out_tb; wire data_valid_tb; // 2. 实例化被测顶层模块 freq_meter_top uut ( .clk (clk_tb), .rst_n (rst_n_tb), .sig_in (sig_in_tb), .frequency (freq_out_tb), .data_valid (data_valid_tb) ); // 3. 生成时钟激励 parameter CLK_PERIOD = 20; // 50MHz时钟,周期20ns initial begin clk_tb = 0; forever #(CLK_PERIOD/2) clk_tb = ~clk_tb; // 每10ns翻转一次,产生50MHz时钟 end // 4. 生成复位激励 initial begin rst_n_tb = 0; // 初始复位 #100; // 复位保持100ns rst_n_tb = 1; // 释放复位 // 可以在这里开始主要的测试逻辑 end // 5. 生成待测信号 sig_in 的激励 // 这是测试的核心,我们需要模拟不同频率的输入信号 initial begin sig_in_tb = 0; wait(rst_n_tb == 1); // 等待复位释放 // 测试案例1:10Hz信号 (周期100ms) generate_signal(100_000_000); // 参数是周期,单位ns (100ms = 100,000,000 ns) #2_000_000_000; // 等待足够长时间,让频率计完成几次测量(比如2秒) // 测试案例2:1KHz信号 (周期1ms) generate_signal(1_000_000); #2_000_000_000; // 测试案例3:1MHz信号 (周期1us) generate_signal(1_000); #2_000_000_000; $stop; // 仿真停止 end // 定义一个任务,用于生成特定周期的方波信号 task generate_signal; input integer period_ns; // 输入参数:信号周期,单位ns integer half_period; begin half_period = period_ns / 2; forever begin sig_in_tb = 1; #(half_period); sig_in_tb = 0; #(half_period); end end endtask // 6. 监控和打印输出 always @(posedge data_valid_tb) begin $display("[%t] Data Valid! Measured Frequency = %d Hz", $time, freq_out_tb); end initial begin // 可选:生成波形文件,用于在ModelSim GUI中查看 $dumpfile("freq_meter_wave.vcd"); $dumpvars(0, freq_meter_tb); // 0表示记录所有层次的信号 end endmodule这个Testbench做了以下几件事:
- 定义了与顶层模块连接的信号。
- 生成了50MHz的系统时钟
clk_tb。 - 生成了复位信号:先低电平(复位有效)100ns,然后拉高。
- 定义了一个任务
generate_signal,它可以产生任意周期的方波。我们在不同时间段调用它,模拟了10Hz、1KHz、1MHz三种频率的输入。 - 使用
$display系统任务,每当data_valid信号有效时,在控制台打印测量结果。 - 使用
$dumpfile和$dumpvars系统任务,生成VCD波形文件,便于在图形界面中直观分析。
4.2 在ModelSim中运行仿真与结果分析
- 编译:在ModelSim中,依次编译
freq_meter_top.v(及其包含的所有子模块文件)和freq_meter_tb.v。 - 仿真:将
freq_meter_tb设为顶层,开始仿真。 - 观察波形:添加关键信号到波形窗口,如
clk_tb,rst_n_tb,sig_in_tb,gate_from_gen(需要从顶层模块中引出或通过层次化路径查看),count_en_to_sync,freq_out_tb,data_valid_tb。运行足够长的时间(比如几秒的仿真时间)。 - 分析关键场景:
- 复位后初始化:观察复位释放后,
gate信号是否开始正常产生(周期2秒)。 - 闸门与计数使能:放大波形,看当
gate信号变高时,count_en是否立即变高;gate变低时,count_en是否立即变低。计数器是否只在count_en为高时对sig_in的上升沿计数。 - 数据锁存:在
gate下降沿之后,是否出现一个时钟周期的data_valid脉冲,同时freq_out上的值被更新为刚才计数的结果。 - 精度验证:对于10Hz的输入,在1秒闸门内应正好计数10次。在波形中数一下
sig_in的上升沿,并在data_valid有效时查看freq_out的值是否为10。对于1KHz和1MHz同理。
- 复位后初始化:观察复位释放后,
理想情况下,对于10Hz信号,输出应为10;1KHz输出1000;1MHz输出1,000,000。但在仿真中,由于闸门开关与信号边沿不同步,可能会出现±1的误差。这正是我们之前提到的原理性误差,在仿真中观察到它是正常的,也验证了我们设计的真实性。
5. 深入仿真:覆盖边界条件与异常情况
一个健壮的设计必须经过严苛的测试。基础的频率测试通过了,我们还需要考虑一些边界和异常情况,这往往是实际项目中出Bug的地方。
5.1 测试场景一:高频信号与计数器溢出
我们的计数器是32位的,最大能计到2^32-1,约42.9亿。在1秒闸门下,这对应约4.3GHz的频率。这看起来很高,但我们需要测试计数器在接近满量程和发生溢出时的行为。
修改Testbench:添加一个测试案例,生成一个周期非常短(比如2ns,即500MHz)的信号。由于我们的系统时钟是50MHz(周期20ns),这个500MHz的信号频率已经超过了系统时钟的奈奎斯特频率(25MHz),直接用generate_signal任务生成会不准确,因为#延时语句的精度和仿真器的调度机制可能无法精确模拟如此高频的信号。更严谨的方法是使用一个高速的“理想”时钟来模拟。
// 在Testbench的initial块中添加 // 测试案例4:高频信号 (用另一个always块模拟) reg fast_clk; initial begin fast_clk = 0; #100; // 等待复位完成 forever #1 fast_clk = ~fast_clk; // 周期2ns, 500MHz end // 在适当的时候,将 sig_in_tb 连接到 fast_clk initial begin // ... 之前的测试 ... #3_000_000_000; // 等待之前的测试完成 // 切换到高频测试 disable generate_signal; // 停止之前的信号生成任务(如果还在运行) sig_in_tb = fast_clk; // 直接连接500MHz时钟 #2_000_000_000; // 运行2秒 // 检查输出,理论上 freq_out 应该接近 500,000,000。 // 但由于同步器需要两个clk周期稳定,实际会计入一些延迟,可能不是精确值。 // 更重要的是观察计数器是否正常工作,没有因亚稳态导致乱计数。 end在这个测试中,我们关注两点:一是同步器能否稳定工作,波形上sig_sync_r1是否是一个干净的、与系统时钟同步的、频率较低的信号(因为500MHz被50MHz系统时钟大幅欠采样);二是最终的freq_out是否为一个合理的、巨大的数值,而不是一个随机值或全零,这能验证计数器在高速脉冲下的稳定性。
5.2 测试场景二:低频信号与测量误差
当待测信号频率很低时,比如0.5Hz(周期2秒),在1秒的闸门内,可能只捕获到0个或1个上升沿,测量结果会在0和1之间跳动,相对误差极大。这暴露了直接测频法对低频测量不友好的缺点。
测试与思考:在Testbench中生成一个0.5Hz的信号(周期2,000,000,000 ns)。观察多个测量周期(需要运行较长的仿真时间),会发现freq_out在0和1之间交替。这引出了对“测周法”或“等精度测频法”的需求思考。你可以在报告中记录这一现象,并作为未来优化设计的方向。
5.3 测试场景三:异步信号带来的亚稳态
这是数字电路中的经典问题。我们虽然用了两级同步器,但理论上亚稳态仍可能发生。我们可以尝试在仿真中制造更恶劣的条件,比如让sig_in的边沿非常接近系统时钟clk的上升沿。
仿真技巧:在Testbench中,可以使用$random或$urandom函数来随机化sig_in边沿相对于clk的位置。虽然仿真模型是理想的,不会像真实电路那样产生真正的亚稳态(仿真中的信号值是确定的0或1),但这种方法可以测试同步链路的逻辑正确性,确保即使在“临界”时刻,系统也不会崩溃或产生逻辑错误。
// 一种简单的随机边沿生成方法(用于压力测试,非精确频率) initial begin sig_in_tb = 0; wait(rst_n_tb == 1); forever begin // 随机生成一个0.5us到1.5us之间的延时 #(500 + {$random} % 1000); sig_in_tb = ~sig_in_tb; end end运行这段代码,并用波形工具仔细观察sig_sync_r0、sig_sync_r1和sig_sync_pulse。在大多数情况下,sig_sync_r1应该能稳定地跟随sig_in的变化,只是有2个时钟周期的延迟。sig_sync_pulse应该是一个干净的、单周期的脉冲。如果发现sig_sync_r1出现毛刺或异常跳变(在仿真中很少见,但若设计有逻辑错误则可能出现),就需要回头检查代码。
6. 从仿真到上板:Quartus II工程设置与真实世界考量
仿真通过,意味着我们的逻辑设计在理想环境下是成立的。但要把它变成FPGA芯片里真正运行的电路,还需要在Quartus II中完成编译、管脚分配、时序约束等步骤。
6.1 创建Quartus II工程与基础设置
- 新建工程:打开Quartus II,选择目标FPGA器件型号(例如Cyclone IV EP4CE10F17C8)。这一步至关重要,不同的器件资源、速度等级不同。
- 添加文件:将我们编写的所有
.v文件(freq_meter_top.v,clk_gen.v,sync_counter.v,freq_ctrl.v)添加到工程中。 - 分析与综合:直接点击“Start Analysis & Synthesis”。这一步Quartus会检查语法错误,并进行基本的逻辑综合。如果有错误,根据报告修改代码。
6.2 管脚分配与物理约束
我们的设计有三个顶层输入输出:clk,rst_n,sig_in,frequency[31:0],data_valid。我们需要告诉Quartus它们对应FPGA的哪个物理引脚。
clk:连接到FPGA的全局时钟引脚(如PIN_E1,具体查开发板手册)。rst_n:连接到一个按键或拨码开关对应的引脚。sig_in:连接到外部待测信号输入的引脚(如某个GPIO)。frequency和data_valid:如果你打算用SignalTap II逻辑分析仪看,可以暂时不分配物理引脚,只作为虚拟节点观察。如果要驱动数码管或发送到串口,则需要分配到对应的IO引脚。
在Quartus的“Pin Planner”工具中完成这些分配。一个关键点:将sig_in所在的引脚设置为“标准3.3V LVTTL”或其他符合你输入信号电平的IO标准。
6.3 时序约束与静态时序分析(STA)
这是保证设计在真实硬件上稳定运行的关键,也是仿真无法完全替代的环节。我们需要创建一个.sdc(Synopsys Design Constraints)文件。
# 时钟约束:定义主时钟clk的频率为50MHz create_clock -name clk -period 20.000 [get_ports clk] # 输入延迟约束:告诉工具外部信号sig_in相对于时钟clk的延迟情况 # 这是一个估计值,需要根据实际电路板情况调整 set_input_delay -clock clk -max 5.000 [get_ports sig_in] set_input_delay -clock clk -min -5.000 [get_ports sig_in] # 输出延迟约束:类似地,约束输出端口 set_output_delay -clock clk -max 5.000 [get_ports frequency] set_output_delay -clock clk -max 5.000 [get_ports data_valid]运行“TimeQuest Timing Analysis”。工具会计算设计中的建立时间(Setup Time)和保持时间(Hold Time)是否满足。如果出现“时序违例”,通常意味着组合逻辑路径太长(在两个寄存器之间),需要优化代码(如插入流水线寄存器)或降低时钟频率。
6.4 上板调试与SignalTap II的使用
编译通过并生成.sof文件后,可以下载到FPGA中。但如何知道它是否真的在工作?frequency是一个32位的总线,我们无法直接看到数值。这时,Quartus自带的逻辑分析仪SignalTap II就派上用场了。
- 新建SignalTap II文件:在Quartus中打开SignalTap II Logic Analyzer。
- 添加采样时钟:选择
clk作为采样时钟。 - 添加待观察信号:将
gate,sig_in(或sig_sync_r1),count_en,data_valid,frequency等关键信号添加进来。注意,frequency是总线,可以以十进制或十六进制形式显示。 - 设置触发条件:例如,我们可以设置触发条件为
data_valid的上升沿。这样,每当一次测量完成,逻辑分析仪就会捕获并显示触发点前后一段时间的数据。 - 编译并下载:将包含SignalTap II逻辑分析仪的设计重新编译,下载到FPGA。
- 运行与分析:给FPGA的
sig_in引脚输入一个已知频率的信号(例如,用另一个FPGA的PIP或函数发生器产生1KHz方波)。在SignalTap II界面点击运行,当触发条件满足后,你就能看到捕获的波形。检查data_valid有效时frequency的值是否等于你输入的信号频率(允许±1误差)。
通过SignalTap II,我们实现了“在片验证”,这是连接仿真理想世界和真实硬件世界的桥梁。如果发现测量值偏差巨大,可能是管脚分配错误、时序问题、或者输入信号质量(毛刺)等原因,需要结合波形逐步排查。
7. 项目总结与扩展思考
走完从设计、仿真到上板验证的完整流程,这个简单的数字频率计项目才算真正完成。回顾整个过程,有几个核心体会:
- 同步设计是基石:对待所有异步输入信号,使用同步器是必须的。两级D触发器是最简单有效的方案,它能将亚稳态概率降到极低。
- 仿真验证不可或缺:Testbench是设计者的沙盘。通过设计覆盖正常功能、边界情况和异常压力的测试场景,能在上板前发现大部分逻辑错误,节省大量调试时间。对于这个频率计,至少要仿真低频、中频、高频以及随机边沿的情况。
- 理解工具链:Quartus II不只是一个代码编辑器。管脚分配、时序约束、逻辑分析仪(SignalTap II)的使用,都是FPGA工程师的必备技能。时序约束尤其重要,它确保了你的设计在指定的速度下能稳定工作。
- 直接测频法的局限:本项目清晰地展示了±1误差的来源。对于追求高精度、宽量程的频率测量,下一步可以研究等精度测频法。其核心思想是:让实际闸门时间恰好等于待测信号的整数个周期。这需要同时用基准时钟和待测信号去计数,通过两个计数值的比值来计算频率,从而在整个量程内保持恒定的相对精度。
这个项目麻雀虽小,五脏俱全。它串联起了Verilog编码、模块化设计、Testbench编写、ModelSim仿真、Quartus工程管理、时序约束和硬件调试等多个关键环节。当你能够独立完成这样一个闭环,并理解其中每一个步骤的“为什么”,那么你就已经掌握了FPGA数字系统设计的基本方法论,可以向着更复杂、更有挑战性的项目进发了。