Skip to content

第四章:串口通信 UART

4.1 UART 基础

通信参数

UART 通信需要双方约定相同参数:
  波特率(Baud Rate):每秒传输的符号数,常用 115200
  数据位:通常 8 位
  停止位:通常 1 位
  校验位:通常 None(无校验)
  → 简写:115200 8N1

数据帧格式:
  起始位(0) | D0 D1 D2 D3 D4 D5 D6 D7 | 停止位(1)

ESP32-S3 UART 资源

UART0:默认调试串口(GPIO43=TX, GPIO44=RX),printf 输出到这里
UART1:可分配到任意 GPIO
UART2:可分配到任意 GPIO

注意:ESP32-S3 的 UART 引脚可以通过 GPIO Matrix 映射到任意 IO

4.2 UART 初始化与收发

基础配置

c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define UART_NUM        UART_NUM_1
#define UART_TX_PIN     GPIO_NUM_17
#define UART_RX_PIN     GPIO_NUM_18
#define UART_BAUD       115200
#define BUF_SIZE        1024

static const char *TAG = "UART";

void uart_init(void)
{
    // 1. 配置 UART 参数
    uart_config_t uart_cfg = {
        .baud_rate  = UART_BAUD,
        .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,
    };
    ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_cfg));

    // 2. 设置引脚
    ESP_ERROR_CHECK(uart_set_pin(
        UART_NUM,
        UART_TX_PIN,    // TX
        UART_RX_PIN,    // RX
        UART_PIN_NO_CHANGE,  // RTS(不用)
        UART_PIN_NO_CHANGE   // CTS(不用)
    ));

    // 3. 安装驱动(分配缓冲区)
    ESP_ERROR_CHECK(uart_driver_install(
        UART_NUM,
        BUF_SIZE * 2,   // RX 缓冲区大小
        BUF_SIZE * 2,   // TX 缓冲区大小(0=同步发送)
        0,              // 事件队列大小(0=不用)
        NULL,           // 事件队列句柄
        0               // 中断标志
    ));

    ESP_LOGI(TAG, "UART%d 初始化完成,TX=%d RX=%d",
             UART_NUM, UART_TX_PIN, UART_RX_PIN);
}

发送数据

c
// 发送字符串
void uart_send_string(const char *str)
{
    uart_write_bytes(UART_NUM, str, strlen(str));
}

// 发送带换行
void uart_println(const char *str)
{
    uart_write_bytes(UART_NUM, str, strlen(str));
    uart_write_bytes(UART_NUM, "\r\n", 2);
}

// 发送格式化字符串(类似 printf)
void uart_printf(const char *fmt, ...)
{
    char buf[256];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    uart_write_bytes(UART_NUM, buf, strlen(buf));
}

// 发送原始字节
void uart_send_bytes(const uint8_t *data, size_t len)
{
    uart_write_bytes(UART_NUM, (const char *)data, len);
}

接收数据

c
// 方式1:非阻塞读取
void uart_receive_nonblock(void)
{
    uint8_t buf[128];
    int len = uart_read_bytes(UART_NUM, buf, sizeof(buf) - 1,
                              pdMS_TO_TICKS(0));  // 超时 0 = 非阻塞
    if (len > 0) {
        buf[len] = '\0';
        ESP_LOGI(TAG, "收到 %d 字节: %s", len, buf);
    }
}

// 方式2:阻塞读取(等待数据)
void uart_receive_block(void)
{
    uint8_t buf[128];
    int len = uart_read_bytes(UART_NUM, buf, sizeof(buf) - 1,
                              pdMS_TO_TICKS(1000));  // 等待最多 1 秒
    if (len > 0) {
        buf[len] = '\0';
        ESP_LOGI(TAG, "收到: %s", buf);
    } else {
        ESP_LOGW(TAG, "接收超时");
    }
}

4.3 UART 事件驱动(推荐方式)

c
#include "driver/uart.h"
#include "freertos/queue.h"

#define UART_NUM        UART_NUM_1
#define BUF_SIZE        1024
#define UART_TX_PIN     GPIO_NUM_17
#define UART_RX_PIN     GPIO_NUM_18

static QueueHandle_t uart_queue;
static const char *TAG = "UART_EVT";

// UART 事件处理任务
static void uart_event_task(void *arg)
{
    uart_event_t event;
    uint8_t *buf = malloc(BUF_SIZE);

    while (1) {
        // 等待 UART 事件
        if (xQueueReceive(uart_queue, &event, portMAX_DELAY)) {
            switch (event.type) {
                case UART_DATA:
                    // 有数据可读
                    int len = uart_read_bytes(UART_NUM, buf,
                                             event.size, pdMS_TO_TICKS(100));
                    buf[len] = '\0';
                    ESP_LOGI(TAG, "收到 %d 字节: %s", len, (char*)buf);

                    // 回显(Echo)
                    uart_write_bytes(UART_NUM, (char*)buf, len);
                    break;

                case UART_FIFO_OVF:
                    ESP_LOGW(TAG, "FIFO 溢出");
                    uart_flush_input(UART_NUM);
                    xQueueReset(uart_queue);
                    break;

                case UART_BUFFER_FULL:
                    ESP_LOGW(TAG, "缓冲区满");
                    uart_flush_input(UART_NUM);
                    xQueueReset(uart_queue);
                    break;

                case UART_BREAK:
                    ESP_LOGW(TAG, "检测到 Break 信号");
                    break;

                case UART_PARITY_ERR:
                    ESP_LOGE(TAG, "奇偶校验错误");
                    break;

                case UART_FRAME_ERR:
                    ESP_LOGE(TAG, "帧错误");
                    break;

                default:
                    ESP_LOGD(TAG, "其他事件: %d", event.type);
                    break;
            }
        }
    }
    free(buf);
    vTaskDelete(NULL);
}

void app_main(void)
{
    uart_config_t cfg = {
        .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(UART_NUM, &cfg);
    uart_set_pin(UART_NUM, UART_TX_PIN, UART_RX_PIN,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // 安装驱动,启用事件队列
    uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2,
                        20, &uart_queue, 0);

    xTaskCreate(uart_event_task, "uart_task", 4096, NULL, 12, NULL);

    ESP_LOGI(TAG, "UART 事件驱动模式启动");
}

4.4 实用功能:命令行解析

c
// 简单命令解析器
#define CMD_BUF_SIZE    128

typedef struct {
    const char *name;
    void (*handler)(const char *args);
    const char *help;
} cmd_entry_t;

// 命令处理函数
static void cmd_led(const char *args)
{
    if (strcmp(args, "on") == 0) {
        gpio_set_level(LED_PIN, 1);
        uart_println("LED 已打开");
    } else if (strcmp(args, "off") == 0) {
        gpio_set_level(LED_PIN, 0);
        uart_println("LED 已关闭");
    } else {
        uart_println("用法: led on|off");
    }
}

static void cmd_info(const char *args)
{
    uart_printf("芯片: %s\r\n", CONFIG_IDF_TARGET);
    uart_printf("运行时间: %lld ms\r\n", esp_timer_get_time() / 1000);
    uart_printf("空闲堆: %lu bytes\r\n", esp_get_free_heap_size());
}

static void cmd_help(const char *args);

// 命令表
static const cmd_entry_t cmd_table[] = {
    {"led",  cmd_led,  "led on|off - 控制 LED"},
    {"info", cmd_info, "info - 显示系统信息"},
    {"help", cmd_help, "help - 显示帮助"},
};
#define CMD_COUNT (sizeof(cmd_table) / sizeof(cmd_table[0]))

static void cmd_help(const char *args)
{
    uart_println("可用命令:");
    for (int i = 0; i < CMD_COUNT; i++) {
        uart_printf("  %s\r\n", cmd_table[i].help);
    }
}

// 解析并执行命令
void process_command(char *line)
{
    // 去除末尾换行
    int len = strlen(line);
    while (len > 0 && (line[len-1] == '\r' || line[len-1] == '\n')) {
        line[--len] = '\0';
    }
    if (len == 0) return;

    // 分割命令名和参数
    char *space = strchr(line, ' ');
    const char *args = "";
    if (space) {
        *space = '\0';
        args = space + 1;
    }

    // 查找并执行命令
    for (int i = 0; i < CMD_COUNT; i++) {
        if (strcmp(line, cmd_table[i].name) == 0) {
            cmd_table[i].handler(args);
            return;
        }
    }
    uart_printf("未知命令: %s(输入 help 查看帮助)\r\n", line);
}

4.5 UART0 与 printf

c
// UART0 是默认调试串口,printf 和 ESP_LOGI 都输出到这里
// 波特率默认 115200

// 重定向 printf 到 UART1(不常用,了解即可)
// 通常直接用 ESP_LOGI 系列函数,不需要重定向

// 读取 UART0 输入(用于交互式调试)
int ch = fgetc(stdin);  // 读取一个字符

📝 第四章练习题

练习 4-1:UART 回显(基础)

目标:掌握 UART 收发

任务

  • 初始化 UART1(TX=GPIO17, RX=GPIO18,115200 8N1)
  • 接收到数据后,原样回显并加上前缀
  • 格式:[ECHO] 你发送的内容
  • 用串口助手测试

练习 4-2:AT 命令解析(基础)

目标:字符串处理 + 命令解析

任务:实现简单 AT 命令集:

AT          → 回复 OK
AT+LED=ON   → 打开 LED,回复 OK
AT+LED=OFF  → 关闭 LED,回复 OK
AT+LED?     → 查询 LED 状态,回复 +LED:ON 或 +LED:OFF
AT+INFO     → 回复芯片信息
其他        → 回复 ERROR

要求

  • 命令以 \r\n 结尾
  • 不区分大小写(at+led=on 也有效)
  • 使用函数表(不要用大量 if-else)

练习 4-3:数据帧协议(进阶)

目标:二进制协议解析

任务:实现自定义二进制帧协议:

帧格式:
  [0xAA] [0x55] [LEN] [CMD] [DATA...] [CHECKSUM]
  帧头2字节  长度   命令  数据      校验和

CMD 定义:
  0x01 = LED 控制,DATA[0]: 0=关,1=开
  0x02 = 查询状态,无 DATA
  0x03 = 设置亮度,DATA[0]: 0~100

CHECKSUM = LEN ^ CMD ^ DATA[0] ^ DATA[1] ^ ...(异或校验)

要求

  • 实现帧解析状态机(逐字节解析)
  • 校验失败时回复错误帧
  • 成功时执行命令并回复 ACK 帧

练习 4-4:UART 透传(进阶)

目标:多 UART 协作

任务

  • UART0(调试口):接收用户命令
  • UART1:连接另一个设备(或自环测试 TX→RX)
  • 实现透传:UART0 收到的数据转发到 UART1,UART1 收到的数据转发到 UART0
  • 统计并打印双向的字节数

练习 4-5:Modbus RTU 帧解析(挑战)

目标:工业协议入门

背景:Modbus RTU 是工业中最常用的串口协议

帧格式:[地址1B][功能码1B][数据NB][CRC2B]
功能码 0x03:读保持寄存器
  请求:[地址][0x03][起始地址H][起始地址L][数量H][数量L][CRC]
  响应:[地址][0x03][字节数][数据...][CRC]

CRC16 计算(Modbus 标准)

任务

  • 实现 Modbus RTU 帧接收(3.5 字符时间超时判断帧结束)
  • 实现 CRC16 校验
  • 模拟一个从站(地址 0x01),响应读寄存器请求
  • 维护 10 个寄存器,初始值为 0~9

练习 4-6:思考题

  1. UART 波特率 115200 意味着每秒传输多少字节?传输 1KB 数据需要多少毫秒?

  2. TX 缓冲区设为 0 和设为 1024 有什么区别?什么时候需要 TX 缓冲区?

  3. 为什么 UART 通信需要双方约定相同的波特率?如果波特率差 1% 会发生什么?

  4. 事件驱动方式(uart_queue)和轮询方式(uart_read_bytes 循环)各有什么优缺点?


本章小结

知识点掌握程度自评
UART 参数配置⬜⬜⬜⬜⬜
数据发送与接收⬜⬜⬜⬜⬜
事件驱动模式⬜⬜⬜⬜⬜
字符串命令解析⬜⬜⬜⬜⬜
二进制帧协议⬜⬜⬜⬜⬜

上一章← 中断与定时器下一章I2C 与 SPI 通信 →

个人知识库