简介:针对FPGA课程设计和数字频率计实验,这份压缩包提供完整的Verilog实现方案与设计报告。文档为单个doc文件,约412KB,系统讲解从原理框图到分频器、门控电路、计数器、锁存译码显示等模块的代码与仿真,并给出ISE综合实现和ELB实验板实测过程。频率测量范围为10Hz~10MHz,六位数码管以kHz显示,支持1s、0.1s、0.01s闸门选择,带有溢出指示和复位功能。报告同时梳理了Verilog HDL语言特点、技术指标、误差分析与实验总结,能够帮助读者理解测频原理、掌握模块化设计方法并快速产出课设报告。资源已有705人学习下载,适合电子类本科生、FPGA入门者以及需要参考完整代码与书写思路的读者。
1. 数字频率计:一份能直接跑到实验板上的缺口资源
在 FPGA 课程设计里,数字频率计是最经典也最容易被低估的一道题。外行看是「数脉冲个数」,真正落过板的人知道,难点全在闸门时基的准确性、BCD 级联计数器的进位时序、以及六位数码管的动态扫描和小数点联动上。这份资源把 Verilog 代码、仿真波形、设计报告三样都备齐了,覆盖 10Hz 到 10MHz、三档闸门时间、溢出指示和记忆显示,用的是 SPARTAN-3A 系列芯片配合 ISE 工具链。适合两类人:一是要做课程设计的学生,代码和报告结构能直接参照;二是想把「测频底层逻辑」彻底吃透的 FPGA 初学者。下文按我从模块到顶层、从代码到实测的顺序拆,最后也会把最容易翻车的六个坑单独拎出来讲。
2. 先看懂架构再动手:测频原理、模块边界和信号流的四路走向
2.1 测频原理:闸门计数才是核心
数字频率计的本质是「在已知准确时间 T 内,数被测信号经过了多少个完整周期」。被测信号 fx 经过放大整形变成同频方波,送入计数器的时钟端;闸门开启期间,方波脉冲才能进入计数器,计得 N 个;那么 fx 就等于 N 除以闸门时间 T。这份资源里用的就是最直接的测频法,没有周期法、多周期同步法这些进阶方案,属于教学设计的标准路线。
这里有个容易忽略的量化误差问题:闸门开启时刻与输入脉冲上升沿之间没有任何相位关系,所以即便输入信号频率完全不变,相邻两次测量也可能差一个脉冲。这个误差在低频段最明显——比如 10Hz 信号,1 秒闸门只能数到 10 个脉冲,±1 就是 ±10%;而在 10MHz 高频段,同样 ±1 误差只占千万分之一。这份设计报告里实测数据有 899.995 kHz、919.994 kHz 这类读数,来源就是这个 ±1 量化误差,不是代码逻辑错了。理解了这一点,后面看仿真波形和实测表格时,心态会稳很多。
2.2 模块信号流:五级链路怎么配合
整个系统分成五部分:分频器把板载时钟分出一组时基信号;门控电路根据按键选择某个时基作为闸门;计数器在闸门开启时对被测信号计数;锁存器在闸门关闭后把计数值固定;译码扫描驱动六位数码管显示。顶层模块实例化时,信号按下面这条链走:
板载时钟 → 分频器 → 多路时基 → 门控电路 → gate_out1 → 计数器 clk 使能 被测信号 → 整形 → 计数器 clk_in 门控电路 → clk_latch → 锁存器使能 门控电路 → clk_clear → 计数器清零 计数器 → 6 组 BCD → 锁存器 → 扫描输出 → 7 段码 + 位选其中最容易搞混的是gate_out1和gate。gate是选通后的时基信号,它决定闸门什么时候开、开多久;gate_out1是门控电路输出给计数器的使能信号,它等于「闸门开」的电平标识。计数器在gate_out1为高时对输入脉冲计数,同时门控电路内部用一个 0 到 13 的计数器控制开闸、锁存、清零三个动作的先后。
2.3 六项硬指标和它们在代码里的落点
设计任务书里有六条硬性指标,每一条都能在源码里找到对应实现位:
| 指标 | 具体要求 | 代码落点 |
|---|---|---|
| 位数 | 六位十进制显示 | cnt1~cnt6六组 BCD 计数器 |
| 量程 | 10Hz ~ 10MHz | 3 档闸门时间覆盖 |
| 溢出指示 | 计数器满时给出溢出标志 | yichu信号输出 |
| 闸门指示 | 闸门开闭状态可见 | led信号输出 |
| 记忆显示 | 测量结果保留到下一次更新 | latch1~latch6锁存器 |
| 输入要求 | 被测信号为脉冲波 | 计数直接接clk_in |
yichu这一项的实现细节值得注意:初始值为 1,六位全 9 后再来一个脉冲,所有计数器清零同时yichu拉低。如果实验板上的 LED 是低电平点亮,这个逻辑直接接上去就能用;如果是高电平点亮,需要在顶层加一级取反。报告里没有提这个极性适配问题,但上板时一定会遇到。
3. 分频器模块:四路时基的分频代码、仿真和阈值调整
3.1 计数分频的实现:阈值翻转变换
分频器module count接收板载时钟clk,通过四个独立计数器分别输出 1Hz、10Hz、100Hz、1kHz 四路时基。核心代码逻辑如下:
if (!rest) begin count1 <= 0; count2 <= 0; count3 <= 0; count4 <= 0; clk_1hz <= 0; clk_10hz <= 0; clk_100hz <= 0; clk_1khz <= 0; end else begin if (count1 == 24000000) begin count1 <= 0; clk_1hz <= ~clk_1hz; end else count1 <= count1 + 1; if (count2 == 2400000) begin count2 <= 0; clk_10hz <= ~clk_10hz; end else count2 <= count2 + 1; if (count3 == 240000) begin count3 <= 0; clk_100hz <= ~clk_100hz; end else count3 <= count3 + 1; if (count4 == 24000) begin count4 <= 0; clk_1khz <= ~clk_1khz; end else count4 <= count4 + 1; end这段代码是典型的「计数器型分频」:每个时钟上升沿判断计数值是否到达阈值,到达就翻转一次输出并清零。count1阈值 24000000,意味着每经过 24000000 个输入时钟翻转一次,翻转两次构成一个完整周期,所以输出周期等于 2 × 24000000 个时钟周期。如果输入clk是 48MHz,那么clk_1hz正好是 1Hz;count2、count3、count4的阈值依次少一个数量级,对应刚好 10 倍频差。
四个计数器共用一个rest复位信号,复位时所有计数值和输出全部归零。原代码用initial做了上电初值,这在仿真时有用,但综合后initial通常不生成实际硬件逻辑,所以硬件上电后仍然依赖rest信号来建立确定状态。
3.2 仿真验证与阈值修正:晶振频率不匹配的坑
仿真时给clk一个 48MHz 时钟源,复位拉低再拉高,观察四路输出的波形。验证点有两个:一是每路输出的周期是否符合预期,二是四路输出之间是否严格满足 10 倍频关系。如果clk_1hz周期为 1s、clk_10hz周期为 100ms、clk_100hz周期为 10ms、clk_1khz周期为 1ms,分频模块就可以放心交给后级。
我在复现这份设计时踩过一个隐性坑:很多教学实验板的板载晶振不是 48MHz,而是 24MHz。同样一份代码,拿 24MHz 时钟跑,clk_1hz实际变成 0.5Hz,闸门时间翻倍成 2s,所有读数系统性偏大一倍。这类问题在仿真里几乎看不出来,因为仿真激励可以随便给 48MHz,但上板后数据全错。
提示:上板前先确认板载晶振频率。如果板子是 24MHz,把四个阈值全部减半(
count1改 12000000,其余按比例类推)即可恢复标准时基。如果板子用 50MHz 晶振,则把 24000000 改 25000000,其余按比例调整。
验证分频模块最直接的办法,是把clk_1hz接到实验板上的一个 LED,用秒表掐一下亮灭周期。这个土办法比任何仿真都可靠,能同时验证阈值参数和复位逻辑是否正确。
4. 门控与计数器:闸门状态机、BCD 级联和溢出标志
4.1 chose 模块:三档闸门选择与 14 拍状态机
门控电路是这份设计里时序最复杂的模块,它负责三件事:根据按键选择时基、产生闸门开闭信号、按顺序发出锁存和清零脉冲。核心是两个always块:
always @(posedge clk_1khz) begin if (!rest) begin gate_out1 <= 0; ff <= 2'b00; end else begin if (key1 == 1 && key10 == 0 && key100 == 0) begin gate <= clk_10hz; ff <= 2'b01; end else if (key1 == 0 && key10 == 1 && key100 == 0) begin gate <= clk_100hz; ff <= 2'b10; end else if (key1 == 0 && key10 == 0 && key100 == 1) begin gate <= clk_1khz; ff <= 2'b11; end else begin ff <= 0; end end end always @(posedge gate) begin count <= count + 1; if (count <= 9) begin gate_out1 <= 1; clk_clear <= 0; clk_latch <= 0; end else if (count == 11) begin gate_out1 <= 0; clk_clear <= 0; clk_latch <= 1; end else if (count == 13) begin gate_out1 <= 0; clk_clear <= 1; clk_latch <= 0; count <= 0; end else begin gate_out1 <= 0; clk_clear <= 0; clk_latch <= 0; end end第一个always在clk_1khz上升沿对gate做选择,key1对应 1s 档(选clk_10hz),key10对应 0.1s 档,key100对应 0.01s 档。ff是两比特档位编码,一路传给显示模块控制小数点位置。
第二个always以gate的上升沿为时钟,运行一个 0 到 13 的计数器:0 到 9 期间gate_out1保持高电平,闸门持续开启,计数器对被测信号计数;到第 11 拍输出clk_latch锁存脉冲,把此时的计数值捕获到锁存器中;到第 13 拍输出clk_clear清零脉冲,把计数器全部清零,为下一轮测量做准备。
这里的时间关系是关键:锁存发生在闸门关闭之后两拍,清零又发生在锁存之后两拍,三者互不重叠,确保「先保存再清零」的顺序不被破坏。为什么count从 10 到 13 中间有两拍空闲?实际是给锁存器的建立时间和计数器的传播留出裕量,避免清零信号和锁存信号同时有效导致数据丢失。
4.2 counter 模块:六位 BCD 级联的进位链
计数器模块维护cnt1到cnt6六组 4 比特 BCD 值,对应个位到十万位。进位逻辑使用逐级判别的方式:
if (clk_clear == 1) begin cnt1 <= 0; cnt2 <= 0; cnt3 <= 0; cnt4 <= 0; cnt5 <= 0; cnt6 <= 0; yichu <= 1; end else if (start == 1) begin if ((cnt6 == 9) && (cnt5 == 9) && (cnt4 == 9) && (cnt3 == 9) && (cnt2 == 9) && (cnt1 == 9)) begin cnt1 <= 0; cnt2 <= 0; cnt3 <= 0; cnt4 <= 0; cnt5 <= 0; cnt6 <= 0; yichu <= 0; end else if ((cnt5 == 9) && (cnt4 == 9) && (cnt3 == 9) && (cnt2 == 9) && (cnt1 == 9)) begin cnt1 <= 0; cnt2 <= 0; cnt3 <= 0; cnt4 <= 0; cnt5 <= 0; cnt6 <= cnt6 + 1; end // ... 后续四级进位,低四级满 9 时向高一级进位 else begin cnt1 <= cnt1 + 1; end end这段代码把「BCD 计数器满九清零、向高位进一」的判断全部展开写成多级分支,好处是逻辑清晰、每个分支都能直接对应到仿真波形;代价是综合后形成比较长的进位链。当cnt1到cnt6全部为 9 且再来一个脉冲时,所有位清零并输出溢出标志yichu == 0,同时六位显示归零——这正是设计任务里要求的「溢出时做溢出指示」。
进位判别的顺序必须从高位往低位写,不能反过来。先判断六位全 9,再判断五位全 9,依次递减,最后才处理普通的个位加一。如果顺序写反,六位全 9 的情况会被后面的分支提前截住,永远进不到溢出分支。
4.3 时序配合:为什么读数不会跳变
把gate_out1、clk_latch、clk_clear三个信号放在同一张时序图里看,整个测量周期的四拍结构非常清楚:
- 闸门开启:
gate_out1拉高 10 个时基周期,计数器累计被测脉冲; - 闸门关闭:
gate_out1拉低,计数器停在当前值; - 锁存更新:
clk_latch发出一个脉冲,计数值写入latch1~latch6; - 清零复位:
clk_clear发出一个脉冲,计数器清零,等待下一轮测量。
在闸门开启期间,锁存器保持上一轮的旧值不变,数码管显示的是上次结果;只有clk_latch到来时才更新为新值。这就是任务书里提到的「记忆显示」——测量结束时才刷新显示,新值到来之前旧值一直保留,避免了闸门开启过程中数码管数字乱跳的问题。上板观察时会发现,每隔一个闸门周期,显示数字跳变一次,并且跳变瞬间所有位同时更新,而不是逐位滚动。
5. 锁存扫描显示与顶层例化:六个实测坑和对应解法
5.1 锁存器与动态扫描:v138 模块的显示逻辑
v138模块承担锁存、位选、译码、小数点控制四件事。clk_1khz驱动的扫描计数器循环选通六位数码管,每个时钟周期切换一位,把对应的 BCD 值送进段码译码逻辑。段码表是共阳极格式,0对应7'b0000001,1对应7'b1001111,依此类推。如果实验板用的是共阴极数码管,需要把所有段码按位取反,或者把case里的常量整体替换成共阴极版本,否则数字显示全是反的。
小数点位置与档位联动:1s 档时ff == 2'b01且扫描到第三位时点亮小数点,显示格式是X.XXX kHz;0.1s 档小数点移到第二位,显示XX.XX kHz;0.01s 档小数点在第一位的右侧,显示XXX.X kHz。这个位置对应关系直接反映被测频率的量级——同样一组数字,小数点在不同位置代表的频率差了十倍,上板验证时必须先确认档位和小数点位置一致。
扫描刷新率是另一个值得记录的参数。clk_1khz驱动六位数码管逐位轮询,每位刷新频率约 166Hz,远高于人眼临界闪烁频率,所以视觉上六位数字是同时稳定点亮的,不会出现逐位滚动的闪烁感。
5.2 顶层例化集成:wire 声明与端口名检查
顶层top模块把四个子模块例化连接,是所有信号汇合的地方。复现时会遇到两类典型的编译问题,都和例化语句有关。
第一类是隐式 wire 问题。顶层只声明了out1~out6、ff、gate_out1等内部信号,但分频器输出的clk_10hz、clk_100hz、clk_1khz直接出现在例化端口连接里。教学版 ISE 遇到这种情况会自动推断为隐式 wire,编译能过;换成更严格的工具链时,未声明的标识符会直接报Identifier has not been declared。解决方式是在module top的信号声明区补齐:
wire clk_10hz; wire clk_100hz; wire clk_1khz; wire clk_latch; wire clk_clear; reg gate_out1; // 或声明为 wire,取决于驱动方式第二类是端口名拼写不一致。原件代码中chose模块的例化语句里出现了.ky1(key1)这类写法,而子模块端口名实际是key1,多个版本的代码互相拷贝时最容易出现这种错位。遇到这类问题,把例化端口名和模块定义里的端口名逐一对照,一字不差才能过编译。
5.3 避坑记录:六条现象、原因与解法
第一条,数字整体偏大一倍左右。原因是分频器阈值按 48MHz 晶振设计而实验板实际是 24MHz,闸门时间翻倍。按实际晶振频率折算阈值即可,24MHz 就把count1从 24000000 改成 12000000。
第二条,仿真通过但上板显示全灭或全亮。原因多半是段码极性与实验板数码管不匹配,共阳极代码配了共阴极硬件。对着实验板原理图确认数码管公共端,把段码表整体取反。资源报告里附带的段码表和实验板型号相关,换板子必须重新核对。
第三条,测量结果高频段正常、低频段跳动大。这是 ±1 量化误差的物理现象,不是设计缺陷。低频测量换更长的闸门时间,比如 10Hz 信号用 1s 闸门只有 ±10% 跳动,用 0.01s 闸门误差更大。想提高低频精度,只能换闸门时间或者在计数器前加前置分频。
第四条,按键切换档位时读数偶尔错乱。原因是机械按键抖动直接进入了门控电路,key信号在切换瞬间可能出现多次跳变,导致ff档位码不稳定。常见做法是在按键信号进 FPGA 后先打两拍同步,再加一个简单的消抖计数器,确认按键电平稳定后再允许档位切换。
第五条,复位按键按下后显示数据没有立刻清零。原因是复位信号是异步的,而计数器模块只在clk_in上升沿判断rest。如果复位时正好没有输入脉冲上升沿,计数器不会立即响应。需要在always块里把rest加入敏感列表做异步复位,而不是只用同步判断。
第六条,闸门指示 LED 状态和读数更新对不上。原因是led信号在分频器的clk_1khz时钟域里翻转,而闸门信号gate_out1在时基时钟域里工作,两个时钟域之间存在相位差。观察 LED 时应该以闸门周期为单位看亮灭节奏,而不是盯到单个时钟沿。若要精确对齐,可以在顶层把gate_out1直接引到 LED 引脚,不经过程序内部产生的led信号。
提示:上面六条里,第一条和第三条例外是「行为正常但参数需要适配」,其余四条都属于代码迁移到新板时最常见的硬伤。拿到一份新资源,先按这四个点排查,比直接跑仿真省时间得多。
6. 上板前的验证习惯:用三组手算值卡死每次仿真
仿真的最终目的是上板前把所有可预见的问题清掉。我在这份设计上养成的习惯是:每次改完代码,先在 ModelSim 或 ISE Simulator 里跑三组特定频率的测试,再把仿真数出来的计数结果和手算值对比。
第一组测 10Hz:用 1s 闸门,理论计数值 10,但因为 ±1 量化误差,仿真结果可能是 9、10 或 11,显示 0.009~0.011 kHz 都算正常。这一组确认低频段量程和 BCD 计数的个位进位。
第二组测 1MHz:用 0.1s 闸门,理论计数值 100000,误差只占十万分之一,仿真结果必须精确是 100000,显示 100.00 kHz。这一组验证中频分段和小数点位置(ff == 2'b10时第二位点亮)。
第三组测 9.5MHz:用 0.01s 闸门,理论计数值 95000,显示 950.0 kHz;再把输入调到 10MHz 以上,观察yichu是否拉低、六位数码管是否全部归零。这一组同时验证高频段和溢出路径。
仿真里只需要关注几个关键时间点:gate_out1拉高瞬间是否正好在时基上升沿;clk_latch是否在闸门关闭后的第 11 拍;clk_clear是否在 13 拍。这三个时间点对了,整体时序就立住了,剩下全是参数适配问题。
报告里的实测表值得拿来当标尺:输入 900kHz 读 899.995、输入 920kHz 读 919.994,这类偏差恰好对得上 ±1 计数误差的合成结果——真实频率落在 899.5 到 900.5 之间,显示任何一个值都符合设计预期。把这张表里的数据作为上板验收的下限标准,比盯着仿真波形更快判断问题出在哪个环节。
从那次以后,我每次拿到 FPGA 频率计类项目,都会强制走一遍这套流程:先查晶振频率和阈值匹配,再核对段码极性,然后跑三组手算值,最后才上板。设计本身并不难,难的是这些参数适配全是玄学一样存在——不摸清板子的实际配置,代码写得再规范也白搭。希望这份拆解能帮你在自己的板子上少走几趟弯路。
本文还有配套的精品资源,点击获取