嵌入式开发在通讯协议中关于浮点数,负数常用处理方式
本文简单分享了在嵌入式开发中,在通讯协议这一方面关于浮点数,负数的下位机发送与上位机接收以及解析相关内容。包括常用的解决方案,比如协商单位,*10上传或者*100上传;使用c语言特性:联合体,内存拷贝函数,再或者用指针强转法。最后分享了关于C#上位机的一些解码办法。
1.简介
在嵌入式开发中,通过串口进行通讯是很常见的手段,但是串口通讯中有个挺麻烦的问题,就是关于浮点数(小数)和负数的发送与接收。
因为常见的串口发送都是发送一长串u8的数组,如果数据范围较大,那就将相邻的两个u8,高八位低八位凑成u16,或者相邻的4个凑u32扩大数据范围(一般情况下u16就够用了,较少有用u32的情况)。
但是浮点数和负数处理起来相对麻烦,因为浮点数的存储原理就比较复杂,不便于直接取高位低位;负数更不用说了,传输过程中都是以u8为单位,全程无符号,直接解码会出现问题。
下面我分享下嵌入式开发通讯协议中,关于小数,负数的处理方法。
1.1浮点数储存原理
一般情况下,float占用4字节,double占用8字节。浮点数存储方式遵循IEEE754标准,标准简介如下:
一个浮点数的组成形如下图,从高位到低位分三部分:
- 符号S (sign):负数s=1 ,正数s=0。
- 阶码E (exponent):表示浮点数的幂次,是2的E次幂(E可能为负,用来表示特别接近于0的数)。
- 尾数M (fraction):一个二进制的小数阶码。
对于位数一定的浮点数来说,阶码E和尾数M的长度都是确定的,如32位浮点数有8位阶码,23位尾数,而64位浮点数有10位阶码,53位尾数.
具体解释请看这位大佬的文章:
IEEE 754浮点数的通俗理解 - 知乎
https://zhuanlan.zhihu.com/p/685673574
1.2负数储存原理
计算机存储数据时候,均以二进制01存储,这就存在原码,反码,补码的概念。
负数通常采用补码形式存储。
具体如下:
- 原码:符号位表示正负,数值位为值的绝对值。例如+1(原)=00000001,-1(原)=10000001。
- 反码:正数反码等于其原码,负数则在原码基础上,符号位不变,数值位取反。
- 补码:正数和负数的补码均等于其反码+1。
具体解释请看这位大佬的文章:
https://zhuanlan.zhihu.com/p/10951945773
https://zhuanlan.zhihu.com/p/10951945773
2.浮点数收发
首先我们先明确此次测试所使用浮点数的16进制码。
- 小数1.5的16进制码:3F C0 00 00
- 小数2.0的16进制码:40 00 00 00
- 小数3.3的16进制码:40 53 33 33
2.1乘以10,乘以100
发送小数最简单的办法就是*10发送,/10接收,这样将可以保留1位小数点,基本上能满足要求;如果一位小数不够的话那就*100发送,/100接收,这样将可以保留2位小数点。
但是注意数据范围,如果*100发送的话u8一般不够用。因为u8最大为255/100=2.55,连传输个3.3都做不到,数据范围这一点需要注意。
下面请看测试:
- 0X21=33
- 0X0F=15
- 0X14=20
- 0X014D=333
- 0X9B=155
- 0XC9=201
双方约定好放大倍率,*10,*100发送,/10,/100接收就好。或者*20,*50,都可以,只要双方约定好,形成统一的格式,那么就可以使用。
2.2使用union(联合体/共用体)
联合体/共用体是C语言的一种数据结构,与结构体类似,他的成员可以是不同类型的变量,与结构体不一样的是联合体/共用体中所有成员变量使用同一块内存空间(空间大小以最大的成员为准),我一直觉得这玩意没啥用,现在看来还是有点意思的。
至于顺序为什么是反的,这里涉及到一个数据存储大小端的问题,各位自行查阅,有空我再写一篇文章分享。
测试代码如下:
include "sys.h"
typedef union
{
float float_test[3];
u8 hex_test[12];
} data_test;
data_test float_to_hex;
int main()
{
all_init();
while (1)
{
u8 tx_buffer[12];
float test_num00 = 0;
float test_num01 = 0;
float test_num02 = 0;
memset(tx_buffer, 0, 12);
test_num00 = 3.3;
test_num01 = 1.5;
test_num02 = 2.0;
float_to_hex.float_test[0] = test_num00;
float_to_hex.float_test[1] = test_num01;
float_to_hex.float_test[2] = test_num02;
tx_buffer[0] = float_to_hex.hex_test[0];
tx_buffer[1] = float_to_hex.hex_test[1];
tx_buffer[2] = float_to_hex.hex_test[2];
tx_buffer[3] = float_to_hex.hex_test[3];
tx_buffer[4] = float_to_hex.hex_test[4];
tx_buffer[5] = float_to_hex.hex_test[5];
tx_buffer[6] = float_to_hex.hex_test[6];
tx_buffer[7] = float_to_hex.hex_test[7];
tx_buffer[8] = float_to_hex.hex_test[8];
tx_buffer[9] = float_to_hex.hex_test[9];
tx_buffer[10] = float_to_hex.hex_test[10];
tx_buffer[11] = float_to_hex.hex_test[11];
USART1_Tx(tx_buffer, 12);
}
}
2.3使用memcpy(内存拷贝)
有很多人说指针是C语言的灵魂,有了指针,就允许开发者直接操纵内存中的数据,读写删改。memcpy是其中之一的函数。使用他,直接输入目标地址,起始地址,数据长度,直接进行数据的搬运,这里注意下搬运范围。
(我这里还用了一个memset函数,是将数组所有成员写0,用作变量初始化,大家有兴趣可以去查一下memory相关的函数。)
一个float是4字节,这里tx_buffer的0~3装test_num00,4~7装test_num01,8~11装test_num02,使用该函数时建议做好内存分配规划。不然很容易出现大问题。
测试代码如下:
#include "sys.h"
int main()
{
all_init();
while (1)
{
u8 tx_buffer[12];
float test_num00 = 0;
float test_num01 = 0;
float test_num02 = 0;
memset(tx_buffer, 0, 12);
test_num00 = 3.3;
test_num01 = 1.5;
test_num02 = 2.0;
memcpy(tx_buffer, &test_num00, 4);
memcpy(&tx_buffer[4], &test_num01, 4);
memcpy(&tx_buffer[8], &test_num02, 4);
USART1_Tx(tx_buffer, 12);
}
}
2.4使用指针强转
有很多人说指针是C语言的灵魂,下面指针强转的做法就是直击灵魂的操作了。
首先获取到浮点数的地址,再把这个地址强行转换为u32类型的地址,再以u32格式把数据提取出来.
说起来有点绕,不知各位大佬有无更好更优雅的方法,欢迎提出。
测试代码如下:
#include "sys.h"
int main()
{
all_init();
while (1)
{
u8 tx_buffer[12];
u32 test_u32_00 = 0;
u32 test_u32_01 = 0;
u32 test_u32_02 = 0;
float test_num00 = 0;
float test_num01 = 0;
float test_num02 = 0;
memset(tx_buffer, 0, 12);
test_num00 = 3.3;
test_num01 = 1.5;
test_num02 = 2.0;
test_u32_00 = *((u32 *)&test_num00);
test_u32_01 = *((u32 *)&test_num01);
test_u32_02 = *((u32 *)&test_num02);
tx_buffer[0] = test_u32_00 >> 8 * 0;
tx_buffer[1] = test_u32_00 >> 8 * 1;
tx_buffer[2] = test_u32_00 >> 8 * 2;
tx_buffer[3] = test_u32_00 >> 8 * 3;
tx_buffer[4] = test_u32_01 >> 8 * 0;
tx_buffer[5] = test_u32_01 >> 8 * 1;
tx_buffer[6] = test_u32_01 >> 8 * 2;
tx_buffer[7] = test_u32_01 >> 8 * 3;
tx_buffer[8] = test_u32_02 >> 8 * 0;
tx_buffer[9] = test_u32_02 >> 8 * 1;
tx_buffer[10] = test_u32_02 >> 8 * 2;
tx_buffer[11] = test_u32_02 >> 8 * 3;
USART1_Tx(tx_buffer, 12);
}
}
2.5原理
上面三种方式操作对于数据发送结果来说都是一样的.
因为单片机/计算机只认识01,所有数据在单片机/计算机都是01存储的,我们人为将不同的数据按照不同规格打包,解码。
已知-0.25的 16进制为BE 80 00 00。
按照s32整数去读是这样:-1098907648
按照u32整数去读是这样:3196059648
按照浮点数去解读是这样:-0.25
无论怎样读,他原始的数据就是BE 80 00 00,他的二进制码不会变。
按照不同的解读方式,能读出来不一样的数据罢了。
2.6浮点数上位机接收(c#为例)
C++与C兼容,同样可以使用联合体,memcpy,指针强转来进行浮点数解码,此处不再举例。
这里提供一份C#上位机浮点解析代码。
C#代码如下:
#region float与HEX互换
public UInt32 FloatToHex(float floatValue)
{
UInt32 uintValue = BitConverter.ToUInt32(BitConverter.GetBytes(floatValue), 0);
return uintValue;//这里是把float的4个字节塞在一个u32里面,如果需要可以自行用位操作把他给挪出来
}
public float HexToFloat(byte data1, byte data2, byte data3, byte data4)
{
UInt32 HEX = (UInt32)((data4 << 8 * 3) | (data3 << 8 * 2) | (data2 << 8 * 1) | (data1 << 8 * 0));
byte[] floatVals = BitConverter.GetBytes(HEX);
return BitConverter.ToSingle(floatVals, 0);
}
#endregion
针对上面的数据,实测效果如下:
3.负数收发
既然我们知道各种转换出现问题的原因是解读方式不一样,那么在进行负数传输时,我们保证解读方式与发送方式一致,就好了。
发的时候,用u16发,收的时候用u16收(s8范围只有-128~+127,数据范围较小,建议根据实际需求,可以选择使用u16。这样数据范围大一些),在数据范围不溢出的情况下,就可以实现负数传输。
测试如下:
使用C#上位机接收测试如下:
希望能够帮助到一些人。
本人菜鸡一只,各位大佬发现问题欢迎留言指出。
更多推荐
所有评论(0)