裸机C语言环形缓冲区避坑:解决高优先级中断丢数据的3种可靠方案
裸机里高优先级中断往环形缓冲写、主循环读,常见丢数据不是缓冲太小一种原因:指针非原子被拆、满时静默覆盖、单字节中断太频繁被更高优先级任务堵住,都会丢。下面给三套可直接落地的方案。
一、单写单读SPSC:2幂缓冲+位与,尽量无锁
中断只做生产者、主循环只做消费者时,把缓冲区大小设为2的幂,用位与代替取模,head/tail声明volatile,32位指针在Cortex-M上单写单读可做到几乎无锁。
#define RB_SIZE 256u
#define RB_MASK (RB_SIZE-1u)
volatile uint8_t rb_buf[RB_SIZE];
volatile uint16_t rb_head; // 仅ISR写
volatile uint16_t rb_tail; // 仅主循环读改
void rb_isr_put(uint8_t d){
uint16_t next = (rb_head+1u)&RB_MASK;
if(next!=rb_tail){ // 有空位才写
rb_buf[rb_head]=d;
rb_head=next;
}else{
rb_overflow_cnt++; // 满:记溢出,不覆盖
}
}
int rb_get(uint8_t *d){
if(rb_tail==rb_head) return -1; // 空
*d=rb_buf[rb_tail];
rb_tail=(rb_tail+1u)&RB_MASK;
return 0;
}
主循环只调rb_get,不碰rb_head写逻辑;ISR只更新rb_head。SPSC下避免同时读写同一指针变量,可显著降低竞态。
二、多路/高优先级竞争:最小临界区+显式满策略
若CAN、UART多个中断都写同一缓冲,或主循环还要算可用空间,必须短关中断。只保护“判满—写—更指针”三行,绝不把解析放进去。满策略要明确:实时遥测丢最旧保最新,日志/命令丢新保完整。
uint8_t rb_put_safe(uint8_t d,int drop_old){
uint8_t ok=0;
__disable_irq();
uint16_t next=(rb_head+1u)&RB_MASK;
if(next!=rb_tail){
rb_buf[rb_head]=d; rb_head=next; ok=1;
}else if(drop_old){
rb_tail=(rb_tail+1u)&RB_MASK; // 丢最旧
rb_buf[rb_head]=d; rb_head=next; ok=1;
}
__enable_irq();
if(!ok) rb_overflow_cnt++;
return ok;
}
Cortex-M可用BASEPRI只屏蔽低优先级,保留真正高优先级中断;全局关中断只在极短区间用,避免把ADC、控制定时器饿死。
三、高速外设:DMA循环+IDLE/半满搬入二级环形
115200以上、多字节突发时,单字节RXNE中断会被高优先级任务挤压出ORE。改DMA循环写到内存大缓冲,用IDLE断帧或半传输/传输完成中断把数据搬进软件环形,CPU只在“一批”到达时介入。
uint8_t dma_buf[512];
// DMA Circular: UART->dma_buf;IDLE中断里算新写指针
void uart_idle_isr(void){
uint16_t left=DMA_GetCurrDataCounter(DMA1_Chx);
uint16_t w=512-left; // DMA当前写位置
while(rb_head!=w){ // 搬入二级环形
rb_isr_put(dma_buf[rb_dma_rd]);
rb_dma_rd=(rb_dma_rd+1u)&511u;
}
clear_idle_flag();
}
不定长用IDLE,定长大块用半满/全满双段搬;H7/F7开D-Cache要把dma_buf放Non-cacheable,避免CPU读到旧值。 DMA只解决“外设到内存”搬运,协议解析仍走环形缓冲,主循环按帧取。
选型小结
低速率、单中断源:方案一位与环形最轻;多中断或要算空闲空间:方案二短临界区;高速串口/CAN/ADC持续流:方案三DMA+环形。三者共同原则——head/tail用volatile、缓冲区2幂、满必须有策略、ISR只写不解析。按这个改,高优先级中断丢包可从偶发百分之几降到可观测、可统计、可回退。





