[原创] Nucleo-F413ZH 工程版测评(中): 浮点计算, 频率计

cruelfox   2016-12-26 09:33 楼主
  浮点计算单元(FPU)的测试。先还是用前面那个程序,修改了一下,避免了中间的double/single类型转换:
  1. uint32_t test(int32_t *data, uint32_t size)
  2. {
  3. uint32_t i;
  4. float diff=0.0;
  5. for(i=0; i< size; i++)
  6. {
  7. float angle=data[i]/3.4178e+7f;
  8. float x = cosf(angle);
  9. float y = sinf(angle);
  10. float x2 = x*x;
  11. float y2 = y*y;
  12. float c= x2+y2;
  13. diff += fabsf(c - 1.0f);
  14. }
  15. return diff*1e6f;
  16. }
  在默认编译指令下,上面的代码不产生浮点指令,而是调用gcc的软浮点程序来计算的:
  1. 10000440 <test>:
  2. 10000440: e92d 41f0 stmdb sp!, {r4, r5, r6, r7, r8, lr}
  3. 10000444: 2900 cmp r1, #0
  4. 10000446: d032 beq.n 100004ae <test+0x6e>
  5. 10000448: 4604 mov r4, r0
  6. 1000044a: 2500 movs r5, #0
  7. 1000044c: eb00 0881 add.w r8, r0, r1, lsl #2
  8. 10000450: f854 0b04 ldr.w r0, [r4], #4
  9. 10000454: f000 fe9e bl 10001194 <__aeabi_i2f>
  10. 10000458: 4916 ldr r1, [pc, #88] ; (100004b4 <test+0x74>)
  11. 1000045a: f000 f9f7 bl 1000084c <__aeabi_fdiv>
  12. 1000045e: 4607 mov r7, r0
  13. 10000460: f000 ffd8 bl 10001414 <cosf>
  14. 10000464: 4606 mov r6, r0
  15. 10000466: 4638 mov r0, r7
  16. 10000468: f001 f812 bl 10001490 <sinf>
  17. 1000046c: 4631 mov r1, r6
  18. 1000046e: 4607 mov r7, r0
  19. 10000470: 4630 mov r0, r6
  20. 10000472: f000 fbcf bl 10000c14 <__aeabi_fmul>
  21. 10000476: 4639 mov r1, r7
  22. 10000478: 4606 mov r6, r0
  23. 1000047a: 4638 mov r0, r7
  24. 1000047c: f000 fbca bl 10000c14 <__aeabi_fmul>
  25. 10000480: 4601 mov r1, r0
  26. 10000482: 4630 mov r0, r6
  27. 10000484: f000 f876 bl 10000574 <__aeabi_fadd>
  28. 10000488: f04f 517e mov.w r1, #1065353216 ; 0x3f800000
  29. 1000048c: f000 fcec bl 10000e68 <__aeabi_fsub>
  30. 10000490: f020 4100 bic.w r1, r0, #2147483648 ; 0x80000000
  31. 10000494: 4628 mov r0, r5
  32. 10000496: f000 f86d bl 10000574 <__aeabi_fadd>
  33. 1000049a: 4544 cmp r4, r8
  34. 1000049c: 4605 mov r5, r0
  35. 1000049e: d1d7 bne.n 10000450 <test+0x10>
  36. 100004a0: 4905 ldr r1, [pc, #20] ; (100004b8 <test+0x78>)
  37. 100004a2: f000 fbb7 bl 10000c14 <__aeabi_fmul>
  38. 100004a6: f000 f84d bl 10000544 <__aeabi_f2uiz>
  39. 100004aa: e8bd 81f0 ldmia.w sp!, {r4, r5, r6, r7, r8, pc}
  40. 100004ae: 4608 mov r0, r1
  41. 100004b0: e8bd 81f0 ldmia.w sp!, {r4, r5, r6, r7, r8, pc}
  42. 100004b4: 4c0260f4
  43. 100004b8: 49742400
  上面反汇编中没有一条是FPU的指令。形如 __aeabi_fmul() 这样的函数是gcc库函数,实现浮点运算。sinf() 和 cosf() 两个函数是C标准库 libm.a 中的。   如何让GCC生成FPU的指令呢?搜了下,在 http://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html 列出了ARM平台的特定选项,其中 -mfpu=xxx 指定使用FPU,不过还没完,还需要 -mfloat-abi=... 指定浮点参数如何传递。结合 https://en.wikipedia.org/wiki/ARM_architecture 的信息,Cortex-M4F对应的FPU应该是 fpv4-sp-d16,所以在GCC编译选项中增加 -mfpu=fpv4-sp-d16 以及 -mfloat-abi=softfp, 然后可以看到编译代码变化:
  1. 10000440 <test>:
  2. 10000440: b538 push {r3, r4, r5, lr}
  3. 10000442: ed2d 8b06 vpush {d8-d10}
  4. 10000446: b389 cbz r1, 100004ac <test+0x6c>
  5. 10000448: ed9f 9a19 vldr s18, [pc, #100] ; 100004b0 <test+0x70>
  6. 1000044c: ed9f aa19 vldr s20, [pc, #100] ; 100004b4 <test+0x74>
  7. 10000450: 4604 mov r4, r0
  8. 10000452: eb00 0581 add.w r5, r0, r1, lsl #2
  9. 10000456: eef7 9a00 vmov.f32 s19, #112 ; 0x70
  10. 1000045a: ecf4 8a01 vldmia r4!, {s17}
  11. 1000045e: eef8 8ae8 vcvt.f32.s32 s17, s17
  12. 10000462: eec8 8a8a vdiv.f32 s17, s17, s20
  13. 10000466: ee18 0a90 vmov r0, s17
  14. 1000046a: f000 fe95 bl 10001198 <cosf>
  15. 1000046e: ee08 0a10 vmov s16, r0
  16. 10000472: ee18 0a90 vmov r0, s17
  17. 10000476: f000 fecd bl 10001214 <sinf>
  18. 1000047a: ee06 0a90 vmov s13, r0
  19. 1000047e: ee66 7aa6 vmul.f32 s15, s13, s13
  20. 10000482: 42ac cmp r4, r5
  21. 10000484: eee8 7a08 vfma.f32 s15, s16, s16
  22. 10000488: ee77 7ae9 vsub.f32 s15, s15, s19
  23. 1000048c: eef0 7ae7 vabs.f32 s15, s15
  24. 10000490: ee39 9a27 vadd.f32 s18, s18, s15
  25. 10000494: d1e1 bne.n 1000045a <test+0x1a>
  26. 10000496: eddf 7a08 vldr s15, [pc, #32] ; 100004b8 <test+0x78>
  27. 1000049a: ee29 9a27 vmul.f32 s18, s18, s15
  28. 1000049e: eebc 9ac9 vcvt.u32.f32 s18, s18
  29. 100004a2: ee19 0a10 vmov r0, s18
  30. 100004a6: ecbd 8b06 vpop {d8-d10}
  31. 100004aa: bd38 pop {r3, r4, r5, pc}
  32. 100004ac: 4608 mov r0, r1
  33. 100004ae: e7fa b.n 100004a6 <test+0x66>
  34. 100004b0: 00000000
  35. 100004b4: 4c0260f4
  36. 100004b8: 49742400
  除了 sinf() 和 cosf() 两个函数没有直接对应的浮点指令的外,其它运算都直接翻译成FPU的指令了。这两个函数在 libm.a 当中,软件算法也是要使用很多的浮点计算的,那么它用指令还是用模拟实现呢?查看下arm-gcc的目录树,发现 libm.a 这个文件有很多个,在不同的子目录下: libm.PNG   Cortex-M4F 对应的 ARM 版本是 ARM-v7e-m, 故有三个 libm.a 对应,分别是默认的、softfp目录下的和fpu目录下的。softfp 和 hard (fpu)是不同的ABI模式,就是参数传递约定不同,不能通用,所以分开成两个库了。默认的那个数学库,应该是不使用浮点处理器的。好,GCC是如何选择使用哪个库来连接呢?我发现是根据 -mfpu 和 -mfloat-abi 选项的。如果直接调用 ld 程序来连接,就要自己选择库文件了。   比较下几种方式的平均执行周期:数值越小执行越快
一般运算用库函数一般运算用FPU
库函数软件模拟 54115359 48338666
库函数用FPU 12504408 6730899
  可发现即使数学库函数用FPU进行浮点运算,在代码中直接用浮点指令而不经过函数调用能节省很多机器周期。   从ST网站下载的软件开发包里面,有CMSIS的数学库:arm_math. 而且提供了源程序。不妨把其中的 arm_sin_f32() 和 arm_cos_f32() 两个函数拿出来试试。这两个函数计算三角函数是用查表加插值的方法。我对比发现虽然它快,计算误差也比GCC的函数大了很多,不要求很准确才敢用啊。   在不用FPU的条件下,平均执行周期为 27879682; 用了FPU以后变为 2326043——程序获得超过十倍的加速。 cycles.PNG   总结:需要单精度浮点运算的时候,使用STM32F413 Cortex-M4F处理器中的FPU是可以极大提高计算能力。没有FPU的时候,浮点计算开销一部分是软件模拟算法的,一部分是运算函数调用产生的。CMSIS的DSP数学库有许多巧妙的算法可以去发掘,牺牲精度,换来更短的执行时间。 本帖最后由 cruelfox 于 2016-12-26 09:33 编辑

回复评论 (2)

  STM32F413有两个32-bit的Timer, 最高可以在100MHz频率下计数。虽然100MHz并不是很高,对于一般单片机背景的DIY项目来说,测个频率也差不多够用了。最直接的测量频率的方法是用被测时钟信号作为定时器的时钟输入,然后将定时器打开,等待一个固定的时间段再关闭,看计数值是多少。 timer.PNG   虽然这样可以测量很高的频率,比如用F413能测到100MHz,但是缺点是分辨率与频率成正比,测量低频信号的相对分辨率就很低了。例如一秒的测量时间,对1000000Hz和1000001Hz是可分辨的,相对分辨率为1ppm;对50Hz和50.2Hz是不能分辨的,相对分辨率不到1%,因为有效数字位数随频率降低而减少。要在不同测量频率下保持同样的相对分辨率,一个办法是用固定参考频率去对测量信号的跳变沿进行捕捉(定位),测量出信号的N个周期经历的总时间T,用N/T计算频率。在基本的测量时间段内,被测量信号频率降低,周期数减少,但总的时间仍然是约等于测量时间,所以一除之后小数的有效数字就增加,分辨率向低频扩展。 capture.PNG   我曾经用Atmel 8位的ATMega48做过一个等精度频率计,MCU运行在10MHz,利用16bit定时器的捕捉功能,做到分辨率6个数字。受到MCU定时器频繁访问、程序指令开销的限制,我用汇编优化了程序,最高也只能测量到300kHz频率。如今有了更强大的STM32F413,我不妨再试下此法能做到什么程度。因为我发现它有几个重要的优点:   (1)STM32F413的Timer 2和Timer 5是32-bit计数的,在以1秒为测量总时间的条件下绝不会溢出。否则必须跟踪每次捕捉的结果,看是否溢出了:16-bit Timer的最大计数范围65535而已。   (2)STM32F413的Timer 2/5 捕捉通道可以使用DMA将每次捕捉的值传送到RAM,比CPU去查询读取快。   (3)STM32F413的Timer 2捕捉通道可以产生一个触发事件,作为Timer 5的计数源——这个功能可以省去软件记录捕捉次数了。 TI1cap.PNG   我的程序核心是这段: code.PNG   Timer 2用来捕捉,clock是最高的100MHz,使用CH1的捕捉功能,并使用DMA1 Stream5进行传输。测量时间是1秒,因此又用了Timer 2的CH4通道比较功能,当记数到100000000时触发中断。因为只关心首次捕捉和最后一次捕捉的结果,而最后一次捕捉结果会保留在CCR1寄存器中,就只需要想办法保存第一次捕捉就行了。我是采用DMA来将捕捉发生时CCR1寄存器的值读取,然后写到SRAM的存储区中,只传输开始的几次捕捉就够了(理论上一次就可以,但频率高了在初始时似乎有不稳定的状态,多存一些后面再判断处理)。   Timer 5工作在从模式,用Timer 2输出的事件进行触发计数。启动顺序是先启动Timer 5, 再启动Timer 2; 关闭则顺序反过来。Timer关闭后整理结果,计算频率,再循环重新开始。   配置PA0为TIM2_CH1功能,测试信号接在这里 PA0.jpg   测试对象:我几年前做的4060振荡+分频时钟模块 4060.jpg   8kHz输出的测量。因为两个晶振都是有误差的,结果不能作为计量标定。由于是等精度频率计,得益于STM32F413的100MHz Timer, 8kHz分辨到0.0001Hz没有难度(若用高精度时钟给MCU,就可以做到高精度)。 freq.PNG   测量频率数字的抖动,除了捕捉本身有一两个时钟沿的随机误差外,可能是MCU内部PLL时钟抖动引起的。另外,测试了这个简单的软件频率计测量到20MHz还可用,要测更高频率就得外部加硬件分频器了。 本帖最后由 cruelfox 于 2016-12-26 09:57 编辑

    freq.c (2016-12-25 00:04 上传)

    6.03 KB, 阅读权限: 5, 下载次数: 2

点赞  2016-12-26 09:33
点赞  2017-1-1 22:07
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复