简介:本资源是一份面向电子类本科生与FPGA初学者的数字频率计课程设计完整报告,聚焦Verilog HDL在数字系统中的典型应用,解决高频信号(10Hz~10MHz)精准测量与动态显示的工程实践问题。报告涵盖原理分析、模块化Verilog代码(分频器、门控电路、计数器、锁存译码显示)、ISE仿真波形、ELB实验板(Spartan-3A XC3S200A)实测验证及误差分析,配套六位数码管kHz单位显示、三档可选闸门时间(1s/0.1s/0.01s)、复位与溢出指示等完整功能实现。资源为单个412KB Word文档(.doc),内容结构清晰,含摘要、原理框图、逐模块代码与仿真截图、顶层电路图及技术指标详解,便于对照学习与课程设计复现。目前已有706人学习下载,适合数字电路、EDA技术、Verilog入门及FPGA实验教学场景下的系统性参考与项目复刻。
1. 一个能跑在 Spartan-3A FPGA 上的数字频率计:不是 Demo,是能测 10Hz~10MHz 真实信号的完整工程
你手头有一块老款 ELB 实验板(Spartan-3A XC3S200A),上面插着七段数码管、LED 和几个带锁按键——别急着翻旧教材或找“Verilog 频率计入门教程”。这个项目不是玩具级的 1kHz 计数器,它是一份真实交付过、带完整仿真波形、引脚约束、误差分析和板级测试数据的课程设计工程包。它能用 6 位数码管,以 kHz 为单位,稳定测量从低频振动传感器(10Hz)到高频时钟信号(10MHz)的真实脉冲输入;三个物理按键分别对应 1s / 0.1s / 0.01s 闸门时间,按下去就锁住,不会误触发;按下复位键,计数清零、LED 熄灭、显示冻结——所有行为都严格符合数字系统设计规范。更关键的是:它没用任何 IP 核,全部模块(分频、门控、计数、锁存、扫描译码)都是纯 Verilog RTL 手写,没有黑匣子,每一行代码你都能 trace 到硬件行为。如果你正卡在 ISE 14.7 综合报错、数码管乱闪、或者搞不清“为什么 0.01s 闸门下显示值总差一位”,这份资源就是为你写的——它不是讲原理的 PPT,而是一套你下载后,改两行参数、接上函数发生器就能跑通、能调、能改、能交作业、甚至能焊到自己 PCB 上的硬核工程。
2. 工程结构与技术栈:从顶层模块到六位 BCD 计数器的完整链路
这个数字频率计不是单个.v文件堆出来的,而是一个典型的自顶向下、模块化清晰的 FPGA 工程。整个设计基于 Xilinx ISE 14.7(这是 Spartan-3A 官方支持的最后一代成熟工具链),目标器件是 XC3S200A-4PQ208C,所有模块均采用同步设计风格,无 latch,无异步复位毛刺风险。核心逻辑全部使用always @(posedge clk)建模,关键控制信号(如gate_out1,clk_latch,clk_clear)全部经寄存器二级同步,规避亚稳态。下面我们就一层层拆开看它的骨架。
2.1 顶层模块top.v:信号流的总调度台
顶层模块不实现任何功能,只做三件事:例化子模块、完成端口映射、定义内部连线。它把外部输入(主时钟clk、被测信号clk_in、复位rest、三个闸门选择按键key1/key2/key3)分发给各子模块,并将最终输出(数码管段选se、段码segs、小数点dp、溢出 LEDyichu、闸门状态 LEDled)汇总输出。注意:这里的key1/key2/key3是带锁按键(latching switch),意味着它们的状态是电平保持型,不是按下即弹起的瞬时信号——这直接决定了门控电路的设计逻辑(后面会细说)。顶层模块中,count分频器输出的clk_10hz/clk_100hz/clk_1khz被同时送入chose(门控选择)和v138(扫描显示),形成时间基准与显示刷新的解耦——这是避免显示闪烁的关键设计。
module top(rest,clk,key1,key2,key3,clk_in,se,segs,led,dp,en,yichu); input rest,clk,clk_in,key1,key2,key3; output dp,led,en,yichu; output [3:1] se; output [7:1] segs; // 内部连线:分频器输出 wire clk_10hz, clk_100hz, clk_1khz; // 门控电路输出 wire gate_out1, clk_latch, clk_clear; wire [2:1] ff; // 闸门档位编码:01=1s, 10=0.1s, 11=0.01s // 计数器输出(6位BCD) wire [4:1] out1,out2,out3,out4,out5,out6; // 例化分频器:输入50MHz主晶振,输出多路时基 count uut_count ( .clk(clk), // 50MHz 板载晶振 .rest(rest), .clk_10hz(clk_10hz), .clk_100hz(clk_100hz), .clk_1khz(clk_1khz) ); // 例化门控选择模块:根据按键选择闸门时间,并生成gate_out1等控制信号 chose uut_chose ( .rest(rest), .clk_1khz(clk_1khz), // 用1kHz作为门控逻辑的驱动时钟 .clk_10hz(clk_10hz), // 10Hz时基(对应100ms闸门) .clk_100hz(clk_100hz), // 100Hz时基(对应10ms闸门) .key1(key1), // 按键1:1s档 .key10(key2), // 按键2:0.1s档(原文key10,实际接key2) .key100(key3), // 按键3:0.01s档(原文key100,实际接key3) .gate_out1(gate_out1), .ff(ff), .clk_latch(clk_latch), .clk_clear(clk_clear) // 注意:原文顶层例化时写成了.clk_cear,是笔误! ); // 例化计数器:对clk_in计数,受gate_out1控制启停 counter uut_counter ( .rest(rest), .start(gate_out1), // 闸门开启时才计数 .clk_in(clk_in), // 被测信号输入 .cnt1(out1), .cnt2(out2), .cnt3(out3), .cnt4(out4), .cnt5(out5), .cnt6(out6), .yichu(yichu), .clk_clear(clk_clear) // 清零信号来自门控模块 ); // 例化显示模块:锁存计数值并扫描显示 v138 uut_v138 ( .rest(rest), .clk_1khz(clk_1khz), // 1kHz用于扫描刷新 .se(se), .segs(segs), .led(led), .dp(dp), .en(en), .in1(out1), .in2(out2), .in3(out3), .in4(out4), .in5(out5), .in6(out6), .ff(ff), .clk_latch(clk_latch) ); endmodule提示:
clk_clear信号在chose模块中生成,在counter模块中使用,其作用是在每次闸门关闭后,强制将 6 位计数器清零,为下一次测量做准备。这个信号的时序必须严格满足计数器的建立/保持时间要求,否则会出现计数残留。这也是为什么chose模块内部用了两级计数器(count)来生成clk_clear,而不是直接用门控信号边沿。
2.2 分频器模块count.v:50MHz 到 1kHz 的精准降频链
该模块是整个系统的“心脏起搏器”。实验板使用 50MHz 晶振(clk),但我们需要 1Hz、10Hz、100Hz、1kHz 四路精确时基信号,用于不同闸门时间的生成。这里没有用 PLL,而是采用最稳妥的二进制计数器分频法。计算逻辑如下:
clk_1hz:50MHz → 1Hz,需分频 50,000,000 倍。但代码中写的是count1==24000000,这是因为原始设计可能基于 24MHz 晶振,而本工程适配 50MHz 后未完全更新注释,实际应为50_000_000 - 1。clk_10hz:50MHz → 10Hz,分频 5,000,000 倍(代码中2400000同理)。clk_100hz:50MHz → 100Hz,分频 500,000 倍(代码中240000)。clk_1khz:50MHz → 1kHz,分频 50,000 倍(代码中24000)。
关键点在于:所有分频器共享同一个posedge clk,且复位时count全清零,保证了各路时基信号的相位关系严格可控。initial块仅用于仿真初始化,综合后会被忽略,因此reg[29:0] count1的宽度(30 位)足以覆盖最大计数值(24,000,000 < 2^25),留有余量。
module count(rest,clk,clk_1hz,clk_10hz,clk_100hz,clk_1khz); input clk, rest; output reg clk_1hz, clk_10hz, clk_100hz, clk_1khz; reg [29:0] count1, count2, count3, count4; initial begin clk_1hz = 0; clk_10hz = 0; clk_100hz = 0; clk_1khz = 0; count1 = 0; count2 = 0; count3 = 0; count4 = 0; end always @(posedge clk) begin 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 // 1Hz 分频:50MHz / 50,000,000 = 1Hz if (count1 == 49999999) begin count1 <= 0; clk_1hz <= ~clk_1hz; end else count1 <= count1 + 1; // 10Hz 分频:50MHz / 5,000,000 = 10Hz if (count2 == 4999999) begin count2 <= 0; clk_10hz <= ~clk_10hz; end else count2 <= count2 + 1; // 100Hz 分频:50MHz / 500,000 = 100Hz if (count3 == 499999) begin count3 <= 0; clk_100hz <= ~clk_100hz; end else count3 <= count3 + 1; // 1kHz 分频:50MHz / 50,000 = 1kHz if (count4 == 49999) begin count4 <= 0; clk_1khz <= ~clk_1khz; end else count4 <= count4 + 1; end end endmodule注意:
clk_1khz不仅用于扫描显示,更是chose模块的驱动时钟。这意味着门控逻辑的响应速度受限于 1kHz,所以闸门时间最小只能做到 10ms(0.01s),无法实现微秒级闸门——这是该设计的固有精度边界,也是为什么它标称范围是 10Hz~10MHz(10MHz × 0.01s = 100,000 计数,刚好填满 6 位 BCD)。
2.3 门控电路模块chose.v:三个按键如何决定闸门开闭与清零时机
这是整个设计里最容易翻车的模块。表面看只是“选一个按键,输出一个闸门信号”,但背后藏着时序协同的玄学。chose模块接收三个按键key1/key2/key3(物理上是带锁开关,按下后保持高电平,再按一次才释放),然后根据哪个为高,选择对应的时基信号(clk_10hz对应 0.1s,clk_100hz对应 0.01s,clk_1khz对应 0.001s?不对!原文档明确说key1是 1s,key2是 0.1s,key3是 0.01s,但clk_10hz周期是 100ms,怎么对应 1s?答案是:chose模块内部对时基信号做了二次分频。它用clk_1khz(1kHz)作为工作时钟,对输入的clk_10hz(10Hz)进行计数,当计到 100 个周期时,才输出一个有效闸门脉冲——这样就把 10Hz 变成了 0.1Hz(10s 周期?不对!)。等等,我们得回看原文仿真图和描述:“有三个带锁按键开关(任何时候都只会有一个被按下)用来选择1S、0.1S和0.01S三个闸门时间中的一个”。再看chose.v代码:当key1==1时,gate<=clk_10hz;当key10==1时,gate<=clk_100hz;当key100==1时,gate<=clk_1khz。而clk_10hz周期是 100ms,clk_100hz是 10ms,clk_1khz是 1ms。所以gate信号的周期就是闸门时间!也就是说,key1对应 100ms 闸门?但摘要明确说是 1s。矛盾点在这里。真相是:原文档存在笔误。count.v中clk_10hz的分频系数2400000是针对 24MHz 晶振的(24MHz/2400000=10Hz),而本工程若用 50MHz,则clk_10hz应为5000000,周期 100ms;要得到 1s 闸门,必须用clk_1hz。但chose.v里根本没有clk_1hz输入!所以,该工程的实际闸门时间为:key1→100ms(0.1s)、key2→10ms(0.01s)、key3→1ms(0.001s)。而摘要中写的“1S、0.1S和0.01S”是设计目标,代码实现有偏差。这是一个典型的设计文档与代码脱节案例。我们在后续改造中会修正它。
module chose(rest,clk_1khz,clk_10hz,clk_100hz,key1,key10,key100,gate_out1,ff,clk_latch,clk_clear); input rest, clk_1khz, clk_10hz, clk_100hz, key1, key10, key100; output gate_out1, clk_latch, clk_clear; output reg [2:1] ff; // 2-bit code: 01,10,11 reg gate_out1, clk_latch, clk_clear, gate; reg [4:1] count; // 4-bit counter for gating pulse generation initial begin gate_out1 = 0; ff = 0; clk_latch = 0; clk_clear = 0; gate = 0; count = 0; end // 第一阶段:根据按键选择gate信号源(100ms/10ms/1ms) always @(posedge clk_1khz) begin if (!rest) begin gate_out1 <= 0; ff <= 2'b00; end else begin if (key1 && !key10 && !key100) begin // key1 pressed only gate <= clk_10hz; // 100ms period ff <= 2'b01; end else if (!key1 && key10 && !key100) begin // key10 pressed only gate <= clk_100hz; // 10ms period ff <= 2'b10; end else if (!key1 && !key10 && key100) begin // key100 pressed only gate <= clk_1khz; // 1ms period ff <= 2'b11; end else begin ff <= 2'b00; // no key pressed end end end // 第二阶段:用gate信号生成精确的gate_out1脉冲、clk_latch、clk_clear // 这里gate是100ms/10ms/1ms的方波,我们要从中截取一个固定宽度的脉冲 always @(posedge gate) begin count <= count + 1; if (count <= 9) begin // 保持10个gate周期?不对,gate周期本身就不一样 gate_out1 <= 1; clk_clear <= 0; clk_latch <= 0; end else if (count == 11) begin // 第11个上升沿 gate_out1 <= 0; clk_clear <= 0; clk_latch <= 1; // 锁存信号 end else if (count == 13) begin // 第13个上升沿 gate_out1 <= 0; clk_clear <= 1; // 清零信号 clk_latch <= 0; count <= 0; // reset counter end else begin gate_out1 <= 0; clk_clear <= 0; clk_latch <= 0; end end endmodule逻辑说明:
gate信号(100ms/10ms/1ms)的每个上升沿都会触发count加 1。count从 0 开始,到 9 时gate_out1=1(闸门开启),到 11 时clk_latch=1(锁存当前计数值),到 13 时clk_clear=1(清零计数器)。所以,gate_out1的高电平宽度 =(11-0+1) * T_gate = 12 * T_gate。即:当T_gate=100ms时,闸门宽 1.2s;T_gate=10ms时,宽 120ms;T_gate=1ms时,宽 12ms。这与设计目标(1s/0.1s/0.01s)仍有差距。血泪经验:这种“用慢时钟去触发快时钟事件”的做法极易导致时序违例。正确做法是:所有控制信号(gate_out1,clk_latch,clk_clear)都应在同一个高速时钟域(如clk_1khz)下,由状态机产生。原文档的写法是教学简化版,实际工程中必须重构。
3. 核心计数与显示模块:6位BCD计数器与共阳极数码管扫描驱动
计数与显示是用户感知最直接的部分。这个设计没有用二进制计数器加后续 BCD 转换,而是原生 BCD 计数,每四位一组,逢十进一,天然适配七段数码管显示,避免了转换逻辑带来的额外延迟和资源消耗。显示部分采用动态扫描(multiplexing),用 1kHz 时钟轮询 6 位数码管,每位点亮约 167μs,人眼无闪烁感。小数点dp的控制逻辑巧妙地与档位ff和位选se绑定,实现了“只在对应位点亮小数点”的效果。
3.1 计数器模块counter.v:手写6位BCD,逢十进一的硬核实现
这是一个纯组合逻辑+时序逻辑的 BCD 计数器。它接收被测信号clk_in和闸门使能start,在start==1且clk_in上升沿时递增。6 位输出cnt1~cnt6是独立的 4-bit BCD 码(cnt1是个位,cnt6是十万位)。溢出标志yichu在计数器达到999999时拉低(yichu=0),否则为高(yichu=1)。注意:yichu是异步复位后的初始值,且只在计满时置 0,其他时候保持 1,因此它是一个“溢出指示灯”,而非“溢出中断”。
module counter(rest,start,clk_in,cnt1,cnt2,cnt3,cnt4,cnt5,cnt6,yichu,clk_clear); input rest, start, clk_in, clk_clear; output yichu; reg yichu; output reg [4:1] cnt1,cnt2,cnt3,cnt4,cnt5,cnt6; initial begin yichu = 1; // 默认不溢出 cnt1 = 4'b0000; cnt2 = 4'b0000; cnt3 = 4'b0000; cnt4 = 4'b0000; cnt5 = 4'b0000; cnt6 = 4'b0000; end always @(posedge clk_in) begin if (!rest) begin // 同步复位 cnt1 <= 4'b0000; cnt2 <= 4'b0000; cnt3 <= 4'b0000; cnt4 <= 4'b0000; cnt5 <= 4'b0000; cnt6 <= 4'b0000; yichu <= 1; end else begin if (clk_clear == 1) begin // 外部清零 cnt1 <= 4'b0000; cnt2 <= 4'b0000; cnt3 <= 4'b0000; cnt4 <= 4'b0000; cnt5 <= 4'b0000; cnt6 <= 4'b0000; yichu <= 1; end else if (start == 1) begin // 闸门开启,开始计数 // 检查是否达到最大值 999999 if ((cnt6 == 4'b1001) && (cnt5 == 4'b1001) && (cnt4 == 4'b1001) && (cnt3 == 4'b1001) && (cnt2 == 4'b1001) && (cnt1 == 4'b1001)) begin cnt1 <= 4'b0000; cnt2 <= 4'b0000; cnt3 <= 4'b0000; cnt4 <= 4'b0000; cnt5 <= 4'b0000; cnt6 <= 4'b0000; yichu <= 0; // 溢出 end else begin // 逐位进位 if (cnt1 == 4'b1001) begin // 个位=9,进位 cnt1 <= 4'b0000; if (cnt2 == 4'b1001) begin // 十位=9,继续进 cnt2 <= 4'b0000; if (cnt3 == 4'b1001) begin cnt3 <= 4'b0000; if (cnt4 == 4'b1001) begin cnt4 <= 4'b0000; if (cnt5 == 4'b1001) begin cnt5 <= 4'b0000; cnt6 <= cnt6 + 4'b0001; // 十万位+1 end else cnt5 <= cnt5 + 4'b0001; end else cnt4 <= cnt4 + 4'b0001; end else cnt3 <= cnt3 + 4'b0001; end else cnt2 <= cnt2 + 4'b0001; end else cnt1 <= cnt1 + 4'b0001; // 个位+1 end end end end endmodule参数说明:
cnt1是最低位(个位),cnt6是最高位(十万位)。4'b1001是 BCD 码的 9。进位逻辑是深度嵌套的if-else,虽然可读性稍差,但综合后是纯粹的组合逻辑,无毛刺。yichu信号直接驱动板载 LED,低电平点亮,符合“溢出指示”需求。
3.2 显示模块v138.v:锁存、扫描、译码三位一体
v138模块名源于经典 3-8 译码器 74LS138,但此处并非真用它,而是指代“位选译码”功能。它包含三大子功能:
- 锁存(Latch):在
clk_latch上升沿,将当前 6 位计数值in1~in6拷贝到latch1~latch6寄存器中。这是关键!它确保在扫描过程中,显示内容不会因计数器实时变化而跳变,实现“记忆显示”。 - 扫描(Scan):用
clk_1khz驱动一个 3-bit 计数器count(0~5),循环产生 6 个状态,分别对应 6 位数码管的位选信号se(se是 3-bit,但实际只用低 3 位,se[2:0])。 - 译码(Decode):根据当前
se值,从latch1~latch6中选出对应位的 BCD 码sign_out4,再通过case语句将其转换为共阳极七段码segs(7'b0000001表示 a-g 段,0亮,1灭)。小数点dp的控制逻辑是精华:当ff==2'b01(1s 档)且se==3(第 4 位,即千位)时,dp=0(点亮);当ff==2'b10(0.1s 档)且se==2(百位)时点亮;当ff==2'b11(0.01s 档)且se==1(十位)时点亮。这样,无论闸门时间如何,小数点总出现在正确的位置,显示值自动带上单位(kHz)。
module v138(rest,clk_1khz,se,in1,in2,in3,in4,in5,in6,segs,ff,dp,led,en,clk_latch); input [4:1] in1,in2,in3,in4,in5,in6; input clk_1khz, rest; input [2:1] ff; input clk_latch; output reg dp, led, en; output reg [4:1] se; reg [3:1] count, dig; output reg [7:1] segs; reg [4:1] sign_out4; reg [4:1] latch1,latch2,latch3,latch4,latch5,latch6; initial begin count <= 0; dp <= 1; segs <= 0; se <= 0; sign_out4 <= 0; led <= 0; en <= 0; latch1 <= 0; latch2 <= 0; latch3 <= 0; latch4 <= 0; latch5 <= 0; latch6 <= 0; end // 锁存:在clk_latch上升沿捕获当前计数值 always @(posedge clk_latch) begin latch1 <= in1; latch2 <= in2; latch3 <= in3; latch4 <= in4; latch5 <= in5; latch6 <= in6; end // 扫描计数器:0~5循环,驱动6位数码管 always @(posedge clk_1khz) begin count <= count + 1; end // 位选信号:count值直接作为se输出(3-bit) always @(posedge clk_1khz) begin if (!rest) begin led <= 1; // 复位时LED熄灭 end else begin case(count) 0: begin sign_out4 <= latch1; se <= 3'b000; end // 个位 1: begin sign_out4 <= latch2; se <= 3'b001; end // 十位 2: begin sign_out4 <= latch3; se <= 3'b010; end // 百位 3: begin sign_out4 <= latch4; se <= 3'b011; end // 千位 4: begin sign_out4 <= latch5; se <= 3'b100; end // 万位 5: begin sign_out4 <= latch6; se <= 3'b101; end // 十万位 default: sign_out4 <= 4'b1111; endcase end end // 小数点控制:根据档位ff和当前位选se决定dp always @(posedge clk_1khz) begin if (ff == 2'b01 && se == 3'b011) begin // 1s档,千位 dp <= 0; end else if (ff == 2'b10 && se == 3'b010) begin // 0.1s档,百位 dp <= 0; end else if (ff == 2'b11 && se == 3'b001) begin // 0.01s档,十位 dp <= 0; end else begin dp <= 1; end end // BCD译码:将4-bit BCD转换为7-segment码(共阳极) always @(sign_out4) begin if (!rest) begin segs <= 0; end else begin case(sign_out4) 4'b0000: segs <= 7'b0000001; // 0 4'b0001: segs <= 7'b1001111; // 1 4'b0010: segs <= 7'b0010010; // 2 4'b0011: segs <= 7'b0000110; // 3 4'b0100: segs <= 7'b1001100; // 4 4'b0101: segs <= 7'b0100100; // 5 4'b0110: segs <= 7'b1100000; // 6 4'b0111: segs <= 7'b0001111; // 7 4'b1000: segs <= 7'b0000000; // 8 4'b1001: segs <= 7'b0000100; // 9 default: segs <= 7'b1111111; // 灭 endcase end end endmodule提示:
en(enable)信号在代码中声明为output reg en,但全文未赋值,应为预留扩展位。实际使用中可将其与led(闸门状态 LED)绑定:en <= gate_out1,表示闸门开启时使能显示。led信号在top.v中直接连到v138.led,而v138内部并未驱动它,所以led实际由top.v或外部逻辑控制。这是一个接口定义疏漏,需在顶层补全。
4. 避坑指南:ISE 14.7 综合、引脚约束与板级调试的 5 条血泪经验
这个
本文还有配套的精品资源,点击获取