基于FPGA的数字频率计设计:从Verilog代码到上板验证的完整实战 简介这份资源是本科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_50minput150 MHz 系统时钟rst_ninput1低电平复位sig_ininput1待测信号输入seg_seloutput8数码管位选seg_dataoutput8数码管段选闸门时间通过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 32d0; gate_en 1b0; end else if (gate_cnt gate_max) begin gate_cnt 32d0; gate_en ~gate_en; // 每满一个闸门周期翻转 end else begin gate_cnt gate_cnt 1b1; end end // 扫描时钟分频50MHz / 50000 1kHz reg [15:0] scan_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt 16d0; scan_clk 1b0; end else if (scan_cnt 16d24999) begin scan_cnt 16d0; scan_clk ~scan_clk; end else begin scan_cnt scan_cnt 1b1; 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 1b0; sig_d1 1b0; sig_d2 1b0; 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 32d0; cnt_done 1b0; end else if (gate_en) begin cnt_done 1b0; if (sig_rise) cnt cnt 1b1; end else begin cnt_done 1b1; // 闸门结束给出完成标志 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 32d0; 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 3d0; else scan_idx scan_idx 1b1; end // 共阴数码管段码表 0-9 function [7:0] seg_code; input [3:0] num; case (num) 4d0: seg_code 8h3f; 4d1: seg_code 8h06; 4d2: seg_code 8h5b; 4d3: seg_code 8h4f; 4d4: seg_code 8h66; 4d5: seg_code 8h6d; 4d6: seg_code 8h7d; 4d7: seg_code 8h07; 4d8: seg_code 8h7f; 4d9: seg_code 8h6f; default: seg_code 8h00; endcase endfunction // 根据扫描索引选择位选和段码 always (*) begin seg_sel 8hff; // 默认全灭 case (scan_idx) 3d0: begin seg_sel 8b11111110; seg_data seg_code(value % 10); end 3d1: begin seg_sel 8b11111101; seg_data seg_code((value/10) % 10); end 3d2: begin seg_sel 8b11111011; 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输出是否在闸门结束时等于 1000cnt_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 项目都强制走一遍「仿真 → 综合 → 上板」的完整链路绝不跳过仿真直接烧板子因为上板调试的时间成本远高于仿真。希望帮到你。本文还有配套的精品资源点击获取