UART 串口通信详解

引言

如果你做过嵌入式开发,一定对串口不陌生------它既是调试时的"救命稻草",也是与传感器、GPS模块、蓝牙模块等外设通信最常用的接口。可以说,掌握了UART,就掌握了嵌入式设备最基本的"语言"

本章将深入讲解 ESP32-S3 的 UART 控制器原理、驱动配置方法,并通过 printf 重定向和串口协议解析两个实战案例,让你彻底吃透串口通信。

适用读者: 已完成第3章学习,对 ESP-IDF 基础编程有一定了解的开发者。


一、UART 串口通信基础

1.1 什么是 UART

UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种异步串行通信协议。它只需要两条数据线就能实现全双工通信:

  • TX(发送):发送数据
  • RX(接收):接收数据

通信双方通过事先约定的波特率(Baud Rate) 来同步数据采样时机,不需要额外的时钟线。

1.2 数据帧格式

UART 传输的基本单位是一帧数据,通常包含:

复制代码
起始位(1) + 数据位(5~8) + 校验位(可选) + 停止位(1~2)

以最常用的格式 8N1 (8数据位、无校验、1停止位)为例,传输一个字节 0x55(即二进制 01010101)的波形如下:

复制代码
空闲(高) ────┐
             │  ┌─┐ ┌─┐ ┌─┐ ┌─┐     ┌─┐
             └──┘ └─┘ └─┘ └─┘ └─────┘ └──
                起始  0  1  0  1  0  1  0  1  停止

注意:UART 从低位开始 发送数据,因此 0x55(0101_0101)在线上先发最低位。

参数 常用值 说明
波特率 9600 / 115200 / 921600 每秒传输的比特数
数据位 8 大多数场景使用 8 位
校验位 None / Even / Odd 奇偶校验,用于简单检错
停止位 1 / 2 帧结束标志

1.3 电平标准

UART 通常指TTL 电平 (0V 为低电平,3.3V 或 5V 为高电平)。但电脑串口(DB9)使用的是 RS-232 电平 (±12V),两者不能直连------这就是为什么我们需要 USB 转 TTL 模块(如 CH340、CP2102)。

⚠️ 重要: ESP32-S3 的 UART 引脚输出为 3.3V TTL 电平,连接 5V 设备时需要确认电平兼容性,必要时加电平转换芯片。


二、ESP32-S3 的 UART 控制器

2.1 硬件资源

ESP32-S3 内置了 3 个独立的 UART 控制器(UART0 ~ UART2),主要特性:

特性 参数
控制器数量 3 个
最高波特率 5 Mbps
FIFO 深度 128 字节(收发各 128 字节)
硬件流控 RTS / CTS 支持
奇偶校验 支持
中断触发 可配置 FIFO 阈值、超时等

2.2 默认引脚分配

控制器 TX RX 默认用途
UART0 GPIO43 GPIO44 默认控制台(烧录/调试)
UART1 --- --- 可自由映射到任意 GPIO
UART2 --- --- 可自由映射到任意 GPIO

ESP32-S3 的 UART0 默认连接到 USB 串口,用于程序烧录和 printf 输出。UART1 和 UART2 没有默认引脚,需要用户在代码中指定。


三、UART 编程实战

3.1 基本配置流程

使用 ESP-IDF 配置 UART 的完整流程如下:

c 复制代码
#include "driver/uart.h"
#include "driver/gpio.h"

#define UART_PORT       UART_NUM_1         // 使用 UART1
#define UART_TX_GPIO    GPIO_NUM_17        // TX 引脚
#define UART_RX_GPIO    GPIO_NUM_18        // RX 引脚
#define UART_BAUD_RATE  115200             // 波特率
#define BUF_SIZE        1024               // 缓冲区大小

void uart_init(void) {
    // 1. 配置 UART 参数
    uart_config_t uart_config = {
        .baud_rate = UART_BAUD_RATE,
        .data_bits = UART_DATA_8_BITS,      // 8 位数据
        .parity = UART_PARITY_DISABLE,       // 无校验
        .stop_bits = UART_STOP_BITS_1,       // 1 位停止位
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 无硬件流控
        .source_clk = UART_SCLK_DEFAULT,     // 默认时钟源
    };
    uart_param_config(UART_PORT, &uart_config);

    // 2. 设置引脚(ESP32-S3 支持任意引脚映射)
    uart_set_pin(UART_PORT, UART_TX_GPIO, UART_RX_GPIO,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // 3. 安装驱动(分配 FIFO 缓冲区)
    uart_driver_install(UART_PORT, BUF_SIZE, BUF_SIZE, 0, NULL, 0);
}

代码解析:

  • uart_param_config:配置波特率、数据格式等协议参数
  • uart_set_pin:将 UART 控制器映射到具体的 GPIO 引脚
  • uart_driver_install:安装驱动并分配收发缓冲区

3.2 收发数据

安装驱动后,可以通过标准 API 进行读写:

c 复制代码
// 发送数据
const char *msg = "Hello ESP32-S3!\r\n";
uart_write_bytes(UART_PORT, msg, strlen(msg));

// 接收数据(阻塞式,带超时)
uint8_t rx_buf[256];
int len = uart_read_bytes(UART_PORT, rx_buf, sizeof(rx_buf),
                          pdMS_TO_TICKS(100));  // 100ms 超时
if (len > 0) {
    printf("Received %d bytes: %.*s\n", len, len, rx_buf);
}

uart_read_bytes 是阻塞调用,建议在 FreeRTOS 任务中使用,配合适当的超时时间以避免任务挂死。

3.3 串口回环实验

最简单的测试------将 TX 和 RX 短接(或用串口助手发送),验证 UART 通信:

c 复制代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"

#define ECHO_UART_PORT  UART_NUM_1
#define ECHO_TX_GPIO    GPIO_NUM_17
#define ECHO_RX_GPIO    GPIO_NUM_18
#define BUF_SIZE        1024

void echo_task(void *arg) {
    uint8_t data[BUF_SIZE];

    while (1) {
        // 等待接收数据
        int len = uart_read_bytes(ECHO_UART_PORT, data, BUF_SIZE,
                                  pdMS_TO_TICKS(100));
        if (len > 0) {
            // 原样回发
            uart_write_bytes(ECHO_UART_PORT, (char *)data, len);
        }
    }
}

void app_main(void) {
    // 配置 UART 参数
    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,
        .source_clk = UART_SCLK_DEFAULT,
    };
    uart_param_config(ECHO_UART_PORT, &uart_config);
    uart_set_pin(ECHO_UART_PORT, ECHO_TX_GPIO, ECHO_RX_GPIO,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(ECHO_UART_PORT, BUF_SIZE, BUF_SIZE, 0, NULL, 0);

    // 创建回环任务
    xTaskCreate(echo_task, "uart_echo", 4096, NULL, 10, NULL);
}

用 USB 转 TTL 模块连接 PC 和 ESP32-S3,打开串口助手发送任意字符------ESP32-S3 会原样返回,这就是串口通信最基础的验证方式。


四、printf 重定向到 UART

4.1 为什么要重定向 printf

printf 是 C 语言最常用的格式化输出函数。在 ESP-IDF 中,printf 默认输出到 UART0(通过 esp_rom_printf),但当我们使用其他 UART 接口时,需要将其重定向到指定串口。

4.2 实现方法

ESP-IDF 提供了 esp_timerconsole 组件,但最直接的方式是自定义 _write 函数(Newlib 底层接口):

c 复制代码
#include "driver/uart.h"

// 指定 printf 输出的 UART 端口
#define PRINTF_UART UART_NUM_1

// 重写 _write 函数
int _write(int file, char *data, int len) {
    uart_write_bytes(PRINTF_UART, data, len);
    return len;
}

只需添加上述函数,所有 printf 调用就会自动路由到 UART1。这对于串口屏、蓝牙模块等需要输出格式化命令的场景非常方便。

c 复制代码
// 重定向后,printf 直接输出到 UART1
void app_main(void) {
    // 先初始化 UART1
    // ...

    printf("System initialized!\n");
    printf("Free heap: %d bytes\n", esp_get_free_heap_size());
    printf("Chip model: %s\n", CONFIG_IDF_TARGET);
}

4.3 注意事项

  • 仅需重写一次_write 是 Newlib 库的弱符号,用户定义后自动覆盖默认实现
  • 线程安全uart_write_bytes 内部已加锁,可在多任务环境中安全使用
  • 缓冲区大小:UART 发送是异步的,大量数据时注意发送缓冲区是否溢出

五、实战:温湿度传感器数据采集

接下来我们做一个综合实战------通过 UART 连接一个 SHT30 温湿度传感器模块(该模块通过 UART 接口输出数据),学习如何解析串口协议。

5.1 协议分析

SHT30 的 UART 协议如下:

命令 发送数据 返回数据
读取温湿度 0x22 0x00 0xA5 0x5A + 6 字节数据 + 校验和

返回数据格式(6 字节):

复制代码
字节0: 温度高位 | 字节1: 温度低位 | 字节2: 温度校验
字节3: 湿度高位 | 字节4: 湿度低位 | 字节5: 湿度校验

5.2 代码实现

c 复制代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"

#define SENSOR_UART     UART_NUM_2
#define SENSOR_TX       GPIO_NUM_10   // 接传感器的 TX
#define SENSOR_RX       GPIO_NUM_9    // 接传感器的 RX
#define BUF_SIZE        256

// CRC 校验
static uint8_t crc8(const uint8_t *data, int len) {
    uint8_t crc = 0xFF;
    for (int i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x80)
                crc = (crc << 1) ^ 0x31;
            else
                crc <<= 1;
        }
    }
    return crc;
}

void app_main(void) {
    // 初始化 UART
    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,
        .source_clk = UART_SCLK_DEFAULT,
    };
    uart_param_config(SENSOR_UART, &uart_config);
    uart_set_pin(SENSOR_UART, SENSOR_TX, SENSOR_RX,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(SENSOR_UART, BUF_SIZE, 0, 0, NULL, 0);

    uint8_t cmd[] = {0x22, 0x00};  // 读取命令
    uint8_t rx_buf[32];

    while (1) {
        // 发送读取命令
        uart_write_bytes(SENSOR_UART, (char *)cmd, sizeof(cmd));
        vTaskDelay(pdMS_TO_TICKS(50));  // 等待传感器响应

        // 读取响应
        int len = uart_read_bytes(SENSOR_UART, rx_buf, 8,
                                   pdMS_TO_TICKS(100));
        if (len >= 6 && rx_buf[0] == 0xA5 && rx_buf[1] == 0x5A) {
            // 校验温度
            if (crc8(&rx_buf[2], 2) == rx_buf[4]) {
                uint16_t temp_raw = (rx_buf[2] << 8) | rx_buf[3];
                float temp = -45.0f + 175.0f * temp_raw / 65535.0f;
                printf("Temperature: %.2f°C\n", temp);
            }

            // 校验湿度
            if (crc8(&rx_buf[5], 2) == rx_buf[7]) {
                uint16_t hum_raw = (rx_buf[5] << 8) | rx_buf[6];
                float hum = 100.0f * hum_raw / 65535.0f;
                printf("Humidity: %.2f%%\n", hum);
            }
        } else {
            printf("Sensor read failed (len=%d)\n", len);
        }

        vTaskDelay(pdMS_TO_TICKS(2000));  // 每 2 秒读取一次
    }
}

这个案例展示了串口协议解析的完整流程:发送指令 → 读取响应 → 校验数据 → 解析值。这是与几乎所有串口外设通信的标准模板。


六、常见问题与调试技巧

6.1 乱码问题

串口收到乱码的最常见原因:

问题 解决方法
波特率不匹配 确认两端波特率完全一致(不要"大概相等")
电平不匹配 3.3V 设备接 5V 信号可能烧坏,反之可能无法识别
接地不良 TX/RX 和 GND 必须共地
TX/RX 接反 交叉连接:A的TX → B的RX,A的RX ← B的TX

6.2 调试工具推荐

  • 逻辑分析仪:最推荐的调试方式,直接看波形,波特率、格式一目了然(推荐 Saleae Logic 或国产 24MHz 8通道版)
  • 串口助手:sscom、Putty、Arduino IDE 串口监视器
  • ESP-IDF Monitoridf.py monitor 自带色彩高亮和时间戳

6.3 波特率上限

ESP32-S3 的 UART 最高支持 5 Mbps,但实际可用波特率受以下因素限制:

  • USB 转 TTL 模块:普通 CH340 最高约 2 Mbps,CP2102 约 1 Mbps,FT232 可达 3 Mbps
  • 线长和干扰:高速串口建议线长不超过 20cm,使用屏蔽线
  • 双方时钟精度:高波特率下时钟偏差容忍度更低

总结

本章我们从理论到实践,系统学习了 ESP32-S3 的 UART 串口通信:

  1. UART 协议基础:异步串行通信的原理与数据帧格式
  2. ESP32-S3 UART 控制器:3 个独立控制器、灵活的引脚映射
  3. 驱动配置与收发:标准 API 的使用方法和回环测试
  4. printf 重定向:一行代码将格式化输出路由到任意串口
  5. 实战:SHT30 传感器:完整的串口协议解析流程
  6. 调试技巧:乱码排查、工具推荐、波特率限制

串口是嵌入式开发的"万能接口",无论是调试输出、日志记录,还是与各类外设通信,它都是你最值得熟练掌握的通信方式。


下篇预告

第5章:I2C 通信与传感器驱动 ------ 相比 UART,I2C 用更少的引脚连接更多的设备。我们将学习 ESP32-S3 的 I2C 控制器,并驱动 OLED 显示屏和多种传感器。


本文基于 ESP-IDF v5.x 编写,GPIO 引脚号请根据实际硬件连接调整。SHT30 实验中请确认传感器模块的工作电压为 3.3V。