
简介STM32F103 與 OLED12864 結合的音樂頻譜項目基於 FFT 算法實時分析音頻信號並以多種可視化樣式呈現適合嵌入式愛好者、電子競賽學生及想深入理解數字信號處理的開發者。資源包共 244 個文件包含 C 源碼與頭文件、Keil 工程配置、編譯中間文件、原理圖 PDF/圖片以及硬件初始化配置文件壓縮後 9.41MB結構清晰便於查閱。已有 15063 人學習下載具備一定參考價值。從中可掌握 STM32F103 外設初始化、OLED 驅動、FFT 頻譜計算與實時顯示的完整流程描述中提到支持多種顯示效果可在此基礎上修改算法與界面用於音樂可視化或音頻分析類項目。原理圖的提供也降低了硬件連接的門檻尤其適合需要快速復現或二次開發的學習者。 把STM32F103C8T6、一块0.96寸OLED12864SSD1306驱动、加一个几块钱的驻极体麦克风模块就能搭出一台实时FFT音乐频谱显示器。很多人一听“FFT”就觉得得F407或者ESP32才跑得动我实测下来72MHz的Cortex-M3在CMSIS-DSP库的Q15定点版本加持下做256点FFT单次运算只要几毫秒而8kHz采样率下一帧256点光是采样就要32毫秒CPU其实有大把空闲时间。这篇文章把整套方案摊开讲模拟前端怎么搭、定时器触发ADCDMA怎么采、CMSIS-DSP的Q15格式有哪些坑、OLED12864上几种能看的花样效果是怎么画出来的以及我调了一个晚上才定位的“第一根柱永远拉满”问题。它特别适合刚学完GPIO和定时器基础例程、想找个项目把ADC、DMA、中断和显示串起来练手的朋友。1. 为什么老掉牙的F103还适合做实时频谱1.1 算力账256点FFT没你想的那么可怕我最早也被“FFT”三个字母唬住以为必须上F4、带FPU、上CMSIS-DSP的浮点库。后来老老实实算了笔账256点基2 FFT需要的复数蝶形运算次数是N/2 * log2(N)也就是128 * 8 1024次。每次蝶形主要是一次复数乘法和两次复数加法在Cortex-M3上就算用标准C语言实现也不过几万条指令。72MHz主频下纯定点实现大概只需要几百微秒到一两毫秒。如果直接用ARM官方的CMSIS-DSP库它针对M3做了Q15/Q31指令级优化256点FFT实测大概1毫秒左右。这个负载对STM32F103来说完全可以忽略。真正耗时间的反而是OLED刷新SSD1306是1KB显存SPI跑8MHz时刷满一屏大约1毫秒也完全能接受。所以瓶颈从来不在“算不完”而在你采进来的是不是干净、连续、带正确时序的数据。只要采样这关做好了F103做实时频谱绰绰有余。1.2 为什么是OLED12864而不是TFT或点阵屏TFT彩屏当然能做得更华丽但ST7735这类屏在F103上驱动稍重SPI刷新率不够的话还容易掉帧。LCD1602/2004做不了图形只能显示字符。传统12864比如ST7920刷新太慢而且并行模式占引脚。OLED12864几乎是这个需求的甜点SSD1306/SSD1315驱动、SPI接口、内部自带1KB显存、每一帧输出128×64像素。做音乐频谱横轴能画64到128根柱状条纵轴64像素也足够表达幅度动态范围。它对引脚占用很少——SPI只要四根线加上DC和RES一共六个GPIO。F103C8T6那片板子上的资源比这个项目需要的多得多。1.3 与F407、ESP32项目对比F407的优势是带FPU可以直接上浮点FFT采样率可以拉到更高瀑布图、灰度效果更轻松。ESP32则库更丰富有人直接I2S接数字麦克风。但这些方案的共同痛点是开发链路更重ESP32的环境配置、F4的板子成本都比F103C8T6要高。我的观点是如果目标是真正搞懂FFT频谱的完整链路而不是看最花哨的成品F103是性价比最高的学习载体。做完这套移植到F4或ESP32无非是换库、改采样方式频谱显示逻辑几乎是通用的。2. 信号进ADC之前麦克风、偏置和最小系统2.1 三种音频输入方案直接建议第二种音频输入接什么决定了后面整个信号链的难易。我试过三种直接用驻极体麦克风模块比如MAX4466模块、MAX9814模块自带放大和偏置输出端已经垫好了直流电平接ADC就能采。这是最省事的方案推荐新手直接买这种成品模块。用麦克风加电阻偏置驻极体麦克风需要一个偏置电压才能工作典型做法是接一个2.2kΩ到10kΩ的上拉电阻到3.3V信号再经过一个1uF到10uF的隔直电容输出。这个方案便宜但输出幅度小大约只有几十到几百毫伏直接进ADC分辨率不够。线路输入手机/电脑耳机孔用3.5mm插座引音频信号不适合直接进ADC因为线路信号幅度可以达到1Vrms以上容易削顶而且没有直流偏置。我最后选了MAX9814模块。它的好处是内置了AGC自动增益控制输入信号大时自动压下来、小时自动抬上去省去了调放大器增益的麻烦。它的输出在无信号时大约稳定在1.65V附近正好是3.3V ADC量程的一半。如果你用普通的MAX4466输出偏置也是大概VCC/2同样没问题。2.2 偏置问题ADC只能采0到3.3VSTM32F103的ADC输入范围是0到VDDA这里VDDA就是3.3V。而麦克风信号是交流的有正有负不能直接怼进PA0。所以必须在信号上叠加一个1.65V左右的直流分量让波形的“零点”抬到量程中间。成品模块已经做好了这件事。如果你自己焊驻极体电路最常见的做法是驻极体麦克风正极接3.3V通过一个上拉电阻到地负极输出交流信号。输出串联电容隔直再用两个10kΩ电阻分压把直流电平拉到1.65V。让信号通过一个由轨到轨运放组成的跟随器或同相放大器再接ADC输入。模拟前端的核心原则是输入ADC的信号最大峰值不要超过3.3V最小不要低于0V否则ADC会削波失真。我调试时习惯先用万用表量静态偏置再用示波器看在最大音量下信号有没有触顶。2.3 原理图核心模块与OLED接线我画这块板子最核心的几个部分你在立创EDA或AD里也能快速复刻最小系统8MHz晶振加两个22pF负载电容NRST接10kΩ上拉和100nF对地电容BOOT0接10kΩ下拉BOOT1直接接地VDDA/VSSA之间的LC滤波不要省VBAT接3.3V。SWD下载口SWDIO接PA13、SWCLK接PA14再加3.3V和GND四针就能用ST-Link下载调试。麦克风模块VCC接3.3VGND接地OUT接PA0ADC1的通道0。OLED接口用SPI模式我推荐接法是OLED引脚STM32F103C8T6引脚说明GNDGND地VCC3.3V电源SCKPA5SPI1_SCKSDA/MOSIPA7SPI1_MOSIRESPB1复位控制低有效DCPB0数据高/命令低选择CSGND直接拉低不再单独控制这里有个小坑要提醒如果OLED模块标注SDA在SPI模式下它其实是MOSI不是I2C的SDA别和I2C接线搞混。另外PA4虽然不是必须引脚但它是SPI1的NSS也是ADC1的输入通道4如果系统里想多留一个ADC输入就别把PA4用作CS直接接地更省事。电源部分我用了AMS1117-3.3把USB的5V降到3.3V输入输出各加一个10uF和100nF电容。实测下来如果供电比较吵频谱底部会出现一条一条的低频“毛”后面我排查那个问题时就发现电源纹波是元凶之一。3. 让数据“按时按量”进内存定时器ADCDMA3.1 采样率选择与定时器参数计算采样率决定了FFT能看到的最高频率。根据奈奎斯特采样定理8kHz采样率能无混叠地还原0到4kHz的信号这个带宽用来显示人声和大部分乐器旋律已经足够OLED横轴也就128像素显示不了太宽的频谱。所以我把采样率定在8kHz。采样率怎么产生用TIM2的更新事件触发ADC启动。STM32F103的ADC可以通过定时器外部触发启动这样采样间隔就由硬件严格保证不会因为中断处理延迟而抖动。要得到8kHz触发频率我用的参数是72MHz主频下PSC 71ARR 124这样定时器计数时钟是1MHz更新频率就是1MHz / 125 8kHz。也就是每隔125微秒触发一次ADC转换。ADS124这个组合实测非常稳采样点间隔基本不会抖动。3.2 DMA和双缓冲别让采样和处理互相干扰ADC用单通道扫描每次转换结果通过DMA自动搬运到内存数组。DMA通道我用的是ADC1的DMA请求挂在DMA1的通道1上。一个很关键的细节是缓冲区的组织方式。我需要256点时域数据而CMSIS-DSP的Q15 FFT要求输入是复数数组也就是实部、虚部交替排列长度是2 * 256 512个short。所以我的做法是定义一个长度为512的int16_t fftBuf[512]。DMA把ADC结果16位搬运到fftBuf的偶数下标位置也就是fftBuf[0], fftBuf[2], fftBuf[4]...。fftBuf的奇数位置在每次FFT前清零表示虚部为0。如果不做双缓冲还有一个常见问题DMA正在往缓冲区写第256个点时CPU可能已经在处理上一帧FFT了两者会打架。更好的做法是DMA“半传输中断”和“传输完成中断”交替起来采样了128点时半传输中断CPU先把前128点复制去处理采样满256点时传输完成中断CPU再处理后128点。这样数据不会被覆盖处理时间也分散了不会出现一毫秒内要干完所有活的情况。实测下来我用一个更简单的方案也够用volatile uint8_t fftReady 0; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); fftReady 1; // 主循环检测到这个标志再处理FFT // 同时重新启动下一次DMA继续采集下一帧 } }主循环里只要检测到fftReady就把fftBuf丢给FFT函数处理完再等下一帧。这样CPU空闲的时候很多后续加瀑布图、峰值保持等效果也不担心算力不够。3.3 CMSIS-DSP的Q15 FFT调用与加窗用CMSIS-DSP库做Q15 FFT代码很短#include arm_math.h arm_cfft_instance_q15 fftInst; int16_t fftBuf[512]; // 256点复数缓冲 int16_t fftMag[256]; // 各频率成分的幅值 void spectrum_init(void) { arm_cfft_init_q15(fftInst, 256); } void spectrum_process(int16_t *buf) { // 1. 去直流把每个采样点减去平均值或减去一个固定的直流偏置 // 2. 加窗逐点乘以汉宁窗系数 // 3. 执行FFT arm_cfft_q15(fftInst, buf, 0, 1); // 4. 计算幅度 arm_cmplx_mag_q15(buf, fftMag, 256); }这里有几个坑要重点说坑一Q15格式的输入范围。Q15是定点数表示范围是-1.0 ~ 0.999916位整数-32768 ~ 32767对应定点小数。ADC原始值是0到4095中心是2048幅度大约只有正负2048。直接塞进Q15 FFT动态范围会小很多。要先把ADC值减掉2048再左移放大比如((int16_t)(adcVal - 2048)) 4这样满量程信号可以被放大到接近32767充分利用Q15的精度。但放大倍数太大在强信号时会溢出保险起见可以做一个简单的削波判断。坑二去直流。麦克风模块输出的1.65V偏置在ADC里就是2048左右。如果不去掉这个直流分量FFT的0号bin会有一个巨大的峰显示出来就是最左边一根柱子永远拉满。这个问题太典型了我单开一整节讲后面会细说。坑三要加窗。实际采集的音频是连续流里截出来的一帧如果不加窗矩形截断会造成频谱泄漏表现为每个真实谱峰旁边出现一条“裙边”分不清是真实信号还是泄漏。最简单有效的是汉宁窗每个采样点乘以0.5减去0.5倍的余弦值。可以预先算好256个窗系数存到ROM里每次FFT前乘一下。加窗后柱状图会变得干净很多代价是频域分辨率略微下降但对128像素宽的屏幕来说完全看不出来。4. OLED12864上的频谱艺术几种显示效果怎么画4.1 SSD1306驱动的刷新本质SSD1306内部显存是64行×128列逻辑上被分成8个“页”每页对应8行像素。操作方式是先通过DC引脚设置命令或数据模式发送页地址和列地址然后连续写入每页的显示数据。我画柱状图时最直接的办法是维护一个uint8_t frameBuf[8][128]每帧先把整个缓冲区清零再向对应位置填充柱状图形最后一次性刷到屏幕。如果每次只更新有变化的列局部刷新也能降低SPI开销但我实测刷全屏1KB的数据在8MHz SPI下只要约1ms全局刷新反而省心也不容易出残影。4.2 频点怎么映射到128个像素FFT拿到256个幅值点其中有用的只有前128个对应0到4kHz。128个点如果直接画成128根柱每根柱宽1像素不仅难看高频区还会到处是细碎的毛刺。我实际的做法是从前128个幅值点里取前42个每根柱宽3像素2像素柱体加1像素空隙正好铺满128列。如果取前42个bin对应的频率范围是0到1312.5Hz你会损失1.3kHz以上的信息。想看到更高频可以改成每隔几个bin合并成一柱或者做非线性映射低频区每1个bin画一柱高频区每4个bin合并成一柱。我后来实现了一个简单的对数映射版本视觉上更接近人耳对频率的感知。4.3 五种可以直接抄的显示效果这套方案我陆陆续续实现了五种效果切换只需要改一个标志位普通柱状图每根柱高度对应幅值取对数压缩后画在屏幕底部向上。这是最基础也最直观的效果。幅值到像素高度的映射建议用pixels (int)(log10(1 mag) * scale)对数压缩后低音和高音的动态范围都能看清线性映射往往只剩低音几个柱在跳。峰值保持每个柱状图顶部保留一条横线或一个亮像素点表示这个频率在过去几百毫秒内的最大幅值横线以固定速度慢慢下降。实现时维护一个peak[42]数组当前幅值比峰值大就更新否则就按每帧减一的速度衰减。对称镜像把一个柱状图在屏幕中线分开左边向右画、右边向左画形成一个向两侧扩散的“对撑”效果播放电子音乐时很有感觉。实现上就是复制同一份数据改变每一列的起点坐标。瀑布图类似于声呐屏幕把最新一帧的频谱放在屏幕最底部或最顶部旧数据逐行向上移动。这个效果非常适合连续观察一段时间内频谱的演化但刷屏数据量稍大需要把整个frameBuf搬到下一行才能写入新行实测在F103上还是能稳住30帧以上。点阵虚线柱在普通柱状图基础上柱体上部改用隔一个点才点亮的方式制造出“频谱在逐渐消散”的层次感。单色OLED上这种点阵模式看起来很高级比纯矩形更有细节。实现时对柱体高度做分段底部是实心顶部是虚线。这些效果的共同原则是先更新内存中的frameBuf再一次性刷屏。不要在绘制期间直接往SSD1306的显存里写否则容易出现上半屏和下半屏数据来自不同帧的撕裂感。5. 一次真实排查第一根柱为什么永远拉满5.1 现象复现与怀疑方向代码写完上电OLED确实动了但最左边第一根柱0Hz那一根在没有任何音乐播放时也几乎满格而且随着播放音乐第一根柱纹丝不动完全不受音量影响。一开始我以为是显示函数写错了把屏幕最左列当成了某种“状态指示条”。我先打印FFT原始输出看到fftMag[0]的值稳定在13000左右而其他bin只有几十到几百。这个现象非常典型fftMag[0]对应的是0Hz也就是直流分量。麦克风模块输出的静态偏置大约1.6VADC采集到2048左右这个直流分量在256点FFT里会全部累积到0号bin幅度就是2048乘以一个放大系数自然远大于交流信号。5.2 用数据说话从打印ADC值到看FFT输出我通过SWD串口把原始ADC值和去直流后的值都打印出来。原始ADC的中位数是2047说明偏置没问题真正的问题是我在FFT之前没有减偏置。于是我加上去直流每帧采样完先计算256个点的平均值然后让每个点减去平均值。这下fftMag[0]降下来了但还没有完全正常——在安静环境下它仍然有几百的底噪屏幕上第一根柱依然高出其他柱一截。接着我观察去直流后的时域波形发现它并不是一条干净的直线而是有一个大约20到30Hz、峰峰值约几十LSB的低频起伏。这个频率正好和电源的参考地噪声、麦克风供电上的纹波有关。麦克风模块对电源纹波太敏感而我的板子USB供电又没有做很好的滤波。5.3 最终根因和修复定位到以后我做了两个修复缺一不可硬件上给麦克风模块的VCC单独加了一节LC滤波一个100Ω电阻串联接3.3V后面并一个100uF电解电容把供电纹波压下去。如果模块输入端能接受5V我甚至建议从5V那边LC滤波后再给模块避免和数字部分共地噪声互相干扰。软件上把去直流从固定减2048改成了实时均值去直流同时在FFT之前套了汉宁窗。汉宁窗虽然不能消除低频噪声但能把频谱泄漏的“裙边”摊薄让低频段的底噪整体下降。修完以后安静环境下第一根柱只有两三个像素高播放音乐时能量分布也正常了。回头总结遇到频谱最左侧异常大的情况排查顺序就是打印原始ADC均值看偏置有没有问题再打印FFT前几个bin看直流泄漏量最后检查时域波形有没有低频噪声。这个思路对所有ADCFFT项目都通用。另外还有一个小坑值得提醒OLED如果和麦克风模块的电源走线挨得太近刷新屏幕时候SPI信号的开关噪声会耦合进模拟前端频谱上会看到一条固定频率的“尖峰”和音乐内容无关。现在PCB上模拟地和数字地我都是单点汇接屏幕排线也尽量远离麦克风输入线。6. 移植到其他平台与FFT的进阶玩法6.1 从F103搬家到F4或ESP32这套代码的框架完全可以平移。搬到STM32F407上最明显的改动是CMSIS-DSP可以直接改用浮点版本arm_cfft_f32(fftInst_f32, fftInput_f32, 0, 1); arm_cmplx_mag_f32(fftInput_f32, fftMag_f32, N);F4有FPU浮点FFT跑得飞快采样率可以拉到16kHz甚至更高屏幕显示上限频率翻倍。移植时只需把ADC的采样时序、DMA通道号、时钟树配置改掉显示的frameBuf逻辑可以原封不动复制。如果是ESP32可以进一步用I2S接口接数字麦克风比如INMP441省掉模拟偏置和放大电路采样数据直接是PCM格式FFT照样用CMSIS-DSPESP-IDF里带了你甚至可以加一个WiFi服务端把实时频谱推送到浏览器显示。但代价是ESP32的ADC线性度差一些模拟输入做音频精度不如F103自带的12位ADC。6.2 FFT这套框架不只是玩具频谱显示只是FFT最直观的应用。你把这套采样和FFT框架换一个传感器改几个参数就能做不少正经场合的事直流采样FFT把ADC不经过隔直电容直接采电池或电源轨FFT就能看到开关电源的纹波频谱分析噪声来自哪个频段。预测性维护与包络谱分析用加速度传感器替代麦克风采集机械设备振动信号包络谱FFT能诊断轴承故障的特征频率。这类工业项目里FFT的核心流程和我这个音乐频谱完全一样——采样、加窗、FFT、取幅值、画图。测量相位CMSIS-DSP的FFT输出本身就是复数实部和虚部可以反推出每个频率分量的相位。用这个可以做两路信号的相位差测量比如电机的相序检测、超声波飞行时间估算。所以这个项目真正的价值不在于那块OLED上跳动的彩色柱子而是帮你把“从模拟信号到频域信息”的完整链路打通了。以后再遇到任何“想看看某段信号里有什么”的需求你都会下意识地先想到调好采样率DMA进缓冲加窗做FFT然后把人眼能看懂的结果画出来。最后分享一个我自己的小习惯在调试FFT这类项目时永远先把时域波形和FFT结果通过串口打印出来而不是直接看屏幕效果。屏幕上只能看到“不对”打印出来的数据才能告诉你“哪里不对”。把这一步做扎实了屏幕上的效果只是时间问题。本文还有配套的精品资源点击获取