1. 问题描述
在使用STM32G431RBT6芯片的开发板的时候,要实现如下图所示的一个按键控制一个浮点型变量的累加与到达边界值清零的功能。
只关于这一部分功能的代码段如下
float x = 0; // 浮点型变量初始化
/* 中间的其它代码与该问题无关,这里省略,只保留功能相关代码 */
if(ucKey_Down == 1) // 按下B1
{
if(x < 3.3)
{
x += 0.3;
}
else if(x >= 3.3)
{
x = 0;
}
}
但是当使用LCD将该值输出时,发现了奇怪的一幕,变量x竟然增加到了3.6后才清零。
2. 发现原因
使用了调试功能后,发现了原因。
原来当LCD显示x值为3.3的时候,x实际值是3.29999971,对于程序中的判断而言,仍比3.3小,所以下一次按下按键,仍然会使该变量加0.3,才有了显示3.6的结果。
至于为什么会出现这样的情况,个人认为是由于浮点型变量与其他变量一样,都是按照二进制存储的,所以事实上在存储变量时,整数部分的数值存储是可以实现十进制与二进制的完全无误差转化的,但是小数部分的存储数值的最小精度的单位并不是按照十进制的10的负几次方,而是二进制存储的精度,也就是最小计数单位2的负几次方,所以计数单位无法完全按照十进制的小数点的精度。对于每次递增的数值0.3不能完全表示成2的负幂指数之和的形式,所以在0.3这个立即数在存储时本身就带有误差。当然,如果在浮点型变量x的初始化时,初始化给x的值的小数部分不能表示成2的负整数幂次之和的形式,那么x的初始化的值同样会在小数的最后几位出现误差,例如
float x = 2.4;
感兴趣的可以尝试一下。
虽然在正常计算和显示数值时可以通过四舍五入直接忽略这一小点误差,但是当做边界判断时,这点误差将可能对程序起到决定性作用。
3. 检验猜测
考虑浮点型变量的小数部分的存储数值为2的负整数幂指数为计算精度的,那么对于小数部分可以表示为若干个2的负幂指数之和的形式的数值,应该就可以实现完全无误差转化了。那么修改上述递增数值重新测试,修改的该部分代码改为
float x = 0; // 浮点型变量初始化
/* 中间的其它代码与该问题无关,这里省略,只保留功能相关代码 */
if(ucKey_Down == 1) // 按下B1
{
if(x < 3.375)
{
x += 0.375;
}
else if(x >= 3.375)
{
x = 0;
}
}
0.375
=
2
−
1
+
2
−
2
0.375=2^{-1}+2^{-2}
0.375=2−1+2−2。调试代码,当x加到最大值时得到结果x=3.375,且没有误差。
因而可以验证前面原因的猜测是正确的。
4.解决方法
为了避免因为存储误差带来的赋值细微误差造成的边界判断出错的问题,可以考虑在边界判断时进行宽松处理,如当判断条件中是小于号“<”接边界值时,即if(x<bound),对则对边界值在功能误差容许范围内进行细微减小处理,如将前面的判断3.3为x的上界的代码改写为
float x = 0; // 浮点型变量初始化
/* 中间的其它代码与该问题无关,这里省略,只保留功能相关代码 */
if(ucKey_Down == 1) // 按下B1
{
if(x < 3.29)
{
x += 0.3;
}
else if(x >= 3.29)
{
x = 0;
}
}
就可以实现在x理论值为3.3时,再次按下按键B1,x将回到0,实现了符合要求的循环递增。类似地,如果判断的条件是大于边界值,那么就作增大判断边界值的宽松处理,如
float x = 3.0; // 浮点型变量初始化
/* 中间的其它代码与该问题无关,这里省略,只保留功能相关代码 */
if(ucKey_Down == 1) // 按下B1
{
if(x >= 0.01)
{
x -= 0.3;
}
else if(x < 0.01)
{
x = 3.3;
}
}
就可以实现每次按下B1按键x递减0.3,直到减到理论值0时,x变为3.3,实现循环递减。