参考文章

https://blog.csdn.net/weixin_44057803/article/details/130670865

一、为什么会有大小端之分?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

字节顺序,又称端序或尾序(英语:Endianness)。在计算机科学计算机科学”)领域中,是跨越多字节的程序对象的存储规则。
在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x地址为0x100,那么其对应地址表达式&x的值为0x100。且x的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。

二、大端模式和小端模式

在计算机中一般讲字节序分为两类:Big-Endian(大端字节序) 和 Little-Endian(小端字节序)。

举一个例子,比如数字0x12 34 56 78在内存中的表示形式。

a)大端模式: Big-Endian 高位字节在前,低位字节在后。

也就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(其实大端模式才是我们直观上认为的模式,和字符串存储的模式差类似)

低地址 ——————–> 高地址
0x12 | 0x34 | 0x56 | 0x78

b) 小端模式:Little-Endian 低位字节在前,高位字节在后。

与大端存储模式相反,也就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 ——————–> 高地址
0x78 | 0x56 | 0x34 | 0x12

c) 网络字节序:TCP/IP各层协议将字节序定义为Big-Endian,因此TCP/IP协议中使用的字节序通常称之为网络字节序。

三、举例

(1)例如,16位宽的数0x1234在小端模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

而在大端模式CPU内存中的存放方式则为:

放一起比较直观:

(2)32bit宽的数0x12345678在Little-endian模式以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:

(3)大端小端没有谁优谁劣,各自优势便是对方劣势:

小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。

五、数组在大端小端情况下的存储

以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
  Big-Endian: 低地址存放高位,如下:
高地址
—————
buf[3] (0x78) – 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) – 高位
—————
低地址
Little-Endian: 低地址存放低位,如下:
高地址
—————
buf[3] (0x12) – 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) – 低位
————–
低地址

六、如何判断编译器的大小端模式?

我们常用的X86结构是小端模式,而KEILC51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

Note:采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。
(我的理解:小端模式在低字节就放一个低位)
(1)下面这段代码可以用来测试一下你的编译器是大端模式还是小端模式:

int main() { short int x; char x0,x1; x=0x1122; x0=*((char*)&x); //低地址单元 ,或者((char*)&x)[0]; x1=*((char*)&x + 1); //高地址单元,或者((char*)&x)[1]; printf("x0=%x\nx1=%x\n",x0,x1); }

若x0=0x11,则是大端; 若x0=0x22,则是小端…
(2)通过强制类型转换截断

BOOL IsBigEndian() { short a = 0x1234; char b = *(char*)&a; if(0x12 == b) { return TRUE; } return FALSE;}

(3)利用联合体共享内存的特性,截取低地址部分

 BOOL IsBigEndian(){ union NUM { shorta; char b; }num;num.a = 0x1234;if(0x12 == num.b) { return TRUE; }return FALSE;}

实际应用-光流通信

光流通信协议PDF

本文主要内容:详细介绍如何从0开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,帮助我们调节一些参数,比如电机PID的调节、波形融合等,以及在我们写通信协议的时候可能遇见的问题或注意事项进行解答,本文主要以匿名上位机为例,新手和小白也可以实现。

一、准备工作:

1、要有该上位机或者虚拟示波器的通信协议或者说通信帧格式
  

只有知道了上位机或者虚拟示波器的通讯格式,双方才能进行通信,就像平常我们的交流一样,交流双方只有在相同语言基础上才能进行交流,再如谍战剧里面收发电报的双方按照同样的规则对数据或者说电波进行解读才能进行信息的传递。

2、要知道你用来发送数据的单片机或者其他设备是大端模式还是小端模式。

3、确保你单片机的串口可以正常的发送数据

可以让单片机通过串口发送一个简单的数据,用串口助手或者其他软件(本文的例子匿名上位机是可以查看的)查看数据是否正确,也就是保证串口可以正确的发送数据,这一步很重要,可以避免不少麻烦,比如我在写数据通信的程序的时候,上位机的数据校验一直失败,我就觉得是数据通信程序哪里写错了,不断地检查,改来改去,最后发现通讯程序是对的,问题出在串口上,我在下载程序到单片机的时候,内部振荡器的频率设置错了,除此之外波特率也是经常出错的地方。

二、进入正题,开始写数据通讯的程序:

1、定义一个数组用来存放准备发送的数据,因为是按字节进行发送的,所以定义为uint8 即8位无符号整型就可以了,一般来说100个容量肯定够用了

uint8 data_to_send[100];

2、根据你要发送数据的类型和个数,确定函数的参数类型和个数,下面以四个uint16型数据为例。

void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d) //F1帧4个 uint16 参数

3、根据通讯真格式将要发送的数据先存放在你定义的数组里,这一步呢我们先把DATA区之前的放到数组里。(为了阅读方便,我把本文例子匿名上位机的灵活通讯帧格式在下面再放一次)

uint8 _cnt = 0;uint8 sumcheck = 0; //和校验uint8 addcheck = 0; //附加和校验uint8 i=0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0XFF;data_to_send[_cnt++] = 0XF1;data_to_send[_cnt++] = 8; //DATA区数据长度

解释一下, _cnt是用来确定当前数据存放在数组的位置,sumcheck和addcheck分别是和校验和附加校验,这三个量都要初始化为0。i是下文进行校验时用到的循环变量。

按照本例中的通讯帧格式,我们首先要发送0xAA,把它放在我们设定的数组 data_to_send【0】中,此时_cnt加1变为1,为存放下一个数据做准备,接着把要发送的 0XFF和0XF1(当然本例用的是F1帧,匿名上位机共F1到FA共十个用户帧供我们选择)放在数组的data_to_send【1】、data_to_send 【2】中,

接下来要存放的就是DATA区的数据长度了,以字节为单位,比如uint8或者int8是一个字节的, uint16或者int16是两个字节的,uint32或者int32是四个字节的。本例的匿名上位机v7.00暂不支持float型数据,可以吧你要发的float型数据乘以10、100、1000…等转化成整数,用uint32或者int32进行传输,本例中我们发送的是四个int16型数据,即2+2+2+2=8,就将8放在data_to_send【3】中。

4、对数据拆分的介绍(在此步中介绍为啥要确定你所用的单片机是小端模式还是大端模式)

一般呢我们用串口传输数据都是按字节传输的,你买的单片机带的串口发送库函数一般也是按字节进行发送的,我们知道uint8或者int8是一个字节的,就可以直接把数据按顺序从data_to_send【4】开始放在数组里,那么对于uint16、int16、uint32、int32这种多字节的数据就得进行数据拆分,比如uint16、int16拆成两个uint8、int8类型的数。同理uint32、int32拆成四个。大多数的数据通信采用了一下拆分方法。

 #define BYTE0(dwTemp) (*(char *)(&dwTemp))#define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))#define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))#define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3))

大致的处理过程是 对变量 dwTemp 去地址,然后将其强制转化成char类型的指针 最后再取出指针所指向的内容,这样就完成了对数据的拆分工作。

简单通俗点说,举个例子,比如uint16或者int16类型的数据1000,把他转换成二进制格式就是 0000 0011 1110 1000 共16位,高八位为 0000 0011,低八位为 1110 1000 ,在前面我们介绍了大端模式和小端模式的区别,即小端系统,高位值存于高地址,低知位值存于低地址。大端系统,与小端系统相反,高位值存于低位地址,低位值存于高位地址。

也是就说对于小端模式 调用BYTE0(1000)就可以获取数据1000的低八位1110 1000 ,BYTE1(1000)就可以获取1000的高八位高八位为 0000 0011 。对于大端模式 调用BYTE0(1000)就可以获取数据1000的高八位 0000 0011,BYTE1(1000)就可以获取1000的低八位高八位为1110 1000 。

5、有了以上数据拆分的基础,接下来就可以把DATA区的数据放到数组里。

还有一点需要注意由于本例中的匿名上位机DATA 数据内容中的数据,采用小端模式,低字节在前,高字节在后。 什么意思呢,这里的小端模式跟上文介绍的单片机的大小端模式无关,不是指同一个东西,这里的小端模式理解为我们向上位机发送数据的时候要先发送数据的低字节,再发送数据的高字节,因此我们在把DATA区的数据放到数组data_to_send里是要先放低字节,拿上文中的数据1000为例要先放1110 1000 再放 0000 0011,即对于小端模式的单片机先调用BYTE0(1000) ,再调用BYTE1(1000),而对于大端模式的单片机先调用BYTE1(1000),再调用BYTE0(1000)。因此对于本例传递4个int16类型的数据区代码如下:

大端模式的单片机(比如常见的51单片机):

data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_d);data_to_send[_cnt++] = BYTE0(_d);

小端模式的单片机(比如常见的32单片机):

data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_d);data_to_send[_cnt++] = BYTE1(_d);

之前我写的时候就是因为没有区分大小端系统,我当时用的是STC8G单片机,本质上可以说属于51系列单片机,是大端模式的,我按照小端模式去写,结果就是虽然可以通过上位机的和校验、附加校验(道理很假单:加法交换律),但是上位机收到的数据指定是错误的。

5、接下来就是和校验、附加校验的计算和将其放到数组里了。

先简单介绍一下什么是和校验、附加校验(数据校验的目的是保证数据的有效性,完整性,准确性,避免因噪声或其他干扰造成的数据丢失造成的数据错误)

和校验计算方法: 从帧头 0xAA 字节开始,一直到 DATA 区结束,对每一字节进行累加操作,只取低 8 位 。
  附加校验计算方法: 计算和校验时,每进行一字节的加法运算,同时进行一次 和校验的累加操作,只取低 8 位。

不明白的话,看下面的程序就很容易理解了:

for ( i = 0; i < data_to_send[3]+4; i++)
{
sumcheck += data_to_send[i]; //和校验
addcheck += sumcheck; //附加校验
}

data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;

在此解释一下为啥是 i < data_to_send[3]+4,根据和校验的定义是从帧头 0xAA 字节开始,一直到 DATA 区结束,而data_to_send[3]里面放的就是DATA区的数据个数而在DATA区之前还有0xAA、0xFF、0xF1、8(针对本文的例子而言) 这4个数据,所以是 data_to_send[3]+4。

计算完后自然就是将其按照顺序放到数组data_to_send里了,到这里我们要发送的数据都按照通讯帧格式放到数组data_to_send里了。多说一句,DATA区里的数据才是我们真正想让上位机或者虚拟示波器显示的数据,发送的其他数据都是为了其能够正确发送和接收而设立的。

6、最后一步:通过串口把data_to_send里的数据按字节依次发送到上位机或者虚拟示波器。串口发送函数因单片机不同,可能不同,大家改为自己单片机的就行,以下是我的单片机的串口发送程序:

uart_putbuff(DEBUG_UART,data_to_send,_cnt);//函数功能:将data_to_send里的_cnt个数据,通过串口DEBUG_UART发送到上位机)

到这里我们的数据通信程序就写完了,建立工程时将调用的数据拆分代码和函数的定义放在h文件中,如

#ifndef __ANO_DT_H
#define __ANO_DT_H
//数据拆分宏定义,在发送大于1字节的数据类型时,比如int16、int32等,需要把数据拆分成单独字节进行发送
#define BYTE0(dwTemp) (*(char)(&dwTemp))
#define BYTE1(dwTemp) (
((char)(&dwTemp) + 1))
#define BYTE2(dwTemp) (
((char)(&dwTemp) + 2))
#define BYTE3(dwTemp) (
((char *)(&dwTemp) + 3))

void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d);
void ANO_DT_Send_F2(int16 _a, int16 _b, int16 _c, int16 _d);
void ANO_DT_Send_F3(int16 _a, int16 _b, int32 _c);

将我们写的数据通信程序放在c文件里,(不要忘记包含以上的头文件和你串口发送函数所在的头文件 )

三、完整的数据通讯的程序例子:

小端模式单片机,通过F1帧发送4个uint16类型的数据(也就是本文主要介绍的例子):

uint8 data_to_send[100];

void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d) //F1帧 4个 uint16 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF1;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE0(_a);
data_to_send[_cnt++] = BYTE1(_a);

data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_d);data_to_send[_cnt++] = BYTE1(_d);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt);//读者记得修改串口发送函数

}

大端模式单片机,通过F1帧发送4个uint16类型的数据(也就是本文主要介绍的例子):

uint8 data_to_send[100];

void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d) //F1帧 4个 uint16 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF1;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE1(_a);
data_to_send[_cnt++] = BYTE0(_a);

data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_d);data_to_send[_cnt++] = BYTE0(_d);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt); //读者记得修改串口发送函数

小端模式单片机,通过F2帧发送4个int16类型的数据:

uint8 data_to_send[100];

void ANO_DT_Send_F2(int16 _a, int16 _b, int16 _c, int16 _d) //F2帧 4个 int16 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF2;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE0(_a);
data_to_send[_cnt++] = BYTE1(_a);

data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_d);data_to_send[_cnt++] = BYTE1(_d);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt);读者记得修改串口发送函数

}

大端模式单片机,通过F2帧发送4个int16类型的数据:

uint8 data_to_send[100];

void ANO_DT_Send_F2(int16 _a, int16 _b, int16 _c, int16 _d) //F2帧 4个 int16 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF2;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE1(_a);
data_to_send[_cnt++] = BYTE0(_a);

data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_d);data_to_send[_cnt++] = BYTE0(_d);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt);读者记得修改串口发送函数

}

小端模式单片机,通过F3帧发送2个int16类型和1个int32类型的数据:

uint8 data_to_send[100];

void ANO_DT_Send_F3(int16 _a, int16 _b, int32 _c ) //F3帧 2个 int16 参数 1个 int32 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF3;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE0(_a);
data_to_send[_cnt++] = BYTE1(_a);

data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE2(_c);data_to_send[_cnt++] = BYTE3(_c);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt); 读者记得修改串口发送函数

}

大端模式单片机,通过F3帧发送2个int16类型和1个int32类型的数据:

uint8 data_to_send[100];

void ANO_DT_Send_F3(int16 _a, int16 _b, int32 _c ) //F3帧 2个 int16 参数 1个 int32 参数
{
uint8 _cnt = 0;
uint8 sumcheck = 0; //和校验
uint8 addcheck = 0; //附加和校验
uint8 i=0;
data_to_send[_cnt++] = 0xAA;
data_to_send[_cnt++] = 0xFF;
data_to_send[_cnt++] = 0xF3;
data_to_send[_cnt++] = 8; //数据长度
data_to_send[_cnt++] = BYTE1(_a);
data_to_send[_cnt++] = BYTE0(_a);

data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE3(_c);data_to_send[_cnt++] = BYTE2(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);for ( i = 0; i < data_to_send[3]+4; i++){sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART,data_to_send,_cnt); 读者记得修改串口发送函数

}

相信通过以上的例子大家可以自己按照自己需要的数据个数和类型自己写(或者改)数据通信程序了