分享

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

 知芯世界 2020-10-28

浮点计算单元(FPU)的测试。先还是用前面那个程序,修改了一下,避免了中间的double/single类型转换:

  1. uint32_t test(int32_t *data, uint32_t size)

  2. {

  3.     uint32_t i;

    完整代码请点击阅读原文

  4. }

复制代码

  在默认编译指令下,上面的代码不产生浮点指令,而是调用gcc的软浮点程序来计算的:

  1. 10000440 <test>:

  2. 10000440:       e92d 41f0       stmdb   sp!, {r4, r5, r6, r7, r8, lr}

  3. 10000444:       2900            cmp     r1, #0

  4. 完整代码请点击阅读原文

  5. 100004b0:       e8bd 81f0       ldmia.w sp!, {r4, r5, r6, r7, r8, pc}

  6. 100004b4:       4c0260f4 

  7. 100004b8:       49742400 

复制代码

  上面反汇编中没有一条是FPU的指令。形如 __aeabi_fmul() 这样的函数是gcc库函数,实现浮点运算。sinf() 和 cosf() 两个函数是C标准库 libm.a 中的。
  如何让GCC生成FPU的指令呢?搜了下,在 http://gcc./onlinedocs/gcc/ARM-Options.html 列出了ARM平台的特定选项,其中 -mfpu=xxx 指定使用FPU,不过还没完,还需要 -mfloat-abi=... 指定浮点参数如何传递。结合 https://en./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. 完整代码请点击阅读原文

  4. 100004b4:        4c0260f4

  5. 100004b8:        49742400

复制代码

  除了 sinf() 和 cosf() 两个函数没有直接对应的浮点指令的外,其它运算都直接翻译成FPU的指令了。这两个函数在 libm.a 当中,软件算法也是要使用很多的浮点计算的,那么它用指令还是用模拟实现呢?查看下arm-gcc的目录树,发现 libm.a 这个文件有很多个,在不同的子目录下:
 
  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——程序获得超过十倍的加速。
 
总结:需要单精度浮点运算的时候,使用STM32F413 Cortex-M4F处理器中的FPU是可以极大提高计算能力。没有FPU的时候,浮点计算开销一部分是软件模拟算法的,一部分是运算函数调用产生的。CMSIS的DSP数学库有许多巧妙的算法可以去发掘,牺牲精度,换来更短的执行时间。

STM32F413有两个32-bit的Timer, 最高可以在100MHz频率下计数。虽然100MHz并不是很高,对于一般单片机背景的DIY项目来说,测个频率也差不多够用了。最直接的测量频率的方法是用被测时钟信号作为定时器的时钟输入,然后将定时器打开,等待一个固定的时间段再关闭,看计数值是多少。 
虽然这样可以测量很高的频率,比如用F413能测到100MHz,但是缺点是分辨率与频率成正比,测量低频信号的相对分辨率就很低了。例如一秒的测量时间,对1000000Hz和1000001Hz是可分辨的,相对分辨率为1ppm;对50Hz和50.2Hz是不能分辨的,相对分辨率不到1%,因为有效数字位数随频率降低而减少。要在不同测量频率下保持同样的相对分辨率,一个办法是用固定参考频率去对测量信号的跳变沿进行捕捉(定位),测量出信号的N个周期经历的总时间T,用N/T计算频率。在基本的测量时间段内,被测量信号频率降低,周期数减少,但总的时间仍然是约等于测量时间,所以一除之后小数的有效数字就增加,分辨率向低频扩展。
 
我曾经用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的计数源——这个功能可以省去软件记录捕捉次数了。

  我的程序核心是这段:
 
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功能,测试信号接在这里
 
  测试对象:我几年前做的4060振荡+分频时钟模块
 
8kHz输出的测量。因为两个晶振都是有误差的,结果不能作为计量标定。由于是等精度频率计,得益于STM32F413的100MHz Timer, 8kHz分辨到0.0001Hz没有难度(若用高精度时钟给MCU,就可以做到高精度)。
 
测量频率数字的抖动,除了捕捉本身有一两个时钟沿的随机误差外,可能是MCU内部PLL时钟抖动引起的。另外,测试了这个简单的软件频率计测量到20MHz还可用,要测更高频率就得外部加硬件分频器了。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多