一, 内容介绍
1. 项目需求
· 通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过四个按键来控制秒表的功能,并在数码管上显示数值。
· 在WebIDE环境下进行Verilog代码编程,综合,仿真,生成JDE代码并下载到FPGA中进行验证。
· 通过GPT等大模型工具对用到的各功能模块进行验证和修改。
2. 完成的功能
2.1 开始按键输入
开始输入使数码管秒表开始以10Hz时钟速率递增(即每0.1秒计数一次)。
2.2 停止按键输入
停止输入使计数器停止递增,但使数码管显示当前计数值。
2.3 增量按键输入
在暂停状态时,每次按下增量按键且无论按住增量按键多长时间,显示值都会增加一次。递增完成后回到暂停状态。
2.4 清除按键输入
清除按键强制计数器值为零。
3. 实现思路及各模块分析
分析题目要求可得,数码管初始化显示0.0s,按下启动按键后开始正向计时,计时到9.9s后自动回到0.0s重新计时;按下暂停按键后,停止计时,数码管保持当前显示不变;在暂停状态时,按下递增按键,数码管最低为递增一并显示出来,递增完成后系统回到暂停状态。将所要完成工作分为如下几个小模块: 10Hz分频器模块,按键消抖模块,数码管显示模块。、
程序流程如图所示:
下面对这些模块功能和代码分别进行说明。
3.1 分频器模块
FPGA分频器是一种用于将输入时钟频率降低为较低频率的电路,常见的有偶数分频和奇数分频。查阅项目使用开发板手册可得,开发板原始输入时钟频率为12MHz。要得到10Hz时钟信号,简单来说则要设计一个计数器,对输入12MHz时钟信号脉冲上升沿计数,每计数1200000,使得输出信号跳变一次,即可得到10Hz时钟信号。参考开发板示例资源及GPT可得模块代码和注释如下:
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
代码使用两个计数器cnt_p和cnt_n来记录计数器的状态,分别用于上升沿触发和下降沿触发。同时,定义了两个寄存器变量clk_p和clk_n来存储分频时钟的输出状态。在上升沿触发时,计数器cnt_p会一直计数,当计数到输入N-1(N为输入分频系数)时清零,同时根据N的奇偶性输出不同的分频时钟状态;下降沿触发时同理。本项目要得到10Hz频率时钟,输入分频系数N应为1200000,为偶数,输出上升沿触发时的计数器输出状态。
3.2 按键消抖模块
按键消抖通常是指在FPGA开发板上,对按键输入信号进行处理,以消除按键操作中由于各种因素导致的输入信号不稳定的现象,从而确保输入信号的准确性和稳定性。在本项目使用开发板中,要进行的是按键机械消抖。当按键被按下时,由于机械结构的原因,可能会产生连续的抖动,导致输入信号出现噪声,影响准确性,因此需要对其进行消抖处理。具体代码参考已给示例并修改如下:
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; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
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
代码定义一个整型参数N,表示要消除抖动的按键数量。还使用了两个寄存器变量key_rst和key_rst_pre来记录按键的状态。key_edge变量来检测按键的下降沿,cnt变量来产生延时,key_sec和key_sec_pre变量来记录延时后按键的状态。当检测到按键下降沿时,会触发一个脉冲信号,并将该按键的状态存储在key_rst中。延时计数器cnt从0开始计数,计数器溢出时,产生一个高脉冲信号,该信号用于检测按键的触发。在延时计数器cnt计数过程中,按键的状态被存储在key_sec中。当延时计数器cnt计数结束时,按键的状态被存储在key_sec_pre变量中。最后,通过比较key_sec和key_sec_pre的值,可检测到按键的有效触发。如果按键状态变低,则产生一个高脉冲信号,此时认为按键有效按下,否则认为按键无效。这样,通过延时计数器cnt的计数和检测按键的有效触发,可以实现按键消抖的功能。
33 数码管显示模块
数码管显示模块是整个系统的主体部分,这一模块根据不同的按键信息,控制数码管分别实现秒表计数,停止计数,递增计数和清除功能。具体代码如下:
module counter
(
clk , //时钟
rst , //复位
go , //启动按键
stop , //暂停按键
add , //递增按键
seg_led_1 , //数码管1
seg_led_2 , //数码管2
);
input clk,rst;
input go;
input stop;
input add;
output [8:0] seg_led_1,seg_led_2; //9位,七段数码管,最后一位是位选信号,在小脚丫上控制一个数码管需要9个信号
wire clk10hz; //10Hz时钟
wire go_pulse; //按键消抖后信号
reg go_flag; //按键标志位
wire stop_pulse;
reg stop_flag;
wire add_pulse;
reg add_flag;
reg [6:0] seg [9:0]; //二维寄存器数组,每个寄存器七位,包含十个元素,存储数字0~9
reg [3:0] cnt_ge; //个位
reg [3:0] cnt_shi; //十位
initial //在过程块中只能给reg型变量赋值,Verilog中有两种过程块always和initial
begin //initial和always不同,其中语句只执行一次
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
go_flag =0;
stop_flag =0;
add_flag =0;
end
// 启动/暂停/递增按键进行消抖
debounce U2 (
.clk(clk),
.rst(rst),
.key(go),
.key_pulse(go_pulse)
);
debounce U3(
.clk(clk),
.rst(rst),
.key(stop),
.key_pulse(stop_pulse)
);
debounce U4(
.clk(clk),
.rst(rst),
.key(add),
.key_pulse(add_pulse)
);
// 用于分出一个10Hz的频率
divide #(.WIDTH(32),.N(1200000)) U1 ( //10Hz则N取1200000
.clk(clk),
.rst_n(rst),
.clkout(clk10hz)
);
//按键动作标志信号产生
always @ (posedge clk10hz or posedge go_pulse or posedge stop_pulse or posedge add_pulse or negedge rst)begin //检测有按键按下 按键检测部分得改,参考之前做的,也可用GPT生成消抖模块
if(add_flag==1)begin
add_flag<=0;
end
if(!rst==1)begin //复位清零
go_flag <= 0;
stop_flag <= 0;
add_flag <= 0;
end
else if(go_pulse)begin //启动标志
go_flag <= 1;
stop_flag <= 0;
add_flag <= 0;
end
else if(stop_pulse) begin //停止标志
go_flag <= 0;
stop_flag <= 1;
add_flag <= 0;
end
else if(add_pulse)begin //递增一次标志
add_flag <=1;
end
end
//9.9s正向计时
always @(posedge clk10hz or negedge rst) begin
if (!rst) begin //复位到初始值0.0
cnt_ge <= 4'd0;
cnt_shi <= 4'd0;
end
else if(go_flag == 1)begin
if (cnt_ge == 9 && cnt_shi == 9) begin // 当个位和十位都为9时,重置计数器
cnt_ge <= 4'd0;
cnt_shi <= 4'd0;
end
else if (cnt_ge == 9) begin // 当个位为9十位不为9时,个位归0,十位加1
cnt_ge <= 4'd0;
cnt_shi <= cnt_shi + 1;
end
else begin
cnt_ge <= cnt_ge + 1; // 若计数器没有达到阈值,则个位加1
end
end
else if(stop_flag==1&&add_flag==1)begin //按下add键
if (cnt_ge == 9 && cnt_shi == 9) begin // 当个位和十位都为9时,重置计数器
cnt_ge <= 4'd0;
cnt_shi <= 4'd0;
end
else if (cnt_ge == 9) begin // 当个位为9十位不为9时,个位归0,十位加1
cnt_ge <= 4'd0;
cnt_shi <= cnt_shi + 1;
end
else begin
cnt_ge <= cnt_ge + 1; // 若计数器没有达到阈值,则个位加1
end
end
else begin //按下stop,保持不变
end
end
assign seg_led_2[8:0] = {2'b00,seg[cnt_ge]};
assign seg_led_1[8:0] = {2'b01,seg[cnt_shi]};//数码管1显示小数点
endmodule
下面给出实验流程,结果,遇到的问题和解决方案
4. 实验流程
本实验采用WebIDE环境进行Verilog代码编程实现。首先在WebIDE网页端创建项目,新建文件,写好代码。
点击逻辑综合,结果显示成功
根据所要实现功能和板卡引脚信息进行管脚分配
最后经FPGA映射,下载JED文件到本地,将下载好的文件传入板卡即可。
5. FPGA资源利用说明
利用GPT帮忙总结可得,在设计中,用到总寄存器数为113个,占所有寄存器数(4635个)的2%,PFU寄存器数113个,占所有PFU寄存器(4320个)的3%。用到SLICEs总数100个,占所有SLICEs(2160个)的5%。LUT4s占用200个,其中用作逻辑LUTs80个,用作ripple逻辑的LUT4s120个。此外,设计中峰值内存使用为173MB。总体看来,设计中FPGA的资源占用少,符合预期。大部分SLICEs和LUT4s被用作逻辑处理,其中一部分用于ripple逻辑和作为Carry的SLICEs。PIO站点的使用比例较高,达到26%。
6. 实验结果
板卡上电后显示0.0s,为初始状态,待按键按下实现对应功能。
当按下启动按键时,开始计数功能,数码管从0.0s计时到9.9s后翻转到0.0s并继续计时。
当按下暂停按键时,计时器停止计时,数码管维持当前计数值并保持显示。
在暂停状态时按下递增按键,计数器从小数位递增一次,并通过数码管显示表示出来,且递增完成后回到暂停状态。
当按下清零按键时,计数器清零,数码显示0.0s,回到初始状态。
由结果可得,已基本完成实验要求。
7. 遇到的主要问题及解决方案
7.1 模块代码编写
最开始对按键机械消抖原理理解不清晰,编写消抖模块代码时存在困难。因此我根据要求,采用GPT大模型设计模块并注释代码,同时参考示例学习,最终解决问题。
7.2 计数器进位逻辑错误
写完代码后发现实际功能并不符合预期,自己仔细检查代码也没有发现问题。因此我利用GPT工具,对代码进行检查并修改,最终找出逻辑错误并纠正。
8. 感想
这次项目对我而言是学习FPGA的一个很好机会。项目采用WEBIDE网页环境实现,不用下载额外软件,感觉十分简便。此外,在此次寒假在家练活动中,我第一次将GPT模型用于代码实现和纠错,自我感受是效率确实提高了很多。代码中的一些逻辑错误自己可能得花很大精力才能纠正,让GPT帮忙的话就十分高效。后续计划进一步学习相关知识,希望能更深一步参与更复杂的FPGA项目,锻炼自己能力。


