简介这份资源是东华大学数字电子技术课程设计的「生理刺激反应时间测试仪」完整工程包面向电子信息、自动化等专业学习FPGA与数字逻辑设计的学生及自学者。项目以FPGA为核心配合Quartus II完成刺激信号生成、反应时间记录与结果显示三大模块可用于教学实验、科研数据采集与健康评估等场景。压缩包共173个文件约1.02MB包含bdf原理图、bsf元件符号、vwf仿真波形、cdb与hdb编译数据库、qsf与qpf工程配置、sof与pof下载文件以及docx数电报告、xlsx数据表、bmp与png图示和readme说明覆盖从设计、仿真到下载验证的完整流程。已有1234人学习下载。读者可获取可综合的Verilog/VHDL逻辑代码、分模块原理图、仿真波形与实验报告理解平均反应时间、标准差等统计方法并借鉴触发器检测与数据处理思路快速复现并扩展自己的课设方案。1. 从一块开发板到一台“生理刺激反应时间测试仪”数电课设里最值得认真做的一个题生理刺激反应时间测试仪说白了就是一台用硬件测“人从看到/听到信号到按下按键”这段时间的装置。它不靠电脑打表而是用 FPGA 内部的高频时钟去数数把反应时间精确到毫秒级。这个题目在数电课设里出现的频率很高因为它把分频、计数、状态机、数码管动态扫描、按键消抖这几个核心知识点全串起来了做完一遍时序逻辑那点玄学基本就通了。它适合两类人一类是正在做数电课设、手里有一块 FPGA 开发板和 Quartus II 的同学想找一个能写进报告、又能真正跑通的方案另一类是想用 FPGA 做简单人机交互测量、需要一套可复现框架的开发者。下面这套方案我在实验室里带人复现过多次从分频参数到消抖窗口都调过踩过的坑会一并写清楚。2. 反应时间测试仪的硬件逻辑怎么搭从时钟分频到状态机2.1 为什么用 1kHz 采样时钟而不是直接拿 50MHz 去计数反应时间的量级在 150ms 到 500ms 之间人眼和手动的极限大概在 100ms 出头。如果用板载 50MHz 晶振直接计数一次 300ms 的反应会得到 15,000,000 个计数值这个数要转成毫秒显示得做除法而除法在 FPGA 里要么耗逻辑资源要么得用 IP 核对课设来说不划算。常见做法是先分频出一个 1kHz 的使能信号每 1ms 让计数器加一。这样计数值直接就是毫秒数省掉除法显示逻辑也简单。分频比是 50,000,000 / 1,000 50,000也就是数到 24,999 翻转一次从 0 开始计。这个参数在 50MHz 板子上通用如果你的板子是 12MHz 或 24MHz把分频上限按同样思路改掉即可。// 1kHz 使能信号生成假设输入时钟 50MHz module clk_en_1kHz( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低电平复位 output reg en_1ms // 每 1ms 拉高一个周期 ); reg [15:0] cnt; // 50000 需要 16 位 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; en_1ms 1b0; end else if (cnt 16d49999) begin cnt 16d0; en_1ms 1b1; // 只拉高一个时钟周期 end else begin cnt cnt 1b1; en_1ms 1b0; end end endmodule这段代码的关键在en_1ms只拉高一个时钟周期它是一个使能脉冲不是占空比 50% 的方波。很多新手会写成翻转式分频结果计数器每个周期加好几次读数直接翻倍。参数上cnt的位宽要能装下 4999916 位最大 65535够用如果系统时钟更高比如 100MHz分频上限变成 99999还是 16 位能装下但再高就要扩位宽。2.2 状态机怎么划分IDLE、WAIT、COUNT、DONE 四态够不够整个测试流程可以拆成四个状态。IDLE 是待机数码管显示全零或上次结果WAIT 是随机延迟阶段系统先等一段随机时间再给刺激防止测试者预判COUNT 是刺激出现、开始计数的阶段DONE 是按键按下、锁定结果并显示。随机延迟这一步很多人偷懒不做直接按开始就立刻亮灯结果测出来的反应时间普遍偏短因为测试者手指已经悬在按键上了。常见做法是用一个自由运行的计数器在 WAIT 态取值取到某个阈值就跳转这样每次延迟不同能有效打掉预判。// 四态状态机核心片段 localparam IDLE 2d0, WAIT 2d1, COUNT 2d2, DONE 2d3; reg [1:0] state, next_state; reg [23:0] rand_cnt; // 自由运行计数器用于随机延迟 reg [15:0] ms_cnt; // 毫秒计数 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; rand_cnt 24d0; ms_cnt 16d0; end else begin rand_cnt rand_cnt 1b1; // 一直跑不停 case (state) IDLE: if (start_btn) state WAIT; WAIT: if (rand_cnt[23:20] 4hF) state COUNT; // 取高位做随机 COUNT: begin if (en_1ms) ms_cnt ms_cnt 1b1; if (react_btn) state DONE; end DONE: if (start_btn) begin ms_cnt 16d0; state WAIT; end endcase end end这里rand_cnt[23:20]取的是自由计数器的高四位等它等于 15 时跳转实际延迟在 0 到 15 个计数周期之间浮动按 50MHz 算大概是 0 到 0.3 秒左右够用。如果你觉得随机性不够可以取更多位或者做异或折叠。ms_cnt用 16 位最大能记 65535ms远超人类反应时间不会溢出。2.3 数码管动态扫描为什么你的显示总在闪四位数码管要显示毫秒值必须用动态扫描。原理是轮流点亮每一位利用人眼视觉暂留看起来像同时亮。扫描频率一般取 1kHz 左右也就是每 1ms 换一位四位轮一遍 4ms刷新率 250Hz肉眼完全看不出闪烁。翻车点在于扫描时钟和计数时钟混用。如果你直接拿 1ms 使能去驱动扫描而扫描逻辑又写在同一个 always 块里综合出来可能出现竞争显示会抖。稳妥做法是单独分一个 1kHz 的扫描使能或者直接用系统时钟分频到 1kHz 给扫描用和计数使能分开。// 数码管动态扫描seg_sel 位选seg_data 段选 reg [1:0] scan_idx; reg [3:0] disp_digit; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_idx 2d0; end else if (scan_en) begin // scan_en 为 1kHz 使能 scan_idx scan_idx 1b1; end end always (*) begin case (scan_idx) 2d0: begin seg_sel 4b1110; disp_digit ms_cnt % 10; end 2d1: begin seg_sel 4b1101; disp_digit (ms_cnt / 10) % 10; end 2d2: begin seg_sel 4b1011; disp_digit (ms_cnt / 100) % 10; end 2d3: begin seg_sel 4b0111; disp_digit (ms_cnt / 1000) % 10;end endcase end取模和除法在综合时会被展开成组合逻辑对四位十进制数来说资源消耗可以接受。如果你在意面积可以改成 BCD 计数器每 1ms 对四个 BCD 位做加一进位显示时直接取位省掉除法器。参数上scan_en的频率不要低于 200Hz否则会有可见闪烁也不要高于 10kHz否则段选信号还没稳定就切换亮度会下降。3. 在 Quartus II 里把工程跑起来引脚、时序与下载3.1 工程建立与文件组织一个顶层加三个子模块在 Quartus II 里新建工程时器件型号按你板子上的实际芯片选常见的是 Cyclone IV 系列。工程目录建议这样组织顶层文件放top.v下面挂clk_en_1kHz.v、debounce.v、seg_driver.v三个子模块再加一个.qsf约束文件。顶层只做例化和连线不写具体逻辑这样改哪个模块都不影响其他部分。引脚分配是新手最容易卡住的地方。时钟输入要分配到板子的晶振引脚按键分配到带消抖电路的按键引脚数码管段选和位选按板子原理图对应分配。分配完一定要跑一次全编译看 Fitter 报告里有没有未分配引脚或电平标准冲突。# 引脚约束示例具体引脚号按你的板子原理图改 set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M15 -to rst_n set_location_assignment PIN_M16 -to start_btn set_location_assignment PIN_N15 -to react_btn set_location_assignment PIN_A3 -to seg_sel[0] set_location_assignment PIN_B4 -to seg_sel[1] set_location_assignment PIN_A4 -to seg_sel[2] set_location_assignment PIN_B5 -to seg_sel[3]这段 Tcl 可以直接贴进.qsf文件也可以在 Assignment Editor 里图形化操作。注意seg_sel是四位位选每一位都要单独分配漏掉一位会导致对应数码管不亮。电平标准默认 3.3V LVCMOS 即可如果你的板子数码管是共阳的段选输出要取反这个在seg_driver里改一行就行。3.2 按键消抖20ms 窗口是怎么算出来的机械按键按下时会有 5ms 到 10ms 的抖动如果不消抖一次按下会被状态机识别成多次触发计数直接乱掉。常见做法是用一个计数器在检测到按键电平变化后开始计数计满 20ms 再确认状态。20ms 这个值来自经验小于 10ms 消不干净大于 50ms 手感变迟钝。// 20ms 消抖假设 1kHz 使能 module debounce( input wire clk, input wire en_1ms, input wire key_in, output reg key_out ); reg [4:0] cnt; // 20 需要 5 位 reg key_sync; always (posedge clk) key_sync key_in; // 打一拍同步 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 5d0; key_out 1b0; end else if (en_1ms) begin if (key_sync ! key_out) begin if (cnt 5d19) begin key_out key_sync; cnt 5d0; end else begin cnt cnt 1b1; end end else begin cnt 5d0; end end end endmodulekey_sync打一拍是为了做跨时钟域同步虽然这里都是同一个时钟但能消除亚稳态风险。cnt计到 19 就是 20 个 1ms 周期正好 20ms。如果你的板子按键是低电平有效把key_out取反再用即可。这个模块对start_btn和react_btn各例化一次互不干扰。3.3 时序约束与编译报告怎么看Quartus II 默认不做时序约束也能编译但报告里的时序数据不可信。建议至少加一条时钟约束告诉工具系统时钟是 50MHz。在.sdc文件里写create_clock -period 20.000 -name clk [get_ports clk]然后重新跑全编译。编译完重点看两个报告Fitter 的 Resource Utilization 看逻辑单元和寄存器用了多少如果超过 80% 就要考虑优化TimeQuest 的 Setup Summary 看最差路径的 slack如果是负数说明时序不满足需要降低逻辑深度或加流水线。反应时间测试仪的逻辑很简单正常情况下 slack 都是正的如果为负多半是除法器那条路径太长改成 BCD 计数就能解决。4. 避坑与排查那些让读数翻倍或显示乱跳的瞬间4.1 读数总是实际值的两倍现象是测出来 400ms但用手机秒表对照大概只有 200ms。原因通常是 1ms 使能信号每个时钟周期都在拉高而不是只拉高一个周期。检查clk_en_1kHz里的en_1ms赋值确认它在计数到 49999 时置一下一个周期必须回零。如果写成en_1ms ~en_1ms那就是 50% 占空比计数器每个周期加两次。4.2 按下按键后数码管显示乱跳现象是 DONE 态显示不稳定数字在几个值之间闪。原因是ms_cnt在 DONE 态还在被en_1ms驱动或者显示逻辑直接接了计数器的组合输出。解决方法是进入 DONE 态时把ms_cnt锁存到一个单独的寄存器result_reg显示只读result_reg计数器和显示彻底解耦。4.3 随机延迟不起作用每次都是同一个时间现象是刺激出现的延迟感觉每次都一样测试者能预判。原因是rand_cnt在 WAIT 态被复位了或者取值条件写成了固定值。检查rand_cnt是否在复位后一直自由运行没有在状态切换时被清零。另外取随机位的时候不要只取一位取四位以上并且用异或或者拼接增加随机性。4.4 下载后板子没反应数码管全灭现象是编译通过、下载成功但板子像没通电一样。先查引脚分配特别是时钟引脚有没有分到普通 IO 上那样时钟进不来。再查复位按键如果rst_n分配到了没有上拉的引脚系统一直处于复位态。最后查数码管共阳共阴共阳数码管段选要给低电平才亮如果代码里给的是高电平就是全灭。4.5 编译报错说多个驱动源冲突现象是综合报错提示某个信号有多个驱动。原因通常是在两个 always 块里对同一个 reg 赋值比如ms_cnt既在状态机里清零又在计数逻辑里加一。解决方法是把所有对同一寄存器的操作合并到一个 always 块里用状态机统一控制清零和使能。5. 把测量精度再压一压从毫秒到亚毫秒的几个改法上面这套方案精度是 1ms对课设来说够了。但如果你想在报告里写出更有说服力的数据或者想把这个东西做成一个能反复用的测量工具有几个地方可以再压。第一个改法是把计数时钟从 1kHz 提到 10kHz也就是 0.1ms 分辨率。分频上限从 49999 变成 4999ms_cnt的位宽不变但显示的时候要处理小数点。常见做法是显示四位最后一位是 0.1ms 位中间加一个小数点段常亮。这样测出来的反应时间能到 200.3ms 这种粒度数据好看很多。第二个改法是加一个多次测量取平均的模式。在 DONE 态之后不直接回 IDLE而是把结果存进一个寄存器组连续测五次后算平均。平均值用累加后右移三位实现不用除法器。这个逻辑加一个 3 位计数器和一个 16 位累加器就行资源消耗很小。第三个改法是刺激源多样化。现在只有 LED可以再加一个蜂鸣器做声音刺激用同一个状态机控制测试者可以选择视觉或听觉模式。声音刺激的反应时间通常比视觉短 20ms 到 30ms这个差异写进报告里是一个很好的讨论点。// 0.1ms 分辨率下的显示处理result_reg 单位是 0.1ms wire [3:0] d0 result_reg % 10; // 0.1ms 位 wire [3:0] d1 (result_reg / 10) % 10; // 1ms 位 wire [3:0] d2 (result_reg / 100) % 10; // 10ms 位 wire [3:0] d3 (result_reg / 1000) % 10; // 100ms 位 // 小数点段在 d1 对应的位上常亮具体段码按你的数码管编码改这段代码把 0.1ms 单位的计数值拆成四位十进制显示时在第二位和第三位之间点亮小数点。注意result_reg的位宽要够0.1ms 分辨率下 500ms 对应 500016 位寄存器绰绰有余。我自己带人做这个题的时候最常提醒的一句话是先把 1ms 版本跑通读数稳定了再去改分辨率。很多人一上来就想做 0.1ms结果消抖没做好、显示没解耦调到最后连 1ms 的读数都信不过。先让数码管老老实实显示一个稳定的数再去抠那零点几毫秒这个顺序别反。希望帮到你。本文还有配套的精品资源点击获取