4. 性能与内存度量方法

UART这个外设,说简单也简单,说复杂也复杂。我见过不少工程师,写个轮询收发没问题,但一旦要上中断、上DMA,就开始手忙脚乱。说白了,UART驱动的核心不在于怎么配置寄存器,而在于怎么把数据流管理好。

这一章,咱们就围绕三个实战点展开:环形缓冲区怎么设计才不丢数据、波特率怎么在运行时动态切换、DMA与中断怎么协同工作。嗯,这些都是我在项目里踩过坑的地方。

4.1 环形缓冲区:UART驱动的“蓄水池”

先问个问题:为什么需要环形缓冲区?

你想想看,CPU处理数据的速度和UART接收数据的速度,往往不在一个节奏上。如果每次收到一个字节就立刻处理,那CPU就被绑死了。如果等收完再处理,又可能丢数据。环形缓冲区就是中间那个“蓄水池”,让生产和消费解耦。

4.1.1 数据结构设计

我个人习惯用这种结构体来管理环形缓冲区:

typedef struct {
    uint8_t *buffer;    // 数据存储区
    uint32_t size;      // 缓冲区大小(必须是2的幂)
    volatile uint32_t head;  // 写指针
    volatile uint32_t tail;  // 读指针
} ring_buffer_t;

这里有个关键点:size为什么必须是2的幂?因为可以用位运算代替取模,效率高很多。我在一个低功耗项目里,CPU主频只有8MHz,用位运算比取模快了将近5倍。

4.1.2 核心操作函数

写指针和读指针的移动,用这个宏:

#define RB_INC(idx, size)  ((idx + 1) & (size - 1))

判断空和满:

static inline bool rb_is_empty(ring_buffer_t *rb) {
    return rb->head == rb->tail;
}

static inline bool rb_is_full(ring_buffer_t *rb) {
    return RB_INC(rb->head, rb->size) == rb->tail;
}

写入一个字节:

bool rb_put(ring_buffer_t *rb, uint8_t data) {
    uint32_t next = RB_INC(rb->head, rb->size);
    if (next == rb->tail) {
        return false;  // 缓冲区满,丢数据
    }
    rb->buffer[rb->head] = data;
    rb->head = next;
    return true;
}
注意:我曾经在STM32F103上犯过一个错误——在中断服务函数里直接调用 rb_put,但没加 volatile 修饰 head 和 tail。结果编译器优化后,中断里改了 head,主循环却读不到新值。嗯,从此以后我再也不敢漏掉 volatile 了。

4.1.3 多生产者/单消费者场景

UART接收中断是生产者,主循环是消费者。这种场景下,只需要保证写操作不被读操作打断就行。我的做法是:

  • 中断里只写 head,不读 tail
  • 主循环里只读 head,只写 tail
  • 这样就不需要关中断,性能好很多

4.2 波特率动态配置:运行时切换的陷阱

波特率动态配置,说白了就是系统运行过程中,根据通信对象改变UART的速率。比如和GPS模块通信用9600,和蓝牙模块通信用115200。

4.2.1 配置流程

我建议的步骤是:

  1. 停止收发:先关掉UART的发送和接收使能
  2. 等待当前传输完成:检查发送移位寄存器是否为空
  3. 重新配置波特率寄存器:写入新的分频值
  4. 重新使能:打开收发使能

代码示例:

void uart_set_baudrate(uart_t *uart, uint32_t baud) {
    // 1. 停止收发
    uart->regs->CR1 &= ~(USART_CR1_TE | USART_CR1_RE);
    
    // 2. 等待发送完成
    while (!(uart->regs->SR & USART_SR_TC));
    
    // 3. 计算并设置波特率
    uint32_t div = uart->pclk / baud;
    uart->regs->BRR = div;
    
    // 4. 重新使能
    uart->regs->CR1 |= (USART_CR1_TE | USART_CR1_RE);
}
小技巧:如果系统里有多路UART,建议把波特率配置封装成一个独立函数,不要和初始化混在一起。这样动态切换时,直接调用这个函数就行,代码复用率高。

4.2.2 避坑指南

我曾经在一个项目里,动态切换波特率后,发现第一个字节总是错的。查了半天,原来是切换后没有清空接收移位寄存器里的残留数据。解决方案很简单:切换前读一次DR寄存器,把残留数据扔掉。

4.3 DMA传输与中断协同:让CPU喘口气

UART加上DMA,才是真正的高性能方案。CPU只需要在DMA传输完成时处理一次,中间完全不用管。

4.3.1 架构设计

我常用的方案是:

  • 接收:DMA循环模式 + 半满/全满中断
  • 发送:DMA普通模式 + 传输完成中断

接收用循环模式,相当于硬件帮你维护了一个环形缓冲区。DMA每收满一半缓冲区,触发一次中断,CPU去处理前半段数据。这样CPU的负载非常均匀。

4.3.2 中断优先级分配

这里有个容易忽略的点:中断优先级怎么配?

中断类型 推荐优先级 说明
DMA接收半满中断 高 数据必须及时搬走,否则会被覆盖
DMA接收全满中断 高 同上
DMA发送完成中断 中 可以稍微延迟,不影响接收
UART错误中断 最高 溢出、帧错误等需要立即处理

4.3.3 代码框架

初始化DMA接收:

void uart_dma_rx_init(uart_t *uart, uint8_t *buf, uint32_t len) {
    // 配置DMA为循环模式
    dma_channel_config_t cfg = {
        .src_addr = &uart->regs->DR,
        .dst_addr = buf,
        .size = len,
        .circular = true,
        .half_int = true,   // 半满中断
        .full_int = true    // 全满中断
    };
    dma_channel_init(uart->dma_rx_ch, &cfg);
    
    // 使能UART的DMA接收
    uart->regs->CR3 |= USART_CR3_DMAR;
}

中断处理:

void uart_dma_rx_irq_handler(uart_t *uart) {
    uint32_t remaining = dma_get_remaining(uart->dma_rx_ch);
    uint32_t processed = uart->dma_buf_size - remaining;
    
    // 计算本次要处理的数据区间
    if (processed > uart->last_processed) {
        // 有新数据到达,交给上层处理
        uint32_t start = uart->last_processed;
        uint32_t end = processed;
        uart->data_callback(uart->dma_buf + start, end - start);
        uart->last_processed = end;
    }
    
    // 如果绕回,处理尾部数据
    if (processed < uart->last_processed) {
        // 处理 [last_processed, size) 和 [0, processed)
        uart->data_callback(uart->dma_buf + uart->last_processed,
                            uart->dma_buf_size - uart->last_processed);
        uart->data_callback(uart->dma_buf, processed);
        uart->last_processed = processed;
    }
}
核心思路:DMA负责搬运数据,中断负责通知CPU“数据到了”,CPU只做轻量级的回调处理。这样CPU的利用率能提升30%以上,特别是在高波特率场景下。

4.4 整体架构图

下面这张图,把UART驱动的三层结构画清楚了:

UART驱动三层架构 UART驱动三层架构 应用层(数据回调) 驱动封装层 环形缓冲区管理 | 波特率动态配置 DMA传输控制 | 中断优先级协调 硬件抽象层(寄存器操作) UART外设 | DMA控制器 | NVIC 数据流向:硬件 → DMA → 环形缓冲区 → 应用回调 中断只在半满/全满/错误时触发,CPU负载极低 关键设计要点 ① 环形缓冲区大小必须是2的幂,用位运算加速 ② 波特率切换前必须清空残留数据

4.5 性能对比

最后,我用一个表格总结三种模式的性能差异:

模式 CPU占用率 最大波特率 适用场景
轮询模式 100%(阻塞) 115200 调试、低速率通信
中断模式 10%~30% 921600 中等速率、数据量不大
DMA+中断 <5% 3Mbps+ 高速率、大数据量

嗯,看到这个表你就明白了。如果你的系统里UART只是用来打印日志,轮询就够了。但如果是和传感器、GPS模块、蓝牙模块通信,我建议直接上DMA+中断方案。前期多花点时间封装好,后面省心很多。

这一章的内容就到这里。环形缓冲区、波特率动态配置、DMA与中断协同,这三个点你吃透了,UART驱动这块基本就稳了。