ESP32-S3 JPEG 实测:从解码库对比到 DMA 优化,最终撞上 SPI 上限

前言

之前做了 ESP32-S3 上常见 JPEG 解码库(Tjpg_Decoder、JPEGDEC、ESP_NEW_JPEG)的性能测试,最终结论是 ESP_NEW_JPEG 在此前测试中表现最强(见 ESP32-S3 + Arduino 各种 JPEG 解码库速度对比,到底哪个才是最快的?勘误:ESP_NEW_JPEG 更新到最新版后,所有分辨率都是最快的)。

但是上次是用的静态图片,每次解码都是同样图片数据,与真实使用场景会有差异,毕竟如果是使用 ESP32-S3 播放视频,每一帧的内容都是不一样的,解码时间也会有波动。

这次直接从视频里抽帧,模拟一下真实视频播放场景,看看在这个条件下能把 FPS 推到多高。

另外上次测试时也没有优化解码与传输步骤,理论上可以使用 DMA 实现 CPU 时间更有效的利用,这次也优化一下代码,对比看看效果。

那么这次测试在以下两个条件下,最终结果会如何?

  1. 图片数据使用真实视频抽帧,每帧内容不同,更接近播放场景;
  2. 加入 DMA 和 Block 合并策略,测试“解码”和“上屏”能否真正并行。

先说结论:在 240×240 + RGB565 + SPI 40MHz 的配置下,最终逼近链路上限,实测约 40 FPS,再往上提升空间很小了 😃。


测试视频

最终性能测试视频

默认 Block 上屏流程演示


测试方案

esp32-jpeg-decode-optimize-2

测试硬件:

  • ESP32-S3-Zero 开发板,双核 240MHz,开启 PSRAM
  • 屏幕:ST7789 240×240,SPI 接口
  • PCB 设计时连接屏幕没有使用标准 SPI 引脚,使用了 GPIO Matrix,因此这里 SPI 总线时钟配置 40 MHz

测试数据:

  • 视频抽帧:2 秒共 60 帧,每帧内容各不相同
  • 导出格式:JPEG,质量 80
  • 分辨率:240×240

测试模式:

① FULL IMAGE + Sync SPI

  • 解码完成后,使用同步 SPI 将像素数据上屏;至于解码输出是分块还是整帧,取决于各解码库自身的默认行为。
  • 单帧最大占用内存:240×240×2 = 115,200 字节
  • SPI 阻塞传输,无 DMA

② Block + DMA

  • 边解码边用 SPI + DMA 推屏
  • 解码库每输出一个 Block,立刻 DMA 传输,解码与传输部分重叠
  • 细分为两种子模式:
    • 不合并 Strip:直接按解码库输出的 Block 尺寸发 DMA
    • 合并 Strip:把多个 Block 合并成 240×16 的整行再统一发

先搞清楚:MCU 是什么

在看数据之前,有个概念要说明一下——MCU(Minimum Coded Unit),JPEG 编码的最小单元。

为了方便理解,本文讨论的图片在当前采样配置下,可把 MCU 理解为常见的 16×16 输出块。

esp32-jpeg-decode-optimize-3

三个库处理 MCU 的方式差别挺大:

Decoder MCU 行为
Tjpg_Decoder 固定 16×16,任何情况都不合并
JPEGDEC 根据模式和分辨率计算,可以合并输出更大的 Strip
ESP_NEW_JPEG 自动合并到 240 宽度(整行输出)

需要注意的是:JPEGDEC 单次输出 Strip 的尺寸,依赖于内部的 iPitch 参数,而 iPitch 又受解码库里 MAX_BUFFERED_PIXELS(默认 2048)这个上限影响。

对于 240×240 图片,如果想单次输出整行(15 个 MCU),需要约 15 × 16 × 16 = 3840 像素的缓冲——如果有这个需求,记得同步修改库里的这个上限值。


测试结果

整体结果对比如下:

esp32-jpeg-decode-optimize-4

一、FULL IMAGE 模式

Decoder MCU Blocks Decode Avg(ms) Draw Avg(ms) FPS Avg
Tjpg_Decoder 16×16 225 29.49 51.44 12.36
JPEGDEC 128×16 30 18.07 28.53 21.46
ESP_NEW_JPEG 240×240 1 11.91 30.56 23.56

这个模式下,ESP_NEW_JPEG 解码速度最快(11.91ms),JPEGDEC 和 ESP_NEW_JPEG 的上屏时间接近,最终 FPS 都不高,最好也只有 23.56 FPS

原因很明显:FULL IMAGE 模式没有 DMA,SPI 是阻塞的,CPU 在等待 SPI 传输期间什么都做不了,解码再快也拉不开差距。上屏时间明显高于解码时间,已经成为主要耗时来源。

Tjpg_Decoder 的 Block 数高达 225 个(每帧要回调 225 次),虽然解码时间看着正常,但大量小块的调度开销也是累积的。ESP_NEW_JPEG 只需要回调 1 次(整帧),调度开销几乎为零。

FULL IMAGE 模式小结

这说明在同步 SPI 模式下,系统瓶颈根本不在 JPEG 解码,而在上屏阻塞;解码器再快,也只能等待 SPI。


二、Block + DMA 不合并 Strip

Decoder MCU DMA Send Decode Avg(ms) Draw Avg(ms) FPS Avg
Tjpg_Decoder 16×16 16×16 32.19 27.93 16.63
JPEGDEC 64×16 64×16 18.55 14.73 30.05
ESP_NEW_JPEG 240×16 240×16 6.70 17.83 40.78

加了 DMA 之后效果非常明显,ESP_NEW_JPEG 直接跑到了 40.78 FPS 🔥

ESP_NEW_JPEG 解码时间只有 6.70ms,比 Tjpg_Decoder 快了将近 5 倍。上屏时间 17.83ms 看着多,但这里其实已经是在等 DMA 空闲——SPI 传输能力到顶了。

值得关注的是 JPEGDEC:解码时间 18.55ms 与 FULL IMAGE 模式几乎相同,但上屏时间从 28.53ms 降到了 14.73ms,DMA 传输和解码的并行效果立竿见影。

Tjpg_Decoder 则因为每次只发 16×16 的小块,DMA 启动次数太多(每帧 225 次),开销反而比 FULL IMAGE 还略高,收益有限。

Block + DMA 不合并 Strip 小结

这说明 DMA 的价值不只是“更快传输”,而是让“解码”和“传输”两段流水线重叠起来,CPU 可以更早地进入下一帧的图像解码工作。

esp32-jpeg-decode-optimize-5

只要 DMA 传输时间不超过解码时间,FPS 就只会受限于解码速度了。


三、Block + DMA 合并 Strip

Decoder MCU DMA Send Decode Avg(ms) Draw Avg(ms) FPS Avg
Tjpg_Decoder 16×16 240×16 30.33 2.31 30.65
JPEGDEC 64×16 240×16 18.27 7.44 38.90
ESP_NEW_JPEG 240×16 240×16 6.72 17.83 40.72

① 合并 Strip 对 Tjpg_Decoder 的上屏时间影响巨大:从 27.93ms 直接降到 2.31ms!DMA 单次传输整行(240×16),减少了大量传输启动开销。

Tjpg_Decoder 合并 Strip 后,上屏时间几乎可以忽略不计,解码时间(30.33ms)才是瓶颈——整体 FPS 从 16.63 跳到 30.65,提升将近一倍,说明之前 DMA 频繁启动的开销有多大。

注意:这里 Tjpg_Decoder 的 2.31ms 容易产生误解,实际传输时间并没有消失,而是被流水线重叠后“吸收”进了解码阶段。

JPEGDEC 从 14.73ms 降到 7.44ms,FPS 涨到 38.90,已经非常接近 40 FPS 了。

③ ESP_NEW_JPEG 本来就是整行输出,合不合并对它没影响,依然稳在 40.72 FPS。这里的 17.83ms 上屏时间本质上不是软件额外开销,而是除去解码时间外 SPI 链路本身的传输耗时。这意味着优化空间已经不在解码侧,而在总线带宽侧。

Block + DMA 合并 Strip 小结

这说明当 Block 太碎时,DMA 启动成本会反过来吞噬收益。合并 Strip 的本质,是减少事务次数,而不是减少数据量。


当前配置下的理论 FPS 上限

根据前面给出的测试条件:

  • 分辨率 240×240
  • RGB565,2 字节每像素
  • 单帧 115,200 字节
  • SPI 40MHz

可以计算出来理论 FPS 上限:

240 × 240 × 2 = 115,200 Byte/frame
115,200 × 8 = 921,600 bit/frame
40,000,000 / 921,600 ≈ 43.4 frame/s

也就是说,在不考虑命令开销、窗口设置、DMA 调度损耗的理想情况下,上限大约就是 43 FPS。

实测 ESP_NEW_JPEG 跑到 40.7 FPS,已经非常接近 SPI 链路极限。


测试结论

经过这轮测试,几个结论比较清晰:

  1. DMA 影响巨大 — 有没有 DMA,FPS 差距可以超过一倍。不用 DMA 的 FULL IMAGE 模式,再快的解码库也被上屏拖累。
  2. 合并 Strip 很重要 — 特别是 Tjpg_Decoder,合并后上屏时间从 27.93ms 暴降到 2.31ms。减少 DMA 启动次数,开销差异极大。
  3. SPI 是最终瓶颈 — ESP_NEW_JPEG 的上屏时间 17.83ms 不是在等解码,是在等 DMA 传输完成。240×240 RGB565 画面,每帧 115,200 字节,40MHz SPI 理论极限就是这个帧率了。再怎么优化解码,SPI 这关过不去。
  4. ESP_NEW_JPEG 综合最强 — 解码时间最短(6.70ms),自动整行输出,在有其他任务占 CPU 的情况下优势更明显。
  5. JPEGDEC 有潜力但需要改库 — 想让它输出更大的 Strip,需要手动修改 MAX_BUFFERED_PIXELS 上限,默认 2048 不一定符合实际工作场景。

使用建议

  • DMA + Merge Strip 有一定编码成本,且竞态控制比较复杂,需要权衡利弊后再使用
  • 只做静态图片显示,优先选解码更快、接入更简单的方案
  • Block 很碎时,一定要想办法合并 Strip,否则 DMA 启动成本会吃掉收益
  • 如果已经接近 SPI 理论上限,还有更高 FPS 需求,可以更多从 SPI 带宽/换接口方向考虑

后续打算

目前在这套 SPI 40MHz 配置下,继续提升 FPS 的收益已经很有限了。不过还有几个方向可以研究:

  • 双核并行:如果要做彩色串流小电视,就还有网络任务,看看是否还能达到 40 FPS。
  • 更高 SPI 时钟:使用 ESP32-S3 原生 SPI 引脚,总线速度理论上可以到 80MHz,FPS 上限可以再提高一些。
  • 视频解码测试:拿真实视频文件直接测试,播放时 FPS 可以到多少。

先这样,下次再折腾 😂

参考资料

发表评论?

0 条评论。

发表评论


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>