基于STM32串口空闲中断+DMA转运+队列实现串口接收

一、简介

        串口通信在日常开发中用的是非常的多,通过串口实现通信的模块也很多比如wifi,蓝牙,以及各类语音模块,还有各类mcu,因此串口可以说是mcu中最常用的通信方式,那么,串口通信的写法也分为很多种,这么多的通信有什么区别呢?下面我简单介绍一下。

  第一种就是最简单的阻塞式串口收发

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

        这种写法调用后CPU持续等待传输完成,期间无法执行其他代码,机会完全占用了cpu,实时性很低。

        第二种就是中断式串口收发

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

        这种写法串口收发一个字节数据之后就会进入一次中断,实时性中等,比较适合数据量比较小。

        第三种就是DMA式串口收发

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

        这种写法串口收发都是通过DMA搬运,可以不经过cpu实现收发,实时性比较高。由此可见,DMA式实现串口收发的效率是最好。下面就是串口配置DMA通道的流程。

二、STM32CUBEMX配置串口

1.配置外部时钟

2.配置时钟树

3.配置调试方式

4.配置串口

5.配置DMA

6.打开串口中断

至此,你一定配置好了串口和DMA,接下来进入Keil5,在初始化的位置加上这两句代码

 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,dmaBuffer1,sizeof(dmaBuffer1));//开启空闲中断
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);

        第一句就是打开空闲中断,打开空闲中断可以大大降低进入中断的次数,从而提高CPU的效率。

        第二句代码是关闭DMA半满中断,否则DMA还没达到缓冲区长度就会把数据搬出去,这样子会导致数据接收错误。

三、队列实现串口接收

                说明:这样一来就可以使用串口+DMA+空闲中断实现数据收发了,那么为什么还需要使用队列呢?一方面,在日常开发过程当中如果从机发送的数据量比较快,此时CPU还需要处理其他任务就会导致数据可能还没处理完就又发来了一包新的数据;另一个方面,使用队列的解耦性是比较好的,因为我们只需要在中断中让数据安全入队就没有问题了,主循环中只需要考虑队列出队的问题,不需要去考虑中断中接收的数据情况;还有一点就是因为队列是先进先出,这样一来,我们想要数据直接去队列中取出来就好。

1.队列定义

一个队列结构体一般包括数据缓冲区、队列头索引、队列尾缓冲区,其中数据缓冲区用于存储接收到的数据,队列头索引用来找到队列第一个数据的位置,队列尾巴就是用来索引到队列中最后一个数据的位置。至于还有一个元素个数可以不用,可以是用队列头索引和队列尾索引计算队列是否是空或满但是需要用一个位置来区分空和满,所以我这边定义了一个元素个数。

#define QUEUE_SIZE 256 // 大于DMA缓冲区
typedef struct {
    uint8_t Data[QUEUE_SIZE];//接收的数据缓冲区
    volatile uint16_t Front;//队列头索引
    volatile uint16_t Rear;//队列尾索引
    volatile uint16_t Count; // 元素计数
} Queue;//队列定义

2.队列初始化

          队列初始化就是将队列头与队列尾还有队列中元素个数置为0

/**
 * @brief 初始化队列结构体
 * @param q 队列指针,指向待初始化的队列实例
 * @note 将队首(Front)、队尾(Rear)和元素计数(Count)归零
 *       实现循环队列的初始空状态
 */
void InitQueue(Queue* q) {
    q->Front = q->Rear = q->Count = 0;  // 队列指针和计数器归零
}

3.队列判满

        队列判满就是判断队列中的元素个数是否等于缓冲区大小,如果等于的话就返回1,否则返回0。

/**
 * @brief 检查队列是否已满
 * @param q 队列指针
 * @return 1表示队列已满,0表示未满
 * @note 通过比较当前元素数量(Count)与队列容量(QUEUE_SIZE)判断状态
 */
int IsFull(Queue* q) {
    return q->Count == QUEUE_SIZE;  // 计数等于容量即满
}

4.队列判空

        队列判满就是判断队列中的元素个数是否等于0,如果等于的话就返回1,否则返回0。

/**
 * @brief 检查队列是否为空
 * @param q 队列指针
 * @return 1表示队列为空,0表示非空
 * @note 通过元素计数器(Count)是否为0判断空状态
 */
int IsEmpty(Queue* q) {
    return q->Count == 0;  // 计数为0即空
}

5.数据入队

        数据入队前需要先关闭全局中断,保证中断不影响数据入队,关闭中断之后就可以判断队列中的数据有没有满,如果满了就不入队,如果没有满就开始入队,队列入队一般采用尾插法,顾名思义,就是把数据放在队列的尾部,每次只需要偏移队列的尾部索引即可。

/**
 * @brief 带临界区保护的入队操作
 * @param q 队列指针
 * @param data 待入队数据
 * @note 1. 使用PRIMASK寄存器实现可嵌套的临界区保护
 *        2. 队列满时通过串口发送错误提示
 */
void EnQueue(Queue* q, uint8_t data) {
    /* -- 临界区开始 -- */
    uint32_t primask = __get_PRIMASK();  // 保存当前中断状态
    __disable_irq();                     // 立即关闭全局中断
    
    if(IsFull(q)) {
        HAL_UART_Transmit(&huart1, (uint8_t*)"queue is full\r\n", sizeof("queue is full\r\n"), 100);
        __set_PRIMASK(primask);
        return;
    }
    
    /* 核心入队操作 */
    q->Data[q->Rear] = data;             // 数据写入队尾
    q->Rear = (q->Rear + 1) % QUEUE_SIZE;// 循环队列指针后移
    q->Count++;                          // 原子计数器递增
    
    /* -- 临界区结束 -- */
    __set_PRIMASK(primask);  // 精确恢复进入前的中断使能状态
}

6.数据出队

        数据出队的前面步骤跟数据入队很像都是要先关闭中断之后在进行出队操作,同样地,入队前需要先判断数据是否满了,出队就要判断数据是否空了,如果空了就不允许再次出队了,然后队列出队就是先进先出嘛,所以出队的时候就要把队头的数据取出来,最后在打开中断即可。

/**
 * @brief 带临界区保护的出队操作
 * @param q 队列指针
 * @param data 出队数据存储地址
 * @note  空队列时需恢复中断后退出
 */
void DeQueue(Queue* q, uint8_t* data) {
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    
    if(IsEmpty(q)) {
        HAL_UART_Transmit(&huart1, (uint8_t*)"queue is empty\r\n", sizeof("queue is empty\r\n"), 100); // 修正提示
        __set_PRIMASK(primask); 
        return;
    } 
    
    /* 核心出队操作 */
    *data = q->Data[q->Front];            // 读取队首数据
    q->Front = (q->Front + 1) % QUEUE_SIZE;// 队首指针后移
    q->Count--;                           // 原子计数器递减
    
    __set_PRIMASK(primask);
}

7.获取队列大小

          想要获取队列大小直接传入队列指针之后返回队列计数值即可。

/**
 * @brief 获取队列当前元素数量
 * @param q 队列指针
 * @return 队列元素个数
 * @note 1. 依赖Count变量实现O(1)复杂度查询
 */
int GetQLen(Queue* q) {
    return q->Count;
}

四、数据处理

        到这里,已经将队列的基础框架以及串口配配置好了,接下来就是如何将才串口跟队列结合起来达到管理数据的效果了。

1.串口中断接收数据

        这里我定义了两个缓冲区并且通过activeBuffer变量进行切换缓冲区,为什么要这么做呢?其实主要还是为了保护缓冲区,如果使用一个缓冲区的话,那么这一个缓冲区不仅要进行入队操作还需要用DMA接收,这样的话,有时候数据要是发送的又快又多就会出现接收问题。

/**
 * @brief 串口空闲中断回调(DMA双缓冲接收核心)
 * @param huart 串口句柄指针
 * @param Size 本次接收的字节数
 * @note 1. 使用双缓冲区防覆盖
 *       2. 每次中断自动切换缓冲区
 */
volatile uint8_t activeBuffer = 1; // 双缓冲切换标志 (1:buf1, 0:buf2)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if(huart == &huart1) {
        uint8_t* data = activeBuffer ? dmaBuffer1 : dmaBuffer2;
        // 数据入队(注意:此处无临界区保护!)
        for(int i = 0; i < Size; i++) {
            EnQueue(&que, data[i]);  // 逐字节入队
        }
        // 切换缓冲区
        activeBuffer = !activeBuffer;
        uint8_t* nextBuf = activeBuffer ? dmaBuffer1 : dmaBuffer2;
        // 重启DMA接收(连续接收模式)
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, nextBuf, DMA_RX_SIZE);
        // 禁用DMA半传输中断(仅用空闲中断)
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); 
        flag = 1; // 触发数据处理标志
    }
}

2.处理接收的数据

        处理数据一般都是用一个状态机实现对数据处理,这里我定义了三个状态,空闲状态、准备状态、发送状态,

        空闲状态主要是获取数据量多少,数据有多少包,数据包的索引。这里有一个坑就是每包的数据大小不能大于128字节,因为我的主控是STM32F103,DMA一次只能发128字节的数据,因此这里我把每包的大小定义为128字节。

        然后准备状态就是把分的包使用dma发送出去,发送完成之后进入发送状态,发送状态主要是在发送完成回调函数中完成的。

typedef enum {
    SendIdle,    // 空闲状态
    SendPrepare, // 准备状态
    SendTransmit // 发送中
} SendState;
/**
 * @brief 基于状态机的队列数据分包发送函数
 * @param q 队列指针,包含待发送数据
 * @note 使用状态机管理发送流程:
 *        SendIdle: 等待发送启动
 *        SendPrepare: 数据分包准备
 *        SendTransmit: DMA发送中
 */
void SendData_by_Queue(Queue* q) {
    switch(QueueSend) {
        case SendIdle:
            if(flag == 1) {  // 数据到达标志
                flag = 0;
                // 获取队列数据量(原子操作)
                TotalDataSize = GetQLen(q);
                if(TotalDataSize == 0) return;
                // 计算分包数(向上取整算法)
                TotalPackets = (TotalDataSize + DMA_MAX_SEND - 1) / DMA_MAX_SEND;
                CurrentPacket = 0;      // 重置包计数器
                QueueSend = SendPrepare; // 进入准备状态
            }
            break;
        case SendPrepare: {
            // 计算当前包大小(末包处理)
            uint16_t PacketSize = (CurrentPacket == TotalPackets - 1) 
                                 ? (TotalDataSize % DMA_MAX_SEND) // 末包取余数
                                 : DMA_MAX_SEND;                  // 非末包取最大值
            if(PacketSize == 0) PacketSize = DMA_MAX_SEND; // 整除保护
            
            // 从队列出队到发送缓冲区
            uint16_t actualSize = 0;
            for(int i = 0; i < PacketSize; i++) {
                if(IsEmpty(q)) break;        // 队列空保护
                DeQueue(q, &SendBuffer[i]);  // 带临界区保护的出队
                actualSize++;
            }
            // 启动DMA发送
            if(HAL_UART_Transmit_DMA(&huart1, SendBuffer, actualSize) == HAL_OK) {
                CurrentPacket++;            // 包序号递增
                QueueSend = SendTransmit;   // 进入传输状态
            } else {
                QueueSend = SendIdle;       // 发送失败回空闲
            }
            break;
        }
        case SendTransmit:  // DMA发送中,由回调函数处理状态切换
            break;
    }
}

3.发送完成回调函数

        这个回调函数里面就是判断数据是否发完了,发完了就回到空闲状态。

/**
 * @brief DMA发送完成中断回调
 * @param huart 串口句柄指针
 * @note 在DMA发送结束时自动触发
 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart == &huart1 && QueueSend == SendTransmit) {
        if(CurrentPacket < TotalPackets) {
            QueueSend = SendPrepare; // 继续下一包
        } else {
            QueueSend = SendIdle;    // 全部完成
        }
    }
}

五、完整代码

Queue.c

#include "Queue.h"
/***********************************************************
功能:串口通信驱动接收不定长数据,队列+DMA接收与空闲中断
作者:k.
创建日期:2025-07-29  
版本:v0.1  
***********************************************************/
Queue que;
volatile SendState QueueSend = SendIdle;//发送数据状态机
volatile uint16_t TotalPackets = 0;//要发送数据包的数量
volatile uint16_t CurrentPacket = 0;//当前发送到那个包
volatile uint16_t TotalDataSize = 0;//总包的大小
uint8_t dmaBuffer1[DMA_RX_SIZE];
uint8_t dmaBuffer2[DMA_RX_SIZE];
uint8_t SendBuffer[DMA_MAX_SEND];
int flag=0;
/**
 * @brief 初始化队列结构体
 * @param q 队列指针,指向待初始化的队列实例
 * @note 将队首(Front)、队尾(Rear)和元素计数(Count)归零
 *       实现循环队列的初始空状态
 */
void InitQueue(Queue* q) {
    q->Front = q->Rear = q->Count = 0;  // 队列指针和计数器归零
}

/**
 * @brief 检查队列是否已满
 * @param q 队列指针
 * @return 1表示队列已满,0表示未满
 * @note 通过比较当前元素数量(Count)与队列容量(QUEUE_SIZE)判断状态
 */
int IsFull(Queue* q) {
    return q->Count == QUEUE_SIZE;  // 计数等于容量即满
}

/**
 * @brief 检查队列是否为空
 * @param q 队列指针
 * @return 1表示队列为空,0表示非空
 * @note 通过元素计数器(Count)是否为0判断空状态
 */
int IsEmpty(Queue* q) {
    return q->Count == 0;  // 计数为0即空
}

/**
 * @brief 带临界区保护的入队操作
 * @param q 队列指针
 * @param data 待入队数据
 * @note 1. 使用PRIMASK寄存器实现可嵌套的临界区保护
 *        2. 队列满时通过串口发送错误提示(注意:串口操作可能引起阻塞)
 */
void EnQueue(Queue* q, uint8_t data) {
    /* -- 临界区开始 -- */
    uint32_t primask = __get_PRIMASK();  // 保存当前中断状态
    __disable_irq();                     // 立即关闭全局中断
    
    if(IsFull(q)) {
        HAL_UART_Transmit(&huart1, (uint8_t*)"queue is full\r\n", sizeof("queue is full\r\n"), 100);
        __set_PRIMASK(primask);
        return;
    }
    
    /* 核心入队操作 */
    q->Data[q->Rear] = data;             // 数据写入队尾
    q->Rear = (q->Rear + 1) % QUEUE_SIZE;// 循环队列指针后移
    q->Count++;                          // 原子计数器递增
    
    /* -- 临界区结束 -- */
    __set_PRIMASK(primask);  // 精确恢复进入前的中断使能状态
}

/**
 * @brief 带临界区保护的出队操作
 * @param q 队列指针
 * @param data 出队数据存储地址
 * @note  空队列时需恢复中断后退出
 */
void DeQueue(Queue* q, uint8_t* data) {
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    
    if(IsEmpty(q)) {
        HAL_UART_Transmit(&huart1, (uint8_t*)"queue is empty\r\n", sizeof("queue is empty\r\n"), 100); // 修正提示
        __set_PRIMASK(primask); 
        return;
    } 
    
    /* 核心出队操作 */
    *data = q->Data[q->Front];            // 读取队首数据
    q->Front = (q->Front + 1) % QUEUE_SIZE;// 队首指针后移
    q->Count--;                           // 原子计数器递减
    
    __set_PRIMASK(primask);
}

/**
 * @brief 获取队列当前元素数量
 * @param q 队列指针
 * @return 队列元素个数
 * @note 1. 依赖Count变量实现O(1)复杂度查询
 */
int GetQLen(Queue* q) {
    return q->Count;
}


/**
 * @brief 基于状态机的队列数据分包发送函数
 * @param q 队列指针,包含待发送数据
 * @note 使用状态机管理发送流程:
 *        SendIdle: 等待发送启动
 *        SendPrepare: 数据分包准备
 *        SendTransmit: DMA发送中
 */
void SendData_by_Queue(Queue* q) {
    switch(QueueSend) {
        case SendIdle:
            if(flag == 1) {  // 数据到达标志
                flag = 0;
                // 获取队列数据量(原子操作)
                TotalDataSize = GetQLen(q);
                if(TotalDataSize == 0) return;
                // 计算分包数(向上取整算法)
                TotalPackets = (TotalDataSize + DMA_MAX_SEND - 1) / DMA_MAX_SEND;
                CurrentPacket = 0;      // 重置包计数器
                QueueSend = SendPrepare; // 进入准备状态
            }
            break;
        case SendPrepare: {
            // 计算当前包大小(末包处理)
            uint16_t PacketSize = (CurrentPacket == TotalPackets - 1) 
                                 ? (TotalDataSize % DMA_MAX_SEND) // 末包取余数
                                 : DMA_MAX_SEND;                  // 非末包取最大值
            if(PacketSize == 0) PacketSize = DMA_MAX_SEND; // 整除保护
            
            // 从队列出队到发送缓冲区
            uint16_t actualSize = 0;
            for(int i = 0; i < PacketSize; i++) {
                if(IsEmpty(q)) break;        // 队列空保护
                DeQueue(q, &SendBuffer[i]);  // 带临界区保护的出队
                actualSize++;
            }
            // 启动DMA发送
            if(HAL_UART_Transmit_DMA(&huart1, SendBuffer, actualSize) == HAL_OK) {
                CurrentPacket++;            // 包序号递增
                QueueSend = SendTransmit;   // 进入传输状态
            } else {
                QueueSend = SendIdle;       // 发送失败回空闲
            }
            break;
        }
        case SendTransmit:  // DMA发送中,由回调函数处理状态切换
            break;
    }
}

/**
 * @brief DMA发送完成中断回调
 * @param huart 串口句柄指针
 * @note 在DMA发送结束时自动触发
 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart == &huart1 && QueueSend == SendTransmit) {
        if(CurrentPacket < TotalPackets) {
            QueueSend = SendPrepare; // 继续下一包
        } else {
            QueueSend = SendIdle;    // 全部完成
        }
    }
}


/**
 * @brief 串口空闲中断回调(DMA双缓冲接收核心)
 * @param huart 串口句柄指针
 * @param Size 本次接收的字节数
 * @note 1. 使用双缓冲区防覆盖
 *       2. 每次中断自动切换缓冲区
 */
volatile uint8_t activeBuffer = 1; // 双缓冲切换标志 (1:buf1, 0:buf2)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if(huart == &huart1) {
        uint8_t* data = activeBuffer ? dmaBuffer1 : dmaBuffer2;
        // 数据入队(注意:此处无临界区保护!)
        for(int i = 0; i < Size; i++) {
            EnQueue(&que, data[i]);  // 逐字节入队
        }
        // 切换缓冲区
        activeBuffer = !activeBuffer;
        uint8_t* nextBuf = activeBuffer ? dmaBuffer1 : dmaBuffer2;
        // 重启DMA接收(连续接收模式)
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, nextBuf, DMA_RX_SIZE);
        // 禁用DMA半传输中断(仅用空闲中断)
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); 
        flag = 1; // 触发数据处理标志
    }
}

Queue.h

#ifndef _Queue_H
#define _Queue_H

#include "stm32f1xx_hal.h"
#include "usart.h"
#define DMA_RX_SIZE 256 // dma接收的数据缓冲区
#define DMA_MAX_SEND 128 // DMA单次最大发送长度
#define QUEUE_SIZE 256 // 队列存放大小
typedef struct {
    uint8_t Data[QUEUE_SIZE];//接收的数据缓冲区
    volatile uint16_t Front;//队列头索引
    volatile uint16_t Rear;//队列尾索引
    volatile uint16_t Count; // 元素计数
} Queue;//队列定义
typedef enum {
    SendIdle,    // 空闲状态
    SendPrepare, // 准备状态
    SendTransmit // 发送中
} SendState;
void InitQueue(Queue* q);//初始化队列
int IsFull(Queue* q);//判满
int IsEmpty(Queue* q);//判空
void EnQueue(Queue* q,uint8_t data);//入队
void DeQueue(Queue* q,uint8_t* data);//出队
int GetQLen(Queue* q);//获取入队数据数量
void SendData_by_Queue(Queue* q);//发送数据
//双缓冲区接收数据避免数据覆盖(当正在入队数据的时候再次触发dma,就会覆盖正在入队的数据导致入队出问题)
extern uint8_t dmaBuffer1[DMA_RX_SIZE];
extern uint8_t dmaBuffer2[DMA_RX_SIZE];
extern Queue que;
extern uint8_t SendBuffer[DMA_MAX_SEND];
extern int flag;//入队完成标志位
#endif

main.c

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();
  InitQueue(&que);
  /* USER CODE BEGIN 2 */
  HAL_UARTEx_ReceiveToIdle_DMA(&huart1,dmaBuffer1,sizeof(dmaBuffer1));//开启空闲中断
  __HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
        
    /* USER CODE BEGIN 3 */
                SendData_by_Queue(&que);
  }
  /* USER CODE END 3 */
}

六、运行效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值