嵌入式C语言环形缓冲区避坑指南:中断与主循环数据同步的3种可靠方案
环形缓冲区(Ring Buffer)是嵌入式系统中中断与主循环数据传递的经典结构——UART接收、ADC采样、SPI数据流都离不开它。然而,很多工程师在实现时踩过同一个坑:中断往缓冲区写数据的同时,主循环正在读,导致读写指针错乱,数据丢失或重复。本文总结三种经过工业验证的同步方案,从简单到高效,适用不同场景。
一、关中断法(临界区保护)
最朴素的方法:在读或写操作前后关中断/开中断,保证操作的原子性。
#define BUF_SIZE 64
uint8_t ring_buf[BUF_SIZE];
volatile uint16_t head = 0, tail = 0;
// 中断中写入(生产者)
void ISR_put(uint8_t data) {
uint16_t next = (head + 1) % BUF_SIZE;
if (next != tail) { // 不满
ring_buf[head] = data;
head = next;
}
}
// 主循环读取(消费者)
uint8_t main_get(uint8_t *data) {
uint8_t ret = 0;
__disable_irq();
if (head != tail) {
*data = ring_buf[tail];
tail = (tail + 1) % BUF_SIZE;
ret = 1;
}
__enable_irq();
return ret;
}
优点:实现简单,逻辑直观。缺点:关中断时间过长会影响系统实时性,尤其在高频中断(如1Msps ADC)中不可接受。
二、无锁原子操作(单生产者单消费者)
如果只有一个中断写入、一个主循环读取(单生产者单消费者),可以利用原子变量避免关中断。关键在于读写指针各自独立,且操作可被原子化。
// 使用C11原子操作或编译器内置函数
#include <stdatomic.h>
atomic_uint16_t head_atomic = 0;
atomic_uint16_t tail_atomic = 0;
void ISR_put(uint8_t data) {
uint16_t h = atomic_load(&head_atomic);
uint16_t t = atomic_load(&tail_atomic);
uint16_t next = (h + 1) % BUF_SIZE;
if (next != t) {
ring_buf[h] = data;
atomic_store(&head_atomic, next); // 最后更新head
}
}
uint8_t main_get(uint8_t *data) {
uint16_t h = atomic_load(&head_atomic);
uint16_t t = atomic_load(&tail_atomic);
if (h != t) {
*data = ring_buf[t];
atomic_store(&tail_atomic, (t + 1) % BUF_SIZE);
return 1;
}
return 0;
}
关键:写者先写数据再更新head,读者先读数据再更新tail,保证数据可见性。由于Cortex-M的32位读写本身就是原子的(对齐访问),甚至可以不用原子库,直接用volatile修饰指针变量。但为了可移植性,建议使用atomic_uint16_t。
优点:零中断封锁,性能最优。缺点:仅适用于单生产者单消费者;多中断源同时写仍需保护。
三、双缓冲(乒乓缓冲)
当数据量较大(如音频帧、图像行)时,逐个元素的环形缓冲区效率低,且中断处理时间过长。双缓冲方案让中断填充一个缓冲区,主循环处理另一个,写满后交换。
#define FRAME_SIZE 256
uint8_t buf0[FRAME_SIZE];
uint8_t buf1[FRAME_SIZE];
volatile uint8_t writing_to = 0; // 0: buf0, 1: buf1
volatile uint8_t ready = 0; // 主循环可读的标志
uint16_t idx = 0;
void ISR_put_sample(uint8_t data) {
if (writing_to == 0) {
buf0[idx++] = data;
} else {
buf1[idx++] = data;
}
if (idx >= FRAME_SIZE) {
idx = 0;
writing_to ^= 1; // 切换缓冲区
ready = 1; // 通知主循环
}
}
void main_process(void) {
if (ready) {
uint8_t *data = (writing_to == 0) ? buf1 : buf0; // 上一帧数据
process_frame(data, FRAME_SIZE);
ready = 0;
}
}
优点:中断内只做简单的数组写入,无指针竞争;适合批量数据处理。缺点:需要双倍内存;切换瞬间可能有短暂的不一致(但可通过ready标志规避)。
四、三个翻车高发点
缓冲区大小是2的幂却不使用位与运算:很多人用% BUF_SIZE取模,但除法指令在MCU上很慢。如果BUF_SIZE是2的幂,用& (BUF_SIZE-1)代替,速度提升数倍。
忘记volatile:读写指针若不声明为volatile,编译器优化可能导致主循环永远看不到中断更新的值。
多中断源共用同一缓冲区:两个不同优先级的中断同时写,即使关中断也无法保证原子性(因为高优先级中断可以打断低优先级中断)。此时需要每个中断独立缓冲区,或使用硬件FIFO。





