目录
- 单片机(STM32)与上位机传输数据的方法
- 1. 传输整形数据
- 2. 传输浮点数据
- 3. 如何打包与解包
单片机(STM32)与上位机传输数据的方法
在进行单片机程序的开发时,常常需要与其他设备进行通信。一种情况是与其他电路板通信,比如STM32主机与STM32从机通信,STM32从机与树莓派主机通信。一种情况是与上位机通信,上位机软件进行人机交互。这个时候需要进行数据传输,传输数据有两种方式,传输整形数据与直接传输浮点数据。
1. 传输整形数据
一种方法是传输整形数据,工业中常用的Modbus就是这种方式。这里以传输16位整形数据为例,一个数据就占用两个字节,可以是正数和负数。
测试代码:
int main()
{
uint16_t me, you;
uint8_t data[100];
me = 120;
data[0] = me >> 8;
data[1] = me;
you = (uint16_t)data[0] << 8 | (uint16_t)data[1];
printf("you = %d", you);
return 0;
}
出来的结果是一样的120
那么负数怎么办呢?其实是一样的。不管是uint16_t还是int16_t,在内存中的存储都是一样的,区别不在于写,而在于怎么读。
int main()
{
uint16_t me, you;
uint8_t data[100];
int16_t para;
me = -120;
data[0] = me >> 8;
data[1] = me;
you = ((uint16_t)data[0] << 8 | (uint16_t)data[1]);
para = (int16_t)((uint16_t)data[0] << 8 | (uint16_t)data[1]);
printf("me = %d\n", me);
printf("me = %d\n", (int16_t)me);
printf("you = %d\n", (int16_t)you);
printf("para = %d", para);
return 0;
}
结果:
上面me是一个uint16_t
类型,怎么能直接让它等于-120呢?当然是可以的,只不过调用me
的时候,是按照uint16_t
类型读取的,结果就是65416,如果按照int16_t
类型读取,结果就是-120。
同理,you
也是一个uint16_t
类型,you = ((uint16_t)data[0] << 8 | (uint16_t)data[1])
是按照移位拷贝的方式将me
的值赋给了you
,只要按照int16_t
类型读取出来,结果就是正确的负数。
理解了这种思想,在进行单片机与其他设备通信的时候,就可以定义一个数组,uint16_t register1[1000]
,数组的索引就是数据地址,一个萝卜一个坑。第二个设备(其他单片机或电脑)同样定义一个数组,uint16_t registe2[1000]
,按照上面的方法一个数据一个数据传输就行了。
再次注意:直接定义无符号数组即可,传输负数时直接赋值,只要另一端收到数据后按照int16_t
类型读取,结果就是正确的负数。
2. 传输浮点数据
传输整形方法的缺点是:(1)不能直接传输浮点数,传输浮点数时需要进行倍数处理。例如0.12,将其乘100变成整形的12,上位机收到后除100变成浮点型的0.12。这种方法较麻烦,哪些地址的数据需要进行倍数,需要下位机和上位机同时定义清楚。(2)有符号和无符号类型数据区分。uint16类型数据较简单,直接传输,直接解析,没问题。int16上位机解析时,就需要进行类型转换了。哪些地址的数据要进行(int16_t)类型转换,也要定义清楚。(3)表示的数据范围有限,16位整形无符号数只能到65535,有符号数除2减半。如果是浮点数,乘掉了倍数,表示范围直接缩水。如果是翻100倍,只能表示到655。
所以,最方便的就是直接传输浮点数,省去很多麻烦。当然浮点数的缺点就是,一个数据要占用4个字节。因此效率是传输整形数据的一半。
传输浮点数,需要定义一个联合体:
union float_data
{
float f_data;
uint8_t byte[4];
};
f_data
和byte[4]
共用4个字节的内存单元,成员f_data
是实际使用的数据,成员byte[4]
是通信时用的数据,各司其职。
使用方法:
#include <stdio.h>
#include <stdint.h>
union float_data
{
float f_data;
uint8_t byte[4];
};
int main()
{
union float_data me, you;
me.f_data = 0.12;
you.byte[3] = me.byte[3];
you.byte[2] = me.byte[2];
you.byte[1] = me.byte[1];
you.byte[0] = me.byte[0];
printf("you = %f", you.f_data);
return 0;
}
出来的结果是一样的,0.12。聪明的读者可以发现,me
和you
对应两个设备。只要按照这种方式进行传输,就可以传输浮点数。传输多个浮点数,me
和you
就可以定义为一个数组,例如me[100]
, you[100]
。
3. 如何打包与解包
知道了数据如何传输,第二步就是思考如何进行数据打包和解包了,因为一个数据帧当然是要传输多个数据的。需要两个设备定义好通信协议,才能正确的解析数据。
数据打包也有两种方式,一种按照功能字,一种按照地址——数据对。
(1)按照功能字
这种方法用一个数据位表示功能字,对方设备收到这一帧数据,根据这个功能字就能判断你这一帧数据是什么,然后进行解析。例如一款陀螺仪的数据上传协议为:
它用第一个字节表示帧头,0x55,第二个字节表示功能字,0x52是角速度输出,0x53是角度输出,单片机读陀螺仪的数据时,按照它给定的这个协议,依次把数据读出来就可以了。
如果是自己定义通信协议,也可以模仿,这种方式每一帧数据都要进行定义,优点是物理意义明确,缺点是一旦确定了,如果想要修改,两端的设备要同时修改。
(2)按照地址数据对
这种方法模拟计算计的存储方式,为每一个数据安排一个地址,请注意这个地址并需要是真正的内存地址,它的核心是“索引”。例如一个数据就可以实现这种功能。uint16_t data[100]
,数组的索引就是地址。例如我用data[0]
表示姓名,data[1]
表示年龄。那么姓名的地址就是0,年龄的地址就是1。
这种方法的优点和缺点与第一种方法相反,物理意义不明确,但移植性强、维护性好。
下面是我自创的一种通信协议,传输浮点数。前两个字节为帧头,不同帧头分别代表从机主动上传、主机下发修改数据、主机下发查询数据。(这种通信协议为一对一,不支持总线通信)
(1)单片机主动上传数据:
发送N个数据(32 bits)一共4N+6个帧字节。
(2)上位机下发更改数据:
发送N个数据(32bits)也是一共2N+6个帧字节。
(3)上位机下发查询数据:
查询从起始地址开始的N个数据,查询帧是6个字节。下位机收到数据按照上传数据格式上传。