本文主要内容:详细介绍如何从0开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,帮助我们调节一些参数,比如电机PID的调节、波形融合等,以及在我们写通信协议的时候可能遇见的问题或注意事项进行解答,本文主要以匿名上位机为例,新手和小白也可以实现。
一、准备工作:
1、要有该上位机或者虚拟示波器的通信协议或者说通信帧格式
如本文的例子匿名上位机的通讯帧格式如下:
再如垆边月晓开发的20通道数字示波器通讯协议:
只有知道了上位机或者虚拟示波器的通讯格式,双方才能进行通信,就像平常我们的交流一样,交流双方只有在相同语言基础上才能进行交流,再如谍战剧里面收发电报的双方按照同样的规则对数据或者说电波进行解读才能进行信息的传递。
2、要知道你用来发送数据的单片机或者其他设备是大端模式还是小端模式。
先介绍一下大小端模式是什么:
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。 这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
简单点说,小端系统,即高位值存于高地址,低知位值存于低地址。大端系统,与小端系统相反,高位值存于低位地址,低位值存于高位地址。
常见的单片机大小端模式:(1)KEIL C51中,变量都是大端模式的,而KEIL MDK中,变量是小端模式的。(2)SDCC-C51是小端寻址,AVRGCC 小端寻址.(3)PC小端,大部分ARM是小端 (4)总起来说51单片机一般是大端模式,32单片机一般是小端模式.
为什么要确定发送数据的单片机的大小端模式呢,在下文中会详细介绍,大小端模式不同,数据通信的程序会有一些差异。
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); 读者记得修改串口发送函数
}
相信通过以上的例子大家可以自己按照自己需要的数据个数和类型自己写(或者改)数据通信程序了
四、匿名上位机的相关配置,本部分内容参考自匿名匿名通信协议V7.00 (对于不是用的匿名上位机的,可跳过此部分内容)
五、上位机显示效果示例 :
如我们随意发送的3个数据如下:
int16 s1=0, s2=0;
int32 s3=0;
while(1)
{
ANO_DT_Send_F3(s1, s2, s3 );
s1+=1;
if(s1>100)
s1=0;
s2=50*sin(100*s1)+5;
s3=50*sin(100*s1+10*s2);
pca_delay_ms(500); //延时每500ms发送一次
}
以下界面说明数据通过了和校验、附加校验: