数据结构小白一小时手搓环形缓冲区

什么是环形缓冲区

环形缓冲区是一种非常高效且常用的数据结构,特别适用于需要处理数据流的场景。它通过循环利用固定大小的内存空间来实现数据的缓存和传输,避免了频繁的内存分配和释放,提高了系统性能和实时性。理解其工作原理和优缺点,可以帮助开发者更好地选择和使用这种数据结构。

环形缓冲区,也称为循环缓冲区(Circular Buffer)、环形队列(Ring Buffer)或循环队列(Circular Queue),是一种固定大小首尾相连的数据结构,可以把它想象成一个时钟表盘或一个首尾相接的圆环。

核心概念

  1. 固定大小: 环形缓冲区在创建时就确定了大小,并且这个大小通常不会改变。这意味着它可以使用的内存空间是预先分配好的,不会像一些动态数据结构(链表、栈、队列、树、图、堆)那样随着数据量的增加而扩张。

  2. 首尾相连: 这是环形缓冲区的关键特征。当数据被添加到缓冲区的“尾部”并且缓冲区已满时,下一个数据会覆盖掉“头部”的数据,就好像缓冲区是一个环一样。类似地,当从“头部”读取数据并且缓冲区为空时,读取操作会从“尾部”继续,形成一个循环。

  3. 读写指针: 环形缓冲区通常使用两个指针来管理数据的读写:

    • 写指针(Write Pointer)/ 头指针 (Head Pointer): 指向下一个可写入数据的位置。
    • 读指针(Read Pointer)/ 尾指针 (Tail Pointer): 指向下一个要读取的数据的位置。

工作原理

  • 写入数据: 当生产者向缓冲区写入数据时,数据会被放置在写指针指向的位置,然后写指针向前移动。
  • 读取数据: 当消费者从缓冲区读取数据时,数据会从读指针指向的位置被取出,然后读指针向前移动。
  • 缓冲区满: 当写指针追上读指针时(考虑循环的因素),表示缓冲区已满。在这种情况下,通常有两种策略:
    • 阻塞写入: 生产者线程/进程被阻塞,直到缓冲区中有空间可用。
    • 覆盖数据: 新的数据会覆盖掉缓冲区中最旧的数据(根据具体应用场景,这可能是可接受的)。
  • 缓冲区空: 当读指针追上写指针时(考虑循环的因素),表示缓冲区为空。在这种情况下,消费者线程/进程通常会被阻塞,直到缓冲区中有数据可用。

直观理解(以时钟为例):

假设一个环形缓冲区的大小是 12 个字节,就像一个有 12 个刻度的时钟。

  • 初始状态: 读指针和写指针都指向 12 点钟位置。
  • 写入数据: 每次写入一个字节,写指针就顺时针移动一个刻度。例如,写入 5 个字节后,写指针指向 5 点钟位置,读指针仍在 12 点钟位置。
  • 读取数据: 每次读取一个字节,读指针也顺时针移动一个刻度。例如,读取 3 个字节后,读指针指向 3 点钟位置,写指针在 5 点钟位置。
  • 缓冲区满: 继续写入数据,直到写指针再次回到 3 点钟位置(追上读指针),此时缓冲区已满。
  • 缓冲区空: 如果继续读取数据,直到读指针再次回到 5 点钟位置(追上写指针),此时缓冲区为空。

优缺点

优点
  • 高效: 环形缓冲区避免了频繁的内存分配和释放,因为它的内存空间是预先分配好的。数据的写入和读取只需要移动指针,不需要移动数据本身,这使得它非常高效,特别是在数据传输速率很高的情况下。
  • 有界: 由于大小固定,环形缓冲区的内存使用是有界的,这对于内存受限的嵌入式系统非常重要。
  • 适用于生产者-消费者模式: 环形缓冲区非常适合用于生产者-消费者模式,例如在多线程编程中,一个线程产生数据,另一个线程消费数据。
缺点
  • 大小固定: 一旦创建,环形缓冲区的大小就不能改变,这在某些应用场景中可能不够灵活。
  • 数据覆盖: 如果缓冲区已满并且采用了覆盖数据的策略,那么可能会丢失旧的数据。
  • 需要处理同步问题: 在多线程环境下,需要使用适当的同步机制(如互斥锁或信号量)来保护环形缓冲区,避免多个线程同时读写导致数据错乱。

应用场景

环形缓冲区在各种计算机系统和应用中都有广泛的应用,特别是在需要高效处理数据流的场景中:

  • 中断处理: 在操作系统中,中断服务程序(ISR)可以使用环形缓冲区来存储中断事件,然后由内核的其他部分来处理这些事件。
  • 设备驱动: 设备驱动程序可以使用环形缓冲区来缓存从设备接收到的数据或要发送到设备的数据。
  • 网络通信: 网络协议栈可以使用环形缓冲区来处理网络数据包。
  • 音频和视频处理: 环形缓冲区可以用来缓冲音频或视频数据流,实现平滑的播放。
  • 实时系统: 在实时系统中,环形缓冲区可以用来保证数据的及时处理,避免数据丢失或延迟。
  • 日志记录: 应用程序可以使用环形缓冲区来存储日志信息,当缓冲区满时,可以覆盖旧的日志或将日志写入文件。

RT-Thread 的环形缓冲区

RT-Thread 的环形缓冲区(Ring Buffer)是一种非常高效且常用的数据结构,特别适合在嵌入式系统中的各种场景,例如 中断处理、设备驱动、通信协议 等。它通过循环利用有限的内存空间来实现数据的缓存和传输,避免了频繁的内存分配和释放,提高了系统性能和实时性。

以下是对 RT-Thread 环形缓冲区的详细介绍:

基本概念

  • 环形结构: 环形缓冲区本质上是一个固定大小的数组,但其首尾相连,形成一个逻辑上的环状结构。
  • 读写指针: 环形缓冲区使用两个指针来管理数据的读写:
    • 读指针 (Read Pointer): 指向下一个要读取的数据的位置。
    • 写指针 (Write Pointer): 指向下一个要写入数据的位置。
  • 数据存取: 当写指针追上读指针时,表示缓冲区已满;当读指针追上写指针时,表示缓冲区为空。

RT-Thread 中的实现

RT-Thread 提供了 rt_ringbuffer 结构体来定义环形缓冲区,其定义位于 ringbuffer.h 头文件中:

struct rt_ringbuffer
{
    rt_uint8_t *buffer_ptr;     /* 缓冲区指针 */
    rt_uint16_t read_index,  /* 读指针索引 */
                write_index; /* 写指针索引 */
    rt_uint16_t buffer_size; /* 缓冲区大小 */
    rt_uint16_t read_mirror, /* 读指针镜像标志 */
                write_mirror;/* 写指针镜像标志 */
};
  • buffer_ptr 指向实际存储数据的内存区域。
  • read_index 读指针的索引,指向下一个要读取数据的位置。
  • write_index 写指针的索引,指向下一个要写入数据的位置。
  • buffer_size 缓冲区的总大小,以字节为单位。
  • read_mirrorwrite_mirror 镜像标志位,用于判断指针是否跨越了缓冲区边界。当指针索引达到缓冲区大小的一半时,镜像标志位会翻转。

RT-Thread 提供的 API

RT-Thread 提供了一系列函数来操作环形缓冲区,这些函数都定义在 ringbuffer.hringbuffer.c 文件中:

  • rt_ringbuffer_init() 初始化环形缓冲区,设置缓冲区大小和指针。
  • rt_ringbuffer_reset(): 重置环形缓冲区,清空数据并复位读写指针。
  • rt_ringbuffer_put() 向环形缓冲区中写入数据。
  • rt_ringbuffer_putchar(): 向环形缓冲区中写入一个字节的数据。
  • rt_ringbuffer_putchar_force(): 强制向环形缓冲区写入一个字节的数据,即使缓冲区已满,也会覆盖旧数据。
  • rt_ringbuffer_get() 从环形缓冲区中读取数据。
  • rt_ringbuffer_getchar(): 从环形缓冲区中读取一个字节的数据。
  • rt_ringbuffer_destroy(): 销毁环形缓冲区,释放内存(如果内存是动态分配的)。
  • rt_ringbuffer_space_len(): 获取环形缓冲区中剩余的可用空间。
  • rt_ringbuffer_data_len() 获取环形缓冲区中已存储的数据长度。

环形缓冲区的优点

  • 高效的数据缓存: 循环利用固定大小的内存,避免了频繁的内存分配和释放。
  • 快速的数据访问: 读写操作只需要移动指针,无需进行数据拷贝。
  • 线程安全: 可以通过适当的同步机制(例如互斥锁或信号量)实现线程安全的访问。
  • 适用于异步操作: 生产者和消费者可以异步地访问环形缓冲区,提高系统效率。

环形缓冲区的缺点

  • 固定大小: 环形缓冲区的大小是固定的,不能动态调整。
  • 数据覆盖: 在强制写入模式下,可能会覆盖旧数据,需要谨慎使用。

使用场景

  • 中断服务程序 (ISR) 与任务之间的数据交换: ISR 可以将数据快速写入环形缓冲区,任务在需要时读取数据。
  • 串口通信: 接收到的串口数据可以先缓存到环形缓冲区中,然后由任务处理。
  • 音频/视频数据流处理: 环形缓冲区可以作为音频或视频数据的临时缓存。
  • 传感器数据采集: 将传感器采集到的数据存储到环形缓冲区中,供后续分析处理。

使用示例

以下是一个简单的使用 RT-Thread 环形缓冲区(调用RTT的API)的示例:

#include <rtthread.h>
#include <rtdevice.h>

#define RING_BUFFER_SIZE 128

static struct rt_ringbuffer rx_ringbuffer;
static rt_uint8_t rx_buffer[RING_BUFFER_SIZE];

int main(void)
{
    // 初始化环形缓冲区
    rt_ringbuffer_init(&rx_ringbuffer, rx_buffer, RING_BUFFER_SIZE);

    // 模拟写入数据
    rt_uint8_t data[] = "Hello, RT-Thread!!!";
    rt_ringbuffer_put(&rx_ringbuffer, data, sizeof(data));

    // 模拟读取数据
    rt_uint8_t read_data[RING_BUFFER_SIZE];
    rt_size_t len = rt_ringbuffer_get(&rx_ringbuffer, read_data, sizeof(read_data));

    // 打印读取到的数据
    rt_kprintf("Read data: %.*s\n", len, read_data);

    return 0;
}

实验现象

image-20241207185908231

写一个自己的环形缓冲区

以下我们仿照 RT-Thread 环形缓冲区实现的简易环形缓冲区。这个简易版本主要实现了基本的功能,例如初始化、写入数据、读取数据、获取数据长度和剩余空间。为了简化,没有实现镜像标志位 (read_mirrorwrite_mirror),也没有考虑线程安全问题。在实际应用中,可能需要根据具体需求添加更多功能和错误处理机制。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义环形缓冲区结构体
typedef struct
{
    unsigned char *buffer;  // 缓冲区指针
    unsigned int   size;    // 缓冲区大小
    unsigned int   read;   // 读指针索引
    unsigned int   write;  // 写指针索引
} MyRingBuffer;

// 初始化环形缓冲区
void my_ringbuffer_init(MyRingBuffer *rb, unsigned char *buffer, unsigned int size)
{
    rb->buffer = buffer;
    rb->size = size;
    rb->read = 0;
    rb->write = 0;
}

// 向环形缓冲区写入数据
unsigned int my_ringbuffer_put(MyRingBuffer *rb, const unsigned char *data, unsigned int length)
{
    unsigned int i;
    for (i = 0; i < length; i++)
    {
        // 检查缓冲区是否已满
        if (((rb->write + 1) % rb->size) == rb->read)
        {
            // 缓冲区已满,返回已写入的字节数
            return i;
        }

        rb->buffer[rb->write] = data[i];
        rb->write = (rb->write + 1) % rb->size;
    }
    return length;
}

// 从环形缓冲区读取数据
unsigned int my_ringbuffer_get(MyRingBuffer *rb, unsigned char *data, unsigned int length)
{
    unsigned int i;
    for (i = 0; i < length; i++)
    {
        // 检查缓冲区是否为空
        if (rb->read == rb->write)
        {
            // 缓冲区为空,返回已读取的字节数
            return i;
        }

        data[i] = rb->buffer[rb->read];
        rb->read = (rb->read + 1) % rb->size;
    }
    return length;
}

// 获取环形缓冲区中已存储的数据长度
unsigned int my_ringbuffer_data_len(const MyRingBuffer *rb)
{
    return (rb->write - rb->read + rb->size) % rb->size;
}

// 获取环形缓冲区中剩余的可用空间
unsigned int my_ringbuffer_space_len(const MyRingBuffer *rb)
{
    return rb->size - 1 - my_ringbuffer_data_len(rb);
}

int main()
{
    // 定义缓冲区大小
    #define BUFFER_SIZE 10

    // 定义缓冲区
    unsigned char buffer[BUFFER_SIZE];

    // 定义环形缓冲区结构体变量
    MyRingBuffer rb;

    // 初始化环形缓冲区
    my_ringbuffer_init(&rb, buffer, BUFFER_SIZE);

    // 测试写入数据
    char *test_data = "abcdefghijklmn";
    unsigned int put_len = my_ringbuffer_put(&rb, (unsigned char *)test_data, strlen(test_data));
    printf("Put %u bytes: %s\n", put_len, test_data);
    printf("Data length: %u\n", my_ringbuffer_data_len(&rb));
    printf("Space length: %u\n", my_ringbuffer_space_len(&rb));
    
    // 测试读取数据
    unsigned char read_data[BUFFER_SIZE];
    unsigned int get_len = my_ringbuffer_get(&rb, read_data, 5);
    read_data[get_len] = '\0'; // 添加字符串结束符
    printf("Get %u bytes: %s\n", get_len, read_data);
    printf("Data length: %u\n", my_ringbuffer_data_len(&rb));
    printf("Space length: %u\n", my_ringbuffer_space_len(&rb));

    // 测试再次写入数据
    put_len = my_ringbuffer_put(&rb, (unsigned char *)"123", 3);
    printf("Put %u bytes: %s\n", put_len, "123");
    printf("Data length: %u\n", my_ringbuffer_data_len(&rb));
    printf("Space length: %u\n", my_ringbuffer_space_len(&rb));

    // 测试读取剩余数据
    get_len = my_ringbuffer_get(&rb, read_data, my_ringbuffer_data_len(&rb));
    read_data[get_len] = '\0'; // 添加字符串结束符
    printf("Get %u bytes: %s\n", get_len, read_data);
    printf("Data length: %u\n", my_ringbuffer_data_len(&rb));
    printf("Space length: %u\n", my_ringbuffer_space_len(&rb));

    return 0;
}

代码解释:

  • MyRingBuffer 结构体: 定义了环形缓冲区的结构,包括缓冲区指针、大小、读指针和写指针。
  • my_ringbuffer_init() 函数: 初始化环形缓冲区,设置缓冲区指针、大小,并初始化读写指针为 0。
  • my_ringbuffer_put() 函数: 向环形缓冲区写入数据。它逐字节写入数据,并在每次写入后检查缓冲区是否已满。如果缓冲区已满,则返回实际写入的字节数。
  • my_ringbuffer_get() 函数: 从环形缓冲区读取数据。它逐字节读取数据,并在每次读取后检查缓冲区是否为空。如果缓冲区为空,则返回实际读取的字节数。
  • my_ringbuffer_data_len() 函数: 计算环形缓冲区中已存储的数据长度。
  • my_ringbuffer_space_len() 函数: 计算环形缓冲区中剩余的可用空间。

实验现象:

image-20241207185744588

需要注意的点:

  • 缓冲区大小: 在这个简易版本中,实际可用的缓冲区大小比定义的 size 少 1,这是为了区分缓冲区为空和缓冲区已满的情况。
  • 线程安全: 这个版本没有考虑多线程访问的情况,如果在多线程环境下使用,需要添加互斥锁等同步机制来保护环形缓冲区。
  • 错误处理: 这个版本没有进行详细的错误处理,例如传入空指针或无效的长度等情况。在实际应用中,需要添加适当的错误处理机制。
  • 数据覆盖: 这个版本实现了在缓冲区满时停止写入的策略。也可以根据需要修改 my_ringbuffer_put 函数来实现数据覆盖的策略。

在串口中的应用

以STM32主控为例。

主要代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "uart_app.h" // 假设这个头文件包含了 HAL 库相关的定义和声明

//----------------- 环形缓冲区代码 -----------------

// 定义环形缓冲区结构体
typedef struct
{
    unsigned char *buffer;  // 缓冲区指针
    unsigned int   size;    // 缓冲区大小
    unsigned int   read;   // 读指针索引
    unsigned int   write;  // 写指针索引
} MyRingBuffer;

// 初始化环形缓冲区
void my_ringbuffer_init(MyRingBuffer *rb, unsigned char *buffer, unsigned int size)
{
    rb->buffer = buffer;
    rb->size = size;
    rb->read = 0;
    rb->write = 0;
}

// 向环形缓冲区写入数据
unsigned int my_ringbuffer_put(MyRingBuffer *rb, const unsigned char *data, unsigned int length)
{
    unsigned int i;
    for (i = 0; i < length; i++)
    {
        // 检查缓冲区是否已满
        if (((rb->write + 1) % rb->size) == rb->read)
        {
            // 缓冲区已满,返回已写入的字节数
            return i;
        }

        rb->buffer[rb->write] = data[i];
        rb->write = (rb->write + 1) % rb->size;
    }
    return length;
}

// 从环形缓冲区读取数据
unsigned int my_ringbuffer_get(MyRingBuffer *rb, unsigned char *data, unsigned int length)
{
    unsigned int i;
    for (i = 0; i < length; i++)
    {
        // 检查缓冲区是否为空
        if (rb->read == rb->write)
        {
            // 缓冲区为空,返回已读取的字节数
            return i;
        }

        data[i] = rb->buffer[rb->read];
        rb->read = (rb->read + 1) % rb->size;
    }
    return length;
}

// 获取环形缓冲区中已存储的数据长度
unsigned int my_ringbuffer_data_len(const MyRingBuffer *rb)
{
    return (rb->write - rb->read + rb->size) % rb->size;
}

// 获取环形缓冲区中剩余的可用空间
unsigned int my_ringbuffer_space_len(const MyRingBuffer *rb)
{
    return rb->size - 1 - my_ringbuffer_data_len(rb);
}

// 判断环形缓冲区是否为空
int my_ringbuffer_is_empty(const MyRingBuffer *rb)
{
    return rb->read == rb->write;
}

// 判断环形缓冲区是否已满
int my_ringbuffer_is_full(const MyRingBuffer *rb)
{
    return ((rb->write + 1) % rb->size) == rb->read;
}

//----------------- 串口应用代码 -----------------

#define RING_BUFFER_SIZE 256
#define READ_BUFFER_SIZE 64

// 定义环形缓冲区和接收缓冲区
MyRingBuffer my_rb;
uint8_t usart_ring_buffer[RING_BUFFER_SIZE];
uint8_t usart_read_buffer[READ_BUFFER_SIZE];

// 假设这是你的 UART 接收 DMA 缓冲区
uint8_t uart_rx_dma_buffer[64];

// UART 接收超时时间
uint32_t uart_rx_ticks = 0;

// 串口初始化函数 (你需要根据你的硬件平台实现)
void ringbuffer_init(void)
{
    // ... 初始化串口硬件,例如波特率、数据位、停止位等 ...

    // 初始化环形缓冲区
    my_ringbuffer_init(&my_rb, usart_ring_buffer, RING_BUFFER_SIZE);

    // ... 启动 DMA 接收 (这里只是一个示例,你需要根据你的 HAL 库进行修改) ...
    // HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
}

/**

 * @brief UART DMA接收完成回调函数
 * 将接收到的数据写入环形缓冲区,并清空DMA缓冲区
 * @param huart UART句柄
 * @param Size 接收到的数据大小
 * @retval None
    **/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    // 将 DMA 缓冲区中的数据写入环形缓冲区
    my_ringbuffer_put(&my_rb, uart_rx_dma_buffer, Size);

    // 清空 DMA 缓冲区
    memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));

    // 重新启动 DMA 接收
    HAL_UARTEx_ReceiveToIdle_DMA(huart, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
}

/**

 * @brief  处理UART接收缓冲区中的数据。

 * 如果在100ms内没有接收到新的数据,将清空缓冲区。

 * @param  None

 * @retval None
    **/
// 处理UART接收缓冲区中的数据
void uart_proc(void)
{
    // 如果环形缓冲区为空,直接返回
    if (my_ringbuffer_is_empty(&my_rb))
        return;

    // 从环形缓冲区读取数据到读取缓冲区
    unsigned int len = my_ringbuffer_get(&my_rb, usart_read_buffer, my_ringbuffer_data_len(&my_rb));
    if (len > 0)
    {
        usart_read_buffer[len] = '\0'; // 添加字符串结束符,假设数据是字符串
        // 打印读取缓冲区中的数据
        printf("ringbuffer data: %s\n", usart_read_buffer);

        // 清空读取缓冲区
        memset(usart_read_buffer, 0, READ_BUFFER_SIZE);
    }
}

int main(void){
    // 初始化串口
    ringbuffer_init();
    // 其他代码 
    while (1)
    {
        // 处理串口数据
        uart_proc();
    }
    
}

代码说明:

  1. MyRingBuffer 相关函数: 与之前提供的代码相同,实现了环形缓冲区的基本操作。
  2. RING_BUFFER_SIZEREAD_BUFFER_SIZE 定义了环形缓冲区和读取缓冲区的大小。
  3. my_rbusart_ring_buffer 定义了环形缓冲区结构体变量和实际存储数据的数组。
  4. usart_read_buffer 用于临时存储从环形缓冲区读取的数据的数组。
  5. uart_rx_dma_buffer: 假设这是你的 UART 接收 DMA 缓冲区。
  6. ringbuffer_init(): 初始化串口和环形缓冲区,并启动 DMA 接收。你需要根据你的硬件平台和 HAL 库实现具体的初始化代码。
  7. HAL_UARTEx_RxEventCallback()
    • 使用 my_ringbuffer_put() 将 DMA 缓冲区 uart_rx_dma_buffer 中的数据写入到环形缓冲区 usart_rb
    • 清空 DMA 缓冲区 uart_rx_dma_buffer
    • 重新启动 DMA 接收:HAL_UARTEx_ReceiveToIdle_DMA(huart, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));这一步非常重要,它使得 DMA 可以继续接收数据。
  8. uart_proc()
    • 使用 my_ringbuffer_is_empty() 检查环形缓冲区是否为空。如果为空,则直接返回。
    • 使用my_ringbuffer_get()从环形缓冲区读取数据到读取缓冲区。
    • 打印读取到的数据。
    • 清空 usart_read_buffer

实验现象:

image-20241207195749510

好的,这次的内容就到这里啦

感谢你的阅读,欢迎点赞关注转发

我们,下次再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值