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的驱动实现,最后落到实际调试。你照着这个路径走,基本不会迷路。

UART串口通信知识体系 UART协议原理 异步、全双工、帧结构 ESP32硬件资源 3个UART、引脚映射 驱动配置 波特率、数据位、校验位 通信模式 轮询 / 中断 / DMA PC通信 USB转串口、printf调试 模块通信 GPS、4G、传感器等 调试与避坑指南 乱码排查、电平匹配、缓冲区优化 核心原则 先确认硬件连接 → 再检查波特率 → 最后看代码逻辑 用逻辑分析仪抓波形,比猜问题快10倍

3.7 小结

UART串口通信,说难不难,说简单也不简单。协议层面就那几根线和几个参数,但真正用好它,需要你对硬件、驱动、调试都有足够的理解。

我个人觉得,掌握UART的关键在于三点:一是把协议帧结构刻在脑子里,二是熟悉ESP32的驱动API,三是学会用工具(逻辑分析仪、串口助手)快速定位问题。做到这三点,大部分串口通信场景你都能轻松应对。

嗯,这一章的内容就到这里。代码示例可以直接拿去用,但建议你动手改改参数,看看不同配置下会有什么效果。实践出真知嘛。