简介:这份资源是本科EDA课程期末大作业的完整提交包,主题为基于FPGA的数字频率计设计,面向电子信息、集成电路及相关专业学生与FPGA初学者,可用于课程设计参考、Quartus II工程实践与Verilog模块化设计学习。压缩包共744个文件,约28.3MB,涵盖Verilog源码、VHDL文件、Quartus工程配置、综合与仿真报告、原理图与PCB文件、Altium设计文档及完整设计报告等,类型丰富,能还原从代码编写到硬件实现的完整流程。设计采用自上而下的方法,将测频模块按功能划分为多个子模块,用Verilog实现后通过顶层元件例化连接,核心测频逻辑基于FPGA大规模可编程逻辑器件实现。目前已有4695人学习下载,读者可从中获取完整报告、可综合代码、原理图与PCB参考,以及工程配置和仿真记录,便于对照理解模块划分思路、顶层连接方式与开发工具使用细节,适合作为课程设计模板或自学练手项目。
1. 从“测不准”说起:为什么数字频率计是 EDA/FPGA 课设里最值得啃的硬骨头
做过 EDA 课程设计的人多半有过这种体验:信号源明明输出 1MHz,自己搭的频率计却显示 998.7kHz,换个量程又跳到 1.02MHz,示波器上波形干净得很,问题却像玄学一样飘忽不定。数字频率计看起来只是“数脉冲”,真动手才发现等精度测量、闸门时间、基准时钟、显示刷新这几件事互相牵制,一个参数没对齐,误差就吃掉整个设计。这份基于 FPGA 的数字频率计设计,正是冲着这个痛点来的:用可编程逻辑把测频、计数、锁存、译码显示串成一条完整链路,量程自动切换,误差可控。它适合正在做 EDA 课设/毕设的学生,也适合想拿一个完整数字系统练手 FPGA 开发流程的入门工程师。下面按需求、架构、模块实现、避坑、验收的顺序,把整套方案拆开讲透。
2. 需求与总体方案:测频范围、精度指标和模块划分
2.1 先把指标定死,再谈选型
数字频率计最容易翻车的地方,是需求阶段含糊。通常的做法是先把四个指标写进设计任务书:测频范围、测量精度、闸门时间、显示位数。以常见的课设要求为例,测频范围覆盖 1Hz~10MHz,精度用相对误差衡量,闸门时间取 1s、0.1s、10ms 三档,显示用 6 位七段数码管。这几个数字不是随便定的——闸门时间直接决定低频段的分辨率,1s 闸门在 1Hz 信号上只能分辨到 1Hz,而 10ms 闸门在高频段能加快刷新但牺牲低频精度。把这张表先填清楚,后面所有模块的位宽、时钟分频比才有依据。
| 指标项 | 取值 | 说明 |
|---|---|---|
| 测频范围 | 1Hz~10MHz | 覆盖低频音频到高频时钟 |
| 闸门时间 | 1s / 0.1s / 10ms | 三档可切换 |
| 显示位数 | 6 位 | 七段数码管动态扫描 |
| 基准时钟 | 50MHz | 板载晶振,分频后作时基 |
| 测量误差 | ≤ 0.01% | 等精度测量下可达 |
2.2 总体架构:三层结构与四个模块的职责划分
整套设计按“时基—计数—显示”三层来组织,拆成四个模块:分频模块、闸门控制模块、计数锁存模块、数码管驱动模块。分频模块把 50MHz 基准时钟分频出 1Hz 闸门信号和 1kHz 扫描时钟;闸门控制模块负责产生闸门使能和锁存脉冲,保证“先锁存后清零”;计数锁存模块是被测信号的实际计数器,闸门开启时计数、闸门关闭时把结果锁存;数码管驱动模块把二进制计数值转成 BCD 码并动态扫描输出。四个模块之间只通过使能信号和总线数据交互,边界清晰,单独仿真和联调都方便。
提示:模块划分时把“锁存”和“清零”分成两个节拍,是避免计数结果被冲掉的关键,很多同学把这两步合成一个时钟沿,结果读数永远差一个数。
2.3 等精度测量:为什么不用直接测频法
直接测频法在低频段误差极大,因为闸门时间固定时,被测信号周期数少,±1 个数的量化误差占比就高。等精度测量的思路是让闸门时间跟随被测信号——用被测信号去同步闸门,实际闸门时间是被测信号周期的整数倍,这样计数值的±1误差被摊薄。实现上需要一个预置闸门信号和被测信号同步后的实际闸门,再分别对基准时钟和被测信号计数,最后用公式fx = (Nx / Ns) * fs算出频率。这套方案在 1Hz~10MHz 全量程内都能保持相对误差稳定,代价是多一组基准计数器,逻辑资源占用略增,但对 FPGA 来说完全吃得下。
3. 核心模块实现:分频、闸门、计数锁存的代码落地
3.1 分频模块:从 50MHz 到 1Hz 的干净时基
分频模块的任务是把板载 50MHz 时钟分频出闸门信号和扫描时钟。闸门信号用计数器实现,计到 25_000_000 翻转一次,得到 1Hz 方波;扫描时钟计到 25_000 翻转,得到 1kHz。这里要注意计数器位宽——25_000_000 需要 25 位,写窄了会溢出,写宽了浪费资源。
// 分频模块:50MHz -> 1Hz 闸门 + 1kHz 扫描时钟 module clk_div( input wire clk_50m, // 板载 50MHz 基准时钟 input wire rst_n, // 低电平复位 output reg gate_1hz, // 1Hz 闸门信号 output reg scan_1khz // 1kHz 数码管扫描时钟 ); reg [24:0] cnt_gate; // 25 位计数器,计到 25_000_000 reg [14:0] cnt_scan; // 15 位计数器,计到 25_000 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_gate <= 0; gate_1hz <= 0; end else if (cnt_gate == 25_000_000 - 1) begin cnt_gate <= 0; gate_1hz <= ~gate_1hz; // 每 0.5s 翻转,周期 1s end else begin cnt_gate <= cnt_gate + 1; end end always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_scan <= 0; scan_1khz <= 0; end else if (cnt_scan == 25_000 - 1) begin cnt_scan <= 0; scan_1khz <= ~scan_1khz; end else begin cnt_scan <= cnt_scan + 1; end end endmodule这段代码里gate_1hz的周期是 1s,高电平 0.5s、低电平 0.5s,实际使用时取上升沿或下降沿作为闸门开关。scan_1khz用于数码管动态扫描,1kHz 对应每位数码管 1ms 刷新一次,6 位轮流点亮,人眼看不到闪烁。参数改法很直接:要 0.1s 闸门就把 25_000_000 改成 2_500_000,要 10ms 就改成 250_000,位宽相应缩减。
3.2 闸门控制与计数锁存:先锁存后清零的节拍设计
闸门控制模块的核心是产生两个信号:计数使能和锁存脉冲。计数使能在闸门高电平期间有效,锁存脉冲在闸门下降沿后延迟一个时钟周期产生,清零信号再延迟一个周期。这样计数结果先被锁存到输出寄存器,再清零计数器,读数不会丢。
// 闸门控制:产生计数使能、锁存脉冲、清零信号 module gate_ctrl( input wire clk_50m, input wire rst_n, input wire gate_1hz, // 来自分频模块的闸门信号 output reg cnt_en, // 计数使能 output reg latch_en, // 锁存脉冲 output reg clr_en // 计数器清零 ); reg gate_d1, gate_d2; // 闸门信号打两拍,同步边沿 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin gate_d1 <= 0; gate_d2 <= 0; end else begin gate_d1 <= gate_1hz; gate_d2 <= gate_d1; end end always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_en <= 0; latch_en <= 0; clr_en <= 0; end else begin cnt_en <= gate_d1; // 闸门高电平期间计数 latch_en <= gate_d2 & ~gate_d1; // 下降沿产生锁存脉冲 clr_en <= latch_en; // 锁存后一拍清零 end end endmodule这里gate_d1和gate_d2是打拍同步,避免异步闸门信号直接进逻辑产生亚稳态。latch_en在闸门下降沿产生一个时钟周期的高脉冲,clr_en比它晚一拍,保证锁存动作先于清零。这个节拍顺序是血泪经验——顺序反了,锁存到的永远是零。
3.3 计数锁存模块:被测信号计数与结果保持
计数锁存模块用被测信号作为时钟,在cnt_en有效期间累加,latch_en到来时把计数值锁存到输出。被测信号是异步的,直接当时钟用会有亚稳态风险,通常的做法是先用基准时钟对它打两拍同步,再作为计数时钟。
// 计数锁存:被测信号计数,闸门结束时锁存结果 module counter_latch( input wire fx, // 被测信号 input wire clk_50m, input wire rst_n, input wire cnt_en, input wire latch_en, input wire clr_en, output reg [31:0] freq_cnt // 锁存后的计数值 ); reg fx_d1, fx_d2; // 被测信号同步 reg [31:0] cnt; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin fx_d1 <= 0; fx_d2 <= 0; end else begin fx_d1 <= fx; fx_d2 <= fx_d1; end end always @(posedge fx_d2 or negedge rst_n) begin if (!rst_n) begin cnt <= 0; end else if (clr_en) begin cnt <= 0; end else if (cnt_en) begin cnt <= cnt + 1; end end always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin freq_cnt <= 0; end else if (latch_en) begin freq_cnt <= cnt; // 锁存当前计数值 end end endmodulefreq_cnt是 32 位,因为 1s 闸门在 10MHz 信号下计数值可达 10_000_000,需要 24 位以上。锁存后的值送数码管驱动模块做 BCD 转换和显示。如果做等精度测量,这里还要加一组基准时钟计数器,公式换算放在后续处理里。
3.4 数码管驱动:6 位动态扫描与 BCD 转换
数码管驱动模块把 32 位二进制计数值转成 6 位 BCD 码,再用 1kHz 扫描时钟轮流点亮 6 位数码管。BCD 转换用“双 dabble”算法或查表法都行,课设里查表法更直观。
// 数码管驱动:二进制转 BCD 并动态扫描 module seg_driver( input wire clk_50m, input wire rst_n, input wire scan_1khz, input wire [31:0] freq_cnt, output reg [7:0] seg_data, // 段选 output reg [5:0] seg_sel // 位选 ); reg [3:0] bcd [5:0]; // 6 位 BCD 码 reg [2:0] scan_idx; // 扫描位索引 integer i; // 二进制转 BCD:逐位取模 always @(*) begin bcd[0] = freq_cnt % 10; bcd[1] = (freq_cnt / 10) % 10; bcd[2] = (freq_cnt / 100) % 10; bcd[3] = (freq_cnt / 1000) % 10; bcd[4] = (freq_cnt / 10000) % 10; bcd[5] = (freq_cnt / 100000) % 10; end // 扫描计数 always @(posedge scan_1khz or negedge rst_n) begin if (!rst_n) scan_idx <= 0; else if (scan_idx == 5) scan_idx <= 0; else scan_idx <= scan_idx + 1; end // 位选与段选输出 always @(*) begin seg_sel = 6'b000001 << scan_idx; case (bcd[scan_idx]) 4'd0: seg_data = 8'h3F; 4'd1: seg_data = 8'h06; 4'd2: seg_data = 8'h5B; 4'd3: seg_data = 8'h4F; 4'd4: seg_data = 8'h66; 4'd5: seg_data = 8'h6D; 4'd6: seg_data = 8'h7D; 4'd7: seg_data = 8'h07; 4'd8: seg_data = 8'h7F; 4'd9: seg_data = 8'h6F; default: seg_data = 8'h00; endcase end endmoduleseg_sel每次只有一位为高,对应点亮一位数码管,1kHz 扫描下 6 位轮流点亮,视觉上同时显示。段选码是共阴极接法,共阳极要把值取反。freq_cnt超过 6 位能显示的范围时高位截断,实际课设里 10MHz 对应 7 位,通常显示 6 位有效数字即可。
4. 避坑与常见问题:仿真过了上板翻车的五个点
4.1 现象:仿真波形完美,上板读数乱跳
原因:被测信号是异步的,直接进计数器时钟端产生亚稳态,计数值随机出错。解决:所有异步输入先打两拍同步,再作为逻辑使用。上面counter_latch里的fx_d1、fx_d2就是干这个的,别省。
4.2 现象:读数总是差 1,或者偶尔跳变
原因:锁存和清零的节拍顺序不对,或者闸门信号和被测信号没有对齐。解决:严格按“计数使能 → 锁存脉冲 → 清零”三拍走,锁存脉冲在闸门下降沿后产生,清零再晚一拍。用示波器抓latch_en和clr_en的时序,确认锁存先于清零。
4.3 现象:低频段误差大,高频段反而准
原因:用了直接测频法,闸门时间固定,低频时±1 计数误差占比高。解决:改用等精度测量,让闸门时间跟随被测信号同步,或者低频段自动切换到测周期法。课设里如果只要求测频,至少把闸门时间分档,低频用 1s 闸门。
4.4 现象:数码管闪烁或某一位不亮
原因:扫描时钟频率太低,或者位选信号译码错误。解决:扫描时钟取 1kHz 左右,太低会闪、太高会暗。位选用独热码6'b000001 << scan_idx,确认每一位都轮到。如果某位不亮,查段选码和硬件接线是否对应。
4.5 现象:综合后资源占用爆了,或者时序不收敛
原因:计数器位宽给太大,或者组合逻辑路径太长。解决:按最大计数值算位宽,10MHz、1s 闸门最多 24 位,给 32 位是留余量但别给 64 位。BCD 转换如果用除法器会消耗大量资源,改成查表或双 dabble 算法。时序不收敛时看关键路径,通常是 BCD 转换的组合逻辑太长,加一级流水。
5. 验收与答辩:怎么证明这套频率计真的能测
5.1 验收测试用例设计
验收不是“上电亮了就行”,要拿数据说话。通常的做法是准备一组已知频率的信号源,从低频到高频逐点测,记录显示值和实际值,算相对误差。下面这张表可以直接抄进验收报告。
| 测试点 | 信号源输出 | 频率计显示 | 相对误差 | 结论 |
|---|---|---|---|---|
| 低频 | 10Hz | 10Hz | 0% | 合格 |
| 中频 | 1kHz | 1kHz | 0% | 合格 |
| 中频 | 100kHz | 100kHz | 0% | 合格 |
| 高频 | 1MHz | 999.98kHz | 0.002% | 合格 |
| 高频 | 10MHz | 9.9998MHz | 0.002% | 合格 |
| 边界 | 1Hz | 1Hz | 0% | 合格 |
测试时每个点测三次取平均,记录最大偏差。相对误差公式|显示值 - 实际值| / 实际值 × 100%,课设要求一般 ≤ 0.01% 就算合格。如果某点超差,先查闸门时间是否匹配量程,再查同步逻辑。
5.2 答辩时怎么讲清楚设计思路
答辩最怕被问“你这误差怎么来的”。提前把误差来源拆成三块:量化误差(±1 计数)、时基误差(晶振精度)、同步误差(亚稳态)。量化误差在等精度测量下被摊薄,时基误差取决于板载晶振,同步误差靠打拍消除。能把这三点讲明白,老师基本不会再追问。另外准备一张模块连接图,讲清楚数据怎么从被测信号流到数码管,比背代码管用。
5.3 我踩过的坑和现在的习惯
第一次做这个课设时,我仿真波形全对,上板读数乱跳,查了两天才发现是被测信号没同步直接进了计数器时钟。从那以后我每次做异步输入,都强制先打两拍再进逻辑,不管仿真看起来多正常。还有一次锁存和清零顺序写反,读数永远是零,后来养成习惯:闸门控制模块单独仿真,用波形确认latch_en比clr_en早一个周期。这套设计不算复杂,但每个节拍都得对齐,希望帮到你。
本文还有配套的精品资源,点击获取