在嵌入式系统开发中,串口通信是最基础也最常用的外设接口之一。今天我要分享的是基于GD32F425RGT6的串口驱动实现,采用RTOS+Event+DMA的架构设计。这种方案在工业控制、物联网终端等场景中特别实用,能够实现高效稳定的数据传输,同时减轻CPU负担。
我使用的硬件平台是GD32F425RGT6核心板,这款MCU属于GD32F4系列,主频高达200MHz,内置硬件FPU,性能强劲。软件层面使用了RT-Thread实时操作系统,配合DMA和事件标志机制,实现了零拷贝的串口数据收发。下面我就详细解析这个驱动方案的设计思路和实现细节。
这个串口驱动的核心设计目标是实现高效、稳定的全双工通信,同时保持较低的资源占用。我采用了分层设计的思想:
特别值得注意的是环形缓冲区的设计,它作为接收数据的临时存储区,可以有效解决数据接收和处理的速率不匹配问题。我设置的缓冲区大小是256字节,实际项目中可以根据具体需求调整。
驱动中定义了一个核心结构体SerialDriver_t,它管理着每个串口实例的所有状态信息:
c复制typedef struct {
uint32_t instance; // 串口外设实例(如USART0)
osEventFlagsId_t evtHandle; // RTOS事件标志组句柄
struct rt_ringbuffer rxRb; // 环形缓冲区结构体
uint8_t* pRxBuf; // DMA接收缓冲区指针
uint8_t* pRbBuf; // 环形缓冲区存储空间
uint16_t rxBufLen; // DMA接收缓冲区长度
uint16_t rbBufLen; // 环形缓冲区长度
uint8_t isInit; // 初始化标志
} SerialDriver_t;
这种设计使得每个串口实例都能独立管理自己的资源和状态,支持多串口并行工作。在实际测试中,即使同时使用USART0和USART1进行高速通信,系统也能稳定运行。
以USART0为例,首先需要配置相关GPIO和时钟:
c复制// 使能相关时钟
rcu_periph_clock_enable(RCU_DMA1);
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
// 配置TX(PA9)和RX(PA10)引脚
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9); // TX
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_10); // RX
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
这里有几个关键点需要注意:
DMA配置是驱动性能的关键,我使用了DMA1的通道2和通道7分别处理接收和发送:
c复制// 发送DMA配置
dma_single_data_parameter_struct dma_param;
dma_single_data_para_struct_init(&dma_param);
dma_param.direction = DMA_MEMORY_TO_PERIPH;
dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_param.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
dma_param.periph_addr = ((uint32_t)&USART_DATA(USART0));
dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_param.priority = DMA_PRIORITY_ULTRA_HIGH;
dma_single_data_mode_init(U0_DMA, U0_TX_DMA_CH, &dma_param);
接收DMA的配置类似,但方向改为DMA_PERIPH_TO_MEMORY。这里我将DMA优先级设为ULTRA_HIGH,确保在系统繁忙时仍能及时处理串口数据。
中断处理是整个驱动的核心逻辑所在,USART0_IRQHandler函数处理两种关键中断:
c复制void USART0_IRQHandler(void) {
// 空闲中断处理
if(usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) != RESET) {
usart_data_receive(USART0); // 清除标志
const uint16_t rx_len = U0_CACHE_BUF_LEN - dma_transfer_number_get(U0_DMA, U0_RX_DMA_CH);
rt_ringbuffer_put_force(&u0.rxRb, u0.pRxBuf, rx_len); // 数据存入环形缓冲区
osEventFlagsSet(u0.evtHandle, USART_RX_EVT); // 通知应用层
// 重新配置DMA接收
dma_channel_disable(U0_DMA, U0_RX_DMA_CH);
dma_flag_clear(U0_DMA, U0_RX_DMA_CH, DMA_FLAG_FTF);
dma_transfer_number_config(U0_DMA, U0_RX_DMA_CH, u0.rxBufLen);
dma_channel_enable(U0_DMA, U0_RX_DMA_CH);
}
// 发送完成中断处理
else if(usart_interrupt_flag_get(USART0, USART_INT_FLAG_TC) != RESET) {
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_TC);
osEventFlagsSet(u0.evtHandle, USART_TX_EVT); // 通知发送完成
dma_channel_disable(U0_DMA, U0_TX_DMA_CH);
dma_flag_clear(U0_DMA, U0_TX_DMA_CH, DMA_FLAG_FTF);
}
}
驱动提供了三个核心API供应用层调用:
以SerialSendAsync为例,其工作流程如下:
c复制int SerialSendAsync(uint32_t usart_periph, uint8_t* buf, uint16_t len, int32_t timeout) {
// 切换到发送模式(针对RS485等半双工接口)
instance_set_mode(usart_periph, USART_RS_TX_MODE);
// 配置并启动DMA发送
dma_channel_disable(U0_DMA, U0_TX_DMA_CH);
dma_memory_address_config(U0_DMA,U0_TX_DMA_CH, DMA_MEMORY_0, (uint32_t)buf);
dma_transfer_number_config(U0_DMA, U0_TX_DMA_CH, len);
dma_channel_enable(U0_DMA, U0_TX_DMA_CH);
// 等待发送完成事件
const uint32_t flags = osEventFlagsWait(pSerial->evtHandle, USART_TX_EVT, osFlagsWaitAny, timeout);
instance_set_mode(usart_periph, USART_RS_RX_MODE); // 切换回接收模式
return (flags & USART_TX_EVT) ? len : 0;
}
在主任务中,我实现了一个简单的回环测试程序:
c复制static void StartAppTask(void* argument) {
// 初始化驱动和硬件
DriverInit();
DrvUsartInit();
SerialStart(USART0, 115200);
static uint8_t buf[128];
for(;;) {
// 接收数据
int sz = SerialRecv(USART0, buf, sizeof(buf), osWaitForever);
if(sz > 0) {
// 回传接收到的数据
SerialSendAsync(USART0, buf, sz, osWaitForever);
}
}
}
这个测试验证了驱动的全双工通信能力。在实际项目中,可以根据需求扩展协议解析、数据分包等功能。
经过实际测试,我总结了几点优化经验:
缓冲区大小选择:DMA接收缓冲区不宜过大,一般128-512字节即可。过大会增加内存占用,过小可能导致数据溢出。
中断优先级配置:串口中断优先级应高于普通任务,但低于系统关键中断(如看门狗)。我的配置是:
c复制nvic_irq_enable(USART0_IRQn, 5, 0); // 优先级5
错误处理增强:当前驱动对异常情况的处理还不够完善,可以增加以下检测:
功耗优化:在低功耗应用中,可以在空闲时关闭串口时钟,收到数据时再唤醒。
在实际开发过程中,我遇到并解决了一些典型问题,这里分享给大家:
数据接收不完整
发送后无法切换回接收模式
高波特率下数据错误
RTOS任务阻塞问题
这个驱动方案已经在多个实际项目中得到验证,包括工业传感器数据采集、智能控制器通信等场景。它最大的优势是将CPU从繁重的数据搬运工作中解放出来,即使在系统高负载时也能保证通信的实时性。