STM32+RTOS+环形缓冲区+DMA半满中断+DMA全满中断+空闲中断实现高效的串口接收框架(笔记)

STM32+RTOS+环形缓冲区+DMA半满中断+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;
			//下面可以放数据分析的代码
		}
  }
}

原视频讲解

相关推荐
blueSatchel3 小时前
STM32F4系列使用ISP下载后,导致芯片被读写保护,无法烧录程序
stm32·嵌入式硬件·接口隔离原则
大侠课堂4 小时前
单片机经典面试题50道
arm开发·单片机·嵌入式硬件·mongodb
恒锐丰小吕4 小时前
无锡黑锋 HF1308 2A高效率升压DC-DC电压调整器技术解析
嵌入式硬件·硬件工程
im_AMBER4 小时前
AI井字棋项目开发笔记
前端·笔记·学习·算法
饕餮争锋5 小时前
Spring事件_发布&监听(2)_笔记
java·笔记·spring
_infinite_5 小时前
STM32常用外设配置
stm32·单片机·嵌入式硬件
一个平凡而乐于分享的小比特5 小时前
EEPROM、Flash、ROM、RAM的联系和区别
嵌入式硬件·常识总结
普中科技6 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 17 章 STM32 中断系统
stm32·单片机·嵌入式硬件·arm·中断系统·普中科技
苟日新日日新又日新Ryze6 小时前
11.24 笔记
java·开发语言·笔记
QT 小鲜肉7 小时前
【数据库】MySQL数据库的数据查询及操作命令汇总(超详细)
数据库·笔记·qt·mysql