[CW32F030系列] 如何使用 ARM FPU 加速浮点计算

[复制链接]
738|11
 楼主| tabmone 发表于 2025-4-23 12:26 | 显示全部楼层 |阅读模式
一、浮点数的存储浮点数按照 IEEE 754 标准存储在计算机中,ARM浮点环境是遵循 「IEEE 754-1985」 标准实现的。
IEEE 754 标准规定浮点数的存储格式有三个域,如图:



  • sign:符号位,0表示正数、1表示负数;
  • exponent:二进制小数的指数值编码;
  • fraction:二进制小数的有效值编码;
具体的编码规则过多,本文重点不在此,不再展开,感兴趣可以阅读我之前的文章:浮点数在计算机中的存储 —— IEEE 754标准[1](可点击阅读原文查看)。
二、浮点支持软件库fplib
1. fplib介绍ARM Cortex-M处理器中计算浮点数的方式有软件和硬件两种。
对于不带 FPU 的处理器,ARM提供了一个「浮点支持软件库」用于计算浮点数:fplib
fplib提供的 API 以__aeabi开头,比如:
  • __aeabi_fadd:计算两个float型浮点数(float占4个字节,32位)
  • __aeabi_dadd:计算两个double型浮点数(double占8个字节,64位)
  • __aeabi_f2d:float型转为double型
  • __aeabi_d2f:double型转为float型
除此之外,fplib库还提供取余、开方等非常多的浮点数操作函数,如有兴趣可以查阅文末我列出的参考文档[2]。
2. 测试代码与优化等级编写如下测试代码:



  1. float a = 5.625;
  2. float b = 5.625;
  3. float res_add, res_sub, res_mul, res_div;

  4. res_add = a + b;
  5. res_sub = a - b;
  6. res_mul = a * b;
  7. res_div = a / b;

  8. printf("res_add = %f\r\n", res_add);
  9. printf("res_sub = %f\r\n", res_sub);
  10. printf("res_mul = %f\r\n", res_mul);
  11. printf("res_div = %f\r\n", res_div);



❝使用这段测试代码,「编译器优化等级推荐设置为-O0」,否则聪明的编译器会直接将结果计算出来编译到程序中,我们就没法研究了。❞



3. armcc测试结果这节我们验证是否ARM使用 fplib 库来计算浮点数,在设置中关闭FPU:



使用MDK编译之后,进入调试模式查看反汇编结果。
在反汇编中可以看到,变量a是float类型,所以编译器分配了一个寄存器用于存储值:



查看0x080031C4处的值,小端存储模式下(低位在低地址),变量a的值是0x40B40000,存储方式符合IEEE 754标准。






再来看看浮点数运算操作的反汇编结果,果然调用fplib库提供的函数完成浮点数的操作:



这里还有一个有趣的小细节,在反汇编中可以看到「使用 %f 占位符打印浮点数时,printf是按照double型传参的」:



4. arm-none-eabi-gcc测试结果使用STM32CubeMX生成makeifle工程,修改makeifle中的等级为-O0,设置为软件浮点计算:



另外还需要注意,默认gcc编译时不支持printf打印浮点数,需要在 makefile 中手动加入以下链接选项:+=

编译完成之后进行反汇编(注意文件名):
-none-objdump -d build-fpu.elf  /usart1-test

同样,在反汇编文件中即可找到浮点计算代码:



三、使用 ARM FPU 加速浮点计算1. ARM FPU的魅力FPU(Floating Point Unit,浮点单元)是ARM内核中的硬件外设,用于硬件计算浮点数,要想使用FPU计算浮点数,需要程序和编译器配合。
  • 在程序中使能/开启FPU硬件外设,「使 FPU 硬件可以正常工作」;
  • 在编译器中设置使用FPU,编译器会将所有浮点计算的代码都编译为「使用FPU操作指令完成」。
目前Cortex-M4、Cortex-M7、Cortex-M33、Cortex-M35P、Cortex-M55处理器中都具备FPU硬件。
在上一节中我们使用fplib软件库来计算浮点数,但是fplib终归还是软件方式,每个计算函数的实现都是通过很多的指令去完成计算,并且最终的程序中还会把函数链接进可执行程序,导致程序体积变大。
「ARM FPU的魅力在于,浮点计算可以通过简单的FPU操作指令去完成,相比之下,不仅计算快,也不会增大程序体积。」
2. 如何使能FPU硬件ARM Cortex - M4内核中将 FPU 作为协处理器设计的,所以通过设置协处理器访问控制(CPACR,Co-processor access control register)来控制是否使能FPU。



复位之后CP11=0、CP10=0,默认禁止访问FPU,因为这是Cortex-M内核的外设,寄存器定义CMSIS-Core中,所以可以直接通过下面这行代码设置CP11=1、CP10=1来允许访问FPU:
SCB>= ;

无论是STM32 HAL库还是标准库,在SystemInit()函数中已经存在使能代码,通过__FPU_PRESENT__FPU_USED来控制:
/* FPU settings ------------------------------------------------------------*/

#if (__FPU_PRESENT == 1) && (__FPU_USED == 1) SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));  /* set CP10 and CP11 Full Access */#endif

并且,在头文件 stm32l431xx.h 中已经使能__FPU_PRESENT宏定义:



__FPU_PRESENT宏定义是一直使能的,那么如何来控制FPU的使能呢?
别忘了还有一个宏定义__FPU_USED,这是留给编译器来控制的!
3. ARMCC编译器如何开启FPUMDK编译器开启FPU的方法非常简单,如图:



在MDK中使能FPU,一方面编译器会设置宏定义__FPU_USED == 1,不放心的话可以在任意位置添加下面的预处理代码,分别在使用/不使用的情况编译一下,查看编译器输出结果:
if __FPU_USED 1


#error FPU = -mfpu=fpv4-sp-d16# float-abiFLOAT-ABI = -mfloat-abi=hard

ABI是应用程序二进制接口(Application Binary Interface),-mfloat-abi用来指定使用哪种方式:
  • soft:使用CPU寄存器组+软件库(fplib)完成浮点操作;
  • softfp:使用CPU寄存组+FPU硬件+软件库完成浮点操作;
  • hard:使用FPU寄存器组+FPU硬件+软件库完成浮点操作;
mfpu选项用来指定FPU架构,具体值可以阅读我在文末给出的参考文档,本文所使用的值fpv4-sp-d16,意味着仅仅使能Armv7 FPv4-SP-D16 单精度浮点单元扩展。
同样,对之前的测试代码编译,查看反汇编结果,可以看到使用了浮点操作全部使用了FPU相关指令。



四、使用**测试FPU加速性能1. 测试准备需要准备一份裸机工程,具有屏幕打点显示功能和串口打印功能。
2. 移植**分形测试代码**测试是通过计算几帧**分形的数据来测试单精度浮点运算的性能,测试代码参考正点原子,如下:

  1. /* Private user code ---------------------------------------------------------*/
  2. /* USER CODE BEGIN 0 */
  3. #define ITERATION 128 //迭代次数
  4. #define REAL_CONSTANT 0.285f //实部常量
  5. #define IMG_CONSTANT 0.01f //虚部常量

  6. //颜色表
  7. uint16_t color_map[ITERATION];

  8. //缩放因子列表
  9. const uint16_t zoom_ratio[] =
  10. {
  11.     120, 110, 100, 150, 200, 275, 350, 450,
  12.     600, 800, 1000, 1200, 1500, 2000, 1500,
  13.     1200, 1000, 800, 600, 450, 350, 275, 200,
  14.     150, 100, 110,
  15. };

  16. //初始化颜色表
  17. //clut:颜色表指针
  18. void InitCLUT(uint16_t * clut)
  19. {

  20.     uint32_t i = 0x00;
  21.     uint16_t red = 0, green = 0, blue = 0;

  22.     for (i = 0;i < ITERATION; i++) {
  23.         //产生 RGB 颜色值
  24.         red = (i*8*256/ITERATION) % 256;
  25.         green = (i*6*256/ITERATION) % 256;
  26.         blue = (i*4*256 /ITERATION) % 256;
  27.         
  28.         //将 RGB888,转换为 RGB565
  29.         red = red >> 3;
  30.         red = red << 11;
  31.         green = green >> 2;
  32.         green = green << 5;
  33.         blue = blue >> 3;

  34.         clut[i] = red + green + blue;
  35.     }
  36. }

  37. //产生 ** 分形图形
  38. //size_x,size_y:屏幕 x,y 方向的尺寸
  39. //offset_x,offset_y:屏幕 x,y 方向的偏移
  40. //zoom:缩放因子
  41. void Generate**_fpu(uint16_t size_x,uint16_t size_y,uint16_t offset_x,uint16_t offset_y,uint16_t zoom)
  42. {

  43.     uint8_t i;
  44.     uint16_t x,y;
  45.     float tmp1,tmp2;
  46.     float num_real,num_img;
  47.     float radius;

  48.     for (y = 0; y < size_y; y++) {
  49.         for (x = 0; x < size_x; x++) {
  50.             num_real = y - offset_y;
  51.             num_real = num_real / zoom;
  52.             num_img = x-offset_x;
  53.             num_img = num_img / zoom;

  54.             i = 0;
  55.             radius = 0;
  56.             while ((i < ITERATION-1) && (radius < 4)) {

  57.                 tmp1 = num_real * num_real;
  58.                 tmp2 = num_img * num_img;
  59.                 num_img = 2*num_real*num_img + IMG_CONSTANT;
  60.                 num_real = tmp1 - tmp2 + REAL_CONSTANT;
  61.                 radius = tmp1 + tmp2;
  62.                 i++;
  63.             }
  64.             //绘制到屏幕
  65.             lcd_draw_color_point(x, y, color_map[i]);
  66.         }
  67.     }
  68. }

  69. /* USER CODE END 0 */





在main函数中创建一些需要的变量:
/* USER CODE BEGIN 1 */


    uint8_t zoom_index 0= , end_time 0/* USER CODE END 1 */

调用初始化函数:

printf"** test by Mculover666\r\n";(;InitCLUT)/* USER CODE END 2 */

调用测试函数:

  1. /* Infinite loop */
  2. /* USER CODE BEGIN WHILE */
  3. while (1)
  4. {
  5.   /* USER CODE END WHILE */
  6.    
  7.   /* USER CODE BEGIN 3 */
  8.   start_time = HAL_GetTick();
  9.   Generate**_fpu(240, 240, 120, 120, zoom_ratio[zoom_index]);
  10.   end_time = HAL_GetTick();
  11.   printf("diff time is %d ms\r\n", end_time - start_time);
  12.     zoom_index++;
  13.       if (zoom_index > sizeof(zoom_ratio)) {
  14.           zoom_index = 0;
  15.       }            
  16. }
  17. /* USER CODE END 3 */



3. 测试结果使用-O2优化等级,在不开 FPU 的情况下,「显示一帧平均需要11s左右」:



程序大小情况:



使用-O2优化等级,在开启 FPU 的情况下,「显示一帧平均需要4s左右」:



程序大小情况:




Jiangxiaopi 发表于 2025-6-9 16:06 | 显示全部楼层
ARM Cortex-M系列的FPU默认关闭,需通过协处理器访问控制寄存器开启
Zhiniaocun 发表于 2025-6-9 16:42 | 显示全部楼层
可以直接设置寄存器,比如SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2));  // 设置CP10和CP11为全访问


Zuocidian 发表于 2025-6-9 17:19 | 显示全部楼层
编译器需设置为生成硬浮点指令,而非调用软件库
Puchou 发表于 2025-6-9 17:55 | 显示全部楼层
若使用[size=0.875]printf等函数,需手动链接浮点支持库
Xiashiqi 发表于 2025-6-9 18:31 | 显示全部楼层
启用FPU后,编译器会将浮点运算转换为硬件指令,而非函数调用
小海师 发表于 2025-6-9 19:06 | 显示全部楼层
对于ARM Cortex-A系列,可结合NEON指令集实现SIMD并行计算
Haizangwang 发表于 2025-6-9 19:42 | 显示全部楼层
编译后查看反汇编代码,确认浮点运算使用的是VFP指令而非函数调用
八层楼 发表于 2025-6-9 20:18 | 显示全部楼层
在启用FPU前,需先配置时钟和中断,避免初始化冲突
guanjiaer 发表于 2025-6-9 20:49 | 显示全部楼层
FPU运算可能产生浮点异常,需确保系统异常向量表正确配置
heimaojingzhang 发表于 2025-6-9 21:26 | 显示全部楼层
FPU会增加芯片功耗和面积,需根据实际需求权衡
抱素 发表于 2025-7-9 15:49 | 显示全部楼层
开启 ARM FPU 功能,编译时启用浮点指令集,代码中直接使用浮点运算。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

37

主题

1686

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部