Haizangwang 发表于 2025-6-12 07:19

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

在嵌入式项目中,串口通信是最常见的外设之一。而在数据量较大或实时性要求高的场景下,结合 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初始化环形队列
* @paramq   队列对象指针
* @parambuf 队列存储数组
* @paramsize 数组长度(必须大于 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向队列压入一个字节
* @paramq    队列对象指针
* @paramdata 待压入的数据字节
* @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 = data;
    q->tail = next;
    return QUEUE_OK;
}

/**
* @brief从队列弹出一个字节
* @paramq      队列对象指针
* @parampdata存放弹出数据的地址
* @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 + 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_tg_dmaBuf;
static bool   g_pktRcvd = false;      // 标记:收到完整包

// 环形队列缓冲与对象
static uint8_t   g_queueBuf;
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 做 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;
    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 != FRAME_HEAD_0 || temp != FRAME_HEAD_1) continue;
      // 校验异或
      if (CalXorSum(temp, PACKET_LEN - 1) != temp) continue;
      // 解析功能字并控制 LED
      if (temp == LED_CTRL_CODE) {
            LedCtrlInfo_t info = { temp, temp };
            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的接收速率进行调整。

maudlu 发表于 2025-7-6 11:06

可配置双缓冲区,DMA 交替写入两个缓冲区,进一步提高可靠性。

burgessmaggie 发表于 2025-7-6 16:55

DMA缓冲区大小足够大,以避免数据丢失。

uytyu 发表于 2025-7-10 14:35

在实际应用中,应该添加适当的错误检测和处理机制,比如超时重连、校验错误处理等。

yorkbarney 发表于 2025-7-11 16:49

使用DMA_CIRCULAR模式可以让DMA在到达缓冲区末尾时自动回到起始位置继续接收数据,这对于持续的数据流非常有用。

sdlls 发表于 2025-7-12 14:17

配置DMA工作为外设到内存模式,DMA的源地址设置为串口的数据寄存器。当串口收到一个字节数据且RBNE(接受非空)标志位为1时,DMA自动将数据寄存器中的数据搬运到内存中

louliana 发表于 2025-7-12 18:14

GD32单片机的USART结合DMA和环形队列的接收方案,能够实现高效、稳定的数据接收

loutin 发表于 2025-7-12 18:50

环形队列的头尾指针正确更新,避免数据覆盖。

robertesth 发表于 2025-7-12 20:00

定义一个足够大的缓冲区作为环形队列的存储空间。
实现队列的头尾指针,用于跟踪队列的开始和结束位置。
实现队列的入队和出队操作,确保数据的先进先出(FIFO)。

belindagraham 发表于 2025-7-12 20:41

缓冲区的大小应根据预期的最大数据量和系统的内存资源进行选择。
页: [1]
查看完整版本: 基于 GD32 的 USART + DMA + 环形队列 接收方案