目录
[2.1 uart.c](#2.1 uart.c)
[2.2 uart.h](#2.2 uart.h)
[2.3 mian.c](#2.3 mian.c)
[2.4 重点提示](#2.4 重点提示)
前言
本文基于第一章创建好的工程,来写一个基于UART的通信工程,本章不对UART这个通讯技术进行讲解,在STM32篇已经涉及到了。本章只是梳理在ESP32使用UART的步骤。
开发板芯片是ESP32-P4、ESP-IDF版本是5.5.3。
一、 ESP32中UART的配置
ESP32-P4芯片中共有六个 UART 控制器,包含五个正常功能的 UART0~UART4 和一个满足低功耗需求的 LP UART。另外,UART 还可以用于红外数据交换 (IrDA) 或 RS485 调制解调器。一些基本特性如下:

注意,ESP32和STM32不一样,它的UART不是事先规定复用到哪些特殊引脚上,而是 可配置使用任意 GPIO 管脚,也就是说你可以选择任何一个GPIOx引脚成为RXD或TXD引脚,当然有些特殊引脚除外。
当然,官方也事先给一些引脚配置了默认复用功能。以ESP32-P4为例,如下:
|------------|-------------------------------|-----------------------------------------|-----------------------------|
| 接口 | 功能 | 功能IO | 默认IO |
| JTAG接口 | 用于调试、烧录代码 | MTCK MTDI MTMS MTDO | GPIO2 GPIO3 GPIO4 GPIO5 |
| USB1P1_N/P | 当不使用上述JTAG接口,可用该接口代替JTAG接口的功能 | USB1P1_N0 USB1P1_P0 USB1P1_N1 USB1P1_P1 | GPIO24 GPIO25 GPIO26 GPIO27 |
| UART0 | 异步通信串口 | UART0_TXD UART0_RXD | GPIO37 GPIO38 |
| LP_UART | 低功耗异步通信串口 | LP_UART_TXD LP_UART_RXD | LP_GPIO14 LP_GPIO15 |
| USB | 专用USB 2.0 接口 | USB D- USB D+ | USB_DM USB_DP |
以上是常用的,实际中还有更多,这些是我们比较关注的,平时优先保证这些引脚的默认复用功能。
rs485
ESP32的UART的五个 UART 控制器支持 RS485 通讯模式,这是stm32所不具备的。RS485 有两线半双工及四线全双工两种选择,UART 模块采用两线半双工模式,也就是只要两根差分信号线,只能收或者发。控制结构图如下:

当DE被使能为1时,就使能驱动器D了,此时使用了RS485 发送模式,使用D+、D-差分信号发送数据,数据内容由TXD决定;当DE被使能为0时,就关闭驱动器D了,此时使用原本UART通讯模式,直接使用TXD发送数据。
当RE被使能为0时,就使能接收器R了,此时使用了RS485 接收模式,使用D+、D-差分信号接收数据,数据内容转化为RXD;当DE被使能为1时,就关闭接收器R了,此时使用原本UART通讯模式,直接使用RXD接收数据。
在其他的MCU中,如STM32,没有内置RS485控制器,也就是图中右半部分,此时想要使用RS485功能需要外置RS485控制器。当然ESP32也可以不使用内置的RS485控制器,改为外置的。
IrDA
IrDA 是 UART 的红外无线版,它的核心作用是让普通串口通过红外光无线通信,且上层软件完全不用改,只改硬件物理层。家里的电视 / 空调遥控器用的就是IrDA协议。
| 对比维度 | 普通 UART(有线串口) | IrDA UART(红外串口) |
|---|---|---|
| 传输介质 | 金属导线(TX/RX/GND) | 红外光(940nm 红外 LED) |
| 信号形式 | 直接电平信号(高 / 低电平) | 脉冲编码信号(硬件自动转脉冲) |
| 通信模式 | 全双工(可同时收发) | 半双工(同一时间只能发 OR 收) |
| 硬件结构 | 仅 UART 控制器,直连导线 | UART + 红外收发模块(发射管 + 接收头) |
| 电气隔离 | 无隔离,需要共地 | 完全电气隔离,无需共地 |
| 传输距离 | 有线 1~5 米 | 红外 0~3 米(必须对准) |
ESP32的UART 实现了其物理层协议。在 IrDA 编码模式下,支持最大信号速率到 115.2 Kbit/s,即 SIR 模式。来看一下这个模式下两种串口的时序对应,需要使用时再去深入研究

ESP32的UART的五个 UART 控制器也支持 IrDA 通讯模式,IrDA 是半双工传输协议,结构图如下

置位 UART_IRDA_EN 使能 IrDA 功能。置位 UART_IRDA_TX_EN(置 1)使能 IrDA 发送数据,这时不允许 IrDA 接收数据;复位 UART_IRDA_TX_EN(清 0) 使能 IrDA 接收数据,这时不允许 IrDA 发送数据。
二、代码编写
这里只使用uart的基本模式。基于工程模板创建组件uart。添加依赖:
- REQUIRES esp_driver_uart
- REQUIRES esp_driver_gpio
驱动代码如下,我这里始终使用GPIO37和GPIO38作为uart串口,是因为我的开发板只有这个引脚接入了空闲的usb接口,当然可以使用其他引脚,不过不好接线读取。可以在0-4中任意改变uaer端口。
2.1 uart.c
cpp
#include <stdio.h>
#include "uart.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "UART";
QueueHandle_t uart_intr_handle= NULL;
void uart_intr_callback(void *arg);
int uart_band_aotuset(void)
{
uart_bitrate_detect_config_t uart_bitrate_config = {
.rx_io_num = uart_rx_pin,
.source_clk = UART_SCLK_XTAL,
};
ESP_ERROR_CHECK(uart_detect_bitrate_start(uart_port, &uart_bitrate_config));
vTaskDelay(pdMS_TO_TICKS(20));
uart_bitrate_res_t res;
ESP_ERROR_CHECK(uart_detect_bitrate_stop(uart_port, true, &res));
if(res.low_period == 0 || res.high_period == 0){
ESP_LOGE(TAG, "检测失败:无有效边沿");
return ESP_FAIL;
}
else{
ESP_LOGI(TAG, "检测成功,波特率为:%d", res.clk_freq_hz / (res.low_period + res.high_period));
return res.clk_freq_hz / (res.low_period + res.high_period);
}
}
void uart_init(void)
{
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,
};
ESP_ERROR_CHECK(uart_driver_install(uart_port, uaer_rx_buffer, uart_tx_buffer, 5, &uart_intr_handle, 0));
ESP_ERROR_CHECK(uart_param_config(uart_port, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(uart_port, uart_tx_pin, uart_rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_set_mode(uart_port, UART_MODE_UART));
//中断配置
uart_intr_config_t intr_config = {
.intr_enable_mask = uart_intr,
.rx_timeout_thresh = 20,
.rxfifo_full_thresh = 50,
};
ESP_ERROR_CHECK(uart_intr_config(uart_port, &intr_config));
uart_enable_intr_mask(uart_port, uart_intr);
ESP_LOGI(TAG, "UART初始化成功,波特率默认为115200");
xTaskCreate(uart_intr_callback, "uart_intr", 4096, NULL, 5, NULL);
}
void uart_intr_callback(void *arg)
{
uart_event_t uart_event;
int wait_read_size, len;
char read_data[100];
while(1)
{
xQueueReceive(uart_intr_handle, (void*)&uart_event, portMAX_DELAY);
switch (uart_event.type)
{
case UART_DATA:
if (uart_event.timeout_flag == false){
ESP_LOGI(TAG, "触发接收FIFO阈值中断, uart rx data size: %d", uart_event.size);
}
else{
ESP_LOGI(TAG, "触发接收超时中断, uart rx data size: %d", uart_event.size);
}
break;
default:
ESP_LOGI(TAG, "其他未知事件触发");
break;
}
uart_get_buffered_data_len(uart_port, (size_t *)&wait_read_size);
len = uart_read_bytes(uart_port, read_data, wait_read_size, 1000);
read_data[len] = '\0';
ESP_LOGI(TAG, "uart rx data: %s", read_data);
}
}
2.2 uart.h
cpp
#ifndef __UART_H_
#define __UART_H_
#include "driver/gpio.h"
#include "driver/uart.h"
#include "hal/uart_ll.h" // 取得uart的寄存器地址
#define uaer_rx_buffer 512
#define uart_tx_buffer 512
#define uart_port UART_NUM_4
#define uart_tx_pin GPIO_NUM_37
#define uart_rx_pin GPIO_NUM_38
#define uart_intr UART_INTR_RXFIFO_TOUT | UART_INTR_RXFIFO_FULL
void uart_init(void); // 初始化uart
int uart_band_aotuset(void); // 检测波特率
#endif
2.3 mian.c
cpp
#include <stdio.h>
#include "user.h"
#include "uart.h"
#include "esp_log.h"
static char* TAG = "MAIN";
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
uart_init();
while (1)
{
char *test_str = "This is a test string.\n";
uart_write_bytes(uart_port, test_str, strlen(test_str));
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
代码流程如上,下面会对一些重点进行提示。具体函数功能可以参考官方的编程手册和我整理的《ESP32 实用API指南》
2.4 重点提示
UART 驱动本身没有像其他组件那样有注册回调函数API,所有的中断事件都会存入队列,函数uart_driver_install 的queue_size 就是用来设置这个中断队列的长度,就是最大能容下几个中断源。uart_queue返回的是中断队列的句柄。
uart_intr_config 与uart_enable_intr_mask需要配套使用,一个用于配置中断源,一个用于使能中断源。
需要创建一个任务专门用于处理uart数据接收和发送,任务栈建议4096起步,否则容易溢出。
声明 #include "hal/uart_ll.h" 能够使用中断源的宏,否则会报错。具体中断源见ESP32-P4 技术参考手册 > UART 控制器 (UART) > UART 中断 [PDF]。
比较常用的中断源是UART_INTR_RXFIFO_TOUT 与**UART_INTR_RXFIFO_FULL,**TOUL是空闲中断,一个数据包接收后产生中断;FULL是接收阈值中断,当一个包的字节数大于设定的阈值便产生中断。
在中断任务中使用队列接收函数xQueueReceive ,阻塞等待中断源的到来,将接收的缓冲区指针用结构体uart_event_t 接收,结构体中uart_event_type_t用来判断中断类型。其中上面说的TOUL和FULL两个中断都会归于类型UART_DATA,其他则是一个类型对应一个中断。
结构体uart_event_t 中的timeout_flag 用来判断类型UART_DATA具体是哪一个中断源触发的,当timeout_flag = false 代表是FULL中断,timeout_flag = ture代表是TOUL中断。另外size用来判断类型UART_DATA接收到的数据大小。
函数uart_band_aotuset用来实现硬件自动波特率检测,我这里并未使用。
三、结果展示
