目录
[暂停 / 运行逻辑](#暂停 / 运行逻辑)
[核心方波发生器模块 programmable_square_wave.v](#核心方波发生器模块 programmable_square_wave.v)
[按键输入顶层模块 square_wave_top.v](#按键输入顶层模块 square_wave_top.v)
[Testbench 代码 tb_programmable_square_wave.v](#Testbench 代码 tb_programmable_square_wave.v)
[4bit PWM 核心模块 pwm_4bit.v](#4bit PWM 核心模块 pwm_4bit.v)
[按键消抖模块 key_debounce.v(用于输入 w)](#按键消抖模块 key_debounce.v(用于输入 w))
[分时复用 LED+PWM 调光顶层模块 led_pwm_top.v](#分时复用 LED+PWM 调光顶层模块 led_pwm_top.v)
[数码管扫描驱动 scan_led_hex_disp.v(复用优化版)](#数码管扫描驱动 scan_led_hex_disp.v(复用优化版))
[通用按键消抖模块 key_debounce.v](#通用按键消抖模块 key_debounce.v)
[数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版)](#数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版))
[自动滚动 + 按键步进 顶层模块 led_scroll_top.v](#自动滚动 + 按键步进 顶层模块 led_scroll_top.v)
[增强秒表核心模块 enhanced_stop_watch.v](#增强秒表核心模块 enhanced_stop_watch.v)
[数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版)](#数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版))
[顶层测试模块 enhanced_stop_watch_top.v](#顶层测试模块 enhanced_stop_watch_top.v)
组合逻辑电路的输出只跟当前的输入有关
然而,大量 常用逻辑电路的输出不仅跟当前的输入有关,而且跟过去的输入也有关。这就要求在电路中 必须包含一些存储元件来记住这些输入的过去值。这种包括锁存器和触发器的电路,我们称 之为时序电路。时序电路是一种能够记忆电路内部状态的电路。与组合逻辑电路不同,时序 逻辑电路的输出不仅取决于当前输入,还与其当前内部状态有关
触发器和锁存器
不同结构功能和不同用途的触发器和锁存器,是基本的时序电路元件,是时序 逻辑电路设计的基础;
基本D触发器
D触发器(DFF)是逻辑电路中最基本的存储元件
上升沿触发的D触发器是最简单的D触发器


从时序波形可以看出,输出信号d的值只在clk的上升沿到达时变化,并存储在触 发器中
无异步复位D触发器
module dff
(
input clk,
input d,
output reg q
);
always @(posedge clk)
q <= d;
endmodule
- 敏感列表中的posedge clk是时钟的上升沿检测函数, posedge(positive edge)指定时钟信号的变化方向为由0 变为1
- 这表明状态变化总是在clk信号的上升沿触发,反应出 触发器边沿触发的特性
- 注意:输入信号d不包含在敏感列表中。这就验证了输 入信号d只在时钟信号的上升沿进行采样,其值的改变并 不会立即改变输出信号
含异步复位的D触发器
D触发器可以包含异步复位信号,从时序波形可以看出,reset脚的高电平能够在任意时刻复位D触 发器,而不受时钟信号控制,它实际上比定期采样输入优先级更高

注:使用异步复位信号违反了同步设计方法,因此应该在正常操作中避免。其主要应用于执行系统 初始化。例如,在打开系统电源之后,我们可以生成一个短的复位脉冲迫使系统进入初始状态。
module dff_reset
(
input clk,reset,
input d,
output regq
);
always @(posedgeclk,posedgereset)
begin
if(reset)
q <= 1'b0;
else
q <= d;
end
endmodule
- reset信号的上升沿也包括在敏感列表 中,同时在if语句中要首先检查其值
- 如果reset信号为1,则将q信号置为0
- 这里所谓的"异步"是指独立于时钟控制器
- 在任何时刻,只要reset是高电平,触 发器输出端q的输出即为低电平
含异步复位和同步使能的D触发器
更加实用的D触发器包含一个额外的控制信号en,能够控制触发器进行输入值采样。注意,使能 信号en只有在时钟上升沿来临时才会生效,所以它是同步信号。如果en没有置1,触发器将保持 先前的值

moduledff_reset_en_1seg
(
input clk,reset,
input en,
input d,
output reg q
);
always @(posedge clk,posedge reset)
begin
if (reset)
q <= 1'b0;
else if(en)
q <= d;
end
endmodule
- 注意,第二个if语句后没有else分支
- 根据Verilog HDL语法,变量如果没有被赋新 值保持其先前的值
- 如果en等于0,q将保持原值
- 因此,省略的else分支描述了这个触发器的 预期行为
D触发器的使能特性在同步快子系统和慢子系统时是非常有用的
例如,假设快子系统和慢子系统的时钟频率分别为50 MHz和1 MHz。我们可以生成一个周期性的使能信号, 每50个时钟周期使能一个时钟周期,而不是另外派生出一个1MHz的时钟信号来驱动慢子系统。慢子系统是 其余49个时钟周期中是保持原来状态的。
这种方法同样可应用于消除门控时钟信号。
由于使能信号是同步的,该电路可以由一个常规的D触发器和简单下一状态逻辑电路构成


基本锁存器
当时钟clk为高电平的时,其输出q的值才会随输入d的数据 变化而更新;当clk为低电平时,锁存器将保持原来高电平时是锁存的值

module latch_1
(
input clk,
input d,
output reg q
);
always @(clk,d)
if(clk)
q <= d;
endmodule
- 当敏感信号clk电平从低变为高时,过程语句被启 动,顺序执行if语句,此时clk为1,于是执行q<=d ,把d的数据更新至q,然后结束if语句
- 当clk从1变为0或者保持低电平都不会触发过程, 锁存器的输出q将保持原来的状态,这就意味着在 设计模块中引入了存储元件。而此处也是省略的 else分支描述了这个触发器的预期行为
含清0控制的锁存器

module latch_reset_1
(
input clk,reset,
input d,
output q
);
assignq = (!reset)? 0:(clk?d:q);
endmodule
-
使用了具有并行语句特色的连续赋 值语句,其中使用了条件操作
module latch_reset_2
(
input clk,reset,
input d,
output regq
);
always @(clk,d,reset)
if(!reset)
q <= 0;
else if(clk)
q <= d;
endmodule -
使用的是过程语句,把数据信号d,复位信 号reset和时钟信号clk都列在敏感信号列表中,从 而实现例reset的异步特性和clk的电平触发特性
寄存器
位寄存器
为了设计一个1位寄存器,它可以在需要时从输入线in_data加载一个值,我们给D触发器增加一 根输入线load,当我们想要从in_data加载一个值时,就把load设置为1,那么在下一个时钟上升 沿,in_data的值将被存储在q中

modulereg_1
(
input clk,reset,
input in_data,
input load,
output reg out_data
);
always @(posedge clk,posedge reset)
if(reset)
out_data <= 0;
else if(load == 1)
out_data <= in_data;
endmodule
- 当load信号为0时,输入线上的数据 in_data就不会在每个时钟clk的上升 沿被不断地重新加载到out_data, 寄存器的输入out_data保持不变;
- 当load信号为1时,在下一个时钟clk 的上升沿,out_data就变为in_data 的值
N位寄存器

module reg_N
#(parameter N = 8)
(
input clk,
input reset,
input [N-1:0] in_data,
input load,
output reg [N-1:0] out_data
);
always @(posedge clk, posedge reset)
if(reset)
out_data <= 0;
else if(load == 1)
out_data <= in_data;
endmodule
- 使用parameter语句,是为了 使总线宽度可调
- 默认状态下,总线宽度为8
如果我们想修改寄存器的位宽,可以使用Verilog 的实例化语句。我们可以实现一个如下所示的16 位寄存器,称为fReg
reg_N#(.N(16))
fReg(
.clk(clk),
.reset(reset),
.load(load),
.in_data(indata),
.out_data(out_data)
);
寄存器组
- 寄存器组是由一组拥有同一个输入端口和一个或多个输出端口的寄存器组成。写地址信号 w_addr指定了数据存储位置,读取地址信号r_addr指定数据检索位置
- 寄存器组通常用于快速、临时存储
一个参数化的寄存器组的实现代码。参数W指定了地址线的位数,表明在这个寄存器组中有2W个 字。参数B指定了一个字的位数
module reg_file
#(
parameter N = 8,//比特数
W = 2 //地址比特数
)
(
input clk,
input wr_en,
input [W-1:0] w_addr,r_addr,
input [BN-1:0] w_data,
output [BN-1:0] r_data
);
reg [BN-1:0] array_reg[2**W-1:0];
always @(posedge clk)
if(wr_en)
array_reg[w_addr] <= w_data;
assign r_data = array_reg[r_addr];
endmodule
二维数组的数据类型:表示array_reg变量是一个含有[2**W-1:0]个元素的数组,每个元素的数据类型是 reg [B-1:0]
虽然描述非常抽象,Xilinx公司的软件能够识别出这种语言构造,并正确地执行
array_reg[...] = ...和... = array_reg[...]语句分别表明解码和多路复用的逻辑
一些应用程序可能需要同时检索多路数据,可以通过添加一个额外的读取端口来解决
例如: r_data2 = array_reg[r_addr_2];
移位寄存器
一个N位的移位寄存器包含N个触发器。在每个时钟脉冲作用下,数据从一个触发器移到另一个触发器
具有同步预置功能的8位移位寄存器
clk是移位时钟信号,load是并行数据预置使能信号,din是 8位并行预置数据端口,qb是串行输出端
工作原理:
- 当clk的上升沿到来时,过程被启动,如果此时预置使能端口load为高电平,则输入端口din的8位二进制数被同步并行移入移位寄存器,用作串行右移的初始值;
- 如果此时预置使能端口load为低电平,则执行赋值语句:reg8[6:0]<=reg8[7:1];
这样完成一个时钟周期后,把上一时钟周期的高7位值reg8[7:1]更新至此存储器的低7位reg8[6:0],实现右移一位的操作。连续赋值语句把移位寄存器最低位通过qb端口输出
module shift_reg8
(
input clk,
input load,
input[7:0] din,
output qb
);
reg[7:0] reg8;
always@(posedge clk)
if(load)
reg8<=din;
else
reg8[6:0]<=reg8[7:1];
assign qb=reg8[0];
endmodule
8位通用移位寄存器
- 通用移位寄存器可以加载并行数据,将其内容向左移位、向右移位或保持原有状态,也可以实现并转串(第一次加载并行输入,然后移位)或者串转并(首先移位,然后进行并行输出)
- 可以把通用移位寄存器分为组合逻辑和时序逻辑两部分,各用一个always块来进行描述。下一状态逻辑使用4选1的多路选择器来选择寄存器所需下一状态的值
d的最低位和最高位被用来作为串行输入的左移和右移操作
module univ_shift_reg
#(parameter N=8)
(
input clk,reset,
input[1:0] ctrl,
input[N-1:0] d;
output[N-1:0] q;
);
//信号声明
reg[N-1:0] r_reg,r_next;
//寄存器
always@(posedge clk,posedge reset)
if(reset)
r_reg<=0;
else
r_reg<=r_next;
//下一状态
always@(*)
case(ctrl)
2'b00:r_next=r_reg; //无操作
2'b01:r_next={r_reg[N-2:0],d[0]}; //左移
2'b10:r_next={d[N-1],r_reg[N-1:1]}; //右移
default:r_next=d; //载入
endcase
assign q=r_reg; //输出逻辑
endmodule
计数器
简单的二进制计数器
一个简单的二进制计数器通过二进制 序列反复循环。例如一个4位二进制计数器 计数,从"0000","0001",...,至"1111"而 后循环
module counter_sim_bin_N
#(parameter N=8)
(
input clk,
input reset,
output[N-1:0] qd,
output cout
);
reg[N-1:0] regN;
always@(posedge clk)
if(reset)
regN<=0;
else
regN<=regN+1;
assign qd=regN;
assign cout=(regN==2**N-1)?1'b1:1'b0; //2**N 2的N次方
endmodule
- 当复位端reset为高电平时,在时钟clk的上升沿,完成对计数器的复位;
- 在复位端为低电平后,计数器从下一个时钟上升沿从0000开始计数,直至计满1111后,再溢出为0000,此时计数器的进位端cout输入一个clk周期的高电平
- 同时计数器从并行输出端口qd同步输出当前计数器的值
通用二进制计数器
通用二进制计数器具有更多功能:
可以实现增/减计数、暂停、预置初值,并有同步清0等功能
计数器采用同步工作方式,同步时钟输入端口是clk
module counter_univ_bin_N
#(parameter N=8)
(
input clk,reset,load,up_down,
input[N-1:0] d,
output[N-1:0] qd
);
reg[N-1:0] regN;
always@(posedge clk)
if(reset)
regN<=0;
else if(load)
regN<=d;
else if(up_down)
regN<=regN+1;
else
regN<=regN-1;
assign qd=regN;
endmodule
reset=1→ 同步清零load=1→ 预置初值en=0→ 暂停en=1, up_down=1→ 加计数en=1, up_down=0→ 减计数
如果要实现暂停
module counter_univ_bin_N
#(parameter N=8)
(
input clk,
input reset, // 同步清零
input load, // 预置数
input en, // 使能(=0 就暂停)
input up_down, // 1=加计数 0=减计数
input [N-1:0] d,
output [N-1:0] qd
);
reg [N-1:0] regN;
always @(posedge clk)
if(reset) // 优先级最高:清零
regN <= 0;
else if(load) // 第二:预置数
regN <= d;
else if(en) begin // 第三:使能=1 才计数
if(up_down)
regN <= regN + 1;
else
regN <= regN - 1;
end
// 👇 这里就是【暂停】
else begin // en=0 → 什么都不做!保持原值
regN <= regN;
end
assign qd = regN;
endmodule
模m计数器
模m计数器的计数值从0增加到m-1,然后循环
参数M,它指定了计数模值m
参数N,它指定了计数器所需的位数,等于
module counter_mod_m
#(parameter N=4, //计数器位数
parameter M=10) //模M缺省为10
(
input clk,reset,
output[N-1:0] qd,
output cout
);
reg[N-1:0] regN;
always@(posedge clk)
if(reset)
regN<=0;
else if(regN<(M-1))
regN<=regN+1;
else
regN<=0;
assign qd=regN;
assign cout=(regN==(M-1))?1'b1:1'b0;
endmodule
常用时序逻辑电路设计实例
数码管扫描显示电路
单个数码管显示电路的设计。每个数码管 包括7个笔画和1个小圆点,需要8个IO口来进行控制。采用这种控制方式,当使用多 个数码管进行显示时,每个数码管都需要8个IO口
十六进制 数七段LED显示译码器
在实际应用中,为了减少 FPGA芯片IO口的使用数量,一般会采用分时复用的扫描显 示方案进行数码管驱动
以四个数码管显示为例,采用扫描显示方案时,四个数码管的 8个段码并接在一起,再 用4个IO口分别控制每个数码管的公共端,动态点亮数码管。这样只用12个IO口就可 以实现4个数码管的显示控制,比静态显示方式的32个IO口数量大大减少。
分时复用的扫描显示利用了人眼的视觉暂留特性,如果公共端控制信号的刷新速度足够快,人眼就不会区分出LED的闪烁,认为4个数码管是同时点亮

在最右端的数码管上显示"3"时,并接的段码信号为"00001101",4个公共端的控 制信号为"1110"。这种控制方式在同一时间只会点亮一个数码管,采用分时复用的模式轮流点亮 数码管,数码管扫描显示电路时序如图4.21所示

-
分时复用的数码管显示电路模块含有四个控制信号an3、an2、an1和an0,以及与控制信号一致的输出段信号sseg
-
控制信号的刷新频率必须足够快才能避免闪烁感,但也不能太快,以免影响数码管的开关切换,最佳工作频率为1000Hz左右
module scan_led_disp
(
input clk,reset,
input[7:0] in3,in2,in1,in0,
output reg[3:0] an,
output reg[7:0] sseg
);
localparam N=18; //对输入50MHz时钟进行分频(50MHz/2^16)
reg[N-1:0] regN;
always@(posedge clk,posedge reset)
if(reset)
regN<=0;
else
regN<=regN+1;
always@(*)
case(regN[N-1:N-2])
2'b00:
begin
an=4'b1110;
sseg=in0;
end
2'b01:
begin
an=4'b1101;
sseg=in1;
end
2'b10:
begin
an=4'b1011;
sseg=in2;
end
default:
begin
an=4'b0111;
sseg=in3;
end
endcase
endmodule
分频计数器(最关键)
localparam N=18; // 计数器位宽18位 reg[N-1:0] regN; // 18位计数器 always@(posedge clk,posedge reset) if(reset) regN<=0; else regN<=regN+1; // 时钟每来一次 +1作用:产生扫描时钟 + 分频
计数器从 0 → 2¹⁸-1 自动循环
取最高 2 位
regN[N-1:N-2]→ 00/01/10/11这 2 位用来 轮流切换 4 个数码管
case( regN[N-1:N-2] ) // 即:regN[17:16]
这两位变化一次,需要 2^16 个时钟因为低 16 位全部循环一遍,最高两位才变一次
数码管扫描频率 :50MHz / 2^16 ≈ 763Hz
每个数码管被点亮的频率:763 ÷ 4 = 190.7Hz
人眼 > 50Hz 就不闪烁
always@(*) case(regN[N-1:N-2]) // 取计数器最高2位:00 01 10 11 2'b00: begin an=4'b1110; // 点亮第0个数码管 sseg=in0; // 显示 in0 的段码 end 2'b01: begin an=4'b1101; // 点亮第1个数码管 sseg=in1; // 显示 in1 end 2'b10: begin an=4'b1011; // 点亮第2个数码管 sseg=in2; // 显示 in2 end default: begin an=4'b0111; // 点亮第3个数码管 sseg=in3; // 显示 in3 end endcase高速轮流点亮 4 个数码管:
- 00 → 亮第 0 个
- 01 → 亮第 1 个
- 10 → 亮第 2 个
- 11 → 亮第 3 个
为什么用
always *?因为
an和sseg是纯组合逻辑输出只要
regN变,输出立刻跟着变。为什么用计数器最高 2 位?
- 计数器不断 +1
- 最高 2 位变化最慢
- 变化频率 = 扫描频率(几百赫兹)
- 频率刚好适合数码管显示
什么是 "分时复用"?
4 个数码管共用一组 sseg 段码线
同一时间只亮一个,轮流亮 → 叫分时复用
节省 FPGA 引脚资源。
18 位计数器 = 分频器 + 扫描控制器
最高 2 位 = 轮流切换 4 个数码管
always 组合逻辑 = 快速切换选通和段码*
动态扫描 = 高速轮流点亮,视觉暂留形成同时亮
分时复用 = 4 个数码管共用 8 根段码线
七段式数码管常用于显示十六进制数字,为了能够利用上面的分时复用电路,我们需要四 个译码电路
另外一个更好的选择是首先输出多路十六进制数据,然后将其译码。这种方案只需要一个译码电 路,使4选1数据选择器的位宽从8位降为了5位(4位16进制数和1位小数点)
另外一个更好的选择是首先输出多路十六进制数据,然后将其译码。这种方案只需要一个译码电 路,使4选1数据选择器的位宽从8位降为了5位(4位16进制数和1位小数点)
module scan_led_hex_disp
(
input clk,reset,
input[3:0] hex3,hex2,hex1,hex0,
input[3:0] dp_in,
output reg[3:0] an,
output reg[7:0] sseg
);
localparam N=18; //对输入50MHz时钟进行分频(50MHz/2^16)
reg[N-1:0] regN;
reg[3:0] hex_in;
reg[3:0] dp;
always@(posedge clk,posedge reset)
if(reset)
regN<=0;
else
regN<=regN+1;
always@(*)
case(regN[N-1:N-2])
2'b00:
begin
an=4'b1110;
hex_in=hex0;
dp=dp_in[0];
end
2'b01:
begin
an=4'b1101;
hex_in=hex1;
dp=dp_in[1];
end
2'b10:
begin
an=4'b1011;
hex_in=hex2;
dp=dp_in[2];
end
default:
begin
an=4'b0111;
hex_in=hex3;
dp=dp_in[3];
end
endcase
always@(*)
begin
case(hex_in)
4'h0: sseg[6:0] = 7'b0000001;
4'h1: sseg[6:0] = 7'b1001111;
4'h2: sseg[6:0] = 7'b0010010;
4'h3: sseg[6:0] = 7'b0000110;
4'h4: sseg[6:0] = 7'b1001100;
4'h5: sseg[6:0] = 7'b0100100;
4'h6: sseg[6:0] = 7'b0100000;
4'h7: sseg[6:0] = 7'b0001111;
4'h8: sseg[6:0] = 7'b0000000;
4'h9: sseg[6:0] = 7'b0000100
4'ha: sseg[6:0] = 7'b0001000;
4'hb: sseg[6:0] = 7'b1100000;
4'hc: sseg[6:0] = 7'b0110001;
4'hd: sseg[6:0] = 7'b1000010;
4'he: sseg[6:0] = 7'b0110000;
default: sseg[6:0] = 7'b0111000; //4'hf
endcase
sseg[7]=dp;
end
endmodule
我们可以在实际的FPGA电路中验证该设计,把8位开关数据作为了两个4位无符号数据的输入,并使两 个数据相加,将其结果显示在四位七段式数码管上
module scan_led_hex_disp_test
(
input clk,
input[7:0] sw,
output[3:0] an,
output[7:0] sseg
);
wire[3:0] a,b;
wire[7:0] sum;
assign a=sw[3:0];
assign b=sw[7:4];
assign sum={4'b0,a}+{4'b0,b};
//实例化4位16进制数动态显示模块
scan_led_hex_disp scan_led_disp_unit
(.clk(clk),.reset(1'b0),.hex3(sum[7:4],.hex2(sum[3:0]),hex1(b),hex0(a),dp_in(4'b1011),an(an),sseg(sseg));
endmodule
秒表
秒表显示的时间分为三个十进制数字,从00.0到99.9秒循环计数。它包含一个同步清零信号clr,使秒表返回00.0,还包含一个启动信号go,开始或者暂停计数
module stop_watch
(
input clk, // 系统时钟 50MHz
input go, // 运行/暂停控制(1运行,0暂停)
input clr, // 同步清零(低电平有效)
output reg [3:0] d2, // 秒十位 0~9
output reg [3:0] d1, // 秒个位 0~9
output reg [3:0] d0 // 0.1秒位 0~9
);
// 50MHz时钟 → 100ms(0.1s)进位:计数上限 4999999
localparam COUNT_VALUE = 23'd4999999;
reg [22:0] cnt; // 0.1s分频计数器
wire tick_100ms; // 100ms 进位脉冲
// ====================== 1. 100ms 分频计数器 ======================
always @(posedge clk) begin
if(!clr) begin // 同步清零(低有效)
cnt <= 23'd0;
end
else if(go) begin // go=1 才计数
if(cnt == COUNT_VALUE)
cnt <= 23'd0;
else
cnt <= cnt + 1'd1;
end
// go=0 时保持原值,实现暂停
end
// 100ms 进位脉冲(高电平一个时钟周期)
assign tick_100ms = (cnt == COUNT_VALUE) ? 1'b1 : 1'b0;
// ====================== 2. 三位十进制计数逻辑 00.0~99.9 ======================
always @(posedge clk) begin
if(!clr) begin // 同步清零
d2 <= 4'd0;
d1 <= 4'd0;
d0 <= 4'd0;
end
else if(go && tick_100ms) begin // 运行且100ms到,才加1
// 最低位:0.1秒位 0~9
if(d0 != 4'd9) begin
d0 <= d0 + 1'd1;
end
else begin
d0 <= 4'd0;
// 中间位:秒个位 0~9
if(d1 != 4'd9) begin
d1 <= d1 + 1'd1;
end
else begin
d1 <= 4'd0;
// 最高位:秒十位 0~9
if(d2 != 4'd9) begin
d2 <= d2 + 1'd1;
end
else begin
d2 <= 4'd0; // 99.9 → 00.0 循环
end
end
end
end
end
endmodule
| 信号 | 位宽 | 功能 |
|---|---|---|
| clk | 1bit | 50MHz 系统时钟 |
| go | 1bit | 1 = 运行,0 = 暂停 |
| clr | 1bit | 0 = 清零,1 = 正常工作 |
| d2 | 4bit | 秒十位(0-9) |
| d1 | 4bit | 秒个位(0-9) |
| d0 | 4bit | 0.1 秒位(0-9) |
时钟分频(核心)
- 50MHz 时钟周期:20ns
- 目标:100ms 进位一次
- 计数次数 = 100ms / 20ns = 5000000
- 寄存器从 0 开始,所以上限 = 5000000 - 1 = 4999999
暂停 / 运行逻辑
go=1+tick_100ms→ 计时加 1go=0→ 所有寄存器保持原值 → 暂停
同步清零
clr=0时,所有计数器归 0 → 直接显示 00.0- 同步清零:只在时钟上升沿生效,稳定可靠
计数规则
-
d0:0~9 循环(0.1 秒) -
d0=9再加 1 → 归 0,d1加 1 -
d1=9再加 1 → 归 0,d2加 1 -
d2=9+d1=9+d0=9→ 全部归 0 → 循环module stop_watch_test
(
input clk,
input[1:0] btn, // btn[0]=clr清零, btn[1]=go启停
output[3:0] an, // 数码管选通
output[7:0] sseg // 段码+小数点(修复:删除错误分号)
);wire[3:0] d2,d1,d0; // 数码管扫描驱动实例化 scan_led_hex_disp scan_led_disp_unit( .clk(clk), .reset(btn[0]), // 清零按键复用为复位(更稳定) .hex3(4'b0), .hex2(d2), // 秒十位 .hex1(d1), // 秒个位 .hex0(d0), // 0.1秒位 .dp_in(4'b1011), // 仅第二个数码管点亮小数点 00.0 .an(an), .sseg(sseg) ); // 秒表计数器实例化(修复:删除重复的.d1(d1)) stop_watch counter_unit( .clk(clk), .go(btn[1]), .clr(btn[0]), .d2(d2), .d1(d1), .d0(d0) );endmodule
为了验证秒表电路,我们可以把它与前面的十六进制分时复用数码管电路结合,显示出秒表的输 出
可编程的方波信号发生器
一个可编程的方波发生器是可以产生用变量(逻辑1)和(逻辑0)表示的方波
指定的时间间隔由两个4比特的无符号整数控制信号m和n指定
打开和关闭的时间间隔是m*100ns和n*100ns
设计一个可编程方波发生器,核心要求:
- 输入:4bit 无符号整数
m(高电平持续时间:m * 100ns)、n(低电平持续时间:n * 100ns)- 输出:占空比由
m:n决定的方波,周期为(m+n)*100ns- 系统时钟:默认采用 FPGA 常用 100MHz(周期 10ns),100ns 对应 10 个时钟周期
- 功能:可通过按键输入
m/n,支持 FPGA 上板验证与示波器观测
设计思路:
1.时钟分频与计数
100MHz时钟周期为10ns,100ns=10个时钟周期,以此为基础计数:
- 高电平阶段:计数m*10个时钟周期
- 低电平阶段:计数n*10个时钟周期
- 状态切换:高电平计数满→切换低电平;低电平计数满→切换高电平,循环往复
2.状态机设计
采用有限状态机(FSM实现):
- S_HIGH:输出高电平,计数器从0递增到(m*10-1),满则切换到S_LOW
- S_LOW:输出低电平,计数器从0递增到(n*10-1),满则切换到S_HIGH
核心方波发生器模块 programmable_square_wave.v
module programmable_square_wave
#(
parameter CLK_FREQ=100_000_000,//系统时钟频率 100MHz
parameter TIME_UNIT=100_00_000 //时间单位 100ns=100_000_000ps
)
(
input clk, //系统时钟 100MHz
input rst_n, //异步复位,低电平有效
input[3:0] m, //高电平时间:m*100ns (0~15)
input[3:0] n, //低电平时间:n*100ns (0~15)
output reg square_out //方波输出
);
//计算100ns对应的时钟周期数:100ns/10ns=10个周期
localparam CNT_100NS=(TIME_UNIT*1_000_000_000)/CLK_PREQ; //固定为10
//状态定义
localparam S_HIGH=1'b1;
localparam S_LOW=1'b0;
reg current_state;
reg[3:0] cnt_100ns; //100ns计数器(0~9,共10个周期)
reg[3:0] cnt_m; //m计数(0~m-1)
reg[3:0] cnt_n; //n计数 (0~n-1)
//状态机:状态切换
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
current_state<=S_HIGH; //复位后从高电平开始
else
begin
case(current_state)
S_HIGH:
begin
if((cnt_m==m-1'b1) && (cnt_100ns == CNT_100NS - 1'b1))
current_state <= S_LOW;
else
current_state <= S_HIGH;
end
S_LOW:
begin
// 低电平计数满:n个100ns完成,切换到高电平
if((cnt_n == n - 1'b1) && (cnt_100ns == CNT_100NS - 1'b1))
current_state <= S_HIGH;
else
current_state <= S_LOW;
end
endcase
end
// 100ns基准计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_100ns <= 4'd0;
end
else begin
if(cnt_100ns == CNT_100NS - 1'b1)
cnt_100ns <= 4'd0;
else
cnt_100ns <= cnt_100ns + 1'b1;
end
end
// m计数器(高电平阶段计数)
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_m <= 4'd0;
end
else if(current_state == S_HIGH) begin
// 每完成1个100ns,m计数+1
if(cnt_100ns == CNT_100NS - 1'b1) begin
if(cnt_m == m - 1'b1)
cnt_m <= 4'd0;
else
cnt_m <= cnt_m + 1'b1;
end
end
else begin
cnt_m <= 4'd0; // 低电平阶段复位m计数
end
end
// n计数器(低电平阶段计数)
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_n <= 4'd0;
end
else if(current_state == S_LOW) begin
// 每完成1个100ns,n计数+1
if(cnt_100ns == CNT_100NS - 1'b1) begin
if(cnt_n == n - 1'b1)
cnt_n <= 4'd0;
else
cnt_n <= cnt_n + 1'b1;
end
end
else begin
cnt_n <= 4'd0; // 高电平阶段复位n计数
end
end
// 方波输出
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
square_out <= 1'b0;
end
else begin
square_out <= current_state;
end
end
endmodule
按键输入顶层模块 square_wave_top.v
module square_wave_top
(
input clk, // 系统时钟 100MHz
input rst_n, // 复位按键,低电平有效
input [3:0] key_m, // m输入按键(4个独立按键,0~15)
input [3:0] key_n, // n输入按键(4个独立按键,0~15)
output square_out // 方波输出,接示波器
);
// 按键消抖(可选,根据按键硬件特性添加)
wire [3:0] m_debounced, n_debounced;
// 若需要消抖,可实例化按键消抖模块,此处省略,直接连接
assign m_debounced = key_m;
assign n_debounced = key_n;
// 实例化方波发生器
programmable_square_wave u_square_wave
(
.clk(clk),
.rst_n(rst_n),
.m(m_debounced),
.n(n_debounced),
.square_out(square_out)
);
endmodule
Testbench 代码 tb_programmable_square_wave.v
module tb_programmable_square_wave();
reg clk;
reg rst_n;
reg [3:0] m;
reg [3:0] n;
wire square_out;
// 实例化DUT
programmable_square_wave uut
(
.clk(clk),
.rst_n(rst_n),
.m(m),
.n(n),
.square_out(square_out)
);
// 生成100MHz时钟
initial begin
clk = 1'b0;
forever #5 clk = ~clk; // 10ns周期
end
// 仿真激励
initial begin
// 初始化
rst_n = 1'b0;
m = 4'd2; // 高电平:2*100ns = 200ns
n = 4'd3; // 低电平:3*100ns = 300ns
#100;
rst_n = 1'b1;
// 仿真5us,观察波形
#5000;
// 切换m/n,验证可编程性
m = 4'd5; // 高电平500ns
n = 4'd5; // 低电平500ns(50%占空比)
#5000;
$finish;
end
endmodule
仿真预期结果
- 当
m=2, n=3时:方波周期 500ns,高电平 200ns,低电平 300ns,占空比 40% - 当
m=5, n=5时:方波周期 1000ns,高低电平各 500ns,占空比 50% - 仿真波形可通过 Vivado/ModelSim 查看,验证计数与状态切换逻辑
- 时钟:连接 FPGA 板载 100MHz 时钟源
- 复位:连接 1 个独立按键(低电平复位)
m/n输入:各用 4 个独立按键(拨码开关更稳定,避免按键抖动),对应 4bit 输入- 方波输出:连接 FPGA GPIO 引脚,接示波器探头
按键消抖优化(可选)
若使用机械按键,需添加按键消抖模块,避免 m/n 输入抖动导致方波异常,消抖模块代码如下:
module key_debounce
#(
parameter CNT_MAX = 20'd999_999 // 20ms消抖(100MHz时钟)
)(
input clk,
input rst_n,
input key_in,
output reg key_out
);
reg [19:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 20'd0;
key_out <= 1'b1;
end
else begin
if(key_in != key_out) begin
if(cnt < CNT_MAX)
cnt <= cnt + 1'b1;
else begin
cnt <= 20'd0;
key_out <= key_in;
end
end
else
cnt <= 20'd0;
end
end
endmodule
示波器观测
- 连接示波器到方波输出引脚,设置合适的时基(如 100ns/div)
- 输入不同
m/n值,验证方波周期、占空比是否符合预期:- 周期 =
(m + n) * 100ns - 占空比 =
m / (m + n) * 100%
- 周期 =
关键设计说明
1. 时钟兼容性
代码通过参数化设计,可适配不同时钟频率:
- 若使用 50MHz 时钟,修改
CLK_FREQ = 50_000_000,CNT_100NS自动计算为 5(5*20ns=100ns)- 无需修改核心逻辑,仅需调整参数
2. 边界条件处理
- 当
m=0或n=0时,方波输出固定电平(高 / 低),符合逻辑预期- 4bit 输入支持
m/n范围 0~15,最大周期(15+15)*100ns = 3us,可根据需求扩展位宽3. 时序优化
- 采用同步时序设计,所有寄存器在同一时钟沿更新,无异步逻辑,避免亚稳态
- 状态机采用一段式 / 三段式结合,逻辑清晰,时序收敛性好,适合 FPGA 综合
常见问题排查
方波频率异常 :检查时钟频率参数是否与板载时钟匹配,
CNT_100NS计算是否正确按键输入抖动:添加按键消抖模块,或使用拨码开关替代机械按键
方波输出毛刺:在输出端添加寄存器打拍,优化时序:
reg square_out_reg;
always @(posedge clk) square_out_reg <= current_state;
assign square_out = square_out_reg;
按键消抖模块key_debounce.v(通用1bit)
// 按键消抖模块:20ms消抖,适用于 50MHz 时钟
module key_debounce
(
input clk, // 50MHz 系统时钟
input rst_n, // 低电平复位
input key_in, // 原始按键输入(按下=0,松开=1)
output reg key_out // 消抖后稳定输出
);
parameter CNT_20MS = 19'd499_999; // 50MHz * 20ms = 100万周期
reg [18:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 19'd0;
key_out <= 1'b1; // 默认松开
end
else begin
if(key_in != key_out) begin
if(cnt == CNT_20MS) begin
cnt <= 19'd0;
key_out <= key_in;
end
else begin
cnt <= cnt + 1'b1;
end
end
else begin
cnt <= 19'd0;
end
end
end
endmodule
方波发生器核心模块programmable_square_wave.v
module programmable_square_wave
(
input clk,
input rst_n,
input [3:0] m, // 高电平时间 m*100ns
input [3:0] n, // 低电平时间 n*100ns
output reg square_out
);
localparam CNT_100NS = 5'd49; // 50MHz → 100ns 计数
reg [4:0] cnt_100ns;
reg [1:0] state;
localparam HIGH = 1'b0;
localparam LOW = 1'b1;
reg [3:0] cnt_h;
reg [3:0] cnt_l;
// 100ns 基准计时器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_100ns <= 5'd0;
else if(cnt_100ns == CNT_100NS)
cnt_100ns <= 5'd0;
else
cnt_100ns <= cnt_100ns + 1'b1;
end
wire tick_100ns = (cnt_100ns == CNT_100NS);
// 状态机
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state <= HIGH;
cnt_h <= 4'd0;
cnt_l <= 4'd0;
end
else begin
case(state)
HIGH: begin
square_out <= 1'b1;
if(tick_100ns) begin
if(cnt_h == m-1) begin
state <= LOW;
cnt_h <= 4'd0;
end
else
cnt_h <= cnt_h + 1'b1;
end
end
LOW: begin
square_out <= 1'b0;
if(tick_100ns) begin
if(cnt_l == n-1) begin
state <= HIGH;
cnt_l <= 4'd0;
end
else
cnt_l <= cnt_l + 1'b1;
end
end
endcase
end
end
endmodule
square_wave_top.v
module square_wave_top
(
input clk, // 50MHz 时钟
input rst_n, // 复位按键(低电平有效)
input [3:0] key_m, // m 控制按键组(4bit)
input [3:0] key_n, // n 控制按键组(4bit)
output square_out // 方波输出(接示波器)
);
wire [3:0] m_debounced;
wire [3:0] n_debounced;
// ====================== m[3:0] 四路消抖 ======================
key_debounce u_m0 (.clk(clk), .rst_n(rst_n), .key_in(key_m[0]), .key_out(m_debounced[0]));
key_debounce u_m1 (.clk(clk), .rst_n(rst_n), .key_in(key_m[1]), .key_out(m_debounced[1]));
key_debounce u_m2 (.clk(clk), .rst_n(rst_n), .key_in(key_m[2]), .key_out(m_debounced[2]));
key_debounce u_m3 (.clk(clk), .rst_n(rst_n), .key_in(key_m[3]), .key_out(m_debounced[3]));
// ====================== n[3:0] 四路消抖 ======================
key_debounce u_n0 (.clk(clk), .rst_n(rst_n), .key_in(key_n[0]), .key_out(n_debounced[0]));
key_debounce u_n1 (.clk(clk), .rst_n(rst_n), .key_in(key_n[1]), .key_out(n_debounced[1]));
key_debounce u_n2 (.clk(clk), .rst_n(rst_n), .key_in(key_n[2]), .key_out(n_debounced[2]));
key_debounce u_n3 (.clk(clk), .rst_n(rst_n), .key_in(key_n[3]), .key_out(n_debounced[3]));
// ====================== 方波发生器实例化 ======================
programmable_square_wave u_square
(
.clk(clk),
.rst_n(rst_n),
.m(~m_debounced), // ~ 取反:按键按下=1,更符合直觉
.n(~n_debounced),
.square_out(square_out)
);
endmodule
按键规则
按下按键 = 对应位为 1
松开 = 0
消抖时间:20ms
m[3:0]:控制高电平持续时间(0~15)×100ns
n[3:0]:控制低电平持续时间(0~15)×100ns
- 输出方波公式
周期 = (m + n) × 100ns
高电平 = m × 100ns
低电平 = n × 100ns
- 时钟
默认适配 50MHz 时钟(FPGA 最常用)
- 把 3 个文件 全部加入工程
- 绑定引脚:clk、rst_n、key_m [3:0]、key_n [3:0]、square_out
- 综合 → 实现 → 下载
- 用示波器看
square_out
pwm和LED调光器
方波的占空比表示在一个周期内高电平(逻辑1)的百分比。PWM(脉冲宽度调制)电路可以输 出一个可变占空比的方波。一个4比特分辨率PWM中,4比特控制信号w指定占空比。w信号是一个 无符号整数和占空比为
1.设计一个4比特分辨率的PWM电路,写出程序并且仿真。
2.将程序下载到FPGA板上用示波器进行验证。
3.在分时复用LED电路的基础上增加一个信号,并加入PWM电路。PWM电路指定LED点亮的时间 百分比。我们可以通过改变占空比控制LED的亮度。可以通过观察示波器验证电路的操作。
设计一个4bit 分辨率 PWM 电路,核心要求:
- 占空比由 4bit 无符号整数
w控制,占空比 =w / 16(w范围 0~15,对应 0%~93.75% 占空比)- 支持 FPGA 上板,通过示波器验证波形
- 结合分时复用 LED 电路,实现 PWM 调光(通过占空比控制 LED 亮度)
- 系统时钟默认采用 FPGA 常用50MHz(周期 20ns)
核心设计原理
- PWM 基本原理
PWM(脉冲宽度调制)通过在固定周期内调整高电平持续时间,实现占空比控制:
-
周期固定为 16 个基准计数周期(4bit 分辨率)
-
当计数器值 <
w时,输出高电平;否则输出低电平 -
占空比 = 高电平时间 / 周期 =
w / 16
- 时钟分频与基准计数
50MHz 时钟周期 20ns,为保证 PWM 频率人眼无闪烁(>50Hz),设计 PWM 周期为1ms:
-
1ms = 50000 个 50MHz 时钟周期
-
16 个基准计数周期对应 1ms → 每个基准周期 = 1ms / 16 = 62.5μs
-
基准计数器:50MHz → 62.5μs 对应计数 3124(0~3124,共 3125 个周期)
4bit PWM 核心模块 pwm_4bit.v
module pwm_4bit
#(
parameter CLK_FREQ = 50_000_000, // 系统时钟50MHz
parameter PWM_FREQ = 1000 // PWM频率1kHz(周期1ms)
)(
input clk, // 系统时钟
input rst_n, // 异步复位,低电平有效
input [3:0] w, // 4bit占空比控制信号(0~15),占空比w/16
output reg pwm_out // PWM输出
);
// 计算参数:PWM周期对应时钟数 = CLK_FREQ / PWM_FREQ = 50000
localparam PWM_PERIOD = CLK_FREQ / PWM_FREQ;
// 4bit分辨率,每个步长对应时钟数 = PWM_PERIOD / 16 = 3125
localparam STEP_CNT = PWM_PERIOD / 16;
// 计数器:0~PWM_PERIOD-1,用于生成PWM周期
reg [15:0] cnt;
// 周期计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 16'd0;
else if(cnt == PWM_PERIOD - 1'b1)
cnt <= 16'd0;
else
cnt <= cnt + 1'b1;
end
// PWM输出逻辑:cnt < w*STEP_CNT 时输出高电平,否则低电平
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
pwm_out <= 1'b0;
else
pwm_out <= (cnt < w * STEP_CNT) ? 1'b1 : 1'b0;
end
endmodule
按键消抖模块 key_debounce.v(用于输入 w)
module key_debounce
#(
parameter CNT_MAX = 19'd499_999 // 20ms消抖,50MHz时钟
)(
input clk,
input rst_n,
input key_in,
output reg key_out
);
reg [18:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 19'd0;
key_out <= 1'b1; // 默认松开(高电平)
end
else begin
if(key_in != key_out) begin
if(cnt == CNT_MAX) begin
cnt <= 19'd0;
key_out <= key_in;
end
else
cnt <= cnt + 1'b1;
end
else
cnt <= 19'd0;
end
end
endmodule
分时复用 LED+PWM 调光顶层模块 led_pwm_top.v
实现 4 位数码管分时复用 + PWM 调光,通过w控制 LED 亮度:
module led_pwm_top
(
input clk, // 50MHz系统时钟
input rst_n, // 复位按键,低电平有效
input [3:0] key_w, // 4bit占空比控制按键w(0~15)
output [3:0] an, // 4位数码管位选
output [7:0] sseg, // 数码管段码+小数点
output led_pwm // PWM控制LED输出
);
// 内部信号
wire [3:0] w_debounced;
wire pwm_out;
wire [3:0] hex_data;
// ====================== 4路按键消抖 ======================
key_debounce u_w0(.clk(clk), .rst_n(rst_n), .key_in(key_w[0]), .key_out(w_debounced[0]));
key_debounce u_w1(.clk(clk), .rst_n(rst_n), .key_in(key_w[1]), .key_out(w_debounced[1]));
key_debounce u_w2(.clk(clk), .rst_n(rst_n), .key_in(key_w[2]), .key_out(w_debounced[2]));
key_debounce u_w3(.clk(clk), .rst_n(rst_n), .key_in(key_w[3]), .key_out(w_debounced[3]));
// ====================== PWM模块实例化 ======================
pwm_4bit u_pwm
(
.clk(clk),
.rst_n(rst_n),
.w(~w_debounced), // 按键按下为低电平,取反后w=0~15
.pwm_out(pwm_out)
);
// ====================== 数码管显示w值(分时复用) ======================
// 数码管扫描驱动(复用之前秒表项目的模块)
scan_led_hex_disp u_disp
(
.clk(clk),
.reset(~rst_n),
.hex3(4'd0),
.hex2(4'd0),
.hex1({3'b000, ~w_debounced[3]}), // 显示w的二进制/十进制
.hex0(~w_debounced),
.dp_in(4'b1110),
.an(an),
.sseg(sseg)
);
// ====================== LED PWM输出 ======================
assign led_pwm = pwm_out;
endmodule
数码管扫描驱动 scan_led_hex_disp.v(复用优化版)
module scan_led_hex_disp
(
input clk,reset,
input[3:0] hex3,hex2,hex1,hex0,
input[3:0] dp_in,
output reg[3:0] an,
output reg[7:0] sseg
);
localparam N=18; // 50MHz分频,扫描频率约190Hz,无闪烁
reg[N-1:0] regN;
reg[3:0] hex_in;
reg dp;
// 分频计数器
always@(posedge clk or posedge reset)
if(reset)
regN <= 0;
else
regN <= regN + 1;
// 数码管扫描选通
always@(*)
case(regN[N-1:N-2])
2'b00: begin an=4'b1110; hex_in=hex0; dp=dp_in[0]; end
2'b01: begin an=4'b1101; hex_in=hex1; dp=dp_in[1]; end
2'b10: begin an=4'b1011; hex_in=hex2; dp=dp_in[2]; end
default: begin an=4'b0111; hex_in=hex3; dp=dp_in[3]; end
endcase
// 七段译码
always@(*)
begin
case(hex_in)
4'h0: sseg[6:0] = 7'b0000001;
4'h1: sseg[6:0] = 7'b1001111;
4'h2: sseg[6:0] = 7'b0010010;
4'h3: sseg[6:0] = 7'b0000110;
4'h4: sseg[6:0] = 7'b1001100;
4'h5: sseg[6:0] = 7'b0100100;
4'h6: sseg[6:0] = 7'b0100000;
4'h7: sseg[6:0] = 7'b0001111;
4'h8: sseg[6:0] = 7'b0000000;
4'h9: sseg[6:0] = 7'b0000100;
4'ha: sseg[6:0] = 7'b0001000;
4'hb: sseg[6:0] = 7'b1100000;
4'hc: sseg[6:0] = 7'b0110001;
4'hd: sseg[6:0] = 7'b1000010;
4'he: sseg[6:0] = 7'b0110000;
default: sseg[6:0] = 7'b0111000;
endcase
sseg[7] = ~dp; // 低电平点亮小数点
end
endmodule
tb_pwm_4bit.v
module tb_pwm_4bit();
reg clk;
reg rst_n;
reg [3:0] w;
wire pwm_out;
// 实例化DUT
pwm_4bit uut
(
.clk(clk),
.rst_n(rst_n),
.w(w),
.pwm_out(pwm_out)
);
// 生成50MHz时钟(周期20ns)
initial begin
clk = 1'b0;
forever #10 clk = ~clk;
end
// 仿真激励
initial begin
// 初始化
rst_n = 1'b0;
w = 4'd8; // 50%占空比
#100;
rst_n = 1'b1;
// 仿真10ms,观察波形
#10_000_000;
// 切换w值,验证占空比变化
w = 4'd4; // 25%占空比
#10_000_000;
w = 4'd12; // 75%占空比
#10_000_000;
$finish;
end
endmodule
仿真预期结果
| w 值 | 占空比 | 高电平时间 | 低电平时间 |
|---|---|---|---|
| 0 | 0% | 0 | 1ms |
| 8 | 50% | 0.5ms | 0.5ms |
| 15 | 93.75% | 0.9375ms | 0.0625ms |
硬件连接
时钟:连接 FPGA 板载 50MHz 时钟源
复位:1 个独立按键(低电平复位)
w输入:4 个独立按键 / 拨码开关(对应 4bit 输入)PWM 输出:连接 FPGA GPIO 引脚,接示波器探头
LED:将 PWM 输出接 LED 驱动电路(共阳极 / 共阴极需适配极性)
数码管:连接 4 位共阴 / 共阳数码管(对应
an和sseg引脚)
示波器验证步骤
下载程序到 FPGA,复位电路
示波器设置:时基 1ms/div,电压 5V/div
输入不同
w值,验证占空比:
w=8:方波周期 1ms,高低电平各 0.5ms,占空比 50%
w=4:高电平 0.25ms,低电平 0.75ms,占空比 25%
w=15:高电平 0.9375ms,低电平 0.0625ms,占空比 93.75%LED 调光验证
w=0:LED 完全熄灭(0% 占空比)
w=8:LED 中等亮度(50% 占空比)
w=15:LED 最亮(93.75% 占空比)人眼无闪烁,亮度随
w线性变化
LED循环电路
开发板有四个七段式LED数码管,因此一次只能显示四个符号。我们通过将数据不断旋转和移 动就可以显示更多的信息。例如,假设输入信息是10位数(如"0123456789"),可以将其显示为 "0123""1234""2345"......"6789""7890""0123"
-
电路的输入信号en进行启用或暂停旋转,输入信号dir指定方向(向左或向右)。设计程序并且下载16 到FPGA板上进行验证。
-
用按键控制循环,按一下显示下一组数。设计程序并且下载到FPGA板上进行验证。
基于 4 位七段数码管,实现长数据滚动显示,核心要求:
- 自动滚动模式 :
- 输入
en控制启停(1 = 滚动,0 = 暂停)- 输入
dir控制方向(1 = 左移,0 = 右移)- 示例数据
0123456789,滚动显示0123→1234→2345...→7890→0123循环- 按键步进模式 :
- 每按一次按键,显示下一组 4 位数据
- 支持循环往复,按到末尾后回到开头
核心设计思路
- 数据结构
存储 10 位待显示数据:
{4'd0,4'd1,4'd2,4'd3,4'd4,4'd5,4'd6,4'd7,4'd8,4'd9}用一个 **4bit 偏移寄存器
pos** 控制当前显示窗口:
pos=0:显示data[39:36,35:32,31:28,27:24]→0 1 2 3
pos=1:显示data[35:32,31:28,27:24,23:20]→1 2 3 4...
pos=9:显示data[7:4,3:0,39:36,35:32]→7 8 9 0
pos=10:回到pos=0循环
- 时钟与分频
系统时钟:50MHz(FPGA 常用)
滚动刷新频率:0.5Hz(每 2 秒滚动一次,人眼舒适)
分频计数器:
50MHz / 0.5Hz = 100_000_000,计数到99_999_999产生滚动脉冲
- 按键消抖
机械按键添加 20ms 消抖,避免误触发
消抖后产生单脉冲,用于步进模式的位置更新
通用按键消抖模块 key_debounce.v
module key_debounce
#(
parameter CNT_MAX = 19'd499_999 // 20ms消抖,50MHz时钟
)(
input clk,
input rst_n,
input key_in,
output reg key_out,
output reg key_pulse // 消抖后单脉冲输出(按键按下时产生1个时钟周期高电平)
);
reg [18:0] cnt;
reg key_sync;
// 两级同步,避免亚稳态
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_sync <= 1'b1;
key_out <= 1'b1;
cnt <= 19'd0;
key_pulse <= 1'b0;
end
else begin
key_sync <= key_in;
if(key_sync != key_out) begin
if(cnt == CNT_MAX) begin
cnt <= 19'd0;
key_out <= key_sync;
key_pulse <= ~key_sync; // 按键按下(低变高)产生脉冲
end
else
cnt <= cnt + 1'b1;
end
else begin
cnt <= 19'd0;
key_pulse <= 1'b0;
end
end
end
endmodule
数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版)
module scan_led_hex_disp
(
input clk,reset,
input[3:0] hex3,hex2,hex1,hex0,
input[3:0] dp_in,
output reg[3:0] an,
output reg[7:0] sseg
);
localparam N=18; // 50MHz分频,扫描频率约190Hz,无闪烁
reg[N-1:0] regN;
reg[3:0] hex_in;
reg dp;
// 分频计数器
always@(posedge clk or posedge reset)
if(reset)
regN <= 0;
else
regN <= regN + 1;
// 数码管扫描选通
always@(*)
case(regN[N-1:N-2])
2'b00: begin an=4'b1110; hex_in=hex0; dp=dp_in[0]; end
2'b01: begin an=4'b1101; hex_in=hex1; dp=dp_in[1]; end
2'b10: begin an=4'b1011; hex_in=hex2; dp=dp_in[2]; end
default: begin an=4'b0111; hex_in=hex3; dp=dp_in[3]; end
endcase
// 七段译码(共阴数码管,低电平点亮)
always@(*)
begin
case(hex_in)
4'h0: sseg[6:0] = 7'b0000001;
4'h1: sseg[6:0] = 7'b1001111;
4'h2: sseg[6:0] = 7'b0010010;
4'h3: sseg[6:0] = 7'b0000110;
4'h4: sseg[6:0] = 7'b1001100;
4'h5: sseg[6:0] = 7'b0100100;
4'h6: sseg[6:0] = 7'b0100000;
4'h7: sseg[6:0] = 7'b0001111;
4'h8: sseg[6:0] = 7'b0000000;
4'h9: sseg[6:0] = 7'b0000100;
default: sseg[6:0] = 7'b1111111; // 灭灯
endcase
sseg[7] = ~dp; // 低电平点亮小数点
end
endmodule
自动滚动 + 按键步进 顶层模块 led_scroll_top.v
module led_scroll_top
(
input clk, // 50MHz系统时钟
input rst_n, // 异步复位,低电平有效
input en, // 滚动启停控制(1=滚动,0=暂停)
input dir, // 滚动方向(1=左移,0=右移)
input key_step, // 步进按键(每按一次,显示下一组)
output [3:0] an, // 4位数码管位选
output [7:0] sseg // 数码管段码
);
// ====================== 内部参数与信号 ======================
// 10位待显示数据:0 1 2 3 4 5 6 7 8 9
localparam [39:0] DATA = {4'd0,4'd1,4'd2,4'd3,4'd4,4'd5,4'd6,4'd7,4'd8,4'd9};
localparam DATA_LEN = 4'd10; // 数据长度10
localparam SCROLL_FREQ = 2; // 滚动频率0.5Hz(2秒一次)
localparam CNT_SCROLL = 50_000_000 / SCROLL_FREQ - 1; // 50MHz→2秒计数
reg [25:0] cnt_scroll;
wire tick_scroll; // 滚动脉冲
reg [3:0] pos; // 当前显示位置(0~9)
wire [3:0] hex3, hex2, hex1, hex0; // 当前显示的4位数据
wire key_step_pulse; // 按键消抖后单脉冲
// ====================== 按键消抖 ======================
key_debounce u_key_step
(
.clk(clk),
.rst_n(rst_n),
.key_in(key_step),
.key_out(),
.key_pulse(key_step_pulse)
);
// ====================== 滚动计数器 ======================
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_scroll <= 26'd0;
else if(cnt_scroll == CNT_SCROLL)
cnt_scroll <= 26'd0;
else
cnt_scroll <= cnt_scroll + 1'b1;
end
assign tick_scroll = (cnt_scroll == CNT_SCROLL);
// ====================== 位置控制逻辑 ======================
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
pos <= 4'd0;
else begin
// 优先级:按键步进 > 自动滚动
if(key_step_pulse) begin
// 按键按下,位置+1(左移)
if(pos == DATA_LEN - 1'b1)
pos <= 4'd0;
else
pos <= pos + 1'b1;
end
else if(en) begin // 自动滚动模式
if(tick_scroll) begin
if(dir) begin // dir=1:左移(位置+1)
if(pos == DATA_LEN - 1'b1)
pos <= 4'd0;
else
pos <= pos + 1'b1;
end
else begin // dir=0:右移(位置-1)
if(pos == 4'd0)
pos <= DATA_LEN - 1'b1;
else
pos <= pos - 1'b1;
end
end
end
// en=0时,pos保持不变,暂停滚动
end
end
// ====================== 数据窗口提取 ======================
assign {hex3, hex2, hex1, hex0} =
(pos == 4'd0) ? DATA[39:24] : // 0 1 2 3
(pos == 4'd1) ? DATA[35:20] : // 1 2 3 4
(pos == 4'd2) ? DATA[31:16] : // 2 3 4 5
(pos == 4'd3) ? DATA[27:12] : // 3 4 5 6
(pos == 4'd4) ? DATA[23:8] : // 4 5 6 7
(pos == 4'd5) ? DATA[19:4] : // 5 6 7 8
(pos == 4'd6) ? {DATA[15:4], DATA[39:36]} : // 6 7 8 9
(pos == 4'd7) ? {DATA[11:4], DATA[39:32]} : // 7 8 9 0
(pos == 4'd8) ? {DATA[7:4], DATA[39:28]} : // 8 9 0 1
(pos == 4'd9) ? {DATA[3:0], DATA[39:24]} : // 9 0 1 2
16'h0000; // 默认灭灯
// ====================== 数码管显示 ======================
scan_led_hex_disp u_disp
(
.clk(clk),
.reset(~rst_n),
.hex3(hex3),
.hex2(hex2),
.hex1(hex1),
.hex0(hex0),
.dp_in(4'b1111), // 全灭小数点
.an(an),
.sseg(sseg)
);
endmodule
自动滚动模式
| 信号 | 功能 |
|---|---|
en=1 |
启动自动滚动,每 2 秒切换一次显示 |
en=0 |
暂停滚动,保持当前显示内容 |
dir=1 |
左移滚动(0123→1234→2345...) |
dir=0 |
右移滚动(0123→9012→8901...) |
按键步进模式
- 每按一次
key_step按键,显示下一组 4 位数据 - 滚动到末尾后自动回到开头,循环往复
- 按键优先级高于自动滚动,按下按键时立即切换,不受滚动周期限制
显示序列(左移)
0123 → 1234 → 2345 → 3456 → 4567 → 5678 → 6789 → 7890 → 8901 → 9012 → 0123...
验证步骤
1.下载程序到 FPGA,复位电路
2.自动滚动验证:
en=1, dir=1:观察数码管从0123开始左移滚动en=1, dir=0:观察数码管右移滚动en=0:滚动暂停,保持当前显示3.按键步进验证:
en=0,每按一次key_step,显示切换到下一组- 按到
9012后,再次按下回到0123,循环正常关键设计优化
- 数据提取逻辑
用
case语句实现窗口提取,逻辑清晰,可直接修改DATA参数更换显示内容支持任意长度数据(只需修改
DATA_LEN和case分支)
- 优先级设计
按键步进优先级高于自动滚动,避免两种模式冲突
消抖模块添加两级同步,避免亚稳态,保证按键稳定
- 可扩展性
可修改
SCROLL_FREQ参数调整滚动速度可扩展为任意长度数据(如 16 位、32 位),只需修改
DATA和pos位宽可添加更多按键功能(如加速 / 减速、暂停 / 继续)
常见问题排查
- 滚动速度异常 :检查
SCROLL_FREQ参数,确认时钟频率匹配- 按键误触发 :调整消抖时间
CNT_MAX,延长消抖时间- 显示乱码:检查数码管共阴 / 共阳极性,段码是否正确
- 滚动方向错误 :检查
dir控制逻辑,确认左移 / 右移的位置增减
增强秒表
在秒表的设计基础上进行扩展
-
添加一个额外的信号up,来控制计数的方向。当up有效时,秒表进行正方向计时,否则进行倒 计时。设计程序并且下载到FPGA开发板上验证。
-
添加分钟数字显示。LED显示格式为M.SS.D,D代表0.1秒和它的范围是0到9之间。SS表示秒, 16 其范围是00和59之间。M代表分钟,它的范围是0到9之间。设计程序并且下载到FPGA开发板上 验证。
在原有秒表基础上,实现两个核心扩展:
方向控制 :添加
up信号,up=1正计时,up=0倒计时格式扩展 :显示格式
M.SS.D,范围:
M:分钟(0~9)SS:秒(00~59)D:0.1 秒(0~9)保留原有功能:
go启停、clr同步清零、4 位数码管动态显示
核心设计思路时钟分频
系统时钟:50MHz(周期 20ns)
0.1s 基准:
50MHz × 0.1s = 5,000,000个周期,计数器0~4,999,999100ms 进位脉冲:
cnt == 4,999,999时产生tick_100ms
- 计数逻辑
正计时(up=1):0.1s 位 0→9→0,秒个位 0→9→0,秒十位 0→5→0,分钟 0→9→0 循环
倒计时(up=0):0.1s 位 9→0→9,秒个位 9→0→9,秒十位 5→0→5,分钟 9→0→9 循环
边界处理:正计时到
9.59.9归零,倒计时到0.00.0保持 / 归零
- 显示格式
4 位数码管:
M . S S D→ 对应hex3=M,hex2=S1,hex1=S0,hex0=D小数点:
dp_in=4'b0111,仅分钟后点亮小数点
增强秒表核心模块 enhanced_stop_watch.v
module enhanced_stop_watch
(
input clk, // 50MHz系统时钟
input go, // 启停控制(1=运行,0=暂停)
input clr, // 同步清零(低电平有效)
input up, // 计数方向(1=正计时,0=倒计时)
output reg [3:0] m, // 分钟 M (0~9)
output reg [3:0] s1, // 秒十位 S1 (0~5)
output reg [3:0] s0, // 秒个位 S0 (0~9)
output reg [3:0] d // 0.1秒 D (0~9)
);
// 50MHz → 100ms 计数上限:4,999,999
localparam CNT_100MS = 23'd4_999_999;
reg [22:0] cnt;
wire tick_100ms;
// ====================== 100ms 分频计数器 ======================
always @(posedge clk) begin
if(!clr) begin
cnt <= 23'd0;
end
else if(go) begin
if(cnt == CNT_100MS)
cnt <= 23'd0;
else
cnt <= cnt + 1'd1;
end
// go=0时保持,实现暂停
end
assign tick_100ms = (cnt == CNT_100MS);
// ====================== 正/倒计时核心逻辑 ======================
always @(posedge clk) begin
if(!clr) begin
// 同步清零:0.00.0
m <= 4'd0;
s1 <= 4'd0;
s0 <= 4'd0;
d <= 4'd0;
end
else if(go && tick_100ms) begin
if(up) begin
// ========== 正计时逻辑 ==========
if(d != 4'd9) begin
d <= d + 1'd1;
end
else begin
d <= 4'd0;
if(s0 != 4'd9) begin
s0 <= s0 + 1'd1;
end
else begin
s0 <= 4'd0;
if(s1 != 4'd5) begin
s1 <= s1 + 1'd1;
end
else begin
s1 <= 4'd0;
if(m != 4'd9) begin
m <= m + 1'd1;
end
else begin
m <= 4'd0; // 9.59.9 → 0.00.0 循环
end
end
end
end
end
else begin
// ========== 倒计时逻辑 ==========
if(d != 4'd0) begin
d <= d - 1'd1;
end
else begin
d <= 4'd9;
if(s0 != 4'd0) begin
s0 <= s0 - 1'd1;
end
else begin
s0 <= 4'd9;
if(s1 != 4'd0) begin
s1 <= s1 - 1'd1;
end
else begin
s1 <= 4'd5;
if(m != 4'd0) begin
m <= m - 1'd1;
end
else begin
m <= 4'd9; // 0.00.0 → 9.59.9 循环
end
end
end
end
end
end
end
endmodule
数码管扫描驱动模块 scan_led_hex_disp.v(复用优化版)
module scan_led_hex_disp
(
input clk,reset,
input[3:0] hex3,hex2,hex1,hex0,
input[3:0] dp_in,
output reg[3:0] an,
output reg[7:0] sseg
);
localparam N=18; // 50MHz分频,扫描频率约190Hz,无闪烁
reg[N-1:0] regN;
reg[3:0] hex_in;
reg dp;
// 分频计数器
always@(posedge clk or posedge reset)
if(reset)
regN <= 0;
else
regN <= regN + 1;
// 数码管扫描选通
always@(*)
case(regN[N-1:N-2])
2'b00: begin an=4'b1110; hex_in=hex0; dp=dp_in[0]; end
2'b01: begin an=4'b1101; hex_in=hex1; dp=dp_in[1]; end
2'b10: begin an=4'b1011; hex_in=hex2; dp=dp_in[2]; end
default: begin an=4'b0111; hex_in=hex3; dp=dp_in[3]; end
endcase
// 七段译码(共阴数码管,低电平点亮)
always@(*)
begin
case(hex_in)
4'h0: sseg[6:0] = 7'b0000001;
4'h1: sseg[6:0] = 7'b1001111;
4'h2: sseg[6:0] = 7'b0010010;
4'h3: sseg[6:0] = 7'b0000110;
4'h4: sseg[6:0] = 7'b1001100;
4'h5: sseg[6:0] = 7'b0100100;
4'h6: sseg[6:0] = 7'b0100000;
4'h7: sseg[6:0] = 7'b0001111;
4'h8: sseg[6:0] = 7'b0000000;
4'h9: sseg[6:0] = 7'b0000100;
default: sseg[6:0] = 7'b1111111; // 灭灯
endcase
sseg[7] = ~dp; // 低电平点亮小数点
end
endmodule
顶层测试模块 enhanced_stop_watch_top.v
module enhanced_stop_watch_top
(
input clk, // 50MHz系统时钟
input [2:0] btn, // btn[0]=clr清零, btn[1]=go启停, btn[2]=up方向
output [3:0] an, // 4位数码管位选
output [7:0] sseg // 数码管段码+小数点
);
wire [3:0] m, s1, s0, d;
// 实例化增强秒表
enhanced_stop_watch u_stop_watch
(
.clk(clk),
.go(btn[1]),
.clr(btn[0]),
.up(btn[2]),
.m(m),
.s1(s1),
.s0(s0),
.d(d)
);
// 实例化数码管驱动(显示格式 M . S S D)
scan_led_hex_disp u_disp
(
.clk(clk),
.reset(~btn[0]),
.hex3(m), // 分钟 M
.hex2(s1), // 秒十位 S
.hex1(s0), // 秒个位 S
.hex0(d), // 0.1秒 D
.dp_in(4'b0111),// 仅hex3(分钟)后点亮小数点 → M. SS D
.an(an),
.sseg(sseg)
);
endmodule
- 按键功能
| 按键 | 功能 |
|---|---|
btn[0] |
同步清零(低电平有效),秒表归0.00.0 |
btn[1] |
启停控制(1 = 运行,0 = 暂停) |
btn[2] |
方向控制(1 = 正计时,0 = 倒计时) |
- 显示格式
- 4 位数码管显示:
M . S S D,例如1.23.4表示 1 分 23.4 秒 - 范围:
0.00.0~9.59.9,正 / 倒计时循环
- 计数逻辑
- 正计时 :
0.00.0→0.00.1→ ... →9.59.9→0.00.0循环 - 倒计时 :
9.59.9→9.59.8→ ... →0.00.0→9.59.9循环 - 暂停:
go=0时,所有计数器保持当前值,秒表暂停
- 硬件连接
信号 引脚连接 clk板载 50MHz 时钟 btn[0]清零按键(低电平有效) btn[1]启停按键 btn[2]方向按键 / 拨码开关 an[3:0]4 位数码管位选 sseg[7:0]数码管段码(a~g + 小数点)
验证步骤
清零测试 :按下
btn[0],显示0.00.0正计时测试 :
btn[2]=1,按下btn[1],秒表从0.00.0开始正计时,到9.59.9后归零循环倒计时测试 :
btn[2]=0,按下btn[1],秒表从9.59.9开始倒计时,到0.00.0后回到9.59.9循环暂停测试 :运行中松开
btn[1],秒表暂停,再次按下继续计时关键设计优化
- 边界处理
正计时到
9.59.9自动归零,避免溢出倒计时到
0.00.0自动回到9.59.9,实现循环同步清零,仅在时钟上升沿生效,无亚稳态风险
- 显示优化
小数点位置精准,符合
M.SS.D格式数码管扫描频率 190Hz,无闪烁,人眼舒适
- 可扩展性
可修改
CNT_100MS参数适配不同时钟频率可扩展分钟范围(0~99),只需增加位宽和计数逻辑
可添加按键消抖模块,避免机械按键误触发
常见问题排查
计数速度异常 :检查
CNT_100MS参数,确认时钟频率匹配显示乱码:检查数码管共阴 / 共阳极性,段码是否正确
方向切换异常 :检查
up信号逻辑,确认正 / 倒计时分支正确清零不生效 :检查
clr信号极性,确认同步清零逻辑
