2024年寒假练 - 用Lattice MXO2的小脚丫FPGA核心板设计反应时间测试系统
该项目使用了Lattice MXO2的小脚丫FPGA核心板,实现了反应时间测试系统的设计,它的主要功能为:分别测试并记录两个人8次的反应时间,将获胜方的反应时间平均值显示在数码管上。
标签
FPGA
数字逻辑
2024年寒假练
SCP基金会YHW
更新2024-04-01
重庆大学
380


项目需求

  • 设计一个简易的反应时间测试系统,能够测试两个人(队友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人以上的测试


附件下载
Verilog代码.zip
项目所有代码
团队介绍
姚闳玮 重庆大学
团队成员
SCP基金会YHW
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号