在嵌入式项目中,串口通信是最常见的外设之一。而在数据量较大或实时性要求高的场景下,结合 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
|
|