项目需求
设计一款反应时间测试系统,测试两个队友(队友A和队友B)看到LED亮起后按键的时间,将响应时间显示在数码管上,每个人测量8次做平均,并将两个人的响应时间作对比,显示出哪一方赢得比赛。
游戏规则:
按下“启动”按钮后,两个7 段显示屏立即设置为显示全0,然后随机一段时间后(大约1 到10秒),相应测试轮次的“立即反应”LED 亮起,并启动毫秒计时器,数码管开始显示计时器值(以毫秒为单位递增)。
“立即反应”LED 亮起后,队友必须尽快按下“响应”按钮来停止计时器。 停止的计时器将包含“立即反应”LED亮起和按钮按下之间的毫秒数,并且该时间将显示在数码管上。
再次按下“启动”按钮将清除计时器并开始新的测试。
重复单个反应时间测量八次,每一个测试,相应的8个LED中的一个亮起,并将八个测量的反应时间值存储在临时保持寄存器中。
按下“平均”按钮可平均8次的测量值,并将平均后的数值显示在数码管上,8个LED全部亮起。
切换队友 - 将SW1的状态改变,重复测试8次,并平均八次的测试结果。
按下“比较”按钮,通过RGB三色灯指示哪个队友获胜(平均用时最短),并在数码管上显示相应的响应时间。
需求分析
本项目要求使用FPGA实现一个反应测试系统,主要功能有测量反应时间,求取反应时间平均值和数据比较功能,此外还需要数码管、LED灯驱动功能。游戏规则已在项目需求中清晰表述,此处不再赘述。由大量统计数据得,人的反应时间不会小于100ms,使用1ms为单位计时将无法在数码管上完整显示,使用5ms为单位并不能直观地看出反应时间的具体数值,因此本项目采用10ms为单位记录与显示反应时间。
系统设计
经过分析,游戏的主要功能,即反应速度的测试和评比,可采用状态机进行设计,同时自动根据当前状态更新LED、数码管的显示状态,如图1所示。
图1 游戏状态机设计
对于数据存储模块,可采用类似移位寄存器的形式循环存储最新的8次反应时间,当数据改变时自动求取最近8次测试的平均值,如图2所示。
图2 数据存储模块设计
将12MHz的输入时钟频率进行分频得到100Hz信号用于记录反应时间与进行特定时间的延时。通过时钟信号驱动一个加法计数器,当用户按下“启动”按钮后取出计数器的值,达到“随机延时”的效果。同时分频信号也用于毫秒计时器的输入时钟。
因为RGB LED需要有常亮和低亮度两种效果,要求精度不高,因此可以采用如下方法设计LED驱动模块。流程图如图3所示
图3 PWM模块设计
此模块由系统时钟驱动,当寄存器的值为3时,打开LED并清零寄存器,否则关闭LED并使寄存器自增。易得寄存器值为3的循环次数占总次数的四分之一。用这种方法实现的PWM不仅可以较为高效地利用FPGA资源,而且可以使LED亮度差异更明显。
在主模块中例化两个RGB LED模块,分别代表队友1和队友2。模块接受一个当前状态输入,查表后(PWM通过带条件表达式的assign语句实现)打开对应颜色的LED输出。
因为项目需求中对各个按钮的分工明确,逻辑上没有出现一个按钮同时作用于两个状态的情况,因此不需要对按钮进行额外的消抖操作,直接在状态机中判断按钮电平,切换状态即可。
数码管驱动
因为毫秒计时器的输出是二进制数,而数码管需要2位10进制数作为输入。在单片机平台一般通过对10进行整除来实现。但FPGA没有硬件除法器,直接进行除法和取余数操作将会消耗较多的FPGA资源。因此采用“加3移位法”将二进制转化为BCD码来实现此功能。因为本人水平有限,此处采用最直接的方法实现此功能。
关键代码实现
随机数生成代码
用FPGA产生随机数是本项目的一个难点。为了用较为简单的方式实现不可预测的随机数,我采用一个计数器(counter)在每个时钟自增。当用户按下按钮1开始测试时,顶层模块向RandDelay模块传递开始随机延时信号(reset),此时随机延时模块从counter中取出当前的计数值,然后利用10毫秒时钟延时一段时间。因为用户按下按钮的时间相对FPGA启动时间是随机的,同时因为输入时钟频率非常高,计数器更新的速度也非常快,不可能人为控制产生的随机数大小,因此可以实现随机延时的效果。
特别地,因为项目要求随机延时不小于1秒,需要判断定时器的值是否小于100。第一次我的方案如流程图所示,每次循环判断计数值。为了节约fpga资源,改为每次装载定时器数据时自动将counter的值加100,实现最少延时1秒的效果。
module RandDelay (
input wire reset,
input wire clk,
input wire clk_10ms,
output reg timeout
);
reg [8:0] rnd = 9'd100;
always @(posedge clk) begin
rnd = rnd + 9'b1;
end
reg [9:0] counter;
reg [9:0] delay;
always @(posedge clk_10ms or posedge reset) begin
if (reset) begin
counter <= 0;
timeout <= 0;
delay <= rnd + 10'd100;
end else begin
if (counter < delay) begin
counter <= counter + 10'd1;
timeout <= 0;
end else begin
timeout <= 1;
end
end
end
endmodule
二进制转BCD代码
module bin_to_bcd(
input wire [6:0] bin, // 8位二进制输入
output [7:0] bcd // BCD输出,4位用于十位,4位用于个位
);
integer i;
reg [14:0] shift_reg; // 扩展的移位寄存器,包括8位输入和额外的4位用于处理过程
always @(bin) begin
shift_reg = {8'b00000000, bin}; // 初始化移位寄存器,前4位为0,后8位为输入的二进制数
// 对每一位二进制数进行处理,共12次迭代
for (i = 0; i < 7; i = i + 1) begin
// 从左至右检查每个BCD位
if (shift_reg[10:7] > 4)
shift_reg[10:7] = shift_reg[10:7] + 4'd3;
if (shift_reg[14:11] > 4)
shift_reg[14:11] = shift_reg[14:11] + 4'd3;
// 左移整个寄存器
shift_reg = shift_reg << 1;
end
end
assign bcd = shift_reg[14:7]; // 将最终的BCD结果赋值给输出
endmodule
数据存储代码
module data_storage(
input [6:0] datain,
input save,
input clk,
output [6:0] avg
);
reg [6:0] data [7:0];
reg [9:0] sum;
reg [2:0] cnt = 3'b0;
reg [3:0] sum_idx;
initial sum_idx = 4'b1000;
// 提取信号
reg last_save;
reg save_reg;
always @(posedge clk) begin
last_save <= save_reg;
save_reg <= save;
end
always @(posedge clk) begin
if(save_reg != last_save) begin
data[cnt] = datain;
cnt = cnt + 3'b1;
sum_idx = 4'b0000;
sum = 10'd0;
end
casez (sum_idx)
4'b0zzz: begin
sum = sum + data[sum_idx];
sum_idx = sum_idx + 4'b0001;
end
4'b1000:;
default: begin
sum_idx = 4'b1000;
end
endcase
end
assign avg = sum[9:3];
endmodule
因篇幅有限,其它代码请看附件。
各个模块的仿真
因为项目代码量较大,故采用Modelsim仿真软件进行仿真。各个模块的仿真图如下。注:仿真中将10ms分频器的分频系数由60000改成了5,方便观察分频效果。
二进制转BCD模块仿真测试
仿真代码:
reg [6:0] bin;
wire [7:0] bcd;
bin_to_bcd bintobcd(
.bin(bin),
.bcd(bcd)
);
initial begin
for (bin = 0; bin < 100; bin = bin + 1) begin
#10;
end
end
数据存储与求平均值模块仿真测试
仿真代码:
reg [6:0] datain;
reg save;
reg clk;
wire [6:0] avg;
data_storage ds(
.datain(datain),
.save(save),
.clk(clk),
.avg(avg)
);
initial begin
save = 1;
clk = 0;
#100;
for (datain = 10; datain < 18; datain = datain + 1) begin
save = ~save;
#100;
end
end
always #2 clk = ~clk;
分频器仿真测试(分频系数由60000改为6)
仿真代码:
reg clk;
wire freq_out;
frequency_divider fd(
.clk(clk),
.freq_out(freq_out)
);
initial begin
clk = 0;
end
always #5 clk = ~clk;
数码管模块仿真测试
仿真代码:
reg onoff;
reg [3:0] seg_data_1;
reg [3:0] seg_data_2;
wire [8:0] seg_led_1;
wire [8:0] seg_led_2;
LEDSegment ls(
.onoff(onoff),
.seg_data_1(seg_data_1),
.seg_data_2(seg_data_2),
.seg_led_1(seg_led_1),
.seg_led_2(seg_led_2)
);
initial begin
onoff = 1;
seg_data_1 = 0;
seg_data_2 = 0;
#10;
seg_data_1 = 1;
#10;
seg_data_2 = 2;
#10;
seg_data_1 = 3;
#10;
seg_data_2 = 4;
#10;
onoff = 0;
#10;
onoff = 1;
#10;
$stop;
end
RGB LED模块仿真测试
仿真代码:
reg [2:0] stat;
reg clk;
wire r;
wire g;
wire b;
RGBLED rgb(
.state(stat),
.clk(clk),
.out_r(r),
.out_g(g),
.out_b(b)
);
initial begin
stat = 3'b000;
clk = 0;
#100;
stat = 3'b001;
#100;
stat = 3'b010;
#100;
stat = 3'b011;
#100;
stat = 3'b100;
#100;
stat = 3'b101;
#100;
#100;
#100;
end
always #5 clk = ~clk;
随机延时模块仿真测试
仿真代码:
`timescale 1ns/1ns
module testbench;
reg clk = 0;
reg clk10 = 0;
reg reset = 0;
wire timeout;
RandDelay rndDelay(
.reset(reset),
.clk(clk),
.clk_10ms(clk10),
.timeout(timeout)
);
initial begin
#100;
reset = 1;
#100;
reset = 0;
end
always @ (posedge timeout) begin
$display("Current_time: %t", $time);
#50;
reset = 1;
#50;
reset = 0;
end
always #5 clk = ~clk;
always #50 clk10 = ~clk10;
endmodule
可以看到,延时的时间差是随机的。
FPGA资源占用
Design Summary:
Number of registers: 156 out of 4635 (3%)
PFU registers: 156 out of 4320 (4%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 225 out of 2160 (10%)
SLICEs as Logic/ROM: 213 out of 2160 (10%)
SLICEs as RAM: 12 out of 1620 (1%)
SLICEs as Carry: 42 out of 2160 (2%)
Number of LUT4s: 430 out of 4320 (10%)