2024年寒假练 - 小脚丫FPGA核心板制作具有启动、停止、递增和清除功能的秒表
一、任务内容
通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。
使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。 秒表应从 0.0 秒计数到 9.9秒,然后翻转,计数值每0.1秒精确更新一次。
秒表使用四个按钮输入:开始、停止、增量和清除(重置)。 开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次); 停止输入使计数器停止递增,但使数码管显示当前计数器值; 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间; 复位/清除输入强制计数器值为零。
二、内容分析
配置时钟,以适当的10HZ速率驱动秒表的计数。
编写代码来处理按键输入。使用适当的逻辑来检测按键按下和释放事件,并根据按键类型执行相应的操作。按键做消抖处理
实现一个计数器,用于记录秒表的当前值。计数器的值应以0.1秒为单位递增,并在达到9.9秒时从0开始计数。
在数码管上显示秒表的值。将计数器的值转换为适当的七段显示器编码,并将其输出到数码管引脚上。
根据按键输入,控制秒表的功能。当按下开始按钮时,启动计数器并开始递增。当按下停止按钮时,停止计数器递增但保持显示当前计数值。按下增量按钮时,增加显示值。按下复位按钮时,将计数器重置为零。
下面几点我们要考虑的:
确保控制时钟的稳定性和准确性,以保证秒表的计时精度。
• 处理按键输入的反弹问题,以避免意外的多次触发。
• 适当地处理按钮长按的情况,以确保增量按钮的功能按需增加。
• 考虑并处理计数器溢出的情况,以确保秒表能够正确地翻转。
代码编写
这里首先我们先通过电子森林,关于小脚丫的一些底层代码进行使用。
1.按键消抖
module debounce (clk,rst,key,key_pulse);
parameter N = 3; //要消除的按键的数量
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; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
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; //延时后检测电平寄存器变量
reg [N-1:0] key_sec;
//延时后检测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
这里关于按键消除的数量,我是通过询问GPT将N改为3,减少了我们主程序代码的数量。
根据GPT的回答,我很快的将实例代码进行修改,让代码更符合自己使用。
3.12MHZ分频代码
这里也是根据电子森林的任意整数分频,来实现我们对10hz的分频。
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
这里我们直接传入时间变量,和分频得出的时间,就能得到我们想要的10hz的频率。
4.主程序代码
这里根据电子森林的数码管显示代码进行修改。
module counter
(
clk, // 时钟
rst, // 复位
increase, // 增量
start, // 开始
stop, // 停止
seg_led_1, // 数码管1
seg_led_2 // 数码管2
);
input clk, rst;
input start, stop, increase;
output [8:0] seg_led_1, seg_led_2;
wire clk10h; // 10Hz 时钟
wire start_pulse, stop_pulse, increase_pulse; // 按键消抖后信号
reg start_flag, stop_flag, increase_flag; // 按键标志位
reg [6:0] seg [9:0];
reg [3:0] cnt_ge; // 个位
reg [3:0] cnt_shi; // 十位
reg increased;
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
// 开始按键进行消抖
debounce U2 (
.clk(clk),
.rst(rst),
.key({start, stop, increase}),
.key_pulse({start_pulse, stop_pulse, increase_pulse})
);
// 用于分出一个 10Hz 的频率
divide #(.WIDTH(32), .N(1200000)) U1 (
.clk(clk),
.rst_n(rst),
.clkout(clk10h)
);
// 按键动作标志信号产生
always @ (posedge clk10h or posedge start_pulse or posedge stop_pulse or posedge increase_pulse or negedge rst) begin
if(increase_flag==1)begin
increase_flag<=0;
end
if (!rst) begin
start_flag <= 1'b0;
stop_flag <= 1'b0;
increase_flag <= 1'b0;
end else if (start_pulse) begin
start_flag <= 1'b1;
stop_flag <= 1'b0;
increase_flag <= 1'b0;
end else if (stop_pulse) begin
stop_flag <= 1'b1;
start_flag <= 0;
increase_flag <= 1'b0;
end else if (increase_pulse) begin
increase_flag <= 1'b1;
end
end
// 0.0-99.9 秒的计时开始
always @ (posedge clk10h or negedge rst) begin
if (!rst) begin
cnt_ge <= 4'd0;
cnt_shi <= 4'd0;
end
else if (stop_flag == 1 && increase_flag==1) begin
cnt_ge <= cnt_ge + 1;
if (cnt_shi == 9 && cnt_ge == 9) begin
cnt_shi <= 4'd0;
cnt_ge <= 4'd0;
end else if (cnt_ge == 9) begin
cnt_ge <= 4'd0;
cnt_shi <= cnt_shi + 1;
end
end else if (start_flag == 1) begin
cnt_ge <= cnt_ge + 1; // 在开始和增加标志位为 1 时增加
if (cnt_shi == 9 && cnt_ge == 9) begin
cnt_shi <= 4'd0;
cnt_ge <= 4'd0;
end else if (cnt_ge == 9) begin
cnt_ge <= 4'd0;
cnt_shi <= cnt_shi + 1;
end
end
else begin
end
end
assign seg_led_1[8:0] = {2'b00, seg[cnt_shi]} | 8'b10000000;
assign seg_led_2[8:0] = {2'b00, seg[cnt_ge]};
endmodule
在原有的基础上,询问了GPT添加小数点显示。
GPT提供了思路,发现直接与上小数点的位数,我们就能实现小数点的点亮。
这里我们设计思路是,当开始按键按下的时候,我们就将我们的cnt每10hz加一次,实现我们的小数点的变化,之后当我们暂停按下时,我们不执行任何语句,程序就会保持不动,通过我们按键的边沿检测,实现我们对暂停情况的操作。这里的增量的操作的本质也是跟开始的cnt操作是一样的,我们这里设置为在暂停的情况下才能够执行,这里我们增量开始还需要判断暂停的标志位,再通过清零标志位,实现我们对小数的改变。
效果图如上。
5.资源利用率情况
- 设备型号:LCMXO2-4000HC
- 封装类型:CSBGA132
- 时钟频率约为154.44MHz和133.209MHz
- 设计使用了3%的SLICE资源
- 设计使用了2%的寄存器资源
6.遇到的主要问题
1.一开始根据实例代码,将hold的程序,修改我们的暂停程序,导致我们的程序,一直与增量的冲突,因为增量要改变数码管,暂停是保持数值不变。而FPGA是并行任务进行,导致我们程序在10hz的前提下同时进行,导致我们的数据无法刷
新。
解决方法:在暂停标志位时,什么语句都不执行,这样的话,就不会与我们的增量冲突。
2.增量只能一直增加,因为在一开始的程序里,按键判断函数,只有在按键按下的上升沿才会执行,改变我们的标志位。之后我想通过在数码管显示函数中,修改增量的标志位,一直失败,因为在always语句下,俩个变量不能同时出现在不同的always中。
解决方法:在按键扫描中也加10hz的扫描,这样的话每当我们增量按下的时候,将我们的增量标志位清除,等待下一个增量。
7.最后总结
在这一次的寒假训练营,我第一次接触到FPGA,在此过程中,不断询问GPT关于FPGA的语句的使用,也得知FPGA的运行逻辑与单片机是不同的,通过并行的逻辑,在代码编写和时序问题上,我们需要考虑的更多。我遇到的问题都是在时序和并行,都是学习FPGA的要点,在处理这些问题上,我总会有一些空缺。也感谢硬禾学堂的寒假训练营,让我体验到FPGA的魅力,在这个寒假也学习到关于FPGA的相关语法,在经历了俩次耗费许久的问题,让我深刻知道了FPGA时序和并行的逻辑的重要性。