STM32+DMA+IDLE+环形缓冲区
STM32+DMA+IDLE+环形缓冲区
介绍
该代码实现了基于STM32的USART2串口高效数据接收机制,核心过程是:初始化时配置USART2、DMA循环接收模式(1024字节缓冲区)和空闲中断;当串口检测到空闲中断(IDLE)时,暂停DMA并计算当前接收位置,通过对比上次记录的DMA游标位置确定完整数据帧的起始偏移和长度(自动处理缓冲区环绕),将帧元数据存入深度为32的环形缓冲区;应用程序通过USART2_GetFrame函数从环形缓冲区提取帧数据(同样处理缓冲区环绕),实现非阻塞式帧数据获取,同时支持阻塞发送和printf重定向,适用于Xmodem等需要可靠接收不定长数据帧的通信场景。
USART.c文件
#include "usart.h"
#include <string.h>
#include <stdio.h>
// ========================================
// 全局变量定义
// ========================================
uint8_t usart2_dma_buffer[USART2_DMA_BUFFER_SIZE]; // DMA接收缓冲区
Usart2CircularBuffer usart2_rx_buffer; // 环形缓冲区控制块
volatile uint16_t usart2_dma_old_pos = 0; // DMA游标,记录上一次数据位置
// ========================================
// 静态辅助函数
// ========================================
/**
* @brief 初始化环形缓冲区控制块
* @param cb 指向环形缓冲区控制块的指针
* @details 将帧计数、读写索引清零,准备接收新数据
*/
static void usart2_cb_init(Usart2CircularBuffer *cb)
{
cb->frame_count = 0;
cb->write_index = 0;
cb->read_index = 0;
}
/**
* @brief 向环形缓冲区添加一帧数据
* @param cb 环形缓冲区控制块指针
* @param start_offset 数据在DMA缓冲区的起始偏移
* @param length 数据长度
* @details 将帧的元数据(偏移和长度)存储到环形缓冲区,自动处理溢出情况
*/
static void usart2_cb_put_frame(Usart2CircularBuffer *cb, uint16_t start_offset, uint16_t length)
{
// 存储新帧的元数据
cb->frame_info[cb->write_index].start_offset = start_offset;
cb->frame_info[cb->write_index].length = length;
cb->write_index = (cb->write_index + 1) % USART2_RING_BUFFER_COUNT;
// 若缓冲区未满,增加帧计数;若满,覆盖旧帧,不增加计数
if (cb->frame_count < USART2_RING_BUFFER_COUNT) {
cb->frame_count++;
}
}
// ========================================
// API 函数实现
// ========================================
/**
* @brief 从环形缓冲区获取一帧完整数据
* @param buf 用户提供的缓冲区,用于复制帧数据
* @param len 输出参数,返回复制的数据长度
* @return 1 表示成功获取一帧,0 表示无数据
* @details 自动处理DMA缓冲区环绕情况,将数据复制到用户缓冲区
*/
int USART2_GetFrame(uint8_t *buf, uint16_t *len)
{
if (usart2_rx_buffer.frame_count == 0) {
return 0; // 无可用数据帧
}
// 获取当前帧的元数据
Usart2RxBuffInfo *frame = &usart2_rx_buffer.frame_info[usart2_rx_buffer.read_index];
uint16_t start = frame->start_offset;
uint16_t length = frame->length;
// 处理DMA缓冲区环绕
if (start + length <= USART2_DMA_BUFFER_SIZE) {
// 数据未跨缓冲区边界,直接复制
memcpy(buf, &usart2_dma_buffer[start], length);
} else {
// 数据跨边界,分两次复制
uint16_t part1 = USART2_DMA_BUFFER_SIZE - start;
memcpy(buf, &usart2_dma_buffer[start], part1);
memcpy(buf + part1, usart2_dma_buffer, length - part1);
}
*len = length;
// 更新读索引并减少帧计数
usart2_rx_buffer.read_index = (usart2_rx_buffer.read_index + 1) % USART2_RING_BUFFER_COUNT;
usart2_rx_buffer.frame_count--;
return 1;
}
/**
* @brief 初始化USART2、DMA和空闲中断
* @param baudrate 波特率,例如115200
* @details 配置GPIO、USART、DMA和NVIC,支持DMA循环接收和空闲中断
*/
void USART2_Init(uint32_t baudrate)
{
GPIO_InitTypeDef gpio_init_struct;
USART_InitTypeDef usart_init_struct;
DMA_InitTypeDef dma_init_struct;
NVIC_InitTypeDef nvic_init_struct;
// 1. 使能外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 使能USART2时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA1时钟
// 2. 配置GPIO (PA2=TX, PA3=RX)
gpio_init_struct.GPIO_Pin = GPIO_Pin_2;
gpio_init_struct.GPIO_Speed = GPIO_Speed_50MHz;
gpio_init_struct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出(TX)
GPIO_Init(GPIOA, &gpio_init_struct);
gpio_init_struct.GPIO_Pin = GPIO_Pin_3;
gpio_init_struct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入(RX)
GPIO_Init(GPIOA, &gpio_init_struct);
// 3. 配置USART2
usart_init_struct.USART_BaudRate = baudrate;
usart_init_struct.USART_WordLength = USART_WordLength_8b;
usart_init_struct.USART_StopBits = USART_StopBits_1;
usart_init_struct.USART_Parity = USART_Parity_No;
usart_init_struct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart_init_struct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &usart_init_struct);
// 4. 配置DMA (Channel6 for USART2_RX)
DMA_DeInit(DMA1_Channel6);
dma_init_struct.DMA_PeripheralBaseAddr = (uint32_t)&(USART2->DR); // 外设地址:USART数据寄存器
dma_init_struct.DMA_MemoryBaseAddr = (uint32_t)usart2_dma_buffer; // 内存地址:DMA缓冲区
dma_init_struct.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设为数据源(接收)
dma_init_struct.DMA_BufferSize = USART2_DMA_BUFFER_SIZE; // 缓冲区大小
dma_init_struct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
dma_init_struct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
dma_init_struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
dma_init_struct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
dma_init_struct.DMA_Mode = DMA_Mode_Circular; // 循环模式
dma_init_struct.DMA_Priority = DMA_Priority_High;
dma_init_struct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel6, &dma_init_struct);
DMA_Cmd(DMA1_Channel6, ENABLE); // 启动DMA
// 5. 启用空闲中断
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); // 启用空闲线检测中断
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); // 启用DMA接收请求
// 6. 配置NVIC中断优先级
nvic_init_struct.NVIC_IRQChannel = USART2_IRQn;
nvic_init_struct.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init_struct.NVIC_IRQChannelSubPriority = 1;
nvic_init_struct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init_struct);
// 7. 初始化环形缓冲区
usart2_cb_init(&usart2_rx_buffer);
usart2_dma_old_pos = 0; // 重置DMA游标
// 8. 启用USART2
USART_Cmd(USART2, ENABLE);
}
/**
* @brief USART2中断服务函数
* @details 处理空闲中断(IDLE),表示一帧数据接收完成,计算帧长度并存入环形缓冲区
*/
void USART2_IRQHandler(void)
{
if (USART_GetITStatus(USART2, USART_IT_IDLE) != RESET) {
// 清除空闲中断标志:读取SR和DR寄存器
volatile uint32_t tmp;
tmp = USART2->SR;
tmp = USART2->DR;
// 停止DMA并计算已接收数据量
DMA_Cmd(DMA1_Channel6, DISABLE);
uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Channel6);
uint16_t new_pos = USART2_DMA_BUFFER_SIZE - remaining;
// 计算帧长度,处理环绕情况
if (new_pos != usart2_dma_old_pos) {
uint16_t length;
if (new_pos > usart2_dma_old_pos) {
length = new_pos - usart2_dma_old_pos;
} else {
length = USART2_DMA_BUFFER_SIZE - usart2_dma_old_pos + new_pos;
}
// 限制最大帧长度,防止溢出
if (length > USART2_MAX_FRAME_SIZE) {
length = USART2_MAX_FRAME_SIZE;
}
// 存入环形缓冲区
usart2_cb_put_frame(&usart2_rx_buffer, usart2_dma_old_pos, length);
}
// 更新DMA游标
usart2_dma_old_pos = new_pos;
// 重新启用DMA
DMA_Cmd(DMA1_Channel6, ENABLE);
}
}
/**
* @brief 发送单个字节(阻塞方式)
* @param data 要发送的字节
* @details 等待发送缓冲区空闲后发送数据
*/
void USART2_SendByte(uint8_t data)
{
USART_SendData(USART2, data);
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); // 等待发送完成
}
/**
* @brief 发送字符串(阻塞方式)
* @param str 以'\0'结尾的字符串
* @details 逐字节发送字符串,直到遇到结束符
*/
void USART2_SendString(const char *str)
{
while (*str) {
USART2_SendByte(*str++);
}
}
/**
* @brief 重定向printf到USART2
* @param ch 要发送的字符
* @param f 文件流指针
* @return 返回发送的字符
* @details 将标准输出重定向到USART2,用于调试输出
*/
int fputc(int ch, FILE *f)
{
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
USART_SendData(USART2, (uint8_t)ch);
return ch;
}
USART.h文件
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#include <stdint.h>
// ========================================
// 配置参数
// ========================================
#define USART2_DMA_BUFFER_SIZE 1024 // DMA接收缓冲区大小(字节)
#define USART2_MAX_FRAME_SIZE 256 // 单帧最大长度(例如Xmodem协议每帧132字节)
#define USART2_RING_BUFFER_COUNT 32 // 环形缓冲区深度(支持高达15KB的固件数据)
// ========================================
// 数据结构定义
// ========================================
// 存储帧在DMA缓冲区中的位置和长度信息
typedef struct {
uint16_t start_offset; // 帧在DMA缓冲区中的起始偏移
uint16_t length; // 帧数据的实际长度
} Usart2RxBuffInfo;
// 环形缓冲区控制块,用于管理接收到的帧
typedef struct {
uint16_t frame_count; // 当前待处理的帧数量
Usart2RxBuffInfo frame_info[USART2_RING_BUFFER_COUNT]; // 帧信息队列
uint16_t write_index; // 写入索引
uint16_t read_index; // 读取索引
} Usart2CircularBuffer;
// ========================================
// 全局变量声明
// ========================================
extern Usart2CircularBuffer usart2_rx_buffer; // 环形缓冲区控制块
extern uint8_t usart2_dma_buffer[USART2_DMA_BUFFER_SIZE]; // DMA接收缓冲区
extern volatile uint16_t usart2_dma_old_pos; // DMA游标(用于中断处理)
// ========================================
// 函数声明
// ========================================
void USART2_Init(uint32_t baudrate); // 初始化USART2,包含DMA和空闲中断
void USART2_SendByte(uint8_t data); // 发送单个字节(阻塞方式)
void USART2_SendString(const char *str); // 发送字符串(阻塞方式)
int USART2_GetFrame(uint8_t *buf, uint16_t *len); // 从环形缓冲区获取一帧数据
#endif
main.c测试
USART2_Init(921600);
Delay_ms(100); // 等待串口稳定
USART2_SendString("USART2 Init OK\r\n");
uint8_t rxbuf[USART2_MAX_FRAME_SIZE]; //单帧缓冲区
uint16_t rxlen; //单帧数据大小
while (1)
{
uint16_t i = 0;
if (USART2_GetFrame(rxbuf, &rxlen)) {
for(i=0;i<rxlen;i++)
{
USART2_SendByte(rxbuf[i]); // 回显
}
USART2_SendString("\r\n");
}
}
测试结果:

回显256个字符,超出的舍弃了。
过程分析

以Xmodem协议为例分析接收处理过程
帧包格式
| Byte 1 | Byte 2 | Byte 3 | Byte 4- Byte 131 | Byte 132- Byte 133 |
|---|---|---|---|---|
| 头标志 | 包序列 | ~包序列 | 包数据 | CRC16(2 Byte) |
XModem是半双工协议,接收方需要在每个包后发送ACK/NAK
- 初始化阶段
主机开始传输时,STM32的USART2通过DMA循环接收模式持续填充1024字节的DMA缓冲区,同时启用空闲中断(IDLE)检测帧边界。 - 帧传输与中断触发
XModem协议每帧132字节(含128字节数据+4字节头部/校验),15KB固件共需120帧(15×1024÷128)。每当一帧传输结束产生线路空闲时,触发IDLE中断。 - 中断处理流程
- 暂停DMA,通过
DMA_GetCurrDataCounter计算当前接收位置(如第n帧结束位置)。 - 与上一帧位置比较确定帧长度(固定132字节)。
- 将帧起始偏移和长度存入深度32的环形缓冲区元数据队列。
- 自动处理缓冲区环绕(例如:第8帧跨越1024字节边界时,计算出924→1023和0→31的分段位置)。
- 暂停DMA,通过
- 应用程序数据提取
- 调用
USART2_GetFrame时,根据元数据从DMA缓冲区复制完整帧(若跨边界则分两次memcpy)。 - 复制完成后更新读索引,释放环形缓冲区空间。
- 调用
- 整体可靠性保障
1024字节DMA缓冲区循环复用,32帧的环形元数据队列确保应用层处理稍慢(如计算CRC时)也能缓冲足够帧数(120帧中任意连续32帧可暂存),避免丢帧,最终可靠完成15KB固件接收。

浙公网安备 33010602011771号