押题五:按键秒表与反应测试

命中方向:GPIO 中断、按键消抖、长短按、毫秒计时、随机等待、NVS 最佳成绩。

模拟任务书

实现秒表和反应测试两种功能。秒表模式下短按开始,再次短按停止,长按清零但保留最佳成绩。Touch 启动反应测试:系统随机等待 1.5–4 秒后亮绿灯,参赛者尽快按键;提前按键判定失败。

  1. 计时精度达到毫秒级,计时期间串口和 Touch 保持响应。
  2. 最好成绩写入 NVS,断电后仍能查询。
  3. 不同状态用 RGB 和蜂鸣器明确提示。

建议评分点

评分项分值
中断与可靠消抖15
秒表开始、停止、复位20
Touch 启动随机反应测试15
提前按键与有效反应判定15
RGB/蜂鸣器状态提示10
最佳成绩 NVS 保存15
串口控制和错误处理10

总分:100

直接使用

工程:arduino_pio/mock_05_button_stopwatch。按键接 GPIO15 与 GND,Touch 导电片接 GPIO4。

START
STOP
RESET
REACTION
BEST
CLEARBEST
STATUS

主要修改:src/task_logic.cpp中的 shortPress()startReaction() 和状态枚举。

高概率变题

  • 秒表改为按键计数,达到次数后报警。
  • 反应测试改为抢答器,Touch 与按键代表两名选手。
  • 最佳成绩改为累计次数或最长持续时间。
  • 要求 10 秒无操作自动复位。

现场修改指南:先画状态图

当前代码已经实现秒表和单人反应测试。先把现场流程写成“什么状态 + 什么输入 → 下一个状态”,再改 ModeshortPress()tick(),不要从灯光代码开始猜。

题目一句话对应哪里

题目关键词函数/变量作用
有哪些状态enum ModeREADY、RUNNING、STOPPED、REACTION_WAIT、REACTION_GO
完成一次计时finishTiming()保存本次结果、更新最佳成绩并鸣叫
按键规则shortPress()根据当前状态开始、停止、判提前或记录反应时间
长按清零longPress()清本次结果但保留最佳成绩
随机等待startReaction()生成 1500..4000 ms 截止时刻
到时亮灯tick()WAIT 到期后进入 GO,并控制各状态输出
串口 START/STOPcommand()复用同一套计时状态

变量翻译

代码含义
started_ms秒表或反应计时真正开始的时刻
reaction_ready_ms随机等待结束、允许按键的时刻
last_elapsed_ms最近一次完成成绩
user1NVS 中的历史最佳毫秒数
user2NVS 中的上次结果
now_ms - started_ms非阻塞计算已经经过多少毫秒

最安全:只改数字

现场要求找到并修改
随机等待改 2..5 秒1500 + (esp_random() % 2501)改为 2000 + (esp_random() % 3001)
完成提示改 1800 Hz/300 msstartBeep(2200, 160)改为 startBeep(1800, 300)
提前按键提示改 800 Hz/200 msstartBeep(500, 400)改为 startBeep(800, 200)
运行灯翻转改 500 msnow_ms / 250改为 now_ms / 500

随机公式:最小值 + random % (最大值 - 最小值 + 1)。单位都是毫秒。

四种高概率变题:可直接照着改

示例 A:只保留普通秒表,短按开始/停止

替换范围:完整替换 shortPress()。其他反应测试函数即使保留也不会影响普通秒表。

void shortPress(contest::ContestApp &app) {
    const uint32_t now_ms = millis();

    if (app.config().mode == RUNNING) {
        finishTiming(app, now_ms - started_ms, "STOPWATCH");
    } else {
        started_ms = now_ms;
        app.setMode(RUNNING);
        app.emit("STATE STOPWATCH=RUNNING");
    }
}

如果题目要求 Touch 也开始/停止,把 touchChanged()中的 startReaction(app)改成 shortPress(app)

示例 B:秒表运行 10 秒后自动停止

插入位置:tick()开头、switch之前加入:

if (app.config().mode == RUNNING &&
    now_ms - started_ms >= 10000) {
    finishTiming(app, 10000, "AUTO_STOP");
}

这里不使用 delay(10000),所以 10 秒期间串口、Touch 和按键仍然响应。

示例 C:改成按键计数,达到 10 次报警

1. 在全局变量区加入:

uint32_t press_count = 0;

2. 完整替换 shortPress()

void shortPress(contest::ContestApp &app) {
    ++press_count;
    app.emit("COUNT VALUE=%lu",
             static_cast<unsigned long>(press_count));

    if (press_count >= 10) {
        app.setMode(STOPPED);
        app.setRgb(90, 0, 0);
        app.startBeep(2200, 300);
    }
}

3. 完整替换 longPress()作为计数清零:

void longPress(contest::ContestApp &app) {
    press_count = 0;
    app.setMode(READY);
    app.emit("COUNT RESET");
}

同步修改:tick()仍按 STOPPED 显示黄色,它会覆盖上面设置的红色。应同时把 tick()的 STOPPED 颜色改红,或者只让 tick()负责颜色。

示例 D:反应测试等待改为固定 3 秒

替换范围:完整替换 startReaction()

void startReaction(contest::ContestApp &app) {
    reaction_ready_ms = millis() + 3000;
    app.setMode(REACTION_WAIT);
    app.emit("STATE REACTION=WAIT");
}

题目要求随机时间时不要写固定值;题目要求精确 3 秒时固定值更容易验收。

改完后的最短验收

  1. 输入 START,等待约 1 秒后输入 STOP,结果应接近 1000 ms。
  2. 按键短按开始/停止,串口结果与操作一致;长按只清本次结果。
  3. 输入 REACTION后提前按键,必须判失败;绿灯后按键才记录成绩。
  4. 输入 BEST,重启后再次查询,最佳成绩应保留。
  5. 计时期间连续输入 STATUS,计时不能暂停。

不要改:按键中断和消抖在公共库中。增加或删除状态时必须同步 Mode枚举、tick()switch以及 createTaskHooks()最大模式。