在算法的学习过程中,应当从功能和原理出发理解代码,而不能从代码出发尝试倒推功能和原理,后者不仅效率极低而且还给人带来挫败感,损伤学习兴趣。

队列的应用场景

循环缓冲区(也被称为环形缓冲区或循环队列)在许多产品和应用中都有着重要的作用,特别是在需要处理数据流的场景中。以下是一些常见的使用案例:

  1. 数据流处理: 循环缓冲区常用于处理不断流入的数据,例如网络数据包、串口数据、音视频数据等。循环缓冲区可以保持数据的连续性,并能平滑处理数据流的速率差异(例如,数据的生成速率和处理速率不匹配的情况)。
  2. 操作系统: 在操作系统中,循环缓冲区被广泛用于内核和驱动程序,处理如键盘输入、鼠标移动等硬件事件。这些事件由硬件在任何时刻产生,而操作系统需要在适当的时机处理它们。
  3. 实时系统: 在实时系统中,循环缓冲区用于处理实时任务,例如传感器数据的收集和处理。实时系统需要快速响应,循环缓冲区可以作为一个"先进先出"(FIFO)的队列,保证数据的实时性。
  4. 音频和视频处理: 在音频和视频处理中,循环缓冲区用于缓存音频样本和视频帧。这样可以保持数据的连续性,并允许多个处理步骤(例如解码、处理、渲染)同时进行。
  5. 网络通信: 在网络通信中,循环缓冲区用于处理网络数据包。这可以保持数据包的顺序,并处理网络的速率差异。
    以上是一些常见的应用,但并不限于这些。实际上,任何需要处理不断流入的数据,或者需要在不同速率的生产者和消费者之间进行数据传递的场景,都可能会使用循环缓冲区。

队列的原理和作用

原理

队列就像是一个管道,如果不断往管道里塞乒乓球,每个乒乓球就会在管道里排成一条队形,先进去的乒乓球就会先出来,这就是队列的“先进先出”规则。

队列原理.png

球从左边进去,进去的动作叫“入列”,进去的球在管道里拍成一个队列,叫做队列缓存,说白了就是数组,内部存了5个球就相当于是buff[5];最右边出来的1号球是最早进去的球,这个出来的动作叫出列,所以遵循了先进先出的规则。

作用

队列最主要的作用是缓存数据。串口接收数据时,我们一般定义一个数组来存储数据,但是假如串口数据频率很快,可能这个数组里存储的数据还没处理完,下一组串口数据又过来了,那么这时候数组里的数据就会被新数据覆盖,导致老数据丢失。像这种情况就可以通过队列来处理,每收到一个字节数据都先入列,然后在应用程序同步解析处理,根据队列先进先出的规则,老数据就不会被新数据“插队”了。
基于这种缓存数据的技术可以灵活应用在各种场景,比如:

  1. 操作系统的消息传递
  2. 大数据处理(一两百字节)

队列程序设计思路

实现队列的方法很多,但工作原理都一样。有三个核心关键点:

  1. 队列缓存(管道)。
  2. 入列(进去的球)。
  3. 出列(出去的球)。

队列缓存

队列缓存直接通过定义数组、链表等数据结构来实现。

入列

入列有两个要点:

  1. 队列缓存应当存储数组下标位置(“队尾”的位置)。如下图,数据①入列前,队尾指针变量指向数组第0个元素;然后将①存放在队尾指针指向位置,成功入列,指针自增,指针变量又指向队尾;将②存放在队尾指针指向位置,成功入列,指针自增,指针变量又指向队尾;以此类推,实现后进后出。

  2. 如果队列缓存满了又有新的数据入列,一般将最早入列的数据丢弃,从而入列新的数据。

入列要点.png

出列

出列则是从最早入列的数据地址开始取,出列的数组下标称为“队头”,同样使用指针变量指向队头,称为队头指针。如下图,一个满编队的队列,有5个数据,队头指针指向队列缓存首地址;①出列后,队头指针自增,指向②的地址;②出列后,队头指针自增,指向③的地址;以此类推,实现先进先出。

出列要点.png

队列程序的编写

在了解队列原理与作用,分析队列程序设计思路之后,就可以开始用程序实现队列。一个产品中会有很多个队列缓存不同的数据,消息有消息缓存,串口有串口缓存,所以将队列的基本操作(比如入列、出列)单独写成函数接口,方便程序调用。此处例子是一个“环形队列”。

queue.cpp

入列函数

入列函数形参。管道(缓存空间):头指针(head)、尾指针(tail)、缓存区地址(pBuff)、缓存区长度(Len);入列参数:入列数据地址(pData)、入列数据长度(DataLen)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void S_QueueDataIn(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char len, unsigned char *pData, unsigned char DataLen)
// (指向队头指针的指针, 指向队尾指针的指针, 指向缓冲区头位的指针, 缓存区长度, 指向入列数据的指针, 此函数支持入列的数据最大长度)
{// 入列函数
for (unsigned char num = 0; num < DataLen; num++,pData++) // 多数据入列
{
(**tail) = (*pData); // 入列数据
(*tail)++; // 队尾指针自增
// 当队尾指针位置超出缓冲区时,此时将队尾指针重新复位到队头。
if(*tail == pBuff+len)
{
*tail = pBuff;
}
// 当队尾指针与队头指针相同时,队头指针自增,丢弃最旧的数据
if (*tail == *head)
{
if (++(*head) == pBuff+len) *head = pBuff; //如果队头指针自增后超出缓冲区,则将其复位到缓冲区首位
else ++head; //队头指针自增
}
}
}

此函数用于向队列中添加数据。它将数据 (*pData) 添加到队列的尾部,如果队列已满(即,尾部指针与头部指针相等),它将移动头部指针来覆盖最旧的数据。如果尾指针超出缓冲区,它将被重置到缓冲区的头部。

出列函数

出列函数形参在形式上与入列函数相似,但其出列参数pData代表出列数据地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned char S_QueueDataOut(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char len, unsigned char *pData)
{// 出列函数,返回值为出列是否成功的状态,所以函数类型为unsigned char
unsigned char status = 0; // 取值状态,默认为0
if (*head != *tail) // 如果队列缓存里有数据
{
*pData = **head; // 将头指针数据传入出列数据指针所指向位置
status = 1; // 出列成功,status置1
if (++(*head) == pBuff+len)
{
*head = pBuff;
}
}
return status;
}

此函数用于从队列中拿取数据。如果队列不为空,则它将头指针指向的数据传输到指定地址(*pData)。如果头指针超出缓冲区,它将被重置到缓冲区的头部。

初始化函数

缓冲区需要初始化才能避免出现不受控制的指针,否则有可能导致程序陷入死循环。

1
2
3
4
5
void S_QueueEmpty(unsigned char **head, unsigned char **tail, unsigned char *pBuff)
{// 初始化函数
*head = pBuff; //头指针位置初始化到缓冲区第一位
*tail = pBuff; //尾指针位置初始化到缓冲区第一位
}

将缓冲区的头指针和尾指针都初始化到队头。

返回缓冲区数据量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned short S_QueueDataLen(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char Len)
{// 返回缓冲区内数据数量
if (*tail > *head)
{
return *tail-*head;
}
else if (*tail == *head)
{
return 0;
}
else
{
return Len-(*head-*tail);
}
}

头文件(queue.h)

以下是以上函数的头文件,定义了结构体,并且利用结构体简化形参,使得函数更具可用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _QUEUE_H_
#define _QUEUE_H_

extern void S_QueueEmpty(unsigned char **head, unsigned char **tail, unsigned char *pBuff);
extern void S_QueueDataIn(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char len, unsigned char *pData, unsigned char DataLen);
extern unsigned char S_QueueDataOut(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char len, unsigned char *pData);
extern unsigned short S_QueueDataLen(unsigned char **head, unsigned char **tail, unsigned char *pBuff, unsigned char Len);

#define QueueEmpty(x) S_QueueEmpty((unsigned char**)&(x).head,(unsigned char**)&(x).tail,(unsigned char*)(x).Buff) //QueueEmpty(keyMsg)
#define QueueDataIn(x,y,z) S_QueueDataIn((unsigned char **)&(x).head, (unsigned char **)&(x).tail, (unsigned char *)(x).Buff, sizeof((x).Buff), (unsigned char *)y, z) //QueueDataIn(keyMsg,pData,DataLen)
#define QueueDataOut(x,y) S_QueueDataOut((unsigned char **)&(x).head, (unsigned char **)&(x).tail, (unsigned char *)(x).Buff, sizeof((x).Buff), (unsigned char *)y) //QueueDataIn(keyMsg,pData)
#define QueueDataLen(x) S_QueueDataLen((unsigned char **)&(x).head, (unsigned char **)&(x).tail, (unsigned char *)(x).Buff, sizeof((x).Buff))
typedef struct
{
unsigned char *head; //队头指针,用于出列
unsigned char *tail; //队尾指针,用于入列
unsigned char Buff[4]; //定义缓冲区
}queue4;
#endif

在出(入)列函数前关闭中断,在出(入)列函数后开启中断

在处理串口通信时,我们常常使用中断服务程序 (Interrupt Service Routine, ISR) 来响应串口接收或发送的事件。然而,当我们进行入列(enqueue)或出列(dequeue)操作时,我们通常会关闭中断,这是为了防止数据的不一致性或竞态条件。

让我们考虑一下这样一个场景:你正在主程序中进行出列操作,即从队列中移除一个元素,同时,串口接收中断发生了,并且在ISR中需要向队列添加一个元素。如果你没有在出列操作时关闭中断,ISR可能会在主程序完成出列操作前就开始运行。结果,两个操作可能同时尝试访问和修改队列,可能会导致数据的不一致或错误。

为了避免这种情况,我们在进行出列或入列操作时,会先关闭中断,确保在操作完成前,ISR不会被触发。这就确保了对队列的操作是原子的(不可中断的),从而保证了数据的一致性。操作完成后,我们再重新开启中断,以便能够响应串口事件。

然而,需要注意的是,长时间关闭中断可能会导致丢失中断,因此,需要尽可能快地完成操作,以便能尽快重新开启中断。在设计代码时,应尽量减少需要关闭中断的代码段的长度和复杂性,避免影响系统的响应性。

主函数(main.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include"iostream"
#include"queue.h"
#include"queue.cpp"
queue4 keyMsg;
int main ()
{
printf("初始化缓冲区:\r\n");
QueueEmpty(keyMsg);
printf("keyMsg.head = 0x%x, keyMsg.tail = 0x%x, &keyMsg.Buff = 0x%x\r\n", keyMsg.head, keyMsg.tail, &keyMsg.Buff);
printf("缓冲区内数据数量:%d bit\r\n", QueueDataLen(keyMsg));

printf("入列 a[2] = {5, 6}数列:\r\n");
unsigned char a[2] = {5, 6};
QueueDataIn(keyMsg,&a,sizeof(a));
printf("keyMsg.head = 0x%x, keyMsg.tail = 0x%x, &keyMsg.Buff = 0x%x\r\n", keyMsg.head, keyMsg.tail, &keyMsg.Buff);
printf("keyMsg.Buff[0] = %d, keyMsg.Buff[1] = %d, keyMsg.Buff[2] = %d, keyMsg.Buff[3] = %d\r\n",keyMsg.Buff[0], keyMsg.Buff[1],keyMsg.Buff[2], keyMsg.Buff[3]);
printf("缓冲区内数据数量:%d bit\r\n", QueueDataLen(keyMsg));

printf("出列一个数据到b:\r\n");
unsigned char b = 0;
unsigned char status = QueueDataOut(keyMsg, &b);
printf("keyMsg.head = 0x%x, keyMsg.tail = 0x%x, &keyMsg.Buff = 0x%x\r\n", keyMsg.head, keyMsg.tail, &keyMsg.Buff);
printf("keyMsg.Buff[0] = %d, keyMsg.Buff[1] = %d, keyMsg.Buff[2] = %d, keyMsg.Buff[3] = %d, b = %d, status = %d\r\n",keyMsg.Buff[0], keyMsg.Buff[1],keyMsg.Buff[2], keyMsg.Buff[3],b, status);
printf("缓冲区内数据数量:%d bit\r\n", QueueDataLen(keyMsg));

printf("入列 c[3] = {7,8,9}数列:\r\n");
unsigned char c[3] = {7,8,9};
QueueDataIn(keyMsg,&c,sizeof(c));
printf("keyMsg.head = 0x%x, keyMsg.tail = 0x%x, &keyMsg.Buff = 0x%x\r\n", keyMsg.head, keyMsg.tail, &keyMsg.Buff);
printf("keyMsg.Buff[0] = %d, keyMsg.Buff[1] = %d, keyMsg.Buff[2] = %d, keyMsg.Buff[3] = %d\r\n",keyMsg.Buff[0], keyMsg.Buff[1],keyMsg.Buff[2], keyMsg.Buff[3]);
printf("缓冲区内数据数量:%d bit\r\n", QueueDataLen(keyMsg));
return 0;
}

定义好我们需要操作的循环缓冲区,然后就可以使用上面的函数对这个对象进行操作。