Appearance
第四章:串口通信 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 映射到任意 IO4.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:思考题
UART 波特率 115200 意味着每秒传输多少字节?传输 1KB 数据需要多少毫秒?
TX 缓冲区设为 0 和设为 1024 有什么区别?什么时候需要 TX 缓冲区?
为什么 UART 通信需要双方约定相同的波特率?如果波特率差 1% 会发生什么?
事件驱动方式(uart_queue)和轮询方式(uart_read_bytes 循环)各有什么优缺点?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| UART 参数配置 | ⬜⬜⬜⬜⬜ |
| 数据发送与接收 | ⬜⬜⬜⬜⬜ |
| 事件驱动模式 | ⬜⬜⬜⬜⬜ |
| 字符串命令解析 | ⬜⬜⬜⬜⬜ |
| 二进制帧协议 | ⬜⬜⬜⬜⬜ |
上一章:← 中断与定时器下一章:I2C 与 SPI 通信 →