串口数据收发中固定位置丢数问题解析与解决方案——引入环形缓冲区(ringbuffer)的实践探讨
目录
一、起因
二、解决办法
1.ringbuffer介绍
2.ringbuffer代码示例
2.1ringbuffer结构体
2.2创建一个ringbuffer
2.3判断ringbuffer的状态
2.4往ringbuffer中放数据
2.5从ringbuffer中取数据
三、实现效果
一、起因
最近在移植letter shell组件成功以后,正常发送命令但是每次回显都不完整并且在固定位置缺少字母(现象如下图)
于是想到应该是串口收发数据出了问题,分析一下收发数据及回显的过程,开始怀疑是中断上下文切换导致的cpu处理不过来串口数据,后面得知这个处理过程时间很短(不超过1us),通过分析知道了是shell回显出了问题,当我用串口给shell发字符“help”,shell通过中断方式接收是没问题的,接收到的是完整的“help”字符,然后shell会通过串口再把字符发回来(回显),在调用串口发送函数时每发送一个字符有个while循环等待发送完毕,这里需要cpu处理数据,就导致了回显出现丢数据的情况。
二、解决办法
对于串口丢数据的情况,看到网上大部分的解决办法是用串口的FIFO模式,串口FIFO可以理解为串口专用的缓存,该缓存采用先进先出方式。
串口FIFO:串口接收的数据,先放入接收FIFO中,当FIFO中的数据达到触发值(通常触发值为1、2、4、8、14字节)或者FIFO中的数据虽然没有达到设定值但是一段时间(通常为3.5个字符传输时间)没有再接收到数据,则通知CPU产生接收中断;发送的数据要先写入发送FIFO,只要发送FIFO未空,硬件会自动发送FIFO中的数据。写入发送FIFO的字节个数受FIFO最大深度影响,通常一次写入最多允许16字节。
这个方法确实不错,但是考虑到有些单片机没有FIFO模式,这里引入和FIFO有点类似的处理方法——ringbuffer环形缓冲区,它不需要硬件支持,是比较通用的处理方法。
1.ringbuffer介绍
ringbuffer定义:
环形缓冲区(Ring Buffer),也称为循环缓冲区、环形队列(Ring Queue)或循环队列(Circular Queue),是一种用于在固定大小的存储区域中存储数据的数据结构。
2.ringbuffer代码示例
这里分享的是一种数据宽度为8位(一字节)的ringbuffer,下面具体分析它的实现过程。
2.1ringbuffer结构体
struct ringbuffer8
{
uint32_t tail;
uint32_t head;
uint32_t length;
uint8_t buffer[];
};
这个结构体包含了环形缓冲区的头尾节点,长度,以及柔性数组buffer[](注:在C99中,结构体中最后一个成员允许是大小未知的数组,这就叫做柔性数组成员),在计算这个结构体的大小时,柔性数组不会包含在其中,即上面结构体的大小为12字节。
2.2创建一个ringbuffer
ringbuffer8_t rb8_new(uint8_t *buff, uint32_t length)
{
ringbuffer8_t rb = (ringbuffer8_t)buff;
rb->length = length - sizeof(struct ringbuffer8);
return rb;
}
创建ringbuffer的接口,供外部调用,开辟的空间转换为结构体类型,并用结构体指针来接收。rb->length为柔性数组的空间大小。
2.3判断ringbuffer的状态
bool rb8_empty(ringbuffer8_t rb)
{
return rb->head == rb->tail;
}
bool rb8_full(ringbuffer8_t rb)
{
return next_head(rb) == rb->tail;
}
这里指定头结点与尾结点重合即ringbuffer空,头结点的下一个结点为尾结点即ringbuffer满。
2.4往ringbuffer中放数据
bool rb8_put(ringbuffer8_t rb, uint8_t data)
{
if (next_head(rb) == rb->tail)
return false;
rb->buffer[rb->head] = data;
rb->head = next_head(rb);
return true;
}
bool rb8_puts(ringbuffer8_t rb, uint8_t *data, uint32_t size)
{
bool ret = true;
for (uint16_t i = 0; i < size && ret; i++)
{
ret = rb8_put(rb, data[i]);
}
return ret;
}
这里提供了两个接口,分别为往ringbuffer中放一字节数据和多字节数据,往ringbuffer放数据前应判断ringbuffer的状态是否为满,然后把数据放入当前头结点所指向空间,再移动头结点到下一位,放多字节数据的过程就是循环调用放一字节数据的接口。
注意:如果ringbuffer满了,再往里放数据的话就会导致数据丢失,不会覆盖旧的数据,使用场景更适合像视频这种丢几帧数据不影响正常观看。
2.5从ringbuffer中取数据
bool rb8_get(ringbuffer8_t rb, uint8_t *data)
{
if (rb->head == rb->tail)
return false;
*data = rb->buffer[rb->tail];
rb->tail = next_tail(rb);
return true;
}
bool rb8_gets(ringbuffer8_t rb, uint8_t *data, uint32_t size)
{
bool ret = true;
for (uint16_t i = 0; i < size && ret; i++)
{
ret = rb8_get(rb, &data[i]);
}
return ret;
}
取数据的过程和放数据的实现过程类似,首先判断ringbuffer的状态是否为空,然后从尾结点所指向空间取数据,再移动尾结点到下一位。
三、实现效果
通过移植ringbuffer到串口接收过程就可以解决丢数据的问题,下面是实现效果。
作者:北北北秋北