ESP圆屏不跑大模型:后台大脑+前端语音客户端的架构实践 📅 发布时间:2026/9/10 5:32:24 👁 浏览次数: 糖球系列走到第三篇这块 ESP 圆屏总算是把身份彻底想明白了它不跑模型它就是后台的语音客户端。很多人跟我一样看到圆形屏幕的 ESP32 开发板第一反应永远是那句“能不能把大模型放进去让它离线也能聊”。我承认我一开始也这么幻想过直到我真去算了算模型体积、内存占用和推理功耗之后这个念头几乎是在同一天被打消的。圆屏背后的那颗芯片从出生那天起就不是奔着推理端去的它是交互端是声音的入口和出口。这篇文章我把整套思路摊开讲清楚为什么 ESP 圆屏本地跑大模型是一个典型的伪需求它在整个语音助手系统里到底扮演什么角色音频链路怎么打通后台模型怎么接以及我在实际开发中踩过的那些编译、网络、显示上的坑。如果你手头正好有一块圆屏开发板正打算做语音助手这篇文章应该能帮你省掉不少弯路。1. 圆屏不是推理设备它是交互前端1.1 先算一笔账这块圆屏到底能跑什么我手上这块圆屏的核心芯片是 ESP32-S3双核 240MHz带 8MB PSRAM。单看参数在 MCU 里确实算能打的了但放到模型推理这个场景里这个配置连门槛都够不到。我拿现在主流的端侧小模型来算过一笔账一个 1B 参数的模型哪怕用 4bit 量化权重文件也要 500MB 起步而 S3 的 Flash 通常只有 8MB 到 16MBPSRAM 更只有 8MB。不用继续算结论已经出来了——装不下。就算你真的想办法把模型塞进去了推理速度也是灾难级的。生成一个 token 需要几十毫秒甚至上百毫秒一段语音回复几百个 token算下来用户要盯着屏幕等几十秒。在这期间芯片满负荷运转功耗飙升一个小圆屏热得能当暖手宝。这种体验说实话连“能用”都算不上。那这块芯片能跑什么我实际测试下来能在本地稳定跑起来的模型主要就两类一类是唤醒词模型比如乐鑫 ESP-SR 里的 WakeNet负责识别几个固定的唤醒词一类是语音活动检测模型也就是 VAD负责判断“当前有没有人说话”。这两类模型本质都是轻量级分类器参数量以几十万到几百万计Flash 占用几百 KB推理一次几毫秒到几十毫秒放在 MCU 上是完全没有问题的。1.2 本地跑不动不代表这个方案不行很多人会把“本地跑不了模型”和“这个方向走不通”画等号这是做嵌入式语音最容易钻的牛角尖。我后来换了个角度想用户在意的从来不是“模型跑在哪”而是“回复质量好不好、响应快不快、交互顺不顺”。把模型放到后台之后前端就彻底瘦身了。圆屏只负责三件事录音、播放、显示状态。这三件事恰恰是 MCU 最擅长的工作。后台那边随便挂一个支持流式输出的模型服务别说是 7B、14B 的参数规模就算直接接商业 API体验也比本地硬跑强了不止一个量级。所以我把这套架构叫做“后台是大脑圆屏是嘴和耳朵”。语音识别、语义理解、内容生成、语音合成全部放后台ESP 圆屏作为一个语音客户端老老实实把手里的声音数据送出去再把后台回传的音频完整地放出来。划分完这条边界之后整个项目突然就变得非常简单和清爽开发工作量也从“研究模型部署”变成了“搞定音频链路”。2. 系统怎么分工后台是大脑圆屏是嘴和耳朵2.1 一次完整对话的链路拆解要理解这套架构最好的方式是把一次完整的语音对话从头到尾走一遍。我举个实际场景用户对着圆屏说了一句“糖球今天天气怎么样”。第一步本地唤醒词模型在低功耗监听模式下持续运行屏幕上显示一个待机呼吸灯动画。当它识别到“糖球”这个唤醒词之后系统立刻从监听模式切入录音模式。第二步ESP 通过 I2S 接口从数字麦克风采集音频数据通常是 16kHz 采样率、16bit 位深、单声道的 PCM 数据按固定分片大小打包通过网络协议实时推送到后台。第三步后台收到音频流之后先做语音识别ASR把音频转成文字“今天天气怎么样”接着把文字交给大语言模型LLM生成一段回答文本再把回答文本交给语音合成模块TTS最终合成为一段音频数据。第四步后台把音频数据按协议切分成小块通过流式通道回传给 ESP 圆屏。第五步ESP 收到音频流之后边收边通过 I2S 输出到音频 codec驱动扬声器播放同时圆屏上显示声波动画或状态提示。整个链路看起来环节很多但每一步的分工都极其清晰。圆屏全程不参与任何“理解”和“生成”它只做时间敏感度最高的事情采集和播放。2.2 哪些能力必须留本地哪些必须上后台我整理过一张分工表把语音助手涉及的能力模块拆开来看放在哪一端其实非常清晰能力模块放置位置原因唤醒词检测本地需要 24 小时低功耗监听网络状态不可依赖本地模型足够胜任语音活动检测VAD本地用于判断说话起始/结束要求低延迟本地处理避免网络抖动影响判断回声消除AEC本地在采集端做信号处理最直接否则远端处理会造成明显的延迟感语音识别ASR后台词表大、需要语言模型本地跑不动高质量模型语义理解/内容生成LLM后台核心智力模块本地根本无法承载语音合成TTS后台高质量音色模型动辄几百 MB本地不现实这张表我后来在好几个项目里复用基本都能成立。有一个容易被忽略的点是回声消除必须放在本地。我最初把 AEC 放到后台做结果声音经过网络来回再到后台处理后延迟已经到了让人无法接受的程度。后来痛定思痛把 AEC 和降噪都放到本地 codec 芯片里处理效果立竿见影。2.3 通信协议选型为什么是 WebSocket 而不是 HTTP客户端与后台的通信方式我一开始图省事用了 HTTP简单粗暴录完一整段音频POST 给后台等结果回来再播放。但这种方式的体验很糟糕主要有两个问题第一用户必须说完一整句话之后才会得到反馈中间没有任何中间态等待感极强第二大语言模型的生成是流式的首 token 很快后面慢慢吐如果用 HTTP 就得等全文生成完才能一起拿到白白浪费了流式带来的响应速度优势。后来我改成了 WebSocket实测下来整个交互丝滑了很多。WebSocket 天然是双向长连接支持全双工通信ESP 这边可以一边往后台推音频帧后台一边往回推 TTS 音频帧整个过程完全不需要轮询也不存在 HTTP 那种频繁建连的开销。对于 ESP 这种资源受限的设备来说维持一个长连接比每轮对话重新握手的开销小得多。在 WebSocket 消息设计上我做了两类消息二进制音频帧和 JSON 控制消息。音频帧前 4 个字节用一个小头部标明帧序号和数据长度后台按帧序重组控制消息用于传递唤醒事件、对话结束标记、错误码等。这样一个 channel 同时承载控制流和音频流代码写起来也简单不至于把协议设计复杂。3. 音频采集与播放客户端工作的核心难点3.1 录音端I2S 配置、采样率与数据格式整个客户端最核心的代码就是音频采集。我用的麦克风是 I2S 接口的数字 MEMS 麦好处是不需要额外的 ADC直接输出数字信号抗干扰能力强接线也少。ESP-IDF 的 I2S 驱动配置起来不算难但有几个参数必须抠明白。采样率方面后台 ASR 服务要求的是 16kHz 单声道 16bit这个其实也是行业事实标准。需要注意的是I2S 的 MCLK 和 BCLK 时钟必须按照 codec 或麦克风的具体要求来配。同样是数字麦克风有的需要 32 倍采样率的 BCLK有的需要 64 倍我在第一次接一个国产麦克风时就被这个问题卡了半天对不到数据后来翻 datasheet 才发现是 BCLK 分频配错了。录音数据的处理上ESP 的 I2S DMA 会把数据直接送到内存环形缓冲区。我设置的缓冲区大小是 3200 字节按 16kHz/16bit/单声道算大约是 100ms 的音频数据。后台语音识别要求的分片大小通常在 20ms 到 100ms 之间100ms 一帧既能保证实时性又不会因为太碎而浪费网络包头开销。实测下来这个尺寸在 WiFi 环境下丢帧率很低。这里我贴一段核心的 I2S 配置代码注意看采样率和缓冲区设置i2s_config_t i2s_cfg { .mode I2S_MODE_MASTER | I2S_MODE_RX, .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, .intr_alloc_flags ESP_INTR_FLAG_LEVEL1, .dma_buf_count 8, .dma_buf_len 400, };dma_buf_len 我设成了 400也就是每个 DMA 缓冲 400 个采样点约 25ms 音频。8 个缓冲循环使用总共能缓冲 200ms 的音频。这样做的目的是给 WiFi 网络抖动留出足够的缓冲余量不至于一瞬间的网络波动就把录音数据冲没了。3.2 播放端流式缓冲与断帧保护播放端的核心要求和录音端不太一样。录音端要保证数据实时往上送播放端要保证数据平滑放出去。后台回传的 TTS 音频是流式的可能突然来一小段隔几十毫秒又来一段如果播放端不做缓冲扬声器就会一卡一卡的听感非常差。我在播放链路上加了两个缓冲层一个网络接收队列一个 I2S 播放环形缓冲区。网络接收队列负责接收并缓存从 WebSocket 收到的音频帧按帧序号排序后写入播放环形缓冲。I2S 播放端则不关心网络只管从环形缓冲持续取数据写入 I2S DMA写到没数据时就输出静音继续等待。播放的采样率不一定和录音相同。我用的后台 TTS 服务输出是 24kHz 单声道 16bit而录音是 16kHz两个采样率在同一个系统里共存。ESP 内部其实有专门的重采样器模块但我为了省事直接要求后台在 TTS 端把音频转成 16kHz 再推给我这样播放和录音共用一套采样率配置代码里少了很多转换逻辑。如果你的后台服务不能改采样率那就得在 ESP 侧做重采样这会吃掉一部分 CPU 算力实测大概多占 10% 左右的负载也不算什么大问题。3.3 状态机管理什么该说什么该听音频客户端最怕出现一种情况系统自己播放 TTS 的声音麦克风又把这个声音录进去重新上传给后台造成无限循环。要解决这个问题光靠本地回声消除还不够必须在软件层面引入一条严格的状态机。我把客户端的状态定义为四态IDLE空闲、RECORDING录音、WAITING等待回复、PLAYING播放回复。每次唤醒词触发后从 IDLE 进入 RECORDINGVAD 检测到用户停顿超过 800ms或者录音长度达到上限 15 秒自动结束录音进入 WAITING后台开始回推音频前先发一个控制消息通知 ESPESP 收到后进入 PLAYING整段音频播放完毕回到 IDLE。状态机的好处是逻辑闭环每个状态都有明确的进入条件和退出条件。我最开始没做状态机只是用几个布尔变量互相控制结果一个“用户说话时被自己的 TTS 打断”就把整个流程搞乱了。后来改成状态机之后代码好维护了bug 也少了一大半。画状态转移的时候记得把异常分支也考虑进去比如网络超时一定要加一个超时退出到 IDLE 的路径否则系统就卡在 WAITING 里再也听不到下一句话。4. 圆屏 UI 与交互设计不跑模型但它会“表演”4.1 用 LVGL 把状态“画”出来圆屏最大的卖点当然是那块圆形的显示屏。既然它不跑模型这块屏的价值就得在交互体验上体现出来。我用的 UI 库是 LVGL在 ESP32-S3 上跑得很流畅遇到圆形屏幕关键是要开启圆形裁剪避免方形区域边角漏光影响观感。另外 LVGL 的 flush 回调用 DMA 加速实测整屏刷新帧率能到 30fps 以上做动态效果完全够用。在界面设计上我把状态机里的四个状态全都转成了视觉表现。IDLE 状态显示一个微弱的呼吸圆环中心是“糖球”两个字像待机一样安静RECORDING 状态显示实时声波动画用 LVGL 的弧形控件模拟环形频谱音量越大圆弧越长WAITING 状态显示一个三点流水动画表示“思考中”PLAYING 状态显示动态的音量柱同时颜色会随着播放节奏变化。做这层 UI 的过程让我体会很深交互前端的光环可能不如“能跑模型”听起来高级但用户的真实感受恰恰来自这些细节。屏幕配合声音让整台设备看起来非常聪明即使它自己什么都没想明白。4.2 交互细节打断、超时与误唤醒有了 UI 之后交互细节就成了决定体验高低的关键。我把几个高频交互场景都过了一遍总结成三条必须处理的策略。第一打断机制。用户在 TTS 播放回复的时候直接说话应该立刻停止播放重新进入录音状态。这个逻辑看起来简单实现时要在状态机里加一个 PLAYING 到 RECORDING 的转移条件。我用的触发方式是触摸圆屏任意位置做打断比单纯靠声音检测判定打断要准确得多不容易被环境音误触发。第二超时保护。唤醒之后如果用户一直不说话或者后台请求超时系统不能无限等下去。我设了两个超时时间唤醒后 5 秒内检测不到语音自动回到 IDLE后台请求超过 10 秒没有响应播放一声提示音后重置连接并回到 IDLE。这两个超时在我实际使用中出现频率非常高是稳定性的关键防线。第三误唤醒抑制。WakeNet 在 TV 声音、敲键盘等场景下偶尔会误触发尤其当唤醒词设置成常见音节组合时。我的做法是叠加一路 VAD 置信度判断唤醒词触发之后先录 300ms 环境音如果 VAD 判断没有真实人声特征就忽略这次唤醒。这个方案的误唤醒率比我预想的低很多值得推荐。5. 编译、烧录与网络问题的避坑记录5.1 ESP-IDF 编译期常见报错在 Windows 环境下用 ESP-IDF 开发最经典的一个问题就是编译过程莫名其妙被打断报错信息类似“ninja.exe 已终止退出代码 1”。我第一次遇到时查了很久后来发现大部分情况是两个原因造成的一是 Windows 杀毒软件实时扫描在编译过程中锁定了临时文件ninja 读写冲突直接退出二是项目路径里有中文或特殊字符导致某些工具链子进程无法解析路径。解决办法是把整个 ESP-IDF 环境、项目目录都放到纯英文路径下比如D:\Projects\esp_sugar然后给 ESP-IDF 的 Python 虚拟环境和 ninja 目录添加杀毒软件白名单。另外编译过程中尽量不要开着太多占用 CPU 的工具ESP-IDF 全量编译本身就很吃 CPU资源紧张时更容易触发各种奇怪问题。还有一个非常容易踩的内存配置问题ESP32-S3 的默认分区表里如果跟着示例代码跑Flash 分区可能没有为音频数据或升级包留足空间。搭配大屏和 LVGL 之后代码和资源文件体积会急剧膨胀编译或烧录时经常报分区溢出。我用的是自定义分区表给 app 分区留了 6MB另外单独划了一个 4MB 的存储分区放音频缓存和配置文件这一步基本一劳永逸。5.2 网络异常时的降级策略圆屏作为一个网络客户端最怕的场景就是后台服务不可达或者 WiFi 信号弱导致音频流频繁中断。我做了三级降级策略实测对体验的提升非常明显。一级降级是本地超时提示。网络已经断了但还没到彻底超时此时后台没有任何响应ESP 在 5 秒后播放一段本地预置的提示音“网络连接失败请稍后再试”然后回到 IDLE。这个提示音我用的是 I2S 直接播放不受后台影响。二级降级是自动重连。WebSocket 断开后ESP 进入指数退避重连模式第一次等 2 秒第二次等 4 秒最长等待 30 秒。重连成功后自动发送一条设备上线消息让后台恢复对话上下文。这个机制保证后台服务重启或者网络波动恢复后设备不需要用户手动干预就能自动恢复工作。三级降级是本地录音缓存。如果录音过程中发现网络发送失败我会把音频暂存在 PSRAM 里最多缓存 10 秒音频等网络恢复后再补传。这个策略在 WiFi 信号不稳定的房间测试时效果很好至少能让用户的语音不白说。代价是代码复杂度增加建议等项目核心链路稳定之后再考虑加这一层。6. 糖球系列的下一步我能想到的几个扩展方向6.1 给圆屏加一个“离线兜底”小模型虽然本地跑不了大模型但不代表完全不能加本地智能。我现在正在试验的是在 ESP 上额外加载一个极小的意图分类模型专门识别“打开灯”“关闭风扇”“播放音乐”这类固定指令。这个模型不需要生成开放式内容只需要做一次几毫秒的分类推理一旦置信度足够高就直接在本地执行不用走后台。这个做法相当于给整个系统加了一条低延迟的快速响应通道。用户发出固定指令时圆屏几乎零延迟就能执行体验非常爽遇到开放式问题再走后台两个通道互补。模型大小控制在 800KB 以内Flash 完全放得下推理速度也在可接受范围。如果你对 ESP-NN 或者 TFLite Micro 熟悉这条路挺值得尝试的。6.2 多块圆屏共用一个后台服务糖球系列目前只有一块圆屏但我后续计划做一个多设备版本客厅一个圆屏卧室一个圆屏全部连接同一个后台语音服务。这里的难点在于后台需要维护多路音频连接的状态并处理设备之间的唤醒冲突。比如用户在客厅唤醒设备并开始对话卧室的设备接收到同样的声音就必须冻结住不能也跟着响应。后台做这种状态同步比在 ESP 端做要容易得多也正好说明这套“圆屏是客户端”的架构在多设备场景下的天然优势。前端瘦身之后增加一个设备只是增加一个 WebSocket 客户端而已后台完全可以横向扩展。这套架构的价值会随着设备数量的增加体现得越来越明显。6.3 断网本地场景的终极体验方案有人在评论区问过我如果完全断网这套系统是不是就变成废物了。其实也不完全是。即使后台完全不可用圆屏本地依然可以做三件事本地唤醒词检测、本地 VAD 语音活动判断、固定指令的本地执行。再加上本地预置的 TTS 提示音、倒计时、秒表、气温显示这些纯本地的功能它依然是一块称职的桌面小设备。我的最终目标是让圆屏在任何网络环境下都能有一个“最低可用体验”网络良好时是聪明绝顶的语音助手断网时也能当一个听话的本地小管家。这两者并不矛盾靠的都是把“客户端”这个角色做到极致。我个人在实际开发中的体会是接手一个硬件产品时最难的不是代码而是想清楚边界。你能做什么、不能做什么、什么该交给更擅长的人去做提前想明白项目才会顺。ESP 圆屏不跑模型这件事我一开始觉得是一种妥协做完之后反而觉得这恰恰是这个产品最清醒的设计。如果你也在做圆屏语音助手先从“客户端”这个角色入手绝对是最稳的起点。