目录
[2.1 uart_m.c](#2.1 uart_m.c)
[2.2 uart_m.h](#2.2 uart_m.h)
[2.3 mian.c](#2.3 mian.c)
[2.4 代码解释](#2.4 代码解释)
[2.4.1 接收回调函数触发方式](#2.4.1 接收回调函数触发方式)
[2.4.2 接收机制](#2.4.2 接收机制)
前言
在《ESP-IDF+vscode开发ESP32第三讲------UART_esp32 串口 接收中断 vscode-CSDN博客》中我们使用了UART去收发数据,相比大家都知道UART经常搭配DMA使用。DMA是直接内存访问技术,无需通过中央处理器(CPU)的干预。对于DMA我这就不多介绍了,在STM32系列中有很多运用和解释。
本章就使用ESP-IDF 中 UART DMA(UHCI)驱动的功能来完成数据的收发。本文使用的开发板是微雪的**ESP32-P4-Module-DEV-KIT。**ESP-IDF版本是6.0.1。基于第三讲的工程来扩展。也建议先学习第三讲再学习本讲内容。
一、HUCI介绍
ESP32-P4 中的五个 UART 接口通过通用主机控制器接口 (UHCI) 共用 1 组 GDMA TX/RX 通道。在 GDMA 模式 下,支持对 HCI 协议数据包的解析 (decoder) 及数据包封装 (encoder)。UHCI_UART_SEL 字段用于选择哪个串口占用 GDMA 通道。数据传输模式如下:

注:UART DMA 与 BT 共享 HCI 硬件,因此请勿同时使用 BT HCI 和 UART DMA,哪怕它们使用的是不同的 UART 端口。
二、完善工程
打开SDK配置编辑器,搜索huci,可以打开调试日志,能看到更多信息。剩下两个开关作用见《UART DMA (UHCI) - ESP32-P4 - --- ESP-IDF 编程指南 v6.0.1 文档》

2.1 uart_m.c
添加头文件 #include "driver/uhci.h",下面是实现代码
cpp
#define uhci_tx_size 1024
#define uhci_rx_size 1024
uhci_controller_handle_t uhci_ctrl = NULL;
bool uhci_rx_callback(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx);
bool uhci_tx_callback(uhci_controller_handle_t uhci_ctrl, const uhci_tx_done_event_data_t *edata, void *user_ctx);
void uhci_take_callback(void *arg);
TaskHandle_t uhci_task_handle = NULL;
size_t rx_data_size = 0;
void uart_dma_init(void)
{
uart_config_t uart_config = {
.baud_rate = 1000000,
.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, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(uart_port, uart_tx_pin, uart_rx_pin, -1, -1, -1, -1));
uhci_controller_config_t uhci_config = {
.uart_port = uart_port,
.tx_trans_queue_depth = 5,
.max_transmit_size = uhci_tx_size,
.max_receive_internal_mem = uhci_rx_size,
.dma_burst_size = 64,
.rx_eof_flags.idle_eof = 1,
};
ESP_ERROR_CHECK(uhci_new_controller(&uhci_config, &uhci_ctrl));
uhci_event_callbacks_t uhci_cbs = {
.on_rx_trans_event = uhci_rx_callback,
.on_tx_trans_done = uhci_tx_callback,
};
ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, (void*)&rx_data_size));
xTaskCreate(uhci_take_callback, "uhci_take_callback", 4096, NULL, 10, &uhci_task_handle);
}
void uhci_take_callback(void *arg)
{
uint8_t *pdata = heap_caps_calloc(1, uhci_rx_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
assert(pdata);
ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, uhci_rx_size));
while(1)
{
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
ESP_ERROR_CHECK(uhci_transmit(uhci_ctrl, pdata, rx_data_size));
ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, uhci_rx_size));
//ESP_ERROR_CHECK(uhci_wait_all_tx_transaction_done(uhci_ctrl, portMAX_DELAY));
}
}
bool uhci_rx_callback(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
{
BaseType_t TaskWoken = pdFALSE;
ESP_LOGI(TAG, "接收到的数据大小: %d", edata->recv_size);
*(size_t*)user_ctx = edata->recv_size;
if(edata->flags.totally_received){
ESP_LOGI(TAG, "所有数据接收完成");
vTaskNotifyGiveFromISR(uhci_task_handle, &TaskWoken);
}
else{
ESP_LOGI(TAG, "还有数据等待接收未完成");
}
return true;
}
bool uhci_tx_callback(uhci_controller_handle_t uhci_ctrl, const uhci_tx_done_event_data_t *edata, void *user_ctx)
{
ESP_LOGI(TAG, "已发送数据大小: %d", edata->sent_size);
return false;
}
2.2 uart_m.h
cpp
#ifndef UART_M_H
#define UART_M_H
#include <string.h> // 字符串处理函数
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
#include "hal/uart_ll.h"
#define uaer_rx_buffer 512
#define uart_tx_buffer 512
#define uart_port UART_NUM_0
#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);
void uart_dma_init(void);
#endif // UART_M_H
2.3 mian.c
cpp
#include <stdio.h>
#include "user.h"
#include "uart_m.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
uart_dma_init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.4 代码解释
uart DMA涉及的API不多,这个工程几乎全用到了。使用步骤如下。关于各API的定义可见《ESP32 实用API指南2_xiao-esp32 api-CSDN博客》
首先需要使用uart_param_config函数初始化通讯配置,然后使用uart_set_pin函数绑定管脚。和单纯使用uart比起来少了uart_driver_install和uart_set_mode函数的使用。原先单纯uart的中断这边也暂时不用了。我这使用了很高的波特率,为1000000,为了模拟DMA大数据收发需要的高速运行。
接着就使用函数uhci_new_controller对dma传输进行配置,接着使用uhci_register_event_callbacks注册接收回调函数和发送回调函数。
最后创建一个工作任务,初始化就完成了。
2.4.1 接收回调函数触发方式
该回调函数主要在以下 3 种核心情况 下被触发(进入):
- 达到预设的接收长度
这是最标准的触发条件。
- 触发机制:使用uart_param_config配置时都会指定一个接收目标缓冲区大小,当 DMA 控制器将 UART FIFO 中的数据搬运到内存,达到了这个预设的字节数,硬件会产生一个中断,进入接收回调函数。
- 满足UHCI 结束标志
这是处理变长数据包时最关键的触发条件。
- 触发机制:使用uart_param_config配置时可以通过rx_eof_flags选择结束标志,有三种,分别是,UART 接收到空帧、UART 处于空闲状态和接收字节数达到特定值。当dma传输工作中满足你设定的状态,就会触发一个中断,进入接收回调函数。
- DMA 错误或溢出
- 触发机制:如果 UART 接收数据的速度太快,而你配置的 DMA 缓冲区太小(或者 DMA 描述符链耗尽),导致数据来不及搬运,就会发生 DMA 溢出(Overflow) 或描述符错误。
而发送回调函数进入方式就是当数据发送完成后进入。
2.4.2 接收机制
所以我这再任务uhci_take_callback中首先初始化了一个DMA接收缓冲区,接着调用函数uhci_receive来来挂载 DMA 缓冲区并启动 DMA 通道,从而接收数据,这是一个单次有效的函数,只有当DMA触发了接受回调后才会停止(如果是DMA 溢出造成的回调函数触发则不会停止)。所以必须在接收完成后(退出接受回调后)再次调用uhci_receive来挂载新的(或同一个)缓冲区,以准备接收下一帧数据。
**注:**由于uhci_receive是单次的,在 "上一次 DMA 触发回调" 到 "下一次调用 uhci_receive重新启动" 之间,存在一个时间差(真空期)。在这个真空期内:DMA 是停止的。此时 UART 收到的数据只能存放在硬件 FIFO 中(ESP32-C3/C6 的 UART FIFO 通常为 128 字节)。
风险:如果你的回调函数执行太慢,或者主任务重启 DMA 太慢,这期间如果对方发来的数据超过了 128 字节,就会发生 FIFO 溢出,导致数据静默丢失。
所以我这在接收回调函数退出时使用vTaskNotifyGiveFromISR启动任务uhci_take_callback,在任务中使用uhci_transmit将数据回传后,继续开启下一轮uhci_receive接收。因为这两个函数都是非阻塞的,所以真空期极短。但是我这因为接收和发送使用同一个缓冲区,所以为了保证发送的数据无误,真空期最少要大于数据发送时间。
三、结果展示


可以看到前三次数据帧个数小于设定的缓冲区大小1024,一次就接收完成,出发了空闲接收回调。当第四次数据包字节数大于1024,首先触发溢出回调中断,此时因为没有完成数据收发,所以不会启动uhci_take_callback任务,从而也不会启动数据回传,而且这种回调触发不会停止DMA接收,所以立马接收了剩余的字节数,接着触发空闲接受回调,此时会启动uhci_take_callback任务,故uhci_transmit会将第二次触发空闲接受回调接受的数据回传回来。
四、扩展
uart使用dma也可以用硬件流控,而且在高速或大数据量传输场景下,强烈建议将 UART DMA 与硬件流控(RTS/CTS)结合使用!
在 UART 接收数据时,数据流向是:外部设备 -> UART RX 引脚 -> UART 硬件 FIFO (通常 128 字节) -> DMA 搬运 -> 内存 (RAM)。
-
如果没有硬件流控(仅用 DMA):
当你的 DMA 缓冲区满了,或者在"上一次 DMA 完成"到"下一次调用
uhci_receive()重启 DMA"的真空期内,如果外部设备继续疯狂发数据,UART 的 128 字节 FIFO 瞬间就会被塞满。FIFO 满后,新来的数据会触发 FIFO 溢出错误(Overrun Error),导致数据永久丢失。 -
加上硬件流控后:
硬件流控是 UART 控制器内部的纯硬件逻辑,它独立于 CPU 和 DMA 运行。当 UART FIFO 中的数据量达到你设定的阈值时,UART 硬件会自动拉高 RTS 引脚电平,告诉外部设备:"我快满了,暂停发送!"。等 DMA 把 FIFO 里的数据搬走,FIFO 水位下降后,硬件又会自动拉低 RTS,让对方继续发送。
硬件流控完美弥补了 DMA 启停过程中的"真空期"和缓冲区不足的问题,实现了零丢包。