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驱动架构 硬件层 UART外设寄存器 | DMA控制器 | NVIC中断控制器 驱动层(中断服务函数) 中断接收ISR | DMA传输完成ISR | 环形缓冲区管理 信号量/消息队列发送(FromISR版本) RTOS适配层 任务创建 | 信号量/队列管理 | 任务间同步与通信 应用层(用户任务) 数据流向 硬件 → 驱动 → RTOS → 应用 中断信号向上传递 数据通过缓冲区传递

这张图展示了数据从硬件到应用的完整路径。你看,每一层各司其职:硬件层负责物理传输,驱动层处理中断和缓冲,RTOS层提供同步机制,应用层只管处理数据。这种分层设计的好处是:哪一层出了问题,我们只需要改那一层的代码,不影响其他层。

我的经验:在实际项目中,我通常把UART驱动做成一个独立的模块,提供统一的API接口。这样不管底层是用中断还是DMA,上层任务调用的接口都一样。比如:uart_send()、uart_recv()、uart_set_callback()。这种设计让代码的可维护性大大提高。

嗯,关于UART驱动移植的核心内容就这些。中断接收和DMA传输是RTOS下最常用的两种方式,选哪个取决于你的实际需求。记住一点:不要让CPU做它不该做的事,把时间留给更重要的任务。

小技巧:调试UART驱动时,我习惯先用逻辑分析仪抓波形,确认硬件通信正常,再去排查软件问题。这样可以快速定位是硬件还是软件的问题,避免在错误的方向上浪费时间。


公众号:蓝海资料掘金营,微信deep3321