现代的计算机系统一般采用 字节(Octet, 8 bit Byte)作为逻辑寻址单位,当物理单位的长度大于 1 个字节时,就要区分字节顺序(Byte Order, or Endianness)。
字节序,即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。字节序也用于描述多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序,常见的字节序有 大端模式(Big Endian) 和 小端模式(Little Endian) 两种,还有一种不太常见的 中端模式(Middle-Endian)。
大端模式: 高字节数据存储在低地址,低字节数据存储在高地址。
小端模式: 高字节数据存储在高地址,低字节数据存储在低地址。
中端模式: 比如以 4 个字节为例,比如以 3-4-1-2 或 2-1-4-3 这样的顺序存储的就是中端模式,这种存储顺序偶尔会在一些小型机体系中的十进制数的压缩格式中出现,但是 ARM 架构平台一般不使用这种模式,这里不做讲解。
假设一个长度为 5 byte 数据 0x0102030405,大端模式下字节序为
Low Address ----------> High Address
+----------------------------------+
| 0x01 | 0x02 | 0x03 | 0x04 | 0x05 |
+----------------------------------+
其中高字节(左边的位)0x01
处于低地址(High-byte first)。
假设一个长度为 5 byte 数据 0x0102030405,小端模式下字节序为
Low Address ----------> High Address
+----------------------------------+
| 0x05 | 0x04 | 0x03 | 0x02 | 0x01 |
+----------------------------------+
其中低字节(右边的位)0x05
处于低地址(Low-byte first)。
在各种计算机体系结构中对于字节,字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
所以在通信之前我们需要事先确定通信双方的字节序,如果双方的字节序不同则需要协商字节序比如以小端模式为准,此时如果发送方为大端模式则要将数据转换为小端模式进行发送。检测平台上字节序有以下两种方法。
方法一:
#include
#include
/**
* union 共用体所有成员公用一个空间,
* 空间为共用体中的最大类型
*/
typedef union {
unsigned short data;
unsigned char buf[2];
} Data;
int main()
{
Data Hello = {0};
Hello.data = 0x0102;
if (
(Hello.buf[0] == 0x01) &&
(Hello.buf[1] == 0x02)
) {
printf("Big Endian\n");
}
if (
(Hello.buf[0] == 0x02) &&
(Hello.buf[1] == 0x01)
) {
printf("Little Endian\n");
}
return 0;
}
方法二:
const int endian = 0x01;
#define is_bigendian() ((*(char *)&endian) == 0)
#define is_littlendbian() ((*(char *)&endian) == 1)
我们知道单片机串口发送一帧的数据量为 1 个字节(8 位),这意味着串口发送的一帧数据能够表达(发送)的数值范围为 -128~127,或 0~255。
其实无论是串口还是并口都是一样的,对于 8 位并口发送一帧的数据量也为 1 个字节(8 位)能够表示的数值范围也为 -128~127,或 0~255。16 位并口发送一帧的数据量为 2 个字节(16 位)能够表示的数值范围为 -32768~32767,或 0~65535 超过该范围也无能为力。
所以无论使用什么接口或传输方式一帧数据能表达的数值范围都是有限的,所以为了能够传输更广的数值范围我们需要将数值规则拆分以分次传输。
事实上串口传输数值无论传输的是浮点(4/8 byte)还是整型(2/4/8 byte)类型数值,只要数值范围超过 1 个字节(8 位)串口都无法直接完整传输。
所以我们需要将数值按照一定的规则进行拆分,比如将浮点数值从高到低位拆分为 4 个字节(byte1~byte 4)来分 4 次传输,第 1 帧传输第 1 个字节,第 2 帧传输第 2 字节以此类推(这也是前面讲解字节序的意义)。这很类似使用串口发送字符串,因为字符串就是由多个字节组成的,然后每帧发送一个字符(1 个字节)。
usart_send_data("Hello, World!");
可以定义结合一些协议,在协议中告诉接收端接收的数值由 4 个字节组成,要求接收端按照 4 个字节来还原原始数值。结合协议可以让传输程序更灵活,可以发送和接收解码任意字节长度的数值。
前面了解了传输浮点数值的原理,接下来了解一下浮点数值如何按字节拆分。浮点数值按字节的拆分方法有两种,分别为利用指针和联合体的方式进行实现,不过联合体(Union)需要依赖特定编程语言(例如 C 语言)的特性进行实现。
我们可以定义一个 char
类型的指针,我们知道 char
类型指针可以逐字节遍历内存中数据,按照该原理我们可以将 char
型指针指向占用 8 个字节空间 double
类型的浮点数据(注意此过程需要强制转换),此时就可以利用指针来逐字节遍历或读取 double
类型包含的 8 个字节数据内容了,如下。
#include
double send_value = 0.0;
unsigned char * send = NULL;
double recv_value = 0.0;
unsigned char recv[8];
int main()
{
send_value = 3.14159;
send = (unsigned char *)&send_value;
printf("send:%lf\n", send_value);
// Send and receive data
for (unsigned char i = 0; i < 8; i++) {
printf("byte%d:%x\n", i, *send);
recv[i] = *send;
send++;
}
recv_value = *((double *)&recv[0]);
printf("recv:%lf\n", recv_value);
return 0;
}
对于 C 语言来说可以利用 Union(联合体),由于联合中的所有元素会共用同一段内存。定义一个联合体,联合体内有一个 double
类型数据和一个 8 个字节大小的 char
类型数组。因为 double
类型数值需要占用 8 个字节,所以 double
数值占用的空间和长度为 8 的 char
类型数组占用的空间是相同的。
有意思的是联合体会将 double
类型的数值拆解为 8 个字节并按照当前平台的字节序排列放置到长度为 8 的数组中(共用内存机制),这样我们可以省略前面指针方式的一个数据强制转换问题,直接使用联合体的数组进行发送即可,如下。
#include
#define MAX_LENTH 8
union send {
char byte[MAX_LENTH];
double value;
};
union recv {
char byte[MAX_LENTH];
double value;
};
int main()
{
send send_value;
recv recv_value;
send_value.value = 3.14159;
recv_value.value = 6.28318;
printf("send:%lf\n", send_value.value);
printf("recv:%lf\n", recv_value.value);
// Send and receive byte
for (unsigned char i = 0; i < MAX_LENTH; i++) {
printf("Send byte:%d\n", i);
recv_value.byte[i] = send_value.byte[i];
printf("Recv byte:%d\n", i);
}
printf("recv:%lf\n", recv_value.value);
return 0;
}