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");
    }

}

测试结果:

image-20250910200812706

回显256个字符,超出的舍弃了。

过程分析

image-20250910201209374

以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的分段位置)。
  • 应用程序数据提取
    • 调用USART2_GetFrame时,根据元数据从DMA缓冲区复制完整帧(若跨边界则分两次memcpy)。
    • 复制完成后更新读索引,释放环形缓冲区空间。
  • 整体可靠性保障
    1024字节DMA缓冲区循环复用,32帧的环形元数据队列确保应用层处理稍慢(如计算CRC时)也能缓冲足够帧数(120帧中任意连续32帧可暂存),避免丢帧,最终可靠完成15KB固件接收。
posted @ 2025-09-10 20:21  海浪博客  阅读(371)  评论(0)    收藏  举报