
MCU 端音频关键词识别系统设计MFCC 特征提取与 CNN 推理全链路实现一、从麦克风电压到识别结果离网语音唤醒的信号处理链在电池供电的 IoT 设备中实现本地关键词识别Keyword Spotting, KWS面临的主要矛盾是DNN 推理需要较大的计算和内存资源而 MCU 平台只有百 KB 级别的 SRAM 和数十 MHz 的时钟频率。以Hey Sensor两个词的唤醒为例要求 24 小时不间断监听在 CR2032 纽扣电池下续航 6 个月以上。从信号处理角度看完整的 KWS 链路包含五个阶段PDM/I2S 音频采样通过数字 MEMS 麦克风以 16kHz 采样率采集音频预加重与分帧补偿高频衰减将连续音频流切分为 30ms 帧480 样本帧移 10ms160 样本MFCC 特征提取将每帧时域信号转换为 13 维或 40 维梅尔频率倒谱系数CNN/DS-CNN 推理对多帧 MFCC 特征做卷积分类后处理与去抖对连续推理结果做平滑和阈值判断其中 MFCC 特征提取是全链路中运算量最大的非推理环节在 Cortex-M4 80MHz 上40 维 MFCC 提取含 FFT、Mel 滤波、DCT约消耗 5ms。对于 100ms 的推理窗口10 帧MFCC 提取的总耗时约占推理周期的 50%是需要重点优化的环节。二、底层机制与原理深度剖析2.1 KWS 全链路信号流flowchart LR subgraph Audio[音频采集] A1[MEMS 麦克风br/PDM 输出] -- A2[PDM → PCMbr/抽取滤波] end subgraph Preprocess[预处理] B1[预加重br/H(z)1-0.97z⁻¹] -- B2[分帧br/帧长30ms, 帧移10ms] B2 -- B3[汉明窗br/w(n)0.54-0.46cos(2πn/N)] end subgraph MFCC[MFCC 特征提取] C1[FFTbr/N512点] -- C2[Mel滤波器组br/40个三角滤波器] C2 -- C3[Log能量br/ln(mel_energy)] C3 -- C4[DCTbr/13维MFCC] end subgraph CNN[CNN 推理] D1[DS-CNNbr/深度可分离卷积] -- D2[全连接层br/Softmax] end subgraph Post[后处理] E1[滑动窗口平滑br/5帧中位数] -- E2[双阈值去抖br/ON0.8, OFF0.3] end Audio -- Preprocess Preprocess -- MFCC MFCC -- CNN CNN -- Post Post -- Output[唤醒 / 非唤醒]2.2 MFCC 中 FFT 的定点和 CMSIS-DSP 加速MFCC 中最耗时的模块是 512 点 FFT。如果使用自己编写的浮点实现在 Cortex-M4 上约消耗 15ms。使用 CMSIS-DSP 库的定点 FFTarm_rfft_q15可将耗时降至约 2ms浮点 FFT 512 点 512/2 * log2(512) * (复数乘加) ≈ 2304 * 2 * 4 18432 次乘法 在 80MHz Cortex-M4单周期 MAC SIMD上约 18432/80M ≈ 0.23ms 的纯计算时间 实际耗时约 2ms额外的开销来自位反转索引、查旋转因子表、数据搬移 优化arm_rfft_fast_q15 使用 Cooley-Tukey 的基-4 分解减少约 25% 的乘法次数2.3 DS-CNN 在 MCU 上的算子映射KWS 模型通常使用深度可分离卷积Depthwise Separable CNN, DS-CNN来压缩模型尺寸。MobileNet 类模型在 224x224 图像上表现优异但在 1D 音频频谱40 频率通道 × N 时间帧上需要调整卷积核的 shape层类型图像2D音频1D输入224×224×340×32×140 频率通道32 帧Conv2D3×3 kernel, stride23×1 kernel, stride(2,1)DepthwiseConv3×33×1FullyConnected1024→N_class256→N_class关键区别音频的频谱维度频率和时间维度帧不应使用相同的 stride。在频率维度使用 stride2 做下采样类似图像的 2D downsampling在时间维度保持 stride1 以避免丢失时序信息。三、生产级代码实现与最佳实践/** * kws_mfcc.c — MCU 端 MFCC 特征提取实现使用 CMSIS-DSP * * 功能从 16kHz PCM 音频数据中提取 13 维 MFCC 特征 * 依赖CMSIS-DSP 库arm_math.h */ #include arm_math.h #include math.h /* MFCC 参数配置 */ #define SAMPLE_RATE 16000 /* 采样率 16kHz */ #define FRAME_LEN 480 /* 帧长 30ms 16kHz */ #define FRAME_SHIFT 160 /* 帧移 10ms 16kHz */ #define FFT_LEN 512 /* FFT 点数≥ FRAME_LEN */ #define NUM_MEL_FILTERS 40 /* Mel 滤波器数量 */ #define NUM_MFCC_COEFFS 13 /* 输出的 MFCC 系数维数不含 C0 */ /* Mel 滤波器组查找表 */ /* 40 个三角滤波器在 257 个 FFT bin 上的权重矩阵 * 由离线 Python 脚本生成编译时链接到 .rodata */ extern const float g_mel_filterbank[NUM_MEL_FILTERS][FFT_LEN / 2 1]; /* DCT 矩阵 */ /* 40×13 的 DCT 矩阵将 40 个 Mel 能量压缩到 13 维 MFCC */ extern const float g_dct_matrix[NUM_MFCC_COEFFS][NUM_MEL_FILTERS]; /* 预加重滤波状态 */ static float g_preemph_prev 0.0f; /* 上一帧最后一个样本 */ /** * brief 单帧 MFCC 特征提取 * * param pcm_frame 输入480 个 int16 PCM 样本 * param mfcc_out 输出13 维 MFCC 系数 (float32) * return 0: 成功, -1: 参数无效 */ int kws_mfcc_extract(const int16_t *pcm_frame, float *mfcc_out) { if (pcm_frame NULL || mfcc_out NULL) { return -1; } /* 工作缓冲区 */ static float32_t frame_float[FRAME_LEN]; /* 浮点化后的帧 */ static float32_t windowed[FRAME_LEN]; /* 加窗后的帧 */ static float32_t fft_input[FFT_LEN * 2]; /* FFT 输入实部虚部交错 */ static float32_t mel_energy[NUM_MEL_FILTERS]; /* Mel 能量 */ static arm_rfft_fast_instance_f32 fft_instance; /* FFT 实例 */ static bool fft_initialized false; /* 初始化 FFT 实例仅执行一次 */ if (!fft_initialized) { arm_rfft_fast_init_f32(fft_instance, FFT_LEN); fft_initialized true; } /* 步骤1: 预加重 (Pre-emphasis) * y[n] x[n] - 0.97 * x[n-1] * 补偿高频衰减提升高频成分的信噪比 */ frame_float[0] (float32_t)pcm_frame[0] - 0.97f * g_preemph_prev; for (int i 1; i FRAME_LEN; i) { frame_float[i] (float32_t)pcm_frame[i] - 0.97f * (float32_t)pcm_frame[i - 1]; } g_preemph_prev (float32_t)pcm_frame[FRAME_LEN - 1]; /* 步骤2: 汉明窗加窗 (Hamming Window) * w[n] 0.54 - 0.46 * cos(2πn / (N-1)) * 减少频谱泄漏 */ for (int i 0; i FRAME_LEN; i) { float window_val 0.54f - 0.46f * arm_cos_f32(2.0f * PI * i / (FRAME_LEN - 1)); windowed[i] frame_float[i] * window_val; } /* 步骤3: FFT (512 点实数 FFT) * CMSIS-DSP 的 RFFT 将结果以 {Re[0], Re[1], ..., Re[N/2], Im[N/2-1], ..., Im[1]} 排列 * 这是 CMSIS 特有的紧凑格式需要后续计算功率谱时注意索引 */ for (int i 0; i FRAME_LEN; i) { fft_input[i] windowed[i]; } /* 零填充帧长之后用 0 填充到 FFT_LEN */ for (int i FRAME_LEN; i FFT_LEN; i) { fft_input[i] 0.0f; } arm_rfft_fast_f32(fft_instance, fft_input, fft_input, 0); /* 步骤4: 功率谱计算 * power[bin] |X(bin)|² Re² Im² * 从 CMSIS RFFT 紧凑格式中提取实部和虚部 */ float32_t power_spectrum[FFT_LEN / 2 1]; power_spectrum[0] fft_input[0] * fft_input[0]; /* DC 分量虚部为 0 */ for (int k 1; k FFT_LEN / 2; k) { float re fft_input[k]; float im fft_input[FFT_LEN - k]; power_spectrum[k] re * re im * im; } power_spectrum[FFT_LEN / 2] fft_input[FFT_LEN / 2] * fft_input[FFT_LEN / 2]; /* 步骤5: Mel 滤波器组 * mel_energy[i] Σ power_spectrum[k] * filterbank[i][k] * 将线性频谱映射到 Mel 尺度人耳感知尺度 */ for (int m 0; m NUM_MEL_FILTERS; m) { float sum 0.0f; for (int k 0; k FFT_LEN / 2; k) { sum power_spectrum[k] * g_mel_filterbank[m][k]; } /* 防止 log(0)将极小值置为阈值 */ mel_energy[m] (sum 1e-10f) ? 1e-10f : sum; } /* 步骤6: Log 能量 * log_mel[m] ln(mel_energy[m]) */ for (int m 0; m NUM_MEL_FILTERS; m) { mel_energy[m] logf(mel_energy[m]); } /* 步骤7: DCT离散余弦变换 * mfcc[c] Σ log_mel[m] * cos(π * c * (m 0.5) / M) * c 1..13跳过 c0即能量系数 */ for (int c 0; c NUM_MFCC_COEFFS; c) { float sum 0.0f; for (int m 0; m NUM_MEL_FILTERS; m) { sum mel_energy[m] * g_dct_matrix[c][m]; } mfcc_out[c] sum; } return 0; } /** * brief KWS 推理对多帧 MFCC 特征进行分类 * * param mfcc_frames 输入NUM_FRAMES 帧 × 13 维 MFCC特征图 40×NUM_FRAMES×1 * param scores 输出N_CLASSES 维的分类得分 * return 得分最高的类别索引, -1 表示错误 */ int kws_infer(const float *mfcc_frames, float *scores) { /* 将 MFCC 特征拷贝到 TFLM 输入张量 */ TfLiteTensor *input g_interpreter-input(0); /* 输入形状: [1, NUM_FRAMES, NUM_MFCC_COEFFS, 1] * 实际按 (time, freq, channel) 排列即 NHWC 格式 */ if (input-type kTfLiteFloat32) { memcpy(input-data.f, mfcc_frames, NUM_FRAMES * NUM_MFCC_COEFFS * sizeof(float)); } else if (input-type kTfLiteInt8) { /* 量化模型需要做输入量化 */ int8_t *q_input input-data.int8; float input_scale input-params.scale; int32_t input_zero input-params.zero_point; for (int i 0; i NUM_FRAMES * NUM_MFCC_COEFFS; i) { int32_t q_val (int32_t)roundf(mfcc_frames[i] / input_scale) input_zero; q_input[i] (int8_t)(q_val -128 ? -128 : (q_val 127 ? 127 : q_val)); } } else { return -1; } /* 执行推理 */ TfLiteStatus status g_interpreter-Invoke(); if (status ! kTfLiteOk) { return -1; } /* 读取输出并找最大值 */ TfLiteTensor *output g_interpreter-output(0); int max_idx 0; float max_val -1e9f; if (output-type kTfLiteFloat32) { for (int i 0; i N_CLASSES; i) { scores[i] output-data.f[i]; if (scores[i] max_val) { max_val scores[i]; max_idx i; } } } else { /* int8 输出需要反量化 */ float output_scale output-params.scale; int32_t output_zero output-params.zero_point; for (int i 0; i N_CLASSES; i) { scores[i] (output-data.int8[i] - output_zero) * output_scale; if (scores[i] max_val) { max_val scores[i]; max_idx i; } } } return max_idx; }后处理与去抖/** * brief 唤醒去抖逻辑双重阈值 防抖计数 * * 策略 * - ON 阈值 0.8唤醒得分超过此值开始计数 * - OFF 阈值 0.3得分低于此值重置计数 * - 防抖窗口 5连续 5 帧超过 ON 阈值才确认唤醒 */ int kws_debounce(float score, int target_class) { static int consecutive_count 0; static const int DEBOUNCE_WINDOW 5; static const float ON_THRESHOLD 0.8f; static const float OFF_THRESHOLD 0.3f; if (score ON_THRESHOLD) { consecutive_count; if (consecutive_count DEBOUNCE_WINDOW) { consecutive_count 0; /* 重置计数器 */ return 1; /* 确认唤醒 */ } } else if (score OFF_THRESHOLD) { consecutive_count 0; /* 重置计数器 */ } /* 在阈值之间保持当前计数不做改变 */ return 0; /* 未达到唤醒条件 */ }四、边界分析与架构权衡MFCC 维数选择。13 维 MFCC 是语音识别的经典选择40 维 MFCC不含 DCT 降维提供更丰富的频谱信息但数据量增加 3 倍。对于 KWS 任务输入到 CNN 的是 40 维频谱 × N 帧的二维特征图使用 DCT 降维到 13 维后再拼帧会导致空间信息损失。工程实践中的权衡如果 CNN 的输入层有足够的感受野kernel size ≥ 5×3使用 40 维原始 Mel 能量而非 13 维 MFCC让网络自己学习最优的频率特征。CMSIS-DSP 的 FFT 实现选择。arm_rfft_fast_f32使用基-4 分解速度最快但需要较大的旋转因子表512 点约 2KB。arm_rfft_f32使用基-2 分解旋转因子表较小~1KB但速度慢约 30%。arm_rfft_q15使用定点计算无浮点运算单元FPU的平台上速度接近arm_rfft_fast_f32。流式处理的内存规划。10 帧的特征图占用10 × 40 × 4 1600 字节float32加上 Arena 缓冲区约 20KB总 SRAM 占用约 25KB。这个数字在 64KB SRAM 的芯片上可接受但在 32KB 的芯片上需要进一步压缩——使用 int8 MFCC 特征每个系数 1 字节将特征图从 1600 字节降到 400 字节。五、总结MCU 端音频关键词识别的全链路设计要点MFCC 提取使用 CMSIS-DSP 的定点 FFT 可显著降低计算耗时从浮点自实现的 15ms 降至 2ms。生产代码需要考虑预加重状态跨帧连续性、Mel 能量的 log(0) 防护。DS-CNN 模型设计音频频谱是 2D 特征图但时间维度不应下采样频率维度可使用 stride2 下采样。模型应针对目标芯片指令集、SRAM 大小做算子兼容性验证。后处理双重阈值 防抖窗口可大幅降低误唤醒率防抖窗口的大小需要在响应延迟和误唤醒率之间做折中窗口越大、延迟越高、误唤醒越低。功耗优化MFCC 每 10ms 执行一次帧移 160 样本CNN 推理每 100ms 执行一次10 帧为一个推理窗口。在帧间使用 MCU 的 Sleep 模式可将平均功耗从 15mA 降至 3mA。量产一致性不同芯片的 MEMS 麦克风频率响应差异可达 ±3dB需要在产线标定中采集参考音频并做频响补偿。这涉及将校准系数写入芯片内部 EEPROM 或 FLASH 的固定区域。