目录
前言
本文基于第一章创建好的工程,来写一个基于UART的通信工程,本章不对UART这个通讯技术本身进行讲解,在STM32篇已经涉及到了。重点在于梳理UART在ESP32上的的衍生功能
开发板芯片是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 MTMSMTDO | GPIO2 GPIO3GPIO4GPIO5 |
| USB1P1_N/P | 当不使用上述JTAG接口,可用该接口代替JTAG接口的功能 | USB1P1_N0 USB1P1_P0USB1P1_N1 USB1P1_P1 | GPIO24GPIO25GPIO26GPIO27 |
| UART0 | 异步通信串口 | UART0_TXDUART0_RXD | GPIO37GPIO38 |
| LP_UART | 低功耗异步通信串口 | LP_UART_TXDLP_UART_RXD | LP_GPIO14LP_GPIO15 |
| USB | 专用USB 2.0 接口 | USB D-USB D+ | USB_DMUSB_DP |
以上是常用的,实际中还有更多,这些是我们比较关注的,平时优先保证这些引脚的默认复用功能。
1.1 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控制器,改为外置的。
1.2 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 发送数据。
1.3 流控
UART 流控是一种防止发送方发得太快、接收方缓存爆掉丢数据的控制机制,UART的流控分为硬件流控和软件流控。
硬件流控
靠 2 根额外硬件信号线实现:RTS / CTS
- CTS (Clear To Send) 输入信号,清除发送,低电平有效
- RTS (Request To Send) 输出信号,请求发送,低电平有效
- 双方CTS接RTS,RTS接CTS,交叉连接。
工作原理如下:
当接收方 RX 缓存快满会拉低 RTS,RTS输出低电平,发送方检测到CTS变低,立即停止发送;等待接收方数据处理完毕,缓存空余,拉高RTX,RTS输出高电平,发送方检测到CTS变高,立即继续发送。空闲时CTS和RTS均上拉。
硬件流控:速度快、实时性强,不干扰数据内容,稳定可靠,工业常用。
ESP32的UART硬件流控主要通过输出信号 rtsn_out 以及输入信号 ctsn_in 进行数据流控制。信号连接图如下:
编辑
软件流控
不占任何额外引脚,直接在串口数据里插入特殊控制字符:
- XOFF = 0x13 → 暂停发送
- XON = 0x11 → 恢复发送
工作原理如下:
当接收方 RX 缓存快满会主动发一个xoff,发送方检测到xoff字节,立即停止发送;等待接收方数据处理完毕,缓存空余会主动发一个xon,发送方检测到xon字节,立即继续发送。
软件流控:不占 GPIO,省引脚,但反应慢,依赖数据链路延迟,高波特率下容易丢包,且不能传二进制数据(因为如果数据里恰好有 0x11/0x13 会被误判)
1.4 GDMA
GDMA 是 ESP32 系列芯片中的通用直接内存访问控制器,核心作用是在 CPU 不参与的情况下,让外设与内存、内存与内存之间直接高速传输数据,从而减轻 CPU 负载、提升系统整体性能,尤其适合大批量数据传输场景。
ESP32-P4 中的五个 UART 接口通过通用主机控制器接口 (UHCI) 共用 1 组 GDMA TX/RX 通道。这代表了同时只能有一个uart接口使用GDMA通道。在 GDMA 模式下,支持对 HCI 协议数据包的解析 (decoder) 及数据包封装 (encoder)。数据传输图如下:
编辑
在 GDMA Rx 通道接收数据前,软件将接收链表准备好。 通用主机控制器接口 (UHCI) 会将 UART 接收到的数据传送给 decoder。经过 decoder 解析之后的数据在 GDMA 通道的控制下存入接收链表指定的 RAM 空间。
官方手册在uart章节并没有涉及到GDMA模式的使用,等后面涉及到了,在进行补充。
二、代码编写
这里只使用uart的基本模式。基于工程模板创建组件uart。添加依赖:
- REQUIRES esp_driver_uart
- REQUIRES esp_driver_gpio
驱动代码如下,我这里始终使用GPIO37和GPIO38作为uart串口,是因为我的开发板只有这个引脚接入了空闲的usb接口,当然可以使用其他引脚,不过不好接线读取。可以在0-4中任意改变uaer端口。
2.1 uart.c
#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
#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
#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用来实现硬件自动波特率检测,我这里并未使用。
三、结果展示
编辑