ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于FPGA的数字频率计设计:从Quartus工程到板级验证的完整实战

基于FPGA的数字频率计设计:从Quartus工程到板级验证的完整实战 简介这份资源是本科EDA课程期末大作业的完整提交包围绕基于FPGA的数字频率计设计展开适合电子信息、集成电路及相关专业学生参考课程设计、实验报告撰写与Quartus II开发入门。设计采用自上而下的EDA方法用Verilog将测频模块拆分为多个子模块再通过顶层元件例化完成整体连接配套完整报告、代码、PCB与原理图便于理解从功能划分到综合实现的完整流程。压缩包共744个文件约28.3MB涵盖Verilog源码与VHD文件、Quartus工程配置qpf、qsf、sof、综合与仿真报告rpt、summary、vho、sdo、原理图与PCB文档schdoc、pcbdoc以及大量中间数据库文件完整保留了工程编译与调试痕迹。目前已有4695人学习下载可帮助读者对照报告复现测频逻辑、梳理模块划分思路并借鉴工程目录组织与排错经验。1. 从一份 EDA 课设压缩包说起FPGA 数字频率计到底能测什么如果你正在做 EDA 课程设计或者刚接触 FPGA 想找一个能跑通完整链路的练手项目这份「基于 FPGA 的数字频率计设计」压缩包大概率能省掉你不少翻论坛的时间。它解决的是一个很具体的问题用 FPGA 测量外部输入信号的频率并把结果稳定地显示出来。听起来简单但真正动手时你会发现从分频、计数、锁存到数码管动态扫描每一步都有坑。这个项目适合两类人一类是课程设计需要交作业、想快速理解完整工程结构的同学另一类是想通过一个「麻雀虽小五脏俱全」的案例把 Verilog 时序逻辑、Quartus 工程管理、Testbench 仿真串起来的自学者。频率计本身不复杂但它把 FPGA 开发中最核心的几个概念——时钟域、边沿检测、计数器位宽、显示驱动——全揉在了一起拿来当入门模板刚刚好。2. 项目架构拆解测频原理与模块划分2.1 数字频率计的核心逻辑闸门时间与计数数字频率计的本质是在一个已知宽度的闸门时间内统计被测信号的脉冲个数。假设闸门时间为 T计数值为 N则频率 f N / T。FPGA 实现时通常用一个 1Hz 或 0.5Hz 的使能信号作为闸门控制一个计数器对被测信号边沿进行累加。这里的关键参数是闸门时间的精度和计数器的位宽。闸门时间由系统时钟分频得到比如 50MHz 时钟分频到 1Hz需要计数 25,000,000 次这要求分频计数器至少 25 位宽。被测信号频率越高单位闸门时间内计数值越大计数器位宽也要相应增加。常见做法是闸门时间取 1 秒这样计数值直接就是频率值省去后续除法运算对初学者最友好。2.2 模块划分从顶层到子模块的职责边界一个可综合的频率计工程通常包含以下几个模块顶层模块例化所有子模块连接时钟、复位、被测信号输入和数码管输出。分频模块将板载高频时钟分频为 1Hz 闸门信号和数码管扫描所需的低频时钟。测频控制模块产生闸门信号、锁存信号和计数器清零信号协调整个测量时序。计数器模块在闸门有效期间对被测信号边沿计数。锁存器模块在闸门结束时锁存计数值保证显示稳定。数码管驱动模块将锁存后的二进制频率值转换为段选和位选信号动态扫描显示。这种划分方式的好处是每个模块职责单一方便单独仿真和调试。我一般会先写测频控制模块因为它是整个系统时序的「大脑」闸门、锁存、清零三个信号的先后顺序一旦搞错显示就会跳变或闪烁。2.3 目录结构压缩包里的工程长什么样解压后典型的 Quartus 工程目录大致如下freq_meter/ ├── db/ # Quartus 编译生成的数据文件 ├── incremental_db/ # 增量编译数据库 ├── output_files/ # 编译输出含 .sof 和 .pof 下载文件 ├── freq_meter.qpf # Quartus 工程文件 ├── freq_meter.qsf # 工程设置文件引脚分配在这里 ├── top.v # 顶层模块 ├── divider.v # 分频模块 ├── measure_ctrl.v # 测频控制模块 ├── counter.v # 计数器模块 ├── latch.v # 锁存器模块 ├── seg_driver.v # 数码管驱动模块 └── tb_freq_meter.v # Testbench 仿真文件其中.qsf文件里的引脚分配是新手最容易翻车的地方。不同开发板的引脚定义不同直接拿别人的工程下载很可能数码管不亮或者被测信号输入引脚接错。常见做法是打开 Assignment - Pin Planner对照自己板子的原理图逐个核对时钟、复位、输入和数码管引脚。3. 跑通工程从 Quartus 编译到板级验证3.1 环境准备与工程打开这个项目基于 Quartus 开发具体版本取决于压缩包内的工程文件格式。如果.qpf文件能直接打开说明版本兼容如果提示版本不匹配可以用文本编辑器打开.qpf把版本号改成你本地安装的版本再重新打开。常见做法是安装 Quartus Prime Lite 版它免费且支持大多数 Cyclone 系列 FPGA。打开工程后先别急着编译检查两件事一是顶层模块是否设置为top二是.qsf里的器件型号是否和你板子上的 FPGA 型号一致。器件选错编译能过但下载会报错。3.2 引脚分配与编译下载引脚分配是硬件验证的第一道坎。以常见的开发板为例系统时钟通常接在专用的 CLK 引脚上复位按键接在某个 GPIO被测信号从扩展口输入数码管位选和段选分别接到对应的 IO。下面是一个引脚分配的示例片段# freq_meter.qsf 引脚分配示例 set_location_assignment PIN_23 -to clk # 50MHz 系统时钟 set_location_assignment PIN_25 -to rst_n # 复位按键低有效 set_location_assignment PIN_28 -to sig_in # 被测信号输入 set_location_assignment PIN_100 -to seg[0] # 数码管段选 a set_location_assignment PIN_101 -to seg[1] # 数码管段选 b set_location_assignment PIN_102 -to seg[2] # 数码管段选 c set_location_assignment PIN_103 -to seg[3] # 数码管段选 d set_location_assignment PIN_104 -to seg[4] # 数码管段选 e set_location_assignment PIN_105 -to seg[5] # 数码管段选 f set_location_assignment PIN_106 -to seg[6] # 数码管段选 g set_location_assignment PIN_107 -to seg[7] # 数码管段选 dp set_location_assignment PIN_110 -to sel[0] # 数码管位选 1 set_location_assignment PIN_111 -to sel[1] # 数码管位选 2 set_location_assignment PIN_112 -to sel[2] # 数码管位选 3 set_location_assignment PIN_113 -to sel[3] # 数码管位选 4这段分配里clk是 50MHz 有源晶振输入rst_n是低电平复位sig_in是被测信号输入引脚。段选和位选的具体引脚号必须对照你的板子原理图修改不能照抄。分配完成后点击 Processing - Start Compilation等待编译完成。如果报错先看 Messages 窗口的 Error 信息常见的是引脚冲突或器件不支持某条约束。编译通过后用 USB-Blaster 下载线连接板子打开 Programmer选择output_files目录下的.sof文件点击 Start。下载成功后给sig_in接入一个已知频率的信号源比如函数发生器输出的 1kHz 方波观察数码管显示是否接近 1000。3.3 仿真验证先跑 Testbench 再上板上板之前强烈建议先跑一遍 Testbench 仿真。压缩包里的tb_freq_meter.v通常会产生一个模拟的被测信号并观察计数器和锁存器的输出。下面是一个简化的 Testbench 结构// tb_freq_meter.v 仿真激励示例 timescale 1ns/1ps module tb_freq_meter; reg clk 0; reg rst_n 0; reg sig_in 0; wire [31:0] freq_val; wire [7:0] seg; wire [3:0] sel; // 50MHz 时钟周期 20ns always #10 clk ~clk; // 模拟被测信号周期 1ms即 1kHz always #500000 sig_in ~sig_in; initial begin rst_n 0; #200 rst_n 1; // 释放复位 #2000000000 $stop; // 仿真 2ms 后停止 end // 例化顶层模块 freq_meter_top u_top ( .clk(clk), .rst_n(rst_n), .sig_in(sig_in), .seg(seg), .sel(sel) ); endmodule这段 Testbench 里clk周期 20ns 对应 50MHzsig_in周期 1ms 对应 1kHz。仿真跑 2ms 后停止理论上闸门时间 1 秒还没到所以看不到完整的锁存值。实际仿真时为了快速验证可以把分频模块的闸门时间参数改小比如改成 1ms这样仿真几毫秒就能看到计数结果。参数化设计的好处就在这里——改一个parameter就能加速仿真不用改逻辑。注意仿真时如果计数器一直不变化先检查闸门信号是否正常产生再看被测信号是否真的在翻转。很多时候是 Testbench 里的always块写错了时间单位。4. 核心代码逐段拆解分频、计数与数码管驱动4.1 分频模块从 50MHz 到 1Hz 闸门分频模块的任务是把系统时钟降到闸门信号所需的频率。下面是一个参数化的分频器实现// divider.v 参数化分频器 module divider #( parameter CNT_MAX 25_000_000 // 50MHz 分频到 1Hz )( input wire clk, input wire rst_n, output reg clk_out ); reg [24:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 25d0; clk_out 1b0; end else if (cnt CNT_MAX - 1) begin cnt 25d0; clk_out ~clk_out; // 翻转输出得到 1Hz 方波 end else begin cnt cnt 1b1; end end endmoduleCNT_MAX决定了输出频率。50MHz 时钟下计数 25,000,000 次后翻转输出周期 1 秒即 1Hz。如果板载时钟是 100MHz这个参数要改成 50,000,000。cnt的位宽必须足够容纳CNT_MAX - 125 位可以表示到 33,554,431够用。如果位宽不够计数器会提前溢出输出频率就错了。我一般会在仿真时把CNT_MAX改小到 100这样几微秒就能看到翻转省时间。4.2 测频控制模块闸门、锁存、清零的时序配合测频控制模块是整个设计里时序最讲究的部分。它需要产生三个信号闸门使能gate_en、锁存使能latch_en和计数器清零cnt_clr。三者的顺序是先清零计数器再打开闸门闸门结束后锁存数据然后再次清零循环往复。// measure_ctrl.v 测频时序控制 module measure_ctrl ( input wire clk, // 1Hz 闸门时钟 input wire rst_n, output reg gate_en, // 闸门使能高有效 output reg latch_en, // 锁存使能上升沿锁存 output reg cnt_clr // 计数器清零高有效 ); reg [1:0] state; localparam IDLE 2d0, GATE 2d1, LATCH 2d2, CLEAR 2d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; gate_en 1b0; latch_en 1b0; cnt_clr 1b1; end else begin case (state) IDLE: begin cnt_clr 1b1; gate_en 1b0; latch_en 1b0; state GATE; end GATE: begin cnt_clr 1b0; gate_en 1b1; latch_en 1b0; state LATCH; end LATCH: begin gate_en 1b0; latch_en 1b1; state CLEAR; end CLEAR: begin latch_en 1b0; cnt_clr 1b1; state GATE; end default: state IDLE; endcase end end endmodule这个状态机用 1Hz 时钟驱动每个状态持续 1 秒。GATE状态下gate_en拉高计数器开始对被测信号计数LATCH状态下latch_en拉高把计数值锁存到显示寄存器CLEAR状态下cnt_clr拉高计数器归零为下一轮测量做准备。这里有个细节latch_en是电平信号锁存器模块应该在latch_en的上升沿或高电平期间更新输出。如果锁存器对latch_en的边沿不敏感显示值可能会在闸门期间跟着计数器变化导致数码管跳数。4.3 计数器与锁存器位宽选择和边沿检测计数器模块对被测信号的上升沿进行计数。这里需要一个边沿检测逻辑把被测信号的上升沿转换成单周期脉冲// counter.v 带边沿检测的计数器 module counter ( input wire clk, // 系统时钟用于同步 input wire rst_n, input wire gate_en, // 闸门使能 input wire cnt_clr, // 清零 input wire sig_in, // 被测信号 output reg [31:0] cnt // 计数值 ); reg sig_d0, sig_d1; wire sig_posedge; // 两级寄存器同步被测信号检测上升沿 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sig_d0 1b0; sig_d1 1b0; end else begin sig_d0 sig_in; sig_d1 sig_d0; end end assign sig_posedge sig_d0 ~sig_d1; // 上升沿脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 32d0; end else if (cnt_clr) begin cnt 32d0; end else if (gate_en sig_posedge) begin cnt cnt 1b1; end end endmodulesig_d0和sig_d1构成两级同步寄存器把异步的被测信号同步到系统时钟域同时sig_d0 ~sig_d1产生一个单周期脉冲代表上升沿。计数器位宽取 32 位足够测量到几十 MHz 的频率。如果被测信号频率接近系统时钟的一半边沿检测可能会漏掉脉冲这是奈奎斯特采样定理的限制不是代码问题。锁存器模块在latch_en有效时把cnt的值复制到输出寄存器// latch.v 数据锁存器 module latch ( input wire clk, input wire rst_n, input wire latch_en, input wire [31:0] cnt_in, output reg [31:0] freq_out ); always (posedge clk or negedge rst_n) begin if (!rst_n) freq_out 32d0; else if (latch_en) freq_out cnt_in; end endmodule这里用latch_en作为条件在系统时钟上升沿采样。如果latch_en持续多个时钟周期freq_out会一直跟随cnt_in所以latch_en最好只保持一个时钟周期。但在 1Hz 时钟域下一个周期就是 1 秒所以实际工程中常用边沿检测来缩短锁存窗口。4.4 数码管动态扫描段选与位选的配合数码管驱动模块负责把 32 位频率值拆成十进制数字并动态扫描显示。下面是一个简化的实现// seg_driver.v 数码管动态扫描 module seg_driver ( input wire clk, // 扫描时钟约 1kHz input wire rst_n, input wire [31:0] freq_val, output reg [7:0] seg, // 段选低有效 output reg [3:0] sel // 位选低有效 ); reg [16:0] scan_cnt; reg [1:0] digit_sel; reg [3:0] bcd_digit; reg [3:0] bcd [3:0]; // 4 位十进制数字 // 扫描计数器产生位选切换信号 always (posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt 17d0; else scan_cnt scan_cnt 1b1; end always (posedge clk or negedge rst_n) begin if (!rst_n) digit_sel 2d0; else if (scan_cnt 17d99_999) digit_sel digit_sel 1b1; end // 将频率值拆分为 BCD 码简化版仅取低四位 always (*) begin bcd[0] freq_val % 10; bcd[1] (freq_val / 10) % 10; bcd[2] (freq_val / 100) % 10; bcd[3] (freq_val / 1000) % 10; end // 位选和段选输出 always (*) begin case (digit_sel) 2d0: begin sel 4b1110; bcd_digit bcd[0]; end 2d1: begin sel 4b1101; bcd_digit bcd[1]; end 2d2: begin sel 4b1011; bcd_digit bcd[2]; end 2d3: begin sel 4b0111; bcd_digit bcd[3]; end default: begin sel 4b1111; bcd_digit 4d0; end endcase end // 段选译码共阳极数码管低有效 always (*) begin case (bcd_digit) 4d0: seg 8b1100_0000; 4d1: seg 8b1111_1001; 4d2: seg 8b1010_0100; 4d3: seg 8b1011_0000; 4d4: seg 8b1001_1001; 4d5: seg 8b1001_0010; 4d6: seg 8b1000_0010; 4d7: seg 8b1111_1000; 4d8: seg 8b1000_0000; 4d9: seg 8b1001_0000; default: seg 8b1111_1111; endcase end endmodulescan_cnt每计数到 99,999 切换一次位选在 50MHz 时钟下大约 2ms 切换一次人眼看起来是同时亮的。bcd数组存储各位十进制数字digit_sel循环选择。段选译码表对应共阳极数码管低电平点亮。如果你的板子是共阴极段选值要取反。这个模块的坑在于除法器和取模运算会消耗较多逻辑资源如果频率值位宽很大综合器可能会报资源不足。常见做法是用双 dabble 算法把二进制转 BCD或者限制显示位数只显示低四位。5. 避坑与排查上板不亮、跳数、编译报错怎么办5.1 数码管不亮或只亮一位现象下载后数码管全灭或者只有一位亮且不切换。原因位选信号没有正确扫描或者段选译码表与数码管类型不匹配。共阳极和共阴极的段选值正好相反接错就全灭。解决先用万用表测数码管公共端是接高还是接低确认类型再检查scan_cnt的比较值是否太小导致扫描频率过高人眼看不到。把扫描时钟降到 1kHz 左右位选切换间隔约 1ms显示最稳定。5.2 频率显示跳数或归零现象数码管显示的数字一直在跳或者每隔一秒归零一次。原因锁存器的锁存时机不对或者计数器清零和锁存同时发生。解决检查测频控制模块的状态机确保latch_en在cnt_clr之前有效且两者不同时拉高。如果锁存器对latch_en的电平敏感改成边沿敏感只在上升沿锁存一次。另外闸门信号和锁存信号如果跨时钟域要加同步器。5.3 编译报错引脚冲突或资源不足现象Quartus 编译报 Error提示引脚重复分配或逻辑单元不够。原因.qsf文件里同一个引脚被分配给多个信号或者除法器、乘法器消耗了太多 LE。解决打开 Pin Planner 查看冲突引脚删除重复分配把除法运算改成移位和查表或者用专用 DSP 块。如果资源实在不够降低显示位数只显示 4 位频率值。5.4 被测信号无输入时显示乱码现象不接信号源时数码管显示随机数字。原因输入引脚悬空FPGA 输入缓冲器拾取噪声计数器乱计数。解决在sig_in引脚上使能内部上拉或下拉电阻或者在代码里加一个输入滤波连续采样多次才认为有效边沿。常见做法是在.qsf里加set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to sig_in。5.5 仿真通过但上板结果不对现象Testbench 波形完全正确下载到板子后频率值偏差很大。原因板载时钟频率和代码里假设的不一致比如代码按 50MHz 分频实际板子是 100MHz闸门时间变成 0.5 秒计数值翻倍。解决确认板子晶振频率修改分频模块的CNT_MAX参数。如果不确定可以用一个已知频率的信号源校准比如输入 1kHz看显示是 1000 还是 2000反推实际时钟频率。6. 二次开发把频率计改成周期测量与占空比显示这个工程跑通之后最自然的改造方向是扩展测量功能。频率计只测频率但同样的硬件可以测周期和占空比。周期是频率的倒数但直接做除法会消耗资源更常见的做法是反过来用被测信号作为闸门统计系统时钟个数。具体来说把sig_in当作闸门信号在它的高电平期间对 50MHz 时钟计数计数值乘以 20ns 就是周期。这样测低频信号时精度更高因为系统时钟频率远高于被测信号。占空比测量则需要分别统计高电平时间和总周期时间。可以在被测信号的上升沿清零一个高电平计数器下降沿锁存高电平计数值同时另一个计数器统计完整周期。两者相除得到占空比。除法可以用移位近似或者把结果送到数码管显示百分比。改造时我一般会保留原来的测频通道增加一个模式选择开关。模式 0 显示频率模式 1 显示周期模式 2 显示占空比。这样一份工程能覆盖更多课设题目要求。下面是一个模式选择的代码框架// mode_sel.v 测量模式选择 module mode_sel ( input wire [1:0] mode, // 模式选择 input wire [31:0] freq_val, // 频率值 input wire [31:0] period_val, // 周期值 input wire [31:0] duty_val, // 占空比 output reg [31:0] display_val ); always (*) begin case (mode) 2d0: display_val freq_val; 2d1: display_val period_val; 2d2: display_val duty_val; default: display_val freq_val; endcase end endmodule这个模块把三个测量值映射到一个显示通道数码管驱动模块不用改只需要在顶层根据模式选择输出。验证时先用信号发生器输出一个已知频率和占空比的方波比如 1kHz、50% 占空比看三个模式下的显示是否分别为 1000、1000000单位 ns和 50。如果周期显示不对检查系统时钟频率是否准确如果占空比偏差大检查高电平计数器的清零和锁存时机。从那以后我每次做 FPGA 课设都会先把仿真跑通再上板而且一定会用已知信号源校准一遍。这个频率计工程虽然简单但它把时序控制、跨时钟域、显示驱动这些核心问题都串了一遍改完周期和占空比测量基本就能应付大多数 EDA 期末大作业的要求了。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表