项目需求
任务要求
设计一款反应时间测试系统,测试两个队友(队友A和队友B)看到LED亮起后按键的时间,将响应时间显示在数码管上,每个人测量8次做平均,并将两个人的响应时间做对比,显示出哪一方赢得比赛。
定义:
- 数码管:显示每一次测试的响应时间,两个队友共用
- LED:每一轮测试,相应轮次的LED灯亮起作为指示,第一次测试L1亮起,第二次测试L2亮起。。。第八次测试L8亮起
- RGB三色灯:指示正在测试的队友, RGB1 - 队友A,RGB2 - 队友B
- 绿色 - 测试过程中
- 蓝色 - 完成8次测试
- 红色 - 完成平均
- 白色 - 比赛结果,白色高亮:赢得比赛;白色暗淡:输掉比赛
- 开关SW1: 拨到上面测试队友A,拨到下面测试队友B
- 轻触按键:
- K1:启动
- K2:响应
- K3:平均
- K4:比较
游戏规则:
- 按下“启动”按钮后,两个7 段显示屏立即设置为显示全0,然后随机一段时间后(大约1 到10秒),相应测试轮次的“立即反应”LED 亮起,并启动毫秒计时器,数码管开始显示计时器值(以毫秒为单位递增)。
- “立即反应”LED 亮起后,队友必须尽快按下“响应”按钮来停止计时器。 停止的计时器将包含“立即反应”LED亮起和按钮按下之间的毫秒数,并且该时间将显示在数码管上。
- 再次按下“启动”按钮将清除计时器并开始新的测试
- 重复单个反应时间测量八次,每一个测试,相应的8个LED中的一个亮起,并将八个测量的反应时间值存储在临时保持寄存器中。
- 按下“平均”按钮可平均8次的测量值,并将平均后的数值显示在数码管上,8个LED全部亮起。
- 切换队友 - 将SW1的状态改变,重复测试8次,并平均八次的测试结果
- 按下“比较”按钮,通过RGB三色灯指示哪个队友获胜(平均用时最短),并在数码管上显示相应的响应时间
需求分析
根据游戏规则总结反应时间测试系统的实现功能如下,在测试过程中 RGB 绿色灯亮起,按下启动键 K 1 后,等待一个随机时间(1~10 s)Led 灯亮起,然后开始计时到按下响应键 K 2 的时间作为玩家的反应时间,重复 8 次后,RGB 蓝色灯亮起,按下平均键 K 3,显示八次的平均值,RGB 红色灯亮起,切换开关 SW 1 测试下一个队友,重复上述过程,最后按下比较键 K 4,RGB 白色灯亮起,并比较两个玩家的平均时间大小,如果玩家 A 平均时间短,则对应 RGB 白色灯高亮,反之低亮。整体流程图如下:
根据流程图分析,可以将整个系统可以分为三大部分
- 控制信号输入部分
- 输入信号 :开关 SW 1 、按键 K 1~K 4 、复位键 RST
- 按键消抖模块
- 时控部分
- 时钟分频模块
- 计时模块
- 倒计时模块
- 显示部分
- Led 灯显示模块
- RGB 灯显示模块
- 数码管显示模块
实现的方式
控制信号输入部分
输入信号处理最好实现,直接将 SW 1,K 1~K 4,以及 RST 的输入信号作为 wire 变量运行在程序中,K 1~K 4 需要加入消抖模块进行处理,因为没有空闲按键,所以选取空闲的开关作为 RST 信号输入。
按键电路原理图
开关电路原理图
显示部分
其次是显示部分,Led 灯和 RGB 灯集成为一个模块,计时模块输出 Led 灯控制信号,获胜信号,还有反应时间。 Led 灯显示和 RGB 显示集成在一个模块里,通过 Led 灯控制信号决定 Led 灯的状态。 RGB 灯通过按键和 SW 1 进行状态控制,最后的比较状态还需要添加 PWM 脉宽调制模块控制 RGB 灯的亮度,还有获胜信号决定那个灯更亮。 数码管显示单独为以一个模块,接收反应时间变量,显示在数码管上。
时控部分
时控部分分为三个模块,一个是时钟分频模块,这是整个系统的核心,没有准确的时钟整个程序都无法运行。人体反应时间大概在 0.2s 左右,所以没必要用 1000 Hz 频率(1ms)的时钟,用 100 Hz 频率(10ms)的时钟,刚好可以把以 10ms 为单位的反应时间显示在两位数码管上,也可以正常做出比较。 题目要求按下启动按键然后随机一段时间后(大约1 到10秒),相应测试轮次的“立即反应”LED 亮起,这里通过一个倒计时模块实现。倒计时模块通过状态机实现,在标志位未置位时,从 1-10s 循环计数,当标志位置位时,记录当前计数值然后开始自减,当计数值为 0 时,返回上一个状态并输出倒计时完成标志位。 计时模块通过按键输入信号以及倒计时完成信号,共同实现状态机,控制 Led 灯状态和数码管显示数值。
功能框图
系统框图
代码及说明
按键消抖和分频模块还有数码管显示都是用现成模块做些调整,直接在顶层模块进行例化,按键消抖有两个地方需要更改: N 的参数,和 cnt 判断相等的数值,因为时钟频率更改了,延时 20ms 只需要两次就可以
按键消抖,分频模块,数码管显示模块
///////////////////////////////////////////////////////////////
///////////////需要在消抖模块内更改输入按键数量N///////////////////
////////parameter N = 4;要消除的按键的数量//////////////
/////////////////////////////////////////////////////////////
debounce key1 ( //按键消抖模块例化
.clk (clk100hz), //100Hz时钟信号输入
.rst (rst), //时钟复位按键
.key (key), //按键信号输入
.key_pulse (key_pulse) //按键消抖后信号输出
);
//////////////////////////////////////////////////////////////
////////////////////、分频模块例化//////////////////////////////
divide # ( .WIDTH(24),.N(12_0_000)) dive
(
.clk(clk), //原时钟信号输入
.rst_n(rst), //时钟复位按键
.clkout(clk100hz) //分频信号输出
);
//////////////////////////////////////////////////////////////
//////////////////////数码管显示模块例化/////////////////////////
segment regled( //数码管显示模块
.seg_data_1 (mscnt[7:4]), //seg_data input
.seg_data_2 (mscnt[3:0]), //seg_data input
.segment_led_1 (seg_led1), //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
.segment_led_2 (seg_led2) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
倒计时模块,计时模块,Led 和 RGB 显示模块
在顶层文件例化
//////////////////////////////////////////////////////////
count u1( //计数模块
.clk(clk100hz), //100Hz时钟信号输入
.sw_edge(sw_edge), //SW切换检测信号
.key(key_pulse), //按键信号输入
.flag(flag), //计时完成信号位
.sw(sw),
.lednum(lednum), //LED状态信号
.mscnt(mscnt), //反应时间输出
.winflag(winflag) //胜利信号
);
/////////////////////////////////////////////////////////////
count_down u2( //倒计时模块
.clk(clk100hz), //100Hz时钟信号输入
.sw_edge(sw_edge), //SW切换检测信号
.key(key_pulse), //按键输入
.flag(flag) //计时完成信号位
);
//////////////////////////////////////////////////////////////
rgb_led u3( //RGBLED模块
.clk(clk), //原时钟信号输入
.clk100hz(clk100hz), //100hz时钟信号输入
.sw(sw),
.sw_edge(sw_edge), //SW切换检测信号
.key(key_pulse), //按键信号输入
.winflag(winflag), //胜利信号
.lednum(lednum) , //LED状态信号
.Led(led) , //输出信号到LED
.rgb_led1(rgb_led1), //输出信号到RGB LED
.rgb_led2(rgb_led2)
);
////////////////////////////////////////////////////////////
倒计时模块实现主要代码
最开始是想用 1s 的时钟信号进行计数,出了很多问题,最后改成了 10 ms 的时钟信号,为了实现 1~10s 的随机计数,程序中就用 100~900 作为计数值进行循环,当按下启动键,跳转到 DOWN 状态进行倒数,到 0 后跳转到 OUT 状态,输出信号置位,随即跳转到 CIR 状态。
parameter INIT = 3'd0 ;//初始化
parameter CIR = 3'd1 ; //循环
parameter DOWN = 3'd2 ;//倒计时
parameter OUT = 3'd3 ;//信号置位
always @(posedge clk) begin
if (sw_edge) begin // 重置状态和计数器
state < = INIT;
end
else begin
case (state)
INIT: begin //初始化
state < = CIR;
end
CIR: begin //循环
if (key[0]) begin
state < = DOWN;
end
end
DOWN: begin //倒计时
if (num == 0) begin
state < = OUT;
end
end
OUT: begin //信号置位
state < = CIR;
end
default: state < = INIT;
endcase
end
end
always @(posedge clk) begin
if (sw_edge) begin // 重置状态和计数器
num < = 0;
in_flag < = 0;
end
else begin
case (state)
INIT: begin //初始化
num < = 0;
end
CIR: begin //循环
num < = (num > = 900) ? 100 : num + 1;
if (key[0]) begin
in_flag < = 0;
end
end
DOWN: begin //倒计时
num < = num - 1;
end
OUT: begin //信号置位
in_flag < = 1;
end
default: num < = 0;
endcase
end
end
计时模块主要代码
state 用来控制 state1 的状态,state 1 控制 Led 状态,和计时状态。 再 WAIT_FLAG 中当倒计时完成信号 flag 为 1 时,跳转到 ADD 后将 reb_state1 状态加 1 赋值给 state1,随即跳转到 REMBER 记录当前 state1 的值赋给 reb_state 1,等待 flag 值为 0 后跳转到 WAIT_FLAG 继续等待 需要直接赋值给数码管显示所以用 BCD 格式计数计时,单独用变量方式再记录每次计数真实值方便后面求平均值。 Led 灯亮后需要显示计数值的变换,按下反应按钮后数码管需要显示计数值不再变化,所以用 cntflag 来实现这个效果。 求完平均值后保存到新的变量里,最后比较时直接比较保存值即可。
///////////////////////////////////////////////////////////////////
parameter INIT = 3'd0 ;//初始化
parameter WAIT_FLAG = 3'd1 ; //等待信号
parameter ADD = 3'd2 ;//状态加一
parameter REMBER = 3'd3 ;//记录状态
always @(posedge clk) begin
if (sw_edge) begin
state < = INIT;
state1 < = 0;
reb_state1 < = 0;//记录state1的值
end else if (key[2]) begin
state1 < = 9;
end else begin
case (state)
INIT: begin
state1 < = 0;
state < = WAIT_FLAG;
end
WAIT_FLAG: begin
if (flag == 1) begin
state < = ADD;
end
end
ADD: begin
state1 < = (state1 > 8) ? 1 : reb_state1 + 1;
state < = REMBER;
end
REMBER: begin
reb_state1 < = state1;
if (flag == 0) begin
state < = INIT;
end
end
default: begin
state < = INIT;
end
endcase
end
end
/////////////////////////////////////////////////////////////////
//////////////////////BCD格式计数器//////////////////////////////
function [7:0] cnter;
input [7:0] cnt;
begin
if(cnt[3:0] == 4'd9) //个位满九?
begin
cnt[3:0] = 4'd0; //个位清零
if(cnt[7:4] == 4'd9 ) //十位满五?
cnt[7:4] = 4'd0; //个位清零
else
cnt[7:4] = cnt[7:4] + 1'b1; //十位加一
end
else cnt[3:0] = cnt[3:0] + 1'b1; //个位加一
cnter = cnt;
end
endfunction
////////////////////////////////////////////////////////////////
//////////////////////Led状态和计数值处理/////////////////////////
always @(posedge clk) begin
if (sw_edge) begin
cnt = 0;
for (j = 0; j < 8; j = j + 1) begin
cnt_ary[j] = 0;
end
reg_lednum = 0;
keyflag = 0;
sum = 0;
reb_sum = 0;
end
else if (key[1]) begin
keyflag = 1; //停止计时标志位
cntflag = 1; //显示标志位
end
else begin
case (state1) //这段让GPT优化了一下原来1~8是分开写的比较长
0: begin
reg_lednum = 0;
cnt = 0;
cntflag = 0;
sum = 0;
end
1, 2, 3, 4, 5, 6, 7, 8: begin
reg_lednum = state1;//Led状态信号输出
cnt = cnter(cnt); //反应时间计时BCD格式
sum = sum + 1; //反应时间计时正常格式
if (keyflag) begin
cnt_ary[state1-1] = cnt; //反应时间记录
keyflag = 0;
reb_sum = reb_sum + sum; //反应总时间记录
end
end
9: begin
reg_lednum = 9;
end
default: begin
reg_lednum = 0;
cnt = 0;
sum = 0;
reb_sum = 0;
end
endcase
end
end
/////////////////////////////////////////////////////////////////
/////////////////////////平均值处理///////////////////////////////
always @(posedge clk)begin
if(sw_edge)begin
key2flag = 0;//求平均标志位
key3flag = 0;//比较标志位
end
else if(key[3])begin
key3flag = 1;
key2flag = 0;
end
else if(key[2])begin
i = reb_sum>>3;
avgeary[7:4] = i/10;
avgeary[3:0] = i%10;
key2flag = 1;
key3flag = 0;
end
end
///////////////////////////////////////////////////////////////
////////////////////平均值记录///////////////////////////////////
always @(posedge clk)begin
if(key2flag)begin
case(sw)
0: begin avgeary2 = avgeary; end
1: begin avgeary1 = avgeary; end
endcase
end
end
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
assign lednum = reg_lednum;//Led状态信号输出
///////////////////////////////////////////////////////////////
////////////////数码管显示值输出/////////////////////////////////
assign mscnt = (key3flag)?
(avgeary1<avgeary2)? avgeary1:avgeary2
: //第一层
(key2flag)? avgeary
: //第二层
cntflag? cnt_ary[state1-1]
: //第三层
((state1==0)?0:cnt);
////////////////////////////////////////////////////////////////
/////////////////////获胜信号处理////////////////////////////////
assign winflag = (avgeary1<avgeary2)?1:0;
Led 和 RGB 显示模块主要代码
显示模块实现较为简单,通过检测按键值改变 state 实现 RGB 的不同状态,最后的比较效果需要 PWM 脉宽调制模块和 winflag 信号配合实现,Led 灯只需要 Lednum 信号决定输出状态。
initial
begin
led[0] = 3'b111;//灭
led[1] = 3'b101;//绿
led[2] = 3'b011;//蓝
led[3] = 3'b110;//红
led[4] = 3'b000;//白
lednub[0] = 8'b11111111;
lednub[1] = 8'b11111110;
lednub[2] = 8'b11111101;
lednub[3] = 8'b11111011;
lednub[4] = 8'b11110111;
lednub[5] = 8'b11101111;
lednub[6] = 8'b11011111;
lednub[7] = 8'b10111111;
lednub[8] = 8'b01111111;
lednub[9] = 8'b00000000;
end
assign Led = lednub[lednum];
always @(posedge clk100hz)begin
if(sw_edge)begin
state =1;
cnt = 0;
end
else begin
case (key)
4'b0001:begin state =1; keyflag=1; end //绿色 - 测试过程中
4'b0010:
begin
if(keyflag)begin
cnt = (cnt>7)?0:cnt+1;
keyflag=0;
end
state =(cnt>7)?2:state;
end//蓝色 - 完成8次测试
4'b0100: state =3; //红色 - 完成平均
4'b1000: state =4; //白色 - 比赛结果
default: state=state;
endcase
end
end
pwm_generator pwm_inst(
.clk(clk), // 输入时钟信号
.duty_cycle(8'h20), // 设置占空比
.pwm_out(flag) // 输出PWM信号
);
pwm_generator pwm_inst2(
.clk(clk), // 输入时钟信号
.duty_cycle(8'hc0), // 设置占空比
.pwm_out(flag1) // 输出PWM信号
);
assign rgb_led1 = (state == 4)?(((winflag)?flag1:flag)?led[state]:led[0]):((sw)?led[state]:led[0]);
assign rgb_led2 = (state == 4)?(((winflag)?flag:flag1)?led[state]:led[0]):((sw)?led[0]:led[state]);
SW 切换检测主要代码
通过检测 SW 的边沿变换实现 SW 切换检测功能,rst 1 是功能复位,如果共用 rst 置位时,时钟复位不运行起不到复位功能所以加一个复位键实现功能复位。
always @(posedge clk100hz)begin //sw切换检测
if(!rst)begin
pre_swrst< = 0;
sw_rst< = 0;
end else begin
sw_rst < = sw;
pre_swrst < = sw_rst;
end
end
assign sw_edge = (!rst1)? 1 : (pre_swrst) ^ (sw_rst);
附:PWM 脉宽调制模块
这段代码是 GPT 生成(用 GPT 生成的代码中唯一能用的代码(T_T))
module pwm_generator(
input clk, // 时钟信号
input [7:0] duty_cycle, // 占空比,取值范围为0-255
output reg pwm_out // PWM输出信号
);
reg [7:0] count;
always @(posedge clk) begin
if (count < 255) begin
count < = count + 1;
end else begin
count < = 0;
end
if (count < duty_cycle) begin
pwm_out < = 1'b1; // 高电平
end else begin
pwm_out < = 1'b0; // 低电平
end
end
endmodule
仿真波形图
测试文件放在附件中
消抖模块仿真
key 输入不同值,key_pulse 输出相应值
分频模块仿真
数码管显示模块仿真
倒计时模块仿真
按键置 1后,随机时间 flag 置 1,下一次按键置 1 后 flag 置 0,随机时间后继续置 1
计时模块仿真
Lednum,mscnt输出不同的值
Led 和 RGB 显示模块仿真
FPGA 的资源利用说明
资源的占用情况总结: 1. 寄存器: - 总共4635个寄存器中有239个被使用,利用率为5%。 2. PFU(Programmable Function Unit)寄存器: - 总共4320个PFU寄存器中有239个被使用,利用率为6%。 3. PIO(Parallel Input/Output)寄存器: - 总共315个PIO寄存器中没有被使用,利用率为0%。 4. SLICEs: - 总共2160个SLICEs中有214个被使用,利用率为10%。 - 其中,SLICEs用作逻辑/ROM的比例为10%,用作RAM的比例为0%,用作Carry的比例为2%。 5. LUT4s(Look-Up Tables): - 总共4320个LUT4s中有427个被使用,利用率为10%。 - 其中,作为逻辑LUTs的数量为325个,作为分布式RAM和Ripple Logic的数量分别为0和102个。 6. 其他资源占用情况: - 没有使用Block RAMs。 - 1个GSR(Global Set/Reset)被使用,利用率为100%。 - 没有使用EFB、JTAG、Readback、Oscillator、Startup等功能。 - 没有使用 Power Controller、Dynamic Bank Controller、DCCA、DCMA、PLLs、DQSDLLs、CLKDIVC、ECLKSYNCA、ECLKBRIDGECS 等组件。
总结感悟
这是我第二次参加硬禾学堂的活动,每次都能获得很大的收获。这次是我第一次成功地完成一个完整的项目,对此我真的非常感激硬禾学堂提供的机会。
刚开始看到题目时,觉得它很简单,不会花费太多时间完成。但在实际开始做项目时,我才发现自己低估了题目的难度。一开始我凭借想象构思了一个大致的结构,但问题很多,不知道从何下手。后来我画了简单的流程图,并采用了一种新的方法,仿真的结果看起来很好。然而,在烧录之后却出现了很多问题,我一直卡在那里很长时间,甚至曾经考虑放弃。
后来,我推翻了第二版方案,重新思考了新的方案,并结合之前的经验,总算有了一个大致的样子。在整个过程中,有一次前天晚上我成功地使其正常运行,但第二天却无法运行。我花了几个晚上的时间,一行一行地检查代码,最终发现是一个很低级的错误——使用了不同的时序。由于我还没有完全养成硬件思维,所以会犯下很多低级错误。在比较大小这部分也遇到了很多困难,尝试了好几种方法,但却无法实现。最后我发现是在例化时少写了一个端口,真的对自己感到非常无语。但幸运的是,最终我解决了所有问题。
第一次成功地完成一个项目时,我真的感到非常激动。虽然这个项目并不是很难,但我能够坚持下来,这对我来说是一次自我提升。再次衷心感谢硬禾学堂!