首页 > 其他分享 >基于STM32F103标准库实现FFT,并实现音乐频谱绘制

基于STM32F103标准库实现FFT,并实现音乐频谱绘制

时间:2025-01-13 18:02:35浏览次数:3  
标签:STM32F103 频谱 DMA FFT TIM 采样 InitStructure ADC 256

整个工程文件是在江科大的OLED显示屏OLED-V2.0版本IIC四针脚接口UTF-8的工程上编写的,在屏幕显示过程中,只用到了OLED显示屏的绘制直线和绘制像素点两个函数(注意,显示屏的绘制函数坐标可以任意指定,而不是按页写入。任一屏幕只要有上述两个函数均可使用。

工程接线:

STM32F103C8T6 ---------TypeC供电/(3.3V也可以但现象可能不理想)

OLED          VCC-----------3.3V

                     GND---------GND

                     SCL----------PB8

                     SDA---------PB9

声音传感器   VCC---------3.3V

                     GND--------GND

                     AOUT-------PA0

                     DOUT-------悬空

一、制作过程

首先需要移植STM32官方的DSP库,在程序中,用定时器定时触发ADC采样,采集声音传感器得到音频信号,并用DMA进行数据转移。因为需要256点FFT变换,故每次触发ADC采样256个值。并将这些值预处理后做FFT变换并计算各次谐波的幅值。由FFT变换的性质可知,前128个幅值点与后128个幅值点对称,所以只需前128个数据(一般采样点的数量是屏幕像素点数的2倍,本款OLED为64*128像素,故推荐采用256点)。最后通过0.96寸OLED屏幕绘制频谱图。

二、需要使用到的模块

1.STM32F103C8T6

2.OLED显示屏(0.96寸四针脚IIC协议)

3.声音检测模块(LM386功放)

4.可调频率发生装置(可以手机或电脑模拟)

三、所用到的基本知识简介

1.声音传感器(LM386音频功放)简介

e3735b8673b146a0856411142808bb0c.png

参数名称                                 参数
音频放大芯片LM386(放大200倍)
工作电压3.3V-5.5V

LM386是一种音频集成功率放大器,具有自身功耗低、更新内链增益可调整、电源电压范围大、外接元件少和总谐波失真小等优点。主要应用于低电压消费类产品。为使外围元件最少,电压增益内置为20。在1脚和8脚之间增加一只外接电阻和电容,便可将电压增益调为200以内的任意值。

引脚表示                                 描述        
DOUT   数字量输出
AOUT模拟量输出
GND电源地
VCC电源正(3.3V-5V)

声音传感器的默认采样频率为1KHz,有效采样频段为50~20KHz(注:无法覆盖整个人耳可听到的频段20~20KHz),故实验实际效果低频段不太准确。

问题:AOUT和DOUT引脚所输出的是什么?

AOUT输出的实际上输出的就是声音信号,接上喇叭可以听到声音。声音信号变化很快,如果要采集声音信号需要很高采样率。例如电话采样率为8KHZ。波形的振幅表示声音的强度大小,声音越大振幅越大。故通过单片机采集到的AD值不能表示声音的大小。此模块也不能测声音具体多少分贝。
DOUT输出的是高低电平,当声音超过设置的阈值,输出低电平。

问题:模块上A和D可调电阻的作用?

A是调节放大增量,D是设置阈值。Aout大过阈值,Dout输出低电平,反之输出高电平

附:声音传感器用户手册及模块原理图

Sound-Sensor-UserManual.pdficon-default.png?t=O83Ahttps://www.waveshare.net/w/upload/1/1e/Sound-Sensor-UserManual.pdfSound_Sensor_V2.pdficon-default.png?t=O83Ahttps://www.waveshare.net/w/upload/c/ce/Sound_Sensor_V2.pdf

2.STM32DSP库FFT(快速傅里叶变换)简介

DSP库下载移植方法

方法一:在线安装(此方法安装了所有的关于DSP库函数)

点击Pack Installer选项

051e4db4e285468188da293f3361d7bb.png

等待加载完成,观察右半侧界面点击Generic->ARM::CMSIS-DSP选择任意版本点击Install即可(示例已安装过,所以为Remove)

b4e392a3b9c2492d8197055212414eb5.png

等待安装完成。

方法二:下载安装包安装(此方法仅提供了256点,1024点FFT变换文件)

基于STM32的FFT音乐频谱.zip
链接: https://pan.baidu.com/s/1XT_iIoIhcJJymkA5urFMVA?pwd=lkh0 提取码: lkh0 

点击此链接,下载文件名为DSP的文件

e3735b8673b146a0856411142808bb0c.png

下载解压完成之后,找到文件中名为DSP的文件.复制粘贴到自己的工程中,路径任意指定(若存放在已有的文件夹下,则可跳过第一步和第二步),因为官方库函数,所以我在工程中单独创建一个新的文件夹存放这些函数.

2c0d4f76dda649ca90f9652c26e1eb66.png

完成之后进入工程,引入库函数,并添加文件路径

进入工程之后,第一步点击Options for Target...选项

8a18ab9d33e747e3b25d1ba46f765081.png

第二步,在弹出的选项卡中,点击C/C++选项,找到Include Paths输入框,在输入框的最右端有三个点,点击该三个点选项,进入Folder Setup选项卡添加DSP库的路径即可.

028ae26e01b941eba6b6ccf3e25ae058.png

完成路径配置之后点击OK,OK,退出此界面.

第三步,点击File Extensions,Books and Environment...选项.

63a28f41d8dd4594a1f1e68f2a383939.png

第四步,在弹出的界面中,第二栏Groups添加新的分组名称为"DSP"或其它名称(若DSP文件存放在了以有的文件分组下,无需进行此次创建),创建完成后,点击新创建的分组选项(若DSP文件存放在了以有的文件分组下,则需点击父目录文件夹名称,开始配置第三栏Files,点击最下方Add Files...选项,在弹出的对话框中选中存放的路径文件夹,导入即可.(因为此次实验只进行256点FFT计算,所以我只添加了cr4_fft_256_stm32.s    ,stm32_dsp.h和table_fft.h则必须添加.(需要1024点FFT计算,只需把cr4_fft_256_stm32.s换成cr4_fft_1024_stm32.s即可,或者同时将他们引入)

9895bcc3ae584d59920cf48588122805.png

点击OK后,Project中弹出以下文件,完成配置.

600b03ca21ab4cbcac7d0b7a04ad6bee.png

附:大佬们的详细安装及测试教程

【玩转单片机系列002】 如何使用STM32提供的DSP库进行FFT - 依旧淡然 - 博客园前些日子,因为需要在STM32F103系列处理器上,对采集的音频信号进行FFT,所以花了一些时间来研究如何高效并精确的在STM32F103系列处理器上实现FFT。在网上找了很多这方面的资料做实验并进行比较,最终选择了使用STM32提供的DSP库这种方法。 本文将以一个实例来介绍如何使用STM3...icon-default.png?t=O83Ahttps://www.cnblogs.com/menlsh/p/4154070.html

STM32 DSP库的使用方法_keil uvision5有dsp库吗-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/u010058695/article/details/112665306

测试是否移植正确(该部分转载上述链接中部分内容):

首先在程序中用math库函数中的sin或cos函数模拟一段确知的连续信号

 1 /******************************************************************
 2 函数名称:InitBufInArray()
 3 函数功能:模拟采样数据,采样数据中包含3种频率正弦波(350Hz,8400Hz,18725Hz)
 4 参数说明:
 5 备    注:在lBufInArray数组中,每个数据的高16位存储采样数据的实部,
 6           低16位存储采样数据的虚部(总是为0)
 7 作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
 8 *******************************************************************/
 9 void InitBufInArray()
10 {
11     unsigned short i;
12     float fx;
13     for(i=0; i<NPT; i++)
14     {
15         fx = 1500 * sin(PI2 * i * 350.0 / Fs) +
16              2700 * sin(PI2 * i * 8400.0 / Fs) +
17              4000 * sin(PI2 * i * 18725.0 / Fs);
18         lBufInArray[i] = ((signed short)fx) << 16;
19     }
20 }

        其中,NPT是采样点数256,PI2是2π(即6.28318530717959),Fs是采样频率44800。可以看到采样数据中包含了3种频率的正弦波,分别为350Hz,8400Hz和18725Hz。

从代码中不难看出计算一个FFT变换,需要将所有数据左移16位,在本实验中传感器所采集到的数据均左移16位即可。高16位存放实数部分,低16位存放虚数部分。

        浅浅的提一下、在实际FFT变换中存在这样一种方法:通过一个eq?N点复数序列求出两个eq?N点实数序列的离散傅里叶变换,进一步提升快速傅里叶变换的效率。因此可以用128点傅里叶变换来巧妙的计算256点FFT傅里叶变换。但是,stm32提供的FFT函数库为基4的傅里叶变换(64点,256点,1024点)。

我们需要进行256点FFT变换,故调用此函数

        void cr4_fft_256_stm32(void *pssOUT, void *pssIN, u16 Nbin);

        参数1:*pssOUT一个用于存放结果的地址,通常为一个长度为Nbin/2的数组。

        参数2:*pssIN一个用于存放待变换数据的地址,通常为一个长度为Nbin的数组。

        参数3:指定待变换的点数。尽管参数在函数定义中被明确指定为 256 点,但这个参数可能用于函数的一般化设计,以便将来可以轻松地修改或扩展为其他点数的 FFT 变换。

  调用该函数之后,在pssOUT数组中就存放了进行FFT运算之后的结果数据。该数组中每个元素的数据格式为:高16位存储虚部,低16位存储实部。

计算各次谐波的幅值

/******************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备  注:先将lBufOutArray分解成实部(X)和虚部(Y),然后计算幅值(sqrt(X*X+Y*Y)
作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
*******************************************************************/
void GetPowerMag()
{
    signed short lX,lY;
    float X,Y,Mag;
    unsigned short i;
    for(i=0; i<NPT/2; i++)
    {
        lX  = (lBufOutArray[i] << 16) >> 16;
        lY  = (lBufOutArray[i] >> 16);
        X = NPT * ((float)lX) / 32768;
        Y = NPT * ((float)lY) / 32768;
        Mag = sqrt(X * X + Y * Y) / NPT;
        if(i == 0)
            lBufMagArray[i] = (unsigned long)(Mag * 32768);
        else
            lBufMagArray[i] = (unsigned long)(Mag * 65536);
    }
}

调用该函数,可计算出各次谐波的幅值,并存储在 lBufMagArray数组中.最后,通过串口或者OLED打印出该数值中的内容.

添加DSP库后,完整的测试代码如下

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "stm32_dsp.h"
#include "Sound_Sensor.h"
#include "math.h"

/******************************************************************
函数名称:InitBufInArray()
函数功能:模拟采样数据,采样数据中包含3种频率正弦波(350Hz,8400Hz,18725Hz)
参数说明:
备    注:在lBufInArray数组中,每个数据的高16位存储采样数据的实部,
          低16位存储采样数据的虚部(总是为0)
作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
*******************************************************************/

uint32_t NPT = 256, Fs = 44800;//44800
float PI2 = 6.283185;
long lBufInArray[256] = {0}, lBufOutArray[128],lBufMagArray[128];

/******************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备  注:先将lBufOutArray分解成实部(X)和虚部(Y),然后计算幅值(sqrt(X*X+Y*Y)
作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
*******************************************************************/
void GetPowerMag()
{
    signed short lX,lY;
    float X,Y,Mag;
    unsigned short i;
    for(i=0; i<NPT/2; i++)
    {
        lX  = (lBufOutArray[i] << 16) >> 16;
        lY  = (lBufOutArray[i] >> 16);
        X = NPT * ((float)lX) / 32768;
        Y = NPT * ((float)lY) / 32768;
        Mag = sqrt(X * X + Y * Y) / NPT;
        if(i == 0)
            lBufMagArray[i] = (unsigned long)(Mag * 32768);
        else
            lBufMagArray[i] = (unsigned long)(Mag * 65536);
    }
}

void InitBufInArray()
 {
     unsigned short i;
     float fx;
     for(i=0; i<NPT; i++)
     {
         fx = 1500 * sin(PI2 * i * 350.0 / Fs) +
              2700 * sin(PI2 * i * 8400.0 / Fs) +
              4000 * sin(PI2 * i * 18725.0 / Fs);
         lBufInArray[i] = ((signed short)fx) << 16;
     }
 }
int main(void)
{
    OLED_Init();            //OLED屏幕初始化   
    uint16_t i = 0;
    uint8_t x = 0;
    int16_t y = 0;
    InitBufIAnArray();
    cr4_fft_256_stm32(lBufOutArray, lBufInArray, NPT);
    GetPowerMag();
	while (1)
	{
        for(i=1; i<128; i++)
        {
            x = i;
            y = 63 - lBufMagArray[x] / 10;
            if(y < 0 )
            {
                y = 0;
            }
            OLED_DrawLine(x,y,x,64);
        }
        /*******************显示*******************/
        OLED_Update();
        OLED_Clear();
    }
}

移植成功实验现象:

3cff30141aaf4dabbe4d1aa0434ef8b3.png

在频率为350Hz,8400Hz和18725Hz时,幅值出现峰值,分别为1492、2696和3996(因像素限制最高只能为64,所以无法显示具体高度,峰值位置正确即可),对应的x值(OLED屏幕横坐标)为2,48,107.或者用串口打印出这些数据.若能观察到此现象或数据相符,则说明已经成功移植.

四、实验代码编写

本实验用到高级定时器1来触发ADC1的采样频率,确保采样间隔稳定.

ADC采样外部触发源

843b9118b19d4b9cb16643a1a65512f6.png

369eb4e3f8ba4c54b7fb15f649e1eb92.png

在该代码中,选择定时器1的通道2来输出PWM触发ADC1来定时采样

由奈奎斯特采样定律可知,采样频率需要大于两倍的信号频率,声音信号的频率范围为20~20kHz,若直接分辨整个人耳可听到的频率范围,至少需要40KHz的采样频率,但是,在屏幕上均匀量化显示,会发现频谱集中在OLED屏幕的前1/4区域,所以为了更好的显示效果,本实验采样频率为10KHz,所绘制的频谱图范围为0~5KHz.采样频率为10KHz,故把定时器的溢出时间(PWM的周期)设为100us.

TIM1.c

//TIM1配置,arr为重加载值,psc为预分频系数
void TIM1_Init(u16 arr,u16 psc)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_OCInitTypeDef TIM_OCInitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); 		//时钟使能

	//定时器TIM2初始化
	TIM_TimeBaseStructure.TIM_Period = arr; 		//设置在下一个更新事件装入活动的自动重装载寄存器周期的值
	TIM_TimeBaseStructure.TIM_Prescaler =psc; 			//设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; 		//设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; 		//TIM向上计数模式
	TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);			//根据指定的参数初始化TIMx的时间基数单位
	
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;		//选择定时器模式:TIM脉冲宽度调制模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;		//比较输出使能
	TIM_OCInitStructure.TIM_Pulse = 50;
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;		//输出极性:TIM输出比较极性低
	TIM_OC2Init(TIM1, & TIM_OCInitStructure);		//初始化外设TIM1_CH2
	
	TIM_Cmd(TIM1, ENABLE); 			//使能TIMx
	TIM_CtrlPWMOutputs(TIM1, ENABLE); 
}

在传感器代码中首先开始总线时钟,始终初始化GPIO代码,该实验选择ADC1的通道1,即PA0引脚,配置为模拟输入模式,只需采集一个通道DMA数据,因此ADC关闭扫描.由定时器定时触发采样,关闭连续转换模式.初始化ADC并使能ADC到DMA.打开DMA转运完成中断,在中断中完成数据移位操作,变成FFT函数可直接使用的格式.

Sound_Sensor.c 

uint16_t AD_Value[256];

void Sound_Sensor_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;            //GPIO配置结构体定义
	ADC_InitTypeDef ADC_InitStructure;              //ADC配置结构体定义
	DMA_InitTypeDef DMA_InitStructure;              //DMA配置结构体定义
	NVIC_InitTypeDef NVIC_InitStructure;            //NVIC配置结构体定义
    
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	        //使能GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	        //使能ADC1通道时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);	  			//使能ADC1通道时钟
    
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;                      //ADC采样,故需要模拟输入
	GPIO_Init(GPIOA, &GPIO_InitStructure);  
    
	//ADC1初始化
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; 		        	//独立ADC模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;  	            		//关闭扫描方式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;	        		//关闭连续转换模式
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC2;//触发源选择定时器1的通道二触发
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; 	    		//采集数据右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 1; 			                //要转换的通道数目
	ADC_Init(ADC1, &ADC_InitStructure);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);				                    //配置ADC时钟,为PCLK2的6分频,72M/6=12M,最好不要超过14M
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5);//配置ADC1通道6为1.5个采样周期,数据较多,以最快速度采样 
	
	ADC_DMACmd(ADC1,ENABLE);//使能DMA转运ADC1的数据
	ADC_Cmd(ADC1,ENABLE);   //使能ADC1
 
	ADC_ResetCalibration(ADC1);			                	//复位校准寄存器
	while(ADC_GetResetCalibrationStatus(ADC1));				//等待校准寄存器复位完成
 
	ADC_StartCalibration(ADC1);			                	//ADC校准
	while(ADC_GetCalibrationStatus(ADC1));	    			//等待校准完成
	
	ADC_ExternalTrigConvCmd(ADC1, ENABLE);	            	//设置外部触发模式使能
	
	//DMA1初始化
	DMA_DeInit(DMA1_Channel1);                   //配置DMA前需要先关闭DMA,因为本实验只初始化一次,在本实验中实测删去影响不大
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//ADC1地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; 		            //内存地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; 				            //方向(从外设到内存)
	DMA_InitStructure.DMA_BufferSize = 256; 						            //传输内容的大小
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 	    	//外设地址固定仅有一个传感器
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 			        //内存地址自增,需要256个数据
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord ; //以半字节长度进行传输(16位)
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord ;         //以半字节长度进行传输(16位)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular  ; 		                    //循环传输模式
	DMA_InitStructure.DMA_Priority = DMA_Priority_High ; 	            	    //优先级按需指定
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;   		                        //失能内存到内存的传输
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE);		//使能传输完成中断
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
	NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	DMA_Cmd(DMA1_Channel1,ENABLE);
    
    TIM1_Init(99,71);//72000000/7200=10000Hz,每100us采集一次
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "stm32_dsp.h"
#include "Sound_Sensor.h"
#include "math.h"

/******************************************************************
函数名称:InitBufInArray()
函数功能:模拟采样数据,采样数据中包含3种频率正弦波(350Hz,8400Hz,18725Hz)
参数说明:
备    注:在lBufInArray数组中,每个数据的高16位存储采样数据的实部,
          低16位存储采样数据的虚部(总是为0)
作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
*******************************************************************/

uint32_t NPT = 256, Fs = 9984;//采样频率为方便计算,选用一个接近10000且,Fs/NTP为整数的值(39)
//OLED显示中,第i个像素点表示的为i*39Hz的频率
float PI2 = 6.283185;
long lBufInArray[256] = {0}, lBufOutArray[128],lBufMagArray[128];

/******************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备  注:先将lBufOutArray分解成实部(X)和虚部(Y),然后计算幅值(sqrt(X*X+Y*Y)
作  者:博客园 依旧淡然(http://www.cnblogs.com/menlsh/)
*******************************************************************/
void GetPowerMag()
{
    signed short lX,lY;
    float X,Y,Mag;
    unsigned short i;
    for(i=0; i<NPT/2; i++)
    {
        lX  = (lBufOutArray[i] << 16) >> 16;
        lY  = (lBufOutArray[i] >> 16);
        X = NPT * ((float)lX) / 32768;
        Y = NPT * ((float)lY) / 32768;
        Mag = sqrt(X * X + Y * Y) / NPT;
        if(i == 0)
            lBufMagArray[i] = (unsigned long)(Mag * 32768);
        else
            lBufMagArray[i] = (unsigned long)(Mag * 65536);
    }
}
void InitBufInArray()
{
    unsigned short i;
    for(i=0; i<NPT; i++)
    {
        lBufInArray[i] = ((signed short)(AD_Value[i] - 2048)) << 16;
    }
}

int main(void)
{
    OLED_Init();            //OLED屏幕初始化   
    Sound_Sensor_Init();    //声音传感器模块初始化
    uint8_t fall_pot[128];
    for(int index = 0; index < 128; index++)//将下落的像素点初始化在最低端
    {
        fall_pot[index] = 63;
    }
    uint16_t i = 0;
    uint8_t x = 0;
    int16_t y = 0;
	while (1)
	{
        for(i=1; i<128; i++)        //绘制128个列像素的每一点幅值,从1开始是为了舍弃谐波中的第一个直流分量
        {
            x = i;                  //OLED显示屏列
            y = 63 - lBufMagArray[x] / 10;      //OLED显示屏左上角坐标为(0,0),右下角为(127, 63)、lBufMagArray[x]/10是为了量化显示结果
            if(y < 0 )          //如果y超出了屏幕,则按最大值绘制
            {
                y = 0;
            }
            OLED_DrawLine(x,y,x,63);//开始画线,前两个参数为线的顶端,后两个为低端位置,低端y轴固定为63
            /*为了更美观的显示,显示下落的点*/
            OLED_DrawPoint(i, fall_pot[i]);//绘制一个像素点
            if(fall_pot[i] >= y)            //像素点不断刷新,并由频谱线举起,(行坐标越小,代表越高)
            {
                fall_pot[i] = y;            //频谱线高于像素点时,更新像素点在频谱线上端
            }
            fall_pot[i]++;                  //使像素点没循环一次下降1,等效于 fall_pot[i]=fall_poll[i]+n;n=1;n越大,下降速度越快。
        }
        /*******************统一显示*******************/
        OLED_Update();
        OLED_Clear();
    }
}

//中断处理函数
void  DMA1_Channel1_IRQHandler(void)
{
	if(DMA_GetITStatus(DMA1_IT_TC1)!=RESET)
    {
		//中断处理代码
        InitBufInArray();           //将数据256个数据均左移16位
        cr4_fft_256_stm32(lBufOutArray, lBufInArray, NPT);//进行FFT变换
        GetPowerMag();                                      //计算各次谐波幅值
		DMA_ClearITPendingBit(DMA1_IT_TC1);                 //清除完成中断标志位
	}
}

经实测、在while循环中可适当加入一些延时函数,这样更容易观察声音的节奏。

五、最终程序实验现象

1.频谱分析测试

固定频率发生在线网址:在线音调发生器 - 播放任何音调或频率

<iframe allowfullscreen="true" data-mediaembed="csdn" frameborder="0" id="UFkT1R2K-1736752339285" src="https://live.csdn.net/v/embed/443110"></iframe>

固定频率频谱

在视频中我们可以观察到,当音调发生频率在5000Hz左右时,峰值刚好在OLED屏幕最右侧,所以达到了实验的预期效果。当频率大于5000Hz时,会发生频谱混叠,现象视频中没有展示。

2.音乐频谱显示

<iframe allowfullscreen="true" data-mediaembed="csdn" frameborder="0" id="Boil0pWy-1736759439219" src="https://live.csdn.net/v/embed/443152"></iframe>

音乐频谱

        在视频中可观察到低频仍然存在较大的波动,经过测试,可能是供电电压不稳定所导致,因此可以选择使用Type-C接口供电,以上情况可能会有所改善。

另外指出,该频谱线宽仅为1px,可适当增加线宽提高观赏性。这里暂不提供代码,思路与上述过程一致,对128个数据等间隔采样即可。

本程序源码:https://pan.baidu.com/s/1XT_iIoIhcJJymkA5urFMVA?pwd=lkh0 提取码: lkh0 

以上内容为个人制作过程所遇到的问题,因能力有限,文章中难免会出现错误,观点仅供参考!

标签:STM32F103,频谱,DMA,FFT,TIM,采样,InitStructure,ADC,256
From: https://blog.csdn.net/2302_80229212/article/details/145097224

相关文章

  • 听音乐还可以这样玩!音乐与视觉的盛宴 让音频频谱美化你的桌面
    听音乐还可以这样玩!音乐与视觉的盛宴让音频频谱美化你的桌面 对于热爱音乐的你来说,每一次聆听都是一次心灵的触动。而现在,芝麻桌面美化软件将这份触动可视化,让你的电脑桌面随着音乐的节奏焕发出独特的生命力。音频频谱效果首先:先到官网下载芝麻桌面美化软件(https://zhima......
  • InnerAudioContext.offTimeUpdate
    InnerAudioContext.offTimeUpdate(functionlistener)基础库1.9.0开始支持,低版本需做兼容处理。小程序插件:支持功能描述移除音频播放进度更新事件的监听函数参数functionlisteneronTimeUpdate传入的监听函数。不传此参数则移除所有监听函数。示例代码constliste......
  • 深入了解分治 FFT
    问题提出算法应用于问题,分治FFT的出现是为了解决这样一个问题:给定序列\(g_{1\dotsn-1}\),求序列\(f_{0\dotsn-1}\)。其中\(f_i=\sum_{j=1}^if_{i-j}g_j\),边界为\(f_0=1\)。具体可以见【模板】分治FFT-洛谷对于这个问题我们要求做到\(\Theta(n\log^2n)\)的......
  • wx.offThemeChange
    wx.offThemeChange(functionlistener)基础库2.11.0开始支持,低版本需做兼容处理。小程序插件:不支持相关文档:DarkMode适配指南功能描述移除系统主题改变事件的监听函数参数functionlisteneronThemeChange传入的监听函数。不传此参数则移除所有监听函数。示例......
  • 信号特征提取及处理/FFT实现/FFT和IFFT(MATLAB)/含噪声信号频域简单幅值阈值处理/频谱
            传统的离散傅里叶变换(DFT)虽然能够实现时域到频域的转换,但计算复杂度极高,对于大规模数据的处理效率十分低下。直到快速傅里叶变换(FFT)算法的出现,才极大地改变了这一局面。FFT算法利用了信号的对称性和周期性等特性,将DFT的计算复杂度从原本的  大幅降低到 ,......
  • 毕业设计基于STM32F103C8T6智能小车设计PWM调速、红外循迹、障碍物跟随、超声波避障、
    (页数:61页、字数:14473字)1绪论1.1前言1.2设计任务与要求1.3智能小车硬件设计思路1.3.1智能小车控制板设计思路1.3.2智能小车底板设计思路2单片机的组成及特点2.1单片机的组成2.2单片机的特点2.3STM32F103C8T6单片机介绍2.4STM32F103C8T6单片......
  • 【教程4>第4章>第11节】通过FPGA实现带频偏基带数据的FFT变换
    本课程学习效果预览 欢迎订阅FPGA/MATLAB/Simulink系列教程《★教程1:matlab入门100例》《★教程2:fpga入门100例》《★教程3:simulink入门60例》《★教程4:FPGA/MATLAB/Simulink联合开发入门与进阶X例》目录1.软件版本2.理论简介3.通过FPGA实现带频偏基带数据的F......
  • 【教程4>第4章>第12节】通过FPGA实现基于FFT变换的频偏估计和补偿
    本课程学习效果预览 欢迎订阅FPGA/MATLAB/Simulink系列教程《★教程1:matlab入门100例》《★教程2:fpga入门100例》《★教程3:simulink入门60例》《★教程4:FPGA/MATLAB/Simulink联合开发入门与进阶X例》目录1.软件版本2.理论简介3.通过FPGA实现基于FFT变换的频偏......
  • 使用频谱分析仪:RBW,Res BW,分辨率带宽;Sweep,扫描;noise floor,底噪,如何降低底噪?
    RBW与Sweep的定义及其特性阐述:ResBW,即ResolutionBandwidth(分辨率带宽),是衡量仪器分辨信号细节能力的重要参数。当RBW的数值越小,意味着像素点的尺寸更为精细,从而能够观察到更为细微的信号特征。Sweep,则指的是扫描时间,它直接关联到信号的刷新速率。具体而言,Sweep时间的增长会......
  • 基于STM32F103的数字电压表设计
    基于STM32F103的数字电压表设计仿真软件:      Proteus8.17编程软件:      Keil5仿真实现:      使用STM32F103的内部ADC(模数转换器)测量外部电压(用电位器来进行电压的改变),将测量到的电压值显示在数码管上,同事与仿真中的电压表进行对比,误差很小。......