浮点数的IEEE 754标准
简而言之,该标准采用了以2为基数的科学记数法记录实数,并将记数范围上的4个边界值定义为不同的特殊值。
上述元素之间的关系为:
符号域S记录了浮点数的符号;阶码域P -偏移量Bias构成了指数(Exponent);尾数域M(Mantissa)则仅记录了实际的尾数的小数部分(Fraction),而其整数部分由于规范化表达而默认为1,为节约空间故不存储;而对于基数(Base),都默认为2。
注:尾数有时候也称为有效数字(Significand)。
以单精度浮点数的一般表达为例作进一步的解释:
符号位S —— 为0时表示正数,为1时表示负数;
阶码域P —— 填入0000 0001 ~ 1111 1110(1~254)范围内的值,通过表达式减去Bias(127)后就表达了数值范围为 -126 ~ +127的指数,可见P的正常取值范围是去头去尾的;
尾数域M —— 填入0000 0000 0000 0000 0000 0000 ~ 111 1111 1111 1111 1111 1111范围内的值,表达了实际尾数的小数部分,实际尾数为“1.M”;
偏移量Bias —— 该值固定为127,是为了方便表达负指数而引入的,相当于将指数取值范围的原点移动到了阶码域P取值范围的中位。
在某些特殊的浮点运算操作下,会造成特殊值,特殊值有:
- 当指数为-127(对应P域全为0),尾数≠0时,表示的是特殊值“非规范化数”;
- 当指数为-127,尾数=0时,表示的是特殊值“0”;
- 当指数为128(对应P域全为1),尾数≠0时,表示的是标识符“NaN(Not a Number)”;
- 当指数为128,尾数=0时,表示的是特殊值“∞”。
上述特殊值的一般形式可整理为如下表格:
> 非规范化数:
非规范数和规范化数之间的主要区别在于:
规范化数尾数的整数部分默认为“1”,所表达的完整尾数为“1.M”;
非规范数尾数的的整数部分默认为“0”,所表达的完整尾数为“0.M”。
非规范数的引入是为了应对两个非常接近的浮点数相减的情况,其运算结果将非常接近0,以至于超出了规范化表达的指数范围(如:运算1.0001*2-125-1.0000*2-125结果为0.0001*2-125,规范化表达为1.0000*2-129,超出了指数的最小值-126)。
此时,若粗暴地将此类结果归0并继续运算的话,在进一步除法运算(如用1.0*2-4除上述结果)时,将导致特殊值“∞”的出现。然而其实际结果却为1,是一个巨大的误差。
为此,IEEE标准引入了非规范数来解决上述问题,当运算出现指数<-126而尾数≠0的结果时,浮点数将自动转换为非规范表达:其尾数域的整数部分默认转变为0,指数部分保留为-127(对应P域全为0),而尾数的小数部分则可以直接表示。
如此一来,上述例子的运算结果可保留为0.001*2-127。可见,非规范数的引入进一步拓展了IEEE标准对微小浮点数的表达能力,使其指数域取值范围拓展为[-150, 127](最接近0的值为±0.0000 0000 0000 0000 0000 001*2-127),该范围可完全覆盖任意两个相近浮点数(规范化或非规范)相减的结果。
> NaN(Not a Number):
NaN用于处理计算中出现错误的情况,如:0.0÷0.0或求负数的平方根等。
要注意的是,所有的NaN值都是无序的。数值比较操作符==、<、<=、>和>=在任一操作数为NaN时均返回False;而操作符!=则当任一操作数为NaN时返回True。
(转载自:https://blog.csdn.net/tercel_zhang/article/details/52537726)
> +0和-0:
一般情况下,+0和-0是等同的。
但是,若遇到以0为除数的运算,用+0的结果将为+∞,用-0的结果将为-∞。这有可能引发错误,需谨慎处理。
IEEE浮点数舍入策略——“四舍六入五留双”:
以单精度浮点数为例,其24位的尾数(隐藏1位)决定了其最大有效数字(二进制下)为24个。当运算的结果出现了多于24位的有效数字时,必须对多出的有效数字进行舍入。舍入的核心目的即是尽可能使舍入后的结果与真实值误差最小(尽可能减小舍入处理的影响)。
IEEE标准的舍入策略设计于应对以下3种情况:
- 当最低有效位的后一位是0时,类似于“四舍”,应向下舍入;
- 当最低有效位的后一位是1,且往后的数位不全为0时,类似于“六入”,应向上舍入;
- 当最低有效位的后一位是1,且往后的数位全为0时,舍入过程遵循“使得最低有效位为0(又称作向偶数舍入)”。
简而言之,该策略就是十进制中的“四舍六入五留双”!
IEEE浮点数的精度
实际上,计算机内以二进制表达浮点数就是在区间[0,1]内以“二分法”对十进制浮点数的小数部分进行逼近的“过程”。
举个例子,十进制的浮点数0.1,表示为具有8位有效数字位(尾数域为7位)的二进制浮点数的过程为:
整数部分= 0,二进制整数也为0;
0.1*2 = 0.2,取整数部分0;
0.2*2 = 0.4,取整数部分0;
0.4*2 = 0.8,取整数部分0;
0.8*2 = 1.6,取整数部分1,余0.6; 【第1个有效数字】
0.6*2 = 1.2,取整数部分1,余0.2; 【第2个有效数字】
0.2*2 = 0.4,取整数部分0; 【第3个有效数字】
0.4*2 = 0.8,取整数部分0; 【第4个有效数字】
0.8*2 = 1.6,取整数部分1,余0.6; 【第5个有效数字】
0.6*2 = 1.2,取整数部分1,余0.2; 【第6个有效数字】
0.2*2 = 0.4,取整数部分0; 【第7个有效数字】
0.4*2 = 0.8,取整数部分0; 【第8个有效数字,舍入为1】
0.8*2 = 1.6,取整数部分1,余0.6; 【需要舍入的数字】
0.6*2 = 1.2,取整数部分1,余0.2; 【舍入参考的数字】
结果为:B0.00011001101
其逼近结果的规范化表达为,对应十进制逼近结果为0.10009765625。直观地判断,这个结果有11位有效数字,那么是否就能认为具有8位尾数的二进制浮点数就可以表达出具有11位有效数字的十进制浮点数呢?
然而并不是!考察有效数字的定义:
有效数字的定义——> 如果近似数的绝对误差不超过它某位数字的半个单位,那么从左到右,第一个不为零的数字起,到这位数字止,每一位数字都称为有效数字。
可见对于近似数0.10009765625,与目标值0.1的差的绝对值为0.00009765625小于0.0005(小数点后第3位的半个单位),因此,其有效数字位只有3位!这是通过直接计算转换结果的误差来计算转换的有效数字位的方法。
此外,还可以通过浮点数的尾数来计算其转换为十进制后的有效数字位数,令尾数域全为1,该二进制数所代表的最大十进制整数的位数就近似于其有效数字。如:上述8位尾数为1111 1111时,表达的十进制数是255,有3位有效数字,因此8位尾数浮点数可表达的有效数字位就为3位;同理,24位尾数浮点数可表达的有效数字位约为8位。
根据上述描述可推测,IEEE浮点数可表达的十进制浮点数的范围和精度如下:
(图片转载自:https://blog.csdn.net/tercel_zhang/article/details/52537726)
浮点数“尺子”比喻
进一步地,讨论用上述8尾数位浮点数表达距离B0.00011001101最近的数,容易推测,应该是B0.00011001100和B0.00011001110,那么我们来看一下这两个数对应的十进制浮点数是多少呢?
根据“四舍六入五留双”的原则,只要在实数轴上落于微小区间内的小数,都会被归纳为0.10009765625,正如0.1与0.10009765625的差为0.00009765625小于2-12(0.000244140625),就被归纳到了0.10009765625的点上。同理,落在该区域内另外两边的小数也会被归纳到0.099609375和0.1005859375的点上。
更形象地说,计算机内的浮点数就是一把“尺子”,以之表达十进制实数就是在用这把“尺子”来量某一条直线的长度,并将该测量结果作为该直线的真实长度来使用。如此一来,就不难理解二进制浮点数有效数字的含义了。
该图展示的是4尾数位2指数位浮点数“尺子”的上的刻度
(图片转载自:https://blog.csdn.net/dreamer2020/article/details/24158303)
> 实际上,大部分的十进制浮点数都无法完全准确地转换为二进制浮点数。
因此,涉及到浮点数运算的代码需要格外谨慎地设计,应尽量避免运算过程中出现数值过小的数据,并在类似
If( 变量 == 0 ) {...;}
的判断中,用一个误差值ERROR来替代0,如替换为:
If( 变量 >= - ERROR && 变量 <= ERROR ) {...;}
—————————————————————————分界线——————————————————————————
Cortex-M4的浮点运算支持
硬件支持单精度浮点运算,提供浮点运算指令集,单周期完成运算;
软件支持双精度浮点运算,使用C语言提供的Run-time Library函数,运算速度较慢,但是对于某些三角函数的运算又是必须的;
支持定点运算(不常用),数据的存储和运算与整型数类似(增加移位操作),运算速度较快,但是可表达的数值较少。
Cortex-M4 FPU——FPv4-SP(Floating Point version 4 - Single Precision)
FPU作为Cortex-M4的协处理器,其协处理编号为#10和#11,可在System Control Block(SCB)的“协处理器访问控制寄存器(CPACR)”中加以使能与权限设置。
与其他协处理器不同,ARM架构CPU通过Thumb-2指令集中的“协处理器访问指令”来访问其他协处理器,而通过FPU专用的“V指令”来访问FPU。不过在Cortex-M4中,FPU是其唯一的协处理器。
> Cortex-M4的FPU属于ARMv7-M处理器架构的一个拓展子集FPv4-SP,而该拓展子集又从属于ARMv7-A和ARMv7-R处理器架构的拓展子集VFPv4-D16(VFP既Vector Floating Point),因此其指令集全都以“V”开头,以区别于其他指令。
FPU与CPU流水线的关系如图所示,可见两者基本上是并行运行的:
FPU可完成单精度浮点数的硬件计算、与FPU寄存器相关的数据传输任务,以及以下数据类型的双向转换任务:
单精度浮点数 <----> 整型数
单精度浮点数 <----> 半精度浮点数
单精度浮点数 <----> 定点数
FPU的使用
使能方面:
用户可以通过在CMSIS-Core文件中设置宏定义__FPU_USED的值为1来使能FPU;也可以通过在Keil-MDKC上设置Code Generation的FPU选项来完成。此时,在编译器生成的代码的SystemInit()函数内就会出现相应的FPU使能指令(完成了写CPACR)。
代码的编译:
对于Cortex-M4处理器,其FPU只能完成单精度浮点数的硬件运算,但是在应用程序中不可避免地需要进行一些双精度浮点运算(或其他复杂的浮点运算),此时还是需要调用“C Run-time Library”的相关函数来完成。然而两种实现过程所用到的指令集和寄存器空间都不一样,切不可用相同的方式进行编译。
换而言之,对不同类型的浮点数需要采用不同编译策略,以实现系统效率的最优化。
Keil-MDK编译器在编译浮点运算有关的函数时,默认的ABI(Application Binary Interface)选项为Auto,也就是ARM手册中所述的“Soft ABI with FPU(-fpu = softvfp + fpv4-sp)”。
如下图中的第二种ABI选项所示,在该模式下编译不同的函数时,编译器根据不同函数的ELF文件内记录的函数属性来决定是由软件还是硬件来完成函数内的浮点运算。
- 当函数需要完成简单浮点运算(单精度)时,采用Hard-float模式,编译器将代码直接编译成发射给FPU执行;
- 当函数需要完成复杂浮点运算(双精度)时,采用Soft-float模式,编译器把浮点运算转换成浮点运算的函数调用和库函数调用,没有FPU的指令调用,也没有浮点寄存器的参数传递,浮点参数的传递也是通过ARM寄存器或者堆栈完成。
> ABI(Application Binary Interface)
用来将C语言源码翻译为对应硬件平台(如ARMv7-M/A/R、Cortex-M、x8086等)的指令集汇编语言的转换协议!
> ELF(Executable and Linking Format)格式
可执行链接格式,该格式用于记录目标文件(*.o、*.elf、*.axf等)的内容,告诉编译器应该如何链接、加载及执行该应用程序(或函数)。
ELF格式包含ELF文件头、程序头、节区(Section)以及节区头部表,功能分别为:
ELF文件头:用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头在文件中的位置等;
程序头:告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。
节区:是*.o文件的独立数据区域,它包含提供给链接器使用的大量信息,如指令(Code)、数据(RO、RW、ZI-data)、符号表(函数、变量名)、重定位信息等,例如每个由C语言定义的函数在*.o文件中都会有一个独立的节区;
节区头:包含了本文件节区的信息,如节区名称、大小等等。
举例:
(转载自:https://blog.csdn.net/weixin_33795093/article/details/93637835)
补充记录一些KEIL-MDK开发的知识点
目标文件有3类,分别为:
- 可重定位文件(Relocatable File),包含基础代码和数据,但它们都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。如:KEIL-MDK生成的*.o文件,Linux的*.o文件,Windows的*.obj文件等。
- 可执行文件(Executable File),它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统(在这里讨论的就是编译器)可根据这些地址信息把程序加载到内存中执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。如:例如KEIL-MDK的armlink生成的*.elf及*.axf文件,Linux的/bin/bash文件,Windows的*.exe文件等。
- 共享目标文件(Shared Object File),类似动态链接库的文件,可以在程序执行的过程中继续参与链接,加入到可执行文件之中。如:Linux的/lib/glibc-2.5.so,Windows的DLL等。> 对于一般的嵌入式系统应用应该不存在这种文件?
hex、bin及axf文件的区别与联系:
bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。
bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据),在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;
axf文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。
(转载自:https://blog.csdn.net/weixin_33795093/article/details/93637835)
MicroLIB:
MicroLIB是缺省 C 库的备选库。 它用于必须在极少量内存环境下运行的深层嵌入式应用程序。 这些应用程序不在操作系统中运行。MicroLIB 进行了高度优化以使代码变得很小。它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性。某些库函数的运行速度也比较慢,例如,memcpy( )。MicroLIB不会尝试成为符合标准的 ISO C 库。
选上“Use MicroLIB”这是KEIL-MDK自带的一个简易的库,例如你用printf()函数的时候,就会从串口1输出字符串,直接默认定向到串口1。
(转载自:https://blog.csdn.net/kelsey11/article/details/51246636)
半主机模式(Semi-Host):
半主机是用于ARM Target的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机(Debug用的PC机)。例如,使用此机制可以启用C库中的函数,如:printf()和scanf(),来使用主机的屏幕和键盘,而不是在目标系统上配备屏幕和键盘。
半主机是通过一组定义好的软件指令(如SVC, Supervisor Call)来实现的,这些指令通过程序控制生成异常。应用程序调用相应的半主机调用,然后调试代理处理该异常。调试代理提供与主机之间的必需通信。