简介:这份资源是本科EDA课程期末大作业《基于FPGA的数字频率计设计》的完整提交包,面向电子信息、自动化等专业正在学习EDA与FPGA开发的学生,以及需要参考测频模块设计思路的初学者。项目基于Quartus II开发环境,采用Verilog编写,核心测频模块遵循自上而下的设计方法,按功能划分为多个子模块,再通过顶层文件的元件例化语句连接成完整系统,配套完整报告、代码、PCB与原理图,可直接对照学习工程组织与实现流程。压缩包共744个文件,约28.3MB,以cdb、hdb、ddb等Quartus工程数据库文件,tdf、v、vhd等源码文件,rpt、qmsg、summary等编译报告,以及sof、qsf、qpf等下载与工程配置文件为主,另含schdoc、pcbdoc等原理图与PCB文档。目前已有4695人学习下载,适合作为课程设计参考、模块化设计范例与工程文件结构的学习样本。
1. 从一份 EDA 课设压缩包说起:FPGA 数字频率计到底能跑多快
每年到了期末,后台总有人问同一个问题:老师扔过来一个.rar,里面是「基于 FPGA 的数字频率计设计」,要求两周内跑通、写报告、答辩。打开一看,Verilog 源码、约束文件、仿真波形、还有一份说明文档,东西不少,但真上手就懵——时钟怎么接、闸门时间怎么定、数码管为什么乱跳。这份资源就是冲着这个场景来的:它把数字频率计从原理到实现完整打包,适合正在做 EDA 课程设计的学生,也适合想拿一个完整小项目练手 FPGA 的工程师。数字频率计本身不复杂,核心就一句话:在固定闸门时间内数被测信号的周期数,再换算成频率。但真到板子上跑,量程切换、精度取舍、显示刷新这几件事,每一个都能让你调一晚上。下面我按自己拆包复现的顺序,把这份资源讲透。
2. 数字频率计的测频原理与整体方案选型
2.1 直接测频法:为什么它是课设的首选
数字频率计最常用的方法是直接测频法,也叫计数法。原理很朴素:用一个已知频率的标准时钟去产生一个固定宽度的闸门信号,闸门打开期间,被测信号经过整形后送入计数器,计数值就是这段时间内的脉冲个数。设闸门时间为 $T_g$,计数值为 $N$,则被测频率 $f_x = N / T_g$。
举个例子,闸门时间取 1 秒,计数器读到 50000,那频率就是 50 kHz。这个公式简单到不需要推导,但它的误差特性决定了整个设计的走向。计数法在测高频时相对误差小,因为 $N$ 大,$\pm 1$ 个数的量化误差占比就小;测低频时 $N$ 很小,误差就上去了。所以课设里通常要求分档:高频档用短闸门,低频档用长闸门,或者干脆用等精度测频法来补。
这份资源采用的是经典直接测频法,配合量程切换。为什么不用等精度法?因为课设的评分点往往在「完整实现 + 报告清晰」,等精度法虽然精度高,但多机同步、握手逻辑对初学者不友好,调试成本高。直接测频法逻辑清晰、模块划分干净,是性价比最高的选择。
2.2 系统框图与模块划分
整个设计可以拆成五个模块,这也是资源里源码的组织方式:
| 模块 | 功能 | 关键信号 |
|---|---|---|
| 分频模块 | 把板载高频时钟分频成 1 Hz 闸门信号 | clk_in, clk_gate |
| 闸门控制 | 产生闸门使能、锁存脉冲、清零脉冲 | gate_en, latch, rst_cnt |
| 计数模块 | 在闸门内对被测信号计数 | sig_in, count_val |
| 锁存模块 | 闸门结束时锁存计数值,避免显示抖动 | count_val, data_out |
| 显示驱动 | 动态扫描数码管,把计数值转成七段码 | data_out, seg, sel |
这个划分的好处是每个模块职责单一,仿真时能单独验证。我一般会先把分频和计数两个模块跑通,再接显示,最后整体联调。资源里的顶层文件就是把这五个模块例化连起来,端口命名比较规范,读起来不费劲。
2.3 闸门时间与量程的取舍
闸门时间不是随便定的。它直接决定两个东西:测量精度和刷新率。闸门时间越长,计数值越大,量化误差越小,但显示刷新越慢。1 秒闸门意味着每秒才更新一次读数,人眼看着还行;如果做 10 秒闸门,精度上去了,但读数半天不动,体验很差。
常见做法是分档:
- 高频档:闸门 0.1 s,适合测 1 MHz 以上信号
- 中频档:闸门 1 s,适合 1 kHz 到 1 MHz
- 低频档:闸门 10 s,适合 1 kHz 以下
资源里默认用的是 1 s 闸门,对应板载 50 MHz 时钟需要分频到 1 Hz。分频系数是 50,000,000,这个数在 Verilog 里要算清楚位宽,否则计数器溢出。我见过有人用 24 位计数器去装 50M,结果高位截断,闸门周期直接错乱,波形看着像玄学。
3. 从源码到板级:分频、计数、锁存、显示四步落地
3.1 分频模块:把 50 MHz 变成 1 Hz 闸门
分频模块是整个系统的时间基准,写错了后面全崩。资源里的分频逻辑是标准的计数器翻转法:
// 50MHz -> 1Hz 分频,计数到 25_000_000 翻转一次 module clk_div( input wire clk, // 板载 50MHz 时钟 input wire rst_n, // 低电平复位 output reg clk_gate // 输出 1Hz 闸门信号 ); reg [25:0] cnt; // 26 位足够装下 25_000_000 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 26'd0; clk_gate <= 1'b0; end else if (cnt == 26'd24_999_999) begin cnt <= 26'd0; clk_gate <= ~clk_gate; // 每半周期翻转,得到 1Hz end else begin cnt <= cnt + 1'b1; end end endmodule逻辑说明:计数器从 0 累加到 24,999,999 正好是 0.5 秒,翻转一次输出,再数 0.5 秒再翻转,合起来一个完整周期就是 1 秒。参数说明:cnt位宽必须满足 $2^{26} = 67,108,864 > 25,000,000$,所以 26 位够用。如果你板子时钟不是 50 MHz,把比较值改成clk_freq/2 - 1即可。注意复位用低电平有效,和大多数开发板按键一致。
3.2 计数与闸门控制:别让计数器和闸门打架
计数模块要在闸门使能期间对被测信号计数,闸门一关立刻停止,同时产生锁存脉冲把数据送出去。这里最容易翻车的地方是闸门信号和被测信号不同步,导致多计或少计一个数。资源里的做法是用闸门信号做使能,被测信号做时钟:
module counter( input wire sig_in, // 被测信号,已整形 input wire gate_en, // 闸门使能,高有效 input wire rst_cnt, // 计数清零 output reg [31:0] count_val // 计数值 ); always @(posedge sig_in or posedge rst_cnt) begin if (rst_cnt) count_val <= 32'd0; else if (gate_en) count_val <= count_val + 1'b1; end endmodule逻辑说明:gate_en为高时每个被测信号上升沿计数加一,rst_cnt用来在每次测量前清零。参数说明:count_val用 32 位是留余量,实际 1 秒闸门测 50 MHz 也才 50,000,000,32 位足够。注意这里用的是异步复位,仿真时要注意复位释放时刻,避免亚稳态。常见做法是在闸门控制模块里用标准时钟同步后再输出rst_cnt,资源里也是这么处理的。
3.3 锁存模块:解决数码管跳变的关键
如果没有锁存,计数器每个周期都在变,数码管显示会疯狂跳动,根本读不出数。锁存模块的作用是在闸门结束时把当前计数值存下来,保持到下一次测量结束:
module latch( input wire clk, input wire latch_en, // 闸门结束时的锁存脉冲 input wire [31:0] count_val, output reg [31:0] data_out ); always @(posedge clk) begin if (latch_en) data_out <= count_val; end endmodule逻辑说明:latch_en是一个单周期脉冲,在闸门下降沿后产生,把计数值锁进data_out。参数说明:latch_en的时序很关键,必须在计数器停止之后、下一次清零之前产生。资源里用了一个状态机来生成这个脉冲,顺序是:闸门开 → 计数 → 闸门关 → 锁存 → 清零 → 下一个闸门。这个顺序错了,要么锁到旧数据,要么锁到零。
3.4 数码管动态扫描:显示驱动的时序要求
数码管动态扫描的原理是快速轮流点亮各位,利用人眼视觉暂留形成稳定显示。资源里用的是 6 位数码管,扫描频率一般在 1 kHz 左右,太快亮度不够,太慢会闪:
module seg_driver( input wire clk, // 扫描时钟,建议 1kHz input wire [31:0] data_out, // 锁存的频率值 output reg [7:0] seg, // 段选 output reg [5:0] sel // 位选 ); reg [2:0] scan_cnt; reg [3:0] disp_num; always @(posedge clk) begin scan_cnt <= scan_cnt + 1'b1; case (scan_cnt) 3'd0: begin sel <= 6'b111110; disp_num <= data_out[3:0]; end 3'd1: begin sel <= 6'b111101; disp_num <= data_out[7:4]; end 3'd2: begin sel <= 6'b111011; disp_num <= data_out[11:8]; end 3'd3: begin sel <= 6'b110111; disp_num <= data_out[15:12]; end 3'd4: begin sel <= 6'b101111; disp_num <= data_out[19:16]; end 3'd5: begin sel <= 6'b011111; disp_num <= data_out[23:20]; end default: sel <= 6'b111111; endcase end // 七段码译码,共阳极低有效 always @(*) begin case (disp_num) 4'd0: seg = 8'hc0; 4'd1: seg = 8'hf9; 4'd2: seg = 8'ha4; 4'd3: seg = 8'hb0; 4'd4: seg = 8'h99; 4'd5: seg = 8'h92; 4'd6: seg = 8'h82; 4'd7: seg = 8'hf8; 4'd8: seg = 8'h80; 4'd9: seg = 8'h90; default: seg = 8'hff; endcase end endmodule逻辑说明:scan_cnt循环计数,每次选中一位数码管,同时把对应位的 BCD 码送译码器。参数说明:sel是位选,低有效;seg是段选,共阳极低有效。如果你的板子是高有效,把译码表取反即可。注意扫描时钟不能直接用 50 MHz,否则亮度极低,需要再分频到 1 kHz 左右。
4. 仿真、约束与上板调试:那些让你调一晚上的坑
4.1 仿真波形怎么看:重点盯三个时刻
仿真不是跑完就行,要看关键时序。我一般重点看三个时刻:闸门上升沿、闸门下降沿、锁存脉冲。闸门上升沿要看计数器是否从零开始;下降沿要看计数是否立刻停止;锁存脉冲要看是否在计数停止后产生。资源里带了 testbench,被测信号用 1 kHz 方波模拟,闸门 1 秒,理论上计数值应该是 1000。如果仿真读到 999 或 1001,说明闸门和被测信号有相位差,属于正常量化误差,不用慌。
4.2 引脚约束:管脚分配错了,板子就是砖
约束文件是把 Verilog 端口映射到物理引脚的关键。资源里给的是某常见开发板的约束模板,但不同板子引脚不同,直接抄会翻车。常见做法是:
# 时钟引脚 set_location_assignment PIN_E1 -to clk # 复位按键 set_location_assignment PIN_M1 -to rst_n # 被测信号输入 set_location_assignment PIN_A1 -to sig_in # 数码管段选 set_location_assignment PIN_C14 -to seg[0] # ... 其余引脚按板子手册填逻辑说明:每个set_location_assignment把端口绑定到具体引脚。参数说明:引脚编号必须查你手上板子的原理图,不能照搬。我见过有人把时钟绑到普通 IO 上,结果板子跑不起来,查了半天以为是代码问题,其实是引脚错了。
4.3 常见问题排查:现象、原因、解决
现象一:数码管显示全亮或全灭。原因通常是位选和段选极性搞反了。共阳数码管段选低有效,共阴高有效,位选也一样。解决方法是查板子手册确认极性,把译码表或位选信号取反。
现象二:读数始终为零。先看被测信号有没有进来,用示波器或逻辑分析仪量一下输入引脚。如果信号正常,再看闸门使能有没有产生,仿真里确认gate_en有波形。常见原因是分频模块比较值算错,闸门周期变成几微秒,计数器根本来不及计。
现象三:读数跳动很大。多半是锁存没做好,或者闸门和清零时序重叠。检查latch_en和rst_cnt是否在同一周期产生,如果是,计数器可能被清零后又锁存,读到零。解决方法是把锁存和清零错开一个周期。
现象四:高频测不准,低频反而准。这是直接测频法的固有特性,高频时量化误差占比小,低频时占比大。如果要求全量程高精度,需要换等精度测频法,但那超出课设范围了。
现象五:综合报错说时钟资源不够。多半是把多个时钟信号都接到了全局时钟引脚。常见做法是用一个主时钟,其他时钟通过分频产生,不要用多个外部晶振。
5. 进阶技巧:把频率计从「能跑」做到「好用」
5.1 量程自动切换的实现思路
默认 1 秒闸门只能覆盖有限范围,想做好用可以加自动量程。思路是:先用一个短闸门粗测,根据计数值判断量级,再切换到合适闸门精测。比如先 0.01 秒闸门测一次,如果计数值小于 100,说明频率低于 10 kHz,切到 1 秒闸门;如果大于 10000,说明高于 1 MHz,切到 0.1 秒闸门。这个逻辑用一个简单状态机就能实现,资源里没带,但接口留了扩展空间。
5.2 用等精度法提升低频精度
等精度测频法的核心是让闸门信号和被测信号同步,实际闸门时间是被测信号周期的整数倍,这样低频时也能保证 $\pm 1$ 个标准时钟的精度。实现上需要两个计数器:一个数被测信号,一个数标准时钟,闸门由被测信号开启和关闭。代价是逻辑复杂一些,但对低频测量提升明显。如果你的课设要求全量程等精度,可以在这个框架上改。
5.3 验证方法:用信号发生器扫一遍量程
板子跑起来只是第一步,验证要系统。我一般会用信号发生器从 10 Hz 扫到 10 MHz,每个量级取几个点,记录读数误差。重点看三个区间:低频段(<1 kHz)误差是否在可接受范围,中频段(1 kHz~1 MHz)是否稳定,高频段(>1 MHz)是否丢数。如果某个区间误差突然变大,多半是闸门时间不匹配,换档再测。
5.4 一个具体技巧:用锁相环替代分频
如果板子有 PLL 资源,可以用 PLL 产生一个干净的闸门时钟,比计数器分频更稳定。PLL 输出的时钟抖动小,闸门边沿更陡,计数误差更小。配置方法是在 Quartus 或 Vivado 里调 IP 核,输入 50 MHz,输出 1 Hz 或 0.1 Hz。注意 PLL 输出低频时可能超出锁定范围,常见做法是 PLL 输出 1 MHz,再分频到 1 Hz。
从那以后我每次做时序相关的课设,都强制先把仿真波形看三遍再上板,省得对着板子发呆。希望帮到你。
本文还有配套的精品资源,点击获取