You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

裸机嵌入式系统中UART中断式数据发送的优雅实现问询

优雅解决裸机UART发送中断驱动的耦合问题

这确实是裸机中断驱动嵌入式系统里的经典痛点——UART接收端的环形缓冲+字节中断模式简直是“丝滑”:ISR把数据丢进环形缓冲就完事,发送方停了中断自动歇,完全不用缓冲或者消费代码操心。但发送端这块,总像卡了个小疙瘩,要么让业务代码去碰硬件寄存器开中断,要么搞轮询,怎么看都不够干净。

我之前在做STM32和NXP的裸机项目时,摸索出一套封装解耦+原子状态检查的方案,完美解决了这个问题,分享给你:

核心思路:把硬件操作完全封装进缓冲类

核心就是让环形缓冲类自己管理UART发送中断的开启/关闭,外部写数据的代码只需要调用缓冲的写入方法,完全不用感知UART的存在。具体分三步:

1. 封装带硬件控制的发送缓冲类

用C++把环形缓冲、UART寄存器操作、中断状态管理打包成一个类,对外只暴露push()(写入待发送数据)和is_full()(检查缓冲是否满)这类接口。

示例代码大概是这样:

#include <cstdint>

class UartTxBuffer {
private:
    static constexpr size_t BUFFER_SIZE = 64;
    uint8_t buffer[BUFFER_SIZE];
    volatile size_t head = 0;    // 下一个写入位置
    volatile size_t tail = 0;    // 下一个发送位置
    volatile bool tx_interrupt_enabled = false;

    // 私有方法:直接操作UART硬件
    void enable_tx_interrupt() {
        // 这里写开启UART发送中断的寄存器操作,比如STM32的USART_CR1_TXEIE置1
        USART1->CR1 |= USART_CR1_TXEIE;
        tx_interrupt_enabled = true;
    }

    void disable_tx_interrupt() {
        // 关闭UART发送中断的寄存器操作
        USART1->CR1 &= ~USART_CR1_TXEIE;
        tx_interrupt_enabled = false;
    }

    // 检查缓冲是否为空
    bool is_empty() const {
        return head == tail;
    }

public:
    // 写入数据到缓冲,返回是否成功
    bool push(uint8_t data) {
        // 计算下一个写入位置
        size_t next_head = (head + 1) % BUFFER_SIZE;
        if (next_head == tail) {
            // 缓冲满了,写入失败
            return false;
        }

        // 进入临界区,防止ISR同时操作head
        __disable_irq();
        buffer[head] = data;
        head = next_head;
        __enable_irq();

        // 如果发送中断没开,现在开启它
        if (!tx_interrupt_enabled) {
            enable_tx_interrupt();
        }

        return true;
    }

    // 供ISR调用:获取下一个要发送的字节
    bool get_next(uint8_t& data) {
        if (is_empty()) {
            // 缓冲空了,关闭中断
            disable_tx_interrupt();
            return false;
        }

        // 进入临界区,防止主程序同时操作tail
        __disable_irq();
        data = buffer[tail];
        tail = (tail + 1) % BUFFER_SIZE;
        __enable_irq();

        return true;
    }
};

// 全局实例(裸机里常用,或者用单例)
UartTxBuffer uart1_tx_buffer;

2. 编写UART发送中断服务程序

ISR只需要调用缓冲类的get_next()方法,拿到数据就写UART发送寄存器,空了就自动关闭中断:

extern "C" void USART1_IRQHandler() {
    if (USART1->SR & USART_SR_TXE) {  // 检查发送寄存器空标志
        uint8_t data;
        if (uart1_tx_buffer.get_next(data)) {
            USART1->DR = data;  // 发送下一个字节
        }
    }
}

3. 业务代码写入数据

不管是主循环里的逻辑,还是其他中断里的代码,只需要调用push()就完事,完全不用管UART中断的状态:

void some_business_logic() {
    // 要发送的数据
    uint8_t msg[] = "Hello, UART!";
    for (size_t i = 0; msg[i] != '\0'; ++i) {
        // 直到写入成功(或者可以加超时处理)
        while (!uart1_tx_buffer.push(msg[i]));
    }
}

为什么这个方案优雅?

  • 完全解耦:业务代码和UART硬件细节彻底分离,不用在写数据的地方写USART1->CR1 |= USART_CR1_TXEIE这种硬件相关代码,耦合度极低。
  • 无额外开销:只有在写入数据且中断未开启的时候才会触发中断开启操作,没有轮询的CPU开销和延迟。
  • 竞态安全:通过__disable_irq()/__enable_irq()保护缓冲的head和tail操作,避免主程序和ISR同时操作导致的异常。

注意事项

  • 临界区的代码要尽可能短,这里只操作几个变量,不会影响中断响应时间。如果是更复杂的操作,可以考虑用硬件原子指令(比如ARM的LDREX/STREX)来替代关中断,进一步减少中断延迟。
  • 缓冲大小要根据实际场景调整,避免频繁出现缓冲满的情况,必要时可以在push()里添加回调或者状态标志,通知上层代码缓冲已满。
  • 如果是多中断上下文写入缓冲,要确保push()的线程安全(这里已经用关中断处理了,裸机里足够)。

内容的提问来源于stack exchange,提问作者Martin Irvine

火山引擎 最新活动