首页 / 学习路径 / 第 04 章 · 串口通信 UART
04
核心 · 通信

串口通信 UART

波特率、全双工、字符串缓冲、指令解析。串口是调试单片机最重要的窗口——学会它,你就学会了和芯片对话。

阅读时长 约 45 分钟硬件 2 件实战案例 1 个前置要求 第 3 章
1

本章要点

UART 是全双工的:TX(发送)和 RX(接收)两根线独立,因此能同时收发。它本质上是一个移位寄存器 + 一个移位时钟的串并转换电路。

双方波特率必须一致,否则采样点错位,收到的全是乱码。ESP32 默认 Serial0 接到 USB,所以调试时用 Serial 即可。

指令解析的通用套路是:缓冲区累积字符,遇到换行符就切一条指令,trim 去空格、统一小写、再分发处理。

2

硬件清单与原理

名称数量作用与原理
ESP32 DevKit V11 块核心控制器。板载 USB 转串口芯片用于下载程序与串口通信。
USB 数据线1 根串口信号的实际载体。必须是数据线,不能用纯充电线。
LED + 220Ω 电阻1 套状态指示。板载 LED 已接在 GPIO2,可直接复用。
3

接线说明

接线示意 · 串口无需外接,USB 即为信号载体 ESP32 DevKit V1 板载 USB 转串口 TXD0 / RXD0 USB 串口监视器 115200 baud 电脑 / Arduino IDE TXD0 RXD0 TX 发送 / RX 接收 独立两根线,可同时收发(全双工)
串口无需外接接线,USB 线即为信号载体。LED 可用板载。

USB 数据线 → ESP32 的 Micro-USB 口 → 电脑

板载 LED 已接 GPIO2,无需外接(外接见第 1 章)

① 打开 Arduino IDE 串口监视器,右下角波特率设为 115200

② 输入 help 回车,确认收到指令列表

提示:若监视器右下角没有波特率选项,请先关闭再重新打开串口监视器。

4

实战案例 04-1 · 串口指令控制 LED

功能用途

实现与电脑串口助手的双向对话:ESP32 主动上报心跳,电脑发指令控制 LED。

预期现象

在串口监视器输入 help 回车,收到指令列表;输入 on 回车,LED 亮起。

ch04_uart.ino编译通过
/*
 * ============================================================================
 * 第 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");
  }
}
完整源码 187 行 · ch04_uart/ch04_uart.inoarduino-cli 编译通过
5

本章小结

① UART 是全双工的:TX 和 RX 两根线独立,能同时收发。

② 双方波特率必须一致,否则采样点错位,收到全是乱码——这是新手第一大坑。

③ 串口是调试单片机最重要的窗口,多打 Serial.println 能省掉一半调试时间。

④ 指令解析通用套路:缓冲区累积 + 行结束符切分 + trim + 统一大小写 + 分发处理。

⑤ Serial.available() 返回还没取走的字节数,用 while 循环一次性读干净。

6

本章练习

练习 1

新增一条指令 blink 500,让 LED 以 500ms 间隔自动闪烁。

练习 2

把 commandBuffer 换成固定长度 char 数组,体验两种写法差异。

练习 3

故意把监视器波特率改成 9600,观察乱码并解释原因。

练习 4

增加一条 echo 指令,把收到的原始字符串原样返回。

7

下一章

05
NEXT · 第 05 章

WiFi 联网与 Web 服务器

STA 与 AP 双模式、连接超时处理、HTTP 请求响应。让设备连上互联网,在手机浏览器里就能控制它。

开始学习