裸机嵌入式系统中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




