使用 Ring Buffer 完成数据传递

使用 Ring Buffer 完成数据传递

概述

在前述章节中,我们依次介绍了 QueueStreamBufferMessageBuffer。上述三者尽管提供了一定的数据缓存能力,但没有提供管理数据溢出的能力。

数据溢出是指在数据缓存区容量一定的情况下,缓存区的空间已经被完全使用,但是仍旧有数据传入时的情况。

在前述的组件中,它们对数据溢出的处理是禁止新数据写入到缓存区,在数据不允许丢失的情况下,这通常没什么问题。但是一些情况下,这种处理方式通常是不可接收的,假设向屏幕上投放影片,在屏幕的缓存区已经用尽的情况下,禁止写入新数据,将导致整体的画面进入延时,此时,允许覆盖一部分“特别”旧的数据,同时保留一部分“较旧”的内容通常是可取的,这样可以实现画面比较流畅,而不至于停顿。

ring buffer 主要的特性其实可以用一句话概括:写满时,自动环绕。其区别于普通缓存区的特性是,一般缓存区写满时,就不允许写了,会返回写失败;ring buffer 在写满时,不会返回失败,会覆盖原先首部的数据,继续写。

在这里插入图片描述

ringbuf 基本的 API 包括创建、发送数据、接收数据、注销数据(只有注销即 return 后的数据其才允许被覆盖,即该缓冲区可以被再次写入新数据):

RingbufHandle_txRingbufferCreate(size_t xBufferSize, RingbufferType_txBufferType)

BaseType_t xRingbufferSend(RingbufHandle_txRingbuffer, const void *pvItem, size_t xItemSize, TickType_t xTicksToWait)

void *xRingbufferReceive(RingbufHandle_txRingbuffer, size_t *pxItemSize, TickType_t xTicksToWait)
    
void vRingbufferReturnItem(RingbufHandle_txRingbuffer, void *pvItem)

值的注意的是,与 Queue、StreamBuffer、MessageBuffer 不同,ringbuf 在读取数据时,得到的数据的引用,即实际的存储在 ringbuf 的数据的指针,而不是拷贝的副本。

再次理解“ring"

很多初学者对 ringbuf 的理解总是想从 “ring”,即环绕的角度取理解,这往往使得对它的理解陷入一个奇怪的 ring。其是,没有一种真正的存储空间天生就是“环”(大部分存储器是线性的地址空间),所谓的“环”只是人为地添加的特性。可以这么说,ringbuf 就是一块缓冲区,在特性上通过读、写索引,实现读、写操作在到达缓冲区末尾时,可以通过配置指定,接下来的读写是否“重头开始”。

在这里插入图片描述

ring buf 的特点:

1)自动管理溢出时的处理。自动管理队头、队尾。

2)弹性的容量,即buffer 中的数据不会被销毁,直到下次被覆盖写入。

3)比链表要快,因为它内部使用数组(连续的内存),再次使用时,不必释放空间、移动其他数据也不必管理链表,只是重新赋值。

ring buf 的分类

在熟悉上面的概念后,可以理解 ESP-IDF 对 rIngbuf 的三种分类:

1)No-Split buffers:(禁止拆分型buffer)保证项目存储在连续内存中,并且在任何情况下(如环绕、内存空间不连续的情况)都不会尝试拆分项目。

2)Allow split buffers:即允许拆分缓冲区数据项存放在两个不连续的内存块中,如果这样做能够允许存储该条目。“允许拆分缓冲区”比“不拆分缓冲区”更节省内存,但在检索时需分两部分返回完整的原条目。

3)Byte buffer:不将数据存储为单独的项。所有数据都以字节序列的形式存储,并且每次发送或检索任意数量的字节。当不需要维护单独的项目(例如字节流)时,使用字节缓冲区。

写入 ring buf 数据时发生什么

如图显示为依次将大小为 18、3 和 27 字节的三个条目写入缓存区时,缓冲区内实际发生的情况:

对于“不拆分”和“允许拆分”缓冲区,每个数据项前面都有一个 8 个字节的标头, Byte buffer 没有标头。此外,每个项目占用的空间向上舍入到最接近的 32 位对齐大小,以保持整体 32 位对齐。但是,项目的真实大小记录在标头中,该标头将在检索项目时返回。

在这里插入图片描述

当数据满时,不可以拆分的 ringbuf 将强制回到缓冲区的头部开始存储新的条目。下图显示缓冲区大小为 128 字节,其中 56 个字节被标识为 free 可用空间,当要发送的项目为 28 字节时,由于是不可拆分的 ringbuf,即便尾部有 16 bytes 发空闲空间,也不会去存储 该 28bytes 的条目,如第二行所示,会在尾部填充占位符,然后如第三行所示,从缓冲区头部开始存储该长度为 28bytes 的条目:

在这里插入图片描述

需求及功能解析

ringbuffer 的内容颇多,这里仅介绍一种 no-split 类型的 ring buffer 使用的方法,并通过打印信息,反映ringbuffer 这种“自动环绕的”特性。

示例解析

示例中,ring buffer 的大小为 0xA0,下述 log,第 8 次发完数据后,读写位置都在 0x90 处,再往下写的话,重新从缓存区的头部开始写了,因此第9次的数据在 0x18 处,注意这里是十六进制,0x90 到 0x18 正好分为两段,0x900xA0,0x000x18,其中 0x90~0xA0 共计16 个字节的存储单元;0x00~0x18 共计 24 个字节的存储单元;因为前者不足以存储 8+16 的 large_item 这个 item,因此直接从 0x00 开始存储了,而不是从 0x90。

This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 295348 bytes
ring buffer Max item size is 40, num of items is 5
TASK2:flag=0
TASK2:free_pos=0x00000010, read_pos=0x00000010, write_pos=0x00000010, acquire_pos=0x00000010, num_of_items_pos=0x00000000
TASK2:flag=1
TASK2:free_pos=0x00000020, read_pos=0x00000020, write_pos=0x00000020, acquire_pos=0x00000020, num_of_items_pos=0x00000000
TASK2:flag=2
TASK2:free_pos=0x00000030, read_pos=0x00000030, write_pos=0x00000030, acquire_pos=0x00000030, num_of_items_pos=0x00000000
TASK2:flag=3
TASK2:free_pos=0x00000040, read_pos=0x00000040, write_pos=0x00000040, acquire_pos=0x00000040, num_of_items_pos=0x00000000
TASK2:flag=4
TASK2:free_pos=0x00000050, read_pos=0x00000050, write_pos=0x00000050, acquire_pos=0x00000050, num_of_items_pos=0x00000000
TASK2:flag=5
TASK2:free_pos=0x00000018, read_pos=0x00000018, write_pos=0x00000018, acquire_pos=0x00000018, num_of_items_pos=0x00000000
TASK2:flag=6
TASK2:free_pos=0x00000030, read_pos=0x00000030, write_pos=0x00000030, acquire_pos=0x00000030, num_of_items_pos=0x00000000
TASK2:flag=7
TASK2:free_pos=0x00000048, read_pos=0x00000048, write_pos=0x00000048, acquire_pos=0x00000048, num_of_items_pos=0x00000000

讨论

适合使用 ring buf 的几种场景

1)在预先知道缓存区需要存放数据的最大量时,可以用ringbuf,对需要频繁扩大缓冲区容量的可以使用链表。

2)允许覆盖旧数据的情况下,可以考虑使用 ringbuf。特别时多媒体处理时,比如,音频的生产者可以覆盖掉声卡暂为来得及处理的旧音频的数据。

3)数据并发处理。可以只读取数据,不 return 数据(删除数据),这样可以在多个线程中获取数据,然后分别执行对应的运算。

总结

1)Ring Buffer 提供了管理数据溢出的机制,当 Ring Buffer 的缓存区足够大时,数据写到缓冲区尾部后将自动从头部开始继续写。当 Ring Buffer 的缓冲区不够大时,即便存在数据溢出风险,但应用本身允许新数据覆盖旧数据的情况下使用 Ring Buffer 也非常合适。

2)ESP-IDF 对 rIngbuf 的三种分类:No-Split buffers、Allow split buffers、Byte buffer。

3)可以配置 rIngbuf 的缓冲区工作在连续内存中,由于 ringbuf 检索数据时是直接引用缓冲区的数据,连续的内存空间可以帮助提升访问效率。

资源链接

1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)

3)下一篇:使用队列集进行传递数据或信号同步

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

物联网老王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值