本章要点
UART 是全双工的:TX(发送)和 RX(接收)两根线独立,因此能同时收发。它本质上是一个移位寄存器 + 一个移位时钟的串并转换电路。
双方波特率必须一致,否则采样点错位,收到的全是乱码。ESP32 默认 Serial0 接到 USB,所以调试时用 Serial 即可。
指令解析的通用套路是:缓冲区累积字符,遇到换行符就切一条指令,trim 去空格、统一小写、再分发处理。
硬件清单与原理
| 名称 | 数量 | 作用与原理 |
|---|---|---|
| ESP32 DevKit V1 | 1 块 | 核心控制器。板载 USB 转串口芯片用于下载程序与串口通信。 |
| USB 数据线 | 1 根 | 串口信号的实际载体。必须是数据线,不能用纯充电线。 |
| LED + 220Ω 电阻 | 1 套 | 状态指示。板载 LED 已接在 GPIO2,可直接复用。 |
接线说明
USB 数据线 → ESP32 的 Micro-USB 口 → 电脑
板载 LED 已接 GPIO2,无需外接(外接见第 1 章)
① 打开 Arduino IDE 串口监视器,右下角波特率设为 115200
② 输入 help 回车,确认收到指令列表
提示:若监视器右下角没有波特率选项,请先关闭再重新打开串口监视器。
实战案例 04-1 · 串口指令控制 LED
实现与电脑串口助手的双向对话:ESP32 主动上报心跳,电脑发指令控制 LED。
在串口监视器输入 help 回车,收到指令列表;输入 on 回车,LED 亮起。
/* * ============================================================================ * 第 4 章 · 实战案例 04-1:串口指令控制 LED(双向通信) * ---------------------------------------------------------------------------- * 硬件清单: * - ESP32 DevKit V1 × 1 * - USB 数据线 × 1 * - LED × 1 + 220Ω 电阻 × 1 * * 接线方式: * GPIO2 → [220Ω] → LED 阳极,LED 阴极 → GND * (很多开发板 GPIO2 上已经焊了板载 LED,可以直接用,省去外接) * * 元器件/接口原理: * 串口(UART)本质上是「一个移位寄存器 + 一个移位时钟」的串并转换电路。 * 发送方把并行数据按位拆成串行 bit 流,接收方再按同样的时序拼回并行数据。 * 它是全双工的:TX(发送)和 RX(接收)两根线独立,因此能同时收发。 * 关键参数是「波特率」:双方必须一致,否则采样点错位,收到全是乱码。 * * 功能用途: * 实现和电脑串口助手的双向对话: * - ESP32 主动上报心跳信息 * - 电脑发送指令,控制 LED 亮灭、修改闪烁速度 * * 支持的指令(不区分大小写): * on → 点亮 LED * off → 熄灭 LED * toggle → 翻转 LED 状态 * speed 200 → 把闪烁间隔改为 200 毫秒 * status → 查询当前状态 * help → 列出所有指令 * * 预期现象: * 在串口监视器里输入 help 回车,收到指令列表;输入 on 回车,LED 亮起。 * ============================================================================ */ #include <Arduino.h> // Arduino 核心库 /* ========================================================================== * 一、引脚定义 * ======================================================================== */ #define LED_PIN 2 // LED 接 GPIO2(板载 LED 就在这个脚上) /* ========================================================================== * 二、参数 * ======================================================================== */ const class="kw">unsigned long DEFAULT_BLINK_MS = 1000; // 默认心跳上报间隔 1 秒 class="kw">unsigned long blinkInterval = DEFAULT_BLINK_MS; // 实际使用的间隔(可被指令修改) class="kw">unsigned long lastReportTime = 0; // 上次上报的时刻 bool ledState = false; // LED 当前状态 bool blinkMode = true; // 闪烁模式开关:true=自动闪烁,false=保持当前状态 class="kw">String commandBuffer; // 字符串缓冲区:暂存还没处理完的指令字符 /* ========================================================================== * 三、初始化 * ======================================================================== */ void setup() { Serial.begin(115200); // 启动串口,波特率 115200 delay(200); // 等待串口芯片就绪 pinMode(LED_PIN, OUTPUT); // LED 引脚设为输出 digitalWrite(LED_PIN, HIGH); // 初始熄灭(低电平点亮) Serial.println(); Serial.println(class="st">"=========================================="); Serial.println(class="st">" ESP32 串口指令控制台 v1.0"); Serial.println(class="st">" 输入 help 查看可用指令"); Serial.println(class="st">"=========================================="); } /* ========================================================================== * 四、自定义函数:打印指令帮助 * ======================================================================== */ void printHelp() { Serial.println(class="st">"---------------- 可用指令 ----------------"); Serial.println(class="st">"on 点亮 LED"); Serial.println(class="st">"off 熄灭 LED"); Serial.println(class="st">"toggle 翻转 LED 状态"); Serial.println(class="st">"speed <ms> 设置闪烁间隔,例:speed 200"); Serial.println(class="st">"status 查询当前状态"); Serial.println(class="st">"help 显示本帮助"); Serial.println(class="st">"-----------------------------------------"); } /* ========================================================================== * 五、自定义函数:处理一条完整的指令 * 参数:class="kw">String cmd —— 已经去掉首尾空格、并转成小写的指令 * ======================================================================== */ void handleCommand(class="kw">String cmd) { Serial.print(class="st">"[收到指令] "); Serial.println(cmd); if (cmd == class="st">"help") { printHelp(); } else if (cmd == class="st">"on") { ledState = true; // 记住状态 blinkMode = false; // 进入「常亮」模式,停止自动闪烁 digitalWrite(LED_PIN, LOW); // 低电平点亮 Serial.println(class="st">"[执行] LED 已点亮"); } else if (cmd == class="st">"off") { ledState = false; blinkMode = false; digitalWrite(LED_PIN, HIGH); // 高电平熄灭 Serial.println(class="st">"[执行] LED 已熄灭"); } else if (cmd == class="st">"toggle") { ledState = !ledState; // 取反 blinkMode = false; digitalWrite(LED_PIN, ledState ? LOW : HIGH); Serial.print(class="st">"[执行] LED 已切换为 "); Serial.println(ledState ? class="st">"ON" : class="st">"OFF"); } else if (cmd == class="st">"status") { Serial.print(class="st">"[状态] LED = "); Serial.print(ledState ? class="st">"ON" : class="st">"OFF"); Serial.print(class="st">" | 模式 = "); Serial.print(blinkMode ? class="st">"自动闪烁" : class="st">"手动常亮"); Serial.print(class="st">" | 间隔 = "); Serial.print(blinkInterval); Serial.println(class="st">" ms"); } else if (cmd.startsWith(class="st">"speed")) { // startsWith 用于判断字符串是否以某段内容开头 // substring(5) 取出从第 6 个字符(下标 5)开始的剩余部分,即空格后面的数字 class="kw">String valueStr = cmd.substring(5); valueStr.trim(); // 去掉首尾空格 // isDigit 判断字符串是否全部由数字组成(这里再配合 toInt 转换) if (valueStr.length() > 0) { long newValue = valueStr.toInt(); // 字符串转整数 if (newValue >= 50 && newValue <= 10000) { blinkInterval = newValue; // 合法范围内才接受 Serial.print(class="st">"[执行] 闪烁间隔已设为 "); Serial.print(blinkInterval); Serial.println(class="st">" ms"); } else { Serial.println(class="st">"[错误] 间隔需在 50 ~ 10000 ms 之间"); } } else { Serial.println(class="st">"[错误] 缺少参数,例:speed 200"); } } else { Serial.println(class="st">"[错误] 无法识别的指令,输入 help 查看帮助"); } } /* ========================================================================== * 六、主循环 * ======================================================================== */ void loop() { class="kw">unsigned long now = millis(); /* ---- 1. 读取串口数据 ---- */ // available() 返回「接收缓冲区里还没取走的字节数」 class="kw">while (Serial.available() > 0) { char c = (char)Serial.read(); // 每次取出一个字符 if (c == '\n' || c == '\r') { // 遇到换行符 → 说明一条指令输入完毕 commandBuffer.trim(); // 去掉首尾多余的空格和回车 if (commandBuffer.length() > 0) { commandBuffer.toLowerCase(); // 统一转小写,让 ON / On / on 都能识别 handleCommand(commandBuffer); } commandBuffer = class="st">""; // 清空缓冲区,准备接收下一条 } else { // 普通字符 → 追加到缓冲区 commandBuffer += c; } } /* ---- 2. 自动闪烁心跳 ---- */ if (blinkMode && (now - lastReportTime >= blinkInterval)) { lastReportTime = now; ledState = !ledState; // 翻转状态 digitalWrite(LED_PIN, ledState ? LOW : HIGH); Serial.print(class="st">"[心跳] t="); Serial.print(now); Serial.print(class="st">" ms | LED "); Serial.println(ledState ? class="st">"ON" : class="st">"OFF"); } }
本章小结
① UART 是全双工的:TX 和 RX 两根线独立,能同时收发。
② 双方波特率必须一致,否则采样点错位,收到全是乱码——这是新手第一大坑。
③ 串口是调试单片机最重要的窗口,多打 Serial.println 能省掉一半调试时间。
④ 指令解析通用套路:缓冲区累积 + 行结束符切分 + trim + 统一大小写 + 分发处理。
⑤ Serial.available() 返回还没取走的字节数,用 while 循环一次性读干净。
本章练习
新增一条指令 blink 500,让 LED 以 500ms 间隔自动闪烁。
把 commandBuffer 换成固定长度 char 数组,体验两种写法差异。
故意把监视器波特率改成 9600,观察乱码并解释原因。
增加一条 echo 指令,把收到的原始字符串原样返回。