2026 ADI CodeFusion 竞赛 - 基于max32655实现关键词唤醒与命令词识别
该项目使用了MAX32650FTHR,实现了关键词唤醒与命令词识别的设计,它的主要功能为:关键词识别,三个关键词,“开灯”,播放等效,“关闭”,关闭等效,“发送”,通过蓝牙发送hello world。
标签
嵌入式系统
数字逻辑
开发板
一两风
更新2026-09-29
8

基于 MAX32655FTHR 的离线语音控制智能灯项目报告

一、选择完成的任务

关键词唤醒与命令词识别

把一块 MAX32655FTHR 开发板做成一个"对着说话就能控灯"的离线语音终端——不用联网,不用唤醒词,听到口令就亮/灭/换色,顺带通过蓝牙把"按键"事件推给手机。本项目不涉及外接 PCB 设计与打样,完全在 MAX32655FTHR 评估板 + MSDK + CodeFusion Studio 上完成,重点验证"边端 KWS(关键词识别)+ Cordio BLE 通知"这条链路在 Cortex-M4F + 128 KB SRAM 的资源约束下能否落地。

二、项目描述

整个系统的核心是"听 — 判 — 动"三段式:开发板上的数字麦克风持续拾音,MAX9867 音频编解码器把 PDM 转成 PCM,I2S + DMA 把 48 kHz 单声道音频搬进 RAM;主循环每 250 ms 切出一段 12000 样本(250 ms)的"切片"喂给 Edge Impulse 训练好的 TFLite Micro KWS 模型;模型输出 4 类概率(close / noise / open / send),主循环根据每类各自的阈值和上下文门控决定是否触发动作——开灯触发彩虹灯效,关灯熄灭 RGB,"发送"指令则通过 BLE Notify 推一条 "helloworld" 字符串到已配对的手机。和常见的"云端语音助手"路线不同,本项目把整条识别链路(特征提取 + 神经网络推理)都跑在 M4F 上,音频不会离开板子,也不需要任何网络栈。代价是模型必须做得很小——本项目用的 TFLite Micro int8 量化模型,arena 静态内存只有 6227 字节,加上 BLE 协议栈、麦克风双缓冲、KWS 切片缓冲,128 KB SRAM 已经被算到只剩几 KB 余量。整个项目的代码量并不大(自写 .c/.cpp 约 1500 行),但每一处资源分配都是反复权衡过的,这也是本项目最值得写进报告的地方。

三、简短的硬件介绍

本项目只用了一块开发板,没有外接任何器件,所有硬件都已焊在 MAX32655FTHR 上。

1. MAX32655FTHR 开发板(主控)

  • MCU: MAX32655,Cortex-M4F 内核,120 MHz,带 FPU、硬件 MAC、单精度 DSP 指令
  • 片上:512 KB Flash、128 KB SRAM、16 KB 指令缓存
  • 板载外设:板载三色 LED(红/绿/蓝,分别接到 GPIO 驱动)、一个用户按键(SW1,本项目未使用)、SWD 调试口、Micro-USB(J4)
  • 无线:板载蓝牙天线 + 蓝牙子系统,使用 Cordio BLE 协议栈

2. 板载数字麦克风 MP34DT01(PDM 拾音)

  • 顶部端口 MEMS 麦克风,单声道 PDM 输出
  • 直接焊在 FTHR 板上,通过板载走线连到 MAX9867 的数字输入口

3. MAX9867 音频编解码器(PDM→PCM 桥接)

  • 板载 I2C 地址 0x18
  • 内部把 PDM 麦克风数据流解调成 I²S 标准 PCM
  • 在本项目中配置为"BCLK/LRCLK 主模式",12.288 MHz 板载 MCLK 进来,产生 48 kHz LRCLK + 32× BCLK,直接驱动 MAX32655 的 I2S0 RX

4. 调试/供电:PC + Micro-USB 线(J4)

  • 供电 + 串口 console(115200 8-N-1)+ 固件烧录(SWD 通过板载 DAPLink)三合一
  • 调试期通过串口看 KWS 实时概率向量、推理状态、各类事件打印

5. BLE 接收端(可选):任意支持 BLE 4.0+ Notify 的手机 App(本项目用 nRF Connect 验证)。手机不是硬件清单,只是验证"发送"指令能成功 Notify 出去。

整个项目无需任何外接 PCB、无需杜邦线、无需额外供电,从 USB 上电到板子能听口令,只需要插一根 Micro-USB 线。
开发板实物图

板卡引脚图
max32655引脚图.png

MAX32655FTHR 顶部组件:


四、方案框图 + 设计思路

1. 总体方案框图


deepseek_mermaid_20260925_e3ef58.png

2. 设计思路

思路 1:边端 AI 优先,拒绝"云端 ASR"路线。对延迟和隐私都敏感——延迟高了用户觉得不跟手,语音上云又会引发用户顾虑。所以选 Edge Impulse + TFLite Micro 跑在 MCU 上,模型小、推理快、断网也能用。

思路 2:48 kHz 直接采样,不做降采样。MP34DT01 给的是 PDM,经过 MAX9867 解调出 48 kHz PCM。早先版本想降到 16 kHz 再训练模型,但降采样器占 0.1 KB 状态 + 多一截代码,反而拖累了维护性;直接把 48 kHz 切片(12000 样本/250 ms)喂给 v2 版本的模型,链路最简洁。代价是 DMA 缓冲 24 KB,SRAM 预算被卡得很紧。

思路 3:切片推理(Sliced Inference),不是全窗推理。EI v2 模型的 SLICE_SIZE=12000、SLICES_PER_MODEL_WINDOW=4——也就是说,模型本身就是按"每来 250 ms 切片就跑一次推理"设计的,1 秒的窗口由 4 个连续切片滑动组成。这样主循环可以 4 Hz 拿到结果,而不是 1 Hz,响应快 4 倍。

思路 4:KWS 必须配合"门控",不能裸跑模型。实测发现模型会出现两类误触发:一类是"幻影 close"(即使在安静环境下偶尔也会高置信度输出 close),另一类是"幻影 follow-up"(说一句"open"后,1 秒滚动窗口里残留的尾音被识别成 send)。所以加了 4 重门控(详见代码说明)——只有当"既高于阈值、又在语音上下文里、又在动作冷却期之外、又不是上一动作的同类别"时才真触发。

思路 5:本地反馈优先,BLE 通知其次。"open/close" 这种"灯控"语义靠 RGB LED 直接给用户反馈,不需要手机;"send" 这种"跨设备"语义才走 BLE Notify 推字符串。这条分工让本地响应零延迟,远程同步只在必要时发生。

五、原理图、PCB 设计

本项目使用 MAX32655FTHR 官方评估板,没有自绘原理图也没有 PCB 设计。所有硬件(MP34DT01、MAX9867、三色 LED、SW1、SWD、Micro-USB)都已焊在板上,只需要通过 J4 插 USB 即可工作。如果后续要做成产品形态,我会按以下思路出图:把 MAX32655 主控 + 24LC 外部 Flash(放 BLE bond 信息 + 模型固件) + 电源 LDO 集成到核心板,把 PDM 麦克风 + RGB LED 留在副板通过 FPC 连接,核心板预留 5 V 供电接口(接 12 V 适配器,内部 DC-DC 降到 3.3 V)。但本报告不展开,留作未来工作。

六、软件流程图 + 调试软件说明 + 关键代码说明

1. 主循环软件流程图


deepseek_mermaid_20260925_84e765.png

2. 调试软件说明

  • CodeFusion Studio (CFS):主 IDE。负责导入 MSDK 工程、配置 build target = MAX32655 FTHR_Apps_P1、生成 .elf/.hex/.bin、SWD 烧录、单步调试和寄存器查看。本项目所有源码编辑、编译、烧录都在 CFS 内完成。
  • unify_builder.exe:EIDE 自带的命令行构建器。在 CFS 内部构建卡住时,可以绕过 IDE 直接用命令行 unify_builder 触发 make,用于排错(EIDE 自身吃掉的日志看不到完整 gcc.mk 报错)。
  • MSYS 1.0 make + ARM GCC:CFS 底层调用的工具链。make 触发 MSDK 的 gcc.mk 完成编译,arm-none-eabi-objcopy 由 make release 目标产生 .hex/.bin。
  • 串口终端(115200 8-N-1):实时打印每 250 ms 的 KWS 4 类概率向量、HIT 标记、动作触发事件。这是调试"模型到底在想什么"和"门控为什么挡掉了这一帧"的唯一手段。
  • nRF Connect:验证 BLE 广播可见、连接成功、Notify 收到 "helloworld" 字符串。

3. 关键代码说明

(1) 麦克风数据通路(mic_capture.c)

MAX9867 工作在 48 kHz 主模式,BCLK = 32× LRCLK,I²S 16-bit 单声道。MAX32655 I²S0 RX 通过 DMA 半满回调(每 4096 样本 ≈ 85.3 ms)把数据搬进 s_buf0 / s_buf1 双缓冲,主循环每 tick 调用 mic_capture_tick() 把刚填好的半区拷进 s_slice 拼出 250 ms 切片。当累计样本数 ≥ 12000,mic_capture_slice_ready() 返回 1,mic_capture_slice() 返回只读指针给 KWS,KWS 处理完后调 mic_capture_slice_ack() 放行下一片。SRAM 预算:s_buf0(8 KB) + s_buf1(8 KB) + s_slice(24 KB) = 40 KB,在 128 KB 总 SRAM 中占了三分之一。

// mic_capture_tick() 核心:把半区拷进 s_slice 并推进写指针
memcpy(&s_slice[s_slice_fill], s_buf_active, MIC_HALF_SAMPLES * sizeof(int16_t));
s_slice_fill += MIC_HALF_SAMPLES;
if (s_slice_fill >= MIC_SLICE_SAMPLES) {
   s_slice_fill = 0;          // 288 样本余量丢弃(1.5%,模型可容忍)
   s_slice_ready = 1;
}

(2) KWS 推理封装(kws_inference.cpp)

EI 切片推理的关键是 signal_t 回调——模型不关心音频从哪来,只问"给我 offset 位置开始的 length 个 float"。这里通过 s_current_samples 静态指针 + kws_get_data() 回调把主循环传入的 int16 切片(乘 1/32768 转 float)送进模型。每调一次 run_classifier_continuous(),模型内部就把这 250 ms 切片算成 MFCC,塞进 1 秒滚动特征矩阵,做一次完整推理,返回 4 类概率。

int kws_classify(const int16_t *samples, size_t n_samples, float probs_out[4]) {
   s_current_samples = samples;     // 绑定本次输入
   s_current_n       = n_samples;
   signal_t signal = { EI_CLASSIFIER_SLICE_SIZE, &kws_get_data };
   ei_impulse_result_t result = {};
   EI_IMPULSE_ERROR err = run_classifier_continuous(&signal, &result, false);
   // ... 把 result.classification[0..3] 填到 probs_out ...
   return argmax(result.classification);  // 返回 0=close 1=noise 2=open 3=send
}

(3) 门控 + 动作(main.c::kws_react())

四重门控按优先级 WARMUP > COOLDOWN > CONTEXT 串行生效,任一关则打印标签但不动手。

// 1) 每类最小置信度
float kws_min_score[4] = { [0]=0.80f, [1]=1.0f, [2]=0.65f, [3]=0.80f };
int is_hit = (probs[idx] >= kws_min_score[idx]) && (idx != 1 /*noise*/);
​
// 2) warm-up:开机 3 秒内不动作(PLL 锁/AGC 收敛噪声会被错认)
// 3) 1 秒硬间隔(同帧内不会触发第二个动作)
// 4) 静音 re-arm:动作后必须看到 2 个连续 noise 切片(各 250 ms)才重新 armed
// 5) speech context:上一帧也必须不是 noise(单帧幻影过滤)
if (is_hit && !warmup && !cooldown && context) {
   if (idx == 2) ledfx_set(LEDFX_RAINBOW);   // open → 彩虹灯
   if (idx == 0) ledfx_set(LEDFX_OFF);       // close → 灭灯
   if (idx == 3 && ble_is_connected())       // send → BLE 推 "helloworld"
       ble_send_string("helloworld");
   s_act_tick = main_loop_ticks;              // 锁住,进入 re-arm
   s_act_idx  = idx;
}

(4) LED 效果机(ledfx.c)

6 步色环(R / R+G / G / G+B / B / B+R)= 红/黄/绿/青/蓝/品红,每步 200 ms,完整一周 1.2 秒。仅 RGB 三色灯硬件,用"三色 + 中间色"切换拿到 6 色调色板,无 PWM,瞬时切换。

七、实物演示及说明

外设进行配置
image.png

引脚进行配置

image.png
查看寄存器里面的内容
image.png

生成代码
image.png

使用CodeFusion Studio 进行编译

image.png

开发板实物图,正面

IMG_20260925_153951.jpg

反面

IMG_20260925_153951.jpg

实物演示,开发板连接蓝牙,和上位机。串口 console 实时打印各种关键词的概率,说出"open"时触发的开灯动作

image.png

说出"send"时触发的向蓝牙发送helloworld的动作

image.png

八、遇到的难点及解决方法

难点 1:Cordio BLE 占用大量 SRAM,KWS 跑不起来

  • 现象:首次链接 BLE 协议栈 + TFLM 推理后,EIDSP_OUT_OF_MEM(-1002)。
  • 根因:BLE 的 WSF heap 默认要占 ~30 KB,留给 TFLM arena + MFCC 特征矩阵的空间不够。
  • 解法:(a) 关闭 BLE 内部 trace,只保留 DM/app 层(TRACE=1);(b) 把 WSF_HEAP_SIZE 调到 0x10000(64 KB,够用);(c) KWS 切片缓冲从 32 KB 缩到 24 KB(去掉已被废弃的 16 kHz 兼容路径);(d) 静态 MFCC 状态变量由 EI 库内部用 .bss 承担,不再额外复制一份。

难点 2:48 kHz I²S 出来的全是"嗡——"的环境噪声,模型识别出"open/send"

  • 现象:刚上电前 1 秒,模型高置信度输出"open"或"send",但实际上是 PLL 锁定 + AGC 收敛时的电子噪声。
  • 根因:开机瞬间硬件还没稳定。
  • 解法:加 KWS_WARMUP_MS=3000,在第一次 KWS 推理开始的 3 秒内只跑分类、打印概率,但禁止任何动作(标签 WARMUP)。3 秒 ≈ 2 个完整 1 秒窗口,足够所有模拟前端稳定下来。

难点 3:说一次 "open" 后,板子会立刻再触发一次 "send"

  • 现象:用户感觉"灯开了之后手机莫名其妙收到 helloworld"。
  • 根因:EI 滚动窗口的 1 秒设计——"open"的尾音还留在窗口里,被模型错认成下一个命令。
  • 解法:经过 4 轮迭代(同帧冷却 → 同类别冷却 → 静音 re-arm → 1 秒硬间隔 + 上一帧上下文门控),最终用 "v18.8" 的组合策略稳定:动作后 1 秒内任何类别不响应 + 必须看到 ≥ 2 个连续 noise 切片才重新 armed + 上一帧也必须不是 noise。

难点 4:MP34DT01 是 PDM 接口,MAX32655 没有原生 PDM 外设

  • 现象:看手册以为能直连,结果 MCU 只支持 I²S。
  • 根因:PDM 到 PCM 的转换必须在外部完成。
  • 解法:利用板载的 MAX9867 codec——它内部有 PDM 接收器,把 PDM 麦克风数据流解调成 I²S PCM,直接接到 MAX32655 的 I²S0 RX。Codec 配成 BCLK/LRCLK 主模式,MCLK 12.288 MHz 进来后产生 48 kHz LRCLK,匹配 MSDK 的 I²S0 EXTERNAL_SCK_EXTERNAL_WS 模式。

难点 5:MSDK gcc.mk 在 conan 缓存里被修改后,conan 重装就回滚

  • 现象:build 跑得通,但第二天 make 又报"找不到 gcc.mk 的 patch"。
  • 根因:conan 把 MSDK 装到 %LOCALAPPDATA%\com.analog.cfs\Data\packages\conan\p\msdkbe2c3e067e70f\p 路径,conan 重装会覆盖该目录。
  • 解法:在 patch 前先 cp gcc.mk gcc.mk.v1.bak 留底,改坏或被回滚后从 .bak 一键还原;project.mk 内对 conan 路径的引用写成 MSYS 形式(/c/Users/...)而非 Windows 形式,绕开 MSYS 1.0 abspath() 对 C:/ 前缀的解析 bug。

难点 6:Boot banner 偶尔尾巴丢失("v1<garbage>KWS: init ok")

  • 现象:printf("... v18.10 ...\n") 输出在串口终端显示成"v1"后面接一串乱码。
  • 根因:ble_init() 内部会调 MXC_UART_Init 复位 UART 的 TX FIFO,banner 还没排完就被冲掉。
  • 解法:banner 后插 50 ms 延时(MXC_DELAY_MSEC(50)),让 ~50 字节 @ 115200 baud 完整排空后再启动 BLE。50 ms 是理论 4.3 ms 所需时间的 10 倍余量,稳。

九、对本次活动的心得体会

1. 边端 AI 真正落地,128 KB SRAM 是真实的"红海"。 本项目最有价值的部分,不是 TFLite Micro 怎么调,也不是 Edge Impulse 怎么训练模型,而是"在 128 KB SRAM 里把 BLE + 麦克风链路 + MFCC 特征矩阵 + TFLM arena + 切片缓冲全部塞进去还能稳定运行"这件事。每省 1 KB 都是显式取舍——把切片缓冲从 32 KB 砍到 24 KB,就是为了给 TFLM 让出空间; 把 kissfft 换成 CMSIS-DSP FFT,因为 kissfft 的 twiddle 表占 ~41 KB 放不下。这种"在硬件红线内反复搬砖"的体验是云端 AI 工程师很难体会的,也是 Cortex-M + 边端 AI 真正的魅力。

2. 离线 KWS 必须配套"防误触发策略",模型只是基础不是全部。 任何真实部署的语音控制产品,误触发率都比识别率重要——用户能接受"说了两遍才识别",但不能接受"安静的时候灯突然自己亮"。本项目经历了 v18.1~v18.8 共 8 个版本的策略迭代才稳定,这一路的踩坑经验比模型本身还宝贵。

3. 工具链的稳定性比想象中重要。 整个项目 60% 的时间花在 MSDK 构建系统(gcc.mk 路径、conan 缓存回滚、MSYS shell 兼容性、make vs make release 区别)上,真正写业务代码的时间不到 20%。CodeFusion Studio / EIDE / conan 这套链对国内开发者来说还不够成熟,文档也不全,需要边踩边总结。如果硬禾 / 贸泽未来还能继续办这个比赛,强烈建议把构建系统做成"开箱即用"的预打包 image,参赛者 clone 下来直接 make 就能跑,而不是花一两周解 conan + gcc.mk 谜题。

4. 个人收获。 这次活动让我第一次把"嵌入式 + 边端 AI + BLE 协议栈"完整跑通了一遍。从最开始的"连 BLE 广播都发不出来",到最后"对着板子说话就能远程推字符串到手机",那种端到端联调成功的感觉,比单纯刷 LeetCode 有趣得多。也让我对 ARM Cortex-M 的中断/缓存/DMA 行为、I²S 时序、Cordio WSF 调度器都有了更深的理解——这些都是看再多书也学不到的,必须实际跑过、踩过坑、查过寄存器才能内化。最后感谢硬禾科技提供这次机会。


附件

由于文件太大,只能放在百度网盘,链接如下:
通过网盘分享的文件:max32655_audio_recognize.zip

链接: https://pan.baidu.com/s/1Ko9Q-4v8Izn3A6IeCgkKiQ?pwd=vg8e 提取码: vg8e

团队介绍
孤独的旅行者
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号