打印
[研电赛技术支持]

基于 GD32 的 USART + DMA + 环形队列 接收方案

[复制链接]
920|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
在嵌入式项目中,串口通信是最常见的外设之一。而在数据量较大或实时性要求高的场景下,结合 DMA 与 环形队列 可以大幅提升系统的性能与可靠性。本文将以 GD32F4 系列 MCU 为例,手把手教你从零开始搭建一个串口接收模块,覆盖以下内容:

环形队列(软件环)的基本实现

USART + DMA + 空闲中断 的初始化

中断服务函数:搬移数据到环形队列

在主循环/任务中读取并按“包”处理数据

一、环形队列(软件环)实现
/*******************************************************************************
*                                环形队列接口                                 *
* 本模块提供简单的循环队列(环形缓冲区)操作,用于暂存字节流并解析协议包。
*******************************************************************************/

typedef enum {
    QUEUE_OK = 0,    // 操作成功
    QUEUE_OVERLOAD,  // 队列已满
    QUEUE_EMPTY      // 队列为空
} QueueStatus_t;

typedef struct {
    uint32_t head;     // 队头索引(有效数据的起始下标)
    uint32_t tail;     // 队尾索引(下一个写入位置)
    uint32_t size;     // 缓冲区容量
    uint8_t *buffer;   // 存储数组指针
} QueueType_t;

/**
* @brief  初始化环形队列
* @param  q   队列对象指针
* @param  buf 队列存储数组
* @param  size 数组长度(必须大于 0)
*/
void QueueInit(QueueType_t *q, uint8_t *buf, uint32_t size) {
    q->buffer = buf;
    q->size   = size;
    q->head   = 0;
    q->tail   = 0;
}

/**
* @brief  向队列压入一个字节
* @param  q    队列对象指针
* @param  data 待压入的数据字节
* @return 队列状态:QUEUE_OK 或 QUEUE_OVERLOAD
*/
QueueStatus_t QueuePush(QueueType_t *q, uint8_t data) {
    uint32_t next = (q->tail + 1) % q->size;
    if (next == q->head) {
        return QUEUE_OVERLOAD; // 队列已满
    }
    q->buffer[q->tail] = data;
    q->tail = next;
    return QUEUE_OK;
}

/**
* @brief  从队列弹出一个字节
* @param  q      队列对象指针
* @param  pdata  存放弹出数据的地址
* @return 队列状态:QUEUE_OK 或 QUEUE_EMPTY
*/
QueueStatus_t QueuePop(QueueType_t *q, uint8_t *pdata) {
    if (q->head == q->tail) {
        return QUEUE_EMPTY; // 队列为空
    }
    *pdata = q->buffer[q->head];
    q->head = (q->head + 1) % q->size;
    return QUEUE_OK;
}



二、USART + DMA + 空闲中断
/*******************************************************************************
*                           USB→UART + DMA + 协议定义                          *
*******************************************************************************/

typedef struct {
    uint32_t uartNo;
    rcu_periph_enum rcuUart;
    rcu_periph_enum rcuGpio;
    uint32_t gpio;
    uint32_t txPin;
    uint32_t rxPin;
    uint8_t irq;
    uint32_t dmaNo;
    rcu_periph_enum rcuDma;
    dma_channel_enum dmaCh;
} UartHwInfo_t;

// 串口硬件配置信息
static UartHwInfo_t g_uartHwInfo = {
    USART0, RCU_USART0, RCU_GPIOA, GPIOA,
    GPIO_PIN_9, GPIO_PIN_10, USART0_IRQn,
    DMA0, RCU_DMA0, DMA_CH4
};

#define USART0_DATA_ADDR    (USART0 + 0x04)   // 串口数据寄存器地址
#define FRAME_HEAD_0        0x55             // 包头字节0
#define FRAME_HEAD_1        0xAA             // 包头字节1
#define CTRL_DATA_LEN       3                // 数据域长度(LED编号+状态 共3字节)
#define PACKET_LEN          (CTRL_DATA_LEN + 4) // 整包长度 = 帧头2 + 长度1 + 功能1 + 数据3 + 校验1
#define MAX_QUEUE_SIZE      64               // 环形队列容量
#define LED_CTRL_CODE       0x06             // 功能字:LED控制

// DMA 中转缓冲区:接收一个完整包
static uint8_t  g_dmaBuf[PACKET_LEN];
static bool     g_pktRcvd = false;        // 标记:收到完整包

// 环形队列缓冲与对象
static uint8_t     g_queueBuf[MAX_QUEUE_SIZE];
static QueueType_t g_rcvQueue;

typedef struct {
    uint8_t ledNo;     // LED 编号
    uint8_t ledState;  // LED 状态(0 关, 非0 开)
} LedCtrlInfo_t;

/*******************************************************************************
*                                硬件初始化                                  *
*******************************************************************************/

// GPIO 配置
static void Usb2ComGpioInit(void) {
    rcu_periph_clock_enable(g_uartHwInfo.rcuGpio);
    gpio_init(g_uartHwInfo.gpio, GPIO_MODE_AF_PP, GPIO_OSPEED_10MHZ, g_uartHwInfo.txPin);
    gpio_init(g_uartHwInfo.gpio, GPIO_MODE_IPU,  GPIO_OSPEED_10MHZ, g_uartHwInfo.rxPin);
}

// UART 波特率、收发及中断配置
static void Usb2ComUartInit(uint32_t baudRate) {
    rcu_periph_clock_enable(g_uartHwInfo.rcuUart);
    usart_deinit(g_uartHwInfo.uartNo);
    usart_baudrate_set(g_uartHwInfo.uartNo, baudRate);
    usart_transmit_config(g_uartHwInfo.uartNo, USART_TRANSMIT_ENABLE);
    usart_receive_config(g_uartHwInfo.uartNo, USART_RECEIVE_ENABLE);
    usart_interrupt_enable(g_uartHwInfo.uartNo, USART_INT_IDLE);
    nvic_irq_enable(g_uartHwInfo.irq, 0, 0);
    usart_enable(g_uartHwInfo.uartNo);
}

// DMA 接收配置
static void Usb2ComDmaInit(void) {
    rcu_periph_clock_enable(g_uartHwInfo.rcuDma);
    dma_deinit(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh);

    dma_parameter_struct dmaStruct;
    dmaStruct.direction    = DMA_PERIPHERAL_TO_MEMORY;        // 外设->内存
    dmaStruct.periph_addr  = USART0_DATA_ADDR;                // 源:串口数据寄存器
    dmaStruct.periph_inc   = DMA_PERIPH_INCREASE_DISABLE;     // 源地址不递增
    dmaStruct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;       // 每次读1字节
    dmaStruct.memory_addr  = (uint32_t)g_dmaBuf;              // 目的:DMA缓冲区
    dmaStruct.memory_inc   = DMA_MEMORY_INCREASE_ENABLE;      // 目的地址递增
    dmaStruct.memory_width = DMA_MEMORY_WIDTH_8BIT;           // 每次写1字节
    dmaStruct.number       = PACKET_LEN;                      // 接收字节数 = 一个包长度
    dmaStruct.priority     = DMA_PRIORITY_HIGH;

    dma_init(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh, &dmaStruct);
    usart_dma_receive_config(g_uartHwInfo.uartNo, USART_RECEIVE_DMA_ENABLE);
    dma_channel_enable(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh);
}

// 总体初始化接口:GPIO + UART + DMA + 队列
void Usb2ComDrvInit(void) {
    // 初始化环形队列
    QueueInit(&g_rcvQueue, g_queueBuf, MAX_QUEUE_SIZE);

    Usb2ComGpioInit();
    Usb2ComUartInit(115200);
    Usb2ComDmaInit();
}

/*******************************************************************************
*                             校验 & LED 控制                                *
*******************************************************************************/

// 异或校验:对 data[0..len-1] 做 XOR
static uint8_t CalXorSum(const uint8_t *data, uint32_t len) {
    uint8_t sum = 0;
    for (uint32_t i = 0; i < len; i++) {
        sum ^= data;
    }
    return sum;
}

// 执行 LED 控制:根据状态打开或关闭 LED
static void CtrlLed(const LedCtrlInfo_t *info) {
    if (info->ledState) TurnOnLed(info->ledNo);
    else                TurnOffLed(info->ledNo);
}



/*******************************************************************************
*                         USART 中断 & DMA 完成标志                            *
*******************************************************************************/

void USART0_IRQHandler(void) {
    // 判断是否为空闲中断(收包完成)
    if (usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_IDLE) != RESET) {
        usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_IDLE);
        usart_data_receive(g_uartHwInfo.uartNo);  // 清除残余标志

        // 如果 DMA 恰好传输了一个完整包长度的字节数
        if (PACKET_LEN == (PACKET_LEN - dma_transfer_number_get(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh))) {
            g_pktRcvd = true; // 标记包接收完成
        }
        // 重启 DMA,准备接收下一个包
        dma_channel_disable(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh);
        dma_transfer_number_config(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh, PACKET_LEN);
        dma_channel_enable(g_uartHwInfo.dmaNo, g_uartHwInfo.dmaCh);
    }
}

/*******************************************************************************
*                             主任务:包处理                                 *
*******************************************************************************/

void Usb2ComTask(void) {
    if (!g_pktRcvd) return;  // 没有新包就退出
    g_pktRcvd = false;       // 清标志,准备下一次接收

    // 将 DMA 缓冲区中的一个整包写入环形队列
    for (uint32_t i = 0; i < PACKET_LEN; i++) {
        if (QueuePush(&g_rcvQueue, g_dmaBuf) == QUEUE_OVERLOAD) {
            // 若队列满,直接丢弃后续字节
            break;
        }
    }

    // 循环处理队列中所有完整包
    uint8_t temp[PACKET_LEN];
    while ((g_rcvQueue.tail + g_rcvQueue.size - g_rcvQueue.head) % g_rcvQueue.size >= PACKET_LEN) {
        // 从队列头依次弹出 PACKET_LEN 字节到 temp
        for (uint32_t i = 0; i < PACKET_LEN; i++) {
            QueuePop(&g_rcvQueue, &temp);
        }
        // 校验帧头
        if (temp[0] != FRAME_HEAD_0 || temp[1] != FRAME_HEAD_1) continue;
        // 校验异或
        if (CalXorSum(temp, PACKET_LEN - 1) != temp[PACKET_LEN - 1]) continue;
        // 解析功能字并控制 LED
        if (temp[3] == LED_CTRL_CODE) {
            LedCtrlInfo_t info = { temp[4], temp[5] };
            CtrlLed(&info);
        }
    }
}



QueueInit / QueuePush / QueuePop:环形队列基本操作,管理字节的缓冲、避免覆盖。

DMA 接收缓冲 g_dmaBuf:只接收一个包长度的数据。

USART0_IRQHandler:识别空闲中断,标记包就绪,重启 DMA。

Usb2ComTask:将接收到的一整包数据推入队列,然后循环检查队列中是否有完整包,依次弹出、校验帧头和异或、解析 LED 控制命令。

校验逻辑:CalXorSum(data, len) 做 XOR,和尾部校验字对比。

LED 控制:CtrlLed() 根据 ledState 点亮或熄灭指定 LED。

三、原理流程图:



————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/2502_91095457/article/details/147977564

使用特权

评论回复
沙发
chenqianqian| | 2025-6-12 08:13 | 只看该作者
楼主测试过最大通信速率能做到多高而不丢包吗?

使用特权

评论回复
板凳
jackcat| | 2025-7-4 14:16 | 只看该作者
DMA控制器允许数据在外部设备和内存之间直接传输

使用特权

评论回复
地板
uiint| | 2025-7-6 10:14 | 只看该作者
传输数量应根据实际应用需求和USART的接收速率进行调整。

使用特权

评论回复
5
maudlu| | 2025-7-6 11:06 | 只看该作者
可配置双缓冲区,DMA 交替写入两个缓冲区,进一步提高可靠性。

使用特权

评论回复
6
burgessmaggie| | 2025-7-6 16:55 | 只看该作者
DMA缓冲区大小足够大,以避免数据丢失。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

55

主题

177

帖子

0

粉丝