嵌入式系统中的环形缓冲区

一、为什么嵌入式系统需要环形缓冲区

有这么一个场景:

主控的一个串口,负责接收源源不断发过来的数据,而这份数据对系统运行十分重要,需要进行复杂的解析计算和处理。如果把数据放在ISR中处理,会产生两个问题:

1、中断回调函数长时间运行无法退出。复杂的逻辑计算会阻塞其它中断,导致系统实时性降低甚至崩溃。

2、造成数据丢失。如果主循环在忙着做其他事务处理,但新数据来了,主控来不及处理,也没地方存,那么数据就会丢失。

这两个问题是嵌入式开发中的经典问题。而环形缓冲区就是解决这两个问题的利器,它的作用就是解耦 。怎么理解解耦呢?其实就是解生产者消费者之间的耦!

1、生产者就像ISR,只需要把数据以最快速度扔进缓冲区。

2、消费者就像是主循环,有空时就从缓冲区取出数据做计算处理。

总结:它允许数据的写入和读取在时间上是不同的,异步的,避免数据丢失和系统阻塞。

二、环形缓冲区的工作原理

环形缓冲区本质上是一个线性数组,但我们通过逻辑上的操作,让它的"头"和"尾"相连,形成一个圆环。

核心构成:

数组buffer:实际存储数据的内存区域。

头指针head(也叫写指针):指向下一个写入数据的位置。

尾指针tail(也叫读指针):指向下一个读取数据的位置。

工作逻辑:

初始化时:head = tail = 0。

写入数据:将数据存入 head 指向的位置,head 向前移动一格。如果 head 到了数组末尾,它会自动跳回数组开头。

读取数据:读取 tail 指向的数据,tail 向前移动一格。同样,tail 到了末尾也会跳回开头。

环形缓冲区的**"环"**这一概念就来自于头指针和尾指针会重新回到开头,循环往复构成的一个环。

状态判断

这个是一个关键点,因为空闲状态和满状态的都符合head == tail 这一条件,于是为 了区分,通常会留一个存储单元不用,满的状态使用**"(head + 1) % size == tail"** 来判断。(这里的size指的是数组的总容量,即物理长度

如何理解这个"(head + 1) % size == tail"呢?我们定下一个规定:当缓冲区只剩下一个位置没被写入时,我们就认为它"满"了。永远不动最后那一个位置。这样满状态就定义就变成了:head的下一个位置就是tail。

牺牲这一个字节的意义在哪里呢?从算法与数据结构的角度上看,就是拿空间换时间。因为不使用这种方法的话,就需要引入一个计数变量Count,并在主循环和中断 并发的环境下,需要对其加锁维护,防止其被打断导致计算错误。而在嵌入式系统这 种有限资源的场景下,追求高效率,易维护,且head若只由一个生产者修改,tail只由一个消费者修改,牺牲这个一个字节的方法也便于在各类架构上的移植,提高效率。

但若是有多处写,多处读的情况,则必须加锁维护,如互斥量,保证系统运行安全。

三、代码逻辑实现

头文件:

cpp 复制代码
#ifndef _RING_BUFFER_H
#define _RING_BUFFER_H

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define RING_BUFFER_SIZE  128

typedef struct {
    uint8_t buffer[RING_BUFFER_SIZE];
    volatile uint16_t head; // 写入位置,使用volatile是为了防止被编译器优化
    volatile uint16_t tail; // 读取位置
} RingBuffer_t;

void rbuffer_Init(RingBuffer_t* rb);
bool rbuffer_Write(RingBuffer_t* rb, uint8_t data);
bool rbuffer_Read(RingBuffer_t* rb, uint8_t* data);
bool rbuffer_IsEmpty(RingBuffer_t* rb);
bool rbuffer_IsFull(RingBuffer_t* rb);
uint16_t rbuffer_GetCount(RingBuffer_t* rb);

#endif

源文件:

cpp 复制代码
#include "ring_buffer.h"

//环形缓冲区初始化
void rbuffer_Init(RingBuffer_t* rb) {
    rb->head = 0;
    rb->tail = 0;
    memset(rb->buffer, 0, RING_BUFFER_SIZE);
}

//环形缓冲区是否满
bool rbuffer_IsFull(RingBuffer_t* rb) {
    // 判满逻辑:Head的下一个位置是Tail
    return ((rb->head + 1) % RING_BUFFER_SIZE) == rb->tail;
}

//环形缓冲区是否空
bool rbuffer_IsEmpty(RingBuffer_t* rb) {
    return rb->head == rb->tail;
}

//数据写入环形缓冲区
bool rbuffer_Write(RingBuffer_t* rb, uint8_t data) {
    if (rbuffer_IsFull(rb)) {
        return false; // 缓冲区满,写入失败
    }
    
    rb->buffer[rb->head] = data;
    rb->head = (rb->head + 1) % RING_BUFFER_SIZE; // 索引回绕
    return true;
}

//从环形缓冲区读数据
bool rbuffer_Read(RingBuffer_t* rb, uint8_t* data) {
    if (rbuffer_IsEmpty(rb)) {
        return false; // 缓冲区空,读取失败
    }
    
    *data = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) % RING_BUFFER_SIZE; // 索引回绕
    return true;
}

//环形缓冲区中的数据个数
uint16_t rbuffer_GetCount(RingBuffer_t* rb) {
    if (rb->head >= rb->tail) {
        return rb->head - rb->tail;
    } else {
        return RING_BUFFER_SIZE - (rb->tail - rb->head);
    }
}

代码都是很简单的逻辑,没有什么难以理解的地方。只需注意两点,一个是head和tail必须使用volatile修饰,告诉编译器我可能会被中断随时修改,不要优化我。

四、实际案例

结合实际场景体现环形缓冲区的作用:代码都是伪代码,只有思路。

1、UART 通信(中断接收,主循环处理)

cpp 复制代码
RingBuffer_t uart_rb;

/* 生产者:UART接收中断 (ISR) */
void UART_ISR(void) {
    // 硬件标志位判断
    uint8_t ch = UART_GetByte(); 
    
    // 关键点:快速写入,不做复杂逻辑,防止阻塞其他中断
    rbuffer_Write(&uart_rb, ch); 
}

/* 消费者:主循环 */
void Main_Task(void) {
    uint8_t data;
    while (1) {
        // 检查缓冲区是否有数据
        if (!rbuffer_IsEmpty(&uart_rb)) {
            // 逐字节取出
            if (rbuffer_Read(&uart_rb, &data)) {
                // 进入解析数据
                Protocol_data(data);
            }
        }
    }
}

2、传感器采集(ADC高速采样,后台低速计算)

cpp 复制代码
bool rbuffer_Write_16Bit(RingBuffer* rb, uint16_t data) {
    // 检查剩余空间是否足够存2个字节
    // Size - 1 是最大可用容量,GetCount是已用空间
    uint16_t free_space = (RING_BUFFER_SIZE - 1) - RB_GetCount(rb);
    
    if (free_space < 2) {
        return false; // 空间不够,写入失败
    }

    // 小端模式:先存低位
    uint8_t low_byte = data & 0xFF;
    uint8_t high_byte = (data >> 8) & 0xFF;

    // 复用基础的 Write 函数
    RB_Write(rb, low_byte);
    RB_Write(rb, high_byte);

    return true;
}

bool rbuffer_Read_16Bit(RingBuffer* rb, uint16_t* data) {
    // 检查是否有足够的数据(至少2个字节)
    if (RB_GetCount(rb) < 2) {
        return false; // 数据不足
    }

    uint8_t low_byte, high_byte;

    // 依次读出
    RB_Read(rb, &low_byte);
    RB_Read(rb, &high_byte);

    // 组合数据
    *data = (uint16_t)low_byte | ((uint16_t)high_byte << 8);

    return true;
}
cpp 复制代码
#define BATCH_SIZE 50  // 每次处理50个采样点

RingBuffer adc_rb;

/*  生产者:定时器/ADC中断*/
void Timer_ADC_IRQHandler(void) {
    uint16_t adc_val = ADC_Read();
    rbuffer_Write_16Bit(&adc_rb, adc_val); 
}

/* 消费者:后台算法任务 */
void Algorithm_Task(void) {
    uint16_t samples[BATCH_SIZE];
    
    while(1) {
        // 只有当缓冲区里的数据够攒够一波时,才开始计算
        if (rbuffer_GetCount(&adc_rb) >= BATCH_SIZE) {
            
            // 批量读取
            for(int i=0; i<BATCH_SIZE; i++) {
                rbuffer_Read_16Bit(&adc_rb, &samples[i]);
            }
            
            // 滤波,计算求均值
            averege(samples, BATCH_SIZE);
        }
    }
}

3、 DMA 使用(双缓冲区)

这是环形缓冲区的硬件形态。使用 DMA 的"半传输中断"和"传输完成中断",将一个线性数组在逻辑上切分为"前半段"和"后半段",形成一个由硬件自动维护的 2 级环形缓冲。

cpp 复制代码
/* 定义双倍长度的缓冲区 */
#define CHUNK_SIZE  512
uint8_t dma_buffer[CHUNK_SIZE * 2]; 

/* 初始化 DMA,开启循环模式 (Circular Mode) */
void DMA_Audio_Init() {
    // 配置DMA源为I2S/ADC,目标为 dma_buffer,长度为 CHUNK_SIZE * 2
    HAL_DMA_Start_IT(&hdma_i2s_rx, (uint32_t)&I2S_DR, (uint32_t)dma_buffer, CHUNK_SIZE * 2);
}

/* 半传输完成中断 (Half Transfer) - DMA填满了前半段 */
void HAL_DMA_RxHalfCpltCallback(DMA_HandleTypeDef *hdma) {
    // 此时硬件在写后半段,CPU安全地处理前半段
    Process_Audio_Data(&dma_buffer[0], CHUNK_SIZE);
}

/* 传输完成中断 (Transfer Complete) - DMA填满了后半段,并回绕到开头 */
void HAL_DMA_RxCpltCallback(DMA_HandleTypeDef *hdma) {
    // 此时硬件回绕去写前半段,CPU安全地处理后半段
    Process_Audio_Data(&dma_buffer[CHUNK_SIZE], CHUNK_SIZE);
}

四、总结

环形缓冲区一句话总结就是类似一个蓄水池,起到削峰填谷的作用。使用时,要注意保护生产者和消费者的原子性和不要忘了加上volatile修饰变量。

相关推荐
大聪明-PLUS16 小时前
在 C++ 中开发接口类
linux·嵌入式·arm·smarc
s1ckrain18 小时前
数字逻辑笔记—组合逻辑电路
笔记·fpga开发·嵌入式
挽天java1 天前
智能终端开发文档
嵌入式
brave and determined1 天前
传感器学习(day02)毫米波雷达:重塑人机交互新维度
单片机·嵌入式硬件·嵌入式·人机交互·传感器·毫米波·嵌入式设计
十五年专注C++开发1 天前
嵌入式软件架构设计浅谈
c语言·c++·单片机·嵌入式
一枝小雨1 天前
9 更进一步的 bootloader 架构设计
stm32·单片机·嵌入式·软件架构·ota·bootloader·aes加密
CS_Zero1 天前
无人机数据链路聚合入门笔记
嵌入式·无人机
Embedded-Xin2 天前
Linux架构优化——spdlog实现压缩及异步写日志
android·linux·服务器·c++·架构·嵌入式
一枝小雨2 天前
7 App代码转AES加密文件生成步骤
stm32·单片机·嵌入式·aes·ota·bootloader·加密升级