DMA与空闲中断下的串口非阻塞收发技术详解:HAL库学习心得
DMA + 空闲中断的串口不定长非阻塞收发(HAL库学习笔记)
代码的相应解释
代码功能
本次代码为 DMA+空闲中断来实现串口的不定长收发(非阻塞方式)
适合需要快速响应和低CPU占用的嵌入式场景
相关术语的解释
阻塞与非阻塞方式的区别
阻塞:函数调用后,程序会一直等待直到数据传输完成才继续执行
不断检查串口状态寄存器直到操作完成,通常允许设置超时时间,防止无限等待。CPU长时间处于等待状态,浪费计算资源对于简单的项目可能干扰不大,对于多线程的任务,采用阻塞的方式往往程序会卡死。
代码示例:
`HAL_UART_Transmit(&huart1, data, sizeof(data), 100);
HAL_UART_Receive(&huart1, buffer, sizeof(buffer), 100);`
非阻塞式:函数调用后立即返回,数据传输在后台进行,允许CPU在传输期间处理其他任务,适合大数据量或高频率通信,不占用大量的CPU资源,相比阻塞方式代码更为复杂需要用到中断或者是DMA
代码示例:
HAL_UART_Transmit_IT(&huart1, data, sizeof(data));//DMA方式
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
//中断方式
// 发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
// 处理发送完成逻辑
}
}
接收完成中断回调函数
下面这个函数为串口的接收完成中断回调函数,也是我们本次功能实现的核心代码
这个回调函数会有两种情况被触发:
1.总线空闲中断(Idle Interrupt):当UART总线在一段时间内无新数据(即总线空闲)时触发。
2.DMA接收完成(DMA Transfer Complete):当DMA接收缓冲区填满(达到设定的最大长度)时触发。
也就是当Size这个参数等于缓冲区的大小,文章后面会给出实例代码
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size{
}
STM32CubeMX 配置
选择外部高速时钟HSE
选择Dubug方式
选择Asynchronous(异步通信)其他参数如波特率 校验位 停止位 默认初始就好
同步通信:发送方在发送数据时,接收方必须处于接收状态,数据传输是实时进行的。数据传输准确但占用资源 例如(SPI I2C)
异步通信:发送方可以在任何时间发送数据,而接收方可以在任意时间接收数据,双方的操作相互独立。时间独立适应性强,协议设计相对复杂(例如串口)
异步通信不需要共享时钟信号 不需要clk信号线,串口仅需要两根线(Tx Rx)
在串口页面点击DMA Setting配置DMA通道点击Add添加两个通道,并且为两个通道更改对应的串口名字
DMA可以理解为不占用CPU资源 找个黑奴给你搬砖
数据搬运方向(Direction)
TX: 内存(Memory)–>外设(Peripheral)
RX: 外设(Peripheral)–>内存(Memory)
点击NVIC 配置中断我这里优先级选择为1
然后配置对应时钟 我使用的开发板为G474所以我频率配置的高一些,如果你使用的F1C8T6可以直接配置为主频72M
这里一定要勾选为HSI 内部高速晶振 否则你的串口会输出乱码
因为有些单片机没有外部高速晶振HSE
代码编写
添加这四个头文件因为我们要仿造printf编写一个非阻塞的打印函数
生成代码之后打开我们进入串口的主函数
在串口的主函数中已经帮我们配好了DMA通道的参数还有串口的初始化
我们下拉到最下面找到这个写函数的地方(我图中圈的位置)
添加相应的变量
uint8_t rxBuffer[50]; //接受缓冲区
volatile uint8_t isUART3Busy = 0; // 发送状态标志
uint8_t txBuffer[512]; // 发送缓冲区
编写打印函数 u1_printf_nonblocking
// 非阻塞发送函数
// 相关参数解释"char *fmt"格式化字符串通过占位符如(%d)指定"..."(可变参数列表)的参数类型
void u1_printf_nonblocking(char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int len = vsnprintf((char *)txBuffer, sizeof(txBuffer), fmt, ap);
va_end(ap); //可变参数的处理将格式化的字符串写入txBuffer
if (len > 0) {
// 检查DMA是否在忙
if (isUART3Busy) {
return; // 忙则直接返回
}
isUART3Busy = 1; // 标记为忙 避免其他代码触发发送时冲突
HAL_UART_Transmit_DMA(&huart3, txBuffer, len);//以DMA方式发送txBuffer中的数据
}
}
编写接受完成中断回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//接收完成中断回调
if (huart == &huart3) {
// Size 参数由 HAL_UARTEx_RxEventCallback 自动传入,表示实际接收到的数据字节数 实现接什么发什么
// %.*s 动态绑定长度和缓冲区地址
u1_printf_nonblocking("Received: %.*s\r\n", Size, rxBuffer);
// 重新启动接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer));
__HAL_DMA_DISABLE_IT(&hdma_usart3_rx, DMA_IT_HT);
}
}
编写发送完成中断回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { //DMA发送完成后会调用这个函数
if (huart == &huart3) {
isUART3Busy = 0; // 标记为空闲
}
}
在主函数中开启DMA接收 关闭DMA半完成中断 并且声明相关变量
extern uint8_t rxBuffer[50];
extern DMA_HandleTypeDef hdma_usart3_rx;//在主函数声明相关参数
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart3,rxBuffer,sizeof(rxBuffer));
//第三个参数填最大能接收的长度 接受不定长 启动串口3的DMA接收
__HAL_DMA_DISABLE_IT(&hdma_usart3_rx,DMA_IT_HT);
//禁用DMA的半传输中断 该中断通常用于双缓冲机制 前半段传输完成后 可以在处理前半段的数据时DMA继续接收后半中断
//我们启动不定长接收 数据结束的标志是总线空闲(idle),而非固定长度,所以禁用此中断
u1_printf_nonblocking("Hello World!\r\n%d,%x,%o,%f\r\n", 100, 100, 100, 45.78);//测试代码上电复位发送一次
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
这些函数放入主函数中初始化函数的下面就可以
也别忘了要在usart.h中声明一下u1_printf_nonblocking这个函数不然在主函数中是用不了
点击小剪刀勾选Use McroLIB识别字符串
代码逻辑
在主函数中初始化HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer))
启动UART3的DMA接收,配置为接收数据直到检测到总线空闲(Idle)或缓冲区满(50字节)
禁用DMA的半传输中断(DMA_IT_HT),仅关注空闲事件
接受数据流程:
当UART3接收到数据:DMA将数据存入 rxBuffer,直到触发以下条件之一:
接收满50字节(缓冲区满)
检测到总线空闲(Idle)
接收完成后,HAL库自动调用 HAL_UARTEx_RxEventCallback,并传入实际接收的字节数 Size
在回调函数中:
通过u1_printf_nonblocking(“Received: %.*s\r\n”, Size, rxBuffer) 将接收到的数据回显(接什么发什么)
重新调用 HAL_UARTEx_ReceiveToIdle_DMA 启动下一次接收,禁用半传输中断,避免处理不完整数据(跟主函数初始化一样)
发送数据流程:
调用 u1_printf_nonblocking 发送数据:
使用 vsnprintf 将格式化字符串写入 txBuffer。
检查 isUART3Busy 标志:
若为0(空闲):启动DMA发送(HAL_UART_Transmit_DMA),并置 isUART3Busy = 1。
若为1(忙碌):直接返回,放弃本次发送(防止数据覆盖)。
DMA发送完成后:
HAL库自动调用 HAL_UART_TxCpltCallback。
在回调中置 isUART3Busy = 0,标记发送通道空闲
测试结果
正确打印出相关参数 并且在发送你好之后数据会回显
4月5日改动:
isUART3Busy标志用于标记UART发送状态。当连续调用u1_printf_nonblocking时,若前一次发送未完成(isUART3Busy=1),后续调用直接返回,数据被丢弃。
例如,两次快速调用打印函数时,第二个调用因标志位未复位而被跳过,所以为了解决数据丢失的问题我们会引入环形缓冲区
添加缓冲区后的完整代码:
// --- 配置 ---
#define TX_QUEUE_SIZE 4
#define TX_BUFFER_SIZE 512
typedef struct {
uint8_t buffer[TX_BUFFER_SIZE];
uint16_t len;
} TxItem;
// --- 全局变量 ---
volatile TxItem txQueue[TX_QUEUE_SIZE];
volatile uint8_t txHead = 0;
volatile uint8_t txTail = 0;
volatile uint8_t isUART3Busy = 0;
uint8_t rxBuffer[50];
// --- 临界区保护宏(根据MCU实现,如STM32使用__disable_irq/__enable_irq)---
#define ENTER_CRITICAL() __disable_irq()
#define EXIT_CRITICAL() __enable_irq()
// --- 非阻塞发送函数(带队列和临界区保护)---
void u1_printf_nonblocking(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
// 格式化数据到当前队列头
uint16_t current_head;
int len;
ENTER_CRITICAL();
current_head = txHead;
EXIT_CRITICAL();// // --- 临界区保护开始:禁止中断
len = vsnprintf((char *)txQueue[current_head].buffer, TX_BUFFER_SIZE, fmt, ap);
va_end(ap);
if (len <= 0 || len >= TX_BUFFER_SIZE) return;
ENTER_CRITICAL();
uint8_t next_head = (current_head + 1) % TX_QUEUE_SIZE;
if (next_head != txTail) { // 队列未满
txQueue[current_head].len = len;
txHead = next_head;
// 若UART空闲,立即启动发送
if (!isUART3Busy) {
isUART3Busy = 1;
TxItem *item = &txQueue[txTail];
txTail = (txTail + 1) % TX_QUEUE_SIZE;
HAL_UART_Transmit_DMA(&huart3, item->buffer, item->len);
}
} else {
// 队列满,可在此添加错误处理
}
EXIT_CRITICAL();
// --- 临界区保护结束:恢复中断 ---
}
// --- DMA发送完成回调 ---
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart3) {
ENTER_CRITICAL();
isUART3Busy = 0;
// 检查队列中是否还有数据待发送
if (txTail != txHead) {
isUART3Busy = 1;
TxItem *item = &txQueue[txTail];
txTail = (txTail + 1) % TX_QUEUE_SIZE;
HAL_UART_Transmit_DMA(&huart3, item->buffer, item->len);
}
EXIT_CRITICAL();
}
}
// --- 接收完成回调---
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart == &huart3) {
u1_printf_nonblocking("Received: %.*s\r\n", Size, rxBuffer);
HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer));
__HAL_DMA_DISABLE_IT(&hdma_usart3_rx, DMA_IT_HT);
}
}
本文章为本人学习阶段所写 更多想法是为了作为学习笔记 有不对的地方请大佬们多多批评指正
CUBEMX最新版本6.14配置可能会有BUG 建议使用6.12及以下
作者:还在点灯@