在嵌入式编程时,常常会遇到需要做数据通信的场景。单片机往往只支持一次8位的数据传递,为了传输较长的数据类型,只能先在主机将数据拆分,再在从机重新组合,这里介绍一些实用的数据拆分组合方法
文章目录
- 一、数据类型分析
- 1、类型长度
- 2、类型存储方法
- (1)整形
- 1、补码存储
- 2、unsigned修饰
- (2)浮点形
- 二、数据类型的拆分与合并
- (1)利用位运算
- (2)利用指针
- 1、数据的拆分
- 2、数据的拼接
- (1)分析
- (2)一种常见错误
一、数据类型分析
1、类型长度
C/C++中有多种数据类型,但不管什么类型的数据都是以二进制形式存储的,在不同的系统和编译器中,各种类型转换为二进制后的长度有时会不一样,可以利用sizeof函数来查看你的环境中数据类型的长度,如下:
#include "iostream"
#include "iomanip"
using namespace std;
int main()
{
cout<<left;
cout<<setw(18)<<"char:"<<sizeof(char)<<endl;
cout<<setw(18)<<"unsigned char:"<<sizeof(unsigned char)<<endl;
cout<<setw(18)<<"short:"<<sizeof(short)<<endl;
cout<<setw(18)<<"unsigned short:"<<sizeof(unsigned short)<<endl;
cout<<setw(18)<<"int:"<<sizeof(int)<<endl;
cout<<setw(18)<<"unsigned int:"<<sizeof(unsigned int)<<endl;
cout<<setw(18)<<"long:"<<sizeof(long)<<endl;
cout<<setw(18)<<"unsigned long:"<<sizeof(unsigned long)<<endl;
cout<<setw(18)<<"float:"<<sizeof(float)<<endl;
cout<<setw(18)<<"double:"<<sizeof(double)<<endl;
return 0;
}
运行程序,可以看到我的环境中各数据类型的长度如下
2、类型存储方法
从上面的示例中我们注意到,unsigned关键字不会改变类型长度,而且unsigned只能修饰整形数据,这些都是C/C++中类型存储方法决定的。
(1)整形
1、补码存储
整形数据在内存中用补码形式存储,补码定义最高位为符号位,0代表非负数,1代表负数。具体转换方法如下
非负数 | 负数 |
直接转换为二进制,高位用0填充 | 先得到此负数绝对值的补码(直接转二进制),然后最高位置1,再把除了最高位以外的所有数取反,最后对结果再加1 |
举例来说,定义一个int型的变量a
a=4 | a=-4 |
补码0000 0000 0000 0000 0000 0000 0000 0100 | 补码1111 1111 1111 1111 1111 1111 1111 1100 |
a=4的情形一目了然,这里分析一下a=-4的情况是怎么得到的
- 首先得到a绝对值4的补码,注意int是4个字节:0000 0000 0000 0000 0000 0000 0000 0100
- 符号位置1:1000 0000 0000 0000 0000 0000 0000 0100
- 除符号位以外全部取反:1111 1111 1111 1111 1111 1111 1111 1011
- 整体加一:1111 1111 1111 1111 1111 1111 1111 1100
在C/C++中,用十六进制或八进制输出数据,即可看到补码的效果
#include "iostream"
#include "iomanip"
using namespace std;
int main()
{
int a=-4;
int b=4;
cout<<left<<hex;
cout<<a<<endl<<b<<endl;
return 0;
}
结果如下图所示,与我们的分析相符
2、unsigned修饰
unsigned关键字强制程序不考虑符号位,但不会改变整形数据补码存储的存储方式。也就是说,程序会按上面的方法将变量值转换为补码,然后直接转为十进制数。
在这种情况下,-4会先被存为0xfffffffc,再转十进制为4294967292,这一点也可以编程验证
#include "iostream"
#include "iomanip"
using namespace std;
int main()
{
unsigned int a=-4;
cout<<left;
cout<<a<<endl;
cout<<hex<<a<<endl;
return 0;
}
因此unsigned修饰过的数据类型与未经修饰过类型长度一样就显而易见了
(2)浮点形
浮点型数据采用IEEE格式,与整形的存储格式完全不同,也不能用unsigned进行修饰,具体可以参考这篇文章:
单双精度浮点数的IEEE标准格式
二、数据类型的拆分与合并
(1)利用位运算
说道数据的拆分与合并,本质上就是把数据按8位长度拆开与拼装,首先想到的就是利用位运算处理。
按位与&
运算可以用来拆分,按位或|
运算可以用来合并,关于位运算可参考我的这篇文章:C语言位运算应用实例
下面是一个利用位运算拆分与合并4字节长long型数据的例子
#include "iostream"
#include "iomanip"
using namespace std;
typedef unsigned char u8;
typedef long s32;
//拆分数据
void dataSplit(s32 data,u8 *buf)
{
s32 temp=0xFF;
for(int i=0;i<4;i++)
buf[i]=(data & temp<<8*i)>>8*i;
/*
buf[0]=data & 0xFF;
buf[1]=(data & 0xFF00)>>8;
buf[2]=(data & 0xFF0000)>>16;
buf[3]=(data & 0xFF000000)>>24;
*/
}
//拼接数据
void dataAssmeble(s32 *data,u8 *buf)
{
s32 temp=buf[3];
for(int i=2;i>=0;i--)
temp=(temp<<8)|buf[i];
*data=temp;
}
int main()
{
s32 a=-1024;
s32 res;
u8 buf[4];
dataSplit(a,buf); //拆分,主机可以发送
dataAssmeble(&res,buf); //合并,从机接收后可以拼装
cout<<"原始数据:"<<a<<endl;
cout<<"拆分合并后:"<<res<<endl;
return 0;
}
可见处理正确
需要注意的一点是,这种方法只适用于处理整形数据,因为浮点型数据的存储比较特殊,强行规定了各个位域的含义,若直接进行位运算取出一部分,取出的数据无法被正确解释,所以浮点型不能直接位运算,也就不能直接用这种方法处理
看到这里有人可能会想:若想先把浮点型转成整形,处理后再转回来不就好了吗。注意,别忘了浮点型转整形时会丢失精度,这个方法也不太好
(2)利用指针
大家应该注意到了,这个问题在本质上涉及到如何把内存中的数据解释成变量值,在这一点上或许指针可以帮到我们。我们知道,当你用一个指针指向内存中的一段数据,这段数据就会被解释为这个指针的类型的变量值。这启发我们用以下方法处理:
- 不要进行任何类型转换,以免破坏原始数据
- 找到一个方法,可以在不破坏数据的情况下将其拆开为8位一组,这个需要利用指针
- 现在可以进行数据传输
- 接收到数据后,用位运算把它们按序拼接为一个足够长的整形
- 定义一个原变量类型的指针,指向拼接成的整形的地址
- 取出指针指向的变量值,这就是被发送的原始变量的值了
示例程序如下:
#include "iostream"
#include "iomanip"
using namespace std;
typedef unsigned char u8;
typedef float f32;
typedef unsigned long u32;
//拆分数据
void dataSplit(f32 data,u8 *buf)
{
for(int i=0;i<4;i++)
buf[i]=(*((u8 *)(&data)+i));
}
//拼接数据
f32 dataAssmeble(u8 *buf)
{
u32 temp=buf[3];
for(int i=2;i>=0;i--)
temp=(temp<<8)|buf[i];
f32 *data=(f32*)(&temp);
return *data;
}
int main()
{
f32 a=-3.456;
f32 res;
u8 buf[4];
dataSplit(a,buf); //拆分
res=dataAssmeble(buf); //合并
cout<<"原始数据:"<<a<<endl;
cout<<"拆分合并后:"<<res<<endl;
return 0;
}
注意拆分与拼接的程序出现了变化,下面详细分析一下
1、数据的拆分
//拆分数据
void dataSplit(f32 data,u8 *buf)
{
for(int i=0;i<4;i++)
buf[i]=(*((u8 *)(&data)+i));
}
- (&data)取出原始数据data的地址
- (u8 *)(&data),用一个u8(即unsigned char)型指针指向这个地址
- ((u8 *)(&data)+i),指针加减法会移动指向位置,这里按u8长度为一个单位进行移动,从而依次指向原始数据中的每一段u8数据
- (*((u8 *)(&data)+i)),将这个指针的值取出,也就是取出了原始数据中的每一段u8数据的值
2、数据的拼接
(1)分析
//数据拼接
f32 dataAssmeble(u8 *buf)
{
u32 temp=buf[3];
for(int i=2;i>=0;i--)
temp=(temp<<8)|buf[i];
return *(f32*)(&temp);
}
- 将若干u8数据用位运算拼成一个长度相同的整形数据temp
- (&temp) 把temp的地址取出
- (f32*)(&temp),用一个(f32*)类型指针指向这个地址,确认了解释方法
- (f32)(&temp)取出这个指针的值
(2)一种常见错误
需要注意的是,以下写法是错的!
//错误的拼接
void dataAssmeble(f32 *data , u8 *buf)
{
u32 temp=buf[3];
for(int i=2;i>=0;i--)
temp=(temp<<8)|buf[i];
data=(f32*)(&temp);
}
//-----------------------------------------
//和这个犯得是一样的错误
void func(int a)
{
a=10;
}
还记得初学C/C++时函数传参那部分的内容吗?看下面的fun函数,它并不能对a的实参产生影响,因为这里只是对局部变量形参a做了赋值,函数func退出后就没了
上面的程序也是一样的错误,我们只是对局部的形参指针*data进行了赋值,并没能影响实参指针
上面func函数的错误可以用指针来解决,dataAssmeble函数也可以类似地用双指针解决,参考如下:
//类似这种方法改正
void func(int *a)
{
*a=10;
}
//--------------------------------------
#include "iostream"
#include "iomanip"
using namespace std;
typedef unsigned char u8;
typedef float f32;
typedef unsigned long u32;
void dataSplit(f32 data,u8 *buf)
{
for(int i=0;i<4;i++)
buf[i]=(*((u8 *)(&data)+i));
}
void dataAssmeble(f32 **data , u8 *buf)
{
u32 temp=buf[3];
for(int i=2;i>=0;i--)
temp=(temp<<8)|buf[i];
f32 *p=(f32*)(&temp);//暂存
**data=*p;
}
int main()
{
f32 a=-3.456;
f32 res;
f32 *p=&res;//暂存
u8 buf[4];
dataSplit(a,buf); //拆分
dataAssmeble(&p,buf); //合并
cout<<"原始数据:"<<a<<endl;
cout<<"拆分合并后:"<<res<<endl;
return 0;
}
欢迎讨论~