简介:面向 EDA/FPGA 课程设计的数字频率计完整设计包,适合电子、通信、自动化等专业本科生作为课程设计、期末大作业或自主练手素材。压缩包共包含 744 个文件、约 28.3MB,包含 Quartus 工程数据库与编译报告(cdb/ddb/rpt)、Verilog/VHDL 源文件、原理图与 PCB 设计(pcbdoc/schdoc)以及课程设计报告文档,完整对应了本科阶段提交给老师的作业内容。方案使用 QuartusII 与 Verilog 开发,采用自顶向下设计方法,将频率计核心测频模块按功能拆分为多个子模块,再通过顶层文件中的元件例化语句集成为完整测频逻辑,清晰呈现数字频率计从分频计数到结果显示的整体实现思路。已有 4695 人学习,下载后即可获得完整报告、源代码、PCB 和原理图,既能快速还原课程设计流程,也可作为基于 FPGA 的数字系统设计或频率测量类项目的基础参考。
1. 为什么「基于FPGA的数字频率计」年年有组做,年年有人翻车
数字频率计几乎是FPGA课设里最经典的一道题:需求一句话就能说清,无非是测个方波频率,数码管上显示出来。可你真去问答辩现场,十个人里有六七个会在「测频还是测周」「闸门时间怎么算」这两个问题上被问住。拿个网上下载的压缩包改个名字就交差,仿真通过、上板就乱跳,这类翻车我见过太多次了。
这篇笔记把「基于FPGA的数字频率计」从原理推导到Verilog实现、再到仿真上板一步步拆开讲。适合正在做课设但不想只当搬运工的同学,也适合想把手头工程从「能跑」改成「能讲清楚」的人。读完你会明白,这个题目的难点不在写代码,而在测频方案的选择和跨时钟域的处理——这些才是答辩时真正值分的部分。
2. 测频原理与资源评估:先把误差公式算明白再写代码
2.1 直接测频和测周:一个适合高频,一个适合低频
频率计的本质是在一个已知的时间窗口内数被测信号的脉冲个数。直接测频法就是固定闸门时间 T,数出被测信号脉冲数 N,频率就是 N ÷ T。比如闸门 1 秒,数到 10000 个脉冲,频率就是 10000 Hz。这个思路很简单,但有一个绕不开的缺陷:计数误差永远是 ±1 个脉冲。
把误差写出来更直观。测量相对误差等于 1 ÷ N,也就是 1 ÷ (f × T)。被测频率 f 越低,N 越小,误差越大。测一个 1 Hz 的信号,闸门 1 秒,N 只有 1,相对误差高达 100%,这频率计基本没法用。这是直接测频法的天生瓶颈。
解决低频误差的办法是反过来测周期。固定被测信号的周期数 M,数这个时间内标准时钟的脉冲个数。标准时钟频率 F 很高,所以误差同样压到很小。这就是测周法。但测周法在低频段效果好,一到高频段就吃亏——被测信号频率越高,周期越短,标准时钟的计数值越小,误差又上来了。
所以最朴素的理解是:测频法适合测高频,测周法适合测低频。问题在于,课程设计给的被测信号范围往往横跨好几十年,比如 1 Hz 到 50 MHz,你不可能让使用者手动切方案。这就引出了第三种做法。
2.2 等精度测频:让测量误差与信号频率无关
等精度测频的思路是:不再用固定闸门直接数被测信号,而是让闸门时间受被测信号的上升沿同步。闸门打开后,计数器不立即开始计数,而是等被测信号的下一个上升沿才真正使能;计数结束也是一样,等到闸门关闭后的第一个上升沿才停止。
这样做的好处是,闸门实际打开的时间等于被测信号整数个周期。在这段时间里,被测信号计数器数到的 N 没有 ±1 误差,误差只可能出现在标准时钟的计数上。标准时钟频率通常几十 MHz 起步,所以相对误差被压得很低。
公式是:f_x = N_x ÷ N_s × f_s。N_x 是被测信号计数值,N_s 是标准时钟计数值,f_s 是标准时钟频率。当闸门时间在毫秒级时,N_s 是个很大的数,误差主要来自 N_s 的 ±1 计数,相对误差约等于 1 ÷ N_s。被测信号频率不再直接决定误差,这是它比测频法和测周法都强的地方。
实现上要注意一个关键点:闸门信号和被测信号是异步的,不能用同一个时钟去采。常见做法是把闸门信号用被测信号打两拍做同步,再用同步后的信号作为两个计数器的使能。这一步在代码里很容易写错,写错了等精度就名存实亡。后面第三章会给出完整代码,这里先记住「同步」两个字。
2.3 分频系数与计数器位宽怎么定
写代码前先定参数,这个习惯能省掉后面大量调试时间。首先是基准时钟频率,开发板上一般是 50 MHz 或 100 MHz。假如用 50 MHz 基准时钟,等精度测频的最大测量分辨率是 1 ÷ 50,000,000,也就是 2 × 10⁻⁸ 量级,对课设来说绰绰有余。
然后是闸门时间。常见做法是 1 s、0.1 s、0.01 s 三档可调,用拨码开关或按键切换。闸门时间越长,标准时钟计数值越大,分辨率越高,但刷新速度变慢。测低频的时候,如果闸门太短,可能一个上升沿都没等到,显示就会频繁跳变。我一般在 100 ms 和 1 s 之间做两档,兼顾响应速度和精度。
计数器位宽要按最坏情况算。闸门 1 s 时,50 MHz 基准时钟的计数值最大是 5000 万,二进制需要 26 位。被测信号如果最高 50 MHz,同样要 26 位。留点余量,计数器和锁存寄存器都定义成 28 位最稳妥。位宽不够的后果是计数值溢出后被截断,显示的数字莫名其妙,这类问题在仿真里反而不容易暴露。
再算 BCD 转换的位宽。28 位二进制数最大能表示 2.68 亿,转换成十进制是 9 位。但频率计一般只显示 6 位或 8 位有效数字,可以在锁存之后做截断和量程处理。这里引出一个很简单但常被忽略的结论:计数器位宽不是随便写的,它取决于闸门时间、基准时钟频率和被测信号上限三者乘积的二进制位数。
3. Verilog实现:顶层模块、闸门同步与扫描显示
3.1 顶层模块划分与信号约定
我习惯把整个设计分成四个模块:时钟分频与闸门生成、同步闸门与双计数、数据锁存与 BCD 转换、数码管扫描显示。这样分的原因是每个模块都可以独立仿真,出了问题能快速定位到具体文件,答辩时讲解也清晰。
信号约定要提前定好。基准时钟是 clk_50m,被测信号是 fx_in,复位是低有效 rst_n。闸门信号 gate 是内部生成后经过同步处理得到的 gate_sync,它同时控制两个计数器的使能。计数完成后产生锁存信号 latch_en,把两个计数值打进寄存器。显示部分用位选 scan_sel 和段码 seg_data 输出。
被测信号是外部进来的方波,它和基准时钟没有相位关系,所以入口处必须加两拍同步器,避免采集到亚稳态。同步器的代码很简单,就是两级级联的触发器,但如果漏了这个处理,上板后偶尔出现计数差一两个数就是它的锅。
3.2 同步闸门与双计数器:等精度测频的核心代码
下面这段代码是等精度测频的核心,包含了闸门同步和双计数器。我把模块写成了可综合风格,直接放到工程里就能用。
module freq_core #( parameter CNT_WIDTH = 28 )( input wire clk_50m, input wire rst_n, input wire fx_in, input wire gate_in, // 分频得到的原始闸门 output reg [CNT_WIDTH-1:0] cnt_fx, // 被测信号计数 output reg [CNT_WIDTH-1:0] cnt_ref, // 基准时钟计数 output reg latch_en // 锁存信号 ); // 两级同步器,先把被测信号同步到基准时钟域 reg fx_sync1, fx_sync2; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin fx_sync1 <= 1'b0; fx_sync2 <= 1'b0; end else begin fx_sync1 <= fx_in; fx_sync2 <= fx_sync1; end end // 用同步后的被测信号打一拍得到上升沿 reg fx_ff; wire fx_rise = fx_sync2 & ~fx_ff; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) fx_ff <= 1'b0; else fx_ff <= fx_sync2; end // 闸门信号在被测信号上升沿处同步 reg gate_fx1, gate_fx2; wire gate_sync; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin gate_fx1 <= 1'b0; gate_fx2 <= 1'b0; end else begin gate_fx1 <= gate_in; gate_fx2 <= gate_fx1; end end assign gate_sync = gate_fx2; // 双计数器 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_fx <= {CNT_WIDTH{1'b0}}; cnt_ref <= {CNT_WIDTH{1'b0}}; end else if (latch_en) begin cnt_fx <= {CNT_WIDTH{1'b0}}; cnt_ref <= {CNT_WIDTH{1'b0}}; end else if (gate_sync) begin cnt_ref <= cnt_ref + 1'b1; // 基准时钟一直计数 if (fx_rise) cnt_fx <= cnt_fx + 1'b1; // 被测信号只在上升沿加 end end // 锁存信号:在被测信号上升沿锁存,同时结束本轮计数 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) latch_en <= 1'b0; else latch_en <= fx_rise & ~gate_sync & ~latch_en; end endmodule逻辑说明:gate_in 是分频产生的原始闸门,它先被 fx_sync2 同步到被测信号的时钟域,但实际这里用的是基准时钟采样,真正的同步意义在于让闸门的打开和关闭都在被测信号的上升沿处被确认。cnt_ref 只要 gate_sync 为高就每个基准时钟加 1,cnt_fx 只在检测到被测信号上升沿时加 1,这样两个计数器的起始和结束时刻就对齐了。
参数说明:CNT_WIDTH 默认 28 位,对应前面算好的最大计数值。latch_en 的生成逻辑要仔细看,它是 fx_rise 与 gate_sync 下降沿的组合,作用是在闸门关闭后的第一个被测信号上升沿把当前计数值锁存出去。这段代码里我最容易写错的就是 latch_en 的时序,建议仿真时单独盯这个信号。
3.3 数据锁存与BCD转换
计数完成后,计数值必须立刻锁存到独立寄存器里,否则下一轮计数开始后数据会被覆盖。锁存的设计很简单,在 latch_en 有效的那一拍,把 cnt_fx 和 cnt_ref 各存入一个寄存器,同时把换算标志位置起来。
接下来是等精度测频的换算:f_x = cnt_fx ÷ cnt_ref × 50_000_000。FPGA 不擅长做除法,课程设计里也犯不着用除法器 IP。常见做法是直接用 cnt_fx 作为显示值,前提是标准时钟计数 cnt_ref 恰好等于 50_000_000——这只有在闸门时间刚好 1 秒时成立。如果把闸门做成 1 秒,cnt_ref 在理想情况下就是 5000 万,cnt_fx 直接等于频率值,省掉一整套乘除法电路。
module bcd_convert #( parameter WIDTH = 28, parameter DIGIT = 8 )( input wire [WIDTH-1:0] bin_in, output reg [3:0] bcd_out[DIGIT-1:0] ); integer i, j; reg [WIDTH+4*DIGIT-1:0] shift_reg; always @(*) begin shift_reg = {4*DIGIT{1'b0}} + bin_in; for (i = 0; i < WIDTH; i = i + 1) begin for (j = 0; j < DIGIT; j = j + 1) begin if (shift_reg[4*j +: 4] >= 4'd5) shift_reg[4*j +: 4] = shift_reg[4*j +: 4] + 4'd3; end shift_reg = shift_reg << 1; end for (j = 0; j < DIGIT; j = j + 1) bcd_out[j] = shift_reg[4*j +: 4]; end endmodule这个模块就是俗称的移位加 3 算法,也叫 double-dabbler。原理是模拟二进制转十进制的每一位进位过程,每移位一次,检查每个 BCD 位是否大于等于 5,是就加 3。代码里的4*DIGIT是预留的 BCD 位宽,循环次数等于输入二进制位宽 28。这段代码是可综合的,但综合后组合逻辑会比较大,FPGA 逻辑资源完全扛得住,不必担心。
3.4 数码管扫描显示驱动
数码管显示用动态扫描是课设标准做法。8 个数码管每一位显示 1 ms 左右,轮流点亮,人眼看到的是一整排稳定的数字。扫描时钟用 1 kHz 左右的频率比较合适,从 50 MHz 分频得到就是除以 50000。
module seg_display #( parameter CLK_FREQ = 50_000_000, parameter SCAN_HZ = 1_000 )( input wire clk, input wire rst_n, input wire [DIGIT-1:0][3:0] bcd_in, // 8位BCD数据 output reg [DIGIT-1:0] scan_sel, // 位选 output reg [7:0] seg_out // 段选 ); localparam DIV_CNT = CLK_FREQ / SCAN_HZ / DIGIT; reg [$clog2(DIV_CNT)-1:0] div_cnt; reg [2:0] scan_idx; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 0; scan_idx <= 0; end else if (div_cnt == DIV_CNT - 1) begin div_cnt <= 0; scan_idx <= scan_idx + 1'b1; end else begin div_cnt <= div_cnt + 1'b1; end end always @(*) begin scan_sel = {DIGIT{1'b1}}; scan_sel[scan_idx] = 1'b0; // 位选有效,选中当前位 end reg [6:0] seg_bit; always @(*) begin case (bcd_in[scan_idx]) 4'd0: seg_bit = 7'b1000000; // 共阴数码管段码,按需调整 4'd1: seg_bit = 7'b1111001; 4'd2: seg_bit = 7'b0100100; 4'd3: seg_bit = 7'b0110000; 4'd4: seg_bit = 7'b0011001; 4'd5: seg_bit = 7'b0010010; 4'd6: seg_bit = 7'b0000010; 4'd7: seg_bit = 7'b1111000; 4'd8: seg_bit = 7'b0000000; 4'd9: seg_bit = 7'b0010000; default: seg_bit = 7'b1111111; endcase end always @(posedge clk or negedge rst_n) begin if (!rst_n) seg_out <= 8'b0; else seg_out <= {seg_bit, 1'b0}; // 小数点段关闭 end endmodule位选和段选要配合好:每一位点亮期间,段码必须是稳定的,不能在显示中途切换。上面的代码里位选是组合逻辑直接输出,段码也走了组合逻辑,实际工程里最好把段码打一拍,避免动态扫描时出现轻微拖影。拖影不是大问题,但答辩时被老师盯着看也能看出来,早处理早省心。
4. 仿真与上板:把testbench写对,比调代码更省时间
4.1 testbench结构与激励生成
很多同学仿真翻车,不是因为代码错,而是因为 testbench 的激励根本没有模拟出真实的异步关系。被测信号 fx_in 和基准时钟 clk_50m 是异步的,这是数字频率计仿真的核心要点。你在 testbench 里如果直接写always #10 fx_in = ~fx_in,那是从 0 时刻就同步的,实际中根本不存在这种信号。
正确做法是把被测信号也做成独立时钟源,让它的相位和基准时钟完全无关。最简单的办法是用不同频率的 always 块分别产生。
module tb_freq_meter; reg clk_50m; reg rst_n; reg fx_in; wire [7:0] seg_data; wire [7:0] scan_sel; // 生成100kHz被测信号,用参数化频率设置 localparam FX_FREQ = 100_000; initial begin clk_50m = 0; forever #10 clk_50m = ~clk_50m; // 50MHz基准时钟 end // 被测信号:20ns为一个半周期,等效25MHz,但故意不取整 initial begin fx_in = 0; #25 fx_in = ~fx_in; forever #25 fx_in = ~fx_in; end initial begin rst_n = 0; #200 rst_n = 1; #5000 $stop; end initial begin $monitor("time=%0t fx_cnt=%0d ref_cnt=%0d", $time, tb_freq_meter.u_freq_core.cnt_fx, tb_freq_meter.u_freq_core.cnt_ref); end freq_core u_freq_core( .clk_50m (clk_50m), .rst_n (rst_n), .fx_in (fx_in), .gate_in (tb_gate_ctrl), .cnt_fx (), .cnt_ref (), .latch_en() ); endmodule注意看#25这个写法:基准时钟半周期是 10 ns,被测信号半周期 25 ns,两者永远不同步,这才能模拟出真实的异步采样。如果非要让被测信号和基准时钟存在整数倍关系,仿真结果会过于理想,上板后就露馅。
恢复复位后,建议先给 200 ns 以上的时间让同步器把内部寄存器状态推起来,然后再放开闸门。$stop放在什么时候要估算好:100 ms 的实际闸门在仿真里是跑不完的,所以闸门时间在 testbench 里要单独定义,而不是用顶层分频后的真实值。
4.2 仿真时间太长怎么办:参数化闸门
这是做仿真时最容易卡住的点。顶层逻辑里闸门如果是 1 秒,仿真就得等 1 秒虚拟时间才能看到一次完整的计数结果,而这期间 testbench 的时钟更新都在跑,仿真器可能几分钟都跑不完。解决办法是把闸门生成模块的参数化,仿真实例化时覆盖成 1000 个基准时钟周期,也就是 20 微秒。
module gate_gen #( parameter DIV_CNT = 50_000_000 // 1秒@50MHz )( input wire clk, input wire rst_n, output reg gate ); reg [$clog2(DIV_CNT)-1:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; gate <= 1'b0; end else if (cnt == DIV_CNT - 1) begin cnt <= 0; gate <= ~gate; end else begin cnt <= cnt + 1'b1; gate <= gate; end end endmoduletestbench 里实例化时覆盖参数,比如gate_gen #(.DIV_CNT(5000)) u_gate(...),这样闸门周期就是 5000 个时钟周期,100 微秒,仿真器秒出结果。上板综合时,顶层实例化的参数保持默认的 5000 万,两边互不影响。
这种参数化技巧不只是为了省时间,还能帮你检查代码在不同闸门长度下是否都能正确锁存。我一般会分别用 1000、5000、20000 三个参数各跑一轮,确认计数结果都符合公式。
4.3 引脚约束与上板检查清单
仿真全通之后,上板前还有一道关卡是引脚约束。最容易报错的是未约束引脚,其次是位号和原理图对不上。新板子拿到手先看原理图,确认基准时钟接在哪个引脚、按键是高有效还是低有效、数码管是共阴还是共阳,这些信息全在原理图里。
给一个检查清单,按顺序逐项勾:
| 检查项 | 常见问题 | 处理方式 |
|---|---|---|
| 时钟引脚 | 用错了差分时钟引脚 | 查看原理图中的时钟网络,单端时钟直接约束 |
| 复位引脚 | 按键按下是高电平,代码却用低有效 | 确认按键电路是上拉还是下拉 |
| 数码管极性 | 共阴共阳写反 | 段码表整体取反 |
| 被测信号输入 | 引脚没有接上拉/下拉 | 用开发板自带的按键或信号源连接器 |
| 位选时序 | 高有效位选写成低有效 | 对照原理图修改 scan_sel 极性 |
| 未约束引脚 | 综合时报大量 I/O 错误 | 在约束文件中把所有用到的引脚全部声明 |
上板调试还有一个血泪经验:不要一开始就接真实信号源。先把闸门改成内部自测模式,用一个固定的分频信号替代被测信号,比如把 50 MHz 分频出 1 kHz 接到 fx_in 入口,看频率计显示是否接近 1000 Hz。自测通过,再上信号发生器。这一步能帮你把「代码问题」和「接线问题」隔离开,我每次都先这么干。
5. 课设高频踩坑排查:显示错、跳变、毛刺与时序违例
5.1 显示频率翻倍或减半:多半是闸门时间算错
现象很直接:输入一个 1000 Hz 的标准方波,数码管显示 1999 或者 500 附近乱跳,而不是稳定的 1000。有人马上怀疑计数器写错了,其实计数器只要位宽够,几乎不会出这种问题。
原因分析下来分两类。一类是闸门生成模块里分频系数差了个 2,导致闸门实际宽度不是预设值。假如你想做 1 秒闸门,50 MHz 需要计数 5000 万个周期,但代码里写成 2500 万,闸门只有 0.5 秒,显示自然翻倍。另一类是等精度换算时忘记除以标准时钟计数值,直接拿 cnt_fx 当结果,这在闸门不是整数秒时误差一目了然。
解决办法是先把闸门生成模块单独仿真,测量 gate 信号的高电平时长是否精确等于预定值。这里提醒一个细节:gate 信号如果是周期方波,闸门时间是半个周期,不是整个周期,很多翻车都翻在这个地方。
5.2 低频端跳得厉害:直接测频法的分辨力不够
输入 10 Hz 的信号,数码管一会儿显示 9,一会儿显示 11,换不同的时间点测结果都不同。这个现象特别容易误导人,以为代码里有 bug,其实是因为用了直接测频方案,闸门时间内被测信号的脉冲数太少,±1 误差占比太大。
比如闸门 1 秒,10 Hz 信号只有 10 个脉冲,误差 10%,显示在 9 到 11 之间波动完全正常。如果你非要用直接测频法,那就得把闸门拉到 10 秒,显示刷新慢不说,测量速度也没法接受。
正确路线是切换到等精度测频。等精度方案下,10 Hz 信号在 1 秒闸门里同样只有 10 个脉冲,但标准时钟计数 5000 万个,最终误差取决于 5000 万这个大数的计数误差,而不是 10 的小数计数误差。所以低频端跳变,第一件要怀疑的就是方案选型,而不是具体代码。
5.3 数码管闪烁和数据毛刺:跨时钟域没有处理
上板后手一碰到接线,数字就乱跳;或者信号源频率没变,但显示偶尔偏大偏小。这种随机性很强的现象,多半是跨时钟域在捣鬼。被测信号 fx_in 跟基准时钟是异步的,如果数字频率计的入口直接拿它去和内部计数器做逻辑,就会采集到亚稳态。
亚稳态的表现是不可预测的:某次采样恰好落在信号变化窗口内,触发器输出既不是 0 也不是 1,而是处于中间状态,经过组合逻辑传播出去就变成了随机毛刺。等精度测频里这个坑尤其隐蔽,因为计数器本身跑得很快,一个毛刺就能让计数值错 1。
解决方法是入口处加两级同步器,强迫 fx_in 先经过两拍触发器稳定后再参与计数。如果被测信号频率比较高,两级同步会带来两个周期的延时,但这对测量结果没有影响,因为同步器和闸门的延时是一致的。加了同步器之后毛刺基本消失,这是我在这个项目里最有价值的排查经验之一。
5.4 时序违例:组合逻辑环路和位宽截断
综合报错或者布局布线之后时序不过关,常见原因是代码里写了不纯粹的组合逻辑。BCD 转换模块里如果变量声明写错,综合器会推断出一个巨大的组合逻辑级联,路径延迟超过基准时钟周期,时序自然收不住。
另一个隐蔽问题是位宽截断。两个计数器相加或者移位的时候,如果赋值的左侧位宽小于表达式结果位宽,综合工具会悄悄截断高位,仿真环境里通常看不出问题,但上板后数字会整体掉一位。解决办法是统一用局部参数定义位宽,所有涉及计数器的地方都引用同一个参数,不要在每个模块里重复写数字。
遇到时序违例,先用开发环境的时序报告看最长路径在哪个模块,再回头查这个模块里的循环语句和位宽。BCD 转换建议做两级流水,中间插一拍寄存器,这样路径长度能砍掉一半。课设规模不大,这两招一般就能收时序。
5.5 仿真黑匣子:模块实例名和层次引用写错
仿真里最让人抓狂的报错是「模块实例找不到」或者引用内部信号失败。$monitor或者波波窗口里想观察某个内部信号,结果提示找不到路径,不得不返回去改 testbench。
根本原因是层次路径写错了。上一条代码里我用tb_freq_meter.u_freq_core.cnt_fx引用内部信号,这个写法要求实体名称和模块实例名恰好一致。如果 testbench 顶层名字改了,或者模块实例化时实例名换了,整个层次路径就崩了。
一个不容易翻车的习惯是:testbench 顶层模块名固定写成tb_freq_meter,模块实例名固定写u_freq_core,内部关键信号统一在模块定义后用/* synthesis keep */标记保留。这样仿真器不会因为信号被综合优化掉而找不到,调试效率能提一大截。
6. 进阶:自动量程切换与等精度测频的工程化收尾
基础版做完之后,如果想在课设里加一点亮点,我建议做自动量程切换。思路不复杂:把显示结果按数值范围分成几档,比如小于 1 kHz 显示为 Hz 并点亮小数点位,大于 1 kHz 小于 1 MHz 显示为 kHz,更大显示为 MHz。实现上就是在 BCD 转换后加一个比较器和移位选择逻辑,量程选择结果同时控制小数点的位置。
注意自动量程会带来显示抖动的问题——信号频率落在档位边界附近时,数字会来回切换。教科书里一般不处理这个,但实际项目里可以加入滞回比较:升高时 95 kHz 切换到 kHz 档,降低时 90 kHz 才切回 Hz 档,中间留 5 kHz 缓冲区。滞回量用参数表示,方便调整。
还有一个小习惯值得写进你的工程:在顶层模块里加一个测量完成标志信号 mea_done,每次锁存后拉高一个周期。这个信号在有精确测量需求时可以对外部通知,在课设答辩时可以接一个 LED 指示频率计正在工作,整体电气行为会显得更完整。我自己做这题时,最后就是靠这个标志信号在示波器上验证了测量节拍,比反复刷新数码管直观得多。
回看整个设计,数字频率计最值钱的部分不是那一行行 Verilog,而是你愿不愿意把误差公式先写到纸上、把跨时钟域问题当成一等公民认真处理。第一次做的时候,我图省事跳过等精度方案,结果折腾了两天才发现是原理选型错了;后来老老实实按这篇文章的顺序重写了一遍,半天就跑通上板。希望这段经验能帮你少走同样的弯路,也希望你做完之后不只拿到一个通过的 Demo,而是能把每一步原理讲到让人点头——这才是这份作业真正留给你的东西。
本文还有配套的精品资源,点击获取