
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的计数误差影响小。但测量一次所需时间也越长响应慢。通常我们会设计多个闸门时间档位如1s0.1s10ms来适应不同频率范围。高频信号的挑战当待测频率接近甚至超过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 1b0; 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 1b0; sig_sync_r1 1b0; 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 1b0; 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 1b0; 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 2b00; localparam S_COUNT 2b01; localparam S_LATCH 2b10; 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 1b0; else gate_dly gate; end // 状态转移逻辑 always (*) begin next_state current_state; count_en 1b0; data_valid 1b0; case (current_state) S_IDLE: begin if ((gate 1b1) (gate_dly 1b0)) begin // 检测gate上升沿 next_state S_COUNT; end end S_COUNT: begin count_en 1b1; // 在计数状态使能计数器 if ((gate 1b0) (gate_dly 1b1)) begin // 检测gate下降沿 next_state S_LATCH; end end S_LATCH: begin // 锁存数据并通知输出有效 freq_out count_val_in; // 锁存计数值 data_valid 1b1; // 产生一个周期的有效脉冲 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); // 等待复位释放 // 测试案例110Hz信号 (周期100ms) generate_signal(100_000_000); // 参数是周期单位ns (100ms 100,000,000 ns) #2_000_000_000; // 等待足够长时间让频率计完成几次测量比如2秒 // 测试案例21KHz信号 (周期1ms) generate_signal(1_000_000); #2_000_000_000; // 测试案例31MHz信号 (周期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信号输出应为101KHz输出10001MHz输出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这是保证设计在真实硬件上稳定运行的关键也是仿真无法完全替代的环节。我们需要创建一个.sdcSynopsys 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_r1count_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数字系统设计的基本方法论可以向着更复杂、更有挑战性的项目进发了。