☰
STM32F103C8T6+OLED12864实现音频频谱分析仪:从Proteus仿真到FFT显示
2026/9/28 19:45:32 网站建设 项目流程

做音频分析类的小项目,我一直觉得是嵌入式学习里“性价比”很高的一个方向:它把模拟采样、数字信号处理、外设驱动和人机交互串在了一条完整的链路上,做完一个,等于把MCU的大部分外设都过了一遍。这篇就以一个经典的组合——STM32F103C8T6配合OLED12864(SPI接口)实现音频信号分析仪为例,把Proteus仿真、软硬件设计、代码实现到问题排查的完整过程拆开讲清楚,适合正在学STM32、想做信号处理相关课程设计或者纯粹想玩频谱显示的读者参考。

1. 项目整体设计与核心思路拆解

1.1 音频信号分析仪的功能定位

这个项目本质上做的是三件事:采进来、算出来、显出来。音频信号从模拟通道进入STM32的ADC引脚,MCU按照设定好的采样率连续采集波形数据,攒够一批之后做FFT运算,把时域信号变成频域幅度谱,最后把频谱结果以柱状图的形式刷新到OLED12864屏幕上。

听起来不复杂,但“麻雀虽小,五脏俱全”。时域波形如果也做显示,还能顺带实现一个简易示波器;频域做FFT,又可以分析信号的能量分布。对于学习而言,这个项目非常适合作为“DSP入门+外设综合练习”的载体,因为它强迫你同时关注采样率、缓冲区大小、FFT点数、屏幕刷新率之间的匹配关系,而不是简单跑个流水灯。

1.2 方案选型背后的考量

为什么选STM32F103C8T6?关键在“够用且典型”。这是一颗Cortex-M3内核、主频72MHz、64KB Flash、20KB SRAM的芯片,价格便宜、资料海量、下载调试方式多,哪怕是国产替代型号,引脚和寄存器也完全兼容。对于256点FFT或者128点FFT这种规模的运算,M3跑起来毫无压力,主频和RAM空间都绰绰有余。

OLED选12864而不是1602,原因也简单:频谱显示需要图形化输出,1602只能显示字符,画不了柱状图。12864分辨率为128x64像素,横向可以画64根宽度2像素的柱子,或者32根宽度4像素的柱子,视觉上足够细腻。SPI接口相比I2C最大的优势是刷新速度快,同样是12864,I2C模式下全屏刷新会有明显闪烁,SPI下则基本无感。我实测过同一个工程改成I2C,帧率至少低一半,显示动态频谱时会感觉“拖泥带水”。

仿真选择Proteus则解决了一个现实痛点:不是所有人手边都有现成的音频信号源、开发板和杜邦线。Proteus里可以用虚拟信号发生器仿真麦克风输入、用示波器探针看ADC采样点,还能随时修改电路参数学会硬件连接逻辑。等思路跑通了,再落到实体板子上,可以大大减少debug时间。

2. 硬件链路与STM32F103C8T6核心资源盘点

2.1 芯片资源与音频项目的匹配度

STM32F103C8T6内部集成的资源在这个项目里会被用到以下部分:

  • ADC:2个12位逐次逼近型ADC,最多支持16个外部通道。音频检测只需要1个通道,随便分配到PA0~PA7都可以。12位分辨率意味着采样值范围0~4095,在3.3V参考电压下,理论分辨率约0.8mV,对音频信号来说足够细腻。
  • SPI:2个独立的SPI外设。OLED挂SPI1,主模式、8位数据、极性相位选Mode 0或Mode 3均可,只要和OLED屏的驱动IC匹配。
  • 定时器:基本定时器TIM6/TIM7,或者通用定时器TIM2/TIM3,用来精准控制ADC采样率。软件延时也行但误差大,做频谱分析必须用定时器触发采样,保证采样点间隔均匀。
  • DMA:必须用。ADC连续采样会产生大量数据,如果全靠CPU搬运,中断服务函数会被塞满,没时间做FFT和显示更新。DMA把采样值自动搬运到内存缓冲区,每采满一个批次通过中断通知CPU处理即可。
  • SRAM:20KB。考虑缓冲区、FFT运算的临时变量、显示缓存,完全装得下。我常用的分配方案是:ADC采样缓冲区256字节(半字)或512字节、FFT运算缓冲区约1KB、显示缓存1KB,总计不超过4KB。

2.2 关键引脚分配建议

引脚分配是这类项目的第一个坑,分配不好后面外设冲突会让你头皮发麻。我这个项目的分配表如下:

功能引脚说明
ADC输入PA1音频信号经过偏置电路后的模拟输入
SPI1_SCKPA5OLED时钟线
SPI1_MOSIPA7OLED数据线
OLED_CSPA4片选,拉低选中
OLED_DCPA2数据/命令选择
OLED_RSTPA3复位,低有效
OLED_BLKPA8背光控制,也可直接接3.3V

之所以避开PB口部分引脚,是考虑到后续可能扩展按键、编码器或串口调试,PB6/PB7(I2C1)、PB10/PB11(USART3)这些常用复用功能要留出来。OLED的DC和RST用普通GPIO控制,不走硬件SPI,方便时序上灵活调整。

2.3 音频信号输入链路的处理

麦克风或线路输入的音频信号,不能直接灌进ADC引脚。原因有两个:一是ADC只能采集正电压,而音频信号是双极性的(有正有负);二是信号幅度通常不够,直接采样分辨率太低。

常规做法是“隔直+偏置+放大”三件套:

  • 输入先经过一个耦合电容(典型值1uF~10uF),把直流分量滤掉,只保留交流音频成分;
  • 然后通过两个电阻分压,把信号中心电平抬到1.65V(即3.3V的一半),这样正负半周的信号都能在ADC量程内表示;
  • 如果信号幅度很小,加一个运放做同相放大,把峰峰值放大到1V~3V范围。这里推荐LM358或者LM324,便宜大碗,Proteus里也有模型。

偏置电路的计算其实很机械:假设分压电阻用两个10k,中间点电压就是1.65V;输入信号经过电容耦合后叠加在1.65V上,ADC读到的原始值就在512(1.65V/3.3V*4096)附近上下摆动。采样值减掉512这个直流偏置,就还原出了原始的交流信号。我在代码里就保留了这个偏置校准步骤,后文会细说。

3. ADC采样与预处理环节的实操拆解

3.1 采样率的确定与计算

音频信号分析仪,采样率往往是整个系统的“锚点”。采样率太低会丢失高频成分,太高则FFT频率分辨率变差。奈奎斯特采样定理告诉我们,采样率必须大于信号最高频率的2倍。人耳可听范围上限20kHz,理论上采样率要40kHz以上,但那是高保真音频设备的追求。对一块STM32F103C8T6 + 12864小屏的简化分析仪而言,实际有效显示带宽到5kHz就已经非常实用。

我做的是8kHz采样率 + 256点FFT的组合,综合性价比最高。原因如下:

  • 8kHz采样率,有效分析带宽4000Hz(奈奎斯特频率),覆盖语音和大部分乐音基频;
  • 256点FFT,频率分辨率为8000/256=31.25Hz,人耳对音高的分辨极限大约在这个量级,再细人眼也看不出区别;
  • 采样缓冲区只占256个半字=512字节,RAM占用小,DMA搬运也快。

如果你非要看20kHz的完整音频频谱,可以把采样率提到40kHz、FFT点数做到1024点,但代价是STM32F103C8T6的72MHz主频跑浮点FFT会明显吃力,而且屏幕只有128像素宽,高频段细节显示不出来,性价比很低。

3.2 定时器触发ADC+DMA的完整配置思路

采样的时序是这样的:定时器产生更新事件,触发ADC启动一次转换,转换完成后DMA把结果搬到内存。整个过程不需要CPU介入,直到DMA传输计数归零触发中断,才去处理这一批数据。

参数我这样设置:

// 定时器 TIM2, 72MHz 时钟 // 目标采样率 8000Hz // 分频 PSC = 72 - 1 = 71, 计数周期 ARR = 125 - 1 = 124 // 72MHz / 72 / 125 = 8000Hz TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_InitStructure.TIM_Prescaler = 71; // 72分频 TIM_InitStructure.TIM_Period = 124; // 计数值 125 TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); // 更新事件作为触发输出 TIM_Cmd(TIM2, ENABLE);

ADC侧配置成外部触发模式,触发源选择TIM2的TRGO,连续模式下DMA循环搬运:

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE);

DMA配置为循环模式,外设地址固定为ADC1->DR,内存地址指向采样数组,传输长度256个半字:

DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = FFT_SIZE; // 256 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; 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循环模式有个细节值得注意:它搬完256个点后会重新从头开始,但不会自动通知你“我搬完了一圈”。这时候需要用DMA的传输完成中断,或者用“空闲判断”的方式。更简单的做法是配一个定时器中断,比如每采集完成256个点所需的时间(256/8000=32ms)到了,就在定时器中断里做一次FFT。实际工程里我用的是DMA半传输和传输完成双中断,这样可以把FFT计算和数据处理重叠起来,刷屏更流畅。

3.3 采样数据的预处理流程

ADC拿到的原始值是12位的,范围0~4095,其中包含了直流偏置。做FFT之前,必须先对数据做“去直流”处理,否则FFT结果里第0个频率点(DC分量)会占掉巨大能量,把其他频谱分量都“压扁”。预处理代码很简单:

// 计算直流分量平均值 uint32_t dc_sum = 0; for (uint16_t i = 0; i < FFT_SIZE; i++) { dc_sum += adc_buf[i]; } int32_t dc_offset = dc_sum / FFT_SIZE; // 去除直流,并转换为有符号数 for (uint16_t i = 0; i < FFT_SIZE; i++) { fft_input[i] = (int16_t)(adc_buf[i] - dc_offset) * 16; }

乘16是因为后面做定点FFT时,输入动态范围越大,量化噪声相对越小。当然也不能无脑放大,得保证不溢出,int16_t的范围是-32768~32767,ADC去直流后最大幅度约2048,乘16后最大32768,正好在临界点,实际信号没那么满幅,是安全的。如果你的输入信号偏小导致频谱太矮,可以把这个倍率调成32甚至64,本质上是做一个数字增益。这块儿是我反复调过的地方,经验是:宁可数字增益放大后偶尔削顶(频谱高段稍失真),也不希望频谱显示得只有两三格高。

4. FFT频谱分析的工程化实现

4.1 快速傅里叶变换在MCU上的思路简化

FFT的数学原理这里不铺开讲,只讲工程上怎么落地。DFT的时间复杂度是O(N²),256点DFT要65536次复数乘法,STM32F103C8T6虽然能算,但时间是不可接受的;FFT利用旋转因子的对称性和周期性,把复杂度降到O(Nlog₂N),256点只要2048次复数乘法,在72MHz主频下大概几毫秒就能完成,完全实时。

工程实现有两条路:一是用ST官方CMSIS-DSP库里的arm_cfft_q15,二是自己写基2时间抽取FFT。CMSIS库的优点是优化到极致、稳定可靠,缺点是需要额外引入库文件,并且接口理解起来略绕。手写FFT的代码网上很多,但容易出错且效率不如库。我的建议是:赶进度用库,学原理自己写。这个项目里我用的是CMSIS库,节省了很多调试时间。

4.2 定点FFT与实信号处理的边界细节

CMSIS的arm_cfft_q15函数处理的是复数FFT,但ADC采进来的是实信号(没有虚部)。处理这个问题的标准办法:把实部放在数组偶数位,虚部置0,然后调用Q15版本的FFT。函数原型大概是:

arm_cfft_instance_q15 S; arm_cfft_init_q15(&S, FFT_SIZE); // 初始化 arm_cfft_q15(&S, fft_input, 0, 1); // 正变换 arm_cmplx_mag_q15(&fft_input[0], fft_mag, FFT_SIZE/2); // 求幅度

这里有个很隐蔽的坑:arm_cfft_q15内部要求输入数据定点化到Q15格式并做了缩放,输出结果也带了缩放因子;如果你把原始数据直接塞进去,算出来的幅度谱数值会跟理论值差一个倍数。这很正常,因为FFT会引入N这个倍率,加上CMSIS库为了保证不溢出又做了额外的移位。解决办法有两种:一是反推缩放系数,二是在显示时做“归一化”。

我的做法简单粗暴——显示前做一次动态归一化。先找出256个幅度值里的最大值,然后把每个值除以最大值、乘以64,得到0~63之间的柱高。这样无论输入信号幅度怎么变,显示出来的频谱图总是铺满半个屏幕的,效果很直观。当然代价是失去了绝对幅度的参考意义,但作为“分析仪”是够用的,如果非要绝对刻度,可以在代码里加一个已知幅度的校准信号,测出增益系数后补偿回去。

4.3 加窗处理——一个容易被忽略的细节

做FFT时如果直接截取一段信号,相当于给时域信号加了一个矩形窗,频谱会泄漏。什么叫泄漏?一个纯净的1kHz正弦波,做完FFT后本来应该在1kHz处出现一根直线,但矩形窗会让能量“漏”到旁边的频率点,出现“裙边”。这在显示上表现为谱线变宽、旁边出现多余的小峰,视觉上很脏。

解决方法是加窗。常见窗函数:汉宁窗(Hanning)、海明窗(Hamming)、布莱克曼窗(Blackman)。汉宁窗最常用,旁瓣衰减适中,适合音频频谱显示。加窗就是在FFT之前,把每个时域采样点乘上一个预先算好的窗系数:

// 汉宁窗系数生成 for (uint16_t i = 0; i < FFT_SIZE; i++) { float w = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); window[i] = (int16_t)(w * 32767); } // 应用汉宁窗 for (uint16_t i = 0; i < FFT_SIZE; i++) { fft_input[i] = ((int32_t)fft_input[i] * window[i]) >> 15; }

代价是主瓣略微变宽、幅度略有下降,但换来的是频谱干净得多。我建议这个项目一定要加窗,不加窗的结果在Proteus仿真里看正弦信号还好,一旦用语音或方波信号测试,频谱整个底部都是“毛刺”,根本没法看。

5. OLED12864 SPI驱动的完整实现

5.1 SPI时序与OLED控制芯片的对应

OLED12864这类屏,市面上默认用的驱动芯片主要是SSD1306(128x64)或SH1106(128x64)。两者指令集基本兼容,但SH1106的显存寻址略特殊。购买屏的时候最好跟卖家确认型号,项目里按SSD1306驱动写,大多数情况能通吃。

SPI接口的OLED通常是4线制:SCK、MOSI(数据输入,称为SDA或DIN)、CS、DC。注意这个SPI没有MISO,因为OLED不需要回传数据,是典型的单工写操作。所以STM32这边只启用MOSI即可,PB4等引脚省下来干别的。

时序上,SSD1306的SPI时钟频率可以到10MHz,但STM32F103C8T6的SPI1最高只能到18MHz(72MHz/4),实际跑1~4MHz就稳稳的了。写命令和写数据由DC引脚区分:DC低电平写命令,高电平写数据。CS片选低有效,每次传输前拉低、结束拉高,也可以一直拉低不切换(只有一块屏时)。

5.2 初始化序列——背下来不如理解它

SSD1306的初始化序列看起来是一大串神秘数字,其实拆开就几件事:

  1. 关闭显示;
  2. 设置晶振频率和内部升压;
  3. 设置MUX比例(多路复用比,决定行数);
  4. 设置显示偏移;
  5. 设置起始行;
  6. 设置段重映射和COM扫描方向(决定图像是否镜像/倒置);
  7. 设置显存寻址模式;
  8. 设置对比度;
  9. 清屏;
  10. 开启显示。

这串代码不同厂家的例程大同小异,直接用就行。但要注意一个细节:SSD1306上电后内部有硬复位,如果你把RST引脚悬空,也能工作;但为了保险和可控复位,代码里最好做一次软件复位:拉低RST延时10ms再拉高,然后延时100ms再初始化。这个习惯能避免很多“上电花屏”的灵异问题。

5.3 显示缓存与频谱柱状图的绘制

SSD1306的显存是1KB(128x64/8),分布在8个页(Page0~Page7),每页对应8行像素。写入时要先设置页地址和列地址,然后连续写入每列的一字节(低位在上,即每个bit对应一行的像素)。这个位序问题很容易搞反,拿到新屏可以先画个“全亮帧”测一测,确认方向没问题。如果显示左右镜像,调整初始化里的段重映射指令;上下颠倒则调整COM扫描方向。

频谱柱状图的绘制思路:

  • 把256个FFT幅度值压缩映射到128个屏幕列。要么取每两列的最大值,要么取平均值。我建议用最大值,因为频谱显示更尖锐,看起来“有精神”。
  • 把每列的幅度值(0~63)作为柱高,先计算当前列的柱顶页地址和掩码。
  • 更新显存时,并不是直接把整列数据覆盖写进去,而是先把该列上所有像素点亮(也就是高度范围内显示),超出高度的像素熄灭。

简单实现可以定义一个大数组displayBuf[128][8](1KB),先清空,然后逐列把高度范围内的像素位置1,最后整体刷新到OLED。一种更节省时间、效果也不错的优化是“差异刷新”:只把和上一帧不一样的部分更新。因为频谱刷新时相邻帧大多只是部分变化,差异刷新可以把有效传输量降为全刷的30%以下,帧率提升很明显。我后面实测全刷大约25帧/s,差异刷新能到30~40帧/s。

6. Proteus仿真搭建与联调过程

6.1 元件选型与电路连接

Proteus 8.x以上版本自带STM32F103C8T6模型,不用装第三方库,直接用即可。搭建步骤:

  1. 从元件库搜索STM32F103C8T6,放置芯片;
  2. 搜索OLED12864或SSD1306,版本不同元件名称可能有差异,老版本可能需要用LM016L代替做字符显示,但既然用SPI OLED,建议用较新版本的Proteus,可以直接找到SSD1306模型;
  3. 放置一个虚拟信号发生器(Signal Generator)或直接使用VSINE电压源模拟音频信号;
  4. 放置运放和电阻电容搭建输入偏置电路。

连接关系牢记:信号发生器输出-> 耦合电容 -> 偏置电阻网络 -> STM32 PA1。OLED的SCK、MOSI、CS、DC、RST分别连PA5、PA7、PA4、PA2、PA3。所有芯片共地、OLED供电接3.3V。注意Proteus里STM32的VDDA/VSSA必须接好3.3V和GND,否则程序跑不起来。

6.2 虚拟信号源的设置技巧

Proteus里的信号发生器可以产生正弦波、方波、三角波等,频率可以设置为连续扫频。仿真音频分析仪时,我最常用的配置:

  • 正弦波模式,频率先设1kHz,峰值幅度设1.5V(叠加1.65V偏置后ADC输入范围0.15V~3.15V,不会削顶);
  • 做扫频实验时,用信号发生器的“扫频”功能,从20Hz扫到8kHz,观察频谱柱状图的峰值是否跟随移动,这一步能快速验证FFT频率标定是否正确;
  • 方波模式下,理论上频谱应该出现基频的奇数倍谐波,即如果基频500Hz,则1500Hz、2500Hz...处都应有谱线。这是测试系统动态范围的好方法,做出来了说明加窗和FFT流程基本没问题。

6.3 仿真联调中的双轨验证

Proteus仿真最大的价值是可以同时观察多个点位的波形,这是实体板子不容易做到的。把虚拟示波器探针接在:

  • 信号发生器输出端:看原始模拟信号;
  • PA1引脚:看ADC输入端的偏置效果,这里应该看到叠加在1.65V上的音频信号;
  • PA5(SCK):看SPI时钟是否产生,频率是否正确;
  • PA7(MOSI):看数据是否在持续传输,如果OLED不显示,先查这两个信号有没有动静,可以快速判断是“没跑程序”还是“SPI没配置对”。

仿真里还有一个实用的调试手段:在代码里设置断点(Proteus支持VSM Studio调试模式,或者外部IDE将HEX加载进芯片后看结果),直接查看fft_mag数组的值。如果数组里的值全部是0或者极大,那一定是预处理或FFT调用出了问题,而不是显示的问题,可以极大缩小排查范围。

注意,Proteus仿真LL库没问题但速度比真实芯片慢不少,FFT计算和屏幕刷新都会被拖慢。我实测同样代码,仿真里屏幕刷新大约只有真实板子的1/4~1/5帧率,所以如果仿真里觉得刷新不够流畅,不一定是代码问题,不要纠结在仿真上过度优化,先确认逻辑正确,再上真机测帧率。

7. 常见问题与排查技巧实录

7.1 问题速查表

现象可能原因排查方法
OLED完全不亮,无背光供电未接3.3V、复位引脚被拉低、GPIO配置为复用且错了引脚查电源、RST电平、用示波器看SCK/MOSI是否有活动
OLED亮但全屏乱码或雪花SPI时序极性/相位不对、初始化序列错误、显存方向反了试Mode 0和Mode 3切换;把初始化序列对照SSD1306手册逐条核对
屏幕显示但频谱只有一横线ADC采不到信号、输入偏置不对、采样率太低信号被截断在PA1用探针看电压波形,确认信号幅度和偏置;检查DMA是否真正搬运了数据
频谱峰值不随信号频率移动FFT频率标定错误、采样率和定时器配置不一致确认定时器实际频率,用已知1kHz正弦波检验峰值位置是否约在32(1k/31.25)处
频谱底部毛刺多没有加窗、输入信号谐波太大、DMA传输不连续导致采样间隔抖动先加汉宁窗,再检查DMA循环模式和中断处理是否及时
屏幕刷新很慢,有明显闪烁SPI速率太低、全屏刷新频繁、延时函数拖后腿提高SPI分频设置,改用差异刷新,减少delay_ms的使用

7.2 几个值得单独说一说的坑

第一个大坑是DMA缓冲区的对齐问题。STM32F1系列DMA搬运16位数据时,要求内存地址按半字对齐,这个通常是满足的,但如果你定义了数组后取地址时不小心被编译器优化了,可能出现诡异的数据错位现象。稳妥的做法是在数组定义上用__attribute__((aligned(4)))修饰,或者干脆用uint16_t数组并让编译器自行对齐。

第二个大坑是CMSIS-DSP库的三角近似。旧版本arm_cfft_init_q15依赖浮点计算旋转因子,在无FPU的F103上会模拟浮点,虽然不慢但会引入小幅误差。这个误差体现在频谱上就是靠近Nyquist频率处会有一点点平台噪声,不影响正常显示,但如果强迫症发作,可以换成查表法预生成旋转因子,效果会更干净。

第三个大坑是“拿方波当正弦测试”。有的人为了省事把信号发生器设为方波,直接观察频谱,发现谐波一堆就以为FFT出错。其实方波的频谱本身就是奇次谐波无穷多,这是正确的物理现象。我曾见过有学生在BBS上发帖“求助FFT方向错误”,底下回复都是“你换成正弦再试试”,这个经典笑话充分说明测试信号选择有多重要。

7.3 实体板移植时的几个差异点

如果你看完仿真准备去搭实板,有几个地方和Proteus里不一样,必须调整:

  • 真实OLED模块通常是3.3V供电,如果板子上的稳压输出电流不够,接上屏之后背光一亮会导致ADC参考电压漂移,采出来的频谱底噪变大。解决方法是单独给OLED供电或用质量好一点的LDO;
  • 音频输入的耦合电容选择要特别注意,Proteus里用10uF没问题,实体板如果选了电解电容要注意极性方向,反接会在小信号时严重失真;
  • 真实麦克风模块输出的是单端信号但可能自带偏置,接进ADC前最好用示波器确认一下静态电压。有些麦克风模块输出已经偏置到VCC/2,再经你板子上的偏置电路就会顶到电源轨,削顶失真后频谱里全是偶次谐波。

8. 扩展方向与个人使用的体感

这个项目做完,如果意犹未尽,可以考虑几个方向。

把OLED数据用串口发到上位机,用Python画实时频谱图,相当于把12864升级成一个高分辨率显示器,这样256点FFT的全部细节都能展示出来。我在做第二版的时候就是这么干的,串口波特率115200,每帧发520字节左右,Python端用matplotlib或者pyqtgraph的更新模式,能达到15帧左右的实时绘图,分析效果比OLED强太多。OLED就负责做个便携版的“随身频谱仪”,两不耽误。

也可以把FFT点数扩展为512点,这样频率分辨率翻倍到15.625Hz,显示效果会有明显提升。代价是RAM开销翻倍到4KB左右(两个缓冲区加临时变量),F103C8T6的20KB SRAM虽然够用,但如果同时开显示缓存和串口缓冲,就要精打细算一下。

如果想让分析仪适应更大动态范围的信号,可以加一个模拟AGC电路,用运放加可变增益,或者在数字部分做自动增益控制(AGC)。数字AGC的思路是在处理完一帧数据后算RMS值,如果过小就增加下一帧的数字增益系数,如果削顶就减小。这是音频仪表类项目经常需要用到的技巧,我从频谱仪的AGC一路调过来,写进代码里不过十行左右,但实际体验提升是“质的飞跃”,尤其是拿它对着手机喇叭放音乐的时候,动态不会忽大忽小。

我自己在做这个项目的过程中,最大的感受是:这个项目对新手最大的门槛不在FFT算法的数学,而在“打通链路”的系统思维。采样率怎么定、缓冲区多大、FFT和刷新率怎么配合,这些都环环相扣。你在网上能找到很多只讲其中一环的教程,但很少能把ADC、DMA、FFT、SPI、OLED串起来讲透。这篇算是补上这个空缺。后续如果你在做的时候遇到什么奇怪问题,多试试“信号发生器换波形、采样率降一半、分步看显示”这三个思路,大部分故障都能定位出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询