STM32+RTOS+环形缓冲区+DMA半满中断+DMA全满中断+空闲中断实现高效的串口接收框架
- 一、引言
- 二、STM32CubeMX工程设置
-
- 1.下载方式和RTOS的定时器
- 2.外部晶振
- [3.UART1 DMA接收为循环模式](#3.UART1 DMA接收为循环模式)
- 4.开启RTOS
- 5.加入FIFO.c文件
- 6.使用微库
- FIFO.c"文件
- HAL_UARTEx_ReceiveToIdle_DMA函数的分析
- 中断处理代码
一、引言
在嵌入式系统开发中,串口通信是常见的数据交互方式。然而,传统的串口接收方式(如轮询或普通中断)在处理高速数据流时存在效率低、易丢失数据等问题。本文将详细介绍一种基于STM32、FreeRTOS、环形缓冲区、DMA以及半满/全满/空闲中断的高效串口接收框架,该方案能够有效处理高速串口数据,避免数据丢失,同时降低CPU占用率。
二、STM32CubeMX工程设置
1.下载方式和RTOS的定时器

2.外部晶振

3.UART1 DMA接收为循环模式

4.开启RTOS

5.加入FIFO.c文件

6.使用微库

增加
c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
FIFO.c"文件
c
#include "FIFO.h"
#include "FreeRTOS.h"
/**
* @brief 创建FIFO缓冲区控制结构
* @param p_fifo_buffer 指向FIFO缓冲区控制结构的指针的指针
* @return 1成功,0失败
*
* 为FIFO缓冲区分配内存并初始化控制结构
*/
uint8_t Create_FIFO_Buffer(FIFO_BUFFER_CTRL **p_fifo_buffer)
{
*p_fifo_buffer = (FIFO_BUFFER_CTRL *)pvPortMalloc(sizeof(FIFO_BUFFER_CTRL));
if(*p_fifo_buffer == NULL){
return 0;
}
memset(*p_fifo_buffer, 0, sizeof(FIFO_BUFFER_CTRL));
return 1;
}
/**
* @brief 检查FIFO缓冲区是否为空
* @param p_buffer FIFO缓冲区控制结构指针
* @return 1空,0非空
*
* 通过比较头指针(head)和尾指针(tail)判断缓冲区是否为空
*/
uint8_t FIFO_Buffer_is_Empty(FIFO_BUFFER_CTRL * p_buffer)
{
if(p_buffer == NULL){
return 0;
}
if(p_buffer->head == p_buffer->tail){
return 1;
}
return 0;
}
/**
* @brief 检查FIFO缓冲区是否已满
* @param p_buffer FIFO缓冲区控制结构指针
* @return 1满,0非满
*
* 通过计算头尾指针差值与缓冲区最大长度比较判断是否满
*/
uint8_t FIFO_Buffer_is_Full(FIFO_BUFFER_CTRL * p_buffer)
{
if(p_buffer == NULL){
return 0;
}
if((p_buffer->head - p_buffer->tail) == FIFO_MAX_BUFFER){
return 1;
}
return 0;
}
/**
* @brief 向FIFO缓冲区插入单个字节
* @param p_buffer FIFO缓冲区控制结构指针
* @param byte 要插入的字节
* @return 1成功,0失败(缓冲区已满)
*
* 将字节插入缓冲区的头部位置(环形缓冲区实现)
*/
uint8_t Insert_Byte_to_FIFO_Buffer(FIFO_BUFFER_CTRL * p_buffer, uint8_t byte)
{
if(p_buffer == NULL){
return 0;
}
if(FIFO_Buffer_is_Full(p_buffer)){
return 0;
}
p_buffer->circular_buffer[p_buffer->head % FIFO_MAX_BUFFER] = byte;
p_buffer->head++;
return 1;
}
/**
* @brief 从FIFO缓冲区获取单个字节
* @param p_buffer FIFO缓冲区控制结构指针
* @param byte 用于存储获取的字节
* @return 1成功,0失败(缓冲区为空)
*
* 从缓冲区尾部获取字节,并移动尾指针
*/
uint8_t Get_Byte_from_FIFO_Buffer(FIFO_BUFFER_CTRL * p_buffer, uint8_t *byte)
{
if(p_buffer == NULL){
return 0;
}
if(FIFO_Buffer_is_Empty(p_buffer)){
return 0;
}
*byte = p_buffer->circular_buffer[p_buffer->tail % FIFO_MAX_BUFFER];
p_buffer->tail++;
return 1;
}
/**
* @brief 向FIFO缓冲区插入字节数组
* @param p_buffer FIFO缓冲区控制结构指针
* @param buf 要插入的字节数组
* @param len 要插入的字节数量
* @return 1成功,0失败(空间不足)
*
* 批量插入字节,检查剩余空间是否足够
*/
uint8_t Insert_Buff_to_FIFO_Buffer(FIFO_BUFFER_CTRL * p_buffer, uint8_t *buf, int32_t len)
{
if(p_buffer == NULL){
return 0;
}
int32_t unused;
unused = FIFO_MAX_BUFFER - (p_buffer->head - p_buffer->tail);
if(unused < len){
return 0;
}
for(uint32_t i=0; i<len; i++){
Insert_Byte_to_FIFO_Buffer(p_buffer, buf[i]);
}
return 1;
}
/**
* @brief 从FIFO缓冲区获取字节数组
* @param p_buffer FIFO缓冲区控制结构指针
* @param buf 用于存储获取的字节数组
* @param len 要获取的字节数量
* @return 1成功,0失败(数据不足)
*
* 批量获取字节,检查可用数据是否足够
*/
uint8_t Get_Buff_from_FIFO_Buffer(FIFO_BUFFER_CTRL * p_buffer, uint8_t *buf, int32_t len)
{
if(p_buffer == NULL){
return 0;
}
int32_t used;
used = p_buffer->head - p_buffer->tail;
if(used < len){
return 0;
}
for(uint32_t i=0; i<len; i++){
Get_Byte_from_FIFO_Buffer(p_buffer, &buf[i]);
}
return 1;
}
/**
* @brief 获取FIFO缓冲区剩余可用空间
* @param p_buffer FIFO缓冲区控制结构指针
* @return 剩余空间大小
*
* 计算可用空间 = 总大小 - 已用空间
*/
uint32_t Get_FIFO_Buffer_Avail(FIFO_BUFFER_CTRL * p_buffer)
{
return FIFO_MAX_BUFFER - (p_buffer->head - p_buffer->tail);
}
/**
* @brief 获取FIFO头部位置
* @param p_buffer FIFO缓冲区控制结构指针
* @param pos 用于存储头部位置的指针
* @return 1成功,0失败(输入为空指针)
*
* 返回当前头部指针值,用于外部跟踪缓冲区状态
*/
uint8_t Get_Head_Position(FIFO_BUFFER_CTRL * p_buffer, uint32_t *pos)
{
if(p_buffer == NULL){
return 0;
}
*pos = p_buffer->head;
return 1;
}
/**
* @brief 移动FIFO头部位置
* @param p_buffer FIFO缓冲区控制结构指针
* @param len 移动的步长
* @return 1成功,0失败(输入为空指针)
*
* 用于外部处理后更新头部指针,通常在数据被处理后调用
*/
uint8_t Move_Head_Position(FIFO_BUFFER_CTRL * p_buffer, uint32_t len)
{
if(p_buffer == NULL){
return 0;
}
p_buffer->head = p_buffer->head + len;
return 1;
}
HAL_UARTEx_ReceiveToIdle_DMA函数的分析
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
这个函数有一个缺点,我们来看一下它函数执行的一个内容。

空闲函数,然后并且调用这个函数

设置满回调函数和半满回调函数

可以看到半满函数里面的内容。它会去判断我们这个标记位有没有被设置成空闲事件,如果设置调用"Rx 事件回调",否则调用"取 半完成回调"。

再看全满函数。同样的是判断这个值是不是标记位有没有被设置成空闲事件,如果是同样执行"HAL_UARTEx_RxEventCallback(huart, huart->RxXferSize / 2U);"

回调函数可以看到,在半满中段和满中段里面,我们调用了同一个回调函数。如果设置使用这一个API去开启目空闲中断和DMA的话,按满中断和满中断的调用同一个回调函数。这不利于我们区分当前发生的是半满中段还是满分的。所以我们就不用了。用最普通的直接DMA接收里接收就行了。
在这个函数中可以看到标记位设置为标准的模式

跳转

我们要对它们进行重新定义这个弱函数的内容

中断处理代码
c
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (unsigned char) ch;
return ch;
}
void HAL_UART_RxIDleCallback(UART_HandleTypeDef *huart)// 仅处理 UART 的空闲线检测中断(IDLE interrupt)
{
if(huart->Instance == USART1){
static uint32_t idle_flag = 0x01;// 标识符:表示本次唤醒由空闲中断触发
if(__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))// 再次确认是否为真正的 IDLE 中断标志
{
__HAL_UART_CLEAR_IDLEFLAG(huart); // 必须手动清除 IDLE 中断标志,否则会持续触发中断
// 1. 获取 FIFO 缓冲区当前逻辑 head 位置(即已写入但未"确认"的数据边界)
printf("空闲\n");
uint32_t cur_head_pos = 0;
Get_Head_Position(&uart_fifo_buffer, &cur_head_pos);//获取头指针位置
cur_head_pos = cur_head_pos%FIFO_MAX_BUFFER;
//2. 计算head应该在的位置
// __HAL_DMA_GET_COUNTER 返回剩余未传输字节数,
// 因此 FIFO_MAX_BUFFER - 剩余数 = 已接收字节数 = DMA 当前写入位置(need_head_pos)
uint32_t need_head_pos = FIFO_MAX_BUFFER - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
//3. 计算head应该移动的长度
// 若 need_head_pos >= cur_head_pos:直接相减;
// 否则:需绕过缓冲区末尾,加上 FIFO_MAX_BUFFER 后再减
uint32_t move_head_len = (need_head_pos >= cur_head_pos)?\
(need_head_pos - cur_head_pos):\
(need_head_pos + FIFO_MAX_BUFFER - cur_head_pos);
//4. 移动head位置
Move_Head_Position(&uart_fifo_buffer, move_head_len);
//5. 唤醒数据分析任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(Uart_Analysis_Queue_Handle, &idle_flag, &xHigherPriorityTaskWoken);
// 如果有更高优先级任务被唤醒,则请求立即进行上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
}
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) // 仅处理 UART 的 DMA 半传输完成中断(Half Transfer Complete)
{
if(huart->Instance == USART1){
static uint32_t half_flag = 0x02;
//1. 获取当前head位置
uint32_t cur_head_pos = 0;
printf("半满\n");
Get_Head_Position(&uart_fifo_buffer, &cur_head_pos);
cur_head_pos = cur_head_pos%FIFO_MAX_BUFFER;
//2. 计算head应该在的位置
uint32_t need_head_pos = FIFO_MAX_BUFFER/2;
//3. 计算head应该移动的长度
uint32_t move_head_len = (need_head_pos >= cur_head_pos)?\
(need_head_pos - cur_head_pos):\
(need_head_pos + FIFO_MAX_BUFFER - cur_head_pos);
//4. 移动head位置
Move_Head_Position(&uart_fifo_buffer, move_head_len);
//5. 唤醒数据分析任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(Uart_Analysis_Queue_Handle, &half_flag, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)// 仅处理 UART 的 DMA 传输完成中断(Transfer Complete,即全满)
{
if(huart->Instance == USART1){
static uint32_t cplt_flag = 0x03;
//1. 获取当前head位置
uint32_t cur_head_pos = 0;
printf("全满\n");
Get_Head_Position(&uart_fifo_buffer, &cur_head_pos);
cur_head_pos = cur_head_pos%FIFO_MAX_BUFFER;
//2. 计算head应该在的位置
uint32_t need_head_pos = FIFO_MAX_BUFFER;
//3. 计算head应该移动的长度
uint32_t move_head_len = (need_head_pos >= cur_head_pos)?\
(need_head_pos - cur_head_pos):\
(need_head_pos + FIFO_MAX_BUFFER - cur_head_pos);
//4. 移动head位置
Move_Head_Position(&uart_fifo_buffer, move_head_len);
//5. 唤醒数据分析任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(Uart_Analysis_Queue_Handle, &cplt_flag, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
c
uint8_t temp_data[100] = {0};
void uart_analysis_task(void *argument)
{
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, uart_fifo_buffer.circular_buffer, FIFO_MAX_BUFFER);
uint32_t isr_flag = 0;
uint8_t i = 0;
for(;;)
{
xQueueReceive(Uart_Analysis_Queue_Handle, &isr_flag, portMAX_DELAY);
while(FIFO_Buffer_is_Empty(&uart_fifo_buffer) != 1){
Get_Byte_from_FIFO_Buffer(&uart_fifo_buffer, &temp_data[i]);
i = (i+1)%100;
//下面可以放数据分析的代码
}
}
}