第4章 UART驱动开发:从协议到RTOS框架

UART,说白了就是串口。做嵌入式的人,没人离得开它。我入行那会儿,调试全靠串口打印,连个像样的调试器都没有。现在虽然工具多了,但UART依然是驱动开发的基础课——你搞不定它,后面的I2C、SPI、CAN都别想碰。

这一章,咱们把UART从协议到驱动,从轮询到DMA,再到RTOS下的框架设计,一条线捋清楚。

4.1 UART协议基础——你得知道它在说什么

UART全称是Universal Asynchronous Receiver/Transmitter。注意"异步"这两个字——收发双方没有独立的时钟线,全靠约定的波特率来对齐。

核心要点:UART是异步串行通信,一帧数据包含起始位、数据位、校验位、停止位。双方必须约定完全相同的帧格式和波特率,否则收到的全是乱码。

一帧UART数据长这样:

起始位 数据位(5~9位) 校验位(可选) 停止位(1/1.5/2位)
1位,低电平 LSB先发,MSB后发 奇校验/偶校验/无 高电平,表示帧结束

我遇到过最坑的事:一个同事把数据位配成了7位,但上位机用的是8位。结果每次收到数据都少一位,后面全错位。查了整整一下午,最后发现是协议没对齐。嗯,这种低级错误,犯过一次就再也不会忘了。

我的习惯:项目初期就把UART参数写成一个结构体,放在头文件里。8N1(8数据位、无校验、1停止位)是最通用的配置,除非有特殊要求,否则别乱改。

4.2 轮询模式收发——简单但别滥用

轮询模式,就是CPU死等。发送时,等发送寄存器空;接收时,等接收寄存器非空。代码写起来最直接,但效率最低。

// UART轮询发送一个字节
void uart_poll_send(uint8_t data) {
    // 等待发送缓冲区空
    while (!(UART->SR & UART_SR_TXE));
    // 写入数据
    UART->DR = data;
}

// UART轮询接收一个字节
uint8_t uart_poll_recv(void) {
    // 等待接收数据就绪
    while (!(UART->SR & UART_SR_RXNE));
    // 读取数据
    return (uint8_t)(UART->DR & 0xFF);
}

这段代码,初学者都能看懂。但我要说的是——轮询模式只适合两个场景:

  • 系统初始化阶段:RTOS还没跑起来,打印个启动信息
  • 调试阶段:临时打个log,不需要考虑性能

注意:轮询模式会阻塞CPU。如果你在RTOS的任务里用轮询收数据,整个任务都会被卡住。我曾经见过有人用轮询收GPS数据,结果任务优先级设低了,数据全丢了。别这么干。

4.3 中断模式收发——RTOS下的标配

中断模式,说白了就是让硬件帮你盯着。数据来了,硬件触发中断,CPU在中断服务函数里处理。这样CPU不用死等,可以干别的事。

中断模式的关键点有两个:

  • 中断服务函数要快:只做最必要的事,比如把数据扔进环形缓冲区
  • 数据保护:中断和任务共享的数据,必须加临界区保护
// UART中断接收服务函数(简化版)
void UART_IRQHandler(void) {
    uint8_t data;
    
    if (UART->SR & UART_SR_RXNE) {
        data = (uint8_t)(UART->DR & 0xFF);
        // 把数据放入环形缓冲区
        ringbuf_push(&rx_ringbuf, data);
    }
    
    // 处理溢出错误
    if (UART->SR & UART_SR_ORE) {
        // 读DR清标志
        (void)UART->DR;
    }
}

我个人的习惯是:中断里只做数据搬运,不做协议解析。协议解析放到任务里做,这样不会拖慢中断响应。

避坑指南:我曾经在中断里调用了printf,结果printf内部又用了UART发送,导致递归中断,系统直接挂掉。记住——中断服务函数里,别调用任何可能触发同一中断的函数。

4.4 DMA模式收发——高性能场景的利器

DMA,就是让数据自己走,不经过CPU。CPU只需要告诉DMA控制器:"从内存A搬N个字节到UART数据寄存器",然后就可以去干别的事了。搬完了,DMA给你个中断通知一声。

DMA模式适合什么场景?

  • 大数据量收发:比如通过串口升级固件,一次传几KB甚至几MB
  • 高波特率通信:921600bps以上,CPU逐字节处理根本扛不住
  • 实时性要求高:CPU不能被UART占用太多时间

DMA配置的几个关键参数:

参数 说明 我的建议
传输方向 内存到外设(发送)/ 外设到内存(接收) 明确区分,别搞反
数据宽度 字节/半字/字 UART一般用字节
循环模式 是否自动重载 接收常用,发送少用
中断触发 传输完成/半完成/错误 至少开传输完成中断
// DMA发送配置示例(伪代码)
void uart_dma_send(uint8_t *buf, uint32_t len) {
    // 配置DMA源地址(内存)
    DMA->CMAR = (uint32_t)buf;
    // 配置DMA目标地址(UART数据寄存器)
    DMA->CPAR = (uint32_t)&UART->DR;
    // 配置传输长度
    DMA->CNDTR = len;
    // 使能DMA通道
    DMA->CCR |= DMA_CCR_EN;
    // 使能UART的DMA发送请求
    UART->CR3 |= USART_CR3_DMAT;
}

注意:DMA接收有个坑——你不知道数据什么时候来,来多少。所以DMA接收一般配合空闲中断(IDLE interrupt)使用:DMA一直收,收到空闲中断时,就知道一帧数据结束了。我当年做4G模块驱动时,就靠这个方案收AT指令的响应。

4.5 RTOS下串口驱动框架设计

好了,前面都是基础。现在咱们聊聊RTOS下的串口驱动该怎么设计。这不是简单的把轮询改成中断就完事了,而是要从架构层面考虑。

一个成熟的RTOS串口驱动框架,通常包含这几层:

RTOS串口驱动框架分层架构 应用层(Application) 用户任务:发送/接收数据、协议解析 设备驱动层(Device Driver) open/close/read/write/ioctl 标准接口 硬件抽象层(HAL) 寄存器操作、中断处理、DMA配置 硬件层(Hardware) UART外设、GPIO、DMA控制器 关键设计原则 上层不直接操作寄存器,下层不关心业务逻辑。每层职责清晰,接口稳定。

这个框架,说白了就是分层解耦。每一层只关心自己的事,不越界。我见过很多新手写驱动,应用层直接操作寄存器,结果换个芯片平台,代码全废了。

具体到RTOS下的实现,有几个关键点:

  • 使用信号量或消息队列:中断收到数据后,释放信号量或发消息给等待的任务
  • 支持超时机制:接收不能无限等,要有超时返回
  • 缓冲区管理:环形缓冲区是标配,大小根据波特率和数据量估算
  • 多任务安全:多个任务可能同时操作同一个串口,需要互斥保护
// RTOS串口驱动接收接口示例
int uart_read(uint8_t port, uint8_t *buf, uint32_t len, uint32_t timeout_ms) {
    uint32_t received = 0;
    
    // 获取互斥锁,防止多任务冲突
    if (xSemaphoreTake(uart_dev[port].mutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) {
        return -1; // 超时未获取到锁
    }
    
    while (received < len) {
        // 从环形缓冲区取数据
        if (ringbuf_pop(&uart_dev[port].rx_ringbuf, &buf[received])) {
            received++;
        } else {
            // 缓冲区空,等待信号量
            if (xSemaphoreTake(uart_dev[port].rx_sem, pdMS_TO_TICKS(10)) != pdTRUE) {
                // 超时,返回已接收的数据量
                xSemaphoreGive(uart_dev[port].mutex);
                return received;
            }
        }
    }
    
    xSemaphoreGive(uart_dev[port].mutex);
    return received;
}

我的经验:环形缓冲区的大小,我一般按波特率来算。比如115200bps,一秒钟最多约11.5KB数据。如果系统能在100ms内处理完,缓冲区设2KB就够了。但如果你做的是4G模块,数据可能突发,缓冲区至少给4KB。宁可多占点内存,也别丢数据。

4.6 三种收发模式的对比与选型

最后,咱们把三种模式放在一起对比一下,方便你根据实际场景选型。

模式 CPU占用 实时性 适合场景 代码复杂度
轮询模式 高(一直占用) 差(阻塞) 初始化、调试打印 低
中断模式 中(仅触发时占用) 好(非阻塞) 常规通信、命令交互 中
DMA模式 低(几乎不占用) 最好(后台传输) 大数据量、高波特率 高

选型建议:

  • 项目初期调试,用轮询,简单直接
  • 产品正式版,用中断模式,性价比最高
  • 遇到大数据量或高波特率,上DMA,别犹豫

最后提醒一句:不管用哪种模式,别忘了处理错误。UART常见的错误有溢出错误(ORE)、帧错误(FE)、校验错误(PE)。我见过有人只开了接收中断,没开错误中断,结果溢出后UART直接罢工,数据全丢了。错误处理,一定要做。