深度学习项目复现全流程指南:从环境搭建到结果验证
“喊一声‘刘工’,门锁就开了。”这件事听起来很有科幻感,但把它拆开看,其实就是一条非常典型的边缘计算流水线:麦克风采集声音、本地模型识别唤醒词、云端大模型解析语义、主控芯片控制门锁。这篇文章围绕 PSoC E84 主控,分享一套“本地 NPU 唤醒 + 云端大模型”的语音终端实现思路,覆盖硬件选型、系统架构、状态机设计、核心代码示例、常见坑点与工程落地建议。无论你是做智能家居、门禁系统,还是刚开始接触边缘语音终端,都可以把这套方案当成一个可扩展的样板来参考。
1. 项目背景与需求拆解
1.1 为什么要做“本地唤醒 + 云端大模型”
语音门锁这类产品,最核心的矛盾在于:门锁需要快速响应,又不能一直把声音传到云端。如果每一句声音都上传到大模型,会有三个问题。第一是延迟,网络往返动辄几百毫秒到几秒,门锁场景下用户站在门口,这个等待很难接受。第二是功耗,一直联网、一直录音,对电池供电的门锁来说不可持续。第三是隐私,门口环境可能录下邻居、家人、访客的对话,全部上传并不合适。
所以更合理的做法是:在本地完成“唤醒词检测”,只有当用户喊出“刘工”时,系统才开始录制后续指令,并把这段指令送到云端大模型进行语义理解。这样做的好处很明显——平时系统处于低功耗监听状态,本地 NPU 只运行一个很小的唤醒模型;真正需要云上理解的时候,才产生网络请求。唤醒和连续对话分开,延迟、功耗、隐私三者都能兼顾。
这个思路本质上就是边云协同:本地负责实时性要求高的部分,云端负责语义理解要求高的部分。PSoC E84 这种 MCU 级别的主控,并不是用来跑大模型的,它更适合做外设控制、状态管理和低功耗调度;本地 NPU 用来跑唤醒词模型;云端大模型负责把“请开门”这类自然语言转成门锁控制指令。三者各干各擅长的事,才能把体验做好。
1.2 能力边界:PSoC E84、NPU 与云端大模型怎么分工
先说明一点,PSoC E84 的详细规格要以你手里芯片的数据手册为准,不同系列和丝印之间差异较大。很多 PSoC 系列的 MCU 内部并没有大规模 NPU,所以工程上通常采用“MCU + 外部 NPU 协处理器”的异构方案。如果你的 E84 型号内部集成了 NPU,可以直接复用内部加速器;如果未集成,就外接一个低功耗 NPU 模块。本文按“外接 NPU 加速模块”的思路展开,这样适配性更强,也能讲清楚边界。
在这套系统里,各部分的职责划分如下:
- PSoC E84 主控:负责音频采集、外设管理、状态机调度、控制门锁,以及和 Wi-Fi/BLE 模块通信。它不承担复杂的神经网络推理。
- 本地 NPU 模块:负责对麦克风采集到的音频做唤醒词识别,例如识别“刘工”两个字。由于模型很小,单次推理耗电很低,适合长期监听。
- 云端大模型:负责接收完整的用户指令,比如“刘工,帮我开门”,解析成结构化意图,返回一段 JSON,PSoC E84 再根据这段 JSON 决定是否开门。
这种设计还有一个好处:后续想增加新功能,比如“刘工,查一下门口快递”,不需要改本地模型,只需要改云端大模型的提示词和解析逻辑。本地 NPU 只负责“谁在说话”和“是否说了唤醒词”,云端大模型负责“说了什么”和“想让我做什么”。
1.3 “刘工”不是一个普通的词:唤醒词设计
唤醒词是一套语音交互系统的入口,很多开发者会忽略它的重要性。“刘工”这类唤醒词看起来只是两个汉字,但放到门锁场景里,需要考虑几个问题。
首先是辨识度。唤醒词不能太短,也不能和日常高频词汇太接近。“刘工”两个字相对简洁,但如果用户家里经常有人叫“刘工”,就可能导致频繁误唤醒。所以实际落地上,通常会配合声纹信息或者多轮确认,降低误开锁概率。
其次是发音鲁棒性。同一个唤醒词,不同年龄、不同语速、不同方言口音的人说出来,声学特征差异很大。训练阶段要尽量采集多说话人、多距离、多噪声环境的数据;部署时还要做数据增强,让模型在门口脚步声、风吹声、电视声等背景下也能稳定识别。
最后是离线可用性。唤醒词识别必须在本地完成,不能依赖网络。云端大模型可以宕机,本地 NPU 不可以掉线,因为用户站在门外的时候,系统可能处于断网状态。这也是为什么唤醒环节要坚持“本地 NPU”而不是“云端唤醒”。
2. 系统总体架构
2.1 硬件链路
语音终端的硬件链路并不复杂,但每一环都要匹配好。大致流程是:
麦克风采集模拟或数字音频,经过音频编解码芯片或直接通过 PDM 接口进入主控;主控把音频数据整理成帧,通过 SPI/I2C/UART 送给 NPU 模块;NPU 模块推理后,把结果返回给主控;主控根据结果决定是否连接云端,并通过 GPIO 或舵机驱动模块控制门锁。
硬件选型上,要注意几个关键点。第一,麦克风要选信噪比高的数字麦克风,尤其是在门锁这种金属外壳场景里,共振和风噪会影响识别效果。第二,NPU 模块的接口要尽量选主控原生支持的通信方式,避免逻辑转换芯片引入额外功耗和调试成本。第三,云端的网络通道通常通过外接 Wi-Fi 模块实现,比如 AT 指令型模组,PSoC E84 只需要通过 UART 发送 AT 指令即可,不需要自己跑复杂协议栈。
下面是简化后的硬件连接关系:
数字麦克风 --> PSoC E84 (PDM/I2S) --> NPU 模块 (SPI/I2C)
PSoC E84 --> Wi-Fi/BLE 模组 (UART/AT) --> 云端大模型 API
PSoC E84 --> 电机驱动/舵机 --> 锁体
PSoC E84 --> 按键/指示灯/蜂鸣器 --> 本地状态提示
实际项目中,PSoC E84 的引脚分配需要结合芯片封装和外设复用关系调整,先画好框图再分配 GPIO,能省很多调试时间。
2.2 软件状态机
语音终端不能写成“听到声音就开门”这种线性逻辑,一定要有状态机。状态机的好处是可以把“采集、识别、确认、执行”每个阶段拆开,遇到异常时方便回到 idle 状态。
本文采用的简化状态机包含六个状态:空闲检测、语音活动检测、唤醒确认、指令录音、云端等待、开锁执行。
- 空闲检测状态:系统低功耗运行,麦克风周期性采样,本地 NPU 只做唤醒词检测。
- 语音活动检测状态:检测到有声音,但不是唤醒词,持续监听,超过时间则回到空闲。
- 唤醒确认状态:唤醒词得分超过阈值,启动二次确认,比如提示音、等待用户继续说话。
- 指令录音状态:唤醒后录制一段指令音频,比如“请开门”。
- 云端等待状态:把指令音频上传或转成文字后发送给云端大模型,等待返回结构化结果。
- 开锁执行状态:云端返回“open_lock”意图,执行开门,同时开始超时计时,到时间自动落锁。
这个状态机是后面写主程序的核心。把状态定义清楚,再写代码就顺很多,不会把业务逻辑全部堆在 while 循环里。
2.3 低功耗异构计算架构解读
这几年“低功耗异构计算芯片架构”很流行,核心思想是:不再让 CPU 一个人做所有事情,而是采用 NPU/APU 专用加速单元,结合 CPU/GPU 形成差异化算力分工。放在语音门锁里,CPU(也就是 PSoC E84 的 MCU 内核)负责调度控制,NPU 负责固定模型推理,云端 GPU 负责大规模大模型推理。
有些人会问,PC 的 NPU 能搞集群吗?这个问题的答案和本文场景有关。PC NPU 通常面向单设备近端 AI 推理,比如人脸识别、背景虚化、本地绘画模型加速,它的价值在于低延迟和本地隐私保护,而不是像 GPU 服务器那样做大规模训练集群。对于门锁这种设备,如果用 PC NPU 集群来做唤醒,既不划算,也不可能跟到门锁里。正确的做法是本地小 NPU 跑小模型,云端大模型跑语义理解,两者各司其职。
还有一个容易混淆的点:本地 NPU 并不是什么模型都能跑。NPU 的算力和内存通常很小,只能跑经过量化和裁剪的小模型。本文的唤醒词模型,往往只有几百 KB 甚至几十 KB,经过 INT8 量化后在 NPU 上执行;云端大模型则是几十亿甚至上千亿参数的大模型,跑在云端大规模算力集群上。
3. 环境准备与工程结构
3.1 开发环境与依赖
PSoC E84 对应的开发环境,需要以官方 SDK 为准。不同系列的 PSoC 芯片使用的 IDE 可能不同,常见的有 ModusToolbox、PSoC Creator,也有一部分型号支持 IAR、Keil 等第三方工具链。第一次接触时,建议直接打开官方例程,确认编译下载链路正常,再开始移植语音逻辑。
版本方面,本文不写死具体版本号,因为芯片型号、SDK 版本、NPU 模块工具链更新都很快。你在实际项目中,需要确认以下几类依赖的版本:PSoC 的 SDK 和硬件抽象层版本,NPU 模块的推理库版本,Wi-Fi/BLE 模组的固件版本,以及云端大模型 API 的接口版本。把这些版本信息写在项目 README 里,后续出现问题才好排查。
如果你希望在电脑端先验证云端大模型调用逻辑,建议准备 Python 3.8 或更高版本,安装好 requests 库,并准备好一个云端大模型的 API Key。注意,API Key 要通过环境变量或配置文件注入,不要硬编码到固件里。
3.2 工程目录结构
推荐把固件、模型、工具脚本分目录管理。一个典型的工程结构如下:
voice-lock/
├── firmware/
│ ├── src/
│ │ ├── main.c
│ │ ├── config.h
│ │ ├── mic_pdm.c
│ │ ├── npu_utils.c
│ │ ├── cloud_client.c
│ │ └── lock_ctrl.c
│ └── Makefile
├── model/
│ └── wake_word_model.tflite
├── tools/
│ └── cloud_call_demo.py
└── README.md
这种结构的好处是:固件代码、模型文件、云端脚本互不干扰。模型文件是二进制资源,不需要放到源码目录里;云端脚本可以在 PC 上单独调试,不需要每次烧录到开发板。
3.3 需要的模型与工具链
本文真正运行在 NPU 上的,是一个语音唤醒词模型。你可以用 TensorFlow Lite、Edge Impulse、SensiML 等工具来训练和导出,最终得到适合 NPU 的 INT8 量化模型。具体是 tflite 还是 ONNX,取决于 NPU 模块厂商的工具链。
另外需要准备一个唤醒词音频数据集。“刘工”这个唤醒词,建议至少采集几十个人、每人多遍、不同距离和噪声环境下的录音,然后进行切片和数据增强。如果没有真实数据,可以先使用在线服务合成多人语音,但合成语音和真实语音存在差异,部署后误唤醒率可能偏高,建议最终以真实场景录音为准。
4. 核心实现:本地语音唤醒
4.1 PDM 麦克风采集
门锁终端普遍使用数字麦克风,原因是可以直接把 PDM 信号送到 MCU,不需要额外的音频编解码芯片。PDM 是一种数据密度很高的调制方式,需要 MCU 通过滤波抽取把它转成 PCM 数据,也就是我们平时说的 16-bit、16kHz 采样率音频。
在 PSoC E84 上初始化 PDM 麦克风,思路大致如下。先初始化 PDM 硬件模块,再配置采样率和通道数,然后注册中断或 DMA 回调,把数据从硬件 FIFO 搬到内存环形缓冲区。下面是一个示例代码,重点展示流程,具体寄存器配置请参考你的 SDK。
// 文件路径:firmware/src/mic_pdm.c
#include "mic_pdm.h"
#include "config.h"
#define PDM_BUFFER_SIZE (AUDIO_FRAME_SAMPLES * 2)
static int16_t pcm_buffer[PDM_BUFFER_SIZE];
static volatile uint32_t frame_count = 0;
void mic_pdm_init(void)
{
// 1. 初始化 PDM 硬件外设
// 2. 设置采样率为 SAMPLE_RATE,声道数为 1
// 3. 注册 DMA 中断,将 PDM 滤波后的 PCM 数据写入 pcm_buffer
// 4. 启动外设
}
int16_t *mic_pdm_get_frame(uint32_t timeout_ms)
{
uint32_t start = get_tick_ms();
while (frame_count == 0) {
if (get_tick_ms() - start > timeout_ms) {
return NULL;
}
}
return pcm_buffer;
}
void mic_pdm_clear_frame(void)
{
frame_count = 0;
}
这里比较关键的是采样率。唤醒词模型训练时如果用的 16kHz,那么运行时采样率也必须是 16kHz,否则识别效果会大幅下降。代码里的
AUDIO_FRAME_MS
通常取 20ms 或 30ms,对应一个推理帧的长度。每帧数据量就是
SAMPLE_RATE * AUDIO_FRAME_MS / 1000
。
4.2 VAD 与人声检测
VAD 是语音活动检测,作用是判断麦克风当前输入的是“人声”还是“环境噪声”。如果不用 VAD,NPU 会一直在噪声背景下做推理,一方面浪费功耗,另一方面容易误唤醒。加了 VAD 之后,只有检测到人声能量超过阈值,才把音频帧送入 NPU。
VAD 的实现可以很轻量,比如计算短时能量和过零率。人声的短时能量通常比环境噪声高,且过零率有一定特征。下面是一个简化的 VAD 示例:
// 文件路径:firmware/src/mic_pdm.c
bool vad_is_speech(const int16_t *frame, int samples)
{
int32_t energy = 0;
int32_t zero_cross = 0;
for (int i = 0; i < samples; i++) {
energy += (int32_t)frame[i] * frame[i];
if (i > 0) {
if ((frame[i - 1] > 0 && frame[i] < 0) ||
(frame[i - 1] < 0 && frame[i] > 0)) {
zero_cross++;
}
}
}
energy /= samples;
if (energy > ENERGY_THRESHOLD && zero_cross > ZERO_CROSS_THRESHOLD) {
return true;
}
return false;
}
这种简单 VAD 在安静环境里够用,但门口风噪大时可能需要加入更复杂的动态阈值。工程上不要一上来就追求复杂模型,先用能量 VAD 跑通主链路,再根据实际误唤醒数据逐步优化。VAD 不是为了做语音识别,只是为了降低 NPU 空转次数。
4.3 在 NPU 上跑唤醒词模型
当 VAD 检测到有人声后,主控会把音频帧发送给 NPU 模块。不同 NPU 的接口差异很大,但抽象出来就三件事:把输入数据写到 NPU,触发推理,读取推理结果。下面的代码是接口示意:
// 文件路径:firmware/src/npu_utils.c
#include "npu_utils.h"
#include "config.h"
int npu_init(void)
{
// 根据 NPU 模块手册初始化通信外设
// 例如 SPI/I2C/UART
return 0;
}
int npu_infer_wakeword(const int16_t *pcm, int samples, float *score)
{
static int8_t input_buffer[256];
// 1. 将 PCM 数据归一化并量化为 INT8
quantize_pcm_to_i8(pcm, samples, input_buffer, sizeof(input_buffer));
// 2. 将量化数据发送给 NPU
if (npu_write_input(input_buffer, sizeof(input_buffer)) != 0) {
return -1;
}
// 3. 触发推理并等待完成
if (npu_trigger_inference() != 0) {
return -2;
}
// 4. 读取输出分数,这里假设输出是唤醒词概率
if (npu_read_output(score, sizeof(float)) != 0) {
return -3;
}
return 0;
}
这里需要注意,PCM 数据量可能超过 NPU 单次输入长度,实际工程中会做滑窗处理。比如每个 30ms 帧送入一个推理窗口,连续几帧得分都超过阈值才算唤醒成功。这个“连续确认”机制很重要,能显著降低误唤醒。
阈值设置也很讲究。阈值太高,用户喊破喉咙都唤醒不了;阈值太低,一点声音就开门。建议把唤醒阈值做成可配置参数,通过串口或配置区动态调整,在真实门口环境下反复测试。
4.4 唤醒后的指令录音
唤醒成功后,系统进入“指令录音”状态,这时候才开始录完整的用户指令。唤醒词本身不算指令内容,所以录音起点要在唤醒词结束之后。常见做法是在唤醒词识别到“结束边界”时开始录音,或者用 VAD 检测到超过 200ms 静音后自动结束录音。
指令录音长度不宜太长。门锁场景下,“请开门”“把门打开”这类指令一般不超过 3 秒。录音超过 5 秒还没检测到静音,可以强制结束,防止误录一大段无关对话传到云端。
录音完成后,系统有两种处理方式。第一种是把录音转成文字,此时需要本地或云端语音识别服务;第二种是不转文字,直接提取音频特征发送到云端多模态模型。考虑到现有大模型语音能力差异较大,最稳妥的做法是先用语音识别把指令转成文本,再让大模型对文本做意图解析。
5. 核心实现:云端大模型语义解析
5.1 上行链路选择
PSoC E84 通常没有直接联网能力,需要外接 Wi-Fi 模组。最简单的方案是使用 UART AT 指令模组,主控发送 AT 指令连接 Wi-Fi,再通过 MQTT 或 HTTP 和云端大模型 API 交互。
如果只是门锁这种低频控制场景,HTTP 请求就够了,不需要引入 MQTT。用户按一下门锁按钮,或者喊一次指令,一天最多几十次请求,HTTP 的简单直接更适合。如果你要做多终端状态同步、远程查看日志,再考虑 MQTT。
上行链路的数据量并不大,通常就是几十到几百字节的文本指令。延迟主要来自网络链路和大模型推理本身,所以终端侧要做超时处理。比如设置 5 秒超时,超过后播放“网络开小差了”的语音提示,并回到空闲状态。
5.2 构造系统提示词与指令格式
要让大模型输出稳定的门锁控制指令,不能直接把文字扔给大模型然后让开发者自由解析返回文本。正确做法是使用系统提示词,强制大模型输出 JSON。下面是一个云端调用示例,可以用 Python 脚本先在电脑上验证。
# 文件路径:tools/cloud_call_demo.py
import os
import json
import requests
url = os.getenv("BIG_MODEL_URL", "https://your-model-endpoint.example.com/v1/chat/completions")
api_key = os.getenv("BIG_MODEL_API_KEY", "")
system_prompt = """
你是一个门锁语音助手。用户输入的文本来自门锁端的语音识别结果。
请只输出 JSON,不要输出多余文字。JSON 格式如下:
{
"intent": "open_lock" | "close_lock" | "query_status" | "unknown",
"confidence": 0.0-1.0,
"reply": "给用户播放的提示语"
}
"""
user_text = "刘工,请开门"
payload = {
"model": "your-model",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_text}
],
"temperature": 0.1
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
resp = requests.post(url, json=payload, headers=headers, timeout=10)
data = resp.json()
try:
result = json.loads(data["choices"][0]["message"]["content"])
print("意图:", result["intent"])
print("置信度:", result["confidence"])
print("播报:", result["reply"])
except Exception as exc:
print("解析失败:", exc)
print("原始返回:", data)
把
temperature
调低到 0.1 左右,可以让大模型输出更稳定。系统提示词里明确限定“只输出 JSON”,能减少解析异常。实际生产环境要对大模型返回字符串做容错处理,即使格式异常也不能导致门锁进入异常状态。
5.3 收到意图后的门锁控制
云端大模型返回结构化结果后,PSoC E84 需要根据意图执行操作。先判断
intent
字段,再判断
confidence
是否超过阈值。这样可以避免大模型误判,比如用户说“刘工,请开门”,但置信度只有 0.4,系统就不该执行开门。
开门动作本身要加超时控制和状态回读。舵机或电机带动锁舌后,需要确认锁体是否真的打开了,不能只发一个 GPIO 信号就认为成功。有条件的话,可以在锁体上加位置传感器,主控读取传感器状态,确认开锁完成。
下面是一个简单的锁控模块示例:
// 文件路径:firmware/src/lock_ctrl.c
#include "lock_ctrl.h"
#include "config.h"
void lock_ctrl_init(void)
{
gpio_init(LOCK_OPEN_GPIO, GPIO_OUTPUT);
gpio_init(LOCK_FEEDBACK_GPIO, GPIO_INPUT);
}
int lock_open_with_timeout(uint32_t timeout_ms)
{
uint32_t start = get_tick_ms();
gpio_write(LOCK_OPEN_GPIO, 1);
while (get_tick_ms() - start < timeout_ms) {
if (gpio_read(LOCK_FEEDBACK_GPIO) == 1) {
return 0; // 锁体反馈已开
}
}
gpio_write(LOCK_OPEN_GPIO, 0);
return -1; // 超时未开
}
int lock_close_after_delay(uint32_t delay_ms)
{
delay_ms_sleep(delay_ms);
gpio_write(LOCK_OPEN_GPIO, 0);
return 0;
}
这段代码里,
LOCK_FEEDBACK_GPIO
是锁体的反馈信号脚。如果没有反馈信号,至少要在开门后定时落锁,避免门一直开着。门锁是安全设备,不能只做单向控制,一定要有反馈和超时。
5.4 离线兜底策略
任何云端服务都可能不可用,语音门锁必须设计离线兜底策略。最基础的做法是保留密码键盘、机械钥匙和手机远程开锁作为备用;语音开锁只是多种方式之一,不能成为唯一入口。
在软件上,如果云端请求超时,系统要播放“网络不可用”的提示音,而不是反复重试,避免浪费功耗。同时可以保存一条日志,包括时间、唤醒得分、云端是否响应等信息,方便后续排查。
更进一步,可以把“开门”“关门”这类高频但语义简单的指令,在本地做关键词匹配兜底。比如本地已经识别出用户指令文本包含“开门”“打开”等关键词,即使云端异常,也可以按低置信度策略执行。但这个兜底逻辑需要特别谨慎,宁可不执行也不能误执行,安全风险大于便利性。
6. 完整工程代码示例
6.1 配置文件
下面整理一个相对完整的工程示例,方便你对照落地。首先看配置文件,这里集中放置采样率、阈值、GPIO 编号、云端地址等参数。
// 文件路径:firmware/src/config.h
#ifndef CONFIG_H
#define CONFIG_H
#define SAMPLE_RATE 16000
#define AUDIO_FRAME_MS 30
#define AUDIO_FRAME_SAMPLES (SAMPLE_RATE * AUDIO_FRAME_MS / 1000)
#define ENERGY_THRESHOLD 500
#define ZERO_CROSS_THRESHOLD 10
#define WAKE_SCORE_THRESHOLD 0.70f
#define WAKE_CONFIRM_FRAMES 3
#define LOCK_OPEN_GPIO 8
#define LOCK_FEEDBACK_GPIO 9
#define LOCK_OPEN_TIMEOUT_MS 3000
#define LOCK_AUTO_CLOSE_MS 5000
#define CLOUD_TIMEOUT_MS 10000
#endif
这些参数在调试阶段要允许动态调整。尤其是
WAKE_SCORE_THRESHOLD
和
ENERGY_THRESHOLD
,不同环境差异很大,建议通过串口命令在线修改。把阈值全部编译死在固件里,后续测试会非常痛苦。
6.2 主状态机
主程序的核心是状态机。这里用枚举定义状态,然后在 while 循环里不断执行状态对应的处理函数。为了便于理解,我把每个状态的处理逻辑都写成一个独立函数。
// 文件路径:firmware/src/main.c
#include <stdio.h>
#include "config.h"
#include "mic_pdm.h"
#include "npu_utils.h"
#include "cloud_client.h"
#include "lock_ctrl.h"
typedef enum {
ST_IDLE = 0,
ST_VAD_DETECTING,
ST_WAKE_CONFIRM,
ST_RECORD_CMD,
ST_CLOUD_WAIT,
ST_LOCK_OPEN
} AppState;
static AppState state = ST_IDLE;
void app_handle_idle(void)
{
int16_t *frame = mic_pdm_get_frame(100);
if (frame == NULL) return;
if (vad_is_speech(frame, AUDIO_FRAME_SAMPLES)) {
float score = 0.0f;
if (npu_infer_wakeword(frame, AUDIO_FRAME_SAMPLES, &score) == 0) {
if (score >= WAKE_SCORE_THRESHOLD) {
state = ST_WAKE_CONFIRM;
}
}
}
}
void app_handle_wake_confirm(void)
{
// 连续确认唤醒词,降低误唤醒
static int confirm_count = 0;
float score = 0.0f;
int16_t *frame = mic_pdm_get_frame(100);
if (frame == NULL) return;
if (npu_infer_wakeword(frame, AUDIO_FRAME_SAMPLES, &score) == 0 &&
score >= WAKE_SCORE_THRESHOLD) {
confirm_count++;
if (confirm_count >= WAKE_CONFIRM_FRAMES) {
confirm_count = 0;
state = ST_RECORD_CMD;
}
} else {
confirm_count = 0;
state = ST_IDLE;
}
}
int main(void)
{
system_init();
mic_pdm_init();
npu_init();
cloud_client_init();
lock_ctrl_init();
while (1) {
switch (state) {
case ST_IDLE:
app_handle_idle();
break;
case ST_WAKE_CONFIRM:
app_handle_wake_confirm();
break;
// 其他状态省略,逻辑类似
default:
state = ST_IDLE;
break;
}
}
}
这个主程序把状态流转写得很直观。你可能会问:为什么唤醒要连续确认?因为单帧识别很容易受噪声、语气词影响。连续 3 帧都超过阈值,才说明用户真的说了唤醒词,误开锁概率会低很多。
6.3 云端调用模块
云端调用模块封装了一个函数:输入指令文本,输出意图结果。具体实现会因为 Wi-Fi 模组不同而差异很大,下面给出的是接口层面的示例。
// 文件路径:firmware/src/cloud_client.c
#include "cloud_client.h"
#include "config.h"
int cloud_client_init(void)
{
// 初始化 UART 和 Wi-Fi 模组
// 连接 AP,配置 DNS 等
return 0;
}
int cloud_parse_intent(const char *command_text, IntentResult *result)
{
char http_body[512];
char http_response[2048];
// 1. 构造 JSON 请求体
snprintf(http_body, sizeof(http_body),
"{\"model\":\"your-model\",\"messages\":[{\"role\":\"system\",\"content\":\"...\"},"
"{\"role\":\"user\",\"content\":\"%s\"}]}",
command_text);
// 2. 通过 Wi-Fi 模组发送 HTTP POST 请求
if (wifi_http_post(CLOUD_URL, http_body, http_response, sizeof(http_response)) != 0) {
return -1;
}
// 3. 解析 JSON,填充 intent 和 confidence
if (parse_json_http_response(http_response, result) != 0) {
return -2;
}
return 0;
}
实际开发中,
wifi_http_post
和
parse_json_http_response
都需要根据你的模组 SDK 实现。我的建议是先写一个 Python 脚本把云端链路调通,再回到 C 这边封装 JSON 构造和响应解析,这样问题定位更清晰。
6.4 云端调用演示脚本
如果你现在手里没有完整的 PSoC 硬件,也可以用 Python 先验证云端大模型的效果。脚本中需要你把
BIG_MODEL_URL
和
BIG_MODEL_API_KEY
配置到环境变量,避免把密钥提交到代码仓库。
# 文件路径:tools/cloud_call_demo.py
import os
import json
import requests
def main():
url = os.getenv("BIG_MODEL_URL")
api_key = os.getenv("BIG_MODEL_API_KEY")
if not url or not api_key:
print("请先配置 BIG_MODEL_URL 和 BIG_MODEL_API_KEY 环境变量")
return
payload = {
"model": "your-model",
"messages": [
{"role": "system", "content": "你是一个门锁语音助手,只输出 JSON。"},
{"role": "user", "content": "刘工,请开门"}
],
"temperature": 0.1
}
headers = {"Authorization": f"Bearer {api_key}"}
resp = requests.post(url, json=payload, headers=headers, timeout=10)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
result = json.loads(content)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
这个脚本可以作为后续固件开发的“参考实现”。当你发现固件里解析 JSON 困难时,可以通过大模型输出简化格式,比如让它只输出一个固定的控制码,减少嵌入式端解析复杂度。
6.5 编译烧录与验证
编译烧录的步骤取决于你的开发环境。以通用的 PSoC 开发流程为例,大致是:在 IDE 中导入
firmware
目录的工程,配置好芯片型号,编译生成 hex 文件,然后用烧录器下载到目标板。
验证分三步走。第一步,用串口助手观察主控日志:说“刘工”时是否打印唤醒成功。第二步,在代码里临时把云端调用替换成固定返回“open_lock”,验证门锁控制链路。第三步,恢复正常云端调用,端到端测试语音开锁。每一步都有明确分工,不要一上来就测全流程,否则出问题很难定位。
7. 常见问题与排查思路
7.1 唤醒率低与误唤醒并存
很多开发者会遇到一个矛盾:调高阈值,唤醒率低;调低阈值,误唤醒多。这个问题通常不是单个原因造成的。
首先是数据问题。唤醒词模型训练数据里,“刘工”的样本不足,或者说话人覆盖不够广。解决办法是增加真实环境录音,尤其是门口场景、室内场景、室外嘈杂场景。其次是前端处理问题。PDM 麦克风增益过高或过低,都会导致输入信号不稳定,先检查录音波形是否正常。第三是后处理问题。连续确认帧数设置不合理,也会影响体验。建议把得分打印到日志里,观察真实喊“刘工”和噪声误触发时的得分分布,再做阈值调整。
下表列出常见问题、原因和解决思路:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 喊唤醒词没反应 | 唤醒阈值过高 | 调低阈值,打印得分分布 |
| 不说话时门自动开 | 唤醒阈值过低或模型过拟合 | 提高阈值,增加连续确认帧数 |
| 广播声、电视声误唤醒 | 训练数据缺少噪声背景 | 增加噪声增强样本 |
| 距离稍远就唤醒失败 | 麦克风增益不足 | 检查 PDM 音量和模拟增益 |
| NPU 推理结果不稳定 | 通信时序或供电问题 | 检查 SPI/I2C 时序,加滤波电容 |
| 唤醒后录音为空 | 指令录音起点设置靠后 | 调整录音启动时间,检测唤醒词结束边界 |
7.2 云端响应慢或返回格式不固定
云端大模型响应慢,首先要看延迟是否在网络请求本身。可以先用 Python 脚本直接调用 API,测出纯云端的延迟基线。如果云侧就很慢,要考虑换一个更快的大模型,或者使用流式返回。门锁场景不需要流式,反而要等完整 JSON,所以直接超时处理更合适。
返回格式不固定,是调用大模型最常见的坑。即便提示词写了“只输出 JSON”,模型偶尔也会带解释文字。解决办法有两个:一是加一个 JSON 提取函数,从返回字符串中截取第一个
{
到最后一个
}
再做解析;二是在大模型提示词里给出一个严格示例,并要求固定
intent
枚举值。如果条件允许,最好使用支持函数调用或工具调用的大模型,把意图映射到固定工具参数上,比直接解析自由文本稳定得多。
在 PSoC E84 端,不建议直接跑重量级 JSON 解析库。可以让云端 API 网关先做一层转发和清洗,只返回类似
{"cmd":1,"tts":"好的"}
这样的精简格式,MCU 端只需要判断
cmd
字段即可。
7.3 门锁安全风险排查
语音门锁最怕两类风险:误开锁和非法开锁。
误开锁通常由误唤醒导致,解决思路前面已经说了:提高阈值、加 VAD、加连续确认、加意图置信度。非法开锁则是防攻击问题,录音重放是常见攻击方式。如果系统只靠“声音相似”就开门,攻击者录下一段“刘工,请开门”的音频,就能复制开门能力。面对这类风险,至少要加声纹验证或随机验证码。比如唤醒后让用户说一组随机数字,可以大幅降低录音重放攻击的风险。
另一个容易忽略的问题是权限管理。云端大模型只负责语义解析,不负责安全决策。开锁的控制权必须留在本地主控手里,并且要有多重条件:唤醒词通过、意图置信度通过、声纹通过(可选)、本地安全策略通过。任何一项不满足,都不能执行开锁。此外,建议保留机械钥匙作为最高优先级的物理备份,语音开锁只是增强体验,不能替代基础安全机制。
8. 工程落地经验与最佳实践
8.1 分层设计,把逻辑拆开
语音终端涉及音频、模型推理、网络通信、外设控制,如果所有逻辑都写在
main.c
里,后期维护会非常痛苦。建议至少分成四层:硬件抽象层、算法服务层、云服务层、业务状态层。
硬件抽象层封装 PDM、GPIO、UART 等底层接口;算法服务层封装 VAD、唤醒词推理;云服务层封装 Wi-Fi 模组、HTTP/MQTT 通信和大模型 API;业务状态层只做状态流转和安全决策。层与层之间通过结构体或者函数指针解耦,方便单元测试和平台迁移。比如后面换一款 NPU 模块,只需要改算法服务层,上层状态机不需要动。
8.2 低功耗设计要具体到每一毫安
门锁通常是电池供电,低功耗设计不能只停留在“有低功耗模式”这种口号上。要实测每个模块的静态电流和唤醒电流,尤其是 NPU 模块的睡眠功耗和 Wi-Fi 模组待机功耗。本文的状态机里,空闲状态必须让麦克风、NPU、Wi-Fi 模组都进入低功耗模式,用定时唤醒或中断唤醒的方式做周期监听。
具体来说,可以在空闲时关闭 NPU 电源,让主控每隔 100ms 醒来一次,用低功耗麦克风检测环境能量,超过阈值后才打开 NPU 电源。这一招能省下不少电流。如果你的 PSoC E84 支持深度睡眠,务必把音频采集也设计成可间断模式,不要一直全速采样。低功耗设计没有标准答案,只能根据实际电压、电流数据反复调优。
8.3 安全边界要写进需求,不能靠运气
语音门锁是安全设备,安全策略必须从需求阶段就明确写下来,而不是最后想起来补。建议至少明确以下规则:语音开锁必须经过本地安全策略校验;云端返回的意图不能直接触发开锁,只能作为候选;开锁后必须自动落锁;所有开锁请求都要记录日志;遇到不确定情况,优先拒绝开锁并提示用户使用备用方式。
此外,代码里不要出现“万能密码”或“调试后门”。即使开发阶段为了方便,在固件里留了特殊串口命令,上线前也一定要移除。NPU 唤醒词模型、云端 API Key、Wi-Fi 密码都属于敏感配置,要放在单独的配置分区并做加密处理,不要直接写在源代码里。
8.4 数据隐私与日志策略
语音设备天然涉及隐私。建议在硬件上增加一个物理麦克风开关,用户手动关断麦克风电源后,系统彻底不采集声音,这个开关最好不受软件控制。软件上,本地只保存唤醒前后很短时间窗的音频特征,不保存原始音频;上传到云端的内容,尽量只上传识别后的文本,不上传完整录音。
日志方面,门锁设备本地 Flash 有限,建议只保存关键事件,比如唤醒时间、唤醒得分、云端是否成功、门锁状态等。日志格式保持精简,方便后续通过串口或 BLE 导出分析。不要为了“以后分析”把大量音频存到本地,这既占空间,也有隐私风险。
到这里,这条从“刘工”到“开门”的链路就比较完整了。你会发现,真正让门锁变智能的不是某一个算力单元,而是本地 NPU、PSoC E84 主控和云端大模型之间的协同节奏。本地负责守护低功耗和隐私边界,NPU 负责在嘈杂环境里听懂“刘工”,云端负责理解那句“请开门”背后的意图,主控则守住最后一道安全门。后面如果想继续做,可以把唤醒词换成任意自定义词,把开门换成调灯、开空调,甚至把大模型接到更复杂的工具调用系统里,这套框架依然成立。希望这篇方案能帮你把门顺利打开。
更多推荐
所有评论(0)