1. 项目需求
通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。
使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。秒表应从0.0秒计数到9.9秒,然后翻转,计数值每0.1秒精确更新一次。
秒表使用四个按钮输入:开始、停止、增量和清除(重置)。开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次);停止输入使计数器停止递增,但使数码管显示当前计数器值;每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间;复位/清除输入强制计数器值为零。
2. 完成的功能及达到的性能
2.1 计时器的计时功能实现
基本实现了计时器的基本功能,可以实现从0.0到9.9的计时功能,并且在0.0时单色LED闪亮
2.2 四种按键功能的实现
右侧三个按键依次实现了开始、停止,增量和清除(重置)功能,开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次); 停止输入使计数器停止递增,但使数码管显示当前计数器值; 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间; 复位/清除输入强制计数器值为零。
2.3 功能框图
3. 实现思路
1.首先,需要一个时钟信号来驱动计数器。可以使用FPGA上的时钟模块或外部时钟源来提供稳定的时钟信号。
2.设计一个计数器模块,该模块可以根据时钟信号进行递增操作。可以使用FPGA上的计数器资源或者自己设计一个计数器电路。
3.实现启动功能:可以通过一个按钮或者开关来触发启动功能。当启动信号触发时,计数器开始递增。
4.实现停止功能:同样可以通过一个按钮或者开关来触发停止功能。当停止信号触发时,计数器停止递增。
5.实现递增功能:在计数器模块中,每次接收到时钟信号时,根据启动和停止信号的状态来决定是否进行递增操作。
6.实现清除功能:可以通过一个按钮或者开关来触发清除功能。当清除信号触发时,将计数器的值重置为初始值。
通过以上步骤,可以实现一个具有启动、停止、递增和清除功能的秒表。
4. 实现过程
(1)代码主体部分:
module counter
(
clk , //时钟
rst , //复位
hold , //启动暂停按键
add ,
seg_led_1 , //数码管1
seg_led_2 , //数码管2
led //led
);
input clk,rst;
input hold;
input add;
output [8:0] seg_led_2,seg_led_1;
// wire [8:0] seg_led_1;
// wire [8:0] seg_led_2;
output reg [7:0] led;
wire clk1h; //1Hz时钟
wire hold_pulse; //按键消抖后信号
wire add_pulse;
reg hold_flag = 1'b0; //按键标志位
reg add_flag = 1'b0;
reg back_to_zero_flag ; //计时完成信号
reg [6:0] seg [9:0];
reg [3:0] cnt_ge; //个位
reg [3:0] cnt_shi; //十位
initial
begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
end
wire add_pulse2;
reg add_pulse3;
reg [23:0] add_pulse3_cnt;
reg add_pulse3_cnt_en;
//启动/暂停按键进行消抖
debounce U2 (
.clk(clk),
.rst(rst),
.key(hold),
.key_pulse(hold_pulse)
);
//启动/暂停按键进行消抖
debounce2 U3 (
.clk(clk),
.rst(rst),
.key(add),
.key_sec(add_pulse),
.key_pulse(add_pulse2)
);
//用于分出一个1Hz的频率
divide #(.WIDTH(32),.N(1200000)) U1 (
.clk(clk),
.rst_n(rst),
.clkout(clk1h)
);
//按键动作标志信号产生
// always @ (posedge hold_pulse)
// if(!rst==1)
// hold_flag <= 0;
// else
// hold_flag <= ~hold_flag;
reg hold_pulse_dly;
always @ (posedge clk or negedge rst)begin
if(!rst==1)begin
hold_flag <= 1;
hold_pulse_dly <= 1'b0;
end
else begin
if(hold_pulse && ~hold_pulse_dly)begin
hold_flag <= ~hold_flag;
end
else begin
hold_flag <= hold_flag;
end
hold_pulse_dly <= hold_pulse;
end
end
always @ (posedge add_pulse)
if(!rst==1)
add_flag <= 0;
else begin
add_flag <= 1;
end
always@(posedge clk or negedge rst)begin
if(rst == 1'b0)begin
add_pulse3 <= 1'b0;
add_pulse3_cnt <= 24'd0;
add_pulse3_cnt_en <= 1'b0;
end
else begin
if(add_pulse2)begin
add_pulse3_cnt_en <= 1'b1;
add_pulse3_cnt <= add_pulse3_cnt + 1'b1;
add_pulse3 <= 1'b1;
end
else begin
if(add_pulse3_cnt_en)begin
add_pulse3 <= 1'b1;
if(add_pulse3_cnt == 24'd12_000_00)begin
add_pulse3_cnt <= 24'd0;
add_pulse3_cnt_en <= 1'b0;
end
else begin
add_pulse3_cnt <= add_pulse3_cnt + 1'b1;
add_pulse3_cnt_en <= add_pulse3_cnt_en;
end
end
else begin
add_pulse3_cnt <= add_pulse3_cnt;
add_pulse3 <= 1'b0;
end
end
end
end
//计时完成标志信号产生
always @ (*)
if(!rst == 1)
back_to_zero_flag <= 0;
else if(cnt_shi==0 &&
cnt_ge==0)
back_to_zero_flag <= 1;
else
back_to_zero_flag <= 0;
always @ (posedge clk1h or negedge rst) begin
if (!rst == 1) begin
cnt_ge <= 4'd0;
cnt_shi <= 4'd0;
end
else begin
if(hold_flag)begin
if(add_pulse3)begin
if(cnt_ge == 4'd9)begin
cnt_ge <= 4'd1;
if(cnt_shi == 4'd9)begin
cnt_shi <= 4'd0;
end
else begin
cnt_shi <= cnt_shi + 1'b1;
end
end
else begin
cnt_ge <= cnt_ge + 1'b1;
cnt_shi <= cnt_shi;
end
end
else begin
cnt_shi <= cnt_shi;
cnt_ge <= cnt_ge;
end
end
else begin
if(cnt_ge == 4'd9)begin
cnt_ge <= 4'd1;
if(cnt_shi == 4'd9)begin
cnt_shi <= 4'd0;
end
else begin
cnt_shi <= cnt_shi + 1'b1;
end
end
else begin
cnt_ge <= cnt_ge + 1'b1;
end
end
end
end
//计时完成点亮led
always @ ( back_to_zero_flag)begin
if (back_to_zero_flag==1)
led = 8'b0;
else
led = 8'b11111111;
end
assign seg_led_1[8:0] = {2'b00,seg[cnt_ge]};
assign seg_led_2[8:0] = {2'b00,seg[cnt_shi]};
endmodule
(2)按键消抖模块:
module debounce (clk,rst,key,key_pulse);
parameter N = 1; //要消除的按键的数量
input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_pulse; //按键动作产生的脉冲
reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值
wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲
reg [N-1:0] key_sec;
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
(3)分频模块
module divide ( clk,rst_n,clkout);
input clk,rst_n; //输入信号,其中clk连接到FPGA的C1脚,频率为12MHz
output clkout; //输出信号,可以连接到LED观察分频的时钟
//parameter是verilog里常数语句
parameter WIDTH = 24; //计数器的位数,计数的最大值为2**WIDTH-1
parameter N = 12_000_000; //分频系数,请确保N < 2**WIDTH-1,否则计数会溢出
reg [WIDTH-1:0] cnt_p,cnt_n; //cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
reg clk_p,clk_n; //clk_p为上升沿触发时分频时钟,clk_n为下降沿触发时分频时钟
//上升沿触发时计数器的控制
always @ (posedge clk or negedge rst_n ) //posedge和negedge是verilog表示信号上升沿和下降沿
//当clk上升沿来临或者rst_n变低的时候执行一次always里的语句
begin
if(!rst_n)
cnt_p<=0;
else if (cnt_p==(N-1))
cnt_p<=0;
else cnt_p<=cnt_p+1; //计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器
end
//上升沿触发的分频时钟输出,如果N为奇数得到的时钟占空比不是50%;如果N为偶数得到的时钟占空比为50%
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_p<=0;
else if (cnt_p<(N>>1)) //N>>1表示右移一位,相当于除以2去掉余数
clk_p<=0;
else
clk_p<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
//下降沿触发时计数器的控制
always @ (negedge clk or negedge rst_n)
begin
if(!rst_n)
cnt_n<=0;
else if (cnt_n==(N-1))
cnt_n<=0;
else cnt_n<=cnt_n+1;
end
//下降沿触发的分频时钟输出,和clk_p相差半个时钟
always @ (negedge clk)
begin
if(!rst_n)
clk_n<=0;
else if (cnt_n<(N>>1))
clk_n<=0;
else
clk_n<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
assign clkout = (N==1)?clk:(N[0])?(clk_p&clk_n):clk_p; //条件判断表达式
//当N=1时,直接输出clk
//当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p
//当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多所以是相与
endmodule
(4)增量按钮的实现
module debounce2 (clk,rst,key,key_sec,key_pulse);
parameter N = 1; //要消除的按键的数量
input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_sec; //按键动作产生的脉冲
output [N-1:0] key_pulse;
reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值
reg [N-1:0] key_sec;
wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
5. 遇到的主要难题
利用时钟分频时需要利用clk分出时钟信号,并确保分频系数N < 2**WIDTH-1,否则计数会溢出,cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
条件判断表达式当N=1时,直接输出clk;当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p;当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。
在增量按钮的设计中,也需要利用按键消抖功能,因此做好按键消抖功能也是十分重要的,
按键抖动:按键抖动通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。当按下一次按键,可能在A点检测到一次低电平,在B点检测到一次高电平,在C点又检测到一次低电平。同时抖动是随机,不可测的。那么按下一次按键,抖动可能会误以为按下多次按键。因此我们需要消除按键抖动对我们的影响。
我们利用的是debounce模块,实现了按键消抖。
6. 未来的计划建议
该项目已基本实现了具有启动、停止、递增和清除功能的秒表设计,不过在代码编写时偏冗余,并且占用的fpga资源较多,仍具有较大的改善空间,在之后的学习过程中不断对这些方面进行完善和调整。