第一部分:UART 核心概念
1. 什么是 UART?
-
UART 中文常叫"通用异步收发器 ","异步"意思是通信双方不共享时钟,所以要提前约定好通信参数。 UART 本质上就是按约定好的速度,把 0 和 1 一位一位串行发出去
-
关键特点:
-
串行通信:一位一位传,节省引脚
-
异步通信:无时钟线,靠约定波特率
-
全双工:可同时收发
-
点对点:通常两个设备直连
-
更详细的UART知识
https://blog.csdn.net/2301_81636338/article/details/157764671?spm=1011.2124.3001.6209
2. 一帧数据的组成
-
起始位:1 位,标志传输开始
-
数据位:5~9 位,常用 8 位
-
校验位(可选):奇校验 / 偶校验 / 无校验
-
停止位:1 或 2 位,标志帧结束
-
常见配置 :
8N1(8 数据位,无校验,1 停止位)- 1 起始 + 8 数据 + 1 停止 = 10 位 / 字节
3. 波特率
-
定义:每秒传输的 bit 数
-
例:9600 波特 = 9600 bit/s ≈ 960 字节/秒(因为 1 字节占 10 位)
4. 为什么需要起始位和停止位?
-
因为没有时钟线,接收方需要:
-
通过起始位同步,知道"数据开始了"
-
按约定时间采样每位
-
通过停止位知道"这一帧结束了"
-
5. UART vs I2C vs SPI
| 特性 | UART | I2C | SPI |
|---|---|---|---|
| 同步/异步 | 异步 | 同步 | 同步 |
| 双工方式 | 全双工 | 半双工 | 全双工 |
| 线数 | 2 (TX, RX) + 地 | 2 (SDA, SCL) | 4+ (SCK, MOSI, MISO, CS) |
| 设备数 | 点对点 | 多设备(地址) | 一主多从(片选) |
第二部分:UART 常见问题与排查
-
参数不一致
- 检查波特率、数据位、停止位、校验位是否匹配
-
接线问题
-
TX ↔ RX 交叉连接
-
共地(GND 必须相连)
-
-
软件问题
-
缓冲区溢出
-
接收处理不及时(中断优先级、任务调度)
-
第三部分:ESP32 UART 实践
UART 初始化步骤
-
配置参数结构体
uart_config_t(波特率、数据位、停止位、校验位等) -
设置引脚
uart_set_pin() -
安装驱动
uart_driver_install() -
收发数据
循环发送 "Hello"
c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_err.h"
#define UART_PORT_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_TX_PIN 17
#define UART_RX_PIN 18
#define UART_BUF_SIZE 1024
void app_main(void)
{
uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.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_PORT_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(
UART_PORT_NUM,
UART_TX_PIN,
UART_RX_PIN,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(
UART_PORT_NUM,
UART_BUF_SIZE,
UART_BUF_SIZE,
0,
NULL,
0));
const char *msg = "Hello from ESP32-S3 UART1\r\n";
while (1) {
uart_write_bytes(UART_PORT_NUM, msg, strlen(msg));
printf("UART1 sent: %s", msg);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

回显(Echo)
c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_err.h"
#define UART_PORT_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_TX_PIN 17
#define UART_RX_PIN 16
#define UART_BUF_SIZE 1024
void app_main(void)
{
uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.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_PORT_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(
UART_PORT_NUM,
UART_TX_PIN,
UART_RX_PIN,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(
UART_PORT_NUM,
UART_BUF_SIZE,
UART_BUF_SIZE,
0,
NULL,
0));
uint8_t data[UART_BUF_SIZE];
while (1) {
int len = uart_read_bytes(
UART_PORT_NUM,
data,
UART_BUF_SIZE - 1,
pdMS_TO_TICKS(100));
if (len > 0) {
data[len] = '\0';
printf("UART1 received: %s\n", (char *)data);
uart_write_bytes(UART_PORT_NUM, (const char *)data, len);
}
}
}

中断方式(事件队列)
-
使用队列接收 UART 事件(数据到达、溢出、错误等)
-
适合需要高效处理、不丢数据的场景
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "esp_err.h"
#include "esp_log.h"#define UART_PORT_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_TX_PIN 17
#define UART_RX_PIN 16
#define UART_BUF_SIZE 1024
#define UART_QUEUE_SIZE 20static const char *TAG = "uart1_intr";
static QueueHandle_t uart_queue;static void uart_event_task(void *pvParameters)
{
uart_event_t event;
uint8_t data[UART_BUF_SIZE];while (1) { if (xQueueReceive(uart_queue, &event, portMAX_DELAY)) { switch (event.type) { case UART_DATA: int len = uart_read_bytes( UART_PORT_NUM, data, event.size < UART_BUF_SIZE - 1 ? event.size : UART_BUF_SIZE - 1, 0); if (len > 0) { data[len] = '\0'; ESP_LOGI(TAG, "recv: %s", (char *)data); uart_write_bytes(UART_PORT_NUM, (const char *)data, len); } break; case UART_FIFO_OVF: case UART_BUFFER_FULL: ESP_LOGW(TAG, "overflow or buffer full"); uart_flush_input(UART_PORT_NUM); xQueueReset(uart_queue); break; case UART_BREAK: case UART_PARITY_ERR: case UART_FRAME_ERR: ESP_LOGW(TAG, "error event: %d", event.type); break; default: break; } } }}
void app_main(void)
{
uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.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_PORT_NUM, &uart_config)); ESP_ERROR_CHECK(uart_set_pin( UART_PORT_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); ESP_ERROR_CHECK(uart_driver_install( UART_PORT_NUM, UART_BUF_SIZE, UART_BUF_SIZE, UART_QUEUE_SIZE, &uart_queue, 0)); xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 12, NULL); ESP_LOGI(TAG, "UART1 interrupt event echo started");}

DMA 方式(高性能)
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_attr.h"
#include "esp_log.h"
#include "esp_private/gdma.h"
#include "esp_private/periph_ctrl.h"
#include "hal/uhci_ll.h"
#include "soc/lldesc.h"
#define UART_PORT_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_TX_PIN 17
#define UART_RX_PIN 16
#define DMA_RX_BUF_SIZE 1024
#define DMA_TX_BUF_SIZE 1024
#define DMA_DESC_NUM 1
#define UART_RX_TOUT_THRESH 10
#define TASK_STACK_SIZE 4096
#define RX_DONE_BIT (1U << 0)
#define TX_DONE_BIT (1U << 1)
static const char *TAG = "uart_dma_echo";
static volatile uhci_dev_t *s_uhci_hw = &UHCI0;
static gdma_channel_handle_t s_rx_channel;
static gdma_channel_handle_t s_tx_channel;
static TaskHandle_t s_dma_task_hdl;
DMA_ATTR static uint8_t s_rx_buf[DMA_RX_BUF_SIZE];
DMA_ATTR static uint8_t s_tx_buf[DMA_TX_BUF_SIZE];
DMA_ATTR static lldesc_t s_rx_desc[DMA_DESC_NUM];
DMA_ATTR static lldesc_t s_tx_desc[DMA_DESC_NUM];
static bool IRAM_ATTR rx_eof_callback(gdma_channel_handle_t dma_chan,
gdma_event_data_t *event_data,
void *user_data)
{
BaseType_t high_task_wakeup = pdFALSE;
(void)dma_chan;
(void)event_data;
(void)user_data;
xTaskNotifyFromISR(s_dma_task_hdl, RX_DONE_BIT, eSetBits, &high_task_wakeup);
return high_task_wakeup == pdTRUE;
}
static bool IRAM_ATTR tx_eof_callback(gdma_channel_handle_t dma_chan,
gdma_event_data_t *event_data,
void *user_data)
{
BaseType_t high_task_wakeup = pdFALSE;
(void)dma_chan;
(void)event_data;
(void)user_data;
xTaskNotifyFromISR(s_dma_task_hdl, TX_DONE_BIT, eSetBits, &high_task_wakeup);
return high_task_wakeup == pdTRUE;
}
static void start_rx_dma(void)
{
memset(s_rx_buf, 0, sizeof(s_rx_buf));
memset(s_rx_desc, 0, sizeof(s_rx_desc));
lldesc_setup_link(s_rx_desc, s_rx_buf, DMA_RX_BUF_SIZE, true);
ESP_ERROR_CHECK(gdma_reset(s_rx_channel));
ESP_ERROR_CHECK(gdma_start(s_rx_channel, (intptr_t)&s_rx_desc[0]));
}
static void start_tx_dma(const uint8_t *data, size_t len)
{
if (len == 0 || len > DMA_TX_BUF_SIZE) {
return;
}
memcpy(s_tx_buf, data, len);
memset(s_tx_desc, 0, sizeof(s_tx_desc));
lldesc_setup_link(s_tx_desc, s_tx_buf, len, false);
ESP_ERROR_CHECK(gdma_reset(s_tx_channel));
ESP_ERROR_CHECK(gdma_start(s_tx_channel, (intptr_t)&s_tx_desc[0]));
}
static void uart_dma_init(void)
{
periph_module_enable(PERIPH_UHCI0_MODULE);
periph_module_reset(PERIPH_UHCI0_MODULE);
periph_module_enable(PERIPH_UART1_MODULE);
periph_module_reset(PERIPH_UART1_MODULE);
uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.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_PORT_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM,
UART_TX_PIN,
UART_RX_PIN,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE));
// 设置 UART 空闲超时,空闲一小段时间就认为一帧结束
ESP_ERROR_CHECK(uart_set_rx_timeout(UART_PORT_NUM, UART_RX_TOUT_THRESH));
gdma_channel_alloc_config_t tx_config = {
.direction = GDMA_CHANNEL_DIRECTION_TX,
.flags.reserve_sibling = 1,
};
ESP_ERROR_CHECK(gdma_new_channel(&tx_config, &s_tx_channel));
gdma_channel_alloc_config_t rx_config = {
.direction = GDMA_CHANNEL_DIRECTION_RX,
.sibling_chan = s_tx_channel,
};
ESP_ERROR_CHECK(gdma_new_channel(&rx_config, &s_rx_channel));
ESP_ERROR_CHECK(gdma_connect(s_tx_channel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_UHCI, 0)));
ESP_ERROR_CHECK(gdma_connect(s_rx_channel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_UHCI, 0)));
gdma_strategy_config_t strategy = {
.auto_update_desc = false,
.owner_check = false,
};
ESP_ERROR_CHECK(gdma_apply_strategy(s_tx_channel, &strategy));
ESP_ERROR_CHECK(gdma_apply_strategy(s_rx_channel, &strategy));
gdma_rx_event_callbacks_t rx_cbs = {
.on_recv_eof = rx_eof_callback,
};
ESP_ERROR_CHECK(gdma_register_rx_event_callbacks(s_rx_channel, &rx_cbs, NULL));
gdma_tx_event_callbacks_t tx_cbs = {
.on_trans_eof = tx_eof_callback,
};
ESP_ERROR_CHECK(gdma_register_tx_event_callbacks(s_tx_channel, &tx_cbs, NULL));
uhci_ll_init((uhci_dev_t *)s_uhci_hw);
uhci_ll_set_eof_mode((uhci_dev_t *)s_uhci_hw, UHCI_RX_IDLE_EOF);
s_uhci_hw->escape_conf.val = 0;
uhci_ll_attach_uart_port((uhci_dev_t *)s_uhci_hw, 1);
}
static void dma_echo_task(void *arg)
{
uint32_t notify_value = 0;
(void)arg;
start_rx_dma();
while (1) {
xTaskNotifyWait(0, UINT32_MAX, ¬ify_value, portMAX_DELAY);
if (notify_value & RX_DONE_BIT) {
int rx_len = lldesc_get_received_len(s_rx_desc, NULL);
if (rx_len > 0 && rx_len <= DMA_RX_BUF_SIZE) {
ESP_LOGI(TAG, "rx_len=%d", rx_len);
start_tx_dma(s_rx_buf, rx_len);
}
start_rx_dma();
}
if (notify_value & TX_DONE_BIT) {
ESP_LOGI(TAG, "tx done");
}
}
}
void app_main(void)
{
uart_dma_init();
xTaskCreate(dma_echo_task, "dma_echo_task", TASK_STACK_SIZE, NULL, 12, &s_dma_task_hdl);
ESP_LOGI(TAG, "UART1 DMA echo started");
ESP_LOGI(TAG, "TX=%d RX=%d baud=%d", UART_TX_PIN, UART_RX_PIN, UART_BAUD_RATE);
}

代码较长,核心要点:
初始化 GDMA 通道并连接到 UHCI
使用
lldesc_t链表管理内存通过中断回调通知任务处理数据