基于STM32F103C8T6与FFT的音频频谱分析仪设计与Proteus仿真
发布时间:2026/9/3 3:53:49 作者:尧图编辑部 阅读量:1,286

简介本资源是一套面向单片机课程设计与嵌入式初学者的完整音频信号分析仪实践方案基于STM32F103C8T6主控实现0–5kHz频谱分析功能融合Proteus硬件仿真、SPI驱动OLED12864显示及FFT算法软件实现有效解决嵌入式音频处理中信号采集、频域变换与可视化呈现的学习难点。压缩包共含多个核心模块Proteus仿真工程含STM32最小系统、音频输入模拟电路与OLED接口、Keil工程源码C语言编写涵盖SPI初始化、ADC采样、浮点FFT计算、频谱映射与OLED动态刷新逻辑以及.wav格式测试音频文件用于功能验证整体大小为91.7MB。目前已有1308人学习下载资源结构清晰、注释详尽配套参考资料涵盖STM32外设配置、OLED SPI通信时序、FFT原理与定点/浮点实现差异等关键知识点特别适合课程设计快速上手与原理深化理解。1. 项目缘起为什么用STM32F103C8T6做音频分析仪几年前我在一个智能家居项目里需要实时监测环境噪音当时第一反应是找现成的音频分析模块结果要么太贵要么功能太单一。后来琢磨着能不能用手里最便宜的“蓝色药丸”——STM32F103C8T6最小系统板自己搭一个这玩意儿核心是Cortex-M372MHz主频带12位ADC价格就十来块钱做音频的时域分析和简单的频域处理完全够用。最关键的是它的资源对于学习嵌入式信号处理来说是一个绝佳的平衡点既有足够的性能去跑FFT快速傅里叶变换又不至于复杂到让初学者望而却步。这个“基于STM32F103C8T6的音频信号分析仪”项目核心目标就是实现一个能采集音频信号、进行频谱分析并将结果直观显示在OLED12864屏幕上的便携式设备。它不追求专业声卡级的精度而是聚焦于原理的实践与验证。通过Proteus仿真你可以在没有硬件的情况下完整地走通从信号采集、算法处理到显示输出的全链路这对于理解嵌入式系统开发、模数转换、数字信号处理以及SPI通信协议是一个综合性极强的练手项目。你会发现网上关于STM32和FFT的教程很多但能把ADC采集、FFT运算、OLED驱动和Proteus仿真这几个环节串起来形成一个完整可交互案例的并不多。这个项目正好填补了这个空白尤其适合那些已经点过灯、调过串口想向更实际的信号处理应用迈进一步的嵌入式爱好者。接下来我会拆解整个系统的设计思路、关键代码的实现以及如何在Proteus里搭建和调试这个虚拟的音频分析仪。2. 系统架构设计与核心器件选型做一个音频分析仪听起来复杂但拆解开来就是几个标准模块的串联。我们的核心思路是麦克风或音频输入接口将声音信号转为模拟电压 - STM32的ADC对这个电压进行采样变成数字序列 - CPU对这段数字序列进行FFT运算得到各频率分量的强度 - 最后通过SPI接口将频谱图“画”到OLED屏幕上显示。2.1 主控芯片STM32F103C8T6的能耐与边界为什么是STM32F103C8T6除了便宜和普及我们得算算账看它到底能不能扛起音频FFT的活儿。首先看ADC。STM32F103C8T6的12位ADC在理论上能提供4096个量化等级。对于音频分析我们更关心它的采样率。在72MHz系统时钟下配合ADC预分频实现最高1MHz的采样率是没问题的。根据奈奎斯特采样定理这意味它能无失真地分析最高500kHz的信号。但这是理论极限实际用于语音和音乐分析我们通常只关心20Hz到20kHz这个范围。为了获得较好的频谱分辨率我们通常将采样率设置为40kHz左右2倍于20kHz这个速率对STM32F103C8T6的ADC来说游刃有余。其次是计算能力这是关键。FFT尤其是库函数中常用的基2-FFT对计算量有要求。一个N点的FFT运算复杂度大约是N*log2(N)次复数乘法。如果我们做256点的FFT就需要大约 256 * 8 2048 次复数乘法。STM32F103C8T6没有硬件浮点单元FPU所有浮点运算都得靠软件模拟这会很慢。因此我们必须使用定点数运算的FFT库比如ARM官方提供的CMSIS-DSP库中的定点FFT函数它针对Cortex-M系列内核做了高度优化利用SIMD指令能大幅提升速度。在72MHz主频下完成一个256点的定点FFT所需时间通常在几毫秒以内完全能满足实时性的要求比如每秒更新几十次频谱。最后是内存。STM32F103C8T6有20KB的RAM。做256点FFT需要存储原始采样数据数组256个16位整数、FFT运算过程中的复数数组256个复数每个复数包含实部和虚部以及计算后的幅值数组。粗略估算这大约需要256*2 256*2*2 256*2 ≈ 2KB的内存假设都用16位整型或Q格式定点数远远小于20KB所以内存空间是充足的。注意如果你计划做512点或1024点的FFT以获得更高的频率分辨率就需要仔细核算内存和计算时间了。1024点FFT对RAM的需求和计算耗时都会成倍增长可能会逼近这颗芯片的极限导致刷新率显著下降。对于入门和大多数演示场景256点FFT是性价比最高的选择。2.2 显示模块为什么是OLED12864 SPI接口显示结果部分我们选择了0.96英寸的OLED12864模块并且特意强调是SPI接口版本这背后有充分的理由。首先SPI vs IIC。OLED模块常见的通信接口有IIC和SPI两种。IIC只需要两根线SDA, SCL节省IO口但速度慢。SPI需要四根线SCK, MOSI, DC, CS有时还包括RST占用IO多但速度极快。对于频谱显示这种需要频繁刷新、绘制大量点或柱状图的场景SPI的速度优势是决定性的。它能确保屏幕刷新流畅没有明显的拖影或卡顿。其次硬件SPI vs 软件模拟SPI。STM32F103C8T6有硬件SPI外设SPI1和SPI2。我们强烈建议使用硬件SPI。硬件SPI由DMA控制器或CPU直接操作硬件寄存器进行数据搬运效率极高不占用CPU进行位翻转的耗时操作。在程序中我们只需要配置好SPI的时钟极性、相位、数据位顺序然后往数据寄存器里填数据即可剩下的硬件会自动完成。这能最大程度地将CPU解放出来去处理更重要的ADC采样和FFT计算任务。最后关于**“6针SPI”**。标准的四线SPI是SCK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、CS片选。但对于OLED这种纯接收设备MISO线通常不需要所以常见的OLED SPI模块是4针或5针多一个复位RST。所谓的“6针”可能是在此基础上增加了DC数据/命令线和电源线VCC, GND。在驱动时我们需要通过GPIO控制DC线的高低电平来告诉OLED接下来发送的是命令如设置显示区域还是数据实际的像素点数据。2.3 仿真环境Proteus的角色与局限Proteus在这个项目里扮演着“数字孪生”的角色。它允许我们在电脑上完全模拟STM32芯片的运行、外围电路如ADC输入信号源以及OLED屏幕的显示输出这对于前期算法验证、调试显示逻辑来说效率极高能避免反复烧录芯片。但是必须清醒认识到Proteus仿真的局限性性能仿真不精确Proteus无法精确模拟STM32在72MHz下的真实时序和计算性能。在仿真里跑FFT可能感觉很快但在真实硬件上可能会慢一些。仿真主要用于验证逻辑正确性。ADC信号源在Proteus中我们可以用“信号发生器”元件模拟一个正弦波或音频信号直接连接到STM32的ADC输入引脚。这比用麦克风电路简单直接非常适合测试FFT算法——你可以输入一个1kHz的正弦波然后在频谱上观察是否在对应位置出现一个尖峰。OLED模型Proteus里有常见的OLED12864仿真模型。你需要正确连接SPI线并确保你的驱动程序发送的初始化命令序列和像素数据格式与仿真模型兼容。有时仿真模型和实际屏的细微差异会导致初始化失败这时需要对照模型的数据手册调整代码。3. 核心代码实现与驱动解析有了架构我们来看代码怎么落地。整个工程可以分成几个相对独立的模块系统时钟与外设初始化、ADC采样与DMA、FFT处理、OLED驱动与图形显示。3.1 硬件SPI驱动OLED12864首先解决显示问题。使用STM32CubeMX初始化硬件SPI1假设连接在PA5-SCK, PA7-MOSI并初始化两个GPIOPA4作为CS片选PA6作为DC数据/命令控制线。RST复位线如果需要也用一个GPIO控制。驱动OLED的核心是向它发送命令和数据。发送前需要拉低CS选中设备。然后根据要发送的是命令还是数据设置DC线为低或高最后通过SPI发送一个字节。// 示例通过硬件SPI发送一个字节 void OLED_WR_Byte(uint8_t data, uint8_t cmd) { if(cmd) OLED_DC_Set(); // DC1 写数据 else OLED_DC_Clr(); // DC0 写命令 OLED_CS_Clr(); // 片选使能 HAL_SPI_Transmit(hspi1, data, 1, 100); // 使用HAL库发送 OLED_CS_Set(); // 片选禁止 }初始化序列需要严格按照OLED控制器通常是SSD1306的数据手册来包括设置对比度、显示起始行、扫描方向、开启显示等。网上有大量现成的驱动代码但要注意区分是给IIC用的还是SPI用的以及屏幕分辨率是128x64还是128x32。绘制频谱柱状图时我们通常将屏幕的Y轴从上到下对应幅值从大到小。计算得到每个频率点或频率区间的幅值后将其映射到屏幕的某个高度然后调用画线或画矩形函数从屏幕底部向上绘制相应高度的竖线。3.2 ADC采样与DMA传输配置为了不丢失音频数据ADC采样必须连续进行。如果采用查询或中断方式CPU会被频繁打断影响FFT计算。最佳实践是使用ADCDMA的模式。配置流程如下ADC配置选择ADC1设置一个规则通道例如通道0对应PA0引脚。设置采样时间为较长的周期如239.5个周期以提高转换精度。设置连续转换模式。DMA配置为ADC1配置一个DMA流例如DMA1_Channel1。设置方向为从外设ADC到内存内存地址自增数据宽度为半字16位对应ADC的12位结果。触发与启动设置ADC由软件触发然后启动DMA最后启动ADC。一旦启动ADC就会以设定的速率连续转换并将结果通过DMA自动搬运到你指定的内存数组比如uint16_t adc_buffer[FFT_LENGTH]中。双缓冲技巧这是实现流畅分析的关键。我们准备两个同样大小的缓冲区BufferA和BufferB。DMA当前正在填充BufferA。当BufferA被填满时DMA会触发一个“传输完成”中断。在这个中断服务函数里我们做两件事第一立刻将DMA的目标地址切换到BufferB让ADC继续采样填充BufferB而不间断第二设置一个标志位告诉主循环“BufferA已经准备好了可以拿去进行FFT计算了”。主循环检测到这个标志位就对BufferA的数据进行处理。这样就实现了采样和处理的并行避免了数据丢失。// DMA传输完成中断服务函数示例 void DMA1_Channel1_IRQHandler(void) { if (__HAL_DMA_GET_FLAG(hdma_adc1, DMA_FLAG_TC1)) { __HAL_DMA_CLEAR_FLAG(hdma_adc1, DMA_FLAG_TC1); // 清除中断标志 // 切换DMA目标缓冲区 if (current_buffer bufferA[0]) { HAL_ADC_Stop_DMA(hadc1); // 这里可以设置 bufferA_ready 标志 current_buffer bufferB[0]; } else { HAL_ADC_Stop_DMA(hadc1); // 这里可以设置 bufferB_ready 标志 current_buffer bufferA[0]; } HAL_ADC_Start_DMA(hadc1, (uint32_t*)current_buffer, FFT_LENGTH); } }3.3 定点FFT计算与频谱提取如前所述我们使用CMSIS-DSP库的定点FFT函数。首先需要将库文件添加到工程并包含头文件#include arm_math.h。第一步数据预处理。ADC采样的结果是0-4095之间的无符号整数。FFT函数通常要求输入是q15_t或q31_t格式的定点数Q15表示小数点在第15位。我们需要将ADC值转换为有符号的定点数并通常需要减去直流分量平均值以防止直流分量在频谱0Hz处产生一个巨大的峰值掩盖其他低频信号。#define FFT_LENGTH 256 q15_t fft_input[FFT_LENGTH*2]; // FFT函数要求输入是复数实部虚部交错存储 q15_t fft_output[FFT_LENGTH]; arm_rfft_instance_q15 S; // FFT实例结构体 // 在准备好ADC数据后 uint32_t sum 0; for(int i0; iFFT_LENGTH; i) sum adc_buffer[i]; uint16_t dc_offset sum / FFT_LENGTH; // 计算直流分量 for(int i0; iFFT_LENGTH; i) { // 减去直流偏移并缩放到Q15格式的范围-1 ~ 0.9999 // 假设ADC参考电压3.3V满量程对应Q15的0x7FFF (32767) int32_t temp (int32_t)(adc_buffer[i] - dc_offset) * 32767 / 4096; if(temp 32767) temp 32767; if(temp -32768) temp -32768; fft_input[i*2] (q15_t)temp; // 实部 fft_input[i*21] 0; // 虚部初始为0 }第二步执行FFT。arm_rfft_init_q15(S, FFT_LENGTH, 0, 1); // 初始化0表示正向FFT1表示位反转 arm_rfft_q15(S, fft_input, fft_output); // 执行FFT结果在fft_output中第三步计算幅值谱。FFT输出的结果是复数我们需要计算每个复数的模幅度来得到频谱。对于实数序列的FFT其输出具有共轭对称性通常我们只取前FFT_LENGTH/2个点对应从0到采样率一半的频率范围。q15_t magnitude[FFT_LENGTH/2]; for(int i0; iFFT_LENGTH/2; i) { q15_t real fft_output[i*2]; q15_t imag fft_output[i*21]; // 使用CMSIS-DSP库的平方和开方函数或使用近似公式提高速度 // 近似公式magnitude ≈ |real| |imag| 或者 max(|real|, |imag|) min(|real|, |imag|)/2 magnitude[i] abs(real) abs(imag); // 一种快速的幅值近似 }现在magnitude数组就包含了从0Hz到Fs/2Fs是采样率频率范围内的信号强度分布。每个数组元素对应的频率是i * Fs / FFT_LENGTH。4. Proteus仿真搭建与调试技巧在Proteus中搭建这个仿真项目是验证逻辑的绝佳方式。4.1 原理图绘制要点MCU模型在元件库中搜索“STM32F103C8”将其放置到图纸中。电源与地虽然仿真可能简化但最好还是给芯片的VDD/VSS引脚接上电源VCC/VDD接 3.3VVSS接地。ADC输入信号从左侧工具条选择“Generator Mode”然后选“SINE”正弦波发生器。将其一端接地另一端连接到STM32的某个ADC引脚例如PA0。双击信号发生器设置幅值如2V和频率如1kHz。幅值不要超过3.3V以免模拟输入超限。OLED模块搜索“OLED12864”或“SSD1306”找到SPI接口的模型。将其SCL、SDA、DC、RST、CS等引脚与STM32对应的GPIO连接好。注意供电引脚VCC, GND也要接好。时钟与复位STM32需要外部晶振吗在Proteus仿真中通常可以依赖芯片内部RC振荡器HSI所以外部晶振电路可以省略。复位电路也可以简化直接用一个上拉电阻到VCC或者使用Proteus提供的“DEFAULT”复位信号。4.2 程序加载与仿真运行编译代码在Keil MDK或STM32CubeIDE中编译你的工程生成.hex或.elf文件。加载程序在Proteus中双击STM32芯片在弹出的属性窗口中找到“Program File”一栏点击文件夹图标选择你编译好的.hex文件。在“Clock Frequency”一栏填入你的系统时钟频率比如72MHz。开始仿真点击Proteus左下角的运行按钮。如果一切正常你应该能看到OLED屏幕上出现初始化画面随后开始显示动态的频谱柱状图。调试信号你可以右键点击ADC输入线选择“Place Wire Label”给它起个名字如AUDIO_IN然后打开“Debug”菜单下的“Digital Oscilloscope”数字示波器。在示波器中添加你的信号标签就能看到输入的模拟波形。同时OLED屏幕会显示处理后的频谱。你可以动态调整信号发生器的频率观察频谱图上的峰值是否随之移动这是验证FFT算法是否正确的最直观方法。4.3 常见仿真问题排查OLED不显示这是最常见的问题。首先检查SPI线连接是否正确特别是DC和CS线。其次检查你的OLED初始化代码序列是否与Proteus中的仿真模型兼容。有些仿真模型可能需要特定的初始化命令顺序。尝试在网上搜索针对Proteus SSD1306的驱动代码。ADC采样值不对在程序中可以通过串口打印出ADC的原始采样值。在Proteus中你可以添加一个“Virtual Terminal”虚拟终端连接到STM32的串口TX引脚并将程序中的调试打印信息输出到这里查看采样值是否随输入信号变化。仿真运行极慢Proteus仿真速度取决于电脑性能和仿真复杂度。确保你没有在代码里使用大量的printf到虚拟终端这会使仿真慢如蜗牛。尽量关闭不必要的调试输出。5. 从仿真到实物的关键调整与优化仿真成功了恭喜你但把程序烧录到真实的“蓝色药丸”开发板和OLED屏上很可能还会遇到一两个坎。这里分享几个关键的调整点。5.1 时钟配置的差异在Proteus中我们可能直接使用了内部HSI时钟8MHz甚至默认设置。但在真实硬件上为了达到72MHz的主频以获得最佳性能我们通常需要配置外部高速晶振HSE和PLL锁相环。务必使用STM32CubeMX正确配置时钟树并确保生成的SystemClock_Config()函数被正确调用。如果时钟配置不对不仅系统速度异常SPI、ADC、定时器的时序也会全部错乱。5.2 ADC参考电压与信号调理仿真中的信号发生器是理想的。现实中麦克风或音频线输出的信号电压范围可能不符合STM32 ADC的0-3.3V要求。偏置电压音频信号是交流信号有正有负。而STM32的ADC只能测量0-Vref通常Vref接3.3V的正电压。因此需要一个运放电路将音频信号“抬升”到1.65VVref/2的直流偏置上使其在1.65V上下波动。最简单的办法是使用一个电压跟随器电路结合电阻分压提供一个1.65V的虚地。抗混叠滤波ADC采样前必须加一个低通滤波器抗混叠滤波器滤除高于采样频率一半Fs/2的高频成分防止混叠失真。一个简单的RC低通滤波器截止频率略高于你关心的最高音频频率如25kHz就很有必要。参考电压稳定性确保给STM32的VDDA和VSSA模拟电源和地供电干净、稳定。如果条件允许可以将Vref引脚单独接一个稳定的基准电压源而不是直接连到3.3V这能提高ADC的精度。5.3 FFT点数与采样率的权衡在实物上你需要通过定时器来精确控制ADC的采样率。例如使用一个定时器触发ADC的规则组转换并将触发频率设置为40kHz。采样率Fs决定了你能分析的最高频率Fs/2。对于音频40kHz足够。FFT点数N决定了频率分辨率Fs/N。Fs40kHzN256那么分辨率是40000/256 ≈ 156Hz。这意味着频谱上相邻两个点代表的频率相差156Hz。这对于区分低音鼓~100Hz和男声基频~200Hz是勉强的。如果你想看得更细比如分辨50Hz和60Hz的工频干扰就需要提高点数N到512或1024或者降低采样率Fs。但提高N会增加计算量和内存消耗降低Fs会降低可分析的最高频率。你需要根据实际需求权衡。5.4 显示优化与实时性在实物上SPI的时钟速度可以提得很高。STM32F103的SPI在72MHz系统时钟下分频后达到18Mbps甚至更高是很容易的这能极大加快OLED的刷屏速度。 绘制频谱时为了避免屏幕闪烁可以采用局部刷新策略。即只更新频谱柱状图变化的部分而不是每次都清空全屏再重画所有内容。这需要更精细的图形函数控制。 实时性方面需要在主循环中合理安排时间。一个典型的循环可能是检查DMA缓冲区就绪标志 - 执行FFT和幅值计算 - 更新OLED显示 - 可能还有其他的任务如按键扫描。使用示波器或者翻转GPIO引脚的方式测量一下完成一次FFT和显示的时间确保这个时间小于你采集一帧数据的时间例如256点40kHz采样率是6.4ms否则就会掉帧。最后这个项目最大的乐趣在于“看得见”的声音。当你对着麦克风说话、播放音乐屏幕上实时跳动的频谱柱状图就是对数字信号处理原理最生动的诠释。从仿真到实物从代码到波形每一步遇到的问题和解决过程都会让你对嵌入式系统、信号处理和硬件交互有更深的理解。本文还有配套的精品资源点击获取