ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ESP32-S3打造AI陪伴设备:端云架构与工程实践全解析

ESP32-S3打造AI陪伴设备:端云架构与工程实践全解析 很多人看到“AI陪伴设备”这几个字第一反应是“一个音箱接个大模型API”。真上手之后你会发现光是让设备稳定听清你说话、在断网时还能给出回应、在不刷固件的情况下增加新技能就够你折腾好一阵子。我们用了两个多月从一块合宙的 ESP32-S3-N16R8 开发板起步搭起了一套可持续演进的端云架构。这台设备原型现在能陪聊、讲故事、设闹钟、做视力保护提醒而且新增技能完全不用动固件。这篇文章不是“点灯”教程而是完整记录我们做这套东西时的核心决策和踩坑过程覆盖硬件选型、音频链路、BLE配网、端云分工、Agent化演进以及几个差点把项目拖垮的工程坑。适合正在做AI硬件原型的嵌入式开发者、想给产品加AI能力的创客以及做IoT云端和应用层的工程师参考。1. 为什么是 ESP32-S3一块开发板背后的选型逻辑1.1 从候选方案对比说起先亮结论我们最终选了 ESP32-S3-N16R8 这个型号16MB Flash 8MB PSRAM主控是 ESP32-S3 双核 Xtensa LX7主频最高 240MHz支持 Wi-Fi 4 和 BLE 5.0。这个芯片在 AIoT 圈子里不算新但放到“AI 陪伴设备”这个场景里它有几个很难替代的长处。先看我们当时对比过的几类方案经典 ESP32 / ESP32-D0WD价格低、生态成熟但RAM只有520KB没有PSRAM那么大跑唤醒词模型时性能余量小。做纯传感器网关绰绰有余做语音交互就比较紧。ESP32-S3多了向量指令扩展ESP-DSP里的矩阵乘法、FFT这类操作有明显加速官方SDK里对语音、屏幕、摄像头的外设支持也全适合做交互型设备。RP2040 / nRF5340前者双核Cortex-M0算力有限WiFi还得外挂后者BLE很强但WiFi要另接模块增大了硬件面积和功耗管理的复杂度。树莓派 Zero 2 W / 全志这类 Linux 板系统能力强可以直接跑 Python、跑 Whisper但功耗、启动速度和结构尺寸都不占优做玩具原型行做成一个随时在线的陪伴设备发热和供电都是问题。这里有个容易被忽略的点AI陪伴设备的“陪伴”首先意味着长时间在线其次才是聪明。所以功耗、启动速度、外设实时性和算力同等重要。ESP32-S3 的 240MHz 双核在绝对算力上不够看但用在“唤醒、音频采集、网络通信、轻量本地逻辑”这些环节刚刚好重活交给云端。1.2 陪伴设备对硬件的需求清单如果你也准备做类似的东西建议先列一份需求清单再回头看硬件。我们的清单大概是这样的语音输入至少一路数字麦克风能在16kHz采样率下稳定工作语音输出I2S接口的D类功放比如MAX98357直接推3W小喇叭网络Wi-Fi BLE一个用于数据通信一个用于配网和近场交互存储Flash至少8MB原因很简单——固件、可选的语音模型、字体资源、OTA双分区加起来很容易就超过4MB内存最好是带PSRAM的型号。AI相关的音频处理通常会一次性申请大buffer没有PSRAM会频繁触发内存分配失败调试与扩展留出UART、I2C、SPI后面加屏幕、加传感器、加电机都不至于推翻重来我们用的板子是合宙的 ESP32-S3-N16R8因为它是开发板里少见的 N16R8 大存储版本而且引脚基本都引出了焊线、飞线都方便。如果直接从模组做起M21、S3-12K 这类也行但原型阶段没必要给自己添堵。1.3 双核240MHz的性能余量到底够不够拿到板子之后我最关心的是性能余量因为音频处理最怕“任务调度卡顿导致的声音断裂”。我们的FreeRTOS任务划分大致是这样CPU0 跑I2S音频采集、WiFi/MQTT协议栈CPU1跑唤醒词检测、VAD和业务逻辑。ESP-IDF默认就会把协议栈放在一个核上另一个核留给应用这个天然的分工很适合我们。实测下来四个任务常驻的情况下I2S读音频、唤醒词检测、MQTT保活、业务逻辑CPU占用率在唤醒词检测时才明显上涨平时待机只有个位数。内存方面内部RAM我们只放了关键任务栈和DMA描述符音频数据统一走PSRAM这样即便同时缓存10秒的录音也不会把内存挤爆。所以回答标题那个问题对语音陪伴设备来说性能余量不是用来跑大模型的而是用来吸收工程实现中的各种浪费。只要把内存规划好ESP32-S3完全够用。2. 音频链路是生命线从麦克风选型到端到端延迟预算2.1 数字麦克风与I2S数据流设备要“陪聊”第一件事是听清人说话。我们用了 INMP441 数字麦克风I2S 接口16kHz 16bit 单声道。为什么不用模拟麦克风加ADC因为模拟链路还要解决偏置、放大、抗干扰的问题数字麦直接把PDM转成I2S数据代码少一个量级。关键代码其实很简单ESP-IDF里配置一个 I2S 驱动开启 DMA 接收然后从 DMA buffer 里拿数据。这里最常见的错误是把每次读取的一帧数据想成“一句话”实际上音频是流式的必须用一个持续追加的环形缓冲区把帧串起来。size_t bytes_read 0; int16_t *buf (int16_t *)heap_caps_malloc(frame_samples * sizeof(int16_t), MALLOC_CAP_SPIRAM); while (1) { i2s_channel_read(i2s_rx_handle, buf, frame_bytes, bytes_read, portMAX_DELAY); status ring_buffer_write(audio_rb, buf, frame_bytes); if (status ! RB_OK) { // 这里要处理缓冲区满了的情况直接丢帧或标记溢出 overflow_cnt; } }配套的还有一件事DMA的buffer大小要靠实测调。我们最初用默认配置设备一开扬声器就出现爆音后来把DMA描述符数量和buffer size都调大了一档问题立刻缓解。原因是WiFi和音频共用总线时DMA在总线竞争下无法保证稳定出数据buffer太小就会欠载。2.2 唤醒词与VAD让设备知道该“听”了语音唤醒用的是乐鑫官方的 ESP-SResp-skainet框架支持中文唤醒词和命令词识别。模型可以烧录到Flash里直接跑支持多命令词唤醒。我们把唤醒词设成“你好小智”又加了三个本地命令词“停止”“大声点”“安静”这三个词不走云端端侧识别后直接执行能减少很多误操作。VADVoice Activity Detection语音活动检测的作用更基础判断当前是人声还是静音。VAD一旦判断是静音就可以停止上云、降低CPU频率甚至进入light sleep这对电池供电的设备非常重要。我们实测纯待机VAD在跑、WiFi连接已保持时平均电流大约在80-120mA之间如果使用电池供电这个值还有优化空间但对原型机来说已经能接受。这里有一个容易被忽略的“声音打架”问题TTS播放的时候麦克风会把扬声器的声音也收进去。工业音箱有专门的AEC回声消除模块但我们原型阶段没有上。我们的做法是播放TTS时临时屏蔽唤醒和VAD播完再恢复。代价是一段TTS期间用户无法打断用起来有点像老式录音电话。如果做二代产品我建议把AEC或打断检测提前到需求里否则体验感差很多。2.3 端侧处理与整体交互延迟真实产品里用户对延迟是很敏感的。我们把整条链路的延迟预算拆开看大概是这样的唤醒词检测150-250ms用户说完话音频结束判定VAD检测到静音300-800ms录音上云、云端ASR300-500msLLM处理500-1500msTTS首包输出400-800ms端侧播放即时加起来端到端响应大概在2-3秒之间。这个数字在一问一答的场景下勉强能接受但一旦涉及多轮对话体验就会感觉“像在和一个反应慢半拍的人聊天”。我们的处理方式是ASR采用流式识别用户还没说完云端就开始出中间结果TTS选择流式返回边下边播。首包的延迟优化优先级远高于生成完整内容的延迟这一点在做端云架构时一定要想清楚。3. BLE配网与设备身份从裸板到云上节点的第一步3.1 为什么选BLE配网而不是SmartConfig或AP配网一台设备要上云绕不开配网。市面上常见的配网方式有三种配网方式操作体验稳定性适用场景AP配网SoftAP手机需要切换WiFi连设备热点再切回来最稳定不依赖路由器信号环境复杂、设备量大SmartConfigESP Touch手机App发UDP组播包无需切换WiFi依赖路由器是否允许组播兼容性问题多家庭路由器但兼容性看运气BLE配网App通过BLE把WiFi信息发给设备设备自动连接BLE信道独立基本不受WiFi环境影响交互体验好还能反向读设备状态我们选BLE配网核心原因是体验和可控性的平衡。BLE配网过程中App能实时知道设备连WiFi的结果是密码错误、路由器信号弱还是连上外网了这些信息都能通过BLE通知直接弹给用户。SmartConfig做不到这种双向交互AP配网又要求用户在手机设置里来回切换容易把小白用户搞懵。ESP32-S3 上实现BLE配网有两种协议栈选择Bluedroid 和 NimBLE。Bluedroid 功能全但体积大、内存占用高NimBLE 是开源实现内存占用低得多配网场景只需要 GATT serverNimBLE完全够用。3.2 GATT服务设计与状态机实现我们设计的GATT服务非常简单一个Service三个CharacteristicWrite Characteristic接收SSIDWrite Characteristic接收WiFi密码Notify Characteristic上报配网状态0x01: 连接中0x02: 成功0x03: 密码错误0x04: 超时流程状态机如下BLE_START_ADV - BLE_WAIT_CREDENTIAL - WIFI_CONNECTING - WIFI_CONFIG_SUCCEED | | ---------- WIFI_CONFIG_FAIL ---- 重新等待设备上电后在BLE广播中携带自己的设备IDApp扫描到后点击连接依次写入SSID、密码。设备拿到凭据后启动WiFi连接同时通过Notify把状态异步回报给App。无论成功失败App都能在界面立刻看到结果。这里有一个细节WiFi的SSID和密码我们统一用UTF-8编码在JSON里传输而不是分别写两个字段。原因很简单有些SSID本身包含特殊字符分开传容易在字符串边界出问题JSON整体传过去解析一次就够了。JSON体积很小BLE的20字节ATT MTU虽然理论上是限制但实际能协商到更长的MTU传几十字节没压力。3.3 安全细节与设备身份配网过程最容易被人吐槽的是“WiFi密码明文传输”。严格来说BLE链路本身有加密机制只要App和设备完成配对绑定传输内容在BLE链路层就是加密的。我们建议在App侧一定要开启BLE配对不要用“just works”这种免配对模式。配网成功之后设备把WiFi凭据写入NVS。这里建议开启ESP-IDF的NVS加密特性否则二进制flash被读出去凭据就泄露了。同时我们利用芯片eFuse生成一个全局唯一的设备UUID后面MQTT的clientId、云端的设备注册表都靠它来关联数据。设备上云之后身份认证可以分两个等级原型阶段MQTT使用 username/password 唯一clientId服务器校验。量产阶段建议升级为设备证书双向TLS每个设备烧入独立证书服务端离线验证。我们目前停在第一阶段但架构上已经预留了证书校验的扩展点。因为从一开始就意识到设备身份和通信安全很难在架构定稿后再往里塞。4. 端云架构的分工边界哪些逻辑留在端侧哪些必须上云4.1 通信选型为什么是MQTT陪伴设备的端云通信实质是双向低频通信设备主动上报状态云端偶尔下发指令。HTTP轮询不是不行但问题很明显——云端给设备下发消息时必须先建立连接延迟不可控。WebSocket能做到长连接但协议层太重设备端的断线重连逻辑要自己写。我们最终选了MQTT over TLS原因有三个协议是发布/订阅模型天然适合“云端下发指令”这种反向控制断线重连、遗嘱消息、会话保持这些机制都是现成的不用自己造生态成熟云端的EMQX或Mosquitto一跑就起来开发效率高配置上我们把QoS设为1意味着消息至少送达一次但允许重复。为什么不用QoS0可能丢或QoS2恰好一次设备端命令重复一次通常能幂等处理而消息丢一次可能就真的丢了QoS1是性价比最高的折中。4.2 端侧、云端的分层模型我们把整个系统的职责分成四层设备能力层麦克风、扬声器、LED、传感器、按键。这一层只做物理交互不关心业务逻辑。设备业务层唤醒、VAD、本地命令词、播放控制、音频流上传。这一层和设备强相关但尽量不做“理解”的工作。云端网关层设备连接管理、消息路由、状态管理、技能调度。这一层把设备接入与具体AI能力解耦。AI能力层ASR、LLM、TTS、知识库、各种工具调用。这一层为设备提供“智能”也是后期扩展空间最大的地方。这个分层的核心思想是设备端代码追求“稳定”云端能力追求“变化”。设备端如果经常要改意味着每次都要OTA、都要用户配合体验极差。所以我们把所有容易变化的部分对话逻辑、技能定义、文案、模型参数都放到云端通过配置下发而不是固件升级来变更。设备端只维护一套稳定的原子动作协议。4.3 断网降级陪伴设备不能因为没网就变砖一个很容易被忽略的产品需求是设备没网的时候怎么办我们实测过家里断网、云服务故障、用户在电梯里任何一种情况都可能发生。如果设备在没网时只会说“网络开小差了”这个产品基本不会有人在日常使用它。我们的降级策略分三档轻度降级网络通但云端响应慢。此时设备继续尝试同时播放“信号不太稳定再等等我”这类话术让用户感觉设备在思考而不是死机。中度降级网络断开但本地功能可用。闹钟、计时器、本地预置的语音包比如问候语、几个固定笑话全部走本地至少保证“陪伴感”不断。重度降级完全没有网络且电量低。设备进入低功耗模式关键日志保存在本地恢复网络后自动续传。这些降级逻辑不需要云端的参与全部跑在设备端。我们在设备端维护了一个“本地能力表”断网时按优先级从高到低提供服务。比如本地故事机模式设备存了20条不同主题的短故事断网时随机播一条比直接“无网络”体验好太多。5. 从固定对话到AI Agent可持续演进的交互架构5.1 第一版为什么不可持续说实话我们的第一版就是网上最常见的“ESP32-S3 HTTP POST大模型API”玩法设备录音上传到后端后端拿文本调LLM返回一段文本设备TTS播出来。如果用户说“明天7点叫我起床”我们就在后端写正则匹配看到“闹钟”和“7点”就拼一个闹钟指令。这套第一版跑通只花了两天但很快发现三个问题新增一个功能要同时改后端和大模型的提示词还要改设备端动作逻辑改动全链路都跑一遍LLM经常返回些稀奇古怪的文本设备端没法统一处理只能原样TTS出来用户意图一旦跨功能组合比如“明天天气好就叫我去跑步”硬编码逻辑基本无解问题的本质在于把“理解”和“执行”耦合在一起了。LLM擅长理解设备擅长执行中间应该有一个标准化的“动作协议”来衔接。5.2 引入Agent后的动作协议设计我们借鉴了AI Agent的tool calling思想做了一套非常轻量的设备动作协议。云端大模型的作用不再直接输出用户要的文本而是输出一个结构化的“动作意图”例如{ intent: alarm_set, params: { time: 07:00, repeat: [monday, tuesday] }, tts: 好的周一和周二早上7点提醒你我会准时叫你起床。 }设备端不需要理解“alarm_set”背后的业务逻辑它只负责执行动作列表。新增支持一种设备动作需要改动设备固件但如果只是新增一个“技能”——比如健康打卡、成语接龙、心情陪伴——不需要改固件只需在云端注册技能描述、触发规则和参数schema再由LLM根据用户请求选择并填充参数。这套设计思路和当前主流的AI Agent框架是一致的。我们后端用 Spring AI 作为模型的统一接入层好处是它对Qwen、DeepSeek、GLM这类主流模型都有统一接口模型切换只改配置不改代码。Spring AI 里同样提供了函数调用能力我们把这些函数注册成设备能力让LLM自己去路由。5.3 设备端事件总线与原子动作设备端这边我们做了两件事一是统一了“事件”的概念二是统一了“动作”的概念。事件包括唤醒成功、用户说完话、按键按下、定时器到点、传感器触发。动作包括播放TTS、播放URL音频、设置闹钟、点亮LED、马达震动、静音、进入OTA模式。事件产生后进入事件队列设备端动作执行器循环消费事件队列。这套事件总线的价值在后期才体现出来。比如“讲完一个故事后LED自动切换成呼吸灯”这种组合行为以前要写专门的任务现在只需要在云端配置一个“故事结束”事件和“LED呼吸灯”动作的绑定规则。设备端代码一行没动新行为就上线了。5.4 新增一个技能只需要几步实际演示一下假如我们要给设备加一个“成语接龙”技能云端新增一个技能服务接收成语接龙的请求并返回“下一个成语”和解释在技能注册表里添加配置技能名、描述、可调用的设备动作TTS播放、超时等待Spring AI 里注册一个函数定义把“cy_jielong”关联到这个技能服务用户对设备说“我们来玩成语接龙”LLM 判断意图后调用技能返回动作序列设备端正常执行TTS和等待结束整个过程不需要重新烧录固件也不需要改设备端代码。这是我认为“可持续演进”最实在的体现设备端稳定云端活跃能力以配置和函数为单位扩展而不是以固件版本为单位扩展。6. 演进过程中踩过的坑三个差点拖垮项目的工程问题6.1 音频buffer溢出与内存碎片第一个坑出现在第一轮长时间运行测试。设备连续跑了两小时后声音开始频繁出现“滋滋”底噪重启后恢复再过几小时又复发。排查过程先在日志里看音频任务的溢出计数发现从某个时间点开始ring buffer溢出次数飙升到几万。进一步定位发现控制循环里有任务在不停动态申请内存释放内存时间一长导致PSRAM和内部RAM都出现碎片大的音频buffer申请不到只能退而求其次用小块buffer于是音频链条的节奏被打乱。修复方法是两点所有语音链路的内存改成启动时一次性静态分配运行时绝不动态申请所有循环中用到的临时对象尽量复用避免频繁malloc/free内存碎片是嵌入式开发的经典问题在音频这种实时性要求高的场景会被无限放大。写代码时的建议很简单设备端程序的内存分配在main函数里统一规划好。6.2 集体OTA之后MQTT重连风暴有一次我们灰度发布新固件给几十台测试设备同时推送OTA。结果OTA完成后云端的MQTT broker连接数瞬间涨了三倍部分设备被broker主动踢下线然后这些设备立刻重连形成恶性循环。原因很清楚所有设备都是同一时间收到新固件、同一时间重启、同一时间发起MQTT连接造成的“重连风暴”把broker打爆了。我们的解决措施分成三层设备端MQTT重连采用指数退避加随机抖动首次失败0.5秒第二次1秒第三次2秒上限30秒每次加一个0-500ms的随机偏移云端broker的会话过期时间调短避免大量死会话长期占用资源发布运维OTA分批推送每批间隔30-60秒避免设备集体上线这套组合拳之后再没出现过重连风暴。顺便说一句指数退避加抖动这段逻辑建议任何做IoT设备的人都提前写上平时用不上一用就救命。6.3 云端响应文案可配置化最后一个坑比较隐蔽设备在异常场景下播报的文案——比如“我没听清”“网络出问题了”“这个技能我还在学”——第一版是硬编码在设备里的。后来产品要调文案结果每次都要发一版OTA来回折腾非常痛苦。我们把这类文案全部搬到云端配置里设备在启动时拉取一次之后每次用到文案时先从本地缓存查询找不到再用默认值。这样产品改文案、调整语气都不需要动设备。这个改动虽然小但对团队协作方式的改变是巨大的设备端开发者不用再陪着产品改文案走发布流程产品自己也能完成迭代。做硬件团队一定要尽早划分“硬件稳定边界”和“业务快速迭代区”不然会被每一次微小的产品改动拖死。如果让我重新做一次这个项目我会在第一天就把动作协议、事件总线和云端配置下发这三件事定下来而不是等版本迭代时再补。设备端的代码可以写得保守、稳定、无趣但云端一定要灵活、快速、可配置这才是AI陪伴这类产品能持续演进的根本逻辑。最后再分享一个小技巧开发阶段一定要把设备日志和事件自动上报打通我们后期定位所有问题几乎都靠日志回放这比任何调试器都管用。
返回列表