ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于ESP32-S3的AI陪伴设备端云架构设计与实现

2026/9/7 20:47:46 拓冰建站 浏览量
基于ESP32-S3的AI陪伴设备端云架构设计与实现 从一块 ESP32-S3 开发板到一台 AI 陪伴设备中间隔着的不是接个麦克风、调个大模型 API这么简单。我前后改了三版第一版只会一问一答第二版能聊天但动不动断线第三版才把唤醒、采集、云端对话、OTA 升级串成一条可持续演进的端云架构链路。这篇文章就把第三版的完整设计和踩坑记录拆开讲。如果你正在用 ESP32-S3 做语音交互、桌面机器人、儿童陪伴玩具或者单纯想搞懂端云架构里哪些活该留在本地、哪些活必须上云这篇都值得读。我会把硬件选型、外设接入、云端链路、工程化手段和真实故障排查一整套放出来你照着搭也能跑起来。1. 整体设计端云架构怎么划分才科学1.1 选 ESP32-S3 的真实原因一开始我也纠结过用不带 Wi-Fi 的 MCU 加外挂模块或者直接上 Linux 小板子。最后定 ESP32-S3核心原因是三点240MHz 双核 Xtensa LX7、512KB SRAM、片内 2.4GHz Wi-Fi 和 BLE 5.0。这个组合刚好卡在能做本地实时处理又能低成本保持在线的甜点上。很多人低估了 ESP-SR 语音识别框架的价值。它可以在本地做唤醒词、命令词识别功耗低、响应快不依赖网络也稳。配合 S3 自带的向量指令加速一个 3MB 左右的唤醒模型跑起来很流畅。相比之下如果选老 ESP32内存只有 320KB跑唤醒模型就得精打细算后面的 OTA 空间更紧张。还有一个现实原因开发成本。S3 的生态资料比很多国产方案全乐鑫官方例程里有 Wi-Fi 配网、蓝牙配网、I2S 麦克风、USB 摄像头等现成参考。这意味着我可以把精力放在端云架构上而不是反复调外设驱动。对团队来说这直接决定了从原型到量产需要多少人月。1.2 端云职责边界与演进路线很多人的第一版是把所有处理丢到云上麦克风采集后直接推流云端返回文字再合成语音。这样做最快但问题也明显——每次交互都要几秒延迟而且完全依赖网络。到了第二版我改成端侧唤醒端侧 VAD云端识别体验立刻上一个台阶。这里说的 VAD 是语音活动检测用来判断人有没有真的在说话。我的划分原则就一条凡是实时性要求高、且模型体积可控的留在端侧凡是需要频繁更新知识、强逻辑推理的放到云端。具体来说端侧负责唤醒词检测、语音活动检测、按键/触摸交互、Wi-Fi 连接状态机、设备电源管理。云端负责语音识别、大模型对话、工具调用、TTS 合成、会话历史管理、用户画像更新。这条边界不是死的后面可以演进。演进路线我建议分四步走第一步纯云端对话验证场景第二步加入端侧唤醒和 VAD降低无效流量第三步加入视觉、传感器等多模态输入第四步引入多个云端 AI Agent 协同和个性化记忆。每一步都保持接口兼容这才是可持续演进的本质。设备端代码接口设计得不依赖某一次交互的具体字段而是抽象成输入事件环境上下文输出动作这样后面加再多功能都只是填表不用推翻重写。2. 硬件层接入麦克风、摄像头与配网2.1 I2S 麦克风驱动与采样参数选择我用的麦克风是 INMP441I2S 数字输出16kHz 采样率24bit 数据宽度。接线很简单VDD 接 3.3VGND 接地SCK 接主时钟WS 接声道选择SD 接数据线。这里有个常见坑INMP441 的 L/R 脚决定输出到左声道还是右声道接 GND 是左声道接 VDD 是右声道代码里声道配置要对应上。驱动代码我直接用 ESP-IDF 的 I2S 标准模式#include driver/i2s_std.h i2s_chan_handle_t rx_chan NULL; i2s_chan_config_t chan_cfg { .id I2S_NUM_0, .role I2S_ROLE_MASTER, .dma_desc_num 6, .dma_frame_num 240, .auto_clear true, }; i2s_std_config_t std_cfg { .clk_cfg { .sample_rate_hz 16000, .clk_src I2S_CLK_SRC_DEFAULT, }, .slot_cfg { .data_mode I2S_SLOT_MODE_STEREO, .slot_mask I2S_STD_SLOT_LEFT, .ws_width I2S_DATA_BIT_WIDTH_16BIT, .bit_width I2S_DATA_BIT_WIDTH_16BIT, }, .gpio_cfg { .mclk I2S_GPIO_UNUSED, .bclk GPIO_NUM_4, .ws GPIO_NUM_5, .dout I2S_GPIO_UNUSED, .din GPIO_NUM_6, .invert_flags {0}, }, }; i2s_new_channel(chan_cfg, NULL, rx_chan); i2s_channel_init_std_mode(rx_chan, std_cfg); i2s_channel_enable(rx_chan);采样率定在 16kHz 是语音识别的标准值既能保证识别准确率又不会让每秒的音频数据量太大。按 16bit 单声道算1 秒音频是 32KB一分钟不到 2MB。这个体量对 Wi-Fi 上行和云端存储都很友好。如果要做音乐或高音质需要 44.1kHz/48kHz但陪伴设备场景基本用不到反而会增加带宽和云端成本。读取数据用i2s_channel_read注意要处理dma_frame_num对齐。我习惯每次读 480 个采样点正好 30ms 一帧方便后面做 VAD 和端点检测。每次读出来的数据先放进环形缓冲区再由唤醒检测或 VAD 任务消费避免音频任务和其他任务互相抢占资源。2.2 USB 摄像头接入与视觉帧管理S3 的 USB OTG 接口可以接 USB 摄像头走 UVC 协议。我用的 OV2640 USB 摄像头模组插上就能被识别。ESP-IDF 里用esp_uvc组件枚举设备、设置分辨率然后通过回调拿到 JPEG 帧。摄像头在陪伴设备里不是必需品但加上之后能解锁不少场景看到主人靠近主动打招呼、识别当前是否有人在房间、拍一张照片让云端大模型描述环境。我最初只在云端对话里加了一个看图能力设备按键或语音触发拍照JPEG 压缩后上传大模型返回图片描述。实测下来描述比较准确尤其是物体识别和场景判断。帧管理最大的问题是内存。S3 的 512KB SRAM 里可用堆常常只有 300KB 左右一张 640x480 的 JPEG 可能占 40~80KB。所以我的策略是拍照时先停掉播放和音频任务从 DMA buffer 抠出一块内存流程走完立即释放。边缘触发时只在需要时才初始化摄像头避免长期占用内存。摄像头初始化本身要几百毫秒但陪伴设备对抓拍实时性要求没那么高可以接受。2.3 BLE 配网的实现与异常处理设备第一次上电没有 Wi-Fi 凭据我用 BLE 配网S3 开机进入配网模式广播一个 BLE GATT 服务手机小程序扫描后写入 SSID 和密码。这一步比 SmartConfig 稳定尤其对于 5G Wi-Fi 或隐藏 SSID 的场景。BLE 配网协议我简化成两个特征值一个可写的 SSID/密码 JSON 特征一个可读的设备状态特征。手机写入 JSON 后设备尝试连接 Wi-Fi成功后通过通知回调告知手机最后退出 BLE 广播进入正常工作模式。关键代码逻辑static void on_write_ssid(esp_ble_gatts_cb_param_t *param) { char json[256] {0}; memcpy(json, param-write.value, param-write.len); cJSON *root cJSON_Parse(json); char *ssid cJSON_GetObjectItem(root, ssid)-valuestring; char *pass cJSON_GetObjectItem(root, pass)-valuestring; // 保存到 NVS然后启动 Wi-Fi 连接 nvs_set_string(wifi_ssid, ssid); nvs_set_string(wifi_pass, pass); esp_wifi_connect(); }这里踩过的坑是手机写入的 JSON 可能超过单个 ATT 包长度要开启ESP_GATT_PERM_WRITE的prepare_write支持或者把数据拆包。更简单的方法是用 MTU 协商把 MTU 调到 247 字节一个 JSON 就能放得下。另外配网完成后要主动停掉 BLE 广播否则不仅耗电还会和 Wi-Fi 共存干扰影响后续连接速度。3. 语音链路与云端服务实现3.1 端侧唤醒词与端点检测我用乐鑫的 ESP-SR 做唤醒词模型跑在端侧默认唤醒词可以自定义比如小伴小伴。唤醒词检测是常开的功耗控制在几十 mA 级别。一旦唤醒设备开始录音并做 VAD检测到人说话才开始真正上传音频说话停顿超过 800ms 或超过最大时长 15s 就结束本轮。这里有个经验不要一唤醒就把所有音频推到云端。很多环境有电视声、空调声不做 VAD 的话云端识别全是噪音。VAD 我用 ESP-SR 自带的vad_handle_t阈值调到中等灵敏度然后在代码里再对 RMS 做一次判断。双保险之后无效流量能降一半以上。VAD 状态切换用简单状态机IDLE空闲→ WAKEDUP已唤醒→ LISTENING录音中→ PROCESSING等待云端返回。状态切换时同时控制 LED 指示灯和提示音让用户知道设备当前在听还是已经在处理。这个反馈极其重要没有反馈用户就会反复喊体验直接崩。3.2 端云消息协议与音频上行音频上行我选了 WebSocket而不是裸 HTTP POST。原因是语音识别结果需要流式返回而且一轮对话往往有多次音频片段。WebSocket 长连接的开销比每次新建 HTTP 小得多还能顺便承载后面的指令下行。协议按 JSON 封装示例{ type: audio_chunk, session_id: a1b2c3, format: pcm_16k_mono, data_base64: ... }设备先发一个session_start里面带设备 ID、唤醒词 ID、请求时间戳。然后持续发audio_chunk最后发session_end。云端按顺序拼接音频送识别服务。为了省流量我一般会用 Opus 压缩16kbps 码率就够了10 秒语音压缩后只有 20KB 左右比裸 PCM 小十倍。考虑到设备可能掉线云端要维护一个会话超时机制如果 15 秒内没有收到任何audio_chunk就自动结束会话并返回提示。设备端则在收不到任何ack时触发重连。WebSocket 断线重连不能太频繁我设置了指数退避从 3 秒开始重试最多隔 30 秒避免网络抖动时疯狂重连把云端打爆。3.3 大模型对话与工具调用云端拿到文本后先走一轮意图判断是闲聊、查天气、设闹钟、讲故事还是调用某个工具。传统做法是训练 NLU 模型但接入大模型之后意图判断直接用大模型做prompt 里给几个工具定义例子让模型输出 JSON 动作。比如你是陪伴设备的助手根据用户请求决定调用哪个工具。 输出格式{tool: weather, city: 北京} 可用工具weather, timer, story, remind, chat如果大模型返回timer云端就解析出时长并生成指令下行到设备设备本地起定时器。这个设计把工具执行留在端侧把工具选择放到云端既灵活又安全。我甚至可以让大模型同时返回多轮回复和动作指令实现你说一句话设备同时开灯、播音乐、弹提醒的效果。这里要特别注意 prompt 安全。设备侧接收的指令必须做白名单校验只允许执行提前定义好的动作类型不能直接把大模型输出当代码执行。云端返回的内容也要做敏感词过滤尤其是面向儿童场景的内容必须加一层审核。4. 可持续演进的工程化手段4.1 OTA 升级与 A/B 分区回滚陪伴设备放在用户家里不像开发板可以随时接串口刷机。所以可持续演进的第一件事就是 OTA。我采用 A/B 双分区方案固件运行时在 A 区云端推送新固件到 B 区校验通过后切换启动分区下一次 boot 进入 B 区。如果新固件起不来看门狗或启动管理器自动回滚到 A 区。ESP-IDF 的esp_ota_ops封装好了 A/B 逻辑。关键点是给每个固件打版本号并做灰度发布。我习惯先把新固件推给 5% 的设备观察一两天崩溃率和在线率再逐步放量。千万不要一口气全量推送否则一个 bug 会让所有设备离线。OTA 下载我用 HTTP分包写入 flash。固件本身 3~5MB2.4G Wi-Fi 环境下大概 1~2 分钟完成。这段时间要保证设备不重启建议在 OTA 过程中屏蔽唤醒词避免误触打断。另外OTA 前要检查设备电量低电量时延后升级防止写到一半掉电变砖。4.2 可替换的模型网关设计云端最容易演进的其实是模型本身。今天用这家大模型明天可能换另一家价格、效果、延迟都会变。如果客户端直接绑定一家模型每次切换都要发固件太痛苦。所以我在云端加了一层模型网关统一接收端侧请求内部再路由到不同模型供应商。网关对外暴露一个稳定接口/v1/conversation参数包含设备 ID、用户 ID、上下文和工具定义。内部实现是插件化的 providerOpenAI 的 GPT 系列、Claude、本地部署模型都实现同一个接口。切换模型时改网关配置就行端侧完全无感。这个设计额外带来两个好处一是可以 A/B 测试不同模型的效果对不同用户走不同模型二是可以做熔断某个模型供应商故障时自动切到备用的用户几乎无感知。网关里还要做上下文裁剪控制 token 数防止长对话把成本拉爆。我的做法是只保留最近 10 轮对话摘要再加最近 3 轮完整消息效果和完整历史差不多成本却能降 60%。4.3 日志采集与用户反馈闭环设备日志如果不回传远端问题等于瞎子摸象。我采用轻量日志上报设备端维护一个环形 bufferWi-Fi 空闲时把最近 50 条日志和关键指标通过 HTTPS POST 到日志服务。日志带上设备 ID、固件版本、网络信号、错误码方便定位。日志上报频率要控制我设成 5 分钟一次每次几 KB流量成本几乎可以忽略。另外一定要设计用户反馈入口。陪伴设备被用户吐槽最多的是听不懂和回复不对。我在 App 里加了一个反馈按钮用户一句话上滑表示不喜欢下滑表示喜欢。云端把反馈连同当时的对话上下文存下来定期抽出来做 badcase 分析然后调 prompt 或换模型形成闭环。这比你自己拍脑袋猜要准确得多。5. 常见问题与排查技巧实录5.1 I2S 麦克风噪声大、有回音怎么办最常见的是麦克风把扬声器声音也收进去了。处理办法有几种硬件上把麦克风远离喇叭加隔音棉软件上在设备播报语音时强制开 AEC回声消除。ESP-SR 里带了 AEC 和 NS 模块在录音后处理链路里加一步即可。别指望完全靠算法消除硬件布局才是根本。还有一类问题是 I2S 时钟配置不对导致的杂音比如 WS 和 BCLK 接反、数据位宽不匹配。我之前因为把ws_width设成 32bit 但麦克风输出 24bit音频听起来像机器人说话。解决方法是INMP441 实际是 24bit 数据宽度但 I2S 协议里会放在 32bit slot 中所以slot_cfg.bit_width可以写 32bit真正读到 PCM 后在软件里取高 16bit同时把ws_width设置成 32bit 对齐。这样录音音量正常也没有奇怪噪声。5.2 BLE 配网超时与重连策略BLE 配网最常见的失败是手机写入 SSID 后设备退出了广播但 Wi-Fi 一直连不上用户拿不到任何反馈。后来我在状态特征里加了连接进展1 表示收到 SSID2 表示正在连 Wi-Fi3 表示连接成功4 表示连接失败。手机端轮询这个特征值失败时弹窗提示检查密码或换 2.4G 频段。另外设备在连接 Wi-Fi 前要调用esp_wifi_set_config配好后先esp_wifi_stop再重新esp_wifi_start否则很容易出现能扫到热点但起不了连接的问题。BLE 广播在连接成功后要主动关掉减少功耗和射频干扰。还有一个容易被忽略的点NVS 里存的旧 Wi-Fi 信息要能在配网模式下清除否则用户换网时永远连的是老路由器。5.3 网络抖动导致语音断流端侧音频推流时云端偶尔会几秒收不到包。我一开始在处理端做超时 5 秒直接结束会话用户体感是话说一半被掐断。改成 10 秒超时后好一些但还是不够。最终方案是设备端做音频重发——云端回ack设备对未确认的音频帧保留 2 秒的滑动窗口超时未确认就重发。但重发也要限制不能无限重试否则会加重网络拥塞。我的策略是超过 3 次重发失败就向用户播报网络不太好请稍后再试并终止会话。这个提示非常重要如果没有用户会以为设备坏了。另外TTS 播报用的是独立连接不跟音频上行共用连接这样识别过程中也能插播正在思考的提示音。5.4 内存不足与任务调度问题S3 虽然标称 512KB SRAM但 Wi-Fi、BLE、蓝牙共存等组件会占用不少。早期我同时开了唤醒词、摄像头、Wi-Fi、BLE、TTS 播放结果频繁重启。排查后发现是任务栈开太大加上堆碎片化严重。解决思路一是任务栈精打细算语音任务栈给 4096 字节就够别随手给 8192二是用esp_pthread配置复用线程三是把大块 buffer 从 heap 挪到静态数组减少碎片。还有一个技巧播放 TTS 时把唤醒词检测暂停播完再恢复能省出不少 CPU 和内存而且不会在设备自言自语时误唤醒。我最深的体会是端云边界不能一开始就定死而是要留出移动的空间。里面最值得投入的不是某个具体模型而是设备端和云端之间的协议抽象、OTA 和日志链路。哪怕以后换更强的主控、换成更聪明的大模型只要这层接口稳定整台设备都可以像软件一样迭代。最后分享一个小技巧给每台设备在云端维护一份能力清单端侧有什么传感器、云端支持什么模型都以 JSON 返回给设备端。这样后续加摄像头、加麦克风阵列时不用改协议设备自己就知道能做什么。这比什么都写在固件里好维护得多。