ESP32-S3打造AI陪伴设备:从硬件选型到端云协同实战
2026/9/7 10:37:21 网站建设 项目流程

1. 为什么是 ESP32-S3:AI 陪伴设备的第一块硬件拼图

1.1 对比了几块板子之后,我为什么留在了 S3 上

做 AI 陪伴设备这件事,最早并不是从一个宏大愿景开始的,而是因为家里那台智能音箱实在太“公事公办”了。我想要的是一个能识别不同家庭成员、能记住前几天聊过的内容、会在早上主动提醒天气和日程的“小东西”,而不是一个只会执行命令的喇叭。这个需求落到硬件选型上,就成了一个非常现实的问题:用哪块板子做端侧基础。

当时我手头有 ESP32、ESP32-C3、ESP32-S2、ESP32-S3 和树莓派 Zero 2W,直接摆在一起做了个对照测试。树莓派 Zero 2W 功能强,但功耗和体积对“陪伴设备”来说都不太友好,而且开机启动流程在锂电池供电场景下很别扭。ESP32 比较老,单核 240MHz 虽然也能跑,但做语音唤醒的同时再去做 Wi-Fi 传输,任务调度会变得很紧,经常出现音频断流。ESP32-C3 是 RISC-V 架构,功耗低、价格低,但外设没 S3 丰富,后期想接摄像头做视觉扩展基本要推倒重来。

最后留下的,就是 ESP32-S3。它最有价值的点在于:两个 Xtensa LX7 内核、最高 240MHz 主频,支持向量指令加速,可以外扩大容量 PSRAM,而且原生支持 USB OTG 和摄像头接口。这意味着什么?意味着我可以在一个 MCU 上同时跑音频采集、唤醒词识别、网络传输,甚至后续加一个低成本摄像头做多模态输入,而不需要为每个功能单独加一块芯片。对于一款要长期演进的陪伴设备,这种“可扩展性”比纸面性能重要得多。

很多人选型时只看主频和内存,实际上做端侧语音设备,第一优先级应该是外设链路和内存带宽。S3 的 PSRAM 让我能放心地缓存音频流、放较小的神经网络模型,Wi-Fi 和 BLE 是同一颗芯片原生支持,不需要外挂模组,天线设计也不用太折腾。这才是它成为“第一块硬件拼图”的根本原因。

1.2 影响选型的三个关键参数:PSRAM、算力、外设链路

如果只让我说三个影响最终决策的参数,我会按以下顺序排列。

第一是 PSRAM 容量。ESP32-S3 常见模组有 2MB 和 8MB PSRAM 两个配置,我建议不要选 2MB 的。原因不复杂:语音陪伴设备的音频缓冲、唤醒词模型的中间特征图、TTS 播放前的音频数据缓存,都会吃内存。8MB PSRAM 版本在跑完系统、网络协议栈和音频链路后,还能剩下相当可观的空间给业务逻辑。实际测试下来,我用 8MB PSRAM 版本,空闲堆内存在 5.5MB 以上,这在做 OTA 差分升级和日志缓冲时非常从容。

第二是双核架构。音频采集和播放都是实时任务,稍微一卡顿就会出现爆音或者丢字。S3 的双核让我可以做一个核专门跑音频中断和 DMA 回调,另一个核跑 Wi-Fi 协议栈和业务逻辑。这个分工在单核板子上很难实现,经验是单核在“音频+网络”并发场景下,CPU 中断延迟会明显升高,体现在用户体验上就是唤醒后响应慢、语音断断续续。

第三是外设链路的丰富程度。我当时就想清楚了一件事:陪伴设备的演进方向一定是多模态。除了麦克风,后面可能加摄像头、触摸传感器、温湿度传感器、甚至一个小屏幕。S3 的 LCD 接口、摄像头 DVP 接口、多路 I2S、ADC、触摸传感都自带,不需要额外用 IO 模拟或者 SPI 转接,这让硬件 PCB 的改动量大幅降低。

从成本角度看,S3 模组比 ESP32 贵几块钱,但对量产设备来说,这点成本差异换来的是不用改版就能加功能的灵活性。我个人的判断是:如果在 2024 年之后做 AI 陪伴类的端侧设备,S3 就是最稳妥的起点。

2. 设备端“听和说”的基础:音频链路和配网怎么搭

2.1 从麦克风到 I2S:把声音送进模型之前要做的处理

音频链路是整个设备最容易被低估的部分。很多人以为买一个麦克风模块,接上 I2S 引脚,就能开始采集声音了。实际上,从麦克风到数据能被唤醒模型使用,中间涉及采样率设计、增益控制、回声消除、降噪、语音活动检测五个环节。

我选的是 INMP441 这类 MEMS 数字麦克风模块,输出直接是 I2S 信号,不需要额外的模拟放大电路。ESP32-S3 的 I2S 外设配置也不复杂,核心是对齐采样率和 DMA 缓冲。

i2s_config_t i2s_config = { .mode = I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX, .sample_rate = 16000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .dma_buf_count = 8, .dma_buf_len = 1024, .use_apll = true, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1 };

注意三点:采样率不要用 48kHz,用 16kHz。云端 ASR 模型大多在 16kHz 下训练,直接用本机 16kHz 可以减少音频重采样带来的延迟和失真。use_apll一定要开,APLL 能降低 I2S 时钟抖动,这对后端语音识别效果影响很大。DMA buffer 数量至少 8 个,长度 1024,太少会出现底层数据覆盖,太多会增加音频延迟,8 和 1024 是我反复试出来的平衡点。

麦克风采集到的原始 PCM 数据,首先要经过回声消除(AEC)。因为设备在播放 TTS 语音时,麦克风会把扬声器的声音一起收进来,如果不做 AEC,唤醒词识别会被自己设备的声音干扰,甚至出现自己喊自己、自己答自己的循环。ESP-ADF 里提供了 AEC 组件的接口,思路是拿参考信号(扬声器播放的数据)和麦克风输入做自适应滤波,将扬声器声音从麦克风信号中减去。

2.2 唤醒词、VAD 与大模型接口之间的分工

端侧 AI 必须明确的一点是:不要把大模型的所有链路都塞进设备里。ESP32-S3 跑不了百亿参数模型,但跑唤醒词检测是完全没问题的。

我的设计是三层音频处理:

  • 第一层是唤醒词检测,永远在设备本机跑。我使用了轻量级关键词识别模型,比如乐鑫提供的 WakeNet 或者自训练的 MobileNet 类小模型,内存占用大约 300KB 到 600KB,推理一次在 30ms 到 80ms。
  • 第二层是语音活动检测(VAD),也是设备端跑。唤醒之后并不代表用户一定在说话,VAD 的作用是检测语音的起点和终点,把有效的语音片段切出来。这层处理可以避免把整段的静音、背景噪声上传到云端,节省流量也降低响应延迟。
  • 第三层才是云端 ASR 和 LLM。设备切好音频片段后,通过 WebSocket 或 HTTP 上传到云端语音识别服务,得到文本后再送入大模型生成回复。

这套分工的核心理由是延迟和成本。如果全部走云端唤醒,意味着设备要一直保持音频上传,功耗、流量、云端费用全都很高。而本地唤醒可以让设备在空闲时几乎不进音频数据,只在检测到唤醒词后才启动上传链路。

VAD 的阈值调参是个细活。阈值太高会把用户的话切断,阈值太低会把后面的环境噪声一起上传。我最后采用的策略是:VAD 判定“有人开始说话”后,保留前面 300ms 的音频缓冲作为语音起点,然后持续检测到 800ms 静音才切断。这个参数对短句和停顿都有不错的适应度。

2.3 BLE 配网的工程实现:用户体验的第一道坎

纯语音设备没有屏幕,配网很容易做成灾难现场。我最开始用的 SoftAP 配网,设备开热点,手机连上去填 Wi-Fi 密码,体验非常折磨。后来换成了 BLE 配网,手机端 App 或小程序直接通过蓝牙把 Wi-Fi 的 SSID 和密码发给设备,设备再切换到 Wi-Fi 模式联网。

BLE 配网的实现不复杂,本质是让 ESP32-S3 同时开启 BLE 广播,手机连上后通过 GATT 协议写入 Wi-Fi 凭据。核心代码如下:

static esp_ble_gatts_cb_t gatts_cb; static void handle_write_event(esp_ble_gatts_cb_param_t *param) { uint8_t *data = param->write.value; size_t len = param->write.len; // 数据格式约定: [类型标记][长度][SSID][长度][密码] // 类型标记: 0x01 表示开始配网,0x02 表示连接结果上报 parse_wifi_credentials(data, len); esp_wifi_connect(); }

这里要注意的是,BLE 和 Wi-Fi 不能同时处于全速工作状态,因为它们共用同一根 2.4G 天线。所以在配网阶段,收到 Wi-Fi 凭据后要立刻停止 BLE 广播,切换到 Wi-Fi 连接模式。连接失败时再切回 BLE,并返回错误码。整个切换过程如果时序没处理好,最常见的表现是 Wi-Fi 一直连不上,或者连上了但 BLE 断不开导致卡死。

配网之外,设备需要一个稳定的“上线”标志。我规定设备必须在本地 flash 保存一个configured标志,配网成功后标记为真,如果 Wi-Fi 密码变更需要重新配网,则通过长按按键或云端远程清除标志。

3. 端云架构的分层逻辑:哪些事必须在端上做,哪些必须上云

3.1 我们最终确定的拓扑:端侧/云端/模型层

端云架构听起来很宏大,但落到这个小设备上,其实就是把一句话说清楚:设备负责感知和执行,云端负责思考和记忆。

这张拓扑图在我脑子里转了很长时间,直到一个用户场景把它彻底逼清楚了。当时我在测试一个连续对话功能,用户问“你觉得我昨晚睡得怎么样”,设备需要回忆前一天晚上的传感器数据和昨晚对话的内容。如果这些数据全部放在设备本地,S3 的 flash 和内存根本装不下多天的记录。如果全部放云端,每次对话都要把历史刷一遍,延迟和费用都不可控。

最后的架构分了三层:

  • 端侧(ESP32-S3):音频采集、播放、唤醒词和 VAD、按键/状态机、传感器采集(温湿度、IMU、光感)、网络连接管理、本地小模型推理。
  • 云侧代理层(Agent Gateway):负责维护设备的连接状态,处理 ASR 语音转写、LLM 调用、TTS 合成、记忆存储、日程管理,以及对外部 API(天气、提醒、搜索)的调用。
  • 模型层:底层接私有的 Llama 系模型或第三方大模型 API,通过代理层统一封装,设备端完全不用关心模型版本和供应商是谁。

设备到云端代理层的连接方式,我最终选择了 WebSocket 长连接而不是 HTTP 轮询。原因是语音交互天然是双向的——设备需要主动上报状态,云端也可能随时推送消息(比如提醒、新消息通知)给设备。WebSocket 长连接把双向通信的建连成本降到了最低,实测在室内环境下,设备 30 秒内不掉线的稳定性在 99% 以上。

3.2 会话与状态管理:别让大模型“失忆”

大模型的上下文窗口是有限且昂贵的,AI 陪伴设备最容易毁掉体验的地方,就是让模型在每一轮对话中都“失忆”。如果不做会话管理,用户上午说“我养了一只猫叫年糕”,下午问“年糕今天乖吗”,模型完全不知道年糕是谁。

我的做法是在云侧代理层里维护一个“会话状态对象”,数据按角色分层:

  • 设备档案:设备名称、型号、所在房间、绑定的家庭成员,这些是长期不变量,每次请求都会注入系统提示词。
  • 对话摘要:每完成一轮有意义的多轮对话,就调用一次摘要模型,把这段对话压缩成一句话存到数据库。当新一轮请求发生时,摘要会作为上下文注入。这比把全部历史消息拼进 prompt 更省 token,且输出更稳定。
  • 短期消息队列:最近 6 轮原始消息保留,做“短期记忆”;更早的历史只存摘要,做“长期记忆”。

落地时的核心代码逻辑大致是:

history = get_short_term_memory(device_id) # 最近6轮 summary = get_long_term_memory(device_id) # 对话摘要 user_info = get_user_profile(device_id) # 家庭成员档案 messages = [ {"role": "system", "content": build_system_prompt_with_context(summary, user_info)}, ] + history + [{"role": "user", "content": user_text}] response = await llm_client.chat(messages=messages, tools=TOOLS) save_exchange(device_id, user_text, response)

这个设计让设备在算力零成本的情况下,拥有了无限长的记忆能力。很多刚开始做 AI 硬件的团队会把“记忆”简单理解为把聊天记录存下来,其实是错误的。真正的记忆应该是经过压缩和提炼的“摘要 + 关键事实”结构。

3.3 与云端大模型交互的接口设计:流式输出和函数调用

设备与人自然对话的体验,很大程度上取决于 TTS 的响应速度。如果大模型要把整段话生成完才交给 TTS,用户要等十几秒。如果采用流式输出,LLM 输出一小段文本后立刻交给 TTS 合成,再让 TTS 边生成边播放,感觉会完全不同。

代理层和大模型的交互使用 SSE 或 WebSocket 流式接口。大模型输出完第一个完整句子,就用标点切分,把句子送给 TTS 模块合成音频分段,再推送到设备播放。实测从用户停止说话到设备开始说话,这个“首句延迟”可以被压缩到 1.2 秒以内。

函数调用是另一个必须做对的地方。陪伴设备不能只会聊天,还需要能做实事,比如“设一个明早 7 点的闹钟”“查一下明天的天气”。这些动作靠 LLM 自己完成不了,需要给它定义工具函数。

我在代理层定义了一套 JSON Schema 格式的工具集:

{ "name": "set_alarm", "description": "设置闹钟", "parameters": { "type": "object", "properties": { "time": {"type": "string", "description": "闹钟时间,格式 HH:MM"}, "repeat": {"type": "array", "items": {"type": "string"}, "description": "重复天数"} }, "required": ["time"] } }

当 LLM 判断用户意图需要调用设备功能时,会返回一个结构化请求,代理层解析后通过 WebSocket 下发指令到 ESP32-S3,设备端执行完成后回传结果。这个“端侧动作执行 + 云端逻辑决策”的模式,让设备成为了一个具备实际操作能力的 AI Agent 终端,而非单纯的聊天工具。

4. 可持续演进的三个支撑点:协议、OTA 和可插拔能力

4.1 消息协议设计:为 3 个月后的新功能留好扩展位

做硬件产品最怕的是“硬件做好了但软件更新不上去”。我的观点是:协议设计是为了让设备在 3 年后还能被云端控制,而不需要用户换硬件。

设备与云端的通信协议,我没有直接用裸 JSON,因为 MCU 端解析 JSON 会消耗不少 CPU。最终选了 JSON 作为调试和日志格式,但在实际业务控制信道上使用了更高效的 CBOR 二进制序列化。不过对读者来说,如果你团队不大、设备量不大,先用 JSON 也完全够用,重点是消息结构设计。

我给每条消息设计了三段式结构:

{ "v": 1, // 协议版本 "msg_id": "uuid", // 消息唯一ID,用于去重和应答 "type": "state.report", // 消息类型:设备上报/云指令/事件通知 "payload": {} // 具体数据 }

协议版本号v是必须的,但更重要的是事件类型的命名方式。我使用domain.action这种语义化命名,比如alarm.setmedia.playota.start。好处是新增功能时不需要改旧消息结构,云端能直接处理新类型的消息,不认识的类型也不会报错,只会记录一条 warning。这种设计让我在后面增加摄像头拍照功能时,只加了一个vision.capture事件,设备固件和云端就能无缝对接。

4.2 固件升级与远程配置:出问题不用拆壳子

OTA 是可持续演进里最关键的工程能力。没有 OTA,任何固件 bug 都得用户手动插线刷机,这对消费级产品是致命的。

ESP32-S3 的 OTA 我使用了双分区方案:一个 running 分区,一个 pending 分区。固件下载完成后写入 pending 分区并校验签名,确认无误后切换启动分区。如果新固件启动失败,看门狗会自动让设备回滚到上一版。这个机制在我后续测试中救过很多次。

关键是,OTA 不能只在大版本迭代时才想到。从第一天起,每一版固件都应该支持 OTA。我的经验是:给 OTA 模块留出至少 1.5MB 的虚拟分区空间,代码里永远保留一个“回滚到上一个版本”的按钮(通过云端指令触发)。

除了固件 OTA,我还实现了“远程配置下发”。配置和固件的区别在于:配置是热更新、不需要重启的。例如设备的音量、唤醒词灵敏度、TTS 音色、提示词模板,这些参数用云端下发一个 JSON 配置就能修改。我把它做成一个单独的 MQTT topic(使用 MQTT over WebSocket),设备收到配置后写入 NVS,下次对话时自动生效。

4.3 多模态扩展:从纯语音陪伴到带视觉的陪伴

既然标题提到了“可持续演进”,就一定要聊聊多模态扩展。ESP32-S3 的 USB OTG 和摄像头接口在这里派上了用场。

第一版我只有音频。测试一段时间后,我发现陪伴设备如果完全看不到环境,用户需要靠语音描述很多事,体验很割裂。后来我在 S3 上接了一个 USB 摄像头模组,数据走 UVC 采集 640x480 黑白/彩色图,当用户说“看看我在哪”或者“这是什么花”的时候,设备抓一帧图上传到云端的视觉模型处理。

有人会说“用户隐私怎么办”,我的处理方式是:摄像头默认物理关闭,需要用户明确用语音或按键打开,且抓拍图片在云端处理完立即删除,设备端不保存原图。这个设计现在已经成为产品的一项卖点,而不是负担。

多模态扩展的关键不在硬件,而在事件触发机制。我在端侧定义了一套统一的“传感器事件”模型,不管来的是音频唤醒、摄像头人体检测、PIR 移动触发,还是 IMU 姿态变化,都归一化成标准事件上报云端。云端根据设备当前状态决定是否调用视觉模型、是否主动推送语音。这让我在加新传感器的时候,固件侧改动量非常小。

5. 落地过程中的硬坑与优化记录

5.1 内存不足、栈溢出、Wi-Fi 掉线:最折磨人的三个问题

这里我得说点大实话。网上关于 ESP32-S3 的教程文章大多停留在“点亮一个 LED”的水平,很少有把真实调试中的坑写出来的。下面这三个问题是我在开发中实际花时间最多的地方。

内存不足。一开始我没用 PSRAM 管理好,有段时间堆内存只剩 20KB,系统随机重启。排查下来有两处元凶:一处是 TLS 握手的缓冲区默认占用了不少内部 RAM,另一处是 TLS 长连接保持后,每个连接至少吃 40KB 到 60KB。解决方式是把 HTTP/TLS 的 buffer 迁移到 PSRAM,只保留关键 DMA buffer 在内部 RAM。ESP-IDF 里配置CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL为较小的值,强制大块分配走 PSRAM。

栈溢出。音频任务如果用默认栈大小,容易在低延迟音频回调中爆栈。最典型的表现是系统运行正常但有随机复位,Task watchdog报错。后来我把所有音频相关任务栈都调到了 8192 以上,问题消失。经验是:MCU 上任务栈不省,宁可多分配几百字节,也不要去赌一个恰好够用的大小。

Wi-Fi 掉线。当设备长时间不活跃,ESP32-S3 的 Wi-Fi 会进入省电模式,但如果 RF 信号不好,省电模式下的重连机制会导致连接迟迟恢复不了。解决方式有两条:一是设置 Wi-Fi 保活周期,每 60 秒发一次空数据帧探测连接;二是把CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE关闭,避免断线状态仍保持功耗调整。

5.2 麦克风回采、回声抑制和唤醒率调优

语音设备绕不开回声问题。我第一版直接把麦克风放在扬声器旁边,唤醒成功率低到没法用。后来做三点改动才真正改善:

  • 硬件上麦克风尽量远离扬声器,两者之间加硅胶隔垫。如果结构允许,用双麦克风做波束成形,效果远超单麦。
  • 软件上开启 AEC,并且必须把扬声器的原始 PCM 数据接到 AEC 模块的 reference 端口。只靠麦克风信号做盲分离,效果非常差。
  • 调唤醒阈值时要在实际房间背景噪声下采集数据,不要只在安静的办公桌上测。唤醒率不是越高越好,误唤醒更让人头疼。我的最终阈值是“唤醒率 95%,误唤醒每 24 小时不超过 2 次”。

5.3 端到端性能数据:一版方案的实际资源占用

下面是这个方案在硬件上的实测数据,供后来者参考。

项目数值备注
固件体积2.3 MB含 Wi-Fi/BLE 协议栈、音频链路、OTA 支持
空闲内部 RAM约 70 KB已排除 WiFi/TLS 缓冲
可用 PSRAM约 5.2 MB8MB 版本,含音频缓冲和模型
本地唤醒词推理延迟约 35 ms双核并行,单核推理
唤醒到首句回复延迟1.1~1.6 s视云端 ASR + LLM + TTS 速度波动
待机功耗约 350 mW开启低功耗 Wi-Fi 模式,不含扬声器
播放 TTS 时功耗约 1.2 W3W 扬声器功率输出

这组数据说明一个事情:纯端侧跑大模型在 ESP32-S3 上是不现实的,但这并不妨碍它成为一个优秀的“感知和执行终端”。把端侧的时延敏感任务做扎实,把云端的智能推理做强大,两者的结合点才是 AI 陪伴设备真正的护城河。

写在最后:从能用原型到可演进产品的几个习惯

如果这条项目经验只能留下几句话,我会浓缩成三条。

第一,先把音频链路跑通,再谈 AI 功能。很多 AI 硬件原型死在“大模型会说话但听不见”上,音频采集、回声消除、缓冲区管理这些基本功,值得花两到三周去打磨。

第二,从第一天就设计好 OTA 和远程日志。设备一旦交到用户手里,你没有串口,看不到日志,唯一的“眼睛”就是远程日志上报和 OTA 回滚能力。这条做好了,后续所有迭代速度都会大幅提升。

第三,架构上的分层永远只为“变化”服务。当初如果把会话状态、工具调用、多模态事件全部写死在固件里,现在加一个摄像头功能就得重新刷机。而把决策放在云端代理层、把执行放在设备端,新功能更像是给云端加一段代码,设备端只做“收到指令,执行动作”这一件事。

从一个开发板到一个真正能陪人聊天、能记住重要事情、能调用闹钟和天气服务的设备,中间的路其实不算长,但每一步都是实打实的工程决策。希望这篇梳理能帮你少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询