在 M5Stack Cardputer 上实现远程桌面串流(基于 H.264)
背景
几年前,我曾试过在 ESP32 上实现串流操作。
当时硬件搭建在面包板上,一快 1.14 寸的屏幕分辨率为 240x135,主控是初代 ESP32。
文章较为详细地描述了实现细节。
时隔五年,我决定重新制作一次,将硬件环境迁移到 M5Stack 家的 Cardputer。

额外说明
我的这块 Cardputer 被我手动升级过一次。原版主控使用的是 ESP32-S3FN8(内置 8MB Flash,无 PSRAM),我将其更换成了 ESP32-S3FH4R2。
牺牲了 4MB Flash,换来了 2MB 的 PSRAM。

上图是更换芯片后的特写。为了保护周围精细的贴片元件,我动用了铝箔胶带进行隔热。可以看到焊点周围还有一些未清理干净的助焊剂残留,虽然卖相一般,但它确实赋予了这台小机器“新生”。
为什么 PSRAM 是必须的:无 PSRAM 的“软压榨”实验
在决定更换芯片之前,我曾深度尝试过在 无 PSRAM 的原版 Cardputer 上跑通 H.264 解码,但这几乎是一次“不可能完成的任务”。
H.264 软件解码器对内存有刚性需求。即便在 240x136 这种极低分辨率下,DPB (Decoded Picture Buffer) 机制加上 SPS 序列参数集的激活,依然需要大量连续的、无碎片的内存块。而 ESP32-S3 的内部 RAM (SRAM) 在运行了 WiFi 协议栈、TCP/IP、WebSocket 服务之后,留给应用的连续空闲空间所剩无几。
为了省下内存,我曾尝试了以下极致操作:
- 移除 LVGL UI 系统:切换到纯裸机显示控制,收回了约 50KB 内存。
- 压榨核心组件:将 WiFi 的动态发送缓冲区从 32 压减至 4,静态接收缓冲区压至 3,收回约 45KB。
- 废弃双缓冲:采用“解码一帧 -> 实时转换 -> 立即推屏”的同步模型,试图消灭显示冗余。
然而,即便通过这些手段腾出了超过 100KB 的额外空间,esp-h264 库在初始化时依然会因为无法申请到足够的连续工作内存而报错。
结论: 对于 H.264 这种对参考帧有依赖的编码格式,PSRAM 是 ESP32-S3 设备的“续命药”。如果你也想玩实时视频流,手动焊接一颗带 PSRAM 的芯片是最高效的解决方案。
架构设计
由于有了额外的 PSRAM,我甚至可以直接在装置上进行 H.264 流的解码。
乐鑫官方提供了一个名为 esp_h264 的组件,能够在 ESP32-S3 上执行软件解码。据实际测试,2MB 的 PSRAM 刚好足以支撑 240x136 分辨率的解码工作(画面原本是 240x135,但 H.264 编码器要求分辨率必须是偶数)。
系统链路
为了保证极低的延迟,我重新设计了整个通信架构:
- 发送端 (Browser): 利用浏览器的
getDisplayMedia捕获桌面,调用WebCodecs进行硬编码,输出 H.264 Annex B 格式流(设置为 GOP=1,即全 I 帧模式),最后通过 WebSocket 发送二进制数据。 - 接收与缓冲 (ESP32-S3): Cardputer 作为 WebSocket Server 接收数据,并存入
RingBuffer(环形缓冲区) 进行平滑。 - 解码核心 (Decode Pipeline): 专门的解码任务从 RingBuffer 取出原始字节流,通过
esp_h264解码出 YUV420P 格式的图像。 - 颜色转换与显示: 解码后的图像实时转换为 RGB565 格式,随后通过 SPI DMA 直接写入 ST7789 屏幕。
核心挑战与解决方案
1. 多核调度与 WDT (看门狗) 报错
在最初的版本中,解码、颜色转换、屏幕刷新和 LVGL UI 更新都在同一个核心上运行,导致 CPU 占用率瞬间爆表,频繁触发 Task Watchdog 系统复位。
解决方案:
- 核心绑定:将 Core 0 分配给 WiFi 协议栈、WebSocket 服务和 LVGL UI 任务。
- 解码专核:将核心 1 (Core 1) 设置为高优先级任务,全力负责 H.264 解码、YUV 转 RGB 以及 LCD 位图刷新。
这种分工策略确保了即便在解码任务高负荷运行时,系统各组件依然能正常“喂狗”,保证了稳定性。

2. 内存管理
esp_h264 软件解码器需要大量的连续内存块。即便开启了 GOP=1,解码器依然会申请一定的空间用于参考帧管理。
我将解码器的所有的工作缓冲区都分配到了 PSRAM 中,而对实时性要求极高的双显示缓冲区与 SPI 描述符则保留在速度更快的 Internal RAM。
3. “跳帧追赶”算法解决延迟
网络波动是串流的大敌。如果数据由于瞬时带宽不足积压在缓冲区,显示的画面就会产生越来越大的延迟。
由于采用的是全 I 帧串流,我设计了一个简单的“追赶算法”:
解码任务在处理数据前,会先探测 RingBuffer 的积压情况。如果发现积攒了多个待解码帧,则直接丢弃所有旧帧,只解码并显示最后一帧。这个技巧让设备在网络恢复后能瞬间重回实时画面。
上位机设计
这次我没有使用 Python,而是直接在 Cardputer 上启动了一个精简的 Web Server 提供前端控制台。前端采用了 DaisyUI 构建界面,能够实时显示当前发送的比特率和流量统计。
为了适配 ESP32-S3 的算力,我将 WebCodecs 的编码配置调整为:
- 比特率:150 - 300 kbps (非常节省带宽)
- 关键帧频率:高频率刷新
- 分辨率:强制限制在 240x136
总结
从最初的 WDT 频繁崩溃,到如今能够实现稳定的 240x135 @ 15-20 FPS 串流,这个项目探索了 ESP32-S3 的极限负载能力。
相比于五年前的面包板方案,现在的 Cardputer 串流方案不仅更加完整(无需 Python 脚本作为桥接),而且在多核优化和内存分配上有了质的提升。即便在微控制器上,也能玩出非常极致的流媒体体验。
硬件建议:必须使用带有 PSRAM 的 ESP32-S3 型号。
在 M5Stack Cardputer 上实现远程桌面串流(基于 H.264)
https://chaosgoo.com/make-remote-streaming-on-m5stack-cardputer/
