Funpack5-3 - 基于MAX32690 EV Kit实时音频频谱分析仪
Funpack5-3 - 基于MAX32690 EV Kit实时音频频谱分析仪
大家好,我是代码小A
非常荣幸参加由电子森林所举办的Funpack S5 #3 活动
一、项目介绍

本项目使用 MAX32690EVKIT 开发板完成了一个实时音频频谱分析仪:板载 MAX9867 音频编解码器通过 I2C 接口配置,以 16kHz 采样率采集 Line-In 输入的音频信号;主控通过 I2S 从机接口配合 DMA 双缓冲实现连续无阻塞采集;采集到的数据经过自编写的 512 点 FFT(加汉宁窗)处理后,在板载 128×128 ST7735 彩色屏幕上实时显示时域波形和频域频谱,并提取主要频率成分显示在屏幕标题栏,同时通过串口输出帧率、主频峰值和输入电平,方便调试与验证。
完成情况-项目完全满足任务要求:
- 音频采集:I2C 初始化并配置 MAX9867;采样率 16kHz(任务建议 8~16kHz);I2S 数字音频接口;DMA 乒乓方式连续采集。
- 信号处理:自实现基-2 快速傅里叶变换;计算频谱分布;提取主频峰值与幅度信息。
- 屏幕显示:实时显示时域波形(单周期触发,48 点)与频域频谱(96 根柱状图);实测刷新率约 30fps,远超 10fps 要求。
二、硬件介绍

- MAX32690EVKIT 主控板:搭载 MAX32690 微控制器(ARM Cortex-M4F 内核,最高 100MHz 主频,3MB Flash、1MB SRAM),片上集成 DMA 控制器、I2S、I2C、SPI、UART 等丰富外设,板载 CMSIS-DAP 调试器与 128×128 TFT 彩屏。
- MAX9867 音频编解码器:低功耗立体声 Codec,支持 I2C 寄存器配置(7 位地址 0x18),内部 ADC/DAC 完成模拟到数字的转换;项目中配置为 I2S 主模式,由 12.288MHz MCLK 通过 NI 分频产生 16kHz LRCLK 与位时钟,作为音频数据与时钟的来源。
- ST7735S 128×128 TFT 显示屏:SPI 接口彩屏,开发板上通过 GPIO 位带模拟 SPI 时序驱动。
- 测试信号源:手机/电脑音频输出经音频线接入板卡 Line-In 接口;配合自编写的网页信号发生器(Web Audio API)可输出任意频率正弦波、扫频、噪声等测试信号。
三、方案框图与项目设计思路
系统框图如下:
设计思路主要有以下几点:
- 数据通路解耦:I2S + DMA 在中断中只做缓冲区指针交接,主循环负责 FFT 与显示,保证采集不丢数据;DMA 乒乓双缓冲配合 32 级 ISR 队列,即使显示耗时也能兜底。
- 自研 FFT,零依赖:512 点基-2 按时间抽取 FFT 自己实现,使用 M4 硬件 FPU(软浮点 ABI 下仍可执行 VFP 指令),不依赖 CMSIS-DSP 库,也不要求切换硬浮点库,避免改动 SDK 构建链。
- 显示性能优先:采用 128×128 RAM 帧缓冲,先整条渲染再批量推送;同时借鉴参考工程的“局部刷新”思想,按列记录脏区,只把变化的列合并成矩形推送到屏幕,稳定信号时 SPI 流量大幅下降。
- 可测试性设计:固件内置自测模式(板内生成 1kHz 正弦信号走完整 FFT/绘制链路),并通过串口 dump 屏幕频谱坐标(ypts + ASCII 图),在没有示波器/逻辑分析仪的情况下也能远程验证绘制是否正确。
四、调试软件、软件流程图及关键代码介绍
调试软件
- MSDK(Analog Devices MAXIM SDK):官方嵌入式开发套件,提供外设驱动、BSP 与构建系统,使用自带 GCC 10.3 工具链。
- VSCode + OpenOCD + CMSIS-DAP:编写代码、编译、烧录与在线调试。
- 串口调试助手:115200-8-N-1 查看运行日志与频谱 dump。
- 自写网页信号发生器(test-tone.html):基于 Web Audio API,可输出正弦/方波/扫频/白噪声等测试信号,频率与音量滑块实时生效,作为频谱仪的激励源。
软件流程图
关键代码介绍
1) 24 位 I2S 数据解码(易错点)
MAX32690 的 I2S 把 24 位采样左对齐存放在 32 位字的高 24 位,驱动源码 MXC_I2S_RevA_ConfigData 中写入 FIFO 前执行 (sample & 0xFFFFFF) << 8 可证实。因此正确解码是算术右移 8 位:
int32_t l = (int32_t)buf[i] >> 8;
int32_t r = (int32_t)buf[i + 1] >> 8;之前误用 (buf << 8) >> 8 会把真实音频解成低位噪声,这正是“频谱绘制异常”的根因之一。
2) 自写 512 点 FFT(基-2 按时间抽取)
位反转置换 + 蝶形运算,旋转因子表预计算,完整实现如下:
static void fft_run(float *re, float *im)
{
uint32_t i, j;
uint32_t len;
/* 位反转置换 */
for (i = 1, j = 0; i < AV_FFT_LEN; i++) {
uint32_t bit = AV_FFT_LEN >> 1;
for (; j & bit; bit >>= 1) {
j ^= bit;
}
j ^= bit;
if (i < j) {
float tr = re[i]; re[i] = re[j]; re[j] = tr;
float ti = im[i]; im[i] = im[j]; im[j] = ti;
}
}
/* 蝶形运算 */
for (len = 2; len <= AV_FFT_LEN; len <<= 1) {
uint32_t half = len >> 1;
uint32_t step = AV_FFT_LEN / len;
for (i = 0; i < AV_FFT_LEN; i += len) {
for (j = 0; j < half; j++) {
uint32_t p = i + j;
uint32_t q = p + half;
uint32_t t = j * step;
float xr = re[q] * tw_re[t] - im[q] * tw_im[t];
float xi = re[q] * tw_im[t] + im[q] * tw_re[t];
re[q] = re[p] - xr;
im[q] = im[p] - xi;
re[p] += xr;
im[p] += xi;
}
}
}
}经与朴素 DFT 数值对比,1kHz 正弦精确落在第 32 bin,验证算法正确。
3) 单周期触发波形显示
48 个点如果横跨整帧(512 点 = 32 个 1kHz 周期),每个周期只摊到 1.5 个点,显示为“零散的点”。解决办法:按主频计算周期长度,寻找上升过零点作为触发起点,把 48 个点全部布在一个周期内并做线性插值,得到平滑的单周期正弦:
uint32_t period = AV_FFT_LEN / track_bin; /* 主频对应的周期采样数 */
uint32_t trig = start;
int found = 0;
if (period < 16) period = 16; /* 高频时放宽到至少 16 采样 */
if (period > 400) period = 400;
/* 在最近两个周期内寻找上升过零点作为触发点 */
for (k = 1; k < 2 * period; k++) {
uint32_t ic = (start - k) & AV_RING_MASK;
uint32_t ip = (start - k - 1) & AV_RING_MASK;
if (ring[ip] <= 0.0f && ring[ic] > 0.0f) {
trig = start - k;
found = 1;
break;
}
}
if (!found) trig = start - period;
/* 一个周期内均匀取 48 点,线性插值 */
for (i = 0; i < AV_WAVEFORM_POINTS; i++) {
float pos = (float)trig +
(float)i * ((float)period / (AV_WAVEFORM_POINTS - 1));
uint32_t i0 = (uint32_t)pos & AV_RING_MASK;
uint32_t i1 = (i0 + 1) & AV_RING_MASK;
float frac = pos - (float)(uint32_t)pos;
float v = ring[i0] * (1.0f - frac) + ring[i1] * frac;
wave_smooth[i] = AV_WAVE_SMOOTH_ALPHA * wave_smooth[i] +
(1.0f - AV_WAVE_SMOOTH_ALPHA) * v;
}4) 脏列局部刷新
每帧渲染完成后对比上一帧坐标,只有变化的列才发送到屏幕,连续变化的列合并为一个矩形批量写入;变化超过 75% 时退化为整条刷新:
static void push_strip_partial(int y0, int h, const uint8_t *dirty)
{
int x, count = 0;
for (x = 0; x < AV_SCR_W; x++) {
count += dirty[x];
}
if (count == 0) {
return; /* 本帧无变化,完全不发 SPI 数据 */
}
if (count > AV_SCR_W * 3 / 4) {
push_area(y0, h); /* 变化过多时整条推送 */
return;
}
x = 0;
while (x < AV_SCR_W) {
while (x < AV_SCR_W && !dirty[x]) x++;
if (x >= AV_SCR_W) break;
int xs = x;
while (x < AV_SCR_W && dirty[x]) x++;
push_area_cols(y0, h, xs, x - 1); /* 连续脏列合并为一个矩形 */
}
}稳态信号下每帧 SPI 数据量从约 29KB 降至几 KB。
5) MAX9867 写寄存器延时补丁
err = MXC_I2C_MasterTransaction(&i2c_req);
if (err) printf("I2C Write Error! Reg: 0x%02X, Code: %d\n", reg, err);
MXC_Delay(200); // 等待 Codec 处理完上一条命令
return err;MAX9867 连续写寄存器时内部需要处理时间,可能拉低 SCL(时钟拉伸),而 MSDK 的阻塞 I2C 事务没有超时,会永久等待;每次写后延时 200µs 即可稳定规避。
五、功能展示图及说明
电脑/手机音频输出通过音频线接入 EV Kit 的 Line-In,USB 线连接电脑供电与 CMSIS-DAP 调试。
屏幕显示效果
屏幕上部分为青色单周期波形曲线(自动增益,正弦波饱满清晰),下部分为绿色频谱柱状图(96 根,线性频率轴),标题栏右上角显示主频(如 3000Hz),主频柱顶有红色标记。
串口实测输出(自测模式,板内 1kHz 正弦)
***** MAX32690 Audio Spectrum Analyzer *****
Sample rate: 16000 Hz, FFT: 512 points
Running...
TFT initialized successfully
Analyzing...
[Stats] FPS=31.2 Peak=1000Hz Lvl=-12dB
=== SPECTRUM DUMP ===
[DUMP] peak_hz=1000 scale_max=26.6 floor=13.4 sig_peak_db=-12.0 sig_rms_db=-15.0
[DUMP] ypts: 119 119 ... 8 7 28 ... 119
=== DUMP END ===dump 中的 ypts 表示屏幕频谱折线上每个显示点的纵坐标,与理论计算值(1kHz 主峰位置、汉宁窗 ±1 bin 的 -6dB 旁瓣)完全吻合,验证了采集、FFT、归一化、绘制的整条链路。
网页信号发生器
自写测试网页支持 1kHz/440Hz/3kHz 正弦、双音、扫频、白/粉红噪声等预设,主频率与音量滑块实时生效;配合板卡可快速验证不同频率下的波形周期与频谱位置。
六、项目中遇到的难题及解决方法
难题 1:频谱所有柱都很高,1kHz 正弦的峰值在 1k/3k/5k/7kHz 之间乱跳。
两个原因叠加:一是 24 位 I2S 数据对齐理解错误导致解码出噪声;二是输入信号削波,1kHz 被削成近似方波产生奇次谐波。解决:修正解码为 (int32_t)buf >> 8;降低 Codec ADC 增益(-12dB)与网页默认音量;峰值检测增加锁定机制(候选频点需连续 3 帧强于当前锁定频点 1.5 倍才切换)。
难题 2:刷新率只有 7.8fps,屏幕卡顿。
瓶颈在 GPIO 位带模拟 SPI:每个字节要经过两个空转延时循环。解决:在工程内覆盖 board.c,删除两个延时循环(ST7735 最小写周期约 66ns,删除后每 bit 仍约 100ns,时序在规格内),位带 SPI 提速约 3 倍;配合帧缓冲整条推送,实测 FPS 提升到 31。
难题 3:如何在没有示波器的情况下验证屏幕绘制是否正确。
为固件增加自测模式与串口 dump:板内生成已知频率正弦信号,跑完整 FFT 与绘制管线,再把屏幕上每个显示点的纵坐标与 ASCII 示意图通过串口输出。将 dump 结果与理论计算对比,即可远程确认整条链路正确。
七、心得体会
本次项目让我对“从采集到显示”的完整音频链路有了系统认识:I2C 配置编解码器、I2S 时序与数据对齐、DMA 连续采集、FFT 频谱分析、小屏显示性能优化,每一环都可能成为瓶颈。
显示优化方面,我实践了几种思路的取舍:全量帧缓冲刷新简单可靠但 SPI 带宽吃紧;参考工程的分段局部刷新更聪明但逐像素发窗口命令反而更慢;最终采用“帧缓冲渲染 + 脏列合并推送”,兼顾了简单性与性能,实测约 30fps。性能优化不是盲目堆技巧,而是要找到真正的瓶颈(这里就是位带 SPI 的延时循环)。
最后,测试工具先行也让我受益匪浅:自测模式 + 串口 dump 让屏幕绘制结果“可量化、可对比”,网页信号发生器让激励信号可控可重复。这套方法不仅解决了本次项目的验证问题,也值得沿用到以后的嵌入式项目中。