
简介这是基于STM32微控制器的孤立词语音识别完整工程适合嵌入式入门开发者、物联网工程师以及语音识别爱好者可在智能家居、简单指令控制等场景中直接借鉴。压缩包共165个文件核心为53个C语言源文件、50个头文件和13个WAV音频样本另有Keil的uvproj/uvopt工程配置、MD说明文档及批处理脚本包体只有2.03MB结构紧凑、容易阅读。目前已有331人学习。内容覆盖音频采集、预处理、特征提取、模型匹配与识别输出五个环节代码中包含STM32的ADC采集、定时器、Flash读写、SD卡存储等外设配置也给出训练用的音频素材和交叉编译脚本。导入Keil MDK后即可定位识别流程并二次开发。对想在低成本MCU上落地离线语音控制、或学习嵌入式信号处理的开发者来说这是一份轻量而完整的参考样例。1. 在 STM32 上做孤立词语音识别先别急着上神经网络手里有一颗 STM32F407 或者 F103想让它听懂几个固定词比如“开灯”“关灯”“前进”“停止”——大多数工程师第一反应是跑个轻量级神经网络然后发现 Flash 和 RAM 双双告急。实际上孤立词语音识别在 MCU 上早有一套成熟且够用的方案核心是 MFCC 特征提取加 DTW动态时间规整模板匹配几百行 C 代码就能跑起来识别 10 个词以内的指令集不需要外部语音芯片也不需要联网。这套做法解决的是“本地、离线、低延迟”的语音控制需求特别适合智能家居节点、车载辅助开关、玩具和工业手持设备。它和“听懂任何话”的连续语音识别完全是两码事孤立词意味着每次只说一个词词的边界要能检测到模板库里每个词预存几组特征实时提取特征后和库里逐条做距离计算。适合的人群是正在做单片机毕设、嵌入式产品预研或者想把手头开发板玩出语音能力的开发者。下面按“原理—实现—训练—进阶”的顺序把整条路径讲透。2. 孤立词语音识别在 MCU 上的理论基座MFCC 为什么够用2.1 从时域波形到特征序列的必经之路麦克风采集到的是一维时域信号STM32 的 ADC 以 8kHz 或 16kHz 采样得到的是幅度随时间变化的波形。直接用波形做匹配的问题是同一词说快说慢、声音大小不同波形差别巨大。所以要先做特征提取把一帧语音通常 20~30ms压缩成一组能代表音色特征的向量MFCCMel 频率倒谱系数至今仍是嵌入式语音识别的主流选择。MFCC 的计算链路是预加重 → 分帧加窗 → FFT → Mel 滤波器组 → 取对数 → DCT。整条链路在 STM32 上最重的部分是 FFT若采样率 8kHz、帧长 256 点一次 256 点 FFT 在 F407 上只需要几十微秒级CMSIS-DSP 库的实数 FFT 跑得很快整个特征提取一帧大概几毫秒实时性完全可以接受。2.2 DTW 匹配解决“说得快与慢”的老牌算法特征序列长度是变化着的——同一个人说同一个词每次发音时长不同帧数就不同。欧氏距离要求向量长度相等没法直接用。DTW 的思路是把两个序列在时间轴上做非线性伸缩找到一条对齐路径使累计距离最小。对孤立词识别来说DTW 的准确率在词表小、说话人相对固定的场景下并不输给简单神经网络而且极省资源。DTW 的核心是动态规划填表。记模板特征序列为T[1..M]待识别特征序列为R[1..N]累计距离D[i][j] d(T[i], R[j]) min(D[i-1][j], D[i][j-1], D[i-1][j-1])。最终距离是D[M][N] / (MN)做长度归一化。遍历完模板库后取最小距离对应的词为识别结果若最小距离仍大于阈值则判定为“不在词表内”。2.3 内存账算清楚才知道 F103 和 F407 的差距一张 DTW 表格的大小是M×N。假设模板平均 40 帧待识别语音 40 帧float数组就是40×40×4 6400字节这不占空间。问题是模板库要常驻内存10 个词、每个词 3 组模板、每组约 40 帧、每帧特征维度 12那么是10×3×40×12×4 57600字节。F103 的 RAM 只有 20K 起步放不下而 F407 有 192K很宽裕。所以硬件选型有个基本判断词表小于 5 个、模板每组存 1 份F103 勉强可行词表上了 10 个直接看 F407 或 GD32F4 系列。这也是标题里“master”字样的含义——通常意味着这套代码已经把资源开销压到了可以在单片机上落地运行的程度。3. 在 STM32 上落地最小识别系统Keil 工程与核心代码3.1 音频采集链路搭建ADC DMA 定时器最常见的做法是使用 STM32 的 ADC 连续采样模式配合定时器触发DMA 把数据搬运到内存缓冲区采样率由定时器决定。以 8kHz 采样率为例定时器触发周期是 125µs。用 CubeMX 配置 PA0 为 ADC 输入音频模块 LM386 放大后输出接到这里ADC 用扫描模式、DMA 循环模式。// 定时器触发 ADCDMA 搬运到环形缓冲区 TIM3-PSC 84 - 1; // 84MHz/84 1MHz 计数频率 TIM3-ARR 125 - 1; // 计数 125 个 125us即 8kHz // DMA 配置外设到内存、循环模式、数据长度 2048 // 每次 ADC 转换完成后触发 DMA 搬运 // 中断里做语音活动检测VAD void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 检查新数据是否到来VAD 状态机推进 } }这段代码配置的是 TIM3 产生更新事件触发 ADC 采样DMA 把结果搬进缓冲区。PSC和ARR的值决定了采样率计算公式是采样率 定时器时钟 / ((PSC1) * (ARR1))。如果用外部 MAX9814 这类带 AGC 的麦克风模块信号幅值变化会小很多VAD 阈值更好设。3.2 端点检测静音段和语音段怎么切分识别的大前提是“每次只处理一个词”。端点检测用短时能量加过零率双门限法最稳。实现思路是把数据流分成 20ms 一帧8kHz 下即 160 个采样点计算每帧的短时能量能量高于高门限进入“语音段”低于低门限且持续 200ms 认为“语音结束”。// 短时能量计算 float compute_energy(int16_t *frame, int len) { float sum 0.0f; for (int i 0; i len; i) { sum (float)frame[i] * frame[i]; } return sum / len; } // 实际中用 sum/len 归一化后阈值根据环境底噪标定 // 例如阈值 底噪能量 * 4底噪在开机后取前 50 帧平均值能量阈值不能是固定值。我一般会在系统启动时采集前 500ms 的音频作为底噪估计然后设能量阈值是底噪的 4 到 8 倍过零率阈值用于区分清音和浊音防止把“嘶”这类摩擦音切开。3.3 MFCC 特征提取的 C 实现要点MFCC 的核心是 FFT 加 Mel 滤波器组。CMSIS-DSP 里已有arm_rfft_fast_f32可直接调用关键参数是帧长 256、FFT 点数 256、Mel 滤波器 24 个、输出 DCT 后取前 12 维系数。// 调用 CMSIS-DSP 库做实数 FFT arm_rfft_fast_instance_f32 fft_inst; arm_rfft_fast_init_f32(fft_inst, 256); // 加窗汉明窗减少频谱泄漏 for (int i 0; i 256; i) { frame_windowed[i] frame[i] * (0.54 - 0.46 * cosf(2 * PI * i / 255)); }加汉明窗这一步不能省省了频谱泄漏会明显拉低 MFCC 特征的区分度。Mel 滤波器组的系数表可以离线用 Python 算好直接以const float数组烧进 Flash不要在单片机上实时计算省下大量 Flash 和启动时间。倒谱系数取前 12 维是因为低维度对说话人音色差异更鲁棒高维度容易把环境噪声细节也学进去。3.4 DTW 匹配函数的参数设置DTW 在嵌入式上有个常见优化累计距离矩阵不需要全量存储。因为D[i][j]只依赖当前行和上一行滚动数组可以把空间从M*N降到2*N个float。识别时先算模板和待识别语音的帧数比若比值大于 2.0 直接跳过因为长度相差过大的两个词几乎不可能是同一词。float dtw_distance(float *tpl, int m, float *ref, int n) { float prev[64] {0}, curr[64] {0}; // 假设每帧特征维度固定 for (int j 0; j n; j) prev[j] 1e9f; for (int i 1; i m; i) { curr[0] prev[0] euclid(tpl i*12, ref 0*12); for (int j 1; j n; j) { float cost euclid(tpl i*12, ref j*12); float m1 prev[j]; // 向上 float m2 curr[j-1]; // 向左 float m3 prev[j-1]; // 对角 // 最小取 min同时给向上和向左加轻微惩罚 float mn m1 m2 ? m1 : m2; if (m3 mn) mn m3; curr[j] cost mn; } // 交换 prev 和 curr 指针 float *tmp prev; prev curr; curr tmp; } return prev[n] / (m n); // 长度归一化 }注意 DTW 的路径约束。上面代码里向上和向左两个方向没有额外权重惩罚是对标准对称形式的简化。实际使用中我习惯在向上和向左时加 1.2 的权重强制路径更偏向对角线能减少同一个词内部某个音素被过度拉伸导致的误匹配。4. 模板库的建立与识别参数调优4.1 模板库的采集策略每人 3 遍是底线识别效果最大的影响因素不是算法是模板怎么录的。常见做法是每个词录 3 到 5 遍作为模板录入时环境噪声要和实际使用场景一致。如果实际场景是室内安静环境模板却在嘈杂的办公室录入MFCC 里的背景噪声成分会成为模板的一部分识别率会急剧下降。模板采集代码的框架和识别相同VAD 检测到起止点后提取特征序列存入 Flash 或 SD 卡。这样可以反复试验模板数量与识别率的关系。// 模板保存每次语音结束把特征序列写入外部 Flash typedef struct { uint8_t word_id; uint8_t frame_num; float features[40][12]; // 固定最大 40 帧 } Template; void save_template(Template *tpl) { // 按 word_id 和 frame_num 写入 SPI Flash // 地址偏移 word_id * MAX_TPL_NUM 当前模板编号 spi_flash_write(feature_addr, (uint8_t*)tpl, sizeof(Template)); }4.2 阈值怎么定用“自言自语”标定识别阈值判定为“不在词表内”的标定方法是把每个词的模板轮流拿出来当测试样本算同类距离分布和异类距离分布阈值取两个分布之间的中点。例如“开灯”模板之间平均距离是 8.2“开灯”模板和“关灯”模板之间平均距离是 15.4阈值可以设 11.5 左右。// 在线调试工具串口命令打印距离矩阵 if (cmd dtw_debug) { for (int i 0; i word_num; i) { float d dtw_distance(tpl[i], tpl[i].frame_num, ref_feat, ref_frame_num); printf(word_%d - %.2f\r\n, i, d); } }这里用串口打印每个模板与当前输入的距离是现场调阈值最高效的手段。很多工程师把阈值写死导致误判是因为从没看过真实距离分布。把上面的调试命令留在固件里正式版用宏开关关掉部署到现场后通过串口工具观察几次识别时的距离值比凭经验调可靠得多。4.3 不当“模板平均党”每词多模板比特征平均更稳另一个常见误操作是把同一个词的 3 遍模板做特征平均合成一条。特征平均在时间对齐不一致时会抹平发音细节反而降低区分度。正确做法是保留多条模板识别时取该词所有模板中距离最小的那条作为该词得分。多模板带来的代价是计算量随模板数线性增长。10 个词 × 3 模板每次识别要做 30 次 DTW。好在 40×40 的滚动数组 DTW 在 168MHz 的 F407 上耗时约 8~15ms30 次就是 0.3 秒左右加上端点检测等待语音结束的 0.5 秒整体延迟符合交互预期。5. 进阶优化从“能跑”到“好用”5.1 噪声鲁棒性加一个简单的谱减法实际使用中最大的坑是底噪变化。空调声、风扇声会抬高整个频段的能量导致 MFCC 特征偏移。谱减法做轻量版在静音段估计噪声频谱幅度语音段频谱减去噪声频谱负值置零。这可以在 FFT 之后、Mel 滤波之前插入。// 噪声谱估计VAD 静音段持续更新 void update_noise_est(float *spec_mag) { for (int k 0; k FFT_SIZE/2; k) { noise_mag[k] 0.9f * noise_mag[k] 0.1f * spec_mag[k]; } } // 谱减 for (int k 0; k FFT_SIZE/2; k) { float sub spec_mag[k] - gamma * noise_mag[k]; spec_mag[k] sub 0 ? sub : 0; }gamma取 1.5减完只保留正数。这个步骤能让识别率在 60dB 环境噪声下不至于断崖下跌。5.2 计算量热点分析FFT 和 DTW 谁才是瓶颈用DWT-CYCCNT寄存器做性能计数会发现MFCC 阶段的 FFT 只占约 2~3ms而 DTW 匹配占了大头。优化方向一是给 DTW 加路径约束块宽度Sakoe-Chiba Band限制累计路径偏离对角线不超过 10 帧计算量直接砍半。// 在循环内增加列范围限制 int j_start (i - band 0) ? i - band : 1; int j_end (i band n) ? i band : n; // 只在此范围内填表其余位置保持无穷大Band 取 8 到 12 对孤立词识别影响很小却能把 40×40 的填表压缩到约 40×17。5.3 低功耗打断场景轮询模式与中断模式的取舍在做电池供电的手持设备时音频前端和识别主流程要分开VAD 前置检测用低功耗定时器 模拟比较器实现有语音活动才唤醒 STM32 做完整 MFCC DTW。STM32L4 系列可以做到待机电流 2µA 级别唤醒到完成一次识别约 150ms5 个词的指令集足够流畅。5.4 验证识别鲁棒性的三组实验拿到一个识别系统后不要直接说“效果好”。我会按以下三组实验验证同人同环境重复 20 次算准确率录音音量从大变小测能量变化下的稳定性距离麦克风 0.3m、1m、2m 三档测灵敏度衰减。其中 2m 距离是嵌入式麦克风方案的分水岭普通驻极体麦克风加到 2m 距离基本要依赖带 AGC 的前置放大器否则 MFCC 特征会被环境底噪淹没。最终一个可用系统的指标大致是10 词表、单说话人、安静环境识别率 95% 以上同环境带空调噪声识别率 85% 以上。如果达不到这个数优先检查端点检测的起止点是否切得干净再看模板是不是录得太散——这两个原因占了识别失败案例的大半。本文还有配套的精品资源点击获取