在笔者回答读者的问题中,很多是关于浮点数的。很多人的问题是“什么是浮点数”,“浮点数怎么显示在数码管上”。这个问题要讲清还真不容易,因此为了讲清楚这个问题,笔者专门设计了一个教学任务来讲解这个问题。
使用单片机来制作智能仪器是单片机的一种重要用途。本章通过一台智能仪器的设计,学习单片机对浮点数的处理方法,学习使用LED数码管显示小数的方法。
在智能仪器的设计中,常有这样一种要求:使用按键面板来设置一个系数,通过A/D转换或计数等方式得到的一个数值乘以该系数得到一个值,将这个值在LED数码管上显示出来。
在这样的设计要求中,所设置的系数往往是一个小数,这样做完乘法后,所得到的结果也会是一个小数,如果输入的数变动范围比较大,那么所得到的结果也会有比较大的变动范围。设有如下的要求:
系数范围:0.001~9.999
计数输入:1~4294967295
显示器:6位LED数码管
要求将计数值乘以系数后用数码管显示出来,并且显示尽可能多的小数位数,通过计算不难得到,所要显示的值的范围达在系数为0.001时最小,当计数值为0.001时,最小计数值为0.001,而计数值很大或者系数较大时,显示值最多可能达到999999。数的范围变化如此之大,必须采用浮点数才能够满足要求,为此,下面首先对浮点数的相关知识作一个介绍,然后编程来实现这一要求。
通常人们书写时常用诸如:
17.3=1.73E1=1.73× 101
-2345.67=-2.34567E3=-2.34567× 103
-0.00012345=-1.2345E-4=-1.2345× 10-4
这种方式其实质就是浮点记数法,和定点数相比,浮点数在运算、存储等方面要复杂得多,下面学习有关浮点数基本知识。
浮点记数方法将一个数用三部分来表示,第一部分为数符,表示正、负数;第二部分表示这个数的有效数字,与相对精度有关;第三部分表示这个数的数量级。在计算机中,数符通常用字节中的一位来表示,规定正数用0表示,负数用1表示;有效数字部分又称为尾数,用若干个字节的纯小数表示;数量级部分又称阶码,它本身也有正负,用二进制补码整数表示。
同样的数值可以有多种浮点数表达方式,比如 17.3 可以表达为 17.3 × 101,0.173 × 102 或者 0.0173 × 103。因为这种多样性,所以有必要对其加以规范化以达到统一表达的目标。对于10进制浮点数来说,规范的浮点数表达方式具有如下形式:
±d.dd...d × 10e
其中 d.dd...d 即尾数,10 为基数,e 为指数。尾数中数字的每个数字 d 介于 0 和基数之间,包括 0。小数点左侧的数字不为 0。
前面小节讨论的是十进制浮点数的表示方法,单片机内部的数值表达是基于二进制的,二进制数同样可以有小数点。
1.二进制浮点数的表示方法
二进制浮点数的表法方法类似于十进制,只是基 等于 2,而每个数字 d 只能在 0 和 1 之间取值。如:二进制数 10101.101 相当于
1 × 2 4 + 0 × 23 + 1 × 22 + 0 ×21+1 × 20 + 1 × 2-1 + 0 × 2-2 + 1 × 2-3。
写成通用表达式为:
±d.dd...d × 2e
其中d在0和1之间取值,e通过一个字节来表示,有符号数,取值范围为-127~+128。尾数是用3字节的(24bit)来表示,由于每个尾数的最高位(即小数点前的1位)总是1,所以不必存储,第1位作为符号位。这样,在计算机内部数据表示的通用格式为:
SEEE EEEE EMMM MMMM MMMM MMMM MMMM MMMM
在这里:
S:尾数的符号位,0表示正,1表示负。
E:指数的值,实际存储的是相对于127的偏移量
M:24位的尾数 (实际存储23位)。
0是一个特殊的值,其指数和尾数都是0。
以下以举几个例子来说明。
(1)-12.5
这个数在C51中的存储方式为0xC1480000,为何会是这样的一个数呢?这个数用二进制来表示就是:
11000001 01001000 00000000 00000000B
其中第1位是尾数的符号位,这里是1,说明这是一个负数。
指数的值是其后的8位即第1个字节的低7位和第2个字节的第1位,组合起来就是:
10000010B,相当于十进制的130。
尾数是其后的23位,即 10010000000000000000000B,实际尾数是用24位来表示的,它的最高位总是1,因此,这个数实际上是这样的:1.10010000000000000000000B,注意其中的小数点位置。
接着,要根据指数值来调整尾数的小数点了,如果指数大于127,那么小数点将右移,而如果这个值小于127,那么小数点将左移,移动的是指数相对于127的偏移量,刚才我们已计算出指数值是130,那么偏移量是130-127=3。这样调整后的数变为:
1100.10000000000000000000B
这个就是最终的数。计算一下:
23+22+2-1=12.5
加上最前面的符号为表示负,因此结果就是-12.5
(2)0.125
这个数在C51中的存储方式是0x3E000000,写成二进制数就是:
00111110 00000000 00000000 00000000B
其中第1位是尾数的符号位,这里是0,说明这是一个正数。
指数是其后的8位,即第一个字节的低7位和第二个字节的高一位即:
01111100B,就是十进制数的124。
而尾数是其后的23位,即0000000 00000000 00000000B,但实际的尾数是24位,第一位总是1,所以真实的尾数是:1.0000000 00000000 00000000B
接着根据指数来调整小数点的位置,由于124小于127,因此,小数点要左移,左移的位数是127-124=3位,这样,最终得到的数是:
0.00 10000000 00000000 00000000B
计算一下:1×2-3=0.125
2.关于浮点数精度的讨论
(1)基于习惯性思维,人们会认为,使用了小数点,可以使得所处理的数据获得很高的精度。其实这是不对的,使用浮点数原因是它能够表达的数的范围很大,而并非精度很高。对于单精度数,由于只有 24 位二进制位的尾数(其中一位隐藏),所以可以表达的最大尾数为 224 - 1 = 16,777,215。也就是说,单精度的浮点数可以表达的十进制数值中,真正有效的数字不高于 8 位。
(2)十进制小数转换为二进制
我们知道,十进制小数转化为二进制小数方法是不断地乘2并取整数部分,但是这种变换本身就会产生误差。例如一个十进制小数0.337转化为二进制小数
0.337×2=0.674 0
0.674×2=1.348 1
0.348×2=0.696 0
0.696×2=1.392 1
0.392×2=0.784 0
0.784×2=1.568 1
.......
这种计算有可能是无穷无尽的,但实际运算中不能无限地算下去,只能取有限的二进制位数。如果计算到这里作为结束,那么就认为0.337的二进制小数是0.010101B。下面再将这个二进制转化为十进制:
1×2-2+1×2-4+1×2-6=0.25+0.0625+0.015625=0.328125
这个数与0.337相差了:0.008875。仔细分析,不难发现,只有尾数是5且位数有限的十进制小数才有可能做到精确转换。可见,对于绝大多数的十进制小数来说,转化为二进制小数本身就有误差。综合第1条所讨论的精度情况,对于单精度浮点数来说,其有效的精度一般不会超过7位。
(3)Keil C51中没有双精度浮点数
在keil C51中可以使用double来定义双精度符号数,但C51处理double的方法和处理float型数据的方法是相同的,因此实际运算时并不会比float型数据得到更高的精度。
例:double x=10.002;
double y;
y=x*12.3456;
得到的结果是y=123.4807(理论上y=123.4806912)。
由此可知,浮点数远比整数复杂得多,这提醒我们,在进行浮点运算中,特别要小心。例如,在整数中判断两个变量a、b是否相等,可以这么样来写:
int a,b;
......对a,b进行计算等操作
if(a==b)
......
else
……
但是,对于浮点数这样来写可能就要出问题。
例:
float a,b;
........经过计算,理论上a和b都应等0.0123
if(a==b)
......
这样写很有可能会出问题,因为a和b也许永远也无法做到真正的完全相等,该条件永完无法实现,但如果写成:
if(a>=b+p)
……
其中p是一个非常小的数,如0.00001等,就比较合理,不容易出错。
3.浮点数中的特殊值
除了正常的浮点数以外,还有一些浮点数错误值。这些错误值作为IEEE的标准而存在,在自己编程时,可以使用函数检查,以确认是否有这种错误产生。IEEE中定义的浮点错误值为:
NaN 0xFFFFFFF 不是一个数
+INF 0x7F80000 正无穷大 (正数溢出)
-INF 0xFF80000 负无穷大 (负数溢出)
当出现诸如对一个负数开根、零除以零等操作时,将会产生NaN错误。如果有两个很大的数相乘,而其结果已超过了C51中浮点数所能表达的范围,此时产生一个正数溢出错误。这可以让程序员有可以通过编程来发现这样的错误,从而做出适当的处理,因此产生正数溢出比简单地让这个数等于浮点数所能表达的最大数更有价值。对于负数来说,也会有这样的一个溢出问题,所以在C51中分别定义了正无穷大和负无穷大两个特殊值。
......
更多的内容请看附件
文章评论(0条评论)
登录后参与讨论