最近很多读者在后台问我:学完 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 绘制图表。
这个流程中,音频采集不占用 CPU,DMA 在后台搬运数据;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 为例是 168MHz),APB1 定时器时钟通常为 84MHz。
以 16kHz 采样率为例,ADC 采用定时器触发模式,一个可行的分频配置是:
- TIM6 预分频 84-1,计数时钟变成 1MHz。
- TIM6