简介:这份资源是本科EDA课程期末大作业的完整提交包,主题为基于FPGA的数字频率计设计,面向电子信息、集成电路及相关专业学生与FPGA初学者,可用于课程设计参考、Quartus II与Verilog入门练手,或作为测频类项目的实现模板。压缩包共744个文件,约28.3MB,涵盖Verilog源码(.v)、VHDL文件(.vhd)、Quartus工程配置(.qpf、.qsf)、综合与仿真报告(.rpt、.vo、.vho)、波形文件(.wlf)、原理图与PCB文件(.schdoc、.pcbdoc)以及大量中间编译产物,完整保留了从设计到验证的全过程。设计采用自上而下的方法,将测频模块拆分为多个子模块,用Verilog实现各功能单元,再通过顶层元件例化完成整体连接,配套完整报告便于理解设计思路。目前已有4695人学习下载,适合需要完整工程参考与排错思路的读者。
1. 从一份 EDA 课设压缩包说起:FPGA 数字频率计到底能跑出什么
如果你正在做 EDA 课程设计,或者刚接触 FPGA 想找一个能完整跑通的小项目练手,这份「基于 FPGA 的数字频率计设计」压缩包大概率能省掉你不少翻论坛、拼代码的时间。它解决的是一个非常具体的问题:用 FPGA 测量输入信号的频率,并把结果稳定地显示出来。听起来简单,但真正动手你会发现,从分频、计数、锁存到数码管动态扫描,每一环都有坑。这个项目适合两类人:一类是课程设计 deadline 逼近、需要一份能编译能上板的参考实现;另一类是已经学过 Verilog 语法,但没做过完整时序逻辑系统,想通过一个闭环项目把「写模块 → 仿真 → 综合 → 下载」这条链路走通。数字频率计本身是 EDA 教学里的经典题目,因为它同时覆盖了时钟分频、边沿检测、计数器设计、数据锁存和显示驱动这几个核心知识点,麻雀虽小,五脏俱全。
2. 系统架构与模块划分:一个频率计由哪几块拼出来
2.1 整体数据流:从待测信号到数码管显示
数字频率计的核心原理是「闸门法」:在一个已知宽度的闸门时间内,统计待测信号的脉冲个数。如果闸门时间是 1 秒,计数值就是频率值(单位 Hz)。这个项目通常采用 1 Hz 到 1 MHz 的测量范围,闸门时间可选 1 秒或 0.1 秒,通过分频器从系统时钟产生。
整个系统的数据流是这样的:待测信号先经过输入整形和边沿检测,变成干净的计数脉冲;闸门信号控制计数器在固定时间内计数;计数结果在闸门结束时被锁存到寄存器;锁存后的数据经过 BCD 转换或直接二进制转十进制,送到数码管驱动模块进行动态扫描显示。系统时钟一般用 50 MHz 晶振,分频出 1 Hz 闸门信号和数码管扫描所需的 1 kHz 刷新时钟。
常见做法是把这些功能拆成独立模块,顶层只做例化和连线。这样仿真时能单独验证每个模块,综合后也方便看资源占用。
2.2 模块清单与目录结构
解压后典型的目录结构如下:
frequency_meter/ ├── rtl/ │ ├── top.v // 顶层模块 │ ├── clk_div.v // 时钟分频模块 │ ├── freq_counter.v // 频率计数模块 │ ├── data_latch.v // 数据锁存模块 │ ├── bin2bcd.v // 二进制转BCD模块 │ └── seg_driver.v // 数码管驱动模块 ├── sim/ │ ├── tb_top.v // 顶层测试平台 │ └── tb_freq_counter.v // 计数模块测试平台 ├── prj/ │ └── fpga_prj.qpf // 工程文件 └── doc/ └── design_report.pdf // 设计报告每个模块职责明确:clk_div负责产生闸门信号和扫描时钟;freq_counter在闸门使能期间对输入脉冲计数;data_latch在闸门下降沿锁存计数值,保证显示稳定不闪烁;bin2bcd把二进制计数结果转成十进制位;seg_driver完成数码管的动态扫描和段码译码。顶层top.v把这些模块串起来,并定义输入输出端口。
注意:不同开发板的数码管极性可能不同,共阳和共阴的段码表要对应修改,否则显示会反相。
2.3 关键参数与端口定义
顶层模块的端口通常包括:
| 端口名 | 方向 | 位宽 | 说明 |
|---|---|---|---|
| clk_50m | input | 1 | 50 MHz 系统时钟 |
| rst_n | input | 1 | 低电平复位 |
| sig_in | input | 1 | 待测信号输入 |
| seg_sel | output | 8 | 数码管位选 |
| seg_data | output | 8 | 数码管段选 |
闸门时间通过clk_div的参数GATE_TIME配置,默认 1 秒。计数位宽一般取 32 位,足够覆盖 1 MHz 量程。数码管位数常见为 8 位,显示频率值加单位指示。
3. 核心模块代码拆解:分频、计数、锁存与显示驱动
3.1 时钟分频模块:闸门信号和扫描时钟怎么来
module clk_div #( parameter CLK_FREQ = 50_000_000, // 系统时钟频率 parameter GATE_TIME = 1 // 闸门时间,单位秒 )( input wire clk, input wire rst_n, output reg gate_en, // 闸门使能,高有效 output reg scan_clk // 数码管扫描时钟,约1kHz ); // 闸门计数器:计到 CLK_FREQ * GATE_TIME - 1 翻转 reg [31:0] gate_cnt; wire [31:0] gate_max = CLK_FREQ * GATE_TIME - 1; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin gate_cnt <= 32'd0; gate_en <= 1'b0; end else if (gate_cnt == gate_max) begin gate_cnt <= 32'd0; gate_en <= ~gate_en; // 每满一个闸门周期翻转 end else begin gate_cnt <= gate_cnt + 1'b1; end end // 扫描时钟分频:50MHz / 50000 = 1kHz reg [15:0] scan_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt <= 16'd0; scan_clk <= 1'b0; end else if (scan_cnt == 16'd24999) begin scan_cnt <= 16'd0; scan_clk <= ~scan_clk; end else begin scan_cnt <= scan_cnt + 1'b1; end end endmodule这段代码做了两件事。第一,用gate_cnt对系统时钟计数,每计满CLK_FREQ * GATE_TIME个周期就把gate_en翻转一次,这样gate_en的高电平持续时间正好是闸门时间。第二,用scan_cnt分频出约 1 kHz 的scan_clk,供数码管动态扫描使用。参数GATE_TIME可以改成 0.1 来切换量程,但注意计数结果需要相应乘以 10 才能得到真实频率值。
逻辑说明:gate_en的翻转周期是 2 倍闸门时间,高电平期间计数,低电平期间锁存和复位计数器。这种设计避免了计数和锁存同时发生导致的竞争。
3.2 频率计数模块:边沿检测与闸门控制
module freq_counter ( input wire clk, input wire rst_n, input wire gate_en, // 闸门使能 input wire sig_in, // 待测信号 output reg [31:0] cnt, // 计数值 output reg cnt_done // 计数完成标志 ); // 输入信号同步打拍,消除亚稳态 reg sig_d0, sig_d1, sig_d2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sig_d0 <= 1'b0; sig_d1 <= 1'b0; sig_d2 <= 1'b0; end else begin sig_d0 <= sig_in; sig_d1 <= sig_d0; sig_d2 <= sig_d1; end end // 上升沿检测 wire sig_rise = sig_d1 & ~sig_d2; // 闸门内计数 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 32'd0; cnt_done <= 1'b0; end else if (gate_en) begin cnt_done <= 1'b0; if (sig_rise) cnt <= cnt + 1'b1; end else begin cnt_done <= 1'b1; // 闸门结束,给出完成标志 end end endmodule这里的关键点是输入信号同步。sig_in是异步信号,直接用来计数会引入亚稳态,所以用三级寄存器打拍,然后用sig_d1 & ~sig_d2检测上升沿。计数只在gate_en为高时进行,闸门结束后cnt_done拉高一个周期,通知锁存模块取数。
参数说明:cnt位宽 32 位,实际综合时如果量程只有 1 MHz,可以缩到 20 位节省资源。sig_rise每个上升沿只产生一个时钟周期的高脉冲,保证每个信号周期只计一次。
3.3 数据锁存与 BCD 转换:让显示不跳变
module data_latch ( input wire clk, input wire rst_n, input wire cnt_done, input wire [31:0] cnt_in, output reg [31:0] cnt_latch ); always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt_latch <= 32'd0; else if (cnt_done) cnt_latch <= cnt_in; // 闸门结束时锁存 end endmodule锁存模块非常简单,但作用很大。如果没有它,数码管会直接显示计数器的实时值,导致低位数字快速跳动,人眼根本看不清。锁存后,显示值在下一个闸门周期内保持不变,刷新率就是闸门频率。
BCD 转换模块负责把 32 位二进制数转成 8 位十进制数字。常见做法是用「双 dabble」算法,或者直接用除法取余。对于课设级别,除法取余更直观:
// 简化版:逐位取余,实际综合可能用流水线 always @(*) begin bcd[3:0] = cnt_latch % 10; bcd[7:4] = (cnt_latch / 10) % 10; bcd[11:8] = (cnt_latch / 100) % 10; // ... 依次类推 end注意:除法器在 FPGA 里资源消耗较大,如果只做课设,综合工具能优化掉常数除法,问题不大。但如果要跑更高频率,建议换成双 dabble 算法。
3.4 数码管动态扫描:段码表和位选逻辑
module seg_driver ( input wire clk, input wire rst_n, input wire scan_clk, input wire [31:0] value, output reg [7:0] seg_sel, output reg [7:0] seg_data ); reg [2:0] scan_idx; always @(posedge scan_clk or negedge rst_n) begin if (!rst_n) scan_idx <= 3'd0; else scan_idx <= scan_idx + 1'b1; end // 共阴数码管段码表 0-9 function [7:0] seg_code; input [3:0] num; case (num) 4'd0: seg_code = 8'h3f; 4'd1: seg_code = 8'h06; 4'd2: seg_code = 8'h5b; 4'd3: seg_code = 8'h4f; 4'd4: seg_code = 8'h66; 4'd5: seg_code = 8'h6d; 4'd6: seg_code = 8'h7d; 4'd7: seg_code = 8'h07; 4'd8: seg_code = 8'h7f; 4'd9: seg_code = 8'h6f; default: seg_code = 8'h00; endcase endfunction // 根据扫描索引选择位选和段码 always @(*) begin seg_sel = 8'hff; // 默认全灭 case (scan_idx) 3'd0: begin seg_sel = 8'b11111110; seg_data = seg_code(value % 10); end 3'd1: begin seg_sel = 8'b11111101; seg_data = seg_code((value/10) % 10); end 3'd2: begin seg_sel = 8'b11111011; seg_data = seg_code((value/100) % 10); end // ... 依次类推 default: ; endcase end endmodule动态扫描的原理是利用人眼视觉暂留,每 1 ms 左右切换一位数码管,8 位轮流显示一遍约 8 ms,刷新率 125 Hz,看起来就是稳定显示。seg_sel是位选信号,低电平有效;seg_data是段选,对应 a 到 g 和小数点。
参数说明:段码表要根据开发板是共阴还是共阳调整。共阳数码管把seg_code取反即可。位选顺序也要和硬件原理图对应,否则显示位置会错乱。
4. 仿真、综合与上板:从代码到硬件的完整链路
4.1 测试平台怎么写:给计数器喂一个已知频率
`timescale 1ns / 1ps module tb_top; reg clk_50m; reg rst_n; reg sig_in; wire [7:0] seg_sel; wire [7:0] seg_data; // 例化顶层 top u_top ( .clk_50m (clk_50m), .rst_n (rst_n), .sig_in (sig_in), .seg_sel (seg_sel), .seg_data (seg_data) ); // 50MHz 时钟 initial clk_50m = 0; always #10 clk_50m = ~clk_50m; // 周期 20ns // 产生 1kHz 待测信号 initial begin sig_in = 0; forever #500000 sig_in = ~sig_in; // 周期 1ms end // 复位和运行 initial begin rst_n = 0; #200 rst_n = 1; #2_000_000_000 $stop; // 仿真 2ms,观察计数 end endmodule这个测试平台产生 50 MHz 时钟和 1 kHz 待测信号,理论上闸门 1 秒后计数值应该是 1000。仿真时可以把GATE_TIME改成 0.001 秒来加速,这样 1 ms 就能看到结果。观察cnt输出是否在闸门结束时等于 1000,cnt_done是否正常拉高。
注意:仿真时间单位要和
timescale对应,#500000在 1ns 精度下是 0.5 ms,正好产生 1 kHz 方波。
4.2 综合与引脚分配:别让工具替你猜
综合前要确认工程里选了正确的器件型号。引脚分配是上板成功的关键一步,常见做法是在 Quartus 的 Pin Planner 里手动绑定,或者写一个.qsf文件:
set_location_assignment PIN_R8 -to clk_50m set_location_assignment PIN_J15 -to rst_n set_location_assignment PIN_A8 -to sig_in set_location_assignment PIN_A15 -to seg_sel[0] # ... 其余引脚依次分配引脚号必须查开发板原理图,不能随便填。clk_50m要接到晶振输出引脚,rst_n接复位按键,sig_in接待测信号源或信号发生器。数码管引脚要对应位选和段选的实际连接。
综合报告里重点看两个数:Logic Elements 占用率和最大工作频率。这个项目资源占用很低,一般不超过 5%,时序也能轻松跑到 100 MHz 以上。如果综合报错,先检查有没有多个驱动源或者位宽不匹配。
4.3 上板验证:信号发生器加示波器最直接
上板后,用信号发生器输出一个已知频率的方波,比如 1 kHz、10 kHz、100 kHz,看数码管显示是否一致。如果没有信号发生器,可以用开发板上的另一个时钟输出引脚自己产生一个已知频率。误差一般在 ±1 个字以内,属于正常量化误差。
如果显示不稳定,先检查闸门信号和锁存时序。常见问题是闸门结束和计数器复位之间的竞争,导致锁存到错误值。解决办法是在cnt_done后延迟一个周期再复位计数器,或者用gate_en的下降沿触发锁存。
5. 避坑与排查:课设里最容易翻车的几个点
5.1 数码管显示乱码或反相
现象:上板后数码管显示的不是数字,而是乱码或者全亮全灭。 原因:段码表极性和开发板不匹配,共阴和共阳搞反了。 解决:查开发板原理图,确认数码管是共阴还是共阳。共阳数码管把段码取反,位选信号也要对应调整高低有效。
5.2 计数值始终为零或满量程
现象:无论输入什么信号,计数值一直是 0 或者直接跳到最大值。 原因:输入信号没有同步打拍,亚稳态导致边沿检测失效;或者闸门信号没有正确产生。 解决:检查sig_in是否真的接到了信号源,用示波器看引脚有没有波形。确认gate_en在仿真里是否正常翻转。如果仿真对但上板不对,多半是引脚分配错了。
5.3 显示值跳动严重
现象:数码管低位数字快速变化,根本读不出稳定数值。 原因:没有锁存,直接显示了实时计数值。 解决:确认data_latch模块在cnt_done时锁存,并且锁存值送到显示模块。如果锁存了还跳,检查cnt_done是否每个闸门周期只产生一个脉冲。
5.4 综合报错「multiple drivers」
现象:综合工具提示某个信号有多个驱动源。 原因:在多个always块里对同一个reg赋值,或者顶层例化时端口连接重复。 解决:每个信号只在一个always块里赋值。检查顶层例化有没有把同一个输出连到两个模块。
5.5 闸门时间不准导致频率误差大
现象:显示频率和信号源设定值差很多,误差超过 1%。 原因:系统时钟频率和参数CLK_FREQ不一致,或者闸门计数器位宽不够溢出。 解决:确认开发板晶振频率,修改CLK_FREQ参数。检查gate_cnt位宽是否足够,50 MHz 计 1 秒需要 26 位,用 32 位是安全的。
6. 二次开发与扩展:从课设到能用的频率计
如果你已经跑通了基础版本,想把它改成一个更实用的频率计,有几个方向可以动手。第一个是自动量程切换:根据当前计数值判断频率范围,自动调整闸门时间。频率低于 1 kHz 时用 1 秒闸门,高于 1 MHz 时用 0.01 秒闸门,这样既能保证低频精度,又能覆盖高频。实现方法是在顶层加一个状态机,根据cnt_latch的值动态修改GATE_TIME参数。注意参数在 Verilog 里是编译时常量,要动态改就得把闸门计数器做成可变阈值,用寄存器存储当前闸门上限。
第二个是增加频率单位显示。现在数码管只显示数字,不知道是 Hz 还是 kHz。可以加一位数码管显示单位,或者用小数点位置表示量程。比如显示「1.000」表示 1 kHz,「10.00」表示 10 kHz。这需要在 BCD 转换后根据量程插入小数点控制位。
第三个是加一个串口输出,把频率值送到电脑端显示。用 UART 发送模块,每秒钟发一次锁存值,波特率 9600 或 115200。这样可以用串口助手看数据,也方便做长期记录。UART 发送模块的代码在大部分 FPGA 教程里都有现成的,直接例化就行。
第四个是提高测量精度。闸门法在低频段误差较大,可以改用等精度测量法:同时计待测信号和标准时钟的脉冲数,用两者的比值算频率。这样在整個量程内精度一致,但代码复杂度会高不少,适合想深入做时序设计的同学。
我自己的习惯是,每次改完参数后先在仿真里跑一遍已知频率,确认计数值正确再上板。上板时先用低频信号验证显示逻辑,再逐步提高频率看量程切换是否正常。从那以后我每次做 FPGA 项目都强制走一遍「仿真 → 综合 → 上板」的完整链路,绝不跳过仿真直接烧板子,因为上板调试的时间成本远高于仿真。希望帮到你。
本文还有配套的精品资源,点击获取