音乐频谱显示小玩具——FFT在STM32中的实现与应用
0、前言音乐频谱显示说白了就是“儿童版”频谱仪。笔者平时比较喜欢听音乐,闲暇之余听音乐的时候如果有个频谱显示的小玩具在旁边跳来跳去的也挺有意思的,所以笔者去万能的某宝上搜索了一下,发现便宜的都很小,大一点的都很贵,而且都需要音频接头输入,很麻烦,所以笔者就自制了这个小玩具。效果图如下图1所示。效果视频:https://www.bilibili.com/video/BV1dZ4y1x77d...
0、前言
音乐频谱显示说白了就是“儿童版”频谱仪。笔者平时比较喜欢听音乐,闲暇之余听音乐的时候如果有个频谱显示的小玩具在旁边跳来跳去的也挺有意思的,所以笔者去万能的某宝上搜索了一下,发现便宜的都很小,大一点的都很贵,而且都需要音频接头输入,很麻烦,所以笔者就自制了这个小玩具。效果图如下图1所示。
效果视频:自制音乐频率响应小玩具_哔哩哔哩_bilibili
其功能有一下3个:
1、实时显示音乐频谱
2、将小玩具倒立,显示当前时间和温度。
3、在显示时间和温度时,一段时间后屏幕自动熄灭。触动小玩具即可再次点亮
1、FFT(Fast Fourier transform)
快速傅里叶变换是一种利用计算机进行高速快捷的离散傅里叶变换的一种方法,而什么是傅里叶变换?傅里叶变换的物理意义是什么呢?
当年,著名的数学家傅里叶表示:任何连续测量的时序或信号,都可以表示为不同频率的正弦波信号的无限叠加。而通过傅里叶变换就可以将叠加的波形以正弦波为基本单位将其拆分开。
而快速傅里叶变换为什么可以快速呢?首先先看一下离散傅里叶变换的公式:
通过公式不难看出如果我们让计算机去计算时,我们需要计算N^2次运算。但是通过观察可以发现,在计算中有想当一般的数据都是冗余的,并且不会显示在频谱中。所以我们可以将这些数据保存下来计算的时候直接调用或在此数据基础上做简单的运算。通过这样的方法我们就可以将DFT的N^2次运算减少到Nlog10(N)次运算了。
例如如果做1000采样点的傅里叶变换使用DFT的运算次数就是1000000次,而使用FFT就可以减少到3000次运算。
2、STM32实现FFT
因为笔者做的这个玩具使用的芯片是STM32单片机F10系列。所以在做FFT的时候没有官方库的支持(以前是有,但是F4系列上线后就取消了,真实万恶的资本主义\dogs)虽然网上有很多教程是直接移植或者找之前的DSP库,但是笔者觉得着实有些麻烦,F10系列没有DSP运算核心,这个DSP库就犹如花瓶。
这里主要借鉴了这位大神的文章:《C》C语言实现FFT算法_杨贵安的博客-CSDN博客_c语言fft
在单片机上做FFT时需要注意堆栈的大小,以免发生硬件错误。代码如下:
double * FFT(double *pr,int n)
{
static double amp_data[FFT_max*2];//定义输出幅值
static double fr_data[FFT_max*2];//定义输出实部
static double fi_data[FFT_max*2];//定义输出虚部
static double pr_data[FFT_max];//缓存输入实部
static double pi_data[FFT_max];//缓存输入虚部
unsigned int k;//n=2^k;
int i,j;
int it,m,is,nv;
double p,q,s,vr,vi,poddr,poddi;
//===============初始化================================//
k = log10(n)/log10(2);//n=2^k;
for(i=0;i<n;i++)
{
pr_data[i] = *(pr+i);//读入缓存
pi_data[i] = 0;//读入缓存
}
//========将pr[0]和pi[0]循环赋值给fr_data[]和fi_data[]==========//
for (it=0; it<=n-1; it++)
{
m=it;
is=0;
for(i=0; i<=k-1; i++)
{
j=m/2;
is=2*is+(m-2*j);
m=j;
}
fr_data[it]=pr_data[is];
fi_data[it]=pi_data[is];
}
//==================================================//
pr_data[0]=1.0;
pi_data[0]=0.0;
p=6.283185306/(1.0*n);
pr_data[1]=cos(p); //将w=e^-j2pi_data/n用欧拉公式表示
pi_data[1]=-sin(p);
//================计算pr_data和pi_data========================//
for (i=2; i<=n-1; i++)
{
p=pr_data[i-1]*pr_data[1];
q=pi_data[i-1]*pi_data[1];
s=(pr_data[i-1]+pi_data[i-1])*(pr_data[1]+pi_data[1]);
pr_data[i]=p-q; pi_data[i]=s-p-q;
}
//===================计算fr_data和fi_data=====================//
for (it=0; it<=n-2; it=it+2)
{
vr=fr_data[it];
vi=fi_data[it];
fr_data[it]=vr+fr_data[it+1];
fi_data[it]=vi+fi_data[it+1];
fr_data[it+1]=vr-fr_data[it+1];
fi_data[it+1]=vi-fi_data[it+1];
}
//=================蝴蝶操作=========================//
m=n/2;
nv=2;
for (i=k-2; i>=0; i--)
{
m=m/2;
nv=2*nv;
for (it=0; it<=(m-1)*nv; it=it+nv)
{
for (j=0; j<=(nv/2)-1; j++)
{
p=pr_data[m*j]*fr_data[it+j+nv/2];
q=pi_data[m*j]*fi_data[it+j+nv/2];
s=pr_data[m*j]+pi_data[m*j];
s=s*(fr_data[it+j+nv/2]+fi_data[it+j+nv/2]);
poddr=p-q;
poddi=s-p-q;
fr_data[it+j+nv/2]=fr_data[it+j]-poddr;
fi_data[it+j+nv/2]=fi_data[it+j]-poddi;
fr_data[it+j]=fr_data[it+j]+poddr;
fi_data[it+j]=fi_data[it+j]+poddi;
}
}
}
//=================计算幅值输出==========================//
for (i=0; i<=n-1; i++)
{
amp_data[i]=sqrt(fr_data[i]*fr_data[i]+fi_data[i]*fi_data[i]); //幅值计算
}
//====================返回数据=========================//
return amp_data;//返回幅值
}
直接通过KEIL仿真可以观察出FFT的输出波形与MATLAB对比图如下图3,图4
方波的FFT测试如下图5所示:
3、显示屏选用与点阵驱动
笔者使用了6个8*8点阵组成的24*16点阵作为显示屏。每个8*8点阵使用MAX7219驱动。MAX7219之间级联保证占用的I/O口最少。其电路原理图如下图6所示
因为MAX7219这款IC输入电压范围在4-5.5V故一般需要5V供电,信号端也需要5-3.3V的转换。虽然大部分STM32的I/O口都是可以容忍5V输入与输出的。但笔者为了保险起见加了3路的3.3V-5V电路。其电路原理图如下。
4、STM32与音频输入电路
笔者将整个小玩具分成了两个块电路板,其中一块全是点阵和MAX7219一块是STM32、锂电池充放电电路、MPU6050外围电路和一路的音频采集电路,引出剩下部分引脚。音频采集电路主要使用咪头,经MAX4466做前置放大,放大后信号直接接入STM32内置ADC引脚。电路图如下图7所示
5、点阵屏时间显示
时间的计算采用STM32内置的RTC时钟即可,首先需要显示数字的字模,根据电路和字体大小(5*3),计算出数字0-9字模如下:
unsigned char disp1[11][3]={
{0x1F,0x11,0x1F},//0
{0x00,0x1F,0x08},//1
{0x1D,0x15,0x17},//2
{0x1F,0x15,0x15},//3
{0x1F,0x04,0x1C},//4
{0x17,0x15,0x1D},//5
{0x17,0x15,0x1F},//6
{0x18,0x17,0x10},//7
{0x1F,0x15,0x1F},//8
{0x1F,0x15,0x1D},//9
{0x00,0x0A,0x00} //:
};
根据电路结构和扫描顺序,写出底层显示驱动程序,程序如下:
//参数从左到右依次为:字体数据,字体长度,字体宽度,起始位置的x坐标(自右向左),起始位置y坐标(自下而上)
void MAX7219_N_display(unsigned char *cData,unsigned char Length,unsigned char Width,unsigned char x,unsigned char y)
{
static u16 iSbufWidth[24];//纵向显示缓存
u16 iSbuf;//用户数据缓存
u16 iSbuf_Middle;//缓存中间变量
unsigned int i;
for(i=0;i<Length;i++)
{
iSbuf = 0;//缓存清零
iSbuf |= *(cData+i)<<y;//只有有效位存在"1"的可能,非有效位全为0
iSbufWidth[i+x] |= iSbuf;//向显示缓存写入有效位的“1”
iSbuf = ~*(cData+i);//按位取反
iSbuf <<= y;//左移到显示位置
iSbuf = ~iSbuf;//相当于左移补1
iSbuf_Middle = 0xffff;//置位
iSbuf_Middle <<=(y+Width);
iSbuf |= iSbuf_Middle;//补全高位使得只有有效位存在"0"的可能,非有效位全为1
iSbufWidth[i+x] &= iSbuf;//向缓存写入有效位的“0”
}
for(i=0;i<8;i++)
{
Write_Max7219_N(i+1,iSbufWidth[i],iSbufWidth[i+8],iSbufWidth[i+16],iSbufWidth[i+16]>>8,iSbufWidth[i+8]>>8,iSbufWidth[i]>>8);//显示
}
}
6、总结
这个装置整体来说比较简单,因为有姿态传感器和一个点阵屏,所以开发的自由度也比较高。本文附资源,资源包含两个电路板的PCB源文件和开发的源代码。如有好玩的想法或者改进欢迎留言讨论。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)