押题三:串口综合控制终端

命中方向:UART 上位机通信、命令解析、全部外设控制、错误处理、自动上报、NVS。

模拟任务书

  1. 使用板载 USB-UART,通信参数 115200、8N1,文本命令以 CR 或 LF 结束。
  2. 命令不区分大小写,单行最长 159 字符;超长、未知命令和越界参数必须回复 ERR。
  3. 支持控制 RGB、蜂鸣器、普通 GPIO,读取 ADC、Touch、按键与运行模式。
  4. 支持设置 ADC 阈值、采样周期、闪烁周期、自动上报周期和 Touch 灵敏度。
  5. 按设置周期自动输出一行状态,不得阻塞接收命令。
  6. 支持 SAVE、LOAD、FACTORY;NVS 数据必须有魔数、版本、长度和校验。
  7. 输入异常不得导致复位;中断内禁止打印和写 NVS。

建议评分点

项目分值验收
UART 初始化与行接收15CR、LF、CRLF 均能正确结束
命令表和大小写15HELP 显示完整;小写命令可用
外设控制和状态读取25RGB、蜂鸣器、GPIO、ADC、Touch、按键均可验收
参数范围与错误回复15无参数、非数字、越界、超长均不崩溃
自动上报与非阻塞15自动上报期间仍能立即执行 LED 命令
NVS 数据完整性10版本或校验错误时使用默认值
连续运行稳定性5连续发送 100 条错误命令不复位

直接使用

Arduino 工程:arduino_pio/mock_03_uart_controller。主要修改 include/contest_user_config.hsrc/task_logic.cpp

help
LED 20 0 80
BEEP 1500 200
GPIO WRITE 21 1
ADC READ
TOUCH READ
SET REPORT 500
SAVE
STATUS
LED 999 0 0
THIS_IS_WRONG

最可能的变题

  • 文本命令改为固定帧头、长度、命令字和校验和。
  • 上位机要求 CSV 或 JSON 样式输出。
  • UART0 改 UART1,需要外接 USB-TTL。
  • 自动上报改成仅状态变化时上报。

现场修改指南:先判断要不要改

HELP、LED、BEEP、GPIO、ADC、TOUCH、MODE、SET、SAVE、LOAD、FACTORY、STATUS 都已由公共库实现。现场只是改这些命令的参数时,不要重写 command();只有出现新的命令名字或新的回复格式才改本题代码。

题目一句话对应哪里

题目关键词函数作用
上电默认模式beginTask()检查 mode,打开自动上报并记录起点
上报内容emitReport()统一生成一行 ADC、Touch、按键和模式
上报周期/灯光tick()每 10 ms 判断是否到达 report_ms
新增命令command()识别命令、检查参数、回复 OK/ERR
HELP 新增一行printHelp()列出题型专属命令
STATUS 附加字段status()打印自动上报开关

变量翻译

代码含义
command已经转成大写的命令名,例如 AUTO
arguments命令名之后的参数正文;没有参数时可能是 nullptr
auto_report是否周期自动发送报告
last_report_ms上一次报告时刻,用于非阻塞计时
app.config().report_ms自动上报间隔,允许 100..60000 ms
return true表示命令已经处理;false会让公共库回复未知命令

先复用现成命令

要求直接使用
设置 RGBLED 20 0 80
蜂鸣器发声BEEP 1500 200
设置模式MODE 2
设置上报周期SET REPORT 500
读 ADC/TouchADC READTOUCH READ
保存配置SAVE

不要重复实现:公共命令在 ContestToolkit.cpp里。题目仍使用同名命令时,直接验收即可。

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

示例 A:新增无参数命令 PING,回复 OK PONG

插入位置:command()函数开头、现有 REPORT判断之前加入:

if (strcmp(command, "PING") == 0) {
    if (arguments != nullptr && *arguments != '\0') {
        app.emit("ERR USAGE PING");
    } else {
        app.emit("OK PONG");
    }
    return true;
}

再把 printHelp()中的提示改成包含 PING,否则功能存在但 HELP 看不到。

示例 B:新增带数字参数 RATE 100..60000

1. 在文件顶部 #include <cstring>下一行加入:

#include <cstdlib>

2. 在 command()返回 false之前加入:

if (strcmp(command, "RATE") == 0) {
    char *end = nullptr;
    const long value = arguments == nullptr
        ? -1 : strtol(arguments, &end, 10);
    while (end != nullptr && (*end == ' ' || *end == '\t')) ++end;
    const bool valid_number = arguments != nullptr &&
        end != arguments && end != nullptr && *end == '\0';

    if (!valid_number || value < 100 || value > 60000) {
        app.emit("ERR USAGE RATE <100..60000>");
    } else {
        app.config().report_ms = static_cast<uint32_t>(value);
        app.emit("OK RATE_MS=%ld", value);
    }
    return true;
}

需要掉电保存时,设置后再发送 SAVE。不要在每次命令中自动写 NVS,频繁写入会降低 Flash 寿命。

示例 C:自动上报改为 CSV 格式

假设上位机要求固定字段:REPORT,毫伏,触摸,按键,模式

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

void emitReport(contest::ContestApp &app) {
    app.emit("REPORT,%lu,%lu,%u,%u",
             static_cast<unsigned long>(app.adcMillivolts()),
             static_cast<unsigned long>(app.touchRaw()),
             app.buttonPressed() ? 1U : 0U,
             app.config().mode);
}

字段顺序必须按任务书:上位机按逗号拆分时,多一个空格、少一个字段或顺序不同都可能扣分。

示例 D:不再周期上报,只在模式变化时上报

1. 在全局变量区加入:

uint8_t last_reported_mode = 255;

2. 在 beginTask()末尾加入:

last_reported_mode = 255;

3. 把 tick()末尾原来的周期判断替换为:

if (app.config().mode != last_reported_mode) {
    last_reported_mode = app.config().mode;
    emitReport(app);
}

这种写法不会在 ADC 值变化时上报;如果题目要求“任何字段变化都上报”,还需要分别保存并比较上一次 ADC、Touch 和按键值。

改完后的最短验收

  1. 先发送小写命令,确认公共层仍会自动转大写。
  2. 对新命令分别测试:正确参数、无参数、非数字、越界、多余参数和未知命令。
  3. 开启自动上报时连续发送 20 条命令,确认接收不会被报告阻塞。
  4. 输入 HELP确认新命令出现;输入 STATUS确认配置与实际一致。
  5. 发送一条 160 字符以上的错误行,程序应回复错误而不是复位。

不要改:公共串口收行、大小写转换和超长保护。除非现场改成二进制协议,否则不要动 ContestToolkit.cpp;二进制协议直接选第 6 套。