STM32F4的FPU启用和基于ARM-DSP库函数的实时信号RMS计算

avatar
作者
猴君
阅读量:2

概述

最近,由于项目需要,学习了一下F4的FPU浮点数运算单元的使用方法和DSP库函数的建立使用,在此简单总结记录。

项目背景分析:

  • MCU:STM32F446RET6
  • 信号链:由传感器采集所需模拟信号,经前端放大、滤波等电路处理后,用24bit ADC采样,通过SPI传送至MCU。
  • 模拟信号主要特征:①信号电压范围为1.7V-3.4V;②频率变化范围为10Hz-50Hz;③峰峰值变化范围为0.3V-1V。
  • 需求:获取该信号的RMS值,用于后续进一步处理。

目标与流程梳理:

  • 目标:实时信号采集、处理
  • 启用FPU运算单元
  • 搭建DSP库函数运行环境
  • 编写信号处理程序
  • 处理结果测试

下面基于STM32F446RET6,使用标准库函数(版本:V1.8.0),在Keil MDK 5.26环境下,完成上述需求。


文章目录


一、FPU的启用

FPU 即浮点运算单元(Float Point Unit)。浮点运算,对于定点 CPU(没有 FPU 的 CPU)来说必须要按照 IEEE-754 标准的算法来完成运算,是相当耗费时间的。而对于有 FPU 的 CPU来说,浮点运算则只是几条指令的事情,速度相当快。

STM32F4 属于 Cortex M4F 架构,带有 32 位单精度硬件 FPU,支持浮点指令集,相对于Cortex M0 和 Cortex M3 等,高出数十倍甚至上百倍的运算性能。

在这里插入图片描述
打开system_stm32f4xx.c文件,在SystemInit函数中,已规定了FPU的启用条件,即486行所示:

  • __FPU_PRESENT == 1
  • __FPU_USED == 1

前者是要检查MCU是否拥有FPU,后者使能FPU,二者需同时满足。

打开stm32f4xx.h头文件,如下图第190行所示,文件已经在编译时将__FPU_PRESENT置为1,意思是该MCU存在FPU,那么下面只需关注如何将__FPU_USED置1即可。

在这里插入图片描述

使能FPU的方法有很多,对于Keil MDK环境,最方便的方法是在Code Generation中将Floating Point Hardware配置为Single Precision,如下图所示:

在这里插入图片描述
经过这个设置,编译器会自动加入标识符__FPU_USED为 1。 这样,遇到浮点运算就会使用硬件FPU相关指令,执行浮点运算,从而大大减少计算时间。

在调试窗口,可以看到对于进行浮点运算的指令,反汇编代码中存在xxx.F32的指令,说明FPU已开启,如下图所示。

在这里插入图片描述

需要注意,当运算中有浮点的数字时,数字后面要加上一个f,例如:

a = (float)Para*9.1E-02f; 

若不加f,Keil会提示warning: #1035-D: single-precision operand implicitly converted to double-precision,意思是单精度运算隐式转换成了双精度运算了,运算速度将无法显著提升。

二、DSP库环境搭建

1、DSP标准库

对于复杂运算,例如RMS计算,如果编程时还是使用math.h头文件,效率提升很有限,因为math.h头文件是针对所有ARM处理器的,其运算函数都是基于定点CPU和标准算法(IEEE-754),没有使用FPU。因此,要充分发挥M4F的浮点功能,需要使用固件库自带的arm_math.h,即搭建DSP运行环境。

STM32F4的DSP库源码和测试实例在ST提供的标准库:stm32f4_dsp_stdperiph_lib.zip里提供,官方下载链接:

http://www.st.com/web/en/catalog/tools/FM147/CL1794/SC961/SS1743/PF257901

在这里插入图片描述

这里使用V1.8.0版本的标准函数库,下载完成后打开DSP_Lib源码包的Source文件夹,里面存放了所有DSP库的源码, 如上图所示。Examples文件夹是相对应的一些测试实例。以下是Source源码文件夹下面的子文件夹包含的DSP库的功能:

BasicMathFunctions
基本数学函数:提供浮点数的各种基本运算函数,如加减乘等运算。

CommonTables
arm_common_tables.c文件提供位翻转或相关参数表。

ComplexMathFunctions
复杂数学功能,如向量处理,求模运算的。

ControllerFunctions
控制功能函数。包括正弦余弦,PID电机控制,矢量Clarke变换,矢量Clarke逆变换等。

FastMathFunctions
快速数学功能函数。提供了一种快速的近似正弦,余弦和平方根等相比CMSIS计算库要快的数学函数。

FilteringFunctions
滤波函数功能,主要为FIR和LMS等滤波函数。

MatrixFunctions
矩阵处理函数。包括矩阵加法 、矩阵初始化 、矩阵反、矩阵乘法、矩阵规模、矩阵减法、矩阵转置等函数。

StatisticsFunctions
统计功能函数。 如求平均值、最大值 、最小值、计算均方根RMS、计算方差/标准差等。

SupportFunctions
支持功能函数,如数据拷贝,Q格式和浮点格式相互转换,Q任意格式相互转换。

TransformFunctions
变换功能。包括复数FFT(CFFT)/复数FFT逆运算(CIFFT)、实数FFT(RFFT)/实数
FFT逆运算(RIFFT)、和DCT(离散余弦变换)和配套的初始化函数。

所有这些DSP库代码合在一起是比较多的,因此,ST提供了.lib格式的文件,方便使用。这些.lib文件就是由 Source文件夹下的源码编译生成的,如果想看某个函数的源码,可在Source文件夹下面查找。

在这里插入图片描述
我们需要根据所用MCU内核类型以及端模式来选择符合要求的 .lib文件,而STM32F4属于CortexM4F内核,小端模式,应选择: arm_cortexM4lf_math.lib(浮点Cortex M4,小端模式)。

2、DSP库运行环境搭建

  1. 添加文件

在工程目录下新建文件夹DSP,放置arm_cortexM4lf_math.lib和相关头文件,过程如图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Include文件夹直接拷贝STM32F4xx标准外设库_V1.8.0\Libraries\CMSIS\Include中的所有文件。

打开工程,添加分组,将arm_cortexM4lf_math.lib添加到工程里面,如图:

在这里插入图片描述

  1. 添加头文件包含路径

添加好.lib文件后,要添加头文件包含路径,将第一步拷贝的Include文件夹和DSP文件夹,加入头文件包含路径,如图:

在这里插入图片描述

  1. 添加全局宏定义

最后,为了使用DSP库的所有功能,还需要添加几个全局宏定义:

ARM_MATH_CM4 __CC_ARM ARM_MATH_MATRIX_CHECK ARM_MATH_ROUNDING 

对于STM32F446RET6,需要添加的所有宏为:

USE_STDPERIPH_DRIVER,STM32F446xx,ARM_MATH_CM4,__CC_ARM,ARM_MATH_MATRIX_CHECK,ARM_MATH_ROUNDING 

注意两个宏之间用,隔开,如下图所示:

在这里插入图片描述

至此,DSP库运行环境搭建完成。


三、基于DSP库的实时信号RMS计算

1、RMS的相关概念

对于电压信号来说,RMS是其方均根值,即有效值。与平均值的概念不同,RMS直接反映信号的能量特征,对于连续的模拟信号而言,其定义计算式如下:

在这里插入图片描述
而对于经过ADC采样后的数字信号而言,其计算式可表示为:

Result = sqrt(((pSrc[0] * pSrc[0] + pSrc[1] * pSrc[1] + ... + pSrc[blockSize-1] * pSrc[blockSize-1]) / blockSize)); 

其中,Result为RMS计算结果,pSrc[]为采样后的数字量,blockSize为处理的数据量。

2、程序编写

打开StatisticsFunctions文件夹中的arm_rms_f32.c文件,可见DSP库对于RMS计算的函数定义,如下图所示:

在这里插入图片描述

因此,要使用arm_rms_f32函数,需要向其传递三个参数:

  • 要进行计算的数据的地址,可将转换后的数据直接存入数组,数组内是待处理的数据。
  • 计算的数据量,即数组中的元素个数。
  • 计算结果地址。

程序编写的整体思路是:

  1. 定义数据存储数组和相关指针、变量;
  2. ADC对模拟差压信号采样,将数字量转换为实际电压值;
  3. 将实际电压值存入数组;
  4. 将数据数组传递至RMS计算函数;
  5. 打印输入、输出数据,于上位机查看;
  6. 以上流程封装为函数Flow_RMS,放在定时器中断服务函数中,控制采样率和输出;
  7. 在此基础上,完善其它控制功能。

另外,若要实现实时信号RMS计算处理,则要每存一个数据,计算一次RMS值,即不断更新数组中的元素,这一需求可通过操作数组指针完成,当指针从数组首地址移动到尾地址时,将指针归位到首地址,思路上类似于数据结构中的循环队列。

为提高程序运行效率,数组元素的赋值采用指针操作,这样往往比下标的赋值方式更快。

根据上述思路,编写程序如下:

PUMP.c

#include "PUMP.h" #include "ads1256.h" #include <stdio.h>  float DataInput[DATA_LENGTH] = {0}; 	//传感器电压值存储数组、RMS计算输入数据存储数组初始化 float *InputPOINT = DataInput;			//输入数据数组指针初始化  float DataOutput;                		//计算后的RMS值 float *OutputPOINT = &DataOutput;		//计算结果变量指针  float *ifPoint = &DataInput[DATA_LENGTH - 1] + 1;	//判断指针,定义于输入数组的最后一个元素+1的地址处  int blockSize = 0;						//每次RMS计算的数据量 int NPA_DData;  						//差压传感器ADC数字值 float NPA_VData;		 				//差压传感器ADC电压值  void Flow_RMS(void) {	  	NPA_DData = Ads1256ReadData(ADS1256_MUXP_AIN1|ADS1256_MUXN_AINCOM);    //ADC采集数据 	NPA_VData = (float)NPA_DData/8388607.0f*5.0f;						   //转换为电压值 	 	*InputPOINT = NPA_VData;		//把数据存入数组,指针指向数组中的某个元素地址 	InputPOINT ++;					//地址+1,以存放下一个数据 	 	if(blockSize != DATA_LENGTH)	//若DataInput数组中的元素数未满,则使RMS计算的数据量+1,与当前数据量保持一致;反之则维持blockSize = DATA_LENGTH不变 		blockSize ++;	  	if(InputPOINT == ifPoint)		//若输入数组的指针移动到数组的最后一个元素+1的地址处,则使指针重新归位 		InputPOINT = DataInput; 	 	 	arm_rms_f32(DataInput, blockSize, OutputPOINT);		//对采集的数据进行RMS计算 	 	printf("%f, %f\r\n", DataOutput, NPA_VData);		//打印RMS计算结果和实时信号值 	 }  

PUMP.h

#ifndef __PUMP_H #define __PUMP_H  #include <stm32f4xx.h> #include "arm_math.h"  #define DATA_LENGTH  250     //数组长度  void Flow_RMS(void);  #endif  

TIM3中断服务函数,4ms-250Hz

void TIM3_IRQHandler(void)   { 	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM3中断发生与否 	{		 		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);   //清除TIM3的中断待处理位 		 		Flow_RMS(); 	} }  

3、结果测试

使用微型气泵为差压传感器提供气压差源,配置不同占空比控制气泵抽气速率。确定条件下,使用VOFA+上位机观察输出,结果如下:

在这里插入图片描述

可见,虽然输入为一系列不规则波形(与气泵引动的气流有关),但经RMS计算,其均方根值基本稳定。由此可见,该方法非常适合于对不规则信号的直流化的应用中,如噪声计算等,此时应考虑硬件RMS计算方案和软件哪种更合适。

后续程序可根据实际应用,加以完善。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!