资讯动态

ESP32圆屏不跑模型?手把手搭建后台语音客户端

发布时间:2026/9/8 11:57:37 来源:尧图企业网站定制
倒腾离线语音助手的时候很多人上来就想在板子上跑一个大模型。我之前也是这么干的结果算力、内存、发热全给你脸色看。做“糖球”这个系列第三篇我干脆换个思路圆屏的 ESP 板子不碰模型推理它只做一个后台的语音客户端把麦克风采集到的声音喂给电脑或者服务器上的模型再把模型返回的文本或语音在圆屏上呈现出来。这套架构跑通之后整机响应快、功耗低、还不用整天担心模型跑到一半 OOM。这篇文章就聊清楚两件事为什么让 ESP 圆屏“退居二线”才是对的选择以及这个语音客户端从硬件到代码是怎么一步步搭起来的。适合手里有 ESP32-S3 圆屏、想自己做语音助手又不想在板子上硬塞模型的同学也适合准备搞分布式语音设备的人当个参考。1. 思路来源为什么让 ESP 圆屏只当“话筒和屏幕”1.1 模型的重量现实的压力一开始我也试过把唤醒词模型放到 ESP32 上跑用的是 300KB 左右的小模型勉强能跑但一开 Wi-Fi、一刷新屏幕推理时间就开始飘。后来换更大的语音识别模型直接内存不够编译都过不去。再说实话哪怕你勉强放下了性能也远不如后端流式中文识别的准确率、上下文理解、甚至多轮对话你在板子上根本做不出像样的效果。不如把“重”的部分放出去。1.2 糖球系列的整体架构逻辑糖球系列的定位是“桌面小语音助理”核心是这颗圆屏。为了保持外观和成本板子不可能用很高的算力。所以我在设计上拆成两层前端设备糖球ESP32-S3 圆形 LCD 麦克风 扬声器负责采集音频、播放回复、显示状态动画。后端服务电脑或局域网服务器跑语音识别、意图理解、大模型对话、语音合成把最终结果传回前端。这样前端无论换了什么语言模型逻辑都不动你甚至可以在后端从离线模型切到在线 API整套系统不需要改板子代码。这就是把设备设计成“后台语音客户端”的核心价值它不决定智力它只决定你说话的方式和听到的方式。2. 硬件选型与屏驱那些坑2.1 圆屏选择GC9A01 还是 ST7789V圆屏现在常见两种驱动GC9A01 和 ST7789V都是 240x240 分辨率。GC9A01 是原生圆屏四个角是裁掉的SPI 时序标准ST7789V 是方形屏加圆形窗口。我用的是 GC9A01原因是它的旋转偏移和坐标计算更简单不像 ST7789V 还要设置圆形显示区域偏移。接线基本是这么一组GPIO11 – SCL / CLKGPIO10 – SDA / DINGPIO9 – RES复位GPIO8 – DC数据/命令GPIO7 – CSGPIO6 – BL背光可用 PWM 控制一开始我直接用普通的TFT_eSPI库驱动配置里把GC9A01_DRIVER打开屏幕能点亮但颜色偏色。后来发现是因为我没有校准TFT_RGB_ORDER和TFT_INVERSION_ON不同批次的屏差挺大建议拿到屏先画一条红绿蓝渐变肉眼确认顺序。2.2 音频采集从 INMP441 到 ES7243语音客户端最重要的输入是麦克风。我一开始选 INMP441I2S 数字输出接线简单SCK / BCLK → GPIO4WS / LRCLK → GPIO5SD / DATA → GPIO3L/R 选择脚直接接地表示左声道INMP441 的信噪比还行但一致性一般买到体质差的底噪偏大。后来我换了 ES7243属于高性能 ADC 方案采样率和增益可以通过 I2C 配置在圆屏 PCB 上集成度更高底噪也要低一些。不过如果你只是自己打样玩INMP441 完全够用省事。关键是采样位宽要统一成 32 位还是 16 位后端需要你传什么格式你就要统一。我这边后端要的是 16kHz、16bit、单声道小端字节序所以代码里做了重采样和格式转换。2.3 板级搭建要点圆屏设备内部空间不大天线部分尤其要注意ESP32-S3 的 PCB 天线不要贴着排线走线否则 Wi-Fi 信号会明显劣化。我第一版就是天线被屏排线盖住结果重启后偶发连不上网。后来把天线区域在 PCB 上挖空出来问题就没了。另外一个容易踩的坑是电源。语音播放瞬间电流会拉到 200mA 往上如果 LDO 余量不够屏幕会自动闪烁严重的话会直接复位。我用的是输入 5V、输出 3.3V、最大 1A 的稳压并且在扬声器输出级加了 220uF 电容。3. 语音客户端的核心实现3.1 麦克风数据的采集与缓存音频采集我用了两块 DMA 缓冲轮流接收 I2S 数据。ESPDMX 这种 DMA 机制可以有效避免采集线程被 Wi-Fi 任务打断导致数据断裂。关键代码如下i2s_config_t i2s_config { .mode (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate 16000, .bits_per_sample I2S_BITS_PER_SAMPLE_32BIT, .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 512, .use_apll false, .tx_desc_auto_clear false, .fixed_mclk 0 };采集回调里把 32 位 I2S 数据压缩成 16 位 PCM然后塞入一个环形队列。这里有一个关键取舍缓冲区不能设得太大否则从你开口到后端收到数据的延迟会叠加。我实测环形队列缓存 200ms 音频数据是比较合适的窗口既能应对 Wi-Fi 波动又不会太迟钝。3.2 WebSocket 长连接与音频流上传客户端要实时上传音频最稳妥的通道是 WebSocket。之前在组件评测里用 HTTP post 整包上传延迟高且代码臃肿后来改用 Arrow 还是 LWiP我用的是 Arduino-WebSocket 库封装连接后每 80ms 发一个音频包。使用的连接信息需要注意端口、路径和二进制分帧方式。我定义了简单的包头第1字节消息类型0x01表示音频帧0x02表示状态事件第2~3字节数据长度小端第4字节起PCM 数据这样后端解析方便前端也能复用同一套通道发送唤醒、点击、电池状态等事件。WebSocket 内部有帧校验重连机制比较成熟比裸 TCP 自己在应用层处理粘包省心。3.3 响应播放与 UI 状态机后端识别完并生成回复后会返回一段文本和一段 TTS 语音 URL 或 Base64 数据。语音客户端要做的一是播放语音二是把文本显示到圆屏上。播放我用 ESP32 的 I2S 输出驱动一个 MAX98357A 功放播放任务从队列里取音频块逐块写入 I2S。这时候要注意Wi-Fi 任务和播放任务并发很容易导致 GPIO 冲突。因为 I2S 播放需要连续时钟我就把播放任务优先级提到最高同时在小字库渲染和 Wi-Fi 接收之间做了时间片隔离保证声音不会断断续续。UI 状态机我抽象成几个状态IDLE、LISTENING、THINKING、SPEAKING。后端不同的音量动作通过事件触发切换圆屏上用不同颜色和动画表达比如THINKING状态转圈、SPEAKING状态显示声波频谱。这个状态机跑在独立任务里与网络通信解耦。4. 与模型侧的协作方式4.1 客户端与后端的通信协议要成为一个“后台语音客户端”通信协议要先定清楚。我不建议直接把前端逻辑和后端深度耦合最好在中间加一个网关层统一处理会话、鉴权、音频流和事件回调。我定的协议大概这样方向消息类型内容说明前端→后端AUDIO_FRAME16kHz / 16bit PCM 分片数据前端→后端EVENT_STATE唤醒、按下、电池低电量等后端→前端TEXT_REPLY回复文本带 session_id后端→前端AUDIO_REPLYTTS 音频数据MP3 或 PCM后端→前端STATE_CHANGE通知前端切换状态后端模型侧根本不用关心你用的是圆形屏还是方形屏它只管接收文本和音频、返回文本和音频。这个分离设计就是标题那句话的意义模型在后台糖球只是前台的语音客户端。4.2 wake word 与 VAD 处理逻辑很多语音设备喜欢一直录着音后台做唤醒词检测。但 ESP 算力弱跑唤醒模型仍然占用不少 CPU。我后来把唤醒词也放到了后端前端只做 VAD人声活动检测检测到连续人声后把音频流持续推送后端用自己的唤醒模型判断是否触发。VAD 的判断我用了能量阈值加零交叉率组合。因为麦克风位置和噪音环境不固定阈值要动态适应计算滑动窗口内音频平均能量。若当前窗口能量超过均值 1.6 倍且持续 80ms认为有人声开段。人声段持续 400ms 以上才向后台发出开始识别事件。这个方法不依赖模型代码量少实际用下来误触发率也能接受。4.3 断线重连与状态同步无线的环境总有波动语音客户端不能一断线就死掉。我做了这几件事每次音频帧带序号后端做去重和排序避免乱序导致文本错乱。断线后前端进入本地“低功耗待机”状态屏幕常显时钟不再发送音频后台检测到重连后推送一次完整状态同步。后端在每次回复里带上session_id前端保存最近一条会话状态用于恢复画面。在实际测试中2.4GHz 频段里 Wi-Fi 和数据传输有冲突偶尔会出现 300ms 左右的断流。因为做了序号重排和后端缓冲用户几乎感知不到。5. 踩坑实录与性能调优5.1 内存不足与 PSRAM 使用ESP32-S3 内部 RAM 有限如果你不搞 PSRAM音频缓冲、显示帧缓冲、WebSocket 缓冲区很快挤爆。我现在把大块缓冲都放到 PSRAMuint8_t *audio_buf (uint8_t*)heap_caps_malloc(10240, MALLOC_CAP_SPIRAM);屏幕的 DMA 缓冲也可以放到 PSRAM但这会抬升启动时间。我的经验是音频环形队列放 PSRAM显示帧缓冲还是放内部 RAM因为屏幕刷新 DMA 太依赖快速访问。如果你编译时遇到increase max_memory if possible这类错误基本就是内部 RAM 不足别硬调堆大小先检查哪些全局变量无意中占了太多内部 RAM。5.2 音频粘包与时序问题调试时会发现后端收到的音频偶尔出现粘连实际原因是 WebSocket 发送间隔不稳定。解决方法是把发送节奏从delay()改成基于时间戳的触发uint32_t next_tick 0; void audio_sender_loop() { if (millis() next_tick) { send_audio_frame(); next_tick millis() 80; } }不要依赖循环末尾的Delay否则遇到 Wi-Fi 优先级抢占音频包就会不均匀。测试下来80ms 对应每秒 12.5 帧直观听感延迟很小。5.3 屏幕卡顿与 DMA 优化圆屏刷新频率不高但做动画和波形显示时直接fillRect刷新全屏会严重卡顿。我后来只更新变更区域用双缓冲加pushImage局部刷新。如果波形更新频率 20fps视觉上已经够顺滑。一个隐性问题是 SPI 时钟和 I2S 时钟共用总线吗我的设计中屏幕 SPI 和音频 I2S 在不同 GPIO相互独立但 SPI 频率太高会干扰 I2S 时序。我实测 SPI 40MHz、I2S 16kHz 采样可以稳定共存如果 SPI 拉到 80MHz偶尔会有爆音。保守起见屏和音频同时使用时SPI 时钟设到 40MHz 就好。5.4 功耗与发热控制圆屏设备长时间处于IDLE和THINKING状态发热主要来自背光和 Wi-Fi。我在IDLE状态把背光亮度降到 30%CPU 频率切到 80MHz只有进入LISTENING和SPEAKING才把频率拉到 240MHz。实测整体功耗在待机时约 0.3W播放回复时最高约 0.8W。对于桌面设备来说完全可以接受板子基本不烫。如果你发现模型调用频繁导致 Wi-Fi 持续满速可以考虑在后端合并多段 TTS 音频减少长连接心跳频率。最后再分享一个小技巧在做这种“后台语音客户端”的时候把前端设备当成一个漂亮的开关所有聪明的事情都交给后台。你要维护的饼干粒子就少很多以后换更强的模型、换更大的语义库前端代码几乎不用动。我自己在糖球系列后面几篇还会扩展多设备并联、声源定位和个性化音色但整个地基就停留在“客户端”这三个字上。把重活推给该干活的地方设备才会又小、又省、又快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价