3、UART串口通信:UART协议原理,ESP32与PC及模块间的数据收发
串口通信,在嵌入式开发里就像呼吸一样自然。你几乎找不到哪块开发板没有UART接口。说白了,它就是设备之间最基础、最直接的对话方式。ESP32当然也不例外,而且它一口气集成了3个UART控制器,用起来相当灵活。
我个人习惯把UART比作两个人打电话——一根线说,一根线听,再加一根地线做参考。就这么简单。但越是简单的东西,坑往往也越隐蔽。今天我们就把它彻底聊透。
3.1 UART协议原理:从物理层到数据帧
UART的全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意“异步”这两个字——收发双方没有共享时钟,全靠约定的波特率来对齐节奏。
核心要点:UART通信只需要3根线——TX(发送)、RX(接收)、GND(地)。全双工,可以同时收发。
3.1.1 数据帧结构
一个完整的UART数据帧长这样:
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 逻辑0,标志传输开始 |
| 数据位 | 5~9 bit | 常用8 bit,即一个字节 |
| 校验位 | 0/1 bit | 奇校验/偶校验/无校验 |
| 停止位 | 1/1.5/2 bit | 逻辑1,标志传输结束 |
举个例子:波特率115200、8数据位、无校验、1停止位。我们通常简写为115200-8-N-1。这个配置在ESP32开发里最常见。
3.1.2 波特率与时序
波特率决定了每秒传输多少bit。115200意味着每bit约8.68微秒。收发双方必须用同一个波特率,否则数据全是乱码。
我的经验:调试时如果收到一堆乱码,90%是波特率没对上。剩下10%是TX和RX接反了。先查这两样,别急着怀疑代码。
3.2 ESP32的UART硬件资源
ESP32有3个UART控制器:UART0、UART1、UART2。它们的默认引脚分配如下:
| UART | TX默认引脚 | RX默认引脚 | 备注 |
|---|---|---|---|
| UART0 | GPIO1 | GPIO3 | 通常连接USB转串口芯片 |
| UART1 | GPIO10 | GPIO9 | 可自由映射 |
| UART2 | GPIO17 | GPIO16 | 可自由映射 |
注意,UART0默认被板载的USB转串口占用。你通过USB线连接电脑时,用的就是它。UART1和UART2的引脚可以任意映射到其他GPIO,这是ESP32的一大优势。
避坑提醒:UART1的默认引脚GPIO9和GPIO10,在某些模组上被用于Flash通信。如果你发现程序下载后无法启动,先检查是不是占用了这两个引脚。我建议UART1尽量映射到其他空闲引脚。
3.3 ESP32与PC通信:打印调试信息
这是每个嵌入式工程师每天都要做的事。把调试信息通过串口打印到电脑终端上。
3.3.1 硬件连接
用一根USB线连接ESP32开发板和电脑。板载的USB转串口芯片(CP2102或CH340)会自动完成电平转换。你不需要额外接任何线。
3.3.2 代码实现
ESP-IDF里,串口打印用printf就行。底层已经帮你封装好了。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "UART_DEMO";
void app_main(void)
{
// 配置UART0参数
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM_0, &uart_config);
uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL, 0);
while (1) {
ESP_LOGI(TAG, "ESP32 is running, free heap: %d bytes",
esp_get_free_heap_size());
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
这段代码每秒打印一次剩余堆内存。你用任何串口工具(比如PuTTY、screen、Arduino IDE的串口监视器)都能看到输出。
小技巧:ESP-IDF的ESP_LOGI会自动添加时间戳和标签,比裸printf好用得多。我所有项目都用它,调试效率高不少。
3.4 ESP32与模块通信:以GPS模块为例
PC通信只是热身。真正体现UART价值的是与外部模块交互。我们拿一个常见的GPS模块(比如NEO-6M)来演示。
3.4.1 硬件连接
GPS模块通常输出NMEA 0183协议数据,波特率9600。接线如下:
| ESP32 | GPS模块 |
|---|---|
| GPIO16 (UART2 TX) | RX |
| GPIO17 (UART2 RX) | TX |
| GND | GND |
| 3.3V | VCC |
注意交叉连接——ESP32的TX接模块的RX,ESP32的RX接模块的TX。这个我刚开始也搞反过,后来养成了习惯:先画连接图再动手。
3.4.2 代码实现:接收NMEA数据
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_log.h"
#define GPS_UART UART_NUM_2
#define GPS_TX_PIN 16
#define GPS_RX_PIN 17
#define BUF_SIZE 256
static const char *TAG = "GPS";
void gps_uart_init(void)
{
uart_config_t uart_config = {
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(GPS_UART, &uart_config);
uart_set_pin(GPS_UART, GPS_TX_PIN, GPS_RX_PIN,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(GPS_UART, BUF_SIZE * 2, 0, 0, NULL, 0);
}
void app_main(void)
{
gps_uart_init();
uint8_t data[BUF_SIZE];
while (1) {
int len = uart_read_bytes(GPS_UART, data, BUF_SIZE - 1,
pdMS_TO_TICKS(100));
if (len > 0) {
data[len] = '\0';
ESP_LOGI(TAG, "Received: %s", (char *)data);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
这段代码从GPS模块读取原始NMEA语句并打印。你会看到类似$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47这样的数据。
关键点:uart_read_bytes是非阻塞的,最后一个参数是超时时间。如果100ms内没收到数据,函数返回0。这样不会卡死主循环。
3.5 UART通信的常见问题与调试技巧
做UART通信这么多年,我踩过的坑可以写一本小册子。挑几个最常见的说说。
3.5.1 数据丢失或乱码
- 波特率不匹配:用示波器或逻辑分析仪实测一下,别完全相信手册。
- 电平不匹配:3.3V设备接5V模块,需要电平转换。我曾经直接连,烧了一个GPS模块的串口。
- 缓冲区太小:如果数据量大,默认的缓冲区可能溢出。用
uart_driver_install时把缓冲区设大一些。
3.5.2 中断与DMA的选择
ESP32的UART支持三种模式:轮询、中断、DMA。
| 模式 | 适用场景 | CPU占用 |
|---|---|---|
| 轮询 | 低速、简单调试 | 高 |
| 中断 | 常规数据收发 | 低 |
| DMA | 高速、大数据量 | 极低 |
我个人习惯:调试阶段用轮询,稳定后切中断。DMA一般留给需要持续高速收发的场景,比如和4G模块通信。
曾经踩过的坑:有一次用DMA接收GPS数据,发现偶尔丢包。查了两天才找到原因——DMA传输完成中断里做了太多处理,导致下一个DMA请求被延迟。后来把数据处理放到任务里,中断里只做标志位设置,问题解决。
3.6 UART通信知识体系
下面这张图把UART通信的核心脉络梳理了一遍。从协议原理到硬件连接,再到ESP32的驱动实现,最后落到实际调试。你照着这个路径走,基本不会迷路。
3.7 小结
UART串口通信,说难不难,说简单也不简单。协议层面就那几根线和几个参数,但真正用好它,需要你对硬件、驱动、调试都有足够的理解。
我个人觉得,掌握UART的关键在于三点:一是把协议帧结构刻在脑子里,二是熟悉ESP32的驱动API,三是学会用工具(逻辑分析仪、串口助手)快速定位问题。做到这三点,大部分串口通信场景你都能轻松应对。
嗯,这一章的内容就到这里。代码示例可以直接拿去用,但建议你动手改改参数,看看不同配置下会有什么效果。实践出真知嘛。