去年底我把一块吃灰很久的微雪 ESP32-S3-N16R8 开发板翻了出来本意只是跑个屏幕 Demo结果不知不觉把它做成了一台能聊天、能记事、能讲冷笑话的 AI 陪伴设备。过程中最有价值的收获不是最终那个会发光的小盒子而是整套从硬件到云端的架构设计——一台设备能不能长期用下去、持续加功能在第一天选板子和画架构图的时候就决定了。这篇文章就来说说我踩过的坑、做过的取舍以及为什么 ESP32-S3 这类资源算不上富裕的芯片反而适合用来做端云架构的端侧载体。如果你是电子爱好者、AI 应用开发者或者想把自己的小硬件项目迭代成“准产品”的人这篇文章会回答几个核心问题为什么端云分工是这类设备的最优解、端侧那 240MHz 的双核到底该干什么活、云端服务怎么做才不会被某一个大模型厂商锁死以及如何让一坨代码升级了十几次之后老设备还能照常工作。中间会穿插大量可复现的接线参数、代码结构和踩坑记录方便你照着抄。1. 先把目标定清楚这不是做一个 Demo而是做一个能长期运行的设备很多人做 AI 硬件容易陷入一个误区先在云上把大模型 API 调通能聊天了然后就往开发板上一塞LCD 屏一亮就觉得完事。这样做出来的东西第一天很新鲜第二周就会因为连不上网、语音唤醒失灵、设备人格前后矛盾而被扔进抽屉。“可持续演进”这个词重点不在“演进”而在“可持续”。1.1 为什么选 ESP32-S3 而不是树莓派或其他芯片我最初也纠结过要不要换树莓派 Zero 2W甚至想过上一块全志的 Linux 小板子。但评估完陪伴设备的需求之后我把候选范围收窄到了 ESP32-S3。原因有几点首先它内置了 WiFi 和 BLE不需要外挂射频芯片天线匹配也都帮你做好这对一个外壳里要塞下电池、麦克风、喇叭的设备来说非常省空间也少了很多 RF 调试的麻烦。其次ESP32-S3 的双核 Xtensa LX7 虽然算力不算强但在这类设备里并不需要端侧跑大模型。真正吃算力的语音识别、大模型推理、语音合成都放在云端端侧只需要负责音频采集、唤醒词、网络通信和交互控制。8MB PSRAM 和 16MB Flash 的配置N16R8 型号对这类任务来说非常充裕甚至还有富余。第三外设接口很全。I2S 可以同时接数字麦克风和数字功放GPIO 够接圆形 LCD、按键、LED 灯环硬件设计上可以做到非常简洁不用复杂的电平转换。对比树莓派它性能强但启动要等 Linux 系统功耗高供电复杂关机还怕掉 SD 卡。对一台需要“随手一按就能说话”的陪伴设备来说ESP32-S3 这种上电几百毫秒就能出声音的特性反而是巨大的体验优势。1.2 产品形态倒推架构端侧做什么、云端做什么我习惯的做法是先从使用场景倒推功能再倒推架构。这台设备的使用场景很明确放在床头或书桌上用户靠近之后说一句唤醒词然后下达指令或闲聊它要能听清、能快速回答、能记住上次聊到哪里还要根据时间段表现出不同的语气比如深夜的回答会简短温柔一些早晨会催促喝水。这些功能里语音唤醒必须在端侧做否则每次交互都要把音频传到云端既费流量又费电隐私层面也说不过去。音频采集、按键检测、屏幕显示、状态提示音这些实时性要求高的活也必须在端侧完成。而语义理解、知识问答、情感回应、文本转语音播放这些重计算任务放到云端。这看起来是理所当然的分工但真正执行时会遇到一个问题哪些数据该上传、哪些格式能压缩、网络断了怎么办。这些细节在后面的章节里逐一展开。1.3 “可持续演进”到底指什么我把可持续演进拆成了三个具体目标模型可替换今天接的 Qwen明天想换 DeepSeek或者接一个本地部署的模型云端代码不能被某个厂商的 SDK 绑死。功能可扩展设备先只做一个聊天机器人过阵子想加“定时播报天气”“控制床头灯”这种技能端侧和云端都要有地方安放新逻辑而不是在原来的代码上疯狂打补丁。设备可升级用户手里的设备必须支持 OTA 升级而且升级过程要能断点续传、能失败回滚不然每次改版本都要用户重新烧录那只能算是实验室原型。带着这三个目标去做线框图、定协议、选通信方式很多选择就变得清晰起来。后面所有章节实际上都是围绕这三个目标展开的。2. 硬件电路设计哪些必须自己动手改哪些用现成模块硬件设计原则是能用现成模块的绝不自己画电路。毕竟这是个偏软件和架构的项目自己画四层板贴片焊接的周期太长没必要。但对于音频链路、供电这些影响体验的环节选择哪些模块、怎么接线需要花点心思。2.1 核心器件清单与选型理由我的硬件清单如下部件型号/规格选型理由主控板微雪 ESP32-S3-N16R816MB Flash 8MB PSRAMUSB 串口调试方便排针引出全部 GPIO显示屏GC9A01 圆形 1.28 英寸 LCD陪伴设备大多长着一张“圆脸”圆形屏视觉效果好GC9A01 是成熟且便宜的方案麦克风INMP441I2S 数字麦克风直接输出数字信号抗干扰好不需要模拟前端电路接 4 根线就能用功放/喇叭MAX98357A I2S 数字功放 3W 喇叭功率体积比合适免去 D 类功放的设计声音清晰电池18650 单节 充放电一体模块结构简单容量大测试方便不用设计复杂的电源管理辅助按键轻触开关若干物理按键用于配网、紧急打断比全靠语音更可靠关于微雪这块板子多说一句N16R8 里的 8MB PSRAM 对这个项目至关重要。CLI 交互时经常要缓存音频帧、JSON 解析结果、屏幕帧缓冲如果只有 512KB 内部 RAM代码会频繁触发内存分配失败。8MB 的 PSRAM 虽然在速度上比不上内部 RAM但存这类间歇性数据绰绰有余。2.2 I2S 麦克风与扬声器的接线与配置要点麦克风INMP441和功放MAX98357A都走 I2S但不要想着把它们挂成“同一组 I2S 引脚同步运行”。我实测发现麦克风和扬声器同时工作容易引发 I2S 总线的时钟竞争尤其 S3 的 GPIO 矩阵在配置上不如外置 MCU 灵活。稳妥做法是分两个 I2S 外设分别驱动麦克风用 I2S_NUM_0功放用 I2S_NUM_1引脚独立。接线上重点注意 INMP441 的 L/R 引脚它决定数据在左右声道中的位置。我习惯把 L/R 拉低表示输出到左声道对应代码里采样数据的左声道。如果 L/R 悬空或接错会采到一整片寂静或者重复一列的怪异数据。下面是 ESP-IDF 里麦克风侧的 I2S 配置核心代码#include driver/i2s_std.h #define I2S_MIC_SCK GPIO_NUM_4 #define I2S_MIC_WS GPIO_NUM_5 #define I2S_MIC_DIN GPIO_NUM_6 i2s_chan_handle_t mic_channel; i2s_chan_config_t chan_cfg I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); i2s_new_channel(chan_cfg, mic_channel, NULL); i2s_std_config_t std_cfg { .clk_cfg I2S_STD_CLK_DEFAULT_CONFIG(16000), // 16kHz 采样率 .slot_cfg I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .gpio_cfg { .mclk I2S_GPIO_UNUSED, .bclk I2S_MIC_SCK, .ws I2S_MIC_WS, .dout I2S_GPIO_UNUSED, .din I2S_MIC_DIN, .invert_flags { .mclk_inv false, .bclk_inv false, .ws_inv false, }, }, }; i2s_channel_init_std_mode(mic_channel, std_cfg); i2s_channel_enable(mic_channel);采样率定为 16000Hz、16bit、单声道是为了后续压缩传输。16kHz 已经覆盖语音大部分有效频段再高的采样率只是徒增网络流量ASR 端反而要降采样。喇叭侧 MAX98357A 的配置思路类似只是把 DIN 换成 DOUT同时初始化另一路 I2S_NUM_1采样率可以用 44100Hz 或 48000Hz因为你播放的 TTS 音频往往来自云端码率比较高转成 16bit 44.1kHz 更合适。2.3 电源与功耗长期运行第一要务陪伴设备往往通宵通电功耗不控制好就是一块持续发热的暖手宝。实测下来ESP32-S3 在无 WiFi 连接、CPU 跑 light sleep 时电流可以压到 40mA 上下但一旦 WiFi 保持长连接电流轻松跳到 100mA 以上。所以电源优化的核心是不要在毫无交互的时候保持 WiFi 活跃而是用唤醒事件驱动联网。我设计了两级唤醒机制第一级硬件定时器每隔 1 秒唤醒 CPU检测麦克风信号能量。如果连续几帧能量超过阈值立刻切到全速模式初始化 WiFi。第二级如果超过 10 分钟没有任何交互进入更深的 light sleep此时连定时器唤醒也拉长到 5 秒一次只保留 RTC 域的工作。18650 电池容量按 3000mAh 算平均工作电流 60mA理论续航超过 50 小时。如果只用按键交互、不依赖语音唤醒续航还能更长。这里没有上动态调频做极致优化因为这个功耗水平对于桌面设备来说已经够用。如果你要做随身设备就得把降频电源管理玩得更细甚至要考虑是否在空闲时关闭 PSRAM 供电。3. 端侧软件架构唤醒、采样、连接管理硬件就绪之后端侧软件决定了设备“好不好用”。ESP32-S3 的端侧软件我用了 ESP-IDF 框架没用 Arduino——虽然 Arduino 生态上手快但做多任务、内存管理、OTA 时需要更底层的控制ESP-IDF 在 FreeRTOS 之上提供了统一的多线程模型和事件循环长期看更稳。3.1 语音链路是怎么跑的从麦克风到云端端侧语音链路我用“流式分片”的思路麦克风持续采样但不是等用户说完一整句话再发送而是每积累 60ms 的音频就做一次预处理判断有没有人声能量然后把音频分片压缩后通过 WebSocket 推送到云端。这样做有几个好处降低第一报文延迟。用户按下按键或唤醒后 300ms 内云端就能收到第一片语音ASR语音识别可以提前开始工作而不是等说完再做语音活动检测。便于实现打断检测。如果端侧检测到用户停止说话超过阈值就发送一个vad_stop事件云端可以立刻对已接收的文本进行处理。减少内存占用。60ms 的 PCM 数据只有 1920 字节16k16bit0.06s缓存几片也不会吃光内存。音频编码上最简单的是直接传裸 PCM但 16kHz 16bit 单声道意味着每秒 32KBWiFi 传输没问题但可靠性差、延迟高、还容易被网络瓶颈卡住。我用轻量级的 ADPCM 编码把音频压缩到 4bit 每样本体积减为原来的四分之一端侧解码和编码的资源开销又极小。如果你的代码里空间宽裕可以用 Opus压缩比更好但 ESP-IDF 里集成 Opus 库会占用不少 Flash我用在另一个带更大 Flash 的项目上才划算。语音分片的 JSON 封装大致是{ type: audio_chunk, session_id: a3f2c1, seq: 0, codec: adpcm, duration_ms: 60, data: base64 }注意语音数据我用 Base64 放进 JSON虽然浪费了 33% 体积但换来了跨平台、跨协议的统一性云端解析时也是统一解码。如果后续带宽吃紧可以改成二进制 WebSocket 帧但通常会先推 JSON因为调试成本低。3.2 BLE 配网没有屏幕和键盘的设备的第一次联网真正的痛点来了。设备没有键盘和触摸屏怎么把 WiFi 密码烧进去传统做法是在 AP 模式下开一个网页但用户在手机浏览器里输密码的体验并不好而且容易因为网关 IP 冲突导致打不开配置页。我最终用 BLE 做配网这也是很多量产智能设备的通用做法热搜词里能看到不少人搜“esp32-s3 ble配网”说明这个需求真实存在。具体流程分四步设备启动后先检查有没有保存 WiFi 配置没有则自动进入配网模式开启 BLE GATT 服务。手机端小程序或 App 扫描到设备广播名连接后进行 WiFi SSID 和密码的写入。设备收到密码后尝试连接路由器连接成功就通过 BLE 回发一个成功标志再断开 BLE 并保存配置。配网超时 3 分钟没有任何写入操作自动关机或者进入低功耗待机避免一直开着蓝牙耗电。这里需要设计好 BLE 的服务特征值特征 UUID简写属性说明0xFF01Write写入 WiFi SSID0xFF02Write写入 WiFi 密码0xFF03Notify设备向手机回传配网状态0xFF04Write写入云端 API 地址可选用于多环境调试把云端 API 地址做成可写特征非常有用。开发时可以指向测试服务器量产后再通过远程配置覆盖成正式域名不需要重新烧固件。BLE 配网的代码很多但有个容易被忽略的细节ESP32-S3 的蓝牙和 WiFi 共用 2.4GHz 射频配网过程中如果先开启了 WiFi 扫描再开 BLE 广播两者会互相抢射频时间片导致配网时手机找不到设备。正确流程是先开 BLE再停止 WiFi配网成功后再关闭 BLE、打开 WiFi顺序绝对不能反。3.3 网络不稳定时的策略队列、重连与本地提示设备放在书桌上信号不一定稳定。尤其路由器重启、跨房间移动时WiFi 断连是常态。如果不做处理代码会在sendfailed的泥潭里越陷越深。我采用的是三层策略发送端维护一个环形音频队列最多缓存 2 秒的语音。断网期间用户说的话先存队列网络恢复后按顺序补发。网络任务用指数退避算法做重连第一次 1 秒第二次 2 秒第三次 4 秒最多等 30 秒。重连期间不影响按键和屏幕响应。如果 30 秒仍然无法连接设备主动提示“网络走丢了我还在”。这个语气提示很关键因为它让用户知道设备还活着而不是死机了。消息层面我放弃了 MQTT 作为主通信选择了 WebSocket。原因很简单对话是双向实时流MQTT 虽然有 broker 各种主题过滤但流式恢复和二进制大包处理反而不如 WebSocket 直观。MQTT 更适合设备上报遥测数据比如电量、温度、在线状态所以我的架构里两者并存遥测走 MQTT语音和文本交互走 WebSocket。4. 云端服务把大模型封装成一个会聊天的“人格”端侧搞得再花哨云端才是“AI 味”的来源。但这部分也往往是架构腐化最严重的区域。很多人的云端服务是一坨把所有逻辑塞在回调函数里的 Python Web 服务大模型 SDK 直接写在路由里、会话上下文用全局字典存着、换个模型就要改业务代码。这样的服务别说“可持续演进”撑过两周迭代都难。4.1 云端整体形态网关 会话服务 模型网关 记忆存储我最后落地的云端是一个多服务结构每个服务可以独立部署、独立扩容设备接入网关负责 WebSocket 连接管理、鉴权、协议解析、设备在线状态维护。会话服务负责对话状态机、上下文管理、意图路由、技能调度。模型网关适配不同大模型的统一接口层上层不直接感知底层模型。记忆存储短期使用 Redis长期使用 PostgreSQL 或向量库。技能服务天气、闹钟、百科等外部动作的独立服务。其中模型网关是“可持续演进”的关键工程。它做的事情很简单把不同大模型厂商的 API 差异封装掉。举例来说Qwen 的接口用dashscope包或者通过 OpenAI 兼容接口调用DeepSeek 和智谱 GLM 也支持 OpenAI 风格接口但温度参数范围、上下文消息格式仍有细微差别。如果业务代码里直接写某个 SDK那每次换模型都要改一大片。模型网关的做法是统一封装为内部的chat_completion(request)接口返回一个统一的流式协议。里面处理厂商密钥、模型名映射、超时重试、内容过滤以及最重要的 message 历史管理。模型网关还内置了一个简单的路由规则普通闲聊用价格更低的模型复杂逻辑推理用更强的模型这样能把成本削下来一半以上。我用 FastAPI 写了这个网关核心文件结构类似services/ gateway/ # WebSocket 长连接网关 session/ # 会话管理、状态机 model_gateway/ # 统一大模型接入层 skills/ # 技能调度 memory/ # 记忆读写这些服务互相之间通过内部 HTTP/gRPC 调用。前期服务量小用 HTTP 就够了等交互量大了再切 gRPC 也不迟。4.2 流式对话协议的设计从按下按键到收到回复的时间预算AI 陪伴设备最影响体验的指标是“唤醒到首次可听声音的时间”也就是 TTFBTime To First Byte。我给自己定的预算如下阶段耗时预算唤醒 连接确认 300ms语音分片上传 ASR 识别 800ms大模型首 token 返回 1000msTTS 合成首包返回 600ms端侧播放 100ms总计 2800ms三秒以内用户基本感知不到“卡”。要做到这个预算每一步都要抠。先说 ASR。我采用流式识别云端收到第一个音频分片就立即调用 ASR 的 partial result 接口识别过程中不断把中间结果发回端侧但端侧不显示也不语音播放只做日志。语音停止后ASR 的 final result 被送到会话服务。这样把识别时间和语音采集时间重叠而不是“采集 5 秒再识别 5 秒”。大模型环节流式输出是必然的。模型网关收到用户意图之后立刻把 instruction 与历史记录拼接然后发起 LLM 流式请求每生成一个 token 就通过 WebSocket 推向设备。设备端收到的是带标记的文本增量不等到完整回答才开始合成语音而是一边生成一边把已完成的自然语句送入 TTS。这个流水线并发执行能压缩不少感受延迟。协议帧格式我定为{ type: llm_delta, session_id: a3f2c1, content: 今天, final: false, meta: {model: qwen-plus} }当final变为 true 时设备端知道这句话播完可以执行断句停顿。整段回复结束后设备再发送一个reply_completed事件云端收到后才把这一轮消息写入记忆库。防止设备意外关机导致对话记录丢失。4.3 记忆与人格管理AI 陪伴设备不像工具它有“感情连续性”大模型本身没有记忆每次调用 API 时都需要携带历史消息否则它会忘了你三分钟前说过的话。但把所有历史都一股脑塞给模型也不现实模型上下文窗口有限而且陪伴类设备进入第二轮长聊后如果不对历史做裁剪一张请求能超出上限。记忆架构分三层短期记忆最近 10 轮对话全部放进请求上下文保证对话不“断片”。中期记忆当天关键事件、用户重复强调过的事情比如“我明天要早起”用 Redis 存结构化 JSON按 session 隔离。系统在构造 prompt 时把这些事插入到“当前用户状态”区块。长期记忆跨天的、值得长期保留的信息例如生日、偏好、喜欢的称呼写入 PostgreSQL 向量库。每次会话开始时用 embedding 检索与当前话题相关的旧记忆插入到上下文中。人格管理是陪伴设备的灵魂。我给设备设计了一个“人格包”的概念本质是一份 YAML 或 JSON 文件定义了名字、语气词、说话风格、知识边界。系统 prompt 的核心就是这份人格包。而且人格包可以云端动态更新今天它是个元气少女明天可以改成温柔大叔端侧完全不用改代码。人格包样例片段name: 小光 tone: 温暖、简洁、偶尔幽默 rules: - 每句话不超过25个字 - 称呼用户为你 - 当用户表达负面情绪时先共情再给建议 - 不知道的事情要承认不知道不要编造这套文件驱动的设计带来了巨大的可维护性。产品想调语气改配置即可不需要重新训练或者改业务代码。4.4 “技能”插槽设计让云端的 Agent 能力可持续扩展只靠聊天没法支撑设备的长期使用价值用户早晚会厌倦空谈。我在会话服务之上加了一层技能调度思路很接近 Function Calling / Agent 机制大模型在生成回复时如果判断需要外部信息就输出一个结构化的 function call而不是直接编造答案。以天气为例模型在对话中识别出用户想查天气不再答复“我好像没有联网”而是输出{ type: function_call, name: query_weather, arguments: {city: 北京, date: 2025-06-10} }会话服务看到这个 function call 就去调用天气服务的 HTTP API拿到结果之后把真实天气数据拼进 prompt让大模型基于数据继续生成回复。这个流程对大模型来说是透明的技能服务本身也不需要理解自然语言只管返回结构化数据。技能列表我用注册表维护新增技能只需要三步写一个独立技能服务、在注册表里声明 name 和参数 schema、定义技能的指令描述。剩下的事情模型会自动根据用户表述去选择是否调用。这比硬编码关键词规则健壮太多了。实际线上跑了两个月我加过闹钟、倒数日、菜谱推荐、附近书店查询这些技能端侧固件一次没动过。技能执行的安全边界也要考虑不是所有技能都允许设备主动触发。我用了一个简单的权限层级普通技能由模型自动触发高风险技能比如传播个人数据、修改系统配置需要设备端按键确认后才会真正执行。聊天陪伴设备不应该在没有用户知情的情况下把自己的状态发给外部服务。5. 可持续演进的落地手段OTA、配置下发与灰度架构分层得再清晰如果设备固件永远停留在出厂版本演进也无从谈起。这一节讲我怎么让上千行 C 代码“在不打扰用户的情况下自我更新”。5.1 OTA 固件升级用户设备不是测试板ESP32-S3 的 OTA 其实不复杂核心原理就是双分区OTA 分区 A/B 或者 A/BRollback。升级时固件包先下载到备用分区校验哈希和签名然后设置启动标志跳到新分区启动。如果新固件启动后几分钟内没有上报“我起来了”系统自动回滚到旧分区。我的 OTA 流程是云端识别到设备有新固件版本通过 WebSocket 下发一条ota_available消息。设备收到消息后检查电源状态。如果电池电量低于 30%延后升级避免升级中途断电变砖。设备请求固件下载地址。下载时用 HTTP Range 断点续传每块写盘前做 SHA-256 校验。下载完成全量校验后将当前运行状态保存到 NVS非易失存储然后调用esp_ota_set_boot_partition切换分区并重启。重启后先跑自检成功运行 5 分钟后向云端上报ota_done云端才最终结束这次发布。整个过程对用户基本无感。唯一会感知到的是设备屏幕弹出“我学会了一个新技能重启一下”的提示。签名验证务必开启。ESP32-S3 支持 secure boot但我没有在开发阶段启用硬件级 secure boot因为一旦开启烧错一次就要用 jtag 强制清除,很折腾。我采用软方案固件包内嵌 RSA 签名Bootloader 里用公共密钥验证签名再写入 OTA 分区。防不了大神级攻击者但对一台陪伴设备来说足够。5.2 端侧配置热更新不等发版就能改行为OTA 只解决“固件代码更新”的问题。更多时候我们只想改一下唤醒词、音量、服务器域名、人格温度参数完全没必要发版。因此我在端侧加了一个配置管理模块订阅云端的 MQTT 主题。默认配置放在 NVS 里云端配置更新时会下发一个增量 JSON 补丁{ op: replace, path: /audio/wakeword, value: 小光小光 }端侧解析补丁、合并到当前配置树然后决定哪些配置立即生效比如音量、唤醒灵敏度哪些需要重启生效比如 WiFi 相关的底层参数。这个机制极大提升了设备的“可玩性”。有时候早上调一下 prompt 里的语气规则下午就能在设备上感受到变化周期从“烧录固件”缩短到“云端改一行文案”。5.3 云端服务灰度与兼容性老设备也能用新模型可持续演进的另一面是保证服务端更新不把存量设备搞挂。我采用了两条规则第一API 版本永远带在路径或消息字段里例如v1/chat。新字段只做追加绝不删除旧字段。如果某个字段语义变化了新增一个字段名而不是把旧字段覆盖掉。第二模型网关只做“行为增强”不做“行为破坏”。比如切换到新模型之后如果新模型的流式输出节奏太慢网关会自动降到同步接口兜底保证设备端不会进入无限等待。灰度发布同样重要。我的做法是在网关里加一个简单的开关按设备 ID 哈希取模比如 10% 设备先接入新模型跑 24 小时观察报错率和平均响应耗时稳定后再慢慢扩到 50%、100%。这个灰度逻辑不复杂却能避免“全量上了一个有问题的提示词导致所有设备开始复读”之类的灾难。6. 实测中的坑与心得跑通容易跑稳才见功力如果你把这篇文章从上往下看到这里说明你确实想把它做成一个能长期运行的东西而不只是想刷个 LED。那么最后分享一些实践中积累的、比较难从文档里查到的坑和心得。6.1 麦克风采集到的声音是“碎”的I2S 与 WiFi 的共存问题我第一次把 INMP441 接入 S3开心地发现麦克风能出声了但还是有细微的爆音和周期性断片。查了很久才发现是 WiFi 天线和 I2S 外设争抢总线带宽引起的。ESP32-S3 的 WiFi 走 DMAI2S 也走 DMA当两个 DMA 同时频繁搬运数据总线仲裁造成 I2S 数据偶尔丢帧。解决办法有几个方向一是把 I2S 的时钟速度适当降低16kHz 采样率的时钟本来就慢但要在 RX 侧开足够深的 DMA buffer允许 WiFi 抢带宽时的数据暂存二是把麦克风数据通过i2s_channel_read一次性多读一点减少读取频次三是如果项目里同时跑着很多网络任务给语音任务分配更高 FreeRTOS 优先级。我自己最后的配置是麦克风 DMA buffer 深度设为 240 个样本每次读取 960 样本60ms语音任务优先级设为 5网络任务为 3。实测爆音基本消失。6.2 8MB PSRAM 也会不够内存优化与 JSON 解析开销别被 8MB PSRAM 的外表迷惑。程序里如果随意使用动态内存、频繁做 JSON 解析和字符串拷贝8MB 也会被快速吃光。我这里说的“吃光”倒不是内存占用绝对值超标而是频繁的内存碎片导致大块连续内存分配失败。我的优化策略是语音采集和网络发送共用一个环形缓冲区数据不再重复拷贝。JSON 解析不保留原始字符串解析后立刻把需要的数据提取成 C 结构体然后释放 JSON 对象。所有固定长度的字符串设备 ID、会话 ID、状态标签都预分配好空间不用动态malloc。屏幕的 LVGL 缓冲用小一点的尺寸比如 1/10 屏幕大小的分块刷新而不是给整个 240x240 分一个全量帧缓冲。这会牺牲一点刷新率但对静态界面来说毫无影响。实际内存消耗大概长这样模块RAM 占用FreeRTOS 内核120KBWiFi 协议栈220KBI2S 音频缓冲16KBLVGL 屏幕缓冲320KB业务逻辑 网络任务180KB余量约 1.2MBPSRAM只要你没有在中断回调里做大内存分配PSRAM 还是很够的。最大的隐患往往是三方库的默认缓冲区比如 MQTT 库默认收发 buffer 只有 2KB如果 JSON 全部塞进去会回调失败。6.3 GC9A01 圆形屏的色彩与视觉表现细节圆形屏很有吸引力但 GC9A01 的驱动有一些需要适应的地方。它对 SPI 频率敏感SPI 时钟太高时会出现色斑和雪花。我最终把 SPI 时钟锁在 40MHz 以下显示稳定和刷新率都过得去。另外屏的初始化序列最好参考芯片厂家的规格而不是随便抄别人的初始化数组因为不同批次屏幕在 gamma 上会有差异。实测下来用原厂序列的颜色更准。UI 上的经验是陪伴设备不适合放大量文字小屏幕更合适用表情、动画、呼吸灯之类的反馈。我用 LVGL 做了一个简单的“状态脸”网络的涟漪、听语音时声波、回答时雀跃表情。视觉反馈比文字状态栏更贴近“陪伴”的感觉。6.4 关于“陪伴感”端云架构之外的软实力最后聊点技术之外的。我见过很多 AI 硬件把精力全放在大模型对话质量上但设备真正让人愿意用的往往是那些细节唤醒成功时屏上闪过的光、说完话之后哪怕回答不对也能接住话题的语气、深夜自动把音量调低一丝的体贴。这些不完全是端云架构的范畴但架构设计时必须给它们留位置——端侧要有承载这些交互的场景引擎云端要有能下发这些规则的配置通道。所谓“可持续演进”最终服务的不是技术指标而是用户愿意长期把设备放在床头。当我观察到用户用了三个月还在对它说话、还会因为它的语气变化笑出来那时候我才觉得架构里的每一分设计都有了回报。如果你正在做类似的端云 AI 硬件我想给你的建议是先想清楚三省——设备能省多少电、云端能省多少带宽、用户能省多少操作成本然后才写第一行代码。架构的迭代速度永远赶不上需求变化真正扛住时间考验的往往就是你最初愿意多做的那一点点抽象设计。