
最近很多读者在后台问我学完 STM32 基础外设、跑通 FreeRTOS 之后下一步做什么项目最能检验综合能力。我通常会推荐音频频谱分析。理由很简单这个项目同时踩中了嵌入式开发的三个关键领域——音频采集、数字信号处理、图形界面渲染而它们的交集正好构成一个典型的实时系统问题CPU 资源有限任务多了会互相干扰处理慢了数据就丢。这篇文章要讲的是用 FreeRTOS LVGL CMSIS-DSP 三件套在 MCU 上实现实时音频频谱分析。选这套组合不是随意的CMSIS-DSP 提供针对 Cortex-M 优化的 FFT 算法FreeRTOS 负责把采集、计算、刷新拆成独立任务LVGL 负责把频谱数据渲染成好看的柱状图。三者分工明确组合起来能覆盖从音频输入到屏幕输出的完整链路。读完你可以照着移植到自己的板子上也能把每一步背后的工程取舍理解清楚。1. 这篇文章真正要解决的问题音频频谱分析在嵌入式圈子里一直很有吸引力。效果直观是表面原因更核心的是它把很多“知道但不会用”的知识点串了起来。很多人学完 ADC 采样、DMA 传输、FreeRTOS 任务创建、LVGL 控件绘制每个部分单独拿出来都能跑但合在一起就出问题而且出的问题往往不是某个外设坏了而是系统结构设计不合理。拆开来看真正难的地方是三个层面第一音频采样要稳定。频谱分析的基础是时域数据如果 ADC 的采样间隔不均匀FFT 算出来的频谱会出现频率偏移和杂散分量。很多初学者直接用 ADC 连续转换模式加延时这种做法在低频段勉强能用但频率稍微高一点就明显失真。解决思路是用定时器触发 ADC配合 DMA 搬运让采样行为脱离 CPU 控制。第二FFT 计算要足够快。MCU 不像 PC 有强大算力一个 256 点的 FFT 如果用纯 C 循环去写在 168MHz 的 Cortex-M4 上可能要花几十毫秒如果算 1024 点就更夸张。CMSIS-DSP 的价值在于它针对 ARM Cortex-M 做了指令级优化FFT 从“能不能跑”变成“能不能在采样窗口内跑完”。第三UI 刷新不能阻塞任务。LVGL 的渲染很吃 CPU尤其是带透明效果和复杂控件的时候。如果把 FFT 计算和 LVGL 刷新放在同一个上下文里计算期间屏幕不更新刷新期间音频数据丢失。必须用 RTOS 把采集、计算、显示拆成独立任务各干各的。这篇文章适合已经熟悉 STM32 基础开发、简单用过 FreeRTOS 的读者。你不用是 DSP 算法专家不需要手写傅里叶变换只需要理解每个组件扮演的角色然后把它们正确拼接起来。文章会给出完整代码也会解释每一步为什么这样设计。2. 核心概念从采样点到频谱柱状图2.1 音频数字化采样率、位深与奈奎斯特定理音频信号在本质上是连续变化的模拟电压MCU 只能处理离散的数字值所以第一步是采样。ADC 把某个时刻的电压值量化成一个数字这个操作以固定频率重复进行得到的序列就是采样信号。采样间隔必须满足奈奎斯特定理采样率至少是信号最高频率的两倍否则高频分量会混叠到低频段频谱图上一片混乱。人耳能听到的频率上限约 20kHz所以高质量音频用 44.1kHz 采样率但如果只做语音频谱分析16kHz 甚至 8kHz 完全够用FFT 点数也可以更少计算压力更小。位深决定动态范围。STM32 的 ADC 通常是 12 位量化到 0~4095中间值 2048 对应零电平。把原始整数转换成浮点并减去直流偏置是进入 FFT 之前必须做的一步操作。2.2 FFT把时域信号变成频域分布FFT快速傅里叶变换是数字信号处理里最经典的算法作用是把一段时域波形拆解成一系列频率分量的叠加。打个比方一段复杂的音频波形就像一份混合果汁FFT 相当于帮你分析出里面含了多少种水果、每种占多少比例。FFT 输入是一段采样序列输出是等长的复数序列。每个复数代表某个频率分量的幅度和相位其中频率点 i 对应的实际频率是f(i) i * 采样率 / FFT点数例如采样率 16000Hz、FFT 点数 256那么频率分辨率是 62.5Hz第 16 个点代表 1kHz。这个对应关系在验证计算结果时非常重要。FFT 结果前一半0 到 采样率/2是有效的正频率分量后一半是镜像直接丢弃。这就是为什么 256 点 FFT 只能得到 128 个有效频点。2.3 CMSIS-DSP 在项目中扮演什么角色CMSIS-DSP 是 ARM 官方提供的数字信号处理库包含 FFT、滤波、矩阵运算、数学函数等大量例程并且针对 Cortex-M 系列内核做了汇编级优化。在 Cortex-M4 或 M7 上它还能自动利用 FPU 和 SIMD 指令加速浮点运算。这个项目里最核心的两个函数是arm_rfft_fast_f32和arm_cmplx_mag_f32。前者做实数序列 FFT后者计算复数幅度谱。使用 CMSIS-DSP 的最大价值不是你省了写 FFT 算法的时间而是你获得了经过验证、性能可预测的计算路径这在实时系统里很重要。2.4 FreeRTOS 与 LVGL 的分工FreeRTOS 是这个系统的“调度中枢”。它负责让音频采集、FFT 计算、LVGL 刷新三个任务互相配合互不阻塞。CMSIS-DSP 的 FFT 函数本身不是实时系统它只是一段计算代码必须在 FreeRTOS 的某个任务上下文中被调用。LVGL 是一个图形库它自带控件系统和事件循环但它不关心你的应用层数据从哪里来。因此三者的协作模式是硬件外设把音频数据采集到内存DMA 中断通过 FreeRTOS 信号量唤醒 FFT 任务FFT 任务计算完频谱后把处理结果通过队列发送给 UI 任务UI 任务在周期性地调用lv_timer_handler时更新图表控件。3. 系统架构与 FreeRTOS 任务设计3.1 整体数据流在写代码之前必须先把数据流画清楚。音频频谱分析仪的数据链路可以分四段模拟音频信号进入 ADC 引脚。定时器触发 ADC 采样DMA 把采样值连续搬运到内存数组。DMA 半传输/全传输完成中断发出信号量唤醒 FFT 任务。FFT 任务计算频谱把柱状条数据通过队列发给 UI 任务LVGL 绘制图表。这个流程中音频采集不占用 CPUDMA 在后台搬运数据FFT 计算只处理已经采集好的数据块LVGL 只负责显示 FFT 任务算好的结果。每个环节的依赖关系非常明确。3.2 任务划分与优先级设计任务划分的基本原则是实时性要求高的任务优先级高非实时任务优先级低。这个项目里FFT 任务需要在下一个 DMA 半周期到来之前把当前数据块处理完否则数据会丢失因此优先级最高。UI 任务只要保证每秒刷新 15~30 帧即可优先级可以低一些。推荐创建两个任务FFT 任务阻塞在信号量上DMA 中断释放信号量后开始计算计算完成后把结果发送到队列。UI 任务延缓 50ms 左右调用一次lv_timer_handler同时从队列取频谱数据更新图表。在实际系统中还可能有 LED 闪烁、按键扫描等任务但优先级不要高于 FFT 任务。任务优先级的设置可以直接反映在这个项目里如果 UI 任务优先级高于 FFT 任务屏幕刷新很流畅但音频数据会频繁丢失频谱图出现卡顿和跳变如果两者差距过大UI 帧率又会明显下降。3.3 任务间通信信号量、队列与共享内存任务间通信方式的选择是这个项目里比较容易被忽略的设计点。很多初学者习惯用全局变量加标志位这在简单场景下可行但一旦两个任务的访问时机不固定就会出现数据撕裂或漏处理。推荐方式信号量用于 DMA 中断唤醒 FFT 任务。使用计数信号量可以防止多次中断事件被合并丢失。队列用于 FFT 任务向 UI 任务传递频谱数据。将 40 个柱状条数据打包成一个结构体拷贝进队列UI 任务取出时是完整快照不会出现读一半被覆盖的问题。共享内存 特殊同步机制适合大数据量场景例如直接把 FFT 输出缓冲区共享但需要增加互斥保护设计复杂度较高入门项目不推荐。在 FreeRTOS 里DMA 中断上下文不能调用阻塞 API只能使用带FromISR后缀的中断安全函数。中断释放信号量后必须调用portYIELD_FROM_ISR否则唤醒的高优先级任务要等到下一个 tick 才被调度实时性得不到保证。4. 环境准备与工程配置4.1 硬件选型建议音频频谱分析项目对主控的基本要求是有 ADC、有 DMA、有足够 RAM 存放采样缓冲和 FFT 中间结果。带 FPU 的 Cortex-M4 以上型号体验更好因为 CMSIS-DSP 的浮点 FFT 性能会大幅提升。这里给出了几种组合供参考硬件模块推荐方案说明主控STM32F407、STM32F746、STM32H750 等优先选带 FPU、主频 168MHz 以上型号屏幕SPI 屏或并口 RGB 屏320x240 以下分辨率越高渲染压力越大入门 2.4 寸 SPI 屏足够音频输入板载音频编解码器、外接麦克风模块、音频线直连 ADC纯学习用麦克风模块最简单模块输出再接 ADC 引脚调试工具串口打印、逻辑分析仪验证 FFT 结果和任务实时性时非常有用4.2 软件工具链软件环境建议使用 STM32CubeMX 生成初始化代码编译工具链可以是 Keil、IAR 或 GCC。CMSIS-DSP 可以直接在 CubeMX 的软件包选项中勾选添加也可以在工程里手动加入源码。LVGL 推荐使用 v8.x 或 v9.x 版本两者 API 有些差异本文代码以 v8 为主。如果你还没有做过 LVGL 移植可以在 PC 上用 LVGL 模拟器先把界面逻辑跑通再同步到 MCU 工程。模拟器可以省去大量屏幕驱动调试时间。4.3 STM32CubeMX 中的关键配置在 CubeMX 中需要配置的主要外设包括 ADC、DMA、定时器、FreeRTOS 和屏幕驱动引脚。时钟树建议把系统主频设置到最高以 F407 为例是 168MHzAPB1 定时器时钟通常为 84MHz。以 16kHz 采样率为例ADC 采用定时器触发模式一个可行的分频配置是TIM6 预分频 84-1计数时钟变成 1MHz。TIM6