串口缓冲区的实现
1、队列介绍
队列 (Queue):是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在一端插入(入队),在另一端进行删除(出队)。
图1.1 队列示意图
如图1.1所示:类似售票排队窗口,先到的人肯定能先买到票,然后先走,后来的人只能后买到票。
队列主要分为普通队列和环形队列。
1.1 普通队列
图1.1.1 普通队列示意
在计算机中,每个信息都是存储在存储单元中的,做个比喻,图1.1.1的一些小正方形格子就是一个个存储单元,可以理解为常见的数组,存放一个个的信息。当有大量数据的时候,我们不能存储所有的数据,那么计算机处理数据的时候,只能先处理先来的,处理完后,就会把数据释放掉,再处理下一个。由于后来的数据只能往后排队,因此已经处理的数据的内存就会被浪费掉。如过要将剩余的数据都往前移动一次,那么效率就会低下了,肯定不现实,所以,环形队列就出现了。
1.2 环形队列
图1.2.1 环形队列示意
它的队列就是一个环,它避免了普通队列的缺点,就是有点难理解而已,其实它就是一个队列,一样有队列头,队列尾,一样是先进先出(FIFO)。我们采用顺时针的方式来对队列进行排序。
队列头 (Head) :允许进行删除的一端称为队首。
队列尾 (Tail) :允许进行插入的一端称为队尾。
1.2.1 环形队列的实现
在计算机中,也是没有环形的内存的,只不过是我们将顺序的内存处理过,让某一段内存形成环形,使他们首尾相连。简单来说,你可以理解为这就是一个数组,只不过有两个指针,一个指向列队头,一个指向列队尾。指向列队头的指针(Head)是缓冲区可读的数据,指向列队尾的指针(Tail)是缓冲区可写的数据。通过移动这两个指针(Head) &(Tail)即可对缓冲区的数据进行读写操作了,直到缓冲区已满(头尾相接),将数据处理完,可以释放掉数据,又可以进行存储新的数据了。
1.2.2 实现的原理
初始化的时候,列队头与列队尾都指向0,当有数据存储的时候,数据存储在‘0’的地址空间,列队尾指向下一个可以存储数据的地方‘1’,再有数据来的时候,存储数据到地址‘1’,然后队列尾指向下一个地址‘2’。当数据要进行处理的时候,肯定是先处理‘0’空间的数据,也就是列队头的数据,处理完了数据,‘0’地址空间的数据进行释放掉,列队头指向下一个可以处理数据的地址‘1’。从而实现整个环形缓冲区的数据读写。
图1.2.2.1 环形队列存储示意图
队列头就是指向已经存储的数据,并且这个数据是待处理的。下一个CPU处理的数据就是1;而队列尾则指向可以进行写数据的地址。当1处理了,就会把1释放掉。并且把队列头指向2。当写入了一个数据6,那么队列尾的指针就会指向下一个可以写的地址。
图1.2.2.2 环形队列处理示意图
2、程序实现
- 定义结构体
在uart1Process.h文件中定义结构体和宏定义,然后声明一个结构体。
#define RINGBUFF_LEN 1024 //串口缓冲区大小
typedef struct
{
unsigned short Head;
unsigned short Tail;
unsigned short Length;
unsigned char Ring_Buff[RINGBUFF_LEN];
}RingBuff_t;
extern RingBuff_t USART1_ring_Buff;
将上面声明的结构体在uart1Process.c文件中定义,顺便再初始化一下。如下所示:
RingBuff_t USART1_ring_Buff =
{
0,
0,
0,
{0}
};
在uart1Process.c文件中编写函数
/******************************************************************
*-Function Name :Write_RingBuff
*-Description :向缓冲区的末尾存储一个字节
*-Entry Parameter:
*-Output :
*-Return Value :
*-Version :
*-Date :
*-Others :
********************************************************************/
unsigned char Write_RingBuff(unsigned char data)
{
if(USART1_ring_Buff.Length >= RINGBUFF_LEN) //判断缓冲区是否已满
{
return FLASE;
}
USART1_ring_Buff.Ring_Buff[USART1_ring_Buff.Tail] = data;
USART1_ring_Buff.Tail = (USART1_ring_Buff.Tail+1)%RINGBUFF_LEN;//防止越界非法访问
USART1_ring_Buff.Length++;
return TRUE;
}
/******************************************************************
*-Function Name :Read_RingBuff
*-Description :读取缓冲区一个字节
*-Entry Parameter:
*-Output :
*-Return Value :
*-Version :
*-Date :
*-Others :
********************************************************************/
unsigned char Read_RingBuff(unsigned char *rData)
{
if(USART1_ring_Buff.Length == 0)//判断非空
{
return FLASE;
}
*rData = USART1_ring_Buff.Ring_Buff[USART1_ring_Buff.Head];//先进先出FIFO,从缓冲区头出
USART1_ring_Buff.Head = (USART1_ring_Buff.Head+1)%RINGBUFF_LEN;//防止越界非法访问
USART1_ring_Buff.Length--;
return TRUE;
}
由于我用的单片机是STM32F405,所以读取串口中断函数如下所示。代码中的Rec_rt_tick = rt_tick_get();是记录下当前的时间,以便于在串口接收数据停止后的一段时间后开始处理串口缓冲区的数据。如果串口刚收到数据就去读取串口缓冲区,则此时可能一个数据包还没有接收完成。我在这里的做法是当串口接收数据停止300ms后,再去处理串口缓冲区数据。
unsigned long Rec_rt_tick = 0;
void USART1_IRQHandler(void)
{
rt_uint8_t c;
rt_uint32_t usStatus;
rt_interrupt_enter();
usStatus = USART1->SR;
if((usStatus & USART_IT_RXNE) != RESET)
{
c = USART_ReceiveData(USART1) & 0xFF;
Write_RingBuff(c);
Rec_rt_tick = rt_tick_get();
}
if((usStatus & USART_FLAG_CTS) != RESET)
{
USART_ClearFlag(USART1,USART_FLAG_CTS);
}
if((usStatus & USART_FLAG_LBD) != RESET)
{
USART_ClearFlag(USART1,USART_FLAG_LBD);
}
rt_interrupt_leave();
}
3 从串口缓冲区中读取数据包
基本上数据包的格式都包含如下四个部分
帧头 | 数据 | 校验码 | 帧尾 |
此时根据自己定义的数据包格式从串口缓冲区中读取一个完整的数据包,经过校验合格后,处理该数据包。有时候,串口会连续接收几个数据包,当处理完一个数据包后,串口缓冲区中还有数据,即USART1_ring_Buff.Length不为0,要继续从串口缓冲区中读取数据。有时候会读到无效的数据,比如校验不通过、和自己定义的数据格式不符,这种情况就要做其它处理了。不然就直接丢掉这些数据。