数据接收并原样回发的机制是: 成功接收到一个数, 触发
进入中断, 在中断函数中将数据读取出来, 然后立即。 这一种数据处理机制是“ 非缓冲中断方式” , 虽然这种数
据处理方式不消耗时间, 但是这种数据处理方式严重的缺点是: 数据无缓冲区, 如果先前接收的的数据如果尚未
发送完成( 处理完成) , 然后串口又接收到新的数据, 新接收的数据就会把尚未处理的数据覆盖, 从而导致“ 数
据丢包” 。
对于“数据丢包” , 最简单的办法就是使用一个数组来接收数据: 每接收一个数据, 数组下标偏移。 虽然这
样的做法能起到一定的“缓冲效果” , 但是数组的空间得不到很好的利用, 已处理的数据仍然会占据原有的数据
空间, 直到该数组“满载” ( 数组的每一个元素都保存了有效的数据) , 将整个数组的数据处理完成后, 重新清
零数组, 才能开启新一轮的数据接收。
那么, 有什么好的数据接收处理机制既能起到“ 缓冲” 效果, 又能有效地利用“ 数组空间” ? 答案是: 有的,
那就是“ 环形缓冲区” 。
环形缓冲区就是一个带“ 头指针” 和“尾指针” 的数组。 “ 头指针” 指向环形缓冲区中可读的数据, “ 尾指
针” 指向环形缓冲区中可写的缓冲空间。 通过移动“ 头指针” 和“尾指针” 就可以实现缓冲区的数据读取和写入。
在通常情况下, 应用程序读取环形缓冲区的数据仅仅会影响“ 头指针” , 而串口接收数据仅仅会影响“ 尾指针” 。
当串口接收到新的数组, 则将数组保存到环形缓冲区中, 同时将“ 尾指针” 加 1, 以保存下一个数据; 应用程序
在读取数据时, “头指针” 加 1, 以读取下一个数据。 当“ 尾指针” 超过数组大小, 则“ 尾指针” 重新指向数组
的首元素, 从而形成“ 环形缓冲区” ! , 有效数据区域在“ 头指针” 和“ 尾指针” 之间。 如下图所示。
![1445b7b8a3c6b47d2527a68b6277a199.png](https://i-blog.csdnimg.cn/blog_migrate/cf2374f72fec68c091e0f2f264915a30.png)
当然, 环形缓冲区的“ 头指针” 和“尾指针” 可以用“ 头变量” 和“ 尾变量” 来代替, 因为切换数组的元素
空间, 除了可以用“ 指针偏移法” 之外, 还可以用“ 素组下标偏移法” 。 当串口接收到新的数组, 则将数组保存
到环形缓冲区中, 同时将“ 尾变量” 加一, 以保存下一个数据; 应用程序在读取数据时, “头变量” 加一, 以读
取下一个数据。
“环形缓冲区” 数据接收处理机制的好处在于: 利用了队列的特点, 一头进, 一头出, 互不影响, 在数据进
去( 往里存) 的时候, 另一边也可以把数据读出来, 而读出来的数据, 留下的空位, 又可以增加多的存储空间,
从而避免一边接收数据且一边处理数据会在数据量密集的时候而导致的丢掉数据或者数据产生冲突的问题。
如果仅有一个线程读取环形缓冲区的数据, 只有一个串口往环形缓冲区写入数据, 则不需要添加互斥保护机
制就可以保证数据的正确性。
需要注意的是, 如果串口每接收 x 个字节的数据才处理一次, 则环形缓冲区的缓冲数组的大小必须是 x 的 N
倍, 具体 N 为多少, 需要结合具体的数据接收速率以及处理速率, 适当调节。 这就好比喻, 水壶永远大于水杯,
这样子水壶才能存放很多杯水。
如果觉得前文隐晦难懂, 那么下面我们来一起讨论一下环形队列的具体状态以及实现。 下文构建的环形队列
采用的是“头变量” “尾变量” 来控制队列的存储和读取。
首先, 我们会构造一个结构体, 并定义一个结构变量。
#define MAX_SIZE 12//缓冲区大小
typedefstruct
{
unsigned charhead;//缓冲区头部位置
unsigned chartail;//缓冲区尾部位置
unsigned charringBuf[MAX_SIZE];//缓冲区数组
}ringBuffer_t;
ringBuffer_t buffer;//定义一个结构体
定义一个结构头体则表示新的消息队列已经创建完成。 新建创建的队列, 头指针 head 和尾指针 tail 都是指
向数组的元素 0。 如下图所示, 此时的消息队列是“空队列” 。
![236e8cad36c4768b6af3d44c81424e4f.png](https://i-blog.csdnimg.cn/blog_migrate/22337d00596bce5c7420b46a30b194a3.png)
当如果有 11 个数据 abcdefghijk 存入缓冲队列, 则如下图所示:
![d974bbc6ff802c919327511a7fdb3f4d.png](https://i-blog.csdnimg.cn/blog_migrate/f09f8e3b93df891a19ec4b38f1e4e31f.png)
#define BUFFER_MAX 36 //缓冲区大小
typedef struct
{
unsigned char headPosition; //缓冲区头部位置
unsigned char tailPositon; //缓冲区尾部位置
unsigned char ringBuf[BUFFER_MAX]; //缓冲区数组
} ringBuffer_t;
ringBuffer_t buffer; //定义一个结构体
首先, 需要构建一个结构体 ringBuffer_t, 如果定义一个结构体变量 buffer, 则意味着创建一个环形缓冲区。
往环形缓冲区存数据
/**
* @brief 写一个字节到环形缓冲区
* @param data: 待写入的数据
* @return none
*/
void RingBuf_Write(unsigned char data)
{
buffer.ringBuf[buffer.tailPositon]=data; //从尾部追加
if(++buffer.tailPositon>=BUFFER_MAX) //尾节点偏移
buffer.tailPositon=0; //大于数组最大长度 归零 形成环形队列
if(buffer.tailPositon == buffer.headPosition)//如果尾部节点追到头部节点, 则修改头节点偏移位置丢弃早
期数据
if(++buffer.headPosition>=BUFFER_MAX)
buffer.headPosition=0;
}
将数据存放到 tailPosition 所指向的元素空间。
tailPosition 变量自增 1, 并且判断, 如果大于最大缓冲, 则将 tailPosition 归零。
如果 tailPositon 与 headPosition 相等, 则表示, 数据存入速度大于数据取出速度, 从到导致“追尾”。 此时
headPosition 需要自增 1, 以丢弃早期数据, 这也就是数据“覆盖现象”, 这种现象是不允许存在的, 解决这种现象的办法是将缓
冲队列的空间再开大点。
如果 headPosition 也大于最大数组, 则需要将 headPosition 清零。
读取环形缓冲区的数据
/**
* @brief 读取环形缓冲区的一个字节的数据
* @param *pData:指针, 用于保存读取到的数据
* @return 1 表示缓冲区是空的, 0 表示读取数据成功
*/
u8 RingBuf_Read(unsigned char* pData)
{
if(buffer.headPosition == buffer.tailPositon) //如果头尾接触表示缓冲区为空
{
return 1; //返回 1, 环形缓冲区是空的
}
else
{
*pData=buffer.ringBuf[buffer.headPosition]; //如果缓冲区非空则取头节点值并偏移头节点
if(++buffer.headPosition>=BUFFER_MAX)
buffer.headPosition=0;
return 0; //返回 0, 表示读取数据成功
}
}
首先判断 headPosition 是否等于 tailPositon, 如果相等, 则表明, 此时缓冲区是空的。
缓冲区为空, 则直接返回, 不执行后面的程序
如果缓冲区不为空, 则条件成立并执行
读取 headPosition 所指向的环形缓冲队列的元素空间的数据。
headPosition 自增 1 以读取下一个数据。 如果 headPosition 大于最大值 BUFFER_MAX, 则将 headPosition 归零。
返回 0, 表示读取数据成功。
7.3.2 Hal_uart.c
/**
* @brief 串口 1 中断函数
* @param none
* @return none
*/
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判断接收标志位是否为 1
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清楚标志位
RingBuf_Write(USART_ReceiveData(USART1));
//阻塞等待直到传输完成
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
数据接收成功, 则将数据存入环形缓冲队列。
/**
************************************************************
* @file main.c
* @brief MCU entry file
* @date 2017-11-20
* @version V010100
* @copyright 深圳信盈达电子有限公司
* @note
***********************************************************/
#include "Hal_Led/Hal_Led.h"
#include "Hal_delay/delay.h"
#include "Hal_Key/Hal_Key.h"
#include "Hal_Relay/Hal_Relay.h"
#include "Hal_Usart/hal_uart.h"
#include "ringbuffer.h"
/**
* @brief 程序入口
* @param none
* @return none
*/
int main(void)
{
u8 data = 0;
SystemInit(); //系统时钟初始化
delayInit(72); //滴答定时器初始化
uartxInit();
while(1)
{
if(0 == RingBuf_Read(&data)) //从环形缓冲区中读取数据
{
USART_SendData(USART1,data); //读取接收到的数据并回发数据
} d
elayMs(1); //延时 1ms:使得处理数据的速度小于接收数据的速度, 用于验证接收缓冲区的“缓冲”特性
}
}
读取环形缓冲的数据, 如果环形缓冲队列有数据, 则条件成立
将数据原样回发
延时 1ms 的目的是: 使得处理数据的速度小于接收数据的速度, 用于验证接收缓冲区的“缓冲”特性。
实际上, 在项目工程中, 项目代码的执行是消耗一定的 CPU 时间的, 本例程序用延时 1 毫秒来等效替代项目代
码执行小号的 CPU 时间。
微信:18924630379