基于 MAX32690EVKIT 的音频频谱分析与实时显示
摘要
本项目完成了 MAX32690EVKIT 上的实时音频频谱分析链路。板载 MAX9867 通过 I2C2 完成工作模式配置,并以 48 kHz、双声道、24 bit 左对齐格式向 MAX32690 的 I2S 接口输出数字音频。Cortex-M4F 使用两块 512 字的乒乓 DMA 缓冲区连续接收数据,在主循环中完成左声道抽取、去直流、RMS 计算、Hann 加窗、256 点实数 FFT、幅度谱和主频插值;随后将 128 点波形或频谱封装为 141 B 显示帧,经 SEMA 邮箱交给片上 RISC-V 核。RISC-V 负责 128×128 ST7735 TFT 的差分刷新,SW2 可在时域和频域界面之间切换。设计刷新率分别为 46.875 fps 和 23.4375 fps,均高于题目要求的 10 fps。
1. 项目介绍
1.1 任务目标
任务要求利用 MAX32690EVKIT 的音频接口完成声音采集、频谱分析和屏幕显示。核心验收点包括:通过 I2C 初始化 MAX9867,通过 I2S/PCM 和 DMA 或中断持续获取音频,使用 FFT 计算频谱并提取主要频率,最后以不低于 10 fps 的速度显示时域波形和频域频谱。
2. 硬件平台介绍
2.1 MAX32690EVKIT
MAX32690EVKIT 是本项目的载体,板上集成 MAX32690、MAX9867 立体声音频编解码器、1.45 英寸 128×128 彩色 TFT、3.5 mm Line-In/Line-Out、USB 供电与 UART、SWD 接口以及通用按键。评估板资料说明,音频编解码器通过 I2C 和 I2S/PCM 与 MCU 相连;TFT 由于硬件引脚分配原因使用 GPIO 模拟 SPI 时序。
2.2 使用到的硬件
硬件 | 本项目中的作用 | 关键参数/连接 |
|---|---|---|
MAX32690 | 主控制器与双核计算平台 | 120 MHz Cortex-M4F;RISC-V RV32 协处理器;I2S、DMA、I2C、SEMA |
MAX9867 | 模拟音频 ADC 与数字音频接口 | I2C 地址 |
J5 Line-In | 接入 PC/手机耳机输出或经过前置放大的麦克风 | 3.5 mm 立体声输入;工程分析左声道 |
CFAF128128B1 类 TFT | 显示波形、频谱和主频 | 128×128;ST7735 控制器;GPIO 模拟 SPI |
SW2 | 时域/频域显示模式切换 | P4.0,按下为低;软件采用边沿检测 |
JP9 / JP10 | 将 I2C2C 路由到 MAX9867 | 均置于 2–3:JP9=SDA,JP10=SCL |
CN1/CN2 与调试器 | 供电、下载、调试和串口日志 | OpenOCD/CMSIS-DAP;UART 115200-8-N-1 |
接线要点: 音源输出接 J5,先把音量调低再逐步增加;JP9、JP10 必须切到 2–3,才能把 I2C2C 的 SDA/SCL 接到编解码器。由于 JP9/JP10 的另一档用于 UART2 CTS/RTS,调试串口只使用 TX/RX 即可。
3. 总体方案框图与设计思路

图 1:系统总体方案框图
3.1 数据链路
- MAX9867 将 J5 的模拟立体声输入转换为 48 kHz、24 bit 数字音频,并提供外部 SCK/WS 给 I2S 从机接口。
- I2S RX DMA 每次写入 512 个 32 位字,即 256 组左右声道采样,单块持续 5.333 ms。
- DMA 回调只完成缓冲区状态切换和下一次 DMA 启动;FFT 不在中断中执行。
- M4 主循环每 4 块生成一次时域帧,每 8 块执行一次 FFT;显示包通过 SEMA 邮箱非阻塞发送。
- RISC-V 核解析显示包,以差分点或垂直色带更新 TFT,避免整屏清除造成闪烁和低帧率。
3.2 关键设计参数
参数 | 取值 | 设计意义 |
|---|---|---|
采样率 | 48 kHz | 覆盖 0–24 kHz;MAX9867 支持;与 12.288 MHz MCLK 匹配 |
DMA 帧 | 256 stereo samples | 5.333 ms/帧;兼顾实时性和 FFT 点数 |
DMA 缓冲 | 2 × 512 × 32 bit | 总计 4096 B;采集与处理交替进行 |
FFT | 256 点 real FFT | 128 个正频率点;分辨率 187.5 Hz/bin |
幅度显示 | 相对峰值 0 至 −60 dB | 弱分量仍可见,避免线性谱被主峰压扁 |
跨核帧 | 141 B / 256 B mailbox | 带 magic/version/sequence,静态断言保证尺寸 |
显示调度 | 4 块 / 8 块 | 时域 46.875 fps;频域 23.4375 fps |

图 2:DMA 块与显示/FFT 调度关系
4. 调试软件与工程组织
4.1 开发与调试工具
软件/组件 | 用途 | 本工程使用方式 |
|---|---|---|
MSDK | 芯片启动、外设驱动、板级支持与构建系统 | |
GNU Arm Embedded 10.3 | 编译 Cortex-M4F 应用 | hard-float, |
xPack RISC-V GCC 12.2 | 编译 RV32 显示固件 | 生成 |
CMSIS-DSP 1.16.2 | 实数 FFT 与数学运算 | |
GNU Make + MSYS2 | 按阶段编译与清理 | |
OpenOCD / CMSIS-DAP | 下载、复位和断点调试 | |
串口终端 | 查看状态、主频、RMS、overrun 和显示统计 | UART2,115200-8-N-1 |
Unity + Host GCC | 纯软件单元测试 | 23 项测试,0 失败 |
4.2 四阶段调试法
工程将复杂链路拆成四个可单独构建的阶段,从输入到输出逐级隔离问题:
阶段 | 验证范围 | 主要观察量 |
|---|---|---|
| MAX9867 + I2S RX + DMA | completed、overruns、dma_error、running/paused |
| 采集 + 左声道 + FFT | peak_hz、RMS、静音判定、1.5/3/6/12 kHz |
| 双核邮箱 + TFT 渲染 | 合成波形/频谱、SW2 切换、残影与卡顿 |
| 完整音频到显示链路 | 频率随输入变化、display sent/dropped/rendered |
关键调试命令如下:
make STAGE=capture clean && make STAGE=capture flash.openocd
make STAGE=dsp clean && make STAGE=dsp flash.openocd
make STAGE=lcd clean && make STAGE=lcd flash.openocd
make STAGE=app clean && make STAGE=app flash.openocd
make RUN_TESTS=1 clean && make RUN_TESTS=1 flash.openocd
5. 软件流程图及关键代码

图 3:完整软件流程图
5.1 MAX9867 与 I2S 初始化
初始化时先将 I2C2 设置为 100 kHz,再调用板级 MAX9867 驱动完成基础配置。工程随后写入 48 kHz 的 NI 参数并回读校验,启用录音路径,设置 ADC 电平为 −12 dB、Line-In 增益为 +10 dB。I2S 采用 24 bit 有效数据、32 bit 字槽、左对齐、MSB 优先、双声道,SCK 与 WS 由 MAX9867 提供。
MXC_I2C_Init(MXC_I2C2, 1, 0);
MXC_I2C_SetFrequency(MXC_I2C2, 100000U);
max9867_init(MXC_I2C2, 12288000U, 1);
codec_set_48khz();
max9867_enable_record(1);
max9867_adc_level(-12, -12);
max9867_linein_gain(10, 10);
request.sampleSize = MXC_I2S_SAMPLESIZE_TWENTYFOUR;
request.adjust = MXC_I2S_ADJUST_LEFT;
request.channelMode = MXC_I2S_EXTERNAL_SCK_EXTERNAL_WS;
request.stereoMode = MXC_I2S_STEREO;
5.2 DMA 乒乓缓冲与中断最小化
缓冲区具有 FREE、CAPTURING、READY、PROCESSING 四种状态。DMA 完成后,中断回调把当前槽置为 READY,并立即寻找 FREE 槽启动下一次接收;主循环只获取最早完成的 READY 帧。若两个槽均被占用,采集暂时暂停并记录 overrun,待 PROCESSING 槽释放后自动恢复。
static void dma_callback(int channel, int result)
{
uint8_t next_slot = AUDIO_BUFFER_SLOT_NONE;
uint8_t completed_slot = dma_slot;
MXC_DMA_ReleaseChannel(dma_channel);
running = 0;
if (result != E_NO_ERROR) {
dma_error = result;
return;
}
audio_buffer_pool_complete(&pool, completed_slot, &next_slot);
if (next_slot != AUDIO_BUFFER_SLOT_NONE) {
dma_error = start_dma(next_slot);
}
}
5.3 FFT 处理链路
每个 DMA 帧包含左右声道交织的 32 位字。算法选择偶数索引的左声道,将有符号样本归一化到约 [−1, 1),求均值并去除直流。RMS 在未加窗的中心化信号上计算;FFT 输入乘以预计算的 Hann 窗,从而降低非整周期信号造成的谱泄漏。
for (i = 0; i < 256; i++) {
int32_t sample = (int32_t)i2s_words[2U * i];
fft_input[i] = (float)sample / 2147483648.0f;
mean += fft_input[i];
}
for (i = 0; i < 256; i++) {
float centered = fft_input[i] - mean / 256.0f;
energy += centered * centered;
fft_input[i] = centered * hann_window[i];
}
arm_rfft_fast_f32(&rfft, fft_input, fft_output, 0);
幅度谱使用 4/N 的比例修正,同时补偿单边谱和 Hann 窗约 0.5 的相干增益。峰值搜索忽略 DC 和 50 Hz 以下频率;对最大 bin 及左右相邻 bin 的功率做抛物线插值,估计偏移限制在 ±0.5 bin,从而在 187.5 Hz 的原始分辨率基础上改善单音频率读数。
5.4 显示帧与双核渲染
时域模式从 256 个左声道采样中隔点提取 128 点并自动缩放;频域模式将 128 个正频率点转换为相对最大值的 −60 至 0 dB。显示包由 magic、version、sequence、frame_count、peak_hz、level 和 128 B plot 组成,总长 141 B,能够装入 256 B SEMA 邮箱。
if (mode == DISPLAY_MODE_TIME && ++waveform_blocks >= 4) {
action.send_display = 1;
action.extract_waveform = !action.run_fft;
}
if (++fft_blocks >= 8) {
action.run_fft = 1;
if (mode == DISPLAY_MODE_FREQUENCY) {
action.send_display = 1;
}
}
display_packet_build(&packet, sequence++, processed, mode, &analysis);
display_bridge_try_send(&packet); /* mailbox full: drop only this frame */
RISC-V 不做整屏重绘:时域只擦除旧点、写入新点;频域只更新柱高变化的垂直区间,并设置 2 像素滞回、1024 单位的单帧工作预算。频谱标题约每 96 帧更新一次,使峰值文字刷新不会持续占用 GPIO 模拟 SPI 带宽。
6. 功能展示与验证
6.1 屏幕显示效果


图 4:TIME/FREQ 界面
上图左侧为时域模式:黑色背景、顶部 TIME 标题和青色波形;右侧为频域模式:顶部显示插值后的主频,图区以绿色填充 128 列频谱。按下并释放 SW2 可在两种模式间切换,软件采用边沿检测,因此持续按住不会重复切换。
7. 项目难题及解决方法
7.1 I2C 路由与串口复用冲突
难点: JP9/JP10 默认可把 P1.7/P1.8 接到 UART2 CTS/RTS,而 MAX9867 需要同一组引脚作为 I2C2C SDA/SCL。若跳帽位置错误,I2C 事务会超时,但串口 TX/RX 仍可能正常,容易误判为编解码器驱动问题。
解决: 将 JP9、JP10 都置于 2–3,并在初始化后回读 0x06/0x07 时钟寄存器,确认总线和配置均有效。
7.2 24 bit I2S 数据的符号与声道定位
难点: DMA 以 32 位字搬运 24 bit 左对齐数据,且左右声道交织。如果直接按 int16_t 读取或声道步长错误,波形会削顶、频谱出现镜像或主频错误。
解决: 先把 32 位字强制转换为 int32_t 保留符号,再按 i2s_words[2*i] 取左声道;波形显示使用 sample >> 16,FFT 使用除以 2³¹ 的浮点归一化。
7.3 DMA 与主循环的竞态
难点: 若 DMA 在主循环处理期间覆盖同一缓冲区,FFT 会读到撕裂数据;若回调中直接运行 FFT,又会拉长中断并导致下一帧丢失。
解决: 显式维护四态缓冲区所有权,中断只推进状态机,获取/释放临界区短暂关中断;两个槽都忙时暂停而不是覆盖,并通过 overrun 计数暴露性能问题。
7.4 频谱泄漏、噪声误报与频率分辨率
难点: 256 点、48 kHz 的原始 bin 间隔为 187.5 Hz,非整周期单音会泄漏到相邻频点;Line-In 底噪还可能被识别为主峰。
解决: 去除帧均值、使用 Hann 窗、设置 RMS 与 peak_power 双门限、忽略 50 Hz 以下频点,并用三点抛物线插值改善单峰读数。
7.5 GPIO 模拟 SPI 的刷新瓶颈
难点: 评估板 TFT 没有接到硬件 SPI,整屏 128×128 像素以 GPIO bit-bang 刷新会占用大量 CPU,容易低于 10 fps。
解决: 把显示从 M4 分离到 RISC-V;时域仅更新改变的像素,频域仅更新柱高差值;加入 2 像素滞回、单帧工作预算和跨帧扫描起点,并降低频率标题更新速率。这样显示负载被限制,音频 DMA 和 FFT 不被阻塞。
7.6 双核镜像的一致性
难点: M4 启动时嵌入 riscv.bin,如果两个核心使用不同输出目录或旧文件未重建,M4 可能悄悄装载过期的 RISC-V 固件。
解决: project.mk 强制 M4 与 RISC-V 使用工程本地 build/buildrv,并让 lcd/app 阶段递归构建 RISC-V 镜像后再链接 M4 应用。
8. 心得体会
本项目最重要的收获,是认识到“FFT 算法能运行”只占实时音频系统的一小部分。真正决定体验的,是采集时钟、DMA 所有权、中断长度、跨核通信、显示带宽和错误退化策略能否形成稳定闭环。采用四阶段调试后,每个阶段都有单独的输入、输出和验收指标,比一次性联调整条链路更容易定位问题。
在信号处理方面,Hann 窗、直流移除、噪声门限和峰值插值虽然代码量不大,却显著改善了频谱的可读性。在系统设计方面,将 TFT 渲染交给 RISC-V,并让显示邮箱满时只丢当前帧,使音频采集保持最高优先级;这种“数据采集不可阻塞、显示允许降级”的取舍同样适用于示波器、振动监测和其他边缘信号分析项目。
最后,项目也提醒我必须区分理论指标、软件测试和实物测量。46.875 fps 与 23.4375 fps 是由调度参数推导出的设计值;23/23 是可复现的软件测试结果;而现场 60 秒的丢帧率、overrun 和屏幕照片应由实物实验给出。清楚标注证据来源,能让项目报告更可信,也便于后续复现和改进。
9. 总结与可扩展方向
工程已经实现从 MAX9867 音频输入、I2S DMA 连续采集、CMSIS-DSP FFT、主频与幅度提取,到双核彩屏显示的完整闭环。当前使用 48 kHz/256 点配置,在保持低延迟的同时提供 187.5 Hz 原始分辨率,并通过插值改善单音读数;双模式刷新率均超过任务要求。
后续可从以下方向扩展:
- 将采样率切换为 16 kHz,并采用 512 或 1024 点 FFT,以提高语音频段的频率分辨率。
- 加入指数平滑、峰值保持和对数频率刻度,进一步提升显示稳定性和视觉可读性。
- 实现麦克风偏置/前置放大方案,或使用 MAX9867 的差分麦克风输入,形成真正的声学采集终端。
- 把 UART 统计保存为 CSV,自动计算 60 秒丢帧率、overrun 和主频误差,形成可重复的硬件验收报告。
- 增加频带能量、基频跟踪或音符识别,在现有 FFT 管线之上扩展声音分析功能。
参考资料
- 硬禾学堂 FunPack 5-3 活动页面
- Analog Devices, MAX32690 Evaluation Kit Data Sheet
- Analog Devices, MAX32690 Datasheet and Product Information
- Analog Devices, MAX9867 Ultra-Low Power Stereo Audio Codec
- Analog Devices MSDK