(任务说明:使用选购的MAX32655FTHR开发板挑战任务六。但调试过程中发现MAX32655因SRAM仅128KB,不足以满足48kHz高采样率下的双音频缓冲buffer的要求,故转而使用了自备的MAX32690开发板完成任务。)
MAX32690 + MAX9867 助听器项目报告
1. 项目概述
本项目基于 ADI MAX32690 EV Kit 和 MAX9867 音频编解码器,实现一个 16 频带并行 IIR 助听器演示系统。MAX9867 负责模拟音频输入、模数转换、数模转换和耳机输出,MAX32690 M4 核负责 DMA 音频搬运、滤波和处方曲线切换。
工程同时保留了双核同步项目的 RISC-V 工程结构,但当前音频处理集中在 M4 核上。开发板上的TFT屏幕和外部 Flash 不参与助听器运行,以减少无关驱动和构建。
(注:MAX32655因SRAM仅128KB,较为紧张,因此采用MAX32690进行验证,本文中所有流程均对MAX32655同样适用。项目源码和算法原型scripts见附件:code.zip)
1.1 项目背景
本人早年因药物影响造成中度听力损伤,而且听力衰减随频率升高而加重,高频部分的听觉补偿需求更加明显。为了把听力损失曲线、滤波器响应和实际听感联系起来,项目选择先在 Python 中验证算法,再移植到嵌入式硬件上进行主观试听。
MAX32655 和 MAX32690 开发板都具备适合实时音频处理的 MCU 算力,并配有 MAX9867 音频编解码器,可以完成音频输入、输出以及 I2C/I2S 控制。因此,本项目尝试利用这些开发板为个人听力情况制作一个可调处方曲线的助听器验证平台。项目目标是验证滤波器结构、补偿曲线、实时处理能力和输出链路,属于个人实验和算法原型验证。
项目所用的ADI MAX32690 EV Kit 开发板如下图:

2. 算法设计来源
算法原型位于 alg_prototype 目录,主要由两个 Python 脚本组成:
HearingAidCrossover.py用于完整的算法探索。它生成 16 个频带,对粉红噪声进行分频、增益补偿、合成,并绘制听力损失、滤波器响应和时域结果。generate_curves.py从相同的滤波器设计思路中整理出批量导出流程,分别生成 Butterworth、Chebyshev I 型和椭圆滤波器的 SOS 系数、处方曲线 JSON 文件和频率响应数据,供后续固件移植使用。
两个脚本采用相同的基本结构:每个频带由一个高通滤波器和一个低通滤波器级联形成带通响应,频带边界按对数方式分布在 50 Hz 到 Nyquist 频率附近。每级滤波器为二阶,使用 SOS 形式保存,以改善数值稳定性。
2.1 模拟听力损失
下图用 16 个频带中心频率表示本人实测的听力损失,并给出对应的目标补偿增益。高频段的损失更大,表示高频听力下降更厉害。

图中的听力损失数组为:
[10, 10, 10, 12, 15, 20, 25, 35, 35, 40, 45, 50, 55, 60, 65, 65] dB
2.2 IIR 滤波器响应和目标曲线
下图中,彩色细曲线表示各个频带的 IIR 响应,合成蓝色曲线表示各频带叠加后的结果,红色虚线表示目标补偿曲线。两者之间的差距来自有限阶数滤波器、频带之间的过渡区以及各频带响应叠加方式。

该图由原型脚本的默认参数生成:Butterworth、二阶滤波器、0.7 倍听力损失补偿。
当前固件中的 hearing_aid.c 使用运行时生成的简化双二阶滤波器,三条曲线通过不同 Q 值和增益比例实现。Python 导出的精确 SOS 系数尚未完全替换进固件,这是后续校准工作的重点。
2.3 IIR 算力估算与可行性
当前算法使用 16 个并行频带。每个频带由一个二阶高通和一个二阶低通组成,因此每个声道共有:
16 个频带 × 2 个二阶 IIR = 32 个二阶 IIR
固件中的一个二阶 IIR 计算形式可以写成:
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
每个输入样本需要 5 次浮点乘法和若干次加减法。按 MAC 近似计算,单声道负载为:
32 个二阶 IIR × 5 MAC = 160 MAC/样本
160 MAC/样本 × 48,000 样本/秒 = 7.68 MMAC/秒
每个频带滤波结果还要乘以该频带的处方增益。计入 16 次增益乘法后,单声道约为 176 次浮点乘法/样本,即约 8.45 million 次浮点乘法/秒。累加 16 个频带的加法、IIR 状态更新、限幅和 DMA 中断管理后,实际指令量会高于这个 MAC 理论值。
如果左右声道独立处理,理论滤波负载约为:
160 MAC/样本/声道 × 2 声道 × 48,000 样本/秒
= 15.36 MMAC/秒
当前固件正常模式暂时处理左声道并复制到左右输出,因此实际滤波负载接近单声道估算;左右独立处理完成后才会达到立体声估算值。
MAX32690 M4 工作在 120 MHz。仅从理论 MAC 数看,立体声的 15.36 MMAC/秒约占 120 MHz 的 12.8%,为 DMA、浮点运算、状态维护和串口诊断保留了较大空间。实际浮点 IIR 还受到加载/存储、流水线、函数调用和缓存命中率影响,因此最终以板上测得的 DMA 回调时间为准。当前开启指令缓存后,实测 16 频带浮点处理的音频忙时约为 49%,仍低于一个 48 kHz 音频块的实时预算,说明该方案在 MAX32690 上具备实时运行可行性。
3. 软件处理流程
3.1 总体流程

3.2 初始化流程
- M4 核选择 120 MHz IPO 时钟,开启指令缓存和定时器。
- 解锁 GPIO 配置,重新初始化按键和 LED,使 I2C2、I2S 的运行时引脚配置生效。
- 初始化 I2C2,速率为 400 kHz,并通过有界事务访问 MAX9867。
- MAX9867 使用 12.288 MHz MCLK,配置为 48 kHz LRCLK 和 2.304 MHz BCLK。
- 配置 24 bit、立体声 I2S,MAX9867 作为时钟控制端,MAX32690 使用外部 SCK/WS。
- 启动 I2S RX/TX DMA。正常模式下,RX 完成的缓冲区经过处理后交给 TX;测试模式使用独立的旋律 TX 缓冲区。
3.3 正常模式音频处理
正常模式下,DMA 每完成 64 个 32 bit 音频字触发一次回调,对应 32 个立体声采样帧,理论处理预算约为 666 μs。每个输入样本依次经过 16 个并行频带,每个频带包含高通和低通两个二阶 IIR,然后乘以当前处方曲线的频带增益,最后求和并限制在约 ±0.98 范围内。
SW2 按键通过消抖后提交曲线切换请求,切换在 DMA 块边界执行,三条曲线按 0 → 1 → 2 → 0 循环。
当前固件的正常模式处理左声道样本,并将结果复制到左右输出声道。左右独立处理仍是后续需要完成的功能。
3.4 关键参数
参数 | 当前值 | 说明 |
|---|---|---|
MCU | MAX32690 | M4 音频处理核,RISC-V 保留双核同步工程 |
核心时钟 | 120 MHz | IPO 时钟 |
音频采样率 | 48 kHz | MAX9867 NI = |
音频格式 | 24 bit、立体声 | I2S 左对齐、MSB 优先 |
BCLK | 2.304 MHz | 48 个 BCLK 每个立体声帧 |
MCLK | 12.288 MHz | MAX9867 外部主时钟 |
I2C | I2C2,400 kHz | MAX9867 控制接口 |
频带数 | 16 | 并行 IIR 频带 |
频带范围 | 50 Hz 至约 24 kHz | 对数间隔,最高边界低于 Nyquist |
单级阶数 | 2 | 每个频带一个高通和一个低通 |
DMA 块大小 | 64 个 32 bit 字 | 32 个立体声帧 |
状态输出周期 | 1 s | 输出 DMA 计数、采样率和 CPU 占用 |
当前版本提供输出限幅,但AGC(自动增益)尚未接入。MAX9867 的 DAC 增益、播放音量和静音状态已经在测试模式中配置。
3.5 关键步骤代码实现
流程图中的音频链路主要由 fha_max32690/m4/src/main.c、hearing_aid.c 和 max9867.c 实现。下面按照初始化、采集、滤波、输出和曲线切换的顺序,介绍正常模式下的关键代码。代码片段摘自当前工程,部分调整了排版或省略了外围代码;它们需要结合原文件中的变量定义和初始化逻辑阅读。
3.5.1 初始化编解码器和 I2S 接口
main.c 中的 init_audio() 先建立 I2C 控制通道,再启动 MAX9867 的播放和录音路径。以下节选保留了正常模式的主要配置调用:
check_init(MXC_I2C_Init(MXC_I2C2, 1, 0), "I2C2");
MXC_I2C_SetTimeout(MXC_I2C2, 100000U);
MXC_I2C_SetFrequency(MXC_I2C2, 400000);
check_init(max9867_init(MXC_I2C2, 12288000, 1), "MAX9867");
check_init(max9867_enable_playback(1), "playback enable");
check_init(max9867_playback_volume(6, 6), "playback volume");
check_init(max9867_enable_record(1), "record enable");
check_init(max9867_adc_level(-12, -12), "ADC level");
check_init(max9867_linein_gain(24, 24), "Line-In gain");
max9867_init() 的最后一个参数为 1,表示由 MAX9867 提供音频位时钟和帧时钟。驱动在 12.288 MHz MCLK 下写入 NI = 0x6000 和 BSEL = 0x02,得到 48 kHz 采样率和每帧 48 个 BCLK。max9867_enable_record(1) 选择双声道 Line-In 送入 ADC,同时关闭 Line-In 直接混入耳机输出的模拟通路,使输入声音经过 MCU 处理后再播放。
MCU 端的 I2S 配置与编解码器相对应,以下为 request 的关键字段:
request.wordSize = MXC_I2S_WSIZE_WORD;
request.sampleSize = MXC_I2S_SAMPLESIZE_TWENTYFOUR;
request.bitsWord = 24;
request.adjust = MXC_I2S_ADJUST_LEFT;
request.justify = MXC_I2S_MSB_JUSTIFY;
request.wsPolarity = MXC_I2S_POL_NORMAL;
request.channelMode = MXC_I2S_EXTERNAL_SCK_EXTERNAL_WS;
request.stereoMode = MXC_I2S_STEREO;
request.bitOrder = MXC_I2S_MSB_FIRST;
这里的外部 SCK/WS 表示 MCU 跟随 MAX9867 的时钟。每个有效样本为 24 bit,在 DMA 内存中以 32 bit 字保存并左对齐,因此后续处理需要进行移位和符号恢复。request 在源码中声明为 static,因为 SDK 后续配置 DMA 时仍会使用该结构体。外围初始化还包括临时解锁 GPIO、开启指令缓存和启动计时器;check_init() 则在初始化失败时打印出错阶段并停止启动音频。
3.5.2 DMA 双缓冲与音频块接收
main.c 将接收数组分成两个块,每块保存 64 个 32 bit 字,即 32 个左右声道采样帧:
#define AUDIO_CHANNELS 2U
#define DMA_WORDS 64U
#define DMA_BYTES (sizeof(uint32_t) * DMA_WORDS)
static uint32_t rx[DMA_WORDS * 2U];
static uint32_t *next_rx = rx;
/* 正常模式下的发送缓冲指针。 */
static uint32_t *tx_buffer = rx + DMA_WORDS;
启动时,RX DMA 接收至前半块,TX DMA 从后半块发送初始静音数据。收到 RX 完成回调后,dma_cb() 保存刚接收完成的块地址,并立即把下一次接收切换到另一半缓冲区:
done = next_rx;
tx_buffer = done;
next_rx = (next_rx == rx) ? rx + DMA_WORDS : rx;
MXC_DMA_ReleaseChannel(channel);
rxch = MXC_I2S_RXDMAConfig(next_rx, DMA_BYTES);
if (rxch < E_NO_ERROR) {
++dma_errors;
last_dma_error = rxch;
fatal = 1;
return;
}
随后对 done 指向的块原地进行滤波和 PCM 回写。TX 完成分支释放旧通道后,通过 MXC_I2S_TXDMAConfig(tx_buffer, DMA_BYTES) 配置后续发送。这样,采集、处理和输出围绕两个音频块循环推进。48 kHz 下每块的时间为 32 / 48000 ≈ 666.7 μs,处理和缓冲交接必须满足这一实时预算;这个数值表示块周期,并不等于整条音频链路的端到端延迟。
3.5.3 建立 16 频带滤波器和处方增益
hearing_aid.c 为三条曲线分别保存滤波器系数、历史状态和频带增益。每个频带先通过高通,再通过低通,形成带通响应。初始化函数整理排版后如下:
void hearing_aid_init(void)
{
unsigned t, b;
const float qs[3] = {0.7071f, 0.9f, 1.2f};
const float ratios[3] = {0.5f, 0.7f, 1.0f};
for (t = 0; t < 3; t++) {
for (b = 0; b < NB; b++) {
design(&f[t][b][0], edges[b], 1, qs[t]);
design(&f[t][b][1], edges[b + 1], 0, qs[t]);
gain[t][b] = powf(10.0f, loss[b] * ratios[t] / 20.0f);
}
}
}
其中 NB = 16,edges 是频带边界,loss 是听力损失数组。design() 的第三个参数用 1 表示高通、0 表示低通,并在设计时将系数除以 a0 进行归一化,同时清零滤波器历史状态。
增益按 10^(听力损失 × 补偿比例 / 20) 从 dB 转为线性幅度。例如某频带损失为 20 dB,补偿比例为 0.5,则目标补偿为 10 dB,对应约 3.16 倍幅度。三条曲线当前同时改变 Q 值和补偿比例,尚不是 Python 中三种滤波器精确 SOS 系数的直接移植。
3.5.4 逐样本 IIR 运算、频带合成和限幅
每个二阶 IIR 的运算由 run() 完成。结构体 biq_t 除保存系数外,还保存前两个输入和输出,因此滤波状态会延续到下一个样本及下一个 DMA 块:
static float run(biq_t *q, float x)
{
float y = q->b0 * x + q->b1 * q->x1 + q->b2 * q->x2
- q->a1 * q->y1 - q->a2 * q->y2;
q->x2 = q->x1;
q->x1 = x;
q->y2 = q->y1;
q->y1 = y;
return y;
}
float hearing_aid_process(float x)
{
float y = 0, z;
unsigned b;
for (b = 0; b < NB; b++) {
z = run(&f[curve][b][0], x);
z = run(&f[curve][b][1], z);
y += z * gain[curve][b];
}
if (y > 0.98f) y = 0.98f;
if (y < -0.98f) y = -0.98f;
return y;
}
16 个频带都接收同一个输入样本 x,各自完成高通、低通和增益补偿,再将结果相加。最后的 ±0.98 硬限幅用于限制数字输出峰值;它没有包络检测、启动/释放时间或压缩比,尚不具备 AGC 或动态压缩器的功能。
3.5.5 24 bit PCM 与浮点样本转换
滤波器处理归一化浮点数,而 I2S DMA 搬运整数 PCM,因此 dma_cb() 在调用算法前后完成格式转换:
for (unsigned i = 0; i < DMA_WORDS; i += AUDIO_CHANNELS) {
int32_t sample = (int32_t)done[i] >> 8;
float output = hearing_aid_process((float)sample / 8388608.0f);
int32_t pcm = (int32_t)(output * 8388607.0f);
if (pcm > 8388607) {
pcm = 8388607;
}
if (pcm < -8388608) {
pcm = -8388608;
}
done[i] = (uint32_t)pcm << 8;
done[i + 1U] = done[i];
}
输入先转换为有符号 32 bit 数,再在当前 ARM 工具链下通过算术右移恢复有符号 24 bit 样本,除以 2^23 后交给滤波器。输出缩放并限制在 24 bit PCM 范围内,再左移 8 位恢复 DMA 格式;左移前转换成无符号数,避免对负有符号整数左移产生未定义行为。
循环每次跨过两个字,只处理左声道,再将结果复制到右声道。因此当前听到的是同一处理结果的双声道输出,尚未实现左右声道独立滤波。
3.5.6 在音频块边界切换处方曲线
SW2 回调通过按键状态检查和 100 ms 消抖后,仅递增 curve_requests,不直接修改滤波器正在使用的曲线。RX DMA 回调在逐样本处理之前统一应用请求:
requests = curve_requests;
while (applied_curve_requests != requests) {
hearing_aid_next_curve();
++applied_curve_requests;
}
active_curve = hearing_aid_curve();
实际曲线切换函数位于 hearing_aid.c:
void hearing_aid_next_curve(void)
{
curve = (curve + 1) % 3;
}
这种方式使一个音频块内的样本使用同一条曲线,避免按键事件在处理中途改变参数。当前实现保留各条曲线原有的滤波状态,未加入切换时的交叉淡化,因此块边界切换本身并不保证没有瞬态声音。
主循环每秒调用状态输出逻辑,报告接收帧率、DMA 错误数、平均处理时间和峰值时间。cpu_avg 根据 DMA 中断累计忙时与实际经过时间的比值计算(包含回调中的滤波处理,但不包含前台串口打印和轮询开销),可用于观察音频任务是否接近实时。
4. CodeFusion Studio 开发和 Debug 记录
本章用于记录 CodeFusion Studio 的工程配置、编译、下载和调试过程。
4.1 工程和工具链
可以CodeFusion Studio中使用图形化界面配置各个外设、引脚和关键寄存器的值,如下图:

4.2 Build 配置
可以CodeFusion Studio中进行构建、清除、下载、调试等一条龙操作,如下图:

4.3 Debug、下载和串口观察
可以CodeFusion Studio中借助调试器进行常规的debug操作,并可使用串口观察输出。如下两图:


5. 主要问题和调试过程
5.1 MAX9867 I2C 初始化失败
最初使用 SDK 提供的阻塞式 I2C API 初始化 MAX9867 时,串口反复报告 error=-9,初始化停在音频启动阶段。进一步检查发现,SDK 的 STOP、TX/RX FIFO 清理和异常处理路径包含无界轮询;当总线状态没有按预期完成时,驱动可能 长时间等待,或者返回一个难以区分具体原因的错误码。
调试时先对照已经验证过的 I2S_DMA_Target 工程,确认硬件连接、MAX9867 地址和 I2C 实例均正确,然后在本项目中增加有界 I2C 事务状态机。每次事务都 记录起始时间,在超时、NACK、仲裁错误或 STOP 异常时主动结束事务,并打印操作 类型、寄存器地址、I2C 标志寄存器和主控制寄存器。通过链接器 --wrap,项目 自己的安全实现替换了 SDK 中相关的 MXC_I2C_Stop、FIFO 清理函数。
最后将 I2C2 配置为 400 kHz,并在 MAX9867 初始化前设置软件超时。这样既保持 了芯片支持的最高标准速率,也避免外设异常导致整个音频初始化卡死。
5.2 GPIO 配置锁导致 I2C/I2S 引脚未生效
CodeFusion Studio 生成的早期启动代码会在初始化 UART 后锁定 GPIO 配置。I2C 和 I2S 驱动随后需要重新配置复用功能,但 GPIO 配置接口在锁定状态下会静默 忽略请求。因此,串口和部分基础外设看似正常,MAX9867 或 I2S 却无法正常工作。
定位过程包括打印 GPIO 配置锁状态、检查 I2C/I2S 初始化后的引脚复用寄存器, 并与锁定前能正常运行的音频示例进行对比。修复后,应用启动时先保存原锁状态, 临时解锁 GPIO,重新初始化 SW2 和 LED,再让 I2C2、I2S 完成音频引脚配置。所有音频外设初始化完成后恢复原来的 GPIO 锁,避免运行期间被其他代码误修改。
6. 展望
6.1 后续工作
- 将
generate_curves.py生成的 Butterworth、Chebyshev I 型和椭圆滤波器 SOS 系数直接导入固件,并与当前运行时设计结果比较。 - 按左右声道分别维护 16 个频带的状态变量,实现真正的 L/R 声道独立处理。
- 增加可配置压缩器或 AGC,包括启动时间、释放时间、阈值、压缩比、最大输出增益和峰值保护。
- 校准 MAX9867 的麦克风/Line-In 输入路径、耳机输出模式和模拟增益,确认旋律与实时声音均可稳定听到。
- 增加离线 Python 与固件之间的响应对比测试,覆盖频率响应、增益误差、溢出和长时间 DMA 稳定性。
6.2 应用场景
该平台适合作为助听器算法原型验证板,用于:
- 不同听力损失曲线和增益的主观试听;
- Butterworth、Chebyshev 和椭圆 IIR 结构的音质比较;
- 实时压缩、AGC 和限幅算法验证;
- 低功耗音频 DMA、编解码器配置实验;
- 后续扩展到耳机增强、语音清晰度改善、听力研究和嵌入式音频教学。
注意:本项目仍属于演示和算法验证平台。若要作为正式的助听器,还需要进行声学标定、反馈抑制、延迟、功耗等方面的验证。