接着上面的话题,我们将数据通过串口接收中断接收到我们事先定义好的数组里面,数据也经过帧头和校验和的校验了,接下来就要将这组数据进行解析。传统的方法我们直接提取数组的内容,接收的数据定义有uint8或者int8还有uint16,int16这种数据,如果分大端小端的话,我们还要进行转换。有的为了节约数据长度,例如开关量,只是利用到了一个字节某个位。例如我这次接收的数据是一组环境温度数据。内容如下:
1. 多参数传感器数据类型表
字 节 | 名称 | 说明 |
---|---|---|
B1 | 帧头1 | 固定值 07h |
B2 | 帧头2 | 固定值 17h |
B3 | 数据 | eCO2 高字节 |
B4 | 数据 | eCO2 低字节 |
B5 | 数据 | eCH2O 高字节 |
B6 | 数据 | eCH2O 低字节 |
B7 | 数据 | TVOC 高字节 |
B8 | 数据 | TVOC 低字节 |
B9 | 数据 | PM2.5 高字节 |
B10 | 数据 | PM2.5 低字节 |
B11 | 数据 | PM10 高字节 |
B12 | 数据 | PM10 低字节 |
B13 | 数据 | Temperature 整数部分 |
B14 | 数据 | Temperature 小数部分 |
B15 | 数据 | Humidity 整数部分 |
B16 | 数据 | Humidity 小数部分 |
B17 | 数据 | illumination高字节 |
B18 | 数据 | illumination 低字节 |
B19 | 数据 | 气压高字节 |
B20 | 数据 | 气压低字节 |
B21 | 数据 | 状态1 |
B22 | 数据 | 状态2 |
B23 | 校验和 | 校验和 |
也就是说接收的数据帧就是由上述数据组成。
我们再来看一下结构体的定义:我们声明的结构体只是描述了对象是由什么组成,并未创建实际的数据对象。因此我们也可以把结构声明为模板,该模板勾勒出这个结构是如何存储数据的。
2. 结构体基础知识
关键字:struct 它表明了跟在其后的是一个结构,后面是一个可选择的标记。所以在申明结构体标量时可以这样申明:
struct UploadFormat deviceuploadformat;
我们将deviceuploadformat声明为UploadFormat结构布局的结构变量。
在结构体中花括号括起来的是结构成员列表,成员都用自己的声明来描述,可以是任意一种C的数据类型,也可以是其他结构。右花括号后面的分号表示结构局部定义结束。同样将结构体也可以声明在函数的外部和内部,外部所有函数都可以使用他的标记UploadFormat,内部只有当前函数可以使用标记UploadFormat。
2.1 定义结构变量
结构布局只是告诉编译器如何表示数据,并未让编译器为数据分配空间。
struct UploadFormat
{
uint8 head[2];
uint16 eco2;
uint8 checksum;
};
而定义结构变量则是为该变量分配空间。
struct UploadFormat deviceuploadformat;
而就计算机而言,上述声明等同于下述声明:
struct UploadFormat
{
uint8 head[2];
uint16 eco2;
uint8 checksum;
} deviceuploadformat;
同样声明结构体和定义结构体变量也可以合成一步:
struct
{
uint8 head[2];
uint16 eco2;
uint8 checksum;
}deviceuploadformate;
注意:如果要多次使用该结构体模板就需要进行标记,或者使用typedef。
2.2 结构体初始化
我们初始化变量和数组的时候是直接赋值,那么初始化结构体也是一个道理:
typedef struct{
__IO uint16_t SetTemp; //设定目标 Desired Value
__IO uint16_t SumError; //误差累计
__IO uint16_t Proportion; //比例常数 Proportional Const
__IO uint16_t Integral; //积分常数 Integral Const
__IO uint16_t Derivative; //微分常数 Derivative Const
__IO float Proportion_Current; //比例常数 Proportional Const
__IO float Integral_Current; //积分常数 Integral Const
__IO float Derivative_Current; //微分常数 Derivative Const
__IO float LastError; //Error[-1]
__IO float PrevError; //Error[-2]
}PID_TypeDef;
PID_TypeDef PidChannel_0 = {
.SetTemp = 50, //设定目标 Desired Value
.LastError = 0, //Error[-1]
.PrevError = 0, //Error[-2]
.Proportion = 80, //比例常数 Proportional Const
.Integral = 0, //积分常数 Integral Const
.Derivative = 80, //微分常数 Derivative Const
.Proportion_Current = 50,
.Integral_Current = 0,
.Derivative_Current = 1
};
简而言之,在使用一对花括号括起来初始化,各初始化项用逗号分隔
因此对于上述数据描述的23个字节,我们可以将其定义为这样一个结构体。
struct UploadFormat
{
//帧头
uint8 head[2];
uint16 eco2;
uint16 ech2o;
uint16 tvoc;
uint16 pm25;
uint16 pm10;
uint16 temperature;
uint16 humidity;
uint8 conrtol1;
int8 control2;
uint8 eco2alarm:1;
uint8 ech2oalarm:1;
uint8 tvocalarm:1;
uint8 pm25alarm:1;
uint8 pm10alarm:1;
uint8 tempalarm:1;
uint8 humidityalarm:1;
uint8 : 1;
uint8 light0:1;
uint8 light1:1;
uint8 light2:2;
uint8 light3:3;
uint8 :4;
//校验位
uint8 checksum;
};
2.3 嵌套结构
如果我还有一个同样的设备呢?这就需要嵌套结构。也就是在结构中包含另一个结构。
struct UploadFormat
{
//帧头
uint8 head[2];
struct DEVICE_UPLOAD
{
uint16 eco2;
uint16 ech2o;
uint16 tvoc;
uint16 pm25;
uint16 pm10;
uint16 temperature;
uint16 humidity;
uint8 conrtol1;
int8 control2;
uint8 eco2alarm:1;
uint8 ech2oalarm:1;
uint8 tvocalarm:1;
uint8 pm25alarm:1;
uint8 pm10alarm:1;
uint8 tempalarm:1;
uint8 humidityalarm:1;
uint8 : 1;
uint8 light0:1;
uint8 light1:1;
uint8 light2:2;
uint8 light3:3;
uint8 :4;
}device1,device2;
//校验位
uint8 checksum;
};
上述是结合起来的,拆分开就是如下形态:
struct DEVICE_UPLOAD
{
uint16 eco2;
uint16 ech2o;
uint16 tvoc;
uint16 pm25;
uint16 pm10;
uint16 temperature;
uint16 humidity;
uint8 conrtol1;
int8 control2;
uint8 eco2alarm:1;
uint8 ech2oalarm:1;
uint8 tvocalarm:1;
uint8 pm25alarm:1;
uint8 pm10alarm:1;
uint8 tempalarm:1;
uint8 humidityalarm:1;
uint8 : 1;
uint8 light0:1;
uint8 light1:1;
uint8 light2:2;
uint8 light3:3;
uint8 :4;
};
struct UploadFormat
{
//帧头
uint8 head[2];
struct DEVICE_UPLOAD device1;//嵌套结构
struct DEVICE_UPLOAD device2;//嵌套结构
//校验位
uint8 checksum;
};
如果访问结构体中的一个成员就要如下:
struct UploadFormat deviceuploadformat;
struct DEVICE_UPLOAD device1;
deviceuploadformat.device1.eco2;
2.4 位域
位域
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:
struct 位域结构名
{ 位域列表 };
其中位域列表的形式为: 类型说明符 位域名:位域长度
2.5 指向结构的指针
我们主要了解以下问题:
1. 如何定义指向结构的指针;
2.如何用这样的指针访问结构体成员;
2.5.1 声明和初始化结构指针
struct UploadFormat *msg;
声明结构体指针主要有关键字struct,结构体标记,星号(*)和指针名称组成。
这个申明并没有创建一个新的结构,但是其指针可以指向任意的现有的UploadFormat类型结构。如果deviceuploadformat为UploadFormat类型的结构变量。那么
msg = &deviceuploadformat;
2.5.2 使用指针访问成员
msg -> head[0];
它等同于:
deviceuploadformat.head[0];
也等同于:
(msg*).head[0];
3. 向函数传递结构信息
3.1 对形参使用const
至于为什么要产地数组的指针呢? 如果编写的是类似int型的函数,通常直接传递数值就可以了,但是对于数组的传递,也只能传递指针,这也是必须的,因为传递指针的效率要更高。因此如果一个函数按值传递数组,那就需要分配足够的空间来存储原数组的副本,然后把原数组所有数据拷贝至新的数组中。而把数组的地址传递给函数,让函数直接处理原数组的值效率更高。
但是传递地址又会产生一些问题,如果按值传递,他的优点是可以保证数据的完整性,如果函数使用的是原始数据的副本,就不会产生原始数据被修改的情况。但是,处理数组的函数通常都是用来处理原始数据,因此传地址有可能修改原始数据。
有时可能需要对原始数据进行修改,但不需要修改原始数据的时候,错误的编程很有可能破坏原始数据。对此,为了避免类似的错误,我们可以对不想改变原始数据的数组添加关键字const。
使用const可以保护数组的数据不被破坏,因此如果编写的函数需要修改数组,在声明数组形参时不使用const,如果编写的函数不用修改数组,则声明数组形参时最好加上const。
向函数传递数据帧也就是数组的地址,再将数组变为结构体指针。
void Uart0FrameHandle(uint8 idata *line)
{
}
如果要编写一个结构体相关的函数,使用结构作为参数还是使用指针作为参数呢?
首先使用指针作为参数的优点是:执行效率快,只需要传递地址;缺点是不能够保护数据,被调函数容易影响原来结构中的数据。通过增加const限定符可以解决。const只能读不能写。加了const限定后,程序运行过程中就不能修改数组的内容了。
传递结构直观清晰,但是浪费时间和存储空间,对于较大的结构,传递指针或者传递函数所需要的成员更合理。
3.2 将数组强制转换为结构体指针
struct UploadFormat *msg = (struct UploadFormat *)line;
我们将数组line强制转化为结构体UploadFormat类型,只是从语法上来说类型进行了改变而已,用新的类型的方式来解释原来内存中的值,即是让结构体按照自己的属性重新读取数组中的数据。
上面的语句分开写就是这样:
void Uart0FrameHandle(uint8 idata *line)
{
struct UploadFormat *msg; //定义一个结构体指针
msg = (struct UploadFormat *)line;//将数组指针的地址强制转换为结构体指针,并赋值给msg
}
同时应该注意的是:由于计算机存储的模式(大端模式/小端模式)以及计算机中字节对齐的问题可能会导致读出来的数据不对。这个是需要大家注意的地方。
这样的操作好处是将接收的数据帧与定义的结构体数据类型进行了匹配。因此我调用结构体数值的同时也就意味着对其数据帧的调用。例如
void Uart0FrameHandle(uint8 idata *line)
{
struct UploadFormat *msg = (struct UploadFormat *)line;
//将数组指针的地址强制转换为结构体指针,并赋值给msg
int16 co2parameter;
int8 checksum;
bit light0state;
co2parameter = msg -> device1.eco2;
checksum = msg -> checksum;
light0stste = msg -> device2.lightstate;
}
这样做的好处是,当数据增添,或者数据类型修改,只需要修改结构体数据类型以及接收数据帧的数据长度即可。不必再纠结以提取数组时数据位置的人工编排,大大提高了数据的可读性和可操作性,非常值得推荐。
4. 联合体介绍
4.1 需求说明
介绍完数据的接收,在介绍一下数据的发送。发送数据帧定义为六个字节:
帧头1 帧头2 功能帧 数据帧1 数据帧2 校验和
我们可以针对上述数据帧的类型,抽象出一个结构体。
但是数据帧1和数据帧2在不同功能下的数据类型是不同的。可能是int8、uint8、int16和uint16。
针对这种情况就需要用到联合体来共同构建我们想要的数据类型。
4.2 联合体介绍
联合union是一个数据类型,它能够在同一个存储空间存储不同的数据类型(不是同时储存)。
union DATA{
int16 i16;
uint8 u8[2];
}cdata;
或者这样申明:
union DATA{
int16 i16;
uint8 u8[2];
};
union DATA cdata
上面形式声明的结构可以存储一个int16类型,两个uint8类型。应该注意的是,所声明的联合体只能存储一个int16类型,或者两个uint8类型。
因此在结构体中引入联合体就会使数据类型变得丰富。
struct SendFormat
{
uint8 head[2];
uint8 cmd;
union DATA{
int16 i16;
uint8 u8[2];
}cdata;
uint8 checksum;
};
或者把联合体放在外面:
union DATA{
int16 i16;
uint8 u8[2];
}cdata;
struct SendFormat
{
uint8 head[2];
uint8 cmd;
UNION DATA cdata;
uint8 checksum;
};
对于发送数据我们对该结构体定义一个结构体指针。
void send_cmd(struct SendFormat *msg)
{
msg->head[0] = 0x17;
msg->head[1] = 0x07;
msg->checksum = CheckSum((uint8 *)msg, 5);
Uart0SendStr((uint8 *)msg, 6);
memset(msg,0,sizeof(struct SendFormat));
}
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法。
首先建立结构体类型变量。
struct SendFormat idata sendmsg;
调用的话就如下调用即可。
sendmsg.cmd = Order_MOTOY;
sendmsg.cdata.i16 = EN_PARA_MidStep;
sendmsg.cdata.u8[0] = 0;
sendmsg.cdata.u8[1] = EN_AUTO_SCAN;