4. UART驱动移植:中断接收与DMA传输在RTOS中的实现
UART这个外设,说简单也简单,说复杂也复杂。裸机时代我们可能就一个轮询搞定,但到了RTOS环境下,事情就变得有意思了。我个人习惯把UART驱动分成三个层次:底层寄存器操作、中断服务函数、以及RTOS适配层。今天咱们重点聊聊中断接收和DMA传输这两个核心点。
4.1 裸机UART驱动的问题在哪?
先说说裸机下的痛点。你想想看,裸机程序里我们通常这么写:
// 裸机轮询接收
uint8_t uart_recv_byte(void) {
while (!(USART->SR & USART_SR_RXNE)); // 死等
return (uint8_t)(USART->DR & 0xFF);
}
这段代码在裸机里跑没问题,但放到RTOS里就麻烦了。为什么?因为while循环会阻塞整个任务,CPU在那干等着,别的任务都别想跑。我在项目中遇到过这种情况:一个串口接收任务占用了90%的CPU时间,导致其他传感器数据采集严重滞后。
核心问题:裸机下的轮询方式在RTOS中会导致任务阻塞,浪费CPU资源。我们需要用中断或DMA来解放CPU。
4.2 中断接收:RTOS下的正确姿势
中断接收的思路其实很简单:让硬件在收到数据时通知CPU,而不是CPU去轮询检查。但在RTOS下,中断服务函数里不能直接调用RTOS的API,这是个常见的坑。
我建议的做法是:中断服务函数只做最轻量级的工作——把数据放到一个环形缓冲区里,然后通过信号量或消息队列通知任务来处理。
// 中断服务函数
void USART1_IRQHandler(void) {
uint8_t data;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (USART1->SR & USART_SR_RXNE) {
data = USART1->DR & 0xFF;
// 放入环形缓冲区
ringbuf_put(&uart_rx_buf, data);
// 发送信号量通知接收任务
xSemaphoreGiveFromISR(xRxSemaphore, &xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 接收任务
void uart_rx_task(void *pvParameters) {
uint8_t data;
while (1) {
// 等待信号量,没有数据时任务阻塞
if (xSemaphoreTake(xRxSemaphore, portMAX_DELAY) == pdTRUE) {
while (ringbuf_get(&uart_rx_buf, &data)) {
// 处理接收到的数据
process_uart_data(data);
}
}
}
}
避坑指南:我曾经在中断里直接调用xSemaphoreGive()而不是xSemaphoreGiveFromISR(),结果系统随机死机。记住:在ISR中必须使用带FromISR后缀的API,并且要正确处理任务切换。
4.3 DMA传输:让数据自己跑起来
中断接收虽然比轮询好,但每个字节都触发一次中断,高频通信时中断开销还是不小。这时候DMA就派上用场了。DMA说白了就是让数据在内存和外设之间直接传输,CPU只需要在传输完成时处理一下就行。
我常用的DMA接收方案是双缓冲模式:
// DMA双缓冲配置
#define DMA_BUFFER_SIZE 256
uint8_t dma_buffer1[DMA_BUFFER_SIZE];
uint8_t dma_buffer2[DMA_BUFFER_SIZE];
void uart_dma_init(void) {
// 配置DMA
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStruct.DMA_BufferSize = DMA_BUFFER_SIZE;
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)dma_buffer1;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
// 使能DMA传输完成中断
DMA_ITConfig(DMA1_Stream0, DMA_IT_TC, ENABLE);
}
void DMA1_Stream0_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (DMA_GetITStatus(DMA1_Stream0, DMA_IT_TC)) {
// 切换缓冲区
uint32_t current_addr = DMA_GetCurrentMemoryTarget(DMA1_Stream0);
if (current_addr == (uint32_t)dma_buffer1) {
// buffer1已满,切换到buffer2
// 通知任务处理buffer1的数据
xSemaphoreGiveFromISR(xDmaSemaphore, &xHigherPriorityTaskWoken);
} else {
// buffer2已满,切换到buffer1
xSemaphoreGiveFromISR(xDmaSemaphore, &xHigherPriorityTaskWoken);
}
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
注意:DMA传输完成中断里不要做复杂的数据处理。我曾经犯过这个错误,在中断里解析数据包,结果导致中断响应时间过长,丢失了后续数据。正确的做法是:中断只负责通知,数据处理交给任务。
4.4 中断 vs DMA:怎么选?
很多初学者会问:到底用中断还是DMA?我的经验是看波特率和数据量:
| 场景 | 波特率 | 推荐方式 | 原因 |
|---|---|---|---|
| 调试日志输出 | 115200以下 | 中断接收 | 数据量小,中断开销可接受 |
| 传感器数据采集 | 115200-921600 | DMA接收 | 数据量大,减少CPU干预 |
| GPS模块通信 | 9600-115200 | 中断接收+环形缓冲 | 数据不定长,中断更灵活 |
| 高速数传链路 | 1Mbps以上 | DMA双缓冲 | 必须用DMA,否则CPU扛不住 |
4.5 RTOS下的UART驱动架构
最后,我画了一张图来展示完整的UART驱动架构。这张图是我在实际项目中总结出来的,经过多次迭代才稳定下来。
这张图展示了数据从硬件到应用的完整路径。你看,每一层各司其职:硬件层负责物理传输,驱动层处理中断和缓冲,RTOS层提供同步机制,应用层只管处理数据。这种分层设计的好处是:哪一层出了问题,我们只需要改那一层的代码,不影响其他层。
我的经验:在实际项目中,我通常把UART驱动做成一个独立的模块,提供统一的API接口。这样不管底层是用中断还是DMA,上层任务调用的接口都一样。比如:uart_send()、uart_recv()、uart_set_callback()。这种设计让代码的可维护性大大提高。
嗯,关于UART驱动移植的核心内容就这些。中断接收和DMA传输是RTOS下最常用的两种方式,选哪个取决于你的实际需求。记住一点:不要让CPU做它不该做的事,把时间留给更重要的任务。
小技巧:调试UART驱动时,我习惯先用逻辑分析仪抓波形,确认硬件通信正常,再去排查软件问题。这样可以快速定位是硬件还是软件的问题,避免在错误的方向上浪费时间。
公众号:蓝海资料掘金营,微信deep3321