押题四:交通灯与倒计时

命中方向:硬件定时器、GPIO 中断、非阻塞状态机、顺序控制、RGB、蜂鸣器、串口参数。

模拟任务书

实现一套三相交通指示控制器。红灯 5 秒、绿灯 5 秒、黄灯 1.5 秒循环。按键或 Touch 产生行人请求:绿灯至少运行 1 秒后提前转黄灯。长按按键进入红灯闪烁的紧急模式,再次长按恢复。所有计时不得使用阻塞式长延时。

  1. 串口每秒输出当前相位和剩余秒数。
  2. 串口可分别修改红、绿、黄相位时间。
  3. 紧急模式以 300 ms 周期闪红灯并短鸣。
  4. 普通 LED 在绿灯阶段点亮,其他阶段关闭。

建议评分点

评分项分值
红绿黄三相自动循环正确20
硬件定时 tick 与非阻塞状态机15
按键中断、消抖和行人请求15
Touch 行人请求10
紧急模式及恢复15
串口改时间、查询倒计时15
错误参数处理与状态日志10

总分:100

接线与工程

功能接法
RGB板载 GPIO38,若不亮改 GPIO48
按键GPIO15 与 GND
TouchGPIO4 接导电片
普通 LEDGPIO21 经 220 Ω 接 LED
蜂鸣器GPIO16

工程:arduino_pio/mock_04_traffic_timer

串口验收

STATUS
REQUEST
PHASE EMERGENCY
PHASE RED
TIME RED 3000
TIME GREEN 4000
TIME YELLOW 1000
SAVE

主要修改:src/task_logic.cpp中的 enterPhase()shortPress()tick()

高概率变题

  • 交通灯改为流水灯、工序控制或电梯状态。
  • 行人请求改为只在红灯结束后生效。
  • 倒计时结束前 3 秒增加蜂鸣提示。
  • 要求断电保存各相位时间。

现场修改指南:先画顺序再改

当前状态表是 0 红、1 绿、2 黄、3 紧急,普通顺序是红 → 绿 → 黄 → 红。如果现场只是修改每段持续时间,直接使用 TIME命令;只有顺序、阶段数量或请求规则变化才改源码。

题目一句话对应哪里

题目关键词函数/变量作用
阶段名称phaseName()把 mode 0..3 翻译成 RED/GREEN/YELLOW/EMERGENCY
每段持续时间phaseDuration()user1/user2/user3取红绿黄时间
进入新阶段enterPhase()统一改变 mode 并重新开始计时
短按/Touch 请求shortPress()只记录请求,何时执行由 tick()决定
长按紧急longPress()进入紧急或恢复之前阶段
顺序和倒计时tick()本题最重要的非阻塞状态机
串口阶段/时间command()解析 REQUEST、PHASE、TIME

变量翻译

代码含义
phase_started_ms当前阶段开始时刻
pedestrian_request是否已有行人/外部请求等待处理
resume_mode进入紧急前的状态,用于恢复
user1/user2/user3红、绿、黄持续毫秒数,可由 SAVE 保存
elapsed当前阶段已经运行了多久
remaining当前阶段剩余毫秒数

最安全:只改时间和提示

现场要求做法
红 3 秒TIME RED 3000
绿 8 秒TIME GREEN 8000
黄 1 秒TIME YELLOW 1000
掉电保持全部设置后输入 SAVE
紧急闪烁翻转 500 mstick()紧急分支中的 now_ms / 300改为 now_ms / 500
紧急蜂鸣 2000 Hz/100 msstartBeep(1800, 120)改为 startBeep(2000, 100)

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

示例 A:普通顺序改成红 → 黄 → 绿 → 红

替换范围:tick()中找到三个 enterPhase()组成的状态转换块,完整替换为:

if (app.config().mode == 0) {
    enterPhase(app, 2, now_ms); // 红 -> 黄
} else if (app.config().mode == 2) {
    enterPhase(app, 1, now_ms); // 黄 -> 绿
} else {
    pedestrian_request = false;
    enterPhase(app, 0, now_ms); // 绿 -> 红
}

同时检查请求条件:原代码只在 mode == 1的绿灯阶段提前处理请求。如果你的新故事中请求作用于其他阶段,也要修改转换块上方的条件。

示例 B:倒计时最后 3 秒,每秒鸣叫一次

替换范围:tick()末尾找到 if (seconds != last_countdown_second),完整替换为:

if (seconds != last_countdown_second) {
    last_countdown_second = seconds;
    app.emit("COUNTDOWN PHASE=%s SEC=%lu",
             phaseName(app.config().mode),
             static_cast<unsigned long>(seconds));

    if (seconds > 0 && seconds <= 3) {
        app.startBeep(2000, 100);
    }
}

利用“秒数变化”判断,蜂鸣器每秒只触发一次;不要直接把鸣叫放在 tick()外层,否则 10 ms 会调用一次。

示例 C:电位器控制绿灯时间 1~10 秒

替换范围:完整替换当前空的 adcSample()

void adcSample(contest::ContestApp &app, uint32_t millivolts) {
    const uint32_t limited = min<uint32_t>(millivolts, 3300);
    app.config().user2 = 1000 + (limited * 9000UL / 3300);
}

0 mV 对应 1000 ms,3300 mV 对应 10000 ms。若要求串口实时显示,可在函数末尾加入 app.emit(),但采样过快时日志会很多。

示例 D:行人请求只记录,等本轮绿灯自然结束后再进入黄灯

原代码会在绿灯运行至少 1 秒后提前切换。要取消提前切换,只需修改一个条件。

找到:

if ((app.config().mode == 1 && pedestrian_request && elapsed >= 1000)
    || elapsed >= duration) {

改为:

if (elapsed >= duration) {

请求标志仍会保留,并在完成一轮后清除。如果题目要求请求决定下一阶段,需要在状态转换块中读取 pedestrian_request

改完后的最短验收

  1. 先把三个时间临时设为 1000 ms 左右,加快完整循环测试。
  2. 输入 STATUS,核对相位名称、请求标志和三个时间。
  3. 分别在红、绿、黄阶段短按,确认请求只在规定阶段生效。
  4. 长按进入紧急,再次长按,确认恢复到进入前的阶段。
  5. 运行过程中连续发送串口命令,倒计时和灯光不能停顿。

不要使用长 delay()阶段等待必须通过 now_ms - phase_started_ms判断。增加阶段时还要同步名称表、时间表、输出、命令范围和 createTaskHooks()最大模式。