通用环形缓冲区 LwRB 使用指南
什么是 LwRB?
LwRB
是一个开源、通用环形缓冲区库,为嵌入式系统进行了优化。源码点击这里(Github)。
LwRB 特性
- 使用 ANSI
C99
编写 FIFO
(先进先出)- 无动态内存分配,数据是静态数组
- 只有单个任务写和单个任务读时,线程是安全的
- 只有单个中断写和单个中断读时,中断是安全的
- 支持内存间的 DMA 操作,实现缓冲区和应用程序内存之间零拷贝
- 对于读数据,提供
peek
(窥读,读数据但不改变读指针) 、skip
(跳读,向前移动读指针,将指定长度的数据标记为已读)函数,对于写数据,提供advance
(跳写,向前移动写指针,比如 DMA 硬件向环形缓冲区写入了 10 字节,应用程序需要调用此函数更新写指针)函数。 - 支持事件通知
临界条件
定义 LwRB
的 读指针为 r
,在读写操作时使用 r
,但仅在读操作时修改 r
;
定义 LwRB
的 写指针为 w
,在读写操作时使用 w
,但仅在写操作时修改 w
;
定义 LwRB
的 环形缓冲区大小为 s
,所有操作都会使用,决不会修改 s
。
- 环形缓冲区可以容纳的最大字节数总是为
s - 1
。这要求在初始化时,环形缓冲区的大小要比实际存储数据多一个字节; w
、r
指针总是指向下一个可写、可读位置;- 当
w == r
时,环形缓冲区为空; - 当
w == r - 1
时,环形缓冲区为满;
常规 API 函数
初始化函数
uint8_t lwrb_init(LWRB_VOLATILE lwrb_t* buff, void* buffdata, size_t size)
用法举例:
#define QUEUE_MAX_SIZE (1024)
lwrb_t format_rb;
uint8_t format_data[sizeof(int) * QUEUE_MAX_SIZE + 1];void init_format(void)
{lwrb_init(&format_rb, format_data, sizeof(format_data));
}
这里需注意环形缓冲区数组 format_data
的定义格式:
uint8_t format_data[sizeof(int) * QUEUE_MAX_SIZE + 1];
规定写入环形缓冲区或者从环形缓冲区读出的最小数据单位是 数据项
。数据项
可能为 1 个字节,也可能为多个字节。
这里的例子数据项
为 int
类型数据。
首先用宏 QUEUE_MAX_SIZE
定义需要保存的最大数据项
数目,这里定义 1024
个数据项
。
环形缓冲区定义为 uint8_t
类型的数组。
环形缓冲区数组大小可以用以下公式确定:
环形缓冲区数组大小=数据项大小∗数据项个数+1环形缓冲区数组大小 = 数据项大小 * 数据项个数 + 1环形缓冲区数组大小=数据项大小∗数据项个数+1
这里 + 1
是因为 LwRB
的实现特性决定的,详见 临界条件 一节。
从环形缓冲区读数据
size_t lwrb_read(LWRB_VOLATILE lwrb_t* buff, void* data, size_t btr)
该函数最多读取 btr
字节的数据(如果有的话),数据从环形缓冲区拷贝到 data
指向的数组。函数返回实际读取的字节数。
用法举例:
lwrb_read(&format_rb, data_buf, sizeof(int));
向环形缓冲区写数据
size_t lwrb_write(LWRB_VOLATILE lwrb_t* buff, const void* data, size_t btw)
该函数最多写入 btw
字节的数据(如果可以的话),数据从 data
指向的数组拷贝到环形缓冲区。函数返回实际写入的字节数。
用法举例:
lwrb_write(&format_rb, data_buf, sizeof(int));
从环形缓冲区窥读数据
size_t lwrb_peek(LWRB_VOLATILE lwrb_t* buff, size_t skip_count, void* data, size_t btp)
函数 lwrb_peek
也可以从环形缓冲区读取最多 btp
字节的数据,数据被拷贝到 data
指向的数组。但是它和 lwrb_read
函数有两点不同:
- 第一,
lwrb_read
函数读取数据后会移动读指针,也就是读取数据后,环形缓冲区会将这些数据移除掉;而lwrb_peek
函数不移动读指针,这就意味着读取数据后,环形缓冲区不会将这些数据移除掉,就好像只是到环形缓冲区中看看这些数据都是什么样子,并不把数据拿出来,所以称为窥读。 - 第二,相比
lwrb_read
函数,lwrb_peek
函数多了一个skip_count
参数。这个参数允许用户先跳过skip_count
指定的字节数,再开始读取。
lwrb_peek
对一些场景十分有用。举一个我用到的例子:
设备与上位机通讯故障后,会将本地采集的不定长数据缓存起来,缓存的格式是:
然后将这些数据存储到环形缓冲区。等到设备与上位机恢复通讯,再将这些数据去除长度字段后上传给上位机。为了数据的可靠传输,设备必须等到上位机确认数据已经收到,才能将这些数据删除掉。
所以在程序设计中,先窥读长度字段,确认长度字段合法后,再窥读剩余数据。
因为使用窥读,所以数据仍保存在环形缓冲区中,直到上位机确认数据已经收到后,再将这些数据从环形缓冲区中删除(会用到尚未介绍的 API 函数)。代码如下:
#define DATA_LEN_NUM 2 //长度字段占用的字节数
#define DATA_SEND_BUF_NUM 100 //数据字段最大字节数read_count = lwrb_peek(&resume_rb_s, 0, resume_read_buf, DATA_LEN_NUM); //窥读长度字段
/*环形缓冲区空处理*/
if(read_count != DATA_LEN_NUM)
{//处理return 0;
}
len = to_uint16_low_first(resume_read_buf); //长度字段
/*长度字段合法性检查*/
if(len == 0 || len > DATA_SEND_BUF_NUM)
{ ASSERT(0);//错误处理return 0;
}
lwrb_peek(&resume_nvrb_s, DATA_LEN_NUM, resume_read_buf, len); //跳过长度字段窥读数据部分
//其它处理
从环形缓冲区跳读数据
size_t lwrb_skip(LWRB_VOLATILE lwrb_t* buff, size_t len)
该函数最多读取 len
字节的数据(如果有的话),数据并不会被保存到用户层,而是直接丢弃掉(环形缓冲区会删除这些数据),就像跳过了这些数据,所以称为跳读。
跳读一般有两个用处:
- 第一,和窥读(
lwrb_peek
)函数配合使用,就如窥读举例使用的场景:先窥读出数据,传送给上位机;上位机确认接收后,用跳读将这些数据丢弃掉。 - 第二,使用硬件(比如 DMA )直接读取环形缓冲区数组后,需要用跳读将这些数据丢弃掉。这会在零拷贝一节中讲解。
零拷贝
这是 LwRB
的关键特性之一。可以结合 DMA 控制器实现环形缓冲区和用户内存之间的零拷贝。
从环形缓冲区零拷贝读取
需要3个函数配合:
/*获取环形缓冲区的线性读地址*/
void* lwrb_get_linear_block_read_address(LWRB_VOLATILE lwrb_t* buff)
/*获取读操作用到的线性数据块长度*/
size_t lwrb_get_linear_block_read_length(LWRB_VOLATILE lwrb_t* buff)
/*跳读*/
size_t lwrb_skip(LWRB_VOLATILE lwrb_t* buff, size_t len)
DMA 读操作需要源地址
和长度
,函数 lwrb_get_linear_block_read_address
用于获取 DMA 需要的源地址
,函数 lwrb_get_linear_block_read_length
用于获取 DMA 需要的长度
。DMA 读取成功后,需要调用函数 lwrb_skip
修改 r
指针,将 DMA 已经读取的数据从环形缓冲区中删除掉。
DMA 只能操作线性地址,而环形缓冲区会有地址回环,因此,可能需要读取 2 次才能将环形缓冲区数据读取完。下面对这句话举例分析。
假设环形缓冲区大小 s = 8
(uint8_t buff_data[8]
),目前处于满状态( w == r - 1
),保存 7 个数据,读指针 r == 5
,写指针 w == 4
。如下图所示:
那么函数 lwrb_get_linear_block_read_address
返回的线性地址
为读指针 r
所在的物理内存地址,这里为 &buff_data[5]
,函数 lwrb_get_linear_block_read_length
返回的线性数据块
长度为 3
字节(buff_data[5]~buff_data[7])。 要特别注意,虽然现在环形缓冲区有 7 字节可读,但是第一个线性连续的数据块只有 3 个字节,另外 4 字节(buff_data[0]~buff_data[3])虽然也是线性连续的,但两个线性连续数据块之间发生了地址回环(buff_data[7] -> buff_data[0])。
因此,第 1 次使用 DMA ,可以一次性读取 3 个字节数据,DMA读取成功后,调用函数 lwrb_skip
修改读( r
)指针,修改后,读指针 r == 0
,环形缓冲区变为:
这里需要再次重复一次上面的操作,函数 lwrb_get_linear_block_read_address
返回的线性地址
为读指针 r
所在的物理内存地址,这里为 &buff_data[0]
,函数 lwrb_get_linear_block_read_length
返回的线性数据块
长度为 4
字节(buff_data[0]~buff_data[3])。
第 2 次使用 DMA ,可以一次性读取 4 个字节数据,DMA读取成功后,调用函数 lwrb_skip
修改 r
指针,修改后,读指针 r == 4
,环形缓冲区为空:
从环形缓冲区零拷贝读取的一般使用方法:
/* Initialization part skipped *//* Get length of linear memory at read pointer */
/* When function returns 0, there is no memoryavailable in the buffer for read anymore */while ((len = lwrb_get_linear_block_read_length(&buff)) > 0) {/* Get pointer to first element in linear block at read address */data = lwrb_get_linear_block_read_address(&buff);/* If max length needs to be considered *//* simply decrease it and use smaller len on skip function */if (len > max_len) {len = max_len;}/* Send data via DMA and wait to finish (for sake of example) */send_data(data, len);/* Now skip sent bytes from buffer = move read pointer */lwrb_skip(&buff, len);
}
向环形缓冲区零拷贝写入
需要3个函数配合:
/*获取环形缓冲区的线性写地址*/
void* lwrb_get_linear_block_write_address(LWRB_VOLATILE lwrb_t* buff)
/*获取写操作用到的线性数据块长度*/
size_t lwrb_get_linear_block_write_length(LWRB_VOLATILE lwrb_t* buff)
/*跳写*/
size_t lwrb_advance(LWRB_VOLATILE lwrb_t* buff, size_t len)
DMA 写操作需要目的地址
和长度
,函数 lwrb_get_linear_block_write_address
用于获取 DMA 需要的目的地址
,函数 lwrb_get_linear_block_write_length
用于获取 DMA 需要的长度
。DMA 写入成功后,需要调用函数 lwrb_advance
修改写( w
)指针,将 DMA 已经写入的数据更新到环形缓冲区控制块中。
DMA 只能操作线性地址,而环形缓冲区会有地址回环,因此,可能需要写入 2 次才能将环形缓冲区写满(与从环形缓冲区零拷贝读取类似)。
线程安全
LwRB
的一个重要特性是支持边写边读或者边读边写操作,这个操作有前提条件,即只有存在单个写入入口点和单个读取出口点时才可以。换句话说,在此条件下,LwRB
是线程安全
的、中断安全
的。
只有单个任务写和单个任务读时满足单个写入入口点和单个读取出口点条件;
只有单个中断写和单个中断读时满足单个写入入口点和单个读取出口点条件;
多个任务写或者多个任务读、多个中断写或者多个中断读都不满足单个写入入口点和单个读取出口点条件,比如:
- 多个线程写
- 多个线程读
- 多个线程写多个线程读
以上三种情况均违反了单个写入入口点和单个读取出口点条件。这时代码不属于线程安全代码,红色虚线框住的部分,需要应用程序进行额外的资源互斥操作。
lwrb_t rb;/* 2 个互斥量, 一个用于写操作一个用于读操作 */
mutex_t m_w, m_r;/* 以下 4 个线程, 2 个写, 2 个读 */
void thread_write_1(void* arg) {/* 使用写互斥 */while (1) {mutex_get(&m_w);lwrb_write(&rb, ...);mutex_give(&m_w);}
}void thread_write_2(void* arg) {/* 使用写互斥 */while (1) {mutex_get(&m_w);lwrb_write(&rb, ...);mutex_give(&m_w);}
}void thread_read_1(void* arg) {/* 使用读互斥 */while (1) {mutex_get(&m_r);lwrb_read(&rb, ...);mutex_give(&m_r);}
}void thread_read_2(void* arg) {/* 使用读互斥 */while (1) {mutex_get(&m_r);lwrb_read(&rb, ...);mutex_give(&m_r);}
}
事件
事件是一个回调函数,函数原型为:
typedef void (*lwrb_evt_fn)(LWRB_VOLATILE struct lwrb* buff, lwrb_evt_type_t evt, size_t bp);
事件分为:读事件、写事件和复位事件。由枚举类型 lwrb_evt_type_t
定义:
typedef enum {LWRB_EVT_READ, /*!< Read event */LWRB_EVT_WRITE, /*!< Write event */LWRB_EVT_RESET, /*!< Reset event */
} lwrb_evt_type_t;
一个典型的事件函数实现为:
/*** \brief Buffer event function*/
void my_buff_evt_fn(lwrb_t* buff, lwrb_evt_type_t type, size_t len) {switch (type) {case LWRB_EVT_RESET:printf("[EVT] Buffer reset event!\r\n");break;case LWRB_EVT_READ:printf("[EVT] Buffer read event: %d byte(s)!\r\n", (int)len);break;case LWRB_EVT_WRITE:printf("[EVT] Buffer write event: %d byte(s)!\r\n", (int)len);break;default: break;}
}
事件函数通过注册的方式提交给环形缓冲区,注册函数为 lwrb_set_evt_fn
,一个注册事件函数的例子:
lwrb_set_evt_fn(&buff, my_buff_evt_fn);
事件函数注册成功后,LwRB
在每次修改读写指针时,都会调用这个事件函数,具体为:
- 读事件:
lwrb_read
函数、lwrb_peek
函数 - 写事件:
lwrb_write
函数、lwrb_advance
函数 - 复位事件:
lwrb_reset
函数(将读写指针设置为 0 )
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
通用环形缓冲区 LwRB 使用指南相关推荐
- Hadoop重点难点:Shuffle过程中的环形缓冲区
点击上方蓝色字体,选择"设为星标" 回复"面试"获取更多惊喜 这篇文章来自一个读者在面试过程中的一个问题,Hadoop在shuffle过程中使用了一个数据结构- ...
- 环形缓冲区的实现原理(ring buffer)
消息队列锁调用太频繁的问题算是解决了,另一个让人有些苦恼的大概是这太多的内存分配和释放操作了.频繁的内存分配不但增加了系统开销,更使得内存碎片不断增多,非常不利于我们的服务器长期稳定运行.也许我们可以 ...
- 架构设计:生产者/消费者模式 第6页:环形缓冲区的实现
2019独角兽企业重金招聘Python工程师标准>>> ◇判断"空"和"满" 上述的操作并不复杂,不过有一个小小的麻烦:空环和满环的时候,R和 ...
- 环形缓冲区: ringbuf.c
#cat aa.c /*ringbuf .c*/ #include<stdio.h> #include<ctype.h>#define NMAX 8 int iput = 0; ...
- c语言数组怎么环形阵列,C语言 用于大阵列的无复制线程安全环形缓冲区
对于大数组(10 ^ 7个元素)上的信号处理,我使用与环形缓冲区连接的不同线程.遗憾的是,只需要太多时间将数据复制到缓冲区和从缓冲区复制数据.当前实现基于boost :: lockfree :: sp ...
- 驱动调试(二)-环形缓冲区到文件
目录 驱动调试(二)-环形缓冲区到文件 目标 框架分析 虚拟文件系统proc dmesg proc_misc_init kmsg_read do_syslog 程序1创建文件 程序2提供读函数 程序3 ...
- 简洁高效的linux kfifo环形缓冲区
代码来自于:https://blog.csdn.net/vertor11/article/details/53741681,侵删. struct kfifo{uint8_t *buffer;uint3 ...
- SQL Server 环形缓冲区(Ring Buffer) -- 介绍
SQL Server 环形缓冲区(Ring Buffer) -- 介绍 以下关于Ring Buffer的介绍转载自: http://zh.wikipedia.org/wiki/%E7%92%B0%E5 ...
- 环形缓冲区ringbuffer
环形缓冲区是生产者和消费者模型中常用的数据结构.生产者将数据放入数组的尾端,而消费者从数组的另一端移走数据,当达到数组的尾部时,生产者绕回到数组的头部. 如果只有一个生产者和一个消费者,那么就可以做到 ...
最新文章
- Python-anaconda-Spyder使用matplotlib画图无法显示报错解决:Figures now render in the Plots pane by default. To mak
- MTK平台的启动流程(secureboot)
- adobe stream的最后一行空行_玩转Java8Stream(五、并行Stream)
- Spring在tomcat下使用JTA事务
- MySQL8的8大新SQL特性
- oracle9i监听自动断开,oracle连接超时自动断开问题
- HTML5 web SQL 和indexedDB的使用
- 半导体界仙童“八叛逆”又一人去世,仅存一人!
- 超图(Hypergraph)概念理解
- Pyrene-PEG-Biotin,芘丁酸聚乙二醇生物素,Biotin-PEG-Pyrene
- 数据结构与算法基本概念
- SDI科普--- SD-SDI/HD-SDI/3G-SDI/12G-SDI
- 计算机界面无法全部显示,电脑屏幕不能完整显示软件界面怎么处理
- 正是岳麓好风景,软件逢君正当时
- 调用PC端、手机、平板摄像头拍照
- web应用防火墙检测恶意流量的方法
- 计算机专业英语中tour的意思,tour旅游 (英语小记)
- 公有云上虚拟机故障恢复
- 【linux视频教程整套共25个视频】Linux初学者入门教程 .
- 函数返回值的优化技术(RVO和右值引用)
热门文章
- 背水一战 Windows 10 (5) - UI: 标题栏
- java散列算法_Java sha1散列算法原理及代码实例
- 2021私域流量营销洞察研究报告
- edger多组差异性分析_edger差异分析,如何查询分组差异的结果
- 计算机毕设(附源码)JAVA-SSM基于的二手房交易系统
- 什么是前端模块化,组件化,工程化?
- 武汉软件测试学校排名,武汉有什么好的软件测试学校
- Python实现彩带飘落和“跑马灯”效果
- 从“刷卡”到“点付”,微信医保支付如何助力新医改
- java输入日期计算天数_JAVA计算输入一个日期到当前日期一共多少天