做嵌入式开发这些年,我见过太多把“语音识别”提上需求、最后又砍掉的项目。原因翻来覆去就那几个:功耗压不住、延迟不能忍、离线就变哑巴。但这两年Edge AI在MCU上真正跑起来之后,情况开始变了。拿手边的NXP i.MX RT1050来说,Cortex-M7跑到600MHz,片上512KB紧耦合内存,外挂Hyper Flash和SDRAM,PDM接口直接接数字麦克风,这种配置已经能在本地点亮一个唤醒词模型外加十来个命令词。往上的RT1170还带NPU,算力更是充裕。大联大世平集团推的智能穿戴参考方案,就是把NXP这摊资源串起来,让开发者不用从零蹚坑。这篇文章想聊的,不光是这颗芯片能跑什么,而是从功耗、语音链路、模型部署到实际调试,一条线讲清楚怎么在穿戴设备上做出真正能用的低功耗语音互动。
1. 这个方案要解决的核心问题:穿戴产品的语音交互为什么难做
1.1 传统语音方案在穿戴设备上走不通的三个原因
先说个容易被低估的事实:穿戴设备的功耗预算,比手机苛刻一个数量级。手机电池3000mAh起步,一天一充可以接受;智能手表、耳机、工牌这类产品,电池能塞进300mAh就算不错,而且用户期望续航至少两到三天。这意味着平均工作电流要控制在几毫安以内,甚至待机时要压到几十微安。
在这个前提下,传统“录音上传云端识别”的路子基本走不通。WiFi或者4G模块一开,电流直接飙升到几十上百毫安,录音和上传整个过程持续几秒钟,平摊到每小时的电量消耗非常恐怖。更麻烦的是时延,语音说完了要等语音助手回复,云端往返加解码,快则六七百毫秒,慢则两秒以上,这种交互在戴手表时尤其别扭——用户抬腕说话,视线还停在屏幕上,结果半天没反应。
隐私也是隐形门槛。麦克风常开、音频数据往外传,用户嘴上不说,心理上一定膈应。前几年不少厂商做过“录音上传”式语音助手,后来舆论压力一大就默默下线了。而Edge AI的好处就在这儿:音频不出设备,唤醒词、命令词、简单意图识别全部在本地MCU完成,需要联网的指令再通过蓝牙交给手机处理。这才是穿戴设备语音交互能长期存在的形态。
1.2 为什么是NXP跨界MCU而不是手机SoC或普通MCU
很多人会问,做语音识别不是应该上高性能应用处理器吗?比如手机SoC或者树莓派那类Linux平台。但穿戴设备里塞一个需要跑Linux、需要DDR、需要大PCB的芯片,成本和功耗都扛不住。
NXP的i.MX RT系列是“跨界MCU”这个品类的典型代表。它没有MMU、不带Linux,本质还是一颗裸跑或跑RTOS的单片机,但主频做到了600MHz,性能已经接近早期手机处理器。对比RT1050、RT1060、RT1170三款芯片,产品定位差异很明显:
| 型号 | 内核 | 亮点 | 适合场景 |
|---|---|---|---|
| i.MX RT1050 | Cortex-M7 @ 600MHz | 512KB TCM,功耗与性能均衡 | 入门级手表、工牌、智能耳机 |
| i.MX RT1060 | Cortex-M7 @ 600MHz | 增加摄像头接口、更多外设 | 带屏可拍照的穿戴设备 |
| i.MX RT1170 | Cortex-M7 + Cortex-M4 | 集成NPU(eIQ Neutron) | 追求复杂模型和多模态交互的旗舰产品 |
对语音交互来说,RT系列的几个外设非常关键。首先是PDM接口,可以直接接数字麦克风,省掉外部Codec和模拟前端,降低BOM成本;同时PDM模块支持DMA搬运,CPU可以不用全程盯着采样。其次是低功耗模式很完整,Run、Wait、Stop、Standby层层递减,配合SDK里的电源管理驱动,可以在毫秒级完成模式切换。再就是eIQ工具链,NXP在自家SDK里集成了TensorFlow Lite Micro和Glow推理引擎,模型转换、部署、性能评估都有现成流程,比从零移植省太多事。
对比STM32H7这类同样基于Cortex-M7的芯片,RT1050的优势主要在大内存和XIP执行。STM32H7虽然主频也不低,但内部RAM通常几十到几百K,跑稍大的语音模型就显得局促,外部SDRAM布线又麻烦。而RT1050的TCM加上外部SDRAM/HyperRAM方案,给了语音处理充裕缓冲区,模型和音频数据都能摊开放,开发体验完全不一样。
2. 低功耗设计是这套方案的地基
2.1 先把功耗账算明白
低功耗设计不是靠“少干活”感性优化,而是要把每一毫安花在哪算清楚。一个简单的动态功耗模型是:
P = C × V² × f
C是翻转电容,V是工作电压,f是时钟频率。对单片机来说,降低电压和降频是最直接的手段;另外一个大头是静态功耗,来自漏电流,和制程、温度、IO状态都有关系。
实际的穿戴设备功耗预算可以这样粗算:假设电池容量200mAh,可用电量按80%算,那就是160mAh。用户期望续航48小时,平均电流预算就是160 ÷ 48 ≈ 3.3mA。如果屏幕、传感器、蓝牙都要占一部分,那留给“语音监听”的电流预算通常只有几百微安到1mA。
所以在原型设计阶段就要画一张电流分配表,把系统分成几种状态:待机、监听、激活、推理、通信。每个状态的电流和时间占比相乘,最后累加得到平均电流。比如监听状态下电流2mA,时间占比10%,对平均电流的贡献就是0.2mA;推理状态电流60mA,但每次只跑50ms,十分钟才触发一次,平摊下来几乎可以忽略。这个表拉出来之后,哪儿该抠功耗就一目了然。
我们当时设计目标定得很明确:待机电流小于50µA,唤醒监听平均电流不超过1mA,一次唤醒到给出反馈的端到端延迟小于300ms。这三个指标是后面所有优化的锚点。
2.2 模式切换:让MCU在99%的时间里睡觉
NXP RT系列的低功耗模式,从浅到深大致有Run、Wait、Stop、Standby四档。很多刚接触的人会把“sleep”和“idle”混着说,其实在Cortex-M的语境里,idle通常指CPU执行WFI指令进入等待,时钟还在跑,外设照常工作;RT系列SDK里对应的Wait模式就是这种。Stop模式则是大部分时钟关掉,只有少量模块(比如LPTMR、RTC、指定唤醒引脚)保留工作,内核停摆,但是RAM内容保持。Standby更进一步,连主要供电域都可以关,唤醒恢复时间更长,适合超低功耗待机。
| 模式 | 典型电流 | 唤醒源 | 恢复时间 | 适用场景 |
|---|---|---|---|---|
| Run | 几十到百mA量级 | - | - | 实时推理、通信 |
| Wait | 几mA到十几mA | 任意中断 | 极短 | 等待DMA采集完成 |
| Stop | 几十µA量级 | LPTMR、GPIO、RTC | 几十µs级别 | 间歇监听 |
| Standby | 几µA量级 | 复位、特定唤醒引脚 | 较长 | 夜间待机 |
低功耗语音监听通常的做法是:MCU大部分时间待在Stop模式,用LPTMR定时唤醒,比如每200ms醒一次,配置好PDM和DMA采集一段几十毫秒的音频,运行一个极轻量的VAD判断有没有语音能量,如果没有就马上再睡回去。只有VAD判定可能有语音的时候,才切到高频运行模式跑完整的唤醒词模型。
这套策略的关键是“醒得快、干事快、睡得快”。如果唤醒恢复要几百微秒,采集加判断要20ms,那平均功耗就会随频率线性上升,所以代码里要尽量减少唤醒后的初始化动作,所有要用到的外设配置在睡之前就准备好。
2.3 外设级节电:让DMA替CPU值班
低功耗设计不光是选模式,外设的使用方式同样决定成败。语音采集是CPU最容易“空转”的场景。如果每来一个采样都要CPU去搬一次数据,那即使内核主频不高,CPU也一直醒着,功耗根本压不下来。
正确做法是PDM接口配DMA。PDM模块按配置好的采样率(比如16kHz)采集数据,DMA把采样结果批量搬到内存缓冲区,攒够一帧(通常是20ms或30ms)后触发DMA中断,CPU只在中断里处理一帧音频。这样CPU大部分时间可以睡在Wait模式,等待DMA的传输完成中断。
此外,NXP的RT系列支持按模块开关时钟。每一个不做事的模块,都可以调用SDK的CLOCK_DisableClock或直接关闭对应外设时钟,把动态功耗降到最低。这里有一个特别容易踩的坑:GPIO口如果悬空,漏电流会比正常接上下拉状态高不少。进入低功耗前,必须把不用的引脚设为上拉或者下拉输出,避免浮空输入造成额外功耗。我们曾经因为一个悬空的I2C上拉引脚没处理,整机Stop模式电流多出将近20µA,查了很久才发现,这个细节工程师一定要记得。
3. 语音交互链路怎么搭:从物理信号到识别结果
3.1 端侧语音处理的完整流程
语音交互看起来是“说话→出结果”两步,实际中间隔着一条很长的链路。穿戴设备端侧语音处理一般是这样:
麦克风(PDM)→ 采样与预处理(分帧、加窗、特征提取)→ VAD语音活动检测 → 唤醒词识别(KWS)→ 命令词识别 → 本地动作或通过蓝牙交给手机。
先说采样。语音识别常用16kHz采样率,16bit量化,单声道,一秒钟产生的数据量是32KB。如果24小时不间断处理,数据量超过2.7GB,这在MCU上完全不可想象,所以必须靠VAD和唤醒词把“真正要处理”的音频比例降到极低。
预处理阶段,MCU上常用的是分帧加窗后提取MFCC或滤波组特征。MFCC在传统语音识别里用得最多,但神经网络模型用log-mel谱更直接。特征提取的窗口一般20-30ms,帧移10ms,这样一秒钟能产出100帧特征。原始音频先归一化到[-1, 1],如果有直流偏置还需要做高通滤波,否则会影响特征质量。
VAD简单实现可以只用时域能量和过零率判断,逻辑很轻、计算量小;稍微复杂一些的可以用一个小网络做语音/非语音二分类,精度更高。VAD的作用是门卫,唤醒词识别才是正式入口。当VAD判定“有人在说话”,才把这一小段音频的特征送给KWS模型,判断是否包含唤醒词。确认唤醒后再录制后续音频做命令识别。
这条链路每一环都在消耗时间和功耗,所以要在系统设计时就定好各环节的延迟预算。我们的目标是:VAD判定时间不超过30ms,唤醒词识别不超过150ms,命令识别不超过100ms,加起来小于300ms,基本能做到“说完唤醒词后300ms内给出响应”,体感上不会有明显等待。
3.2 模型选型与部署:把推理塞进MCU
MCU上跑语音模型,模型体积和计算量是两个硬约束。对于唤醒词这样的任务,常用的是DSCNN(Depthwise Separable Convolution)或者TC-ResNet这类轻量网络。在“Hey Device”这类唤醒词上,DSCNN参数量可以压到几十KB,量化成INT8之后内存占用非常友好,单次推理在600MHz Cortex-M7上大约几十毫秒。
命令词识别说的是“播放”“暂停”“接听”“挂断”这类有限集合指令,模型结构可以和唤醒词模型合并,也可以做成两个模型串行。为了降低切换开销,很多参考设计干脆用一个多分类网络,把“唤醒词”和“各命令词”都作为输出类别,识别流程简化成一次推理。
模型在PC上训练好之后,部署到MCU有几个关键步骤。第一是量化,从FP32转INT8,通常需要准备一批有代表性的校准数据,让转换工具统计激活值分布,确定缩放因子。第二是算子支持,TensorFlow Lite Micro只支持一部分算子,不支持的算子要替换或重写;NXP的eIQ工具链会帮你做算子映射,但遇到自定义算子还是要手写实现。第三是内存规划,TFLite Micro运行时需要一块tensor arena,模型输入、中间激活、输出都在这块缓冲区里分配,arena大小直接影响内存占用,需要来回调优。
模型来源方面,除了自己训练,也可以去公开的模型资源库找现成的关键词唤醒模型,不用从零开始收集语音数据。在NXP的eIQ示例里就有现成的KWS demo,模型和特征提取代码都打包好了,很适合先跑通流程再替换成自己的模型。
3.3 语音互动体验:不能只讲识别率
做语音交互的人容易陷入“准确率越高越好”的思维,但穿戴设备上体验和指标之间是要平衡的。误唤醒是所有语音交互产品的公敌,手表在开会时突然被唤醒弹出语音助手,用户会立刻想关掉这个功能。降低误唤醒的手段有很多:VAD这层先过滤掉非语音噪声;KWS模型的置信度阈值调高;加上两次验证机制,比如唤醒词后面跟一个短静音窗口,没有后续就自动回睡;还可以做时间段管理,晚上自动进入“只听不答”模式。
反馈机制也非常影响体验。屏幕大一点的手表可以显示波纹动画,屏幕上没空间的话就要靠马达振动或者一个小LED。唤醒成功的反馈建议控制在几十毫秒内给出,同时反馈不能过于夸张,否则用户会觉得“这个设备好吵”。
另外要提一下降噪和回声消除。穿戴设备使用环境嘈杂,地铁、街道、健身房背景噪声都很大。如果只依赖模型鲁棒性,识别率会大幅下降。轻量方案是做谱减法,重一点的可以跑一个小型DNN降噪。回声消除更多用在带扬声器的设备上,比如智能音箱手表,如果设备自己放歌的同时还要听用户说话,就必须要做AEC,否则唤醒词识别基本没法用。MCU算力有限,这个模块要结合实际产品形态取舍。
4. 实操过程记录:在RT1050上跑通低功耗语音唤醒
4.1 环境搭建与硬件准备
我手上的原型是基于MIMXRT1050-EVK搭的,外接一块PDM数字麦克风小板,电池供电部分用稳压模块模拟。软件开发环境是MCUXpresso IDE加NXP官方SDK,语音相关部分用到了eIQ推理引擎和TFLite Micro。
搭建环境的步骤并不复杂,但有几个坑值得提前说。SDK版本尽量用新的,老版本里eIQ组件不完整,TensorFlow Lite Micro的版本也比较老,部署新模型容易碰到算子缺失问题。MCUXpresso IDE自带SDK管理,下载对应板卡的SDK包后会生成一批示例工程,建议先编译跑通hello_world和power_mode_switch这两个例程,它们分别验证了工具链和低功耗模式切换,是后面一切改动的基础。
硬件上要注意PDM麦克风的接线,MCLK和DATA两根线都要确认没接错。EVK板上的跳线、供电方式也要检查,如果用调试器供电,电流测量结果会非常不准,后面测功耗时必须换成独立电源,再用串口输出现象辅助调试。
4.2 代码实现:进入低功耗模式、DMA采集与模型推理
低功耗休眠与唤醒的代码,核心是把唤醒源配置清楚。下面这段是基于MCUXpresso SDK的简写示例,逻辑是LPTMR每200ms唤醒一次:
#include "fsl_lptmr.h" #include "fsl_power.h" #include "fsl_gpio.h" static void lptmr_init(uint32_t us) { lptmr_config_t lptmrConfig; LPTMR_GetDefaultConfig(&lptmrConfig); LPTMR_Init(DEMO_LPTMR, &lptmrConfig); LPTMR_SetTimerPeriod(DEMO_LPTMR, us); LPTMR_EnableInterrupts(DEMO_LPTMR, kLPTMR_TimerInterruptEnable); EnableDeepSleepIRQ(LPTMR_IRQn); } void enter_stop_mode(void) { /* 进入Stop模式前确保唤醒源已配置好 */ LPTMR_ClearStatusFlags(DEMO_LPTMR, kLPTMR_TimerInterruptFlag); POWER_EnterStop(POWER_STOP_MODE, 0, 0, 0, true); }这里有一个细节:进入Stop之前要清一次中断标志,否则唤醒后刚回到Stop的调度代码里,中断标志还是置位状态,可能会引起重复唤醒或者死循环。
音频采集采用PDM + DMA的方式。SDK里PDM驱动提供了非阻塞的传输接口,缓冲区半满和全满时都会触发回调:
pdm_config_t pdmConfig; PDM_GetDefaultConfig(&pdmConfig); pdmConfig.sampleRate = 16000; pdmConfig.enableHPF = true; PDM_Init(DEMO_PDM, &pdmConfig); PDM_TransferCreateHandle(DEMO_PDM, &pdmHandle, pdmCallback, NULL); pdm_xfer_t xfer; xfer.data = audioBuffer; xfer.dataSize = AUDIO_FRAME_SIZE; PDM_TransferReceiveNonBlocking(DEMO_PDM, &pdmHandle, &xfer);回调里拿到的数据就是16kHz的PCM。注意PDM输出的是PCM数据,不是原始PDM码流,驱动已经帮你完成滤波抽取了,省了很多底层功夫。
模型推理部分,TFLite Micro的调用模式基本是固定的:
static tflite::MicroErrorReporter microErrorReporter; static const tflite::Model* model = tflite::GetModel(g_kws_model); static tflite::MicroInterpreter interpreter( model, resolver, tensorArena, kTensorArenaSize, µErrorReporter); interpreter.AllocateTensors(); float* input = interpreter.input(0)->data.f; /* 将特征填入input */ int8_t* output = interpreter.output(0)->data.int8; interpreter.Invoke(); /* 解析output,取概率最大的类别 */如果模型是INT8量化过的,输入和输出类型都是int8,特征输入前要按量化参数做scale转换。Tensor arena的大小要按模型实际情况调整,我们用的KWS模型大约占128KB,整个音频处理管线加起来内存占用不到512KB,RT1050的TCM完全放得下。
4.3 实测数据与调优记录
功耗实测数据是这套方案最有说服力的部分。我们用低功耗电流表分别测量各状态下的电流,结果大致如下:
| 状态 | 电流 | 说明 |
|---|---|---|
| Standby | 6µA | 关掉了所有IO,保留RTC |
| Stop + LPTMR | 38µA | LPTMR每200ms唤醒一次,唤醒期间不干活 |
| 唤醒采集+DSP | 8mA(持续约25ms) | 16kHz采样、特征提取 |
| KWS推理 | 65mA(持续约35ms) | 600MHz运行TFLite Micro |
| 命令识别 | 70mA(持续约50ms) | 多分类模型 |
把这些数据代入平均电流模型:假设每小时用户说10次命令,每次命令需要一次唤醒加一次命令识别,那么每小时推理时间大约是0.35秒+0.5秒,按70mA算,耗电约0.02mAh;监听部分每200ms醒25ms,占比12.5%,按8mA算平均1mA,一小时耗电1mAh。这样一个20mAh的语音监听模块,理论续航约20小时,对一款两三天续航的手表来说,这个预算还是在可接受范围内的。
内存方面,模型权重约80KB(INT8),激活缓冲和特征缓存加起来约180KB,代码和数据总共控制在400KB以内,RT1050的512KB TCM正好放下,不需要开SDRAM,这对降低系统复杂度和功耗都有好处。
调优过程中印象最深的是把推理频率从600MHz降到400MHz试了一版,推理时间从35ms涨到约50ms,功耗却只省了不到10%,说明对这类短时推理任务来说,高主频“赶紧跑完赶紧睡”才是更优解。这也是低功耗设计里经常被忽略的一点:很多时候性能过剩比性能不足更费电。
5. 常见问题与排查技巧实录
5.1 唤醒之后系统不稳定、自动复位
这个问题出现的频率极高,尤其是在从Stop模式刚恢复的时候。排查时先看复位原因寄存器,SDK里会记录上一次复位是上电、看门狗、还是引脚复位。我们遇到的一个典型案例是进入了Stop模式后,本应关闭的看门狗还在跑,唤醒后系统看门狗超时复位。解决方法是进入低功耗前临时关掉看门狗,或者在看门狗中断里延长喂狗时间。
另外,如果调试器(J-Link、DAPLink)还连接着目标板,Stop模式可能无法真正进入,或者唤醒后调试会话错乱,表现也是复位。测低功耗时最好把调试器断开,只保留串口输出或者用无线日志。
5.2 低功耗状态下采集不到音频数据
这是做间歇监听时最典型的坑。RT1050的PDM模块在Stop模式下不工作,即使LPTMR定时唤醒,如果唤醒后没有重新使能PDM和DMA,音频缓冲区就一直是空的。我们的做法是在唤醒中断服务程序里先启动PDM采集,采集完成后再统一做VAD和推理,处理完再睡回去。
另一个常见问题是PDM和DMA的缓冲区长度不匹配,导致回调频率异常。比如PDM采样率16kHz、DMA缓冲区设成1600字节,那么回调频率就是每秒10次,也就是每100ms有一次音频帧。如果算法期望每30ms一帧,就要调整DMA缓冲区大小或者做帧拼接。这个参数错位不会导致编译报错,但运行时语音识别会变得非常迟钝。
5.3 量化后识别精度明显下降
INT8量化的精度损失如果超过预期,多半是校准数据选得不好。校准集应该覆盖真实使用环境的语音、静音、噪声,而不是只拿几段干净录音糊弄过去。另外要注意输入特征的量化范围,如果MFCC或mel谱中有个别极大值,会把动态范围拉得很大,导致大部分数据量化精度不足。解决方法是做特征截断,比如把幅度上限设为统计分布的95分位数,超出部分截掉,量化效果会好很多。
还有一个隐蔽问题:训练时特征提取的代码和MCU端特征提取代码不一致。比如训练时用了均值归一化(CMVN),MCU端图省事没做,模型看到的分布完全变了,精度断崖式下降。这种问题最花时间,所以部署前一定要比对特征,把训练脚本里预处理的关键参数迁移过来。
5.4 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| Stop模式电流远超预期 | 引脚悬空、外设时钟未关、调试器连接 | 检查GPIO状态、关闭外设时钟、断开调试器 |
| 唤醒后系统复位 | 看门狗未停、中断标志未清、时钟配置错乱 | 查看复位原因、停看门狗、清标志 |
| 音频数据全零或爆音 | PDM根时钟配置错误、DMA缓冲未对齐 | 核对时钟源和分频系数、按16字节对齐缓冲 |
| 唤醒词经常误触发 | VAD阈值太低、模型置信度阈值低 | 调高阈值、加静音验证窗口 |
| 推理时间过长 | 未开指令缓存、内存访问冲突 | 使能I-Cache/D-Cache、把模型放到TCM |
| 蓝牙连接后休眠异常 | 蓝牙芯片漏电或持续唤醒主控 | 用GPIO控制蓝牙电源、配置低功耗连接参数 |
6. 这个方案后续我还想折腾的方向
整套流程跑通之后,我最大的感受是:Edge AI语音在穿戴设备上能不能落地,瓶颈早就不在“能不能识别”了,而是在“能不能长期开着”。NXP的RT系列配合eIQ工具链,再加上大联大世平这类伙伴提供的参考设计,已经把模型部署和低功耗切换的门槛拆掉了大半,但产品化落地仍然需要团队一遍遍抠功耗、抠误唤醒、抠交互细节。
我个人下一步比较感兴趣的,是让设备支持用户自定义唤醒词。目前KWS模型的训练和部署流程已经很顺,但每次换唤醒词都要重新训练模型,门槛还是有点高。如果能在设备端做一个轻量的唤醒词注册流程,用户说三遍“你好小X”,设备就能在本地微调模型参数,这个词就变成了用户专属唤醒词,这种体验对穿戴设备来说会非常加分。
另一个方向是上RT1170,利用它自带的NPU跑更复杂的音频模型。比如同时做声纹识别和命令识别,或者把噪声抑制和语音识别合并到同一个网络里,算力充足之后交互体验可以再上一个台阶。这块板子我已经在准备了,等跑出数据再单独写一篇。