文章首发于同名微信公众号:DigCore
欢迎关注同名微信公众号:DigCore,及时获取最新技术博文。
经过上一篇《嵌入式硬件通信接口-使用RingBuffer处理数据(一)》简单了解循环缓冲区,读代码后,接着开始设计自己的循环缓冲区功能模块。
设计思路
这里设计的难点在于,如何把控tail这个写地址,每增加一个数据时tail自增,在写的过程中,如果使用缓冲区的所有空间,那么head等于tail的时候,这个情况可能是空或者满,需要在程序设计时候,多加留意。
在实际项目使用中,仍需要考虑的两个问题:
-
读多个字节时,缓冲区内可读数量比用户想读的个数少,这时候是逐字节把可读的都读出来,还是直接报错?
-
写多个字节时,缓冲区内可写数量比用户想写的个数少,这时候是逐字节把可写的都写满,还是直接报错?
鉴于这样的问题,在读多字节和写多字节这两个接口上增加一个变量mode,用于设定接口在读写多字节时遇到长度超出范围,是逐字节继续处理还是直接报错。
还有,相比于上一篇文章中参考的源码,这里的设计思路采用的是地址指针的方式,而不是偏移量。
并且为了区别于缓冲区空或者满,将牺牲掉一个字节的空间:当缓冲区空的时候head等于tail,当缓冲区满的时候head在tail相邻的后一个位置。
本次的设计,使用head指向缓冲区中可读数据的首地址,使用tail指向缓冲区中可读数据的末地址(同时这也是可写数据的首地址):
数据流是55 04 18 02 07
B7这样的一连串16进制数,接收端先收到的是0x55,后收到的是0x04,以这样的顺序逐字节通过物理层接口接收。这也是一个数据包的常规帧结构,也符合人从左到右的阅读顺序。
所以在这里以head指向的是数据包的头,tail指向数据包的尾,head到tail之间就是一个可读数据的范围,同时,tail指向的是可写区域的首地址,当有新的数据进来时,新的数据会被从tail地址继续写到内存,而后tail在环内递增。
知道了循环缓冲区的这几个属性:缓冲区大小、缓冲区在内存中的地址范围、存取数据时的读写地址。可根据这些属性,构造一个循环缓冲区的结构体:
typedef struct{
uint32_t size;
uint8_t *head;
uint8_t *tail;
uint8_t *buff;
}rb_t;
ringbuffer简写rb,其中
size表示用户申请成功的或定义的buffer空间大小;
head是一个地址指针,指向缓冲区读的首地址;
tail也是个地址指针,指向缓冲区读的末地址(也就是写的首地址);
buff还是一个指针,一直指向用户定义或者程序动态申请的内存buffer首地址。
当我们需要使用环形缓冲区前,需要定义数组变量或者申请内存,作为数据实际存储的地方,同时定义一个rb_t的结构体,用于关联数组变量或者内存空间。
功能模块将完成以下接口:
-
初始化
-
可读数量
-
可写数量
-
读一个字节
-
读多个字节
-
写一个字节
-
写多个字节
-
查看指定偏移的数据
-
查看指定数据是否在缓冲区并取其在内存的地址
-
复位清空
关于命名,暂且做个说明:dcclib是DigCore_Library的简写,这是功能模块库层的代码;rb是ringbuffer模块简写。
初始化
初始化的目的是把用户定义的将要使用的数组,与结构体变量关联起来,后续的操作脱离数组,直接操作结构体变量,正如此,结构体变量的初始化后,将对size、head、tail、buff各个成员赋值。
(截图略.)
可读数量
head指向的是可读数据的首地址,该地址内是有数据可以读的;
hail指向的是可读数据的末地址,该地址内没有数据可读,同时tail指向的是一个空的字节,作为写的首地址。
可写数量
这个需要留意的是总有一个字节空间不可写,因此size大小的空间内,除了可读,剩下就是一个空字节和可写数量。
读一个字节
读一个字节时,基于指针地址的操作模式,需要留意的是读指针自增后超过缓冲区的范围时,将读指针折返回到缓冲区的起始位置。
读多个字节
读多个字节,必然是基于读一个字节的基础上实现的。这里相比于前一篇文章提到的参考项目源码,不同的是增加了读模式的选择,当要读的数据个数大于可读数量时,可以利用mode来选择是否仍然逐字节地把能读的都读出来。
如果在调用接口时mode设为FORCE的强制读取,要读的数据个数大于可读数量时,将以RET_RB_RDWRN的错误返回码,旨在说明读取的数据不足,但是也已经把可读的都读出了!
写一个字节
写一个字节时,基于指针地址的操作模式,需要留意的是写指针自增后超过缓冲区的范围时,将写指针折返回到缓冲区的起始位置。
写多个字节
写多个字节,必然是基于写一个字节的基础上实现的。这里相比于前一篇文章提到的参考项目源码,不同的是增加了写模式的选择,当要写的数据个数大于可写数量时,可以利用mode来选择是否仍然逐字节地把能写的都写进去。
如果在调用接口时mode设为FORCE的强制读写入,要写的数据个数大于可写数量时,将以RET_RB_WRWRN的错误返回码,旨在说明可写的空间不足,但是也已经把剩余空间的都写满了!
查看指定偏移的数据
查看指定数据在缓冲区内存的位置
为了在按地址寻数据时不越界超出Buffer范围,这里分情况处理,分别head在tail之前或之后的两种情况处理。
复位清空
其实复位情况,也只是对读写指针操作,当读写指针在同一个位置时,就表示空。实现办法可以是将head赋值给tail,或者将缓冲区的起始地址都重新赋值给head和tail。
其实这两种办法都可以,因为对于环形缓冲区而言,它只关心head和tail的指针地址,而不关心对应内存块的首末
。
同样复位的时候也可以将内存中的数据清零,memset函数实现。
到此,环形缓冲区模块的代码基本搞定了。
还需要执行调试验证,为了验证各个接口的执行效果,编写了测试代码,并且将Debug结果打印输出,有条件可以在线仿真,并且可以具体查看内存中的真实数据!
附上Debug过程中对变量的观察的截图:
同样附上Debug过程中对Buffer在内存中呈现的截图:
参考资料:
《Circular_buffer》@维基百科
https://en.wikipedia.org/wiki/Circular_buffer
《环形缓冲器》@百度百科
https://baike.baidu.com/item/%E7%8E%AF%E5%BD%A2%E7%BC%93%E5%86%B2%E5%99%A8
《Ring-Buffer》@Github
https://github.com/AndersKaloer/Ring-Buffer