资讯动态

STM32F103+OLED12864+FFT音乐频谱:低成本音频可视化的完整实现

发布时间:2026/10/16 19:09:33 来源:尧图企业网站定制
简介面向嵌入式开发者和电子爱好者这份STM32F103OLED12864音乐频谱资料包以FFT频谱分析为核心集成条形图、波形图等多种显示效果并附硬件原理图方便复现完整的音频可视化项目。压缩包共244个文件、约9.41MB核心是STM32 HAL库源码36个.c与65个.h及Keil工程文件另有编译中间文件、PDF原理图、DOCX说明文档、CubeMX配置和实物图片结构清晰便于对照学习。目前已有15089人学习热度可观。资料最大价值在于从ADC采集、FFT运算到OLED刷新的完整代码链配合原理图可快速定位硬件连接适合想深入理解数字信号处理与嵌入式交互的中级开发者参考。1. stm32f103 OLED12864 FFT 音乐频谱小成本也能把声音画成动态画面第一次看到 stm32f103OLED12864FFT 音乐频谱的视频时我第一反应是网上又加了特效一颗 72MHz 的 M3 要采样音频、算傅里叶、再刷新 I2C 的 OLED听着就一定会卡。真把最小系统焊出来才发现128 点定点 FFT 在 F103 上只占几毫秒真正让画面糊掉、频谱乱跳的九成是麦克风放大电路和 OLED 刷新策略。这个标题背后是一条完整的信号链拾音、偏置放大、ADC 采样、加窗 FFT、频率映射、多效果渲染。适合做桌面音频可视化、低成本频谱分析演示也适合拿来做嵌入式信号处理入门。配套的“原理图”拿来不是直接抄的先看三处输入偏置、OLED 接口、电源去耦看懂了再动手焊不然出了问题很容易怀疑 FFT 代码实际锅全在硬件。2. 为什么用 FFT 做频谱以及 128 像素的映射逻辑2.1 时域波形不是不能画而是画不出音色直接让 ADC 采样结果上下跳也能得到一个“会动的画面”但它表达的是声音幅度随时间的变化不是频率分布。你放一段鼓点和一段小提琴时域波形可能都是“噼里啪啦”看不出区别只有把信号转到频域才能看到低频能量在哪、中频能量在哪、高频泛音有多少。音乐本质上是多个频率分量的混合人耳感知的是这些分量在频谱上的分布。单片机做 FFT 就是把一段 ADC 时域数据变成频域数据再按频率从低到高排列映射到 OLED 的列上。所以整个项目里 FFT 不是最难的最难的是“采样数据本身是不是干净、是不是足够均匀”否则 FFT 算出来的只是一个错误的频域结果。为什么选 FFT 而不是直接算 DFT128 点 DFT 要 16384 次复数乘虽然 F103 硬算也不是不能跑但每帧都要算再叠加 OLED 刷新就会吃紧。FFT 把复杂度降到 N log N 量级1000 多次蝶形运算就能完成嵌入式上常用而且成熟。直接用现成定点和库是最稳妥的路径。2.2 定点 FFT、浮点 FFT 在 F103 上的实际取舍F103 是 Cortex-M3 内核72MHz没有硬件浮点单元。如果用float做 FFT所有浮点运算都会变成软浮点库函数调用速度掉一个量级还占很多 Flash。我一般直接用 Q15 定点格式ADC 的 12 位采样值先移到 Q15 范围再交给定点 FFT 库最后把频谱幅度取出来做显示。FFT 点数决定频率分辨率和一帧采集时间。常见搭配是采样率 8kHz、点数 128这样频率分辨率是 8000/128 62.5Hz一帧采集时间是 16ms刚好适合做 20fps 以上的实时显示。如果再想保留更低频的细节可以把点数加到 256但采集时间变成 32ms帧率会下降。OLED 只有 128 列点数 128 的 64 个有效频点已经够用了再多点数只增加低端分辨率视觉上差别不大。采样率 FsFFT 点数频率分辨率一帧采集时长频率上限适用场景8000 Hz12862.5 Hz16 ms4 kHz语音、律动型音乐频谱8000 Hz25631.25 Hz32 ms4 kHz想要低频细节但帧率降低16000 Hz25662.5 Hz16 ms8 kHz高频信息更多M3 压力增大16000 Hz128125 Hz8 ms8 kHz帧率高但低频分辨率差拿 128 点、8k 采样率来说最低几个频点分别落在 0Hz、62.5Hz、125Hz、187.5Hz 附近。对音乐显示是够的如果做精密测量这个分辨率不够看就得把采样率降低或点数加大。对 F103 这种定位视觉体验优先不要追求精度而牺牲帧率。2.3 频点映射到 128 列先算 bin再算列FFT 输出 N 个点前 N/2 个是有效正频率最后一个有效点是 Nyquist 频率 Fs/2。128 点 FFT 有 64 个有效频点正好对应 OLED 的 128 列所以最常见的映射是每个频点占两列左边列和右边列取同一个幅度。/* 128 列 OLED 对应 64 个 FFT 有效频点 */ static uint8_t col_to_bin[128]; for (int col 0; col 128; col) { col_to_bin[col] (uint8_t)(col 1); /* 0,0,1,1,2,2 ... */ } /* 使用时第 col 列的显示高度来自 col_to_bin[col] 这个频点 */ uint16_t mag spectrum[col_to_bin[col]];这段代码的关键是把“屏幕的列坐标”和“FFT 的频点下标”解耦。这样后续想改成对数频率映射或者只显示中间一段频带只需要改这张表不需要动 FFT 和显示主体逻辑。有个容易踩的坑FFT 结果第 0 个点对应直流分量。ADC 输入如果有 1.65V 偏置FFT 后直流分量会非常大表现为屏幕最左边一根柱子永远满格。所以在显示前直接把第 0 点高度置 0或者加一个高通滤波不要让直流进显示。另外如果用的 OLED 模块是 128 列而你想在左右两边留边距可以最后把高度数组整体平移不是把 FFT bin 平移。3. 原理图上最值得先读的三个部分输入调理、OLED 接口、电源3.1 麦克风放大与直流偏置决定频谱是跟着音乐跳还是永远贴地拿到原理图先找麦克风输入这一级。驻极体麦克风需要上拉偏置电阻常见的做法是给 MIC 一个 2.2k 到 10k 的偏置经过耦合电容送到运放同相端。运放输出必须偏置在 ADC 满量程的一半也就是 1.65V这样声音信号才能在 0V 到 3.3V 之间摆动只有正电压、没有负电源。单电源运放的偏置常用两个 10k 电阻分压得到 1.65V再串一个小电容和 100k 电阻进入同相端避免偏置被前级影响。放大倍数不要盲目调大。常见增益在 20 到 50 倍之间也就是 R2/R1 1 在这个范围。如果增益太大说话声稍大一点 ADC 就削顶频谱上会出现一堆虚假高频谐波增益太小FFT 幅度贴地柱子跳不起来。看原理图时重点确认三件事麦克风有没有正确偏置、运放供电是不是单 3.3V 且输出偏置在 1.65V、ADC 输入引脚有没有超过 3.3V 的风险。很多“频谱只在低频跳”的问题不是 FFT 算错而是输入信号本身低频成分过大或偏置不对。3.2 OLED12864 的 I2C、SPI、并口选择对帧率影响很大OLED12864 模块常见主控是 SSD1306 或 SH1106接口有 I2C、SPI、并口。标题里只说 OLED12864没限定接口所以原理图上这部分要仔细认。如果是 I2C 接口只有四根线接线最简单但传输一帧 1KB 图像要十几毫秒如果是 SPI 接口数据线多两根但刷新速度快非常多。接口类型需要引脚一帧 128×64 图像传输估计结论I2C 400kHzSCL、SDA、RST、DC 较少约 20ms 级可玩但刷新率受限SPI 18MHzSCK、MOSI、CS、DC、RST约 1ms 级推荐多效果不闪烁8080 并口8 根数据 控制线最快但占用引脚多效果最流畅布线复杂我建议新做硬件直接选 SPI 接口的 OLED除非你手里只有 I2C 模块。I2C 做静态 UI 没问题但做音乐频谱这类动态画面每帧全屏刷新会明显吃力。如果你非要 I2C就要用“整帧缓存 只更新变化列”的办法否则屏幕闪得没法看。原理图里如果已经有 OLED 插座的原理图看它是否把 DC、CS、RST 接到了可用的 GPIO。很多现成模块把 CS 和 DC 固定接死只能某些接口工作。先看模块背面的电阻配置再对照原理图连线否则焊上去要么白屏要么花屏。3.3 电源和地ADC 的噪声会直接变成频谱底噪F103 的 ADC 是 12 位但模拟和数字共用一个 3.3V 时数字翻转耦合进 VREF 的噪声会到几个 mV换算成频谱就是底噪抬高。原理图里常见的做法是给 ADC 的供电单独走一段线串磁珠或小电感再接 10uF 和 100nF 电容去耦。别把这部分当摆设。OLED 背光和 F103 的 IO 翻转是板上最大的数字噪声源。如果 OLED 和运放共用一根长走线的 3.3V每次刷屏时 ADC 采集都会叠上毛刺。我一般会让 OLED 供电走独立线或者至少让电源从 USB 或稳压输出端先经过 10uF 电容再分给模拟和数字两条支路模拟支路串个 10 到 100 欧电阻做 RC 隔离。地线比电源线更容易翻车。两层板做不到完整模拟地分割的时候不要大面积把模拟地和数字地切开反而在 ADC 的 VSSA 附近单点连接。原理图上如果标了 AGND 和 GND先确认它们有没有在某个点汇合别让所有信号都绕远路回流。4. 软件实现稳定采样、定点 FFT、多种显示效果切换4.1 用定时器触发 ADC而不是 delay 采样采样间隔必须稳定。delay ADC_Read这种方式受中断和 OLED 刷新影响时间抖动很大FFT 出来的谱线会变宽、底噪变高。正确做法是让定时器周期产生触发事件ADC 被硬件触发后自动转换再用 DMA 把结果搬到内存里CPU 只在缓冲区满了以后处理数据。/* 定时器触发 ADC目标采样率 8kHzF103 定时器时钟按 72MHz 算 */ TIM_TimeBaseInitTypeDef tim_base; tim_base.TIM_Prescaler 71; /* 72MHz / (711) 1MHz */ tim_base.TIM_Period 124; /* 1MHz / (1241) 8kHz */ tim_base.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim_base); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); /* TRGO 用于触发 ADC */这里Prescaler71和Period124是常见组合计算逻辑是 72MHz 先被分频到 1MHz再数 125 个周期产生一次更新事件最后就是 8000Hz。如果你想改成 16kHz可以把Period改成 61得到约 16.129kHz误差不大。ADC 配置成外部触发模式触发源选 TIM2 TRGO然后开 DMA 循环传输到缓冲区。注意 DMA 只搬连续的一批样本不要在主循环里等单个样本。批量处理时用一个双缓冲DMA 往 buf A 写主循环处理 buf B处理完再交换指针。这样采样和 FFT 不会互相等画面更稳。4.2 加窗、去直流、转 Q15FFT 输入不是 ADC 裸数据ADC 直接读到的 12 位数据带着直流偏置如果不减去 2048FFT 第一根柱子会顶满。另外128 点 FFT 只截取了一小段时域信号等效于给信号加了矩形窗频谱会泄漏。处理方式是用汉宁窗让帧头帧尾幅度平滑衰减。#define FFT_N 128 #define ADC_MID 2048 __IO uint16_t adc_buf[FFT_N]; q15_t fft_buf[2 * FFT_N]; /* 实部虚部交替存放 */ q15_t hanning_q15[FFT_N]; /* 预计算好的汉宁窗系数 */ /* 从 ADC 缓冲区构造 FFT 输入 */ void prepare_fft_input(void) { for (uint16_t i 0; i FFT_N; i) { q15_t x (q15_t)(((int16_t)adc_buf[i] - ADC_MID) 4); fft_buf[2 * i] (q15_t)(((int32_t)x * hanning_q15[i]) 15); fft_buf[2 * i 1] 0; } /* 调用定点 FFT 库以下为 CMSIS-DSP 风格示意 */ arm_cfft_q15(fft_inst, fft_buf, 0, 1); arm_cmplx_mag_q15(fft_buf, spectrum_q15, FFT_N); }这段代码有三个关键参数ADC_MID是 12 位 ADC 的中间值4是把 12 位采样范围搬进 Q15 满幅避免输入幅度太小汉宁窗系数需要提前算好运行时直接查表不要在每帧里现场算三角函数。arm_cfft_q15和arm_cmplx_mag_q15是 CMSIS-DSP 的常用函数名不同库版本可能叫法不同实际使用时按你工程里的头文件为准。我一般不用float输出直接把 Q15 幅度值用于显示。显示时做一次比例换算把 32767 映射到 OLED 最大高度。如果发现频谱整体偏低不要盲目放大显示系数先检查输入幅度是不是太小否则会把底噪也放大。4.3 多种显示效果柱状、镜像、峰值保持、抖动灰度OLED 是单色屏显示效果靠“刷新策略”撑起来。最简单的是柱状图每列高度来自对应频点高度越高点亮越多的点。可以做一个draw_mode状态机按键切换显示效果。uint8_t draw_mode 0; uint8_t peak[128]; void update_spectrum_display(void) { switch (draw_mode) { case 0: /* 普通柱状图 */ for (uint8_t col 0; col 128; col) { uint8_t h mag_to_height(spectrum_q15[col 1]); draw_vertical_bar(col, h); } break; case 1: /* 镜像频谱 */ for (uint8_t col 0; col 128; col) { uint8_t h mag_to_height(spectrum_q15[col 1]); draw_mirror_bar(col, h); /* 上下对称 */ } break; case 2: /* 柱状图 峰值保持 */ for (uint8_t col 0; col 128; col) { uint8_t h mag_to_height(spectrum_q15[col 1]); if (h peak[col]) peak[col] h; else if (peak[col] 0) peak[col]--; draw_vertical_bar(col, h); draw_pixel(col, OLED_H - 1 - peak[col]); /* 顶部一个亮点 */ } break; default: break; } }这段代码里mag_to_height把 Q15 幅度换算成像素高度peak[]是每列峰值保持数组。峰值保持线的关键不是上升快而是下降慢。下降速度选每帧减 1 像素大概 20 帧降 20 像素看起来有“余晖”感太慢会让画面拖沓。镜像效果是把同一份频谱画到上下两侧视觉冲击力强但高度需要限制在 OLED 一半以下否则上下会重叠。抖动灰度则是用 2x2 点阵模拟不同亮度适合做渐变效果但不要做全屏 4 级灰度因为 OLED 像素切换需要额外时间效果过度会变成闪烁。5. OLED FFT 频谱常见问题与排查5.1 I2C 扫描不到 OLED上拉、地址、供电一个都不能少现象是屏幕白屏程序初始化 OLED 不报错也没有显示。用逻辑分析仪看就知道 SCL 上有规律波形但 SDA 始终没有响应。原因多半是 OLED 模块的 I2C 地址不对或者模块默认地址是 0x3D 而你代码写的是 0x3C。还有不少模块是 5V 供电但电平不转换IO 通信不稳定。解决方法是先只写一个 I2C 扫描函数在总线上遍历 0x3A 到 0x3F把响应的地址通过串口打印出来。同时确认 SCL、SDA 上有没有 4.7k 左右上拉电阻。如果模块自带上拉F103 的 I2C 外设通常还能用如果是 GPIO 模拟 I2C上拉电阻更不能少。别一上来就调 FFT先把屏点亮。5.2 频谱只有最左边两根柱子跳右边全部不动现象是放音乐时屏幕左边两格会动其他区域基本死寂。原因通常有两个采样率被设得非常低导致整个音乐频谱都挤在前几个频点或者 ADC 直流分量没去干净FFT 结果被直流和极低频主导。解决方法是先用已知信号验证。让手机播放 1kHz 正弦波如果 8kHz 采样、128 点 FFT应该在第 16 个频点附近看到明显峰值也就是大约第 32、33 列。如果算出来峰值在第 1、2 列说明定时器分频配错实际采样率远低于 8kHz。检查PSC和ARR是不是按定时器时钟 72MHz 算的如果 APB1 分频配置不同定时器时钟可能是 36MHz公式差一倍。5.3 OLED 刷新时整个屏幕闪或者有滚动条纹现象是普通柱状图能显示但画面一直在闪尤其切到镜像效果后更明显。原因是一次全屏清屏、再逐列绘制OLED 驱动芯片接收到的数据和用户看到的显示画面之间存在“半更新”状态I2C 模式下这个过程拉长变成肉眼可见的闪烁。解决方法是不要用ClearScreen再画直接在内存里维护一个 1024 字节的帧缓冲所有绘制函数都写帧缓冲最后把整帧缓冲通过一次或分几次写入 OLED。更省时间的做法是只写发生变化的页或列但代价是代码复杂一些。如果你的 OLED 支持 SPI我强烈建议这一版就换 SPI刷新率翻几倍闪烁问题会少很多。5.4 ADC 采集到明显的工频干扰频谱底噪高现象是频谱底下一层“草”一直有不放音乐也跳或者有规律地上下波动。原因可能是板子供电来自 USB工频通过地线耦合进来也可能是 OLED 背光刷新导致电源波动而 ADC 和背光共用了电源。解决方法是先用电池或线性稳压供电确认是否还有底噪。如果电池供电就干净说明噪声从外部电源地进来。再看模拟部分有没有 10uF 大电容和 100nF 高频电容。ADC 采样尽量在背光比较稳定的时间段触发不要把背光的 PWM 频率和 ADC 采样频率设置成接近的整数倍否则会形成拍频。5.5 声音一大频谱就“平头”到处都顶格现象是小声播放时频谱看着正常声音一大大量柱状图同时顶到最高点像削平了一样。原因就是运放增益过高或 ADC 输入过幅信号被削顶波形变成方波FFT 会产生很多高次谐波结果整个频谱看起来都是平的。解决方法是把运放增益降下来或者换用更合适的驻极体偏置电阻。如果希望小声也能跳、大声不削顶可以加自动增益控制电路但 AGC 会让频谱幅度跟随时间变化视觉上柱子会有“呼吸感”。我通常宁可留 30% 余量也不做自动增益嵌入式入门阶段先把静态增益调好后面再谈动态范围。6. 进阶验证先让 1kHz 正弦波在固定柱上“动”再谈多效果优化改变显示效果之前先做一次系统标定。我在调试这类频谱项目时第一步永远是拿手机播放 1kHz 纯音然后让程序把 FFT 原始幅度通过串口发出来。8kHz 采样、128 点 FFT1kHz 应该落在第 16 个频点附近。如果峰值出现在第 15 或第 17 点说明实际采样率和你配置的 8kHz 有偏差需要调整定时器分频。这一步可以把“FFT 有问题”和“采样率不对”彻底分开。标定完成后再处理显示动态范围。线性幅度显示有一个问题底噪在低端占很多像素音乐峰值偶尔才顶到中上部看上去整体偏低。常见的做法是显示比例用二次方或者分贝刻度把低幅度区拉开。如果想要更接近“响度感知”可以做一个 32 项分贝查表把 -60dB 到 -6dB 映射到 OLED 高度。/* 把 Q15 幅度换算成分贝再做 60dB 动态范围映射 */ uint8_t dB_to_height(uint16_t mag_q15) { if (mag_q15 2) return 0; int32_t db 20 * log10f((float)mag_q15 / 32767.0f); /* db 范围约 -90 到 0只取 -60 以上部分 */ if (db -60) db -60; uint8_t h (uint8_t)((db 60) * 64 / 60); return h 64 ? 64 : h; }这个函数注意要把log10f改成适合单片机的实现或者提前用脚本生成查表否则每帧 128 次对数运算很快把 M3 拖垮。我的习惯是用 Python 生成 32768 个映射值太占空间一般生成 256 个分段查表误差完全够用。调完显示再考虑效果切换。不要一上来就做五种效果先写出普通柱状图和峰值保持确认刷新率稳定再叠加镜像和灰度。最后可以用串口打印每帧 FFT 处理耗时做到心里有数。我后来每次做类似项目都会固定用“1kHz 标定 双缓冲 定点 FFT”这套流程省掉了很多“玄学”排错时间。希望这份拆解能帮到你少踩几个硬件和软件之间的坑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑