项目需求
- 设计一个简易的反应时间测试系统,能够测试两个人(队友A和队友B)的反应时间。
- 每人测试8次取平均值进行比较,并显示出哪一方赢得比赛。
- 按下“启动”按钮后,两个7 段显示屏立即设置为显示全0,然后随机一段时间后(大约1 到10秒),相应测试轮次的“立即反应”LED 亮起,并启动毫秒计时器,数码管开始显示计时器值(以毫秒为单位递增)。
- “立即反应”LED 亮起后,队友必须尽快按下“响应”按钮来停止计时器。 停止的计时器将包含“立即反应”LED亮起和按钮按下之间的毫秒数,并且该时间将显示在数码管上。
- 再次按下“启动”按钮将清除计时器并开始新的测试。
- 重复单个反应时间测量八次,每一个测试,相应的8个LED中的一个亮起,并将八个测量的反应时间值存储在临时保持寄存器中。
- 按下“平均”按钮可平均8次的测量值,并将平均后的数值显示在数码管上,8个LED全部亮起。
- 切换队友 - 将SW1的状态改变,重复测试8次,并平均八次的测试结果。
- 按下“比较”按钮,通过RGB三色灯指示哪个队友获胜(平均用时最短),并在数码管上显示相应的响应时间。
- RGB三色灯指示两个队友的状态:绿色 - 测试过程中;蓝色 - 完成8次测试;红色 - 完成平均;白色 - 比赛结果,白色高亮:赢得比赛;白色暗淡:输掉比赛
设计思路
控制与输入都使用机械按键,首先需要使用按键消抖模块进行消抖。还需要设计一个数码管显示模块,以便将反应的时间进行显示。使用两个3bit寄存器来存储两人当前的测试轮次。用两个同步RAM来存储两人每轮的测试结果。两个队友的状态可以使用两个状态机来实现,RGB三色灯作为状态机的输出。
按下“启动”按钮后,利用一个定时器模块进行倒计时,倒计时的时间由随机数生成模块产生,倒计时结束后开始测试并启动毫秒计时器,按下“响应”按钮后将此时毫秒计时器的值保存到RAM中,并更新测试轮次,便完成了一次测试。
按下“平均”按钮后计算出8次结果的平均值并保存到寄存器中。按下“比较”按钮后,通过RGB灯白色光的亮度来指示哪个队友赢得比赛,灯的亮度可以使用PWM调光的方式实现,还需要一个PWM脉冲发生模块。
定时器模块、毫秒计时器、按键消抖模块都可以使用同一个周期为1ms的时钟源,时钟源由一个毫秒时钟生成模块产生。
本次项目所使用的FPGA核心板在xo2-4000hcd [小脚丫STEP开源社区]有详细介绍,这里不再赘述
简易展示
测试中:
完成8次测试:
显示平均值:
比赛结果:
FPGA资源占用报告
引脚分配
核心代码介绍
Verilog HDL代码可以用Lattice的Diamond或者Vscode来写。但我更推荐使用小脚丫思得普的WebIDE来进行设计,写代码、仿真与分配引脚都可以在浏览器中进行,非常方便。本项目所有代码也在上面公开:项目Reaction_Time_Test
我也是Verilog的初学者,代码可能写得不是很好。项目工作的大部分时间尺度都在毫秒以上,仿真比较困难,我就不仿真了。
脉冲发生模块:
时间长度为一个时钟周期的脉冲信号很有用,使用移位寄存器和与门的方式即可产生所需脉冲。移位寄存器赋上电初值是防止上电时误触发。
// 单次脉冲发生器,触发延迟一个时钟周期后产生一个时钟周期的高脉冲
module Once_Pulse#(
parameter Trigger = 2'b11 // 选择控制信号触发方式,2'b11为双边沿触发,2'b01为上升沿,2'b10为下降沿
) (
input CLK, // 全局时钟信号
input Ctrl, // 控制信号
output Pulse // 生成的高脉冲信号
);
generate case (Trigger)
2'b11: begin
reg [1:0]Shift = 2'b00;
assign Pulse = Shift[1] ^ Shift[0];
always @(posedge CLK) begin
Shift[0] <= Ctrl;
Shift[1] <= Shift[0];
end
end
2'b01: begin
reg [1:0]Shift = 2'b11;
assign Pulse = ~Shift[1] & Shift[0];
always @(posedge CLK) begin
Shift[0] <= Ctrl;
Shift[1] <= Shift[0];
end
end
2'b10: begin
reg [1:0]Shift = 2'b00;
assign Pulse = Shift[1] & ~Shift[0];
always @(posedge CLK) begin
Shift[0] <= Ctrl;
Shift[1] <= Shift[0];
end
end
default: begin
reg [1:0]Shift = 2'b00;
assign Pulse = 1'b0;
always @(posedge CLK) begin
Shift[0] <= Ctrl;
Shift[1] <= Shift[0];
end
end
endcase
endgenerate
endmodule
毫秒时钟生成模块:
这里采用异步的方式来产生50%占空比周期1ms的时钟信号,通过全局时钟信号频率先计算出需要计数的次数,利用计数器产生一个周期为0.5ms的脉冲信号,此脉冲信号再作为时钟源控制寄存器翻转,以此产生周期1ms的时钟信号。
// 产生50%占空比周期1ms的时钟信号
module Clock_msec #(
parameter CLKRATE = 12_000_000 // 全局时钟信号频率(Hz)
) (
input CLK, // 全局时钟信号
output reg Clkout = 0 // 生成的时钟信号
);
localparam TIMES = ((CLKRATE >> 1)/1_000) - 1;
localparam WIDTH = $clog2(TIMES+1);
// 产生周期为0.5ms的脉冲信号
reg [WIDTH-1:0]CountUnit = 0;
reg HalfUnit_CP = 0;
always @ (posedge CLK) begin
if(CountUnit==TIMES) begin
HalfUnit_CP <= 1'b1;
CountUnit <= 0;
end else begin
HalfUnit_CP <= 0;
CountUnit <= CountUnit+1'b1;
end
end
// 产生周期为1ms的时钟信号(占空比50%)
always @ (posedge HalfUnit_CP) begin
Clkout <= ~Clkout;
end
endmodule
按键消抖模块:
项目中总共有4个按键输入需要消抖,所有这里采用generate for来多次利用重复代码。Once_Pulse模块为脉冲发生模块,作用是发出一个时钟周期的脉冲。在按键按下时(Key_Raw下降沿)会发出一个脉冲并将Debounce设为1此时按键输入无效,按键抬起时毫秒计时器开始计时,到达DELAY毫秒时将Debounce清零此时按键输入有效。这样就完成了按键消抖,由于按键按下时立刻发出一个脉冲,不存在按键延迟的问题。
// 对多个按键的输入进行防抖后输出一个时钟周期的脉冲
// 按键防抖(按下立刻响应)
module Key_Debounce#(
parameter DELAY = 10, // 防抖延迟时间(毫秒)
parameter NUM = 1 // 按键数量
) (
input CLK, // 全局时钟信号
input CLK_ms, // 毫秒时钟信号
input [NUM-1:0]Key_Raw, // 按键原始输入(低电平有效)
output reg [NUM-1:0]Pulse // 防抖后输出脉冲
);
localparam WIDTH = $clog2(DELAY);
// 定时器
reg Debounce[NUM-1:0];
wire Pressed_CP[NUM-1:0];
reg [WIDTH-1:0]Counter[NUM-1:0];
reg Delay_CC[NUM-1:0];
wire Delay_CP[NUM-1:0];
genvar i;
generate
for(i=0;i<NUM;i=i+1) begin: gene_deb
// 按键抬起DELAY(毫秒)后发出脉冲信号
always @(posedge CLK_ms, negedge Key_Raw[i]) begin
if (!Key_Raw[i]) begin
Counter[i] <= 0;
end else if(Counter[i]==DELAY-1'b1) begin
Delay_CC[i] <= ~Delay_CC[i];
Counter[i] <= 0;
end else begin
Counter[i] <= Counter[i]+1'b1;
end
end
Once_Pulse Delay_Pulse (
.CLK ( CLK ),
.Ctrl ( Delay_CC[i] ),
.Pulse ( Delay_CP[i] )
);
Once_Pulse #(.Trigger ( 2'b10 ))
Negedge_Pulse (
.CLK ( CLK ),
.Ctrl ( Key_Raw[i] ),
.Pulse ( Pressed_CP[i] )
);
always @(posedge CLK) begin
if (!Debounce[i] && Pressed_CP[i]) begin
Debounce[i] <= 1'b1;
Pulse[i] <= 1'b1;
end else if(Delay_CP[i]) begin
Debounce[i] <= 1'b0;
Pulse[i] <= 1'b0;
end else begin
Pulse[i] <= 1'b0;
end
end
end
endgenerate
endmodule
数码管显示模块:
这个FPGA核心板的数码管每个引脚是直接与IO口相连接的,显示数字只要用译码器的原理来实现就好了。
随机数生成模块:
使用线性反馈移位寄存器来生成伪随机数。一开始我想生成1到10的随机数,乘以1000后给定时器模块使用,但是乘法器会消耗很多的资源,最后使用一个取巧的方法来实现。随机的时间大约1 到10秒,13bit数的最大值为8191,在第10位加上1就相当于整体加上1024,此时得到的14bit数范围便在[1024,9125],满足设计需求。
// 产生[1024,9215]范围内的随机数
module RNG(
input CLK, // 全局时钟信号
output [13:0]RandNum // 随机数输出
);
reg [31:0] Lfsr = 32'h4F3A9C1B;// 初始化线性反馈移位寄存器
always @(posedge CLK) begin
Lfsr <= {Lfsr[30:0], Lfsr[0]^Lfsr[2]^Lfsr[3]^Lfsr[31]}; // 更新线性反馈移位寄存器
end
// Lfsr[12:10]+1'b1 = Lfsr[12:0]+1024 使得随机数输出范围为[1024,9215]
assign RandNum = {Lfsr[12:10]+4'd1, Lfsr[9:0]};
endmodule
毫秒级定时器模块:
本质上就是一个计数器,控制信号触发时启动计数,计数结束发出一个脉冲。
// 毫秒级定时器
module Timer#(
parameter WIDTH = 14 // 定时时间的位宽
) (
input CLK, // 全局时钟信号
input CLK_ms, // 毫秒时钟信号
input Ctrl_CP, // 启动脉冲(高电平触发)
input [WIDTH-1:0]Time_ms, // 定时时间
output Timeout // 定时结束高脉冲信号
);
reg Timing = 0;
always @(posedge CLK) begin
if (Timeout) begin
Timing <= 0;
end else if(Ctrl_CP) begin
Timing <= 1'b1;
end
end
// 计时
reg [WIDTH-1:0]Counter = 0;
reg Timeout_CC = 0;
always @(posedge CLK_ms, negedge Timing) begin
if (!Timing) begin
Counter <= 0;
end else if(Counter==Time_ms) begin
Timeout_CC <= ~Timeout_CC;
Counter <= 0;
end else begin
Counter <= Counter+1'b1;
end
end
Once_Pulse Timeout_Pulse (
.CLK ( CLK ),
.Ctrl ( Timeout_CC ),
.Pulse ( Timeout )
);
endmodule
PWM脉冲生成模块:
此模块是为了实现LED灯低亮度的效果,产生高电平为1/16个周期的PWM信号即可。
// 6%脉冲宽度PWM脉冲发生器
module PWM_6#(
parameter CLKRATE = 12_000_000, // 全局时钟信号频率(Hz)
parameter PERIOD_us = 4 // 脉冲周期(微秒)
) (
input CLK, // 全局时钟信号
output reg Out = 1'b1 // PWM脉冲信号
);
localparam PERIOD = ((CLKRATE * PERIOD_us)/1_000_000) - 1;
localparam PERIOD_HIGH = ((CLKRATE * PERIOD_us)/16_000_000) - 1;
localparam WIDTH = $clog2(PERIOD+1);
//计数器
reg [WIDTH-1:0] Counter = 0;//周期计时
always @(posedge CLK) begin
if (Counter == PERIOD) begin
Counter <= 0;
Out <= 1'b1;
end else if(Counter == PERIOD_HIGH) begin
Out <= 0;
Counter <= Counter+1'b1;
end else begin
Counter <= Counter+1'b1;
end
end
endmodule
顶层模块:
两个队友的状态使用状态机来实现:TESTING(测试过程中) -> FINISH(完成8次测试) -> AVG(完成平均) -> OVER(比赛结果)
反应时间使用毫秒计时器来判定,由于数码管只能显示两位,反应时间为255毫秒以上的按照255毫秒来计算。每次的测试结果使用同步RAM来存储,计算平均数时先使用8个时钟周期来完成求和,再求平均值,由于8能够被2整除所以求平均值只要截取Sum[10:3]即可。
在项目需求之外,我还添加了一个RST复位信号,在比较结果后可以拨动开关重新开始测试。还利用数码管的小数点来提示是否按下了“响应”按钮,小数点熄灭代表“响应”按钮已经按下,亮起代表“响应”按钮未按下。
其他地方代码注释里都有写,就不做过多说明了。
module Reaction_Time_Test (
input CLK,
input [3:0]Key_Raw,
input SW,
input RST,
output reg [7:0]LED = 8'b1111_1111,
output reg [2:0]RGB_1,
output reg [2:0]RGB_2,
output [17:0]LEDSD
);
reg T_minus = 0;
// 毫秒时钟信号模块
wire CLK_ms;
Clock_msec #(
.CLKRATE ( 12_000_000 ))
Clock_ (
.CLK ( CLK ),
.Clkout ( CLK_ms )
);
// PWM脉冲发生模块
wire PWM;
PWM_6 #(
.CLKRATE ( 12_000_000 ),
.PERIOD_us ( 4 ))
PWM_ (
.CLK ( CLK ),
.Out ( PWM )
);
// 按键消抖模块
wire K1;
wire Respond;
wire Average;
wire Compare;
Key_Debounce #(
.NUM ( 4 ))
u_Key_Debounce (
.CLK ( CLK ),
.CLK_ms(CLK_ms),
.Key_Raw ( Key_Raw ),
.Pulse ( {Compare, Average, Respond, K1} )
);
reg [2:0] Stage_A = 0;// 队友A的测试进度
reg [2:0] Stage_B = 0;// 队友B的测试进度
// 切换队友AB
wire [2:0] Stage = SW ? Stage_A : Stage_B;
localparam
B = 1'b0,
A = 1'b1;
reg Winner;
// 测试进度状态机
localparam
TESTING = 2'b00,
FINISH = 2'b01,
AVG = 2'b10,
OVER = 2'b11;
reg [1:0] State_A = TESTING;
reg [1:0] State_B = TESTING;
wire [1:0] State = SW ? State_A : State_B;
// 状态转移
always @(posedge CLK) begin
case (State_A)
TESTING :begin
if (Respond && SW && Stage_A==3'd7 && !T_minus) begin
State_A <= FINISH;
end
end
FINISH :begin
if (Average && SW) begin
State_A <= AVG;
end
end
AVG :begin
if (Compare) begin
State_A <= OVER;
end
end
OVER :begin
if (RST) begin
State_A <= TESTING;
end
end
default : State_A <= TESTING;
endcase
end
always @(posedge CLK) begin
case (State_B)
TESTING :begin
if (Respond && !SW && Stage_B==3'd7 && !T_minus) begin
State_B <= FINISH;
end
end
FINISH :begin
if (Average && !SW) begin
State_B <= AVG;
end
end
AVG :begin
if (Compare) begin
State_B <= OVER;
end
end
OVER :begin
if (RST) begin
State_B <= TESTING;
end
end
default : State_B <= TESTING;
endcase
end
// 状态输出
always @(*) begin
case (State_A)
TESTING :begin
RGB_1 = 3'b101;
end
FINISH :begin
RGB_1 = 3'b110;
end
AVG :begin
RGB_1 = 3'b011;
end
OVER :begin
if (Winner == A) begin
RGB_1 = 3'b000;
end else begin
RGB_1 = {3{~PWM}};// PWM调节亮度
end
end
default : RGB_1 = 3'b101;
endcase
case (State_B)
TESTING :begin
RGB_2 = 3'b101;
end
FINISH :begin
RGB_2 = 3'b110;
end
AVG :begin
RGB_2 = 3'b011;
end
OVER :begin
if (Winner == B) begin
RGB_2 = 3'b000;
end else begin
RGB_2 = {3{~PWM}};// PWM调节亮度
end
end
default : RGB_2 = 3'b101;
endcase
end
wire Start = (K1 & State==TESTING);
// 随机倒计时
// 随机数生成模块
wire [13:0] RandNum;
RNG Random (
.CLK ( CLK ),
.RandNum ( RandNum )
);
reg [13:0] LockedRandNum = 0;// 锁定当前随机数
always @(posedge CLK) begin
if (Start) begin
LockedRandNum <= RandNum;
end
end
// 定时器模块
wire Timeout;
Timer #(
.WIDTH ( 14 ))
Countdown (
.CLK ( CLK ),
.CLK_ms ( CLK_ms ),
.Ctrl_CP ( Start ),
.Time_ms ( LockedRandNum ),
.Timeout ( Timeout )
);
always @(posedge CLK) begin
if (Start) begin
T_minus <= 1'b1;
end else if(Timeout) begin
T_minus <= 1'b0;
end
end
// LED灯控制
always @(posedge CLK) begin
if (Timeout) begin// 倒计时结束点亮相应测试的LED灯
LED[Stage] <= 1'b0;
end else if(Start&& State==TESTING) begin// 一次检测结束熄灭灯
LED <= 8'b1111_1111;
end else if(State==AVG) begin
LED <= 8'b0000_0000;
end
end
// 毫秒计时器
reg [7:0] Counter = 0;
always @(negedge CLK_ms, posedge T_minus) begin
if (T_minus) begin
Counter <= 0;
end else if(Counter != 8'd255) begin// 最大255,超过255按照255计算
Counter <= Counter+1'b1;
end
end
// 保存记录
reg [7:0] Record_A[7:0];
reg [7:0] Record_B[7:0];
reg [8:0] SR = 0;
reg [2:0] Index;
always @(negedge CLK) begin
if (State != AVG) begin
Index = Stage;
end else if(SR[6]) begin
Index = 3'd7;
end else if(SR[5]) begin
Index = 3'd6;
end else if(SR[4]) begin
Index = 3'd5;
end else if(SR[3]) begin
Index = 3'd4;
end else if(SR[2]) begin
Index = 3'd3;
end else if(SR[1]) begin
Index = 3'd2;
end else if(SR[0]) begin
Index = 3'd1;
end else begin
Index = 3'd0;
end
end
reg First_A = 0;
reg First_B = 0;
reg Recording = 0;
always @(posedge CLK) begin
if (RST) begin
First_A <= 0;
First_B <= 0;
Stage_A <= 0;
Stage_B <= 0;
end else if (Start) begin
if (SW && Stage!=3'd7 && First_A) begin
Stage_A <= Stage+1'b1;
end else if(!SW && Stage!=3'd7 && First_B) begin
Stage_B <= Stage+1'b1;
end
Recording <= 1'b1;
end else if (Respond && State==TESTING && !T_minus && Recording) begin
Recording <= 0;
if (SW) begin
First_A <= 1'b1;
Record_A[Index] <= Counter;
end else begin
First_B <= 1'b1;
Record_B[Index] <= Counter;
end
end
end
// 计算平均数
reg [7:0] Average_A = 0;
reg [7:0] Average_B = 0;
reg [10:0] Sum = 0;
always @(posedge CLK) begin
if (SR[7] && !SR[8] && SW) begin
Average_A <= Sum[10:3];
end else if(SR[7] && !SR[8] && !SW) begin
Average_B <= Sum[10:3];
end
end
always @(posedge CLK) begin
if (Average && State==FINISH) begin
SR <= 0;
Sum <= 0;
end else if (State==AVG) begin
SR[8:1] <= SR[7:0];
SR[0] <= 1'b1;
if (SW) begin
Sum <= Sum+Record_A[Index];
end else begin
Sum <= Sum+Record_B[Index];
end
end
end
// 比较大小
always @(*) begin
if (Average_A < Average_B) begin
Winner = A;
end else begin
Winner = B;
end
end
// 数码管显示数字
reg [7:0] Ave;
always @(*) begin
if (State == OVER && Winner == A) begin
Ave = Average_A;
end else if(State == OVER && Winner != A) begin
Ave = Average_B;
end else if(SW) begin
Ave = Average_A;
end else begin
Ave = Average_B;
end
end
reg [7:0] DataShow;
always @(*) begin
if (Recording) begin
DataShow = Counter;
end else if(State == AVG || State == OVER) begin
DataShow = Ave;
end else begin
if (SW) begin
DataShow = Record_A[Index];
end else begin
DataShow = Record_B[Index];
end
end
end
// 数码管显示模块
LEDSD_Direct #(
.NUM ( 2 ))
LEDSD_Display (
.DataIn ( DataShow ),
.DIG ( 2'b00 ),
.Dp ( {1'b0,Recording} ),
.LEDSD ( LEDSD )
);
endmodule
遇到的难题
小脚丫思得普的WebIDE对Verilog HDL语法要求比较严格,例如变量声明必须在变量使用之前,否则会报错,据我所知主流综合器一般只会给警告,这样做应该也是为了综合器的性能考虑吧,这个问题在一开始困扰了我很久。
未来的计划
- 复位信号修改成任意时刻生效
- 毫秒时钟信号使用IP核来调用PLL资源实现
- 平均数或许可以存储到保存数据的RAM中,节省寄存器资源
- 8次测试结束后自动计算平均数,队友A、队友B共用同一个RAM
- 支持大于255毫秒的反应时间
- 支持2人以上的测试