Arduino 如何通过串口发送浮点数,比如带小数点的温度?
公平是因为每个人都有不公平
逃避责任区:文中结论属于自己学习心得,不保证学术正确性和严谨性。如有问题,欢迎评论区讨论、批评、指正。部分图片、结论、源码可能来自其它文章,由于自己记学习笔记的时候并没有处处都标明出处,如有造成侵权问题,实属抱歉,请第一时间联系我删除。
问题的引入
很多时候我们需要把传感器测得的数据,通过串口或者 Wi-Fi 发送到上位机,而传感器的数据,比如温度、湿度等,往往并不是整型,比如 -12.34 °C,56.78% 等。对此有很多处理方法。比如把数字当成字符串处理,直接使用 Serial.print()
发送,或者通过乘法转换成整型以后再发送,又或者将浮点数拆分成整数和小数两部分发送 ... 下面介绍一种方法,通过先将浮点数转换成字节数组(Byte Array),然后再按字节发送,实现浮点数据的传输。好处就是:精度得到保留,数据长度短且统一。
实现方法
基本思路
用一个单精度浮点数定义一个假想的温度值:
float hypoTemp = -12.34 // 假想从传感器获得的温度值
一个单精度浮点数(float)在 Arduino 中长度为 32 位 ,也就是 4 Byte。 比如上面的 -12.34 把它写成二进制和十六进制就是:
BIN: 1100 0001 0100 0101 0111 0000 1010 0100
HEX: 0xC14570A4
至于这是怎么换算的,请参看 IEEE 754 浮点数换算标准,网上教程大把。实际应用中,你可以通过在线工具,直接换算。
IEEE-754 Floating Point Converter
既然是 4 Byte,那一个非常朴实的想法就是,用一个数组来收纳这 4 Byte,然后再通过 Serial.write(byteArr, 4)
写入到串口。
代码原型
void send_sensor_data()
{
float hypoTemp = -12.34; // 假想的从传感器获得温度值 0xC14570A4
uint8_t charArr[4]; // 用来存储 4 字节的字节数组
uint8_t *p;
p = (uint8_t*) &hypoTemp; // 让指针指向浮点数所在的内存
for(int i=0; i<4; i++)
{
charArr[i] = *p++; // 读取内存,把表示浮点数的字节放到数组中
}
Serial.write(charArr, 4); // 按字节,写入串口,串口得到的是逆序的 OxA4 70 45 C1
}
这个代码的想法可以说非常淳朴了,就是用一个指针,让它指向表示浮点数的字节所在的内存,然后取出来放到数组,构成字节数组。
有几个注意点:
uint8_t
也可以用 Arduino 的byte
来代替,都是表示 8 bit 长度。uint8_t
实际定义为unsigned char
, 但是uint8_t
具有更好的可移植性。因为它只要能用就一定能保证是 8 bit。 而 unsigned char 能保证一定能用,但不保证一定是 8 bit (但是在定义了typedef unsigned char uint8_t
的系统上 char 一定是 8 bit ,这有点绕... ),所以从「想定一个 8 bit 的变量并且有可移植性」角度来说,uint8_t
是最优选择。
unsigned char
is guaranteed to exist, but is only guaranteed to be 8 bits whenCHAR_BIT == 8
.uint8_t
isn't guaranteed to exist, but is guaranteed to be 8 bits when it does
Serial.write()
可以把一个数组的字节全部打印出来,但是需要指明长度。当然, 这里的 4 可以用sizeof()
来求得。 但是由于这里是固定的 4 Byte,所以还是让单片机少点工作吧。
Syntax
Serial.write(val)
Serial.write(str)
Serial.write(buf, len)
Parameters
Serial
: serial port object. See the list of available serial ports for each board on the Serial main page.
val
: a value to send as a single byte.
str
: a string to send as a series of bytes.
buf
: an array to send as a series of bytes.
len
: the number of bytes to be sent from the array.
- 由于内存中字节存放的顺序,或者说大端小端问题,实际运行上面代码后,你在串口工具中看到的字节是逆序的,也就是
0xA4 0x70 0x45 0xC1
。所以在上位机对收到数据进行解析的时候需要格外注意(当然你也可以在单片机上把数据逆过来)。
优化代码
上面的代码,只是展示下思路,其实完全可以用一行代码来代替, 但是核心逻辑是一样的。
Serial.write((uint8_t*) &hypoTemp, sizeof(hypoTemp));
从代码的可复用性角度来说,建议把这个浮点数转字节数组封装成一个函数。
void convFloatToByteArr(float val, uint8_t byteArr[4])
{
memcpy(byteArr, (uint8_t*) &val, 4);
}
void send_float_data()
{
float hypoTemp = -12.34; // 假想的从传感器获得温度值 0xC14570A4
uint8_t byteArrTemp[4]; // 声明一个用来存储 4 字节的字节数组
convFloatToByteArr(hypoTemp, byteArrTemp); // 调用
Serial.write(byteArrTemp, 4); // 写入串口
}
使用联合体 Union
但是后来我又发现,还可以使用联合体 Union (也叫共用体) 继续优化。
// 定义联合体
typedef union {
float floatTemp;
byte byteArrTemp[4];
} uTemp;
void setup() {
uTemp hypoTemp;
hypoTemp.floatTemp = -12.34;
Serial.begin(9600);
Serial.write(hypoTemp.byteArrTemp, 4);
}
使用 Union 的意义在于,Union 中的成员是共享一段内存的,所以里面的字节数组和浮点数是「捆绑在一起的」,无论通过点操作符修改哪一个,另一个都会跟着改变。
现在就可以再次把这个浮点数转字节数封装成一个函数:
void convFloatToByteArr(float val, uint8_t* byteArr)
{
// 定义 Union
union{
float floatVal;
uint8_t byteArr[4];
}uFloatByteArr;
// 函数参数写入 Union
uFloatByteArr.floatVal = val;
// 把 Union 的字节数组拷贝给参数传递进来的数组
memcpy(byteArr, uFloatByteArr.byteArr, 4);
}
void send_float_data()
{
float hypoTemp = -12.34; // 假想的从传感器获得温度值 0xC14570A4
uint8_t byteArrTemp[4]; // 声明一个用来存储 4 字节的字节数组
convFloatToByteArr(hypoTemp, &byteArrTemp[0]); // 调用
Serial.write(byteArrTemp, 4); // 写入串口
}
补充
有时候还会看到结构体(Struct)和联合体 (Union) 一起使用的情况,从代码上看,结构体代替的是上面的字节数组。
//定义结构和联合
typedef union
{
struct
{
unsigned char low_byte;
unsigned char mlow_byte;
unsigned char mhigh_byte;
unsigned char high_byte;
}float_byte;
float value;
}FLAOT_UNION;
经过大佬点拨,使用结构体代替数组的好处就是,结构体相比数组的数字索引,访问过程更加清晰,可读性更好。比如上面的结构体中明确声明了,哪些成员是表示高位字节,哪些成员是低位字节。