MAX32690EVKIT 音频频谱显示项目报告
1. 项目介绍
本项目基于 MAX32690EVKIT 开发板实现了完整的音频采集与频谱显示功能。声音信号通过LINE IN 接口输入,经过 MAX9867 编解码芯片完成模拟到数字的转换,再由主控通过 I2C 配置编解码器、通过 I2S 接口连续读取音频数据。采集到的时域信号一方面用于实时波形显示,另一方面送入 FFT 模块进行频谱分析,最终在彩色屏幕上同时呈现波形图和频谱图。
这个项目的尝试使用了 Zephyr 的线程调度、设备树绑定、DMA 采集、ZBus 消息传递和 LVGL 界面等特性,实际运行时,音频采集、信号处理和图形显示分别放在不同线程中执行,线程之间不直接共享复杂状态,通过ZBUS消息总线传递结果,这样做使得代码结构清楚、便于调试。
2. 使用到的硬件介绍
系统硬件主要由 MAX32690EVKIT 主控板、MAX9867 音频编解码器、麦克风输入接口和彩色显示屏组成。MAX32690EVKIT 负责运行 Zephyr 系统和上层应用,板上的 I2C、I2S、DMA 和显示接口为音频项目提供了完整的外设基础。MAX9867 则负责音频前端,把模拟音频信号转换为 I2S 数字流,并支持采样率、输入增益、时钟格式和电源状态配置。彩色显示屏承担波形和频谱的可视化输出,UART 则用于运行日志和调试信息打印,便于观察外设初始化、采集状态和 FFT 处理结果。
MAX9867 被挂接在设备树 I2C 总线上,I2S 控制器和 DMA 通道也在板级 overlay 中完成了启用与引脚映射。Zephyr 不靠手动管理寄存器地址来管理外设,而是通过设备树来描述硬件连接关系,驱动在启动时能够按配置初始化。例如,音频芯片的节点、I2S 节点和引脚复用都在板级配置中显式声明,这样主程序只需要通过设备模型获取设备即可。
3. 方案框图和项目设计思路介绍
系统数据流从麦克风到显示屏的完整路径:


Zephyr 提供了线程、设备模型和驱动框架,负责让系统跑起来;ZBus 提供线程之间解耦的数据通道,让模块之间能够传消息。音频采集线程把数据发布到 audio_chan,DSP 线程订阅后计算 FFT,再把频谱结果发到 fft_chan,显示线程只关心自己收到的数据是否更新,而不需要知道上游到底是 I2S 还是别的来源,这种结构很适合实时多线程应用。
项目设计上把采集、计算和显示三个部分主动分开。采集线程只管从 I2S 拉取数据,DSP 线程只管做窗函数和 FFT,显示线程只管把结果画到屏幕上。这样做逻辑更加清晰,而且降低了调试成本,每一层都可以通过日志单独验证。

4. 调试软件介绍、软件流程图及关键代码介绍
音频前端初始化写在 src/codec.c 中,核心思路是先把 MAX9867 恢复到默认状态,再按项目需要逐项配置寄存器。初始化时先设置系统时钟和 DAI 格式,再配置 ADC 输入、麦克风增益和电源控制。保证时钟稳定,接口格式对齐后再调节输入增益,这样更容易避免采样率、主从模式或数据对齐错误造成的异常波形。
采集部分位于 src/audio_module.c。这里使用 I2S 配合 DMA 连续读取音频块,并把每次拿到的原始数据整理成统一的音频消息后发布出去。代码里把接收缓冲区放在 non-cache 区域的 slab 中,这是参考了MASDK的例程关闭ICC的思路,避免 DMA 写入与 CPU 读取之间出现缓存不一致。
采集线程里只取左声道,并把 32 位原始样本做位移后再作为有效样本使用。编解码器和 I2S 的实际数据格式往往需要根据硬件对齐方式做转换,代码中计算了当前帧的最大振幅和直流偏置用于调试。
DSP 部分位于 src/dsp_module.c。这里用 CMSIS-DSP 的快速实数 FFT 完成频域分析,先对输入做汉宁窗加权,再进行 FFT 和幅值计算。窗函数的作用是减小频谱泄漏,尤其是当输入信号不是整周期截取时,原始 FFT 结果很容易出现旁瓣扩散。加窗后虽然主峰会略微变宽,但频谱图会更接近真实能量分布。
DSP 线程没有对每一帧都做 FFT,而是通过帧计数进行节流,只对部分采样帧计算结果后再发布到 fft_chan。
这样做是为了平衡 CPU 占用和刷新效果。对于实时屏幕来说,过高的处理频率并不一定有意义,反而会把计算资源浪费在肉眼难以分辨的刷新差异上。
显示部分集中在 src/main.c。这个模块的核心是把音频数据整理成适合视觉表达的形式。波形显示先计算当前帧的最大幅值和 DC Offset,再围绕零交叉点截取固定长度的波形窗口。这样做的目的是让每一帧波形都尽量从相近的相位位置开始,减少图像抖动,显示效果更接近示波器。频谱显示则把 FFT 幅值压缩到屏幕可用范围内,再通过峰值保持和柱状条显示出主频变化。显示线程只从 ZBus 读取结果,不会直接访问采集线程的内部缓冲。这一点体现了 Zephyr + ZBus 架构的好处:采集和显示彼此独立,哪一边更新频率变化,都不会直接破坏另一边的数据结构。
除了音频链路本身,显示链路里还有一个值得单独说明的实现,就是手动编写的 bitbang 驱动。这个驱动位于 src/spi_bitbang_fast.c,它不是为了替代 Zephyr 的通用 SPI 驱动,而是针对当前彩屏接口做的定制化加速方案。项目之所以要这样做,根本原因在于显示刷新的瓶颈在底层 SPI 发送速度和控制时序上。标准 SPI BITBANG 驱动虽然通用,但在这个项目里会多出一些额外抽象和开销,而屏幕只需要固定的 8/9 位时序、固定的片选控制和非常直接的字节流发送,bitbang 的实现根据屏幕时序要求直接翻转 GPIO。比如在 src/spi_bitbang_fast.c 中,msdk_spi_write_byte() 通过 GPIO2 的 CLK 和 MOSI 直接输出 9 个时钟周期,第一个周期先送 D/C 位,后 8 个周期按高位在前的顺序送出数据字节,驱动初始化时还根据 Maxim MSDK board.c 里的成熟硬件实现直接把 CS 和 CLK 拉到了空闲电平。
static ALWAYS_INLINE void msdk_spi_write_byte(uint8_t byte_val, bool is_data)
{
uint8_t tx_byte = byte_val;
for (int j = 0; j < 9; j++) {
MXC_GPIO2->out_clr = TFT_CLK_PIN;
if (j == 0) {
if (is_data) {
MXC_GPIO2->out_set = TFT_MOSI_PIN;
} else {
MXC_GPIO2->out_clr = TFT_MOSI_PIN;
}
} else {
if (tx_byte & 0x80) {
MXC_GPIO2->out_set = TFT_MOSI_PIN;
} else {
MXC_GPIO2->out_clr = TFT_MOSI_PIN;
}
tx_byte = tx_byte << 1;
}
msdk_delay();
MXC_GPIO2->out_set = TFT_CLK_PIN;
msdk_delay();
}
}
bitbang 驱动主要带来了两点改进。第一是屏幕刷新的确定性更好,尤其在频谱图频繁更新时,原来容易出现的卡顿和时序抖动明显减少;第二是它让 LVGL 的刷新周期更容易压到目标范围内,界面上波形和频谱的连续性更好,这部分工作不是为了炫技,而是直接服务于“让显示更快、更稳、更接近实时”的目标。
调试时先验证 Zephyr 设备是否识别成功,再验证 ZBus 是否能把音频数据从采集线程送到 DSP 线程,最后验证 LVGL 是否能按照定时器刷新显示。VS Code 主要用于编辑和构建,串口终端用于观察运行日志。
struct i2s_config config = {0};
config.word_size = SAMPLE_BIT_WIDTH;
config.channels = 2;
config.format = I2S_FMT_DATA_FORMAT_I2S | I2S_FMT_CLK_NF_NB;
config.frame_clk_freq = SAMPLE_FREQUENCY;
config.mem_slab = &i2s_rx_slab;
config.block_size = I2S_BLOCK_SIZE;
config.timeout = 1000;
MAX9867 的初始化配置里最值得注意的是 word_size、channels、format 和 frame_clk_freq 四个字段。word_size 对应每个采样点的位宽,channels 决定 I2S 一帧里有多少通道,format 则决定数据格式和时钟极性,frame_clk_freq 则直接决定采样率。也就是说,I2S 配置不是单独写一个采样率就结束了,而是要把位宽、通道和时序关系一起配齐,否则即使能读到数据,波形也可能错位、偏移或者幅值异常。
static const uint8_t init[][2] = {
{MAX9867_17_PWR_SYS, 0x00},
{MAX9867_05_SYS_CLK, 0x01 << MAX9867_PSCLK_POS},
{MAX9867_06_CLK_HIGH, MAX9867_NI_UPPER_16KHZ},
{MAX9867_08_DAI_FORMAT, MAX9867_MAS | MAX9867_DLY | MAX9867_HIZOFF},
{MAX9867_12_MIC_GAIN_LEFT, 0x14},
{MAX9867_13_MIC_GAIN_RIGHT, 0x14},
{MAX9867_17_PWR_SYS, 0xE3},
};
这段codec寄存器配置完全根据 MAX9867 数据手册的推荐流程进行,先关电源,再按寄存器顺序配置 MAX9867,设置系统时钟、采样率、DAI 格式和输入增益。

5. 功能展示图及说明
工程的界面部分采用上下分屏布局,上半部分显示时域波形,下半部分显示频域频谱。波形图先做零偏移修正再显示,这样能突出声音本身的振荡特征。频谱图采用柱状图形式,柱子高度随 FFT 幅值变化,同时叠加峰值保持点,让主频变化更直观。






6. 项目中遇到的难题及解决方法
项目过程中有一些比较明显的难点。第一是 I2S 数据格式和位宽要与编解码器配置完全对齐,否则读到的样本会出现幅值异常甚至通道错位;第二是音频输入增益不能一味调大,否则会削顶,太小又会淹没在噪声里;第三是显示端不能直接照搬原始样本,需要先做直流偏置修正、零交叉对齐和幅值压缩,否则波形和频谱都不够稳定。
解决这些问题要先在驱动层先保证时钟和接口格式正确,再在算法层做窗函数和统计量处理,最后在界面层做动态范围压缩和刷新节流。项目在调试中先看底层外设,再看线程和消息通路,最后看界面是否按预期刷新。
7. 心得体会
这个项目把音频采集、FFT 分析和图形界面结合在一起,既验证了 MAX32690EVKIT 在多外设协同方面的能力,也让我在调试嵌入式实时系统的过程中积累了比较完整的经验。它也让我更明确地认识到,让整个系统是否能在长期运行中保持可控、可查和可扩展,是嵌入式项目设计中最重要的目标之一。