如何整合散乱数据
对于一个系统的输入端,可能会有多种输入形式。比如单片机要检测按键、传感器、串口接收数据等等。这些输入信号都是独立的,不好管理。因此利用结构体将它整合起来,方便后面以分层的方法进行编程。
我们所需两种结构体:
- 输入数据结构体
- 输入设备结构体
输入数据结构体将全部的数据进行打包,方便后续调用采集到的数据。输入设备结构体主要是拿到源数据,它与底层的硬件驱动链接在一起。
具体的结构如下:
数据结构体
结构体所需的信息如下:
- 类型:用于确定这个结构体是谁的数据
- 数据:用于存放硬件产生的相应信息
具体的结构体声明如下:
/* 输入数据抽象层 */
typedef struct InputEvent{
INPUT_EVENT_TYPE eType; //类型,用于分辨是哪一种输入
TIME_T time; //按键时间
int iKEY; //哪一个按键
int iPressure; //按键状态
char str[INPUT_BUF_LEN];//用于存储串口数据
}InputEvent,*PInputEvent;
在这个声明中,数据有两组:按键数据、串口接收数据。如果想继续扩展的话,只需要在这个结构体后面继续加入数据组即可。
在这个声明中,类型eType采用的是枚举类型,time采用的是宏定义的类型,其他变量使用的是int、char而非uint8_t这种类型。下面将说明为什么要使用这样的定义方法。
枚举声明类型
对于eType,含义是指定当前结构体是谁的数据,它是一个固定值。比如可以规定按键数据就是1,串口数据就是2...等等。这样我们可以使用枚举类型来实现类型的定义。
具体枚举声明如下:
typedef enum{
INPUT_EVENT_TYPE_KEY,
INPUT_EVENT_TYPE_NET,
INPUT_EVENT_TYPE_STDIO
}INPUT_EVENT_TYPE;
使用枚举类型的好处,就是更好的管理这个类型的个数。当前的类型个数是3个,那么我们如果想再加一个类型,只需在INPUT_EVENT_TYPE_STDIO之后再加一行,这样就可以实现了扩展。
宏定义声明类型
对于time,含义是采集到按键按下的时间,但这个时间的存储方式并不知道是什么样子。在stm32中可以通过HAL_GetTick()来读取,返回的是int型,但在其他芯片中,返回可能是结构体或者其他类型。
这种情况下,可以使用宏定义来保证灵活性,这样只需修改宏定义,即可修改time的类型,方便了后面代码的修改。
具体宏定义如下:
#define TIME_T int
不使用uint8_t
uint8_t是一个别名,它声明在其他的文件中。在这种分层的文件中,最好使用最基本的类型名称,而不是使用别名。因此使用的是int、char、unsigned char等类型,而不是他们的别名uint8_t
设备结构体
结构体所需的信息如下:
- 名称:根据名称来找到相应的设备结构体
- 链表指针:以链表方式管理这个结构体
- 设备处理函数指针
- 设备初始化函数指针
- 设备反初始化函数指针
具体的结构体声明如下:
/* 输入设备抽象层 */
typedef struct InputDevice{
char* name; /* 后面可以根据名称来找到相应的设备结构体 */
struct InputDevice* pNext;/* 链表 */
int (*GetInputEvent)(PInputEvent ptInputEvent); /* 指向设备处理函数 */
int (*DeviceInit)(void); /* 指向设备初始化函数 */
int (*DeviceExit)(void); /* 指向设备反初始化函数 */
}InputDevice,*PInputDevice;
这个name是一个固定的值,比如按键为“KEY”,串口为“UART”
有链表指针pNext是为了支持多个设备,将每个设备变为一个结构体,扩展设备只需插入链表
函数有返回值,含义为成功或者失败。
输入子系统
现在我们已经声明了数据存放的结构体,实现了各种输入数据的打包形式。之后我们只需要对这种打包的形式进行读或者写,就可以实现数据的传输。
对于读数据的部分,我们称为业务,它与硬件是隔离开的,是一种纯逻辑性的代码。
对于写数据的部分,我们称为硬件,它与硬件是结合的,是通过控制硬件的代码来获取源数据。
简易框架如下:
环形缓冲区
环形缓冲区的存在理由
从上述可以看出,在程序运行时,硬件要不断的获取源数据并转换成结构体形式存放起来,业务要不断的读取结构体形式的数据并进行相应的业务逻辑。而源数据又不是一个硬件采集的,即:数据有很多种,比如有按键数据,串口数据等等。
这就存在了一个问题,数据存哪?如果只存放在一个结构体中,那么如果处理的慢了,就会覆盖导致数据丢失。这个问题与串口接收的问题类似,因此也采用环形缓冲区的方式解决该问题。
环形缓冲区的实现
因为环形缓冲区就是一个数组(声明中为buf),数组中存放了一个又一个的数据。在串口中存放的是char型数据,而在这里需要存放的是结构体数据,即:前面的数据结构体类型InputEvent。
因此,只需修改buf的类型,而整体的实现框架依旧不变,即可完成功能。下面是具体的代码实现。
1、缓冲区声明
#define INPUT_BUF_SIZE 10
/* 输入子系统的缓冲区 */
/* 用途:存储输入设备的数据,防止覆盖导致数据丢失 */
typedef struct{
/* 缓冲区存放的是输入设备的数据,这数据是以InputEvent结构体形式存在 */
InputEvent buf[INPUT_BUF_SIZE];
int R_point;/* 读指针 */
int W_point;/* 写指针 */
}InputEventBuffer;
2、提供的接口
/* 向下的接口:写缓冲区 */
/* 写缓冲区,将InputEvent类型的变量的值写入buf中 */
int PutInputEvent(PInputEvent ptInputEvent);
/* 向上的接口:读缓冲区 */
/* 读缓冲区,将buf中的数据保存在InputEvent类型的变量中 */
int GetInputEvent(PInputEvent ptInputEvent);
3、接口的具体实现
#include "input_buf.h"
/* 使用static的原因是不让其他.c文件访问到该全局变量 */
static InputEventBuffer g_tInputBuffer;
/* 环形缓冲区写数据 */
/* 返回值: 1:成功写入 0:写入失败 */
int PutInputEvent(PInputEvent ptInputEvent){
if(ptInputEvent == NULL){
return -1;
}
/* 1.判断缓冲区是否已经满了 */
if((g_tInputBuffer.W_point+1)%INPUT_BUF_SIZE == g_tInputBuffer.R_point){
return 0;
}
/* 2.写缓冲区 */
g_tInputBuffer.buf[g_tInputBuffer.W_point] = *ptInputEvent;
/* 3.写指针+1 */
g_tInputBuffer.W_point = (g_tInputBuffer.W_point+1)%INPUT_BUF_SIZE;
return 1;
}
/* 环形缓冲区读操作 */
int GetInputEvent(PInputEvent ptInputEvent){
if(ptInputEvent == NULL){
return -1;
}
/* 1.判断缓冲区是否为空 */
if(g_tInputBuffer.R_point == g_tInputBuffer.W_point){
return 0;
}
/* 2.读缓冲区 */
*ptInputEvent = g_tInputBuffer.buf[g_tInputBuffer.R_point];
/* 3.读指针+1 */
g_tInputBuffer.R_point = (g_tInputBuffer.R_point+1)%INPUT_BUF_SIZE;
return 1;
}
硬件设备抽象
前面我们只知道该将读取到的数据写入环形缓冲区,但并没有方法控制这些采集数据的设备。可以使用前面声明的设备结构体来对设备进行控制。
在编写该代码之前,需要对整个的控制框架进行了解,具体的框架如下:
数据由硬件采集得到,硬件需要驱动但各种内核不同、芯片也不同,这就需要一层一层的编写。
对于最上层的数据,以前面声明的设备结构体与数据结构体进行管理,是与硬件完全分离的代码。
对于内核,向上提供一个接口,供设备结构体进行使用,这一层并不进行对硬件底层的访问
对于芯片,向上提供一个接口,供内核使用,这一层才是真正的硬件驱动代码。