说明:记录自己学习的过程,如有理解上的错误或者不恰当的地方请原谅。
一、简介
LwRB 是一个针对嵌入式系统优化的通用FIFO(先进先出)缓冲区库,之前的名称叫做RingBuffe
,不知道作者在V2.0.0版本时,修改名称为了LwRB。以下为主要的一些特性:
-
用 C (C11) 编写,兼容size_t 数据类型
-
平台无关,无特定架构的代码
-
实现 FIFO(先进先出)缓冲区
-
无动态内存分配,数据为静态数组
-
使用优化的内存复制,而不是循环来读取/写入内存中的数据
-
作为管道使用时,线程安全,只有单个写入和单个读取条目
-
作为管道使用时,单写和单读条目的中断安全
-
适用于从内存到内存的 DMA 传输,缓冲区和应用内存之间零拷贝开销
-
支持数据查看、读取跳过和写入时提前
-
实现对事件通知的支持
-
用户友好的 MIT 许可证
我们重点关注里面标记红色特性,在把LwRB用于串口接收和发送时,零拷贝开销,特别高。
二、相关资料和链接
直接挂上作者写的文档链接,里面有详细的使用技巧介绍、原理讲解,非常生动形象。主页上还有作者开发发的各种其他嵌入式开源小组件。
https://docs.majerle.eu/projects/lwrb/en/latest/index.html
三、移植注意事项
移值平台:DMK5 GD32F303系列芯片(其实和芯片平台无关)
3.1 源码获取
GitHub地址 https://github.com/MaJerle/lwrb 这里我们拉取main分支的代码,他是最新稳定的版本分支。不建议使用develop分支代码,应为为开发状态。
3.2 源码文件介绍
主要关注docs文件和lwrb文件。
docs/examples_src文件重点关注,详细的说明了各个用法例子。docs文件下有许多后缀rst的文件(是一种用于文本数据的文件格式,主要用于Python 编程语言社区的技术文档),这个直接在github网页上就可以了。
docs/lwrb/src文件。我们直接把里面的lwrb.h和lwrb.c文件拎出来,移植就用这两个文件。
3.3 需要修改的地方
把上述的lwrb.h和lwrb.c文件加入工程中。最新稳定V3.0.0版本支持了STDATOMIC 功能,保证数据的原子访问,需要支持这个特性的话,编译器需要支持C11,需要的keil 是AC6的支持。你懂的AC6太麻烦了,我们还是用AC5吧。直接编译会报错:
..\..\User\components\lwrb\lwrb.h(53): error: #5: cannot open source input file "stdatomic.h": No such file or directory
在lwrb.h上直接定义一个LWRB_DISABLE_ATOMIC,表明不使用atomic即可。
#define LWRB_DISABLE_ATOMIC
#ifdef LWRB_DISABLE_ATOMIC
typedef unsigned long lwrb_ulong_t;
#else
#include <stdatomic.h>
typedef atomic_ulong lwrb_ulong_t;
#endif
可能大家这里就会有疑问了,不使用atomic有什么问题吗?答案是没有的,因为size_t 类型在Cortex-M 上是 32 位,我们使用的GD32或者STM32是 32 位在一个周期内写入的。
3.4 最小示例代码
移植完成后,就可以使用了,只是官网的一个最小使用示例:
#include "lwrb/lwrb.h"
/* Declare rb instance & raw data */
lwrb_t buff;
uint8_t buff_data[8];
/* Application variables */
uint8_t data[2]; /* Application working data */
/* Application code ... */
lwrb_init(&buff, buff_data, sizeof(buff_data)); /* Initialize buffer */
/* Write 4 bytes of data */
lwrb_write(&buff, "0123", 4);
/* Print number of bytes in buffer */
printf("Bytes in buffer: %d\r\n", (int)lwrb_get_full(&buff));
/* Will print "4" *
四、LwRB实现原理
主要参考官方给的文档,文档展示了几种不同的缓冲区使用情况,来提供给我们理解内部是如何管理数据,这里自己搬运一下,配合上自己的理解。https://docs.majerle.eu/projects/lwrb/en/latest/user-manual/how-it-works.html
先明确一下这几个缩写的含义:
- R: 代表读指针。在读取/写入操作时读取。仅在读取操作时修改
- W: 代表写指针。在进行读/写操作时读取。仅在写操作时修改
- S: 代表缓冲区大小。在初始化的时候就固定了
- W和R指针的值位于0到S-1之间。例如上述图中定义了一个大小为8的缓冲区作为数组,W 和 R 指针的有效数值范围为 0 - 7。
- R 和 W 将在在 S 处溢出,因此R 和 W有效范围总是 0、1、2、3、......、S - 2、S - 1、0、1、2、3、......、S - 2、S - 1、0、......
- 当S为4时,R和W的取值范围为:0、1、2、3、0、1、2、3、0、1.............
- 缓冲区可容纳的最大字节数总是 S - 1,因此缓冲区最多可容纳 7 个字节
- R 和 W 指针总是指向下一个读/写操作
- 当 W == R 时,缓冲区为空
- 当 W == R - 1 时,缓冲区被认为是满的
- W == R - 1 只有在 W 和 R 溢出缓冲区大小 S 时才有效。
- 总是将 S 加到计算出的数字上,然后使用模数 S 得出最终值
具体的模数应该怎么计算呢?
//假设S为4
2 - 3 = (2 - 3 + S) % S = (2 - 3 + 4) % 4 = (-1 + 4) % 4 = 3
我们来看具体的情况:
- 示例A:缓冲区为空,因为 W == R = 0
- 示例B:缓冲区有 W - R = 4 - 0 = 4 个字节,因为 W > R
- 示例C:缓冲区满,因为 W == R - 1 或 7 == 0 - 1 ,也就是上面说到模数计算公式R代入0,来进行计算 ,7 = (0 - 1 + S) % S = (0 - 1 + 8) % 8 = (-1 + 8) % 8 = 7
- 示例D:缓冲区保存有 S - (R - W) = 8 - (5 - 3) = 6 个字节,因为 R > W
- 示例E:缓冲区满,因为 W == R - 1 或者叫(4 = 5 - 1) ,也就是 S - (R - W) = 8 - (5 - 4) ) = 7 字节。
注意:这里我们重点区分一下,缓冲区满的C和E的情况不同,C示例的缓冲区满,是W>R的条件下,直接用R-1计算是负数,需要进行取模计算公式来满足W==R-1。而E示例的缓冲区满R>W的条件下的,相当于缓冲区到了尾部后又重新到了头部,所以直接利用R-1计算可以得到满足W==R-1,这个时候的利用 S - (R - W)来计算出缓冲区内大小为7字节,缓冲区满了。
五、应用
我主要用于串口数据的接收和发送上面,配置DMA接收半满中断、串口IDLE中断、DAM发送完成中断,可以实现高效的串口数据发送和接收,后续会出两篇博文详细介绍这方面的使用和原理。