1.简介

在嵌入式开发中,通过串口进行通讯是很常见的手段,但是串口通讯中有个挺麻烦的问题,就是关于浮点数(小数)和负数的发送与接收。

因为常见的串口发送都是发送一长串u8的数组,如果数据范围较大,那就将相邻的两个u8,高八位低八位凑成u16,或者相邻的4个凑u32扩大数据范围(一般情况下u16就够用了,较少有用u32的情况)。

但是浮点数和负数处理起来相对麻烦,因为浮点数的存储原理就比较复杂,不便于直接取高位低位;负数更不用说了,传输过程中都是以u8为单位,全程无符号,直接解码会出现问题。

下面我分享下嵌入式开发通讯协议中,关于小数,负数的处理方法。

1.1浮点数储存原理

一般情况下,float占用4字节,double占用8字节。浮点数存储方式遵循IEEE754标准,标准简介如下:

一个浮点数的组成形如下图,从高位到低位分三部分:

浮点数存储原理
  1. 符号S (sign):负数s=1 ,正数s=0。
  2. 阶码E (exponent):表示浮点数的幂次,是2的E次幂(E可能为负,用来表示特别接近于0的数)。
  3. 尾数M (fraction):一个二进制的小数阶码。

对于位数一定的浮点数来说,阶码E和尾数M的长度都是确定的,如32位浮点数有8位阶码,23位尾数,而64位浮点数有10位阶码,53位尾数.

具体解释请看这位大佬的文章:

IEEE 754浮点数的通俗理解 - 知乎https://zhuanlan.zhihu.com/p/685673574

1.2负数储存原理

计算机存储数据时候,均以二进制01存储,这就存在原码,反码,补码的概念。

负数通常采用补码形式存储

具体如下:

  1. 原码:符号位表示正负,数值位为值的绝对值。例如+1(原)=00000001,-1(原)=10000001。
  2. 反码:正数反码等于其原码,负数则在原码基础上,符号位不变,数值位取反。
  3. 补码:正数和负数的补码均等于其反码+1。

具体解释请看这位大佬的文章:

https://zhuanlan.zhihu.com/p/10951945773https://zhuanlan.zhihu.com/p/10951945773

2.浮点数收发

首先我们先明确此次测试所使用浮点数的16进制码。

1.5的16进制码
2.0的16进制码
3.3的16进制码
  1. 小数1.5的16进制码:3F C0 00 00
  2. 小数2.0的16进制码:40 00 00 00
  3. 小数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都做不到,数据范围这一点需要注意。

下面请看测试:

*10发送,/10接收
*100发送,/100接收
  1. 0X21=33
  2. 0X0F=15
  3. 0X14=20
  4. 0X014D=333
  5. 0X9B=155
  6. 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,使用该函数时建议做好内存分配规划。不然很容易出现大问题。

memcpy法

测试代码如下:

#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。

-0.25的16进制码

按照s32整数去读是这样:-1098907648

按照u32整数去读是这样:3196059648

按照浮点数去解读是这样:-0.25

相同的hex值,不同解法,解出来不同的值

无论怎样读,他原始的数据就是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

针对上面的数据,实测效果如下:

将u8数据解码为float

3.负数收发

既然我们知道各种转换出现问题的原因是解读方式不一样,那么在进行负数传输时,我们保证解读方式与发送方式一致,就好了。

发的时候,用u16发,收的时候用u16收(s8范围只有-128~+127,数据范围较小,建议根据实际需求,可以选择使用u16。这样数据范围大一些),在数据范围不溢出的情况下,就可以实现负数传输。

测试如下:

发送测试

使用C#上位机接收测试如下:

上位机接收测试

希望能够帮助到一些人。

本人菜鸡一只,各位大佬发现问题欢迎留言指出。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐