串行通信 (UART)
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种硬件接口电路,用于实现异步串行通信。常见应用包括:与传感器/模块通信,开发板与电脑之间的数据收发(如打印日志、调试信息)等。
基于 UART 的串行通信具有以下特点:
- 异步通信:发送和接收设备不需要共享时钟信号,而是通过预先约定的波特率来同步数据传输。
- 串行传输:数据按位逐个发送,而不是并行传输多个位。
- 全双工:可以同时进行发送和接收操作。
UART 是 异步 通信,意味着它没有共享的时钟线。为了实现数据的正确收发,它们约定使用相同 波特率 和 数据帧格式。
波特率 (Baud Rate) 表示每秒钟传输的数据位数(bps,bits per second)。通信双方必须使用相同的波特率才能正确传输数据。常见的波特率有 9600 和 115200。
每个 UART 数据帧 包含以下部分:
- 起始位 (Start Bit):1 位,总是 0,表示数据传输开始
- 数据位 (Data Bits):通常 5-9 位,常用 8 位,包含实际要传输的数据
- 奇偶校验位 (Parity Bit):可选,用于错误检测
- 停止位 (Stop Bits):1-2 位,总是 1,表示数据传输结束
UART 通信需要两根核心信号线:
- TX (Transmit):发送数据线
- RX (Receive):接收数据线
- GND (地): 通信双方的“共同参考点”,确保电压信号能被正确解读。
- 连接方式:两个设备之间需要交叉连接,即设备 A 的 TX 连接设备 B 的 RX,设备 A 的 RX 连接设备 B 的 TX。此外,两个设备必须共地(连接 GND),以确保信号电平有稳定的参考点。
1. ESP32 中的 UART
ESP32 芯片通常有两个或更多 UART 控制器。每个 UART 控制器可以独立配置波特率、数据位长度、位顺序、停止位位数、奇偶校验位等参数。
在 Arduino 环境中,我们可以通过 Serial
、Serial1
等对象来使用它们。
-
Serial
是默认串口,通常连接到开发板上的 USB 转串口芯片。如果没有搭载 USB 转串口芯片,它也可以通过开启 USB CDC on boot 指向原生 USB。无论是哪种情况,Serial
都可用于与电脑串口监视器通信,也是上传代码和调试常用的接口。 -
Serial1
等 其他串口是额外的硬件 UART,几乎可分配给任意空闲 GPIO 用于连接外部设备。 -
除
Serial
和Serial1
外,部分 ESP32 型号(如 ESP32-S3)还支持Serial2
等更多串口。各型号支持的 UART 数量可查阅芯片数据手册或 ESP32 系列产品介绍表。
这种设计让我们能够同时用 Serial
与电脑调试、打印日志,同时使用 Serial1
等其它 UART 与模块独立通信,互不干扰。
在 Arduino 环境中,Serial
的指向依开发板类型和配置而异,需注意:
-
带 USB 转串口芯片的开发板(如经典 ESP32):
Serial
通常对应底层 UART0,默认的 TX/RX 专用于电脑通信。 -
具有原生 USB 功能的开发板(如 ESP32-S3/S2/C3):
若搭载 USB 转串口芯片,
Serial
对应 UART0,默认的 TX/RX 专用于电脑通信启用 USB CDC 时,
Serial
将直接通过 USB 接口通信,与硬件 UART0 无关。如无 USB 转串口芯片,可通过标注的 RX/TX 使用Serial0
(UART0)。
2. 示例 1:通过串口监视器控制 LED
这个示例将演示 UART 的经典应用:通过电脑发送指令,控制 ESP32 的硬件。我们将通过 Arduino IDE 的串口监视器发送 "on" 或 "off" 字符串,来点亮或熄灭连接在 ESP32 上的 LED。
2.1 搭建电路
需要使用的器件有:
- LED * 1
- 330Ω 电阻 * 1
- 面包板 * 1
- 导线
- ESP32 开发板
按照下面接线图连接电路:
ESP32-S3-Zero 引脚图

2.2 代码
const int ledPin = 7; // 定义 LED 连接的引脚
void setup() {
pinMode(ledPin, OUTPUT); // 设置 LED 引脚为输出模式
Serial.begin(115200); // 初始化串口通信,波特率 115200
while (!Serial) {}; // 等待串口准备好
}
void loop() {
if (Serial.available() > 0) { // 如果串口收到数据
String msg = Serial.readStringUntil('\n'); // 读取一行输入(以换行结束)
msg.trim(); // 去除首尾占用空间但不可见的字符,比如空格和换行符
if (msg == "on") {
// 如果输入为"on", 点亮 LED
digitalWrite(ledPin, HIGH);
Serial.println("LED 已打开");
} else if (msg == "off") {
// 如果输入为"off", 关闭 LED
digitalWrite(ledPin, LOW);
Serial.println("LED 已关闭");
} else {
Serial.println("请输入 'on' 或 'off'"); // 提示有效输入
}
}
}
2.3 代码解析
Serial.begin(115200);
: 初始化Serial
,并将波特率设置为 115200。串口监视器的波特率也需要设置为此值。Serial.available()
: 检查串口接收缓冲区中是否有数据。如果大于 0,说明电脑发送了新消息。Serial.readStringUntil('\n')
: 读取串口缓冲区中的字符,直到遇到换行符 \n 或超时为止,并将读取到的字符组合成一个String
对象。这种方式适合接收由串口监视器发送的、以回车结尾的指令。msg.trim();
: 当我们从串口监视器发送文本并按回车时,除了文本本身,通常还会发送一个换行符(\n
)或回车符(\r
)。trim()
函数会移除字符串首尾的这些空白字符,确保msg == "on"
这样的比较能够成功。if / else if
: 根据清理过的msg
字符串内容,执行相应的digitalWrite()
操作来控制 LED,并向串口监视器打印反馈信息。
2.4 运行结果
-
打开串口监视器,将波特率设置为 115200,确保与代码中设置的波特率一致。
-
在串口监视器中,输入"on"并按回车,LED 灯会亮起;输入"off"并按回车,LED 灯会熄灭。
3. 示例 2:ESP32 之间串口通信
这个示例将展示如何使用 ESP32 的额外硬件串口(Serial1
)实现两块 ESP32 开发板之间的通信。我们将用一块开发板连接的按钮,来控制另一块开发板上连接的 LED。
3.1 搭建电路
需要使用的器件有:
- LED * 1
- 330Ω 电阻 * 1
- 面包板 * 2
- 按钮 * 1
- 导线
- ESP32 开发板 * 2
按照下面接线图连接电路:
ESP32-S3-Zero 引脚图

发送端 (开发板 A) | 接收端 (开发板 B) | 说明 |
---|---|---|
GPIO 11 (RX) | GPIO 2 (TX) | 数据从 B 发送到 A |
GPIO 12 (TX) | GPIO 1 (RX) | 数据从 A 发送到 B |
GND | GND | 必须共地,保证信号稳定 |
3.2 代码
发送端代码 (ESP32 开发板 A)
将此代码上传到连接了按钮的 ESP32 开发板。
#define UART1_RX_PIN 11 // 定义 UART1 的接收引脚(RX)
#define UART1_TX_PIN 12 // 定义 UART1 的发送引脚(TX)
const int buttonPin = 7; // 按钮连接的引脚
int lastButtonState = LOW; // 初始状态为未按下
void setup() {
// 启动默认串口,用于调试输出到电脑
Serial.begin(115200);
// while(!Serial){};
// 启动 Serial1,并指定 RX 和 TX 引脚,用于设备间通信
Serial1.begin(9600, SERIAL_8N1, UART1_RX_PIN, UART1_TX_PIN);
pinMode(buttonPin, INPUT_PULLUP); // 配置按钮引脚为上拉输入模式
Serial.println("Sender Ready. Press the button.");
}
void loop() {
int currentButtonState = digitalRead(buttonPin);
// 如果按钮状态发生变化
if (currentButtonState != lastButtonState) {
if (currentButtonState == HIGH) {
Serial1.write('0'); // 按钮松开时发送'0'
Serial.println("Sent: 0 (Button Released)");
} else {
Serial1.write('1'); // 按钮按下时发送'1'
Serial.println("Sent: 1 (Button Pressed)");
}
lastButtonState = currentButtonState; // 更新按钮状态
delay(50); // 简单的防抖动
}
}
接收端代码 (ESP32 开发板 B)
将此代码上传到连接了 LED 的 ESP32 开发板。
#define UART1_RX_PIN 1 // 定义 UART1 的接收引脚(RX)
#define UART1_TX_PIN 2 // 定义 UART1 的发送引脚(TX)
const int ledPin = 7;
void setup() {
// 启动默认串口,用于调试输出到电脑
Serial.begin(115200);
// while(!Serial){};
// 启动 Serial1,并指定 RX 和 TX 引脚,用于设备间通信
Serial1.begin(9600, SERIAL_8N1, UART1_RX_PIN, UART1_TX_PIN);
pinMode(ledPin, OUTPUT); // 配置 LED 引脚为输出模式
Serial.println("Receiver Ready. Waiting for commands...");
}
void loop() {
// 检查是否从 UART1 串口接收到数据
if (Serial1.available()) {
char command = Serial1.read(); // 读取一个字节(字符)
// 根据接收到的命令控制 LED
if (command == '1') {
// 接收到'1'时点亮 LED
digitalWrite(ledPin, HIGH);
Serial.println("Received: 1 -> LED ON");
} else if (command == '0') {
// 接收到'0'时熄灭 LED
digitalWrite(ledPin, LOW);
Serial.println("Received: 0 -> LED OFF");
}
}
}
3.3 代码解析
两份代码的共同点
#define UART1_RX_PIN ...
/#define UART1_TX_PIN ...
: 使用宏定义来指定Serial1
的 RX 和 TX 引脚。这让代码更易读,方便修改。Serial.begin(115200);
: 两块板子都启动了默认的Serial
口,这样它们可以分别连接到两台电脑(或同一个电脑的两个串口工具)上,打印调试信息,方便我们观察通信过程。Serial1.begin(9600, SERIAL_8N1, UART1_RX_PIN, UART1_TX_PIN);
:- 这是本示例的核心。它初始化了
Serial1
通道。两个开发板通过各自的Serial1
通道通信。 9600
: 这是两块 ESP32 之间通信的波特率,必须保持一致。SERIAL_8N1
: 这是标准的串口配置(8 数据位,无校验,1 停止位)。8
: 8 位数据长度(可选 5、6、7 位)N
: 无校验(可选偶校验 E、奇校验 O)1
: 1 位停止位(可选 2 位)
UART1_RX_PIN, UART1_TX_PIN
: 将Serial1
绑定到定义的 GPIO 引脚上。
- 这是本示例的核心。它初始化了
发送端 (ESP32 开发板 A)
if (currentButtonState != lastButtonState)
: 这个判断用于检测按钮状态的变化(从按下到松开,或从松开到按下),确保只在状态改变时发送一次数据,而不是持续发送。Serial1.write('1');
: 当按钮被按下时(状态变为LOW
),通过Serial1
发送单个字符'1'
给接收端。Serial1.write('0');
: 当按钮松开时(状态变为HIGH
),发送'0'
。
接收端 (ESP32 开发板 B)
if (Serial1.available())
: 在主循环中,不断检查Serial1
的接收缓冲区是否有数据。char command = Serial1.read();
: 如果有数据,读取一个字节(字符)并存入command
变量。if (command == '1')
: 判断接收到的字符。如果是'1'
,就点亮 LED;如果是'0'
,就熄灭 LED。同时,通过自己的Serial
口打印收到的信息和执行的动作,方便调试。
3.4 运行结果
- 将两份代码分别上传到两块 ESP32 开发板。
- 你可以用两根 USB 线将两块板子都连接到电脑上,并打开两个串口监视器窗口,分别对应两块板子的 COM 端口。
- 按下 发送端 (ESP32 开发板 A) 的按钮,你会观察到:
- ESP32 A 的串口监视器打印出
"Sent: 1 (Button Pressed)"
。 - 接收端 (ESP32 开发板 B) 上的 LED 亮起。
- ESP32 B 的串口监视器打印出
"Received: 1 -> LED ON"
。
- ESP32 A 的串口监视器打印出
- 松开按钮,你会观察到:
- ESP32 A 的串口监视器打印出
"Sent: 0 (Button Released)"
。 - 接收端 (ESP32 开发板 B) 上的 LED 熄灭。
- ESP32 B 的串口监视器打印出
"Received: 0 -> LED OFF"
。
- ESP32 A 的串口监视器打印出
4. 拓展阅读
为什么启用 USB CDC 后,即使代码与串口监视器的波特率设置不匹配,通信依然正常?
核心原因是,此时开发板与电脑之间使用的是 原生 USB 通信,它模拟成了一个 虚拟串口,而不再是传统的 UART 串口通信。在 USB 通信协议中,“波特率”这个概念是无效的。
简而言之:
- 通信方式变了:你用的不是依赖固定速率的 UART 信号,它传输的是数据包,其速度由 USB 协议本身协商决定。数据传输速度极快,远超常规波特率。
- 波特率设置被忽略:代码中的
Serial.begin(波特率)
和监视器的波特率设置都被忽略了。保留这个函数主要是为了与旧代码兼容。 - 适用对象:这个现象只出现在支持原生 USB 的开发板上(如 ESP32-S2/S3, Leonardo, Zero 等)。对于 Arduino Uno/Nano 这类使用独立 USB 转串口芯片的板子,波特率必须严格匹配。
5. 相关链接
- Universal Asynchronous Receiver-Transmitter (UART) | Arduino Documentation
- Serial | Arduino Documentation
- String() | Arduino Documentation
- String length() and trim() Commands | Arduino Documentation
- trim() | Arduino Documentation
- Serial.write() | Arduino Documentation
- USB CDC | Arduino-ESP32 documentation
- USB CDC On Boot | Arduino-ESP32 documentation