4. UART中断驱动:基于中断的UART收发与环形缓冲区

UART通信,说白了就是串口收发数据。很多初学者喜欢用轮询方式——CPU一直在那等着,啥也干不了。我刚开始做项目时也这么干,结果一个简单的GPS数据解析就把主循环卡死了。后来我才明白,中断才是嵌入式开发的灵魂。

这一章,我们来聊聊UART中断驱动,以及它的好搭档——环形缓冲区。

4.1 为什么必须用中断?

先看一个场景:你的MCU要同时处理按键扫描、LED闪烁、传感器采集,还要通过UART接收GPS模块每秒发送的NMEA数据。如果用轮询,CPU得不停检查UART接收寄存器有没有新数据。你想想看,这得浪费多少CPU时间?

中断的好处很明显:

  • 实时响应:数据来了立刻通知CPU,不耽误事
  • CPU解放:没数据时CPU可以干别的活
  • 不丢数据:尤其是高速率通信,轮询很容易漏掉字节

核心原则:中断服务函数(ISR)要短、平、快。千万别在ISR里做复杂处理,比如字符串解析、协议解码——这些事交给主循环去做。

4.2 环形缓冲区:中断与主循环的桥梁

中断来了,数据存哪?直接扔全局变量?那肯定不行。主循环还没处理完,下一个中断又来了,数据就被覆盖了。

环形缓冲区(Ring Buffer)就是解决这个问题的。它像一个循环队列,ISR只管往里面写数据,主循环只管从里面读数据。两者互不干扰。

4.2.1 环形缓冲区的工作原理

说白了,就是一个固定大小的数组,加上两个指针:

  • 写指针(write index):ISR往这个位置写数据,写完往后移
  • 读指针(read index):主循环从这个位置读数据,读完往后移

当指针到达数组末尾时,自动回到开头——这就是“环形”的由来。

我的习惯:缓冲区大小选2的幂次方,比如64、128、256。这样可以用位运算代替取模运算,效率更高。

4.2.2 数据结构定义

/* 环形缓冲区结构体 */
typedef struct {
    uint8_t *buffer;      /* 缓冲区指针 */
    uint16_t size;        /* 缓冲区大小(2的幂次方) */
    volatile uint16_t head;  /* 写指针(ISR使用) */
    volatile uint16_t tail;  /* 读指针(主循环使用) */
} ring_buffer_t;

注意volatile关键字。为什么?因为head和tail分别在中断和主循环中被修改,编译器可能会优化掉这些变量的读取。加上volatile,告诉编译器“每次都要从内存读,别给我缓存”。

4.2.3 核心操作函数

环形缓冲区需要四个基本操作:初始化、写、读、判空。我直接给出代码,你拿去就能用。

/* 初始化 */
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buf, uint16_t size) {
    rb->buffer = buf;
    rb->size = size;
    rb->head = 0;
    rb->tail = 0;
}

/* 写入一个字节(ISR中调用) */
int ring_buffer_put(ring_buffer_t *rb, uint8_t data) {
    uint16_t next = (rb->head + 1) & (rb->size - 1);
    if (next == rb->tail) {
        return -1;  /* 缓冲区满了 */
    }
    rb->buffer[rb->head] = data;
    rb->head = next;
    return 0;
}

/* 读取一个字节(主循环中调用) */
int ring_buffer_get(ring_buffer_t *rb, uint8_t *data) {
    if (rb->head == rb->tail) {
        return -1;  /* 缓冲区空了 */
    }
    *data = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) & (rb->size - 1);
    return 0;
}

/* 判断是否为空 */
int ring_buffer_is_empty(ring_buffer_t *rb) {
    return (rb->head == rb->tail);
}

我曾经踩过的坑:在ISR里调用ring_buffer_get,在主循环里调用ring_buffer_put。这会导致数据错乱。记住:ISR只写,主循环只读,这个规矩不能破。

4.3 UART中断接收实战

理论讲完了,咱们来点实战。以STM32为例,配置UART接收中断。

4.3.1 初始化配置

/* 定义环形缓冲区 */
#define RX_BUF_SIZE 256
static uint8_t rx_buf[RX_BUF_SIZE];
static ring_buffer_t uart_rx_rb;

/* UART初始化 */
void uart_init(void) {
    /* 初始化环形缓冲区 */
    ring_buffer_init(&uart_rx_rb, rx_buf, RX_BUF_SIZE);
    
    /* 配置UART(省略时钟、GPIO配置) */
    USART1->BRR = 0x341;  /* 115200bps @ 72MHz */
    USART1->CR1 |= USART_CR1_RE | USART_CR1_TE;  /* 使能收发 */
    USART1->CR1 |= USART_CR1_RXNEIE;  /* 使能接收中断 */
    NVIC_EnableIRQ(USART1_IRQn);  /* 使能NVIC中断 */
    USART1->CR1 |= USART_CR1_UE;  /* 使能UART */
}

4.3.2 中断服务函数

void USART1_IRQHandler(void) {
    uint8_t data;
    
    if (USART1->SR & USART_SR_RXNE) {
        data = (uint8_t)(USART1->DR & 0xFF);
        ring_buffer_put(&uart_rx_rb, data);  /* 存入缓冲区 */
    }
    
    /* 处理溢出错误 */
    if (USART1->SR & USART_SR_ORE) {
        (void)USART1->DR;  /* 读DR清除ORE标志 */
    }
}

4.3.3 主循环读取数据

int main(void) {
    uint8_t ch;
    
    uart_init();
    
    while (1) {
        /* 从环形缓冲区读取数据 */
        if (ring_buffer_get(&uart_rx_rb, &ch) == 0) {
            /* 处理接收到的字节 */
            process_received_byte(ch);
        }
        
        /* 其他任务 */
        handle_led();
        handle_sensor();
    }
}

4.4 UART中断发送

接收搞定了,发送呢?其实发送也可以用中断,但思路刚好反过来:

  • 发送缓冲区:主循环往里面写数据
  • 发送中断:UART发送完成中断触发时,从缓冲区取下一个字节发送

这样做的好处是,发送长数据时CPU不用一直等。比如你要发100个字节,开启发送中断后,CPU只管把数据塞进缓冲区,剩下的交给硬件和ISR。

关键点:发送中断的触发条件是“发送数据寄存器为空”(TXE)或“发送完成”(TC)。一般用TXE中断,效率更高。

4.5 知识体系总览

下面这张图总结了UART中断驱动与环形缓冲区的整体架构。你可以把它当作设计参考。

UART中断驱动与环形缓冲区架构图 UART中断驱动与环形缓冲区架构 UART外设硬件 中断服务函数 ISR 主循环 / 应用任务 RXNE中断 数据就绪 环形缓冲区 D0 D1 D2 D3 D4 D5 D6 D7 写指针 (head) 读指针 (tail) 说明:ISR只操作写指针(head),主循环只操作读指针(tail)。 当head追上tail时,缓冲区满;当tail追上head时,缓冲区空。

4.6 常见问题与避坑指南

问题 原因 解决方案
数据丢失 缓冲区太小,ISR写入速度超过主循环读取速度 增大缓冲区,或优化主循环处理速度
数据错乱 ISR和主循环同时操作同一个变量,没有加volatile 检查所有共享变量是否加了volatile
中断嵌套导致死锁 高优先级中断中调用了环形缓冲区操作函数 ISR中只做最简单的数据搬运,复杂逻辑放主循环
缓冲区溢出未处理 没有检查ring_buffer_put的返回值 每次写入后检查返回值,溢出时做错误处理

我曾经踩过的坑:有一次调试一个GPS模块,数据总是断断续续。查了半天,发现是环形缓冲区大小设成了255,不是2的幂次方。位运算(head + 1) & (size - 1)在size=255时完全失效。改成256后一切正常。嗯,从此我记住了:缓冲区大小必须是2的幂次方。

4.7 小结

这一章我们聊了UART中断驱动的核心——环形缓冲区。说白了,就是用空间换时间,用中断换效率。你只要记住三个要点:

  • ISR只写,主循环只读——这是铁律
  • 缓冲区大小选2的幂次方——位运算效率高
  • 共享变量加volatile——防止编译器优化