LwRB 使用说明
LwRB (lightweight ring buffer) 是一个轻量级的环形缓冲区,除了支持 ring buffer 常规的读写操作,它还有自己的特色,比如支持读写事件通知,支持 DMA 零拷贝收发数据。
- LwRB 项目工程为:https://github.com/MaJerle/lwrb。
- RT-Thread 移植版本为:https://github.com/Jackistang/lwrb2rtt 。
下面依次介绍相应功能的使用方式:
LwRB 原理浅析
详细内容请参考 LwRB - How it works,以下仅写出我的理解。
LwRB 内部有一块地址连续的缓冲区,这个缓冲区是用户调用 lwrb_init()
时指定的,接口如下:
uint8_t lwrb_init(LWRB_VOLATILE lwrb_t* buff, void* buffdata, size_t size);
而且 LwRB 内部维护了一个读(R)、写(W)指针,分别指向下一个可读、下一个可写的位置。写入一个字节数据时,将数据放入写指针的位置,并将写指针加 1 ,若超出缓冲区范围,则回滚到缓冲区起始处。
例如上图例程,缓冲区大小 S
为 8 :
- 初始时:
R
与W
相等,代表 LwRB 为空。 - 写入 4 个字节:
W
移动到 4 的位置,此时 LwRB 存储了 4 个字节。 - 写入 3 个字节:
W
移动到 7 的位置,此时 LwRB 存储了 7 个字节,而且W == R-1
, 或者说W == (R + S - 1) % S
,代表 LwRB 已满。 - 读出 5 个字节,并再次写入 4 个字节:
W
移动到 3,R
移动到 5,此时 LwRB 存储了 6 个字节。 - 写入 1 个字节:
W
移动到 4,此时 LwRB 存储了 7 个字节,而且W == R-1
,或者说W == (R + S - 1) % S
,代表 LwRB 已满。
需要注意的是,lwrb 内部 W == R
代表 LwRB 为空,W == (R + S - 1) % S
代表 LwRB 为满。
读写操作
LwRB 里的读写操作接口为:
/* Read/Write functions */
size_t lwrb_write(LWRB_VOLATILE lwrb_t* buff, const void* data, size_t btw);
size_t lwrb_read(LWRB_VOLATILE lwrb_t* buff, void* data, size_t btr);
size_t lwrb_peek(LWRB_VOLATILE lwrb_t* buff, size_t skip_count, void* data, size_t btp);
-
lwrb_write()
用于向 LwRB 里写入数据,并移动写指针。 -
lwrb_read()
用于从 LwRB 里读出数据,并移动读指针。 -
lwrb_peek()
用于从 LwRB 里读出数据,不移动读指针。
这里需要区分 lwrb_read()
和 lwrb_peek()
的不同之处,read 是真正地从 LwRB 里把数据取出来,会修改读指针,read 之后该数据就被 LwRB 移除了;而 peek 操作从 LwRB 里读数据,但并不取出来,不会修改读指针,peek 之后数据仍在 LwRB 里。
lwrb_peek()
函数还有一些特殊的用法,函数原型如下:
size_t lwrb_peek(LWRB_VOLATILE lwrb_t* buff, size_t skip_count, void* data, size_t btp);
-
buff
- LwRB 对象 -
skip_count
- 跳过几个数据再开始读取 -
data
- 读取数据缓冲区 -
btp
- 需要读取的字节数(bytes to peek)
同时该函数返回实际读取的字节数。以下来理解如何使用:
假设 LwRB 内部状态如下:W
处于 4 的位置,R
处于 6 的位置。
此时我们想要从 LwRB 里读取 4 个字节,调用 lwrb_peek(&buff, 0, data, 4);
,则最终得到的数据为:6, 7, 0, 1
,该函数内部帮我们处理了读指针回滚的情况,这是目前许多 ring buffer 不具备的功能。而且该函数还支持跳过最开始处的字节,例如调用 lwrb_peek(&buff, 2, data, 4);
,则最终得到的数据为:0, 1, 2, 3
,该函数内部自动跳过了读指针对开始处的 6, 7
这两个字节数据。
例程可参考:getting_started.c 和 lwrb_peek.c 。
读写事件通知
详细内容请参考 LwRB - Events,以下仅写出我的理解。
读写事件通知接口如下:
typedef void (*lwrb_evt_fn)(LWRB_VOLATILE struct lwrb* buff, lwrb_evt_type_t evt, size_t bp);
void lwrb_set_evt_fn(LWRB_VOLATILE lwrb_t* buff, lwrb_evt_fn fn);
lwrb 的事件通知功能是用户调用 lwrb_set_evt_fn()
向 lwrb 注册一个回调函数,然后 lwrb 每次进行读写数据时,都会通过这个回调函数通知用户当前写入了多少个字节,或读取了多少个字节。
该功能可用于打印日志,释放信号量等,更多使用场景还待挖掘。
例程可参考:event.c 。
DMA 零拷贝收发数据
详细内容请参考 LwRB - DMA for embedded systems,以下仅写出我的理解。
LwRB 提供的零拷贝功能,按照工作方式可分为两种类型:
- 从 LwRB 里零拷贝读取数据
- 向 LwRB 里零拷贝写入数据
先介绍从 LwRB 里零拷贝读取数据,相关接口为:
/* Read data block management */
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);
假设当前 LwRB 内部状态如下图所示:
LwRB 内部缓冲区已满,W
处于 4 的位置,R
处于 5 的位置。
接口 lwrb_get_linear_block_read_address()
可获取当前 LwRB 内部缓冲区第一个可读取字节的地址,例如上图中的 R
,而 lwrb_get_linear_block_read_length()
函数获取当前 LwRB 内部缓冲区可读取的,且地址连续的字节数,例如上图中会得到结果 3,表示可连续读取后续的数据 5, 6, 7
。
利用这两个函数,我们就得到了一个指针 p
和一个大小 len
,p
指向数据的起始地址,len
代表数据的字节大小,将 p
和 len
传给 DMA,即可直接数据发送了。
DMA 发送完成后,我们还需要更新 LwRB 里的 R
指针,通过 lwrb_skip()
函数移动 R
指针,如下图所示:
至此,我们就完成了从 LwRB 里零拷贝读取数据的流程,例程可参考:zero_copy_from_lwrb_memory.c 。
向 LwRB 里零拷贝写入数据的操作也类似,相关接口为:
/* Write data block management */
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);
假设当前 LwRB 内部状态如下图所示:
LwRB 内部缓冲区为空,R
和 W
都处于 4 的位置。
接口 lwrb_get_linear_block_write_address()
可获取 LwRB 内部缓冲区第一个可写入字节的地址,例如上图中的 W
,而 lwrb_get_linear_block_read_length()
函数返回 LwRB 当前可连续地址写入的大小,例如上图中的结果为 4,表示可连续写入 4, 5, 6, 7
处的数据。
利用这两个函数,我们就得到了一片地址连续的缓冲区,包括其首地址 p
和大小 len
,将其传入 DMA 即可开始接收数据了。
DMA 接收完成后,我们还需要更新 LwRB 内部的 W
指针,通过 lwrb_advance()
函数移动 W
指针,如下图所示:
至此,我们就完成了向 LwRB 里零拷贝写入数据的流程,例程可参考:zero_copy_to_lwrb_memory.c 。
线程安全
当只有一个写线程和一个读线程时,LwRB 是线程安全的,不需要额外加锁。
详细内容请参考 LwRB - Thread safety。