文章目录
前言
之前做了 ESP32-S3 上常见 JPEG 解码库(Tjpg_Decoder、JPEGDEC、ESP_NEW_JPEG)的性能测试,最终结论是 ESP_NEW_JPEG 在此前测试中表现最强(见 ESP32-S3 + Arduino 各种 JPEG 解码库速度对比,到底哪个才是最快的?、勘误:ESP_NEW_JPEG 更新到最新版后,所有分辨率都是最快的)。
但是上次是用的静态图片,每次解码都是同样图片数据,与真实使用场景会有差异,毕竟如果是使用 ESP32-S3 播放视频,每一帧的内容都是不一样的,解码时间也会有波动。
这次直接从视频里抽帧,模拟一下真实视频播放场景,看看在这个条件下能把 FPS 推到多高。
另外上次测试时也没有优化解码与传输步骤,理论上可以使用 DMA 实现 CPU 时间更有效的利用,这次也优化一下代码,对比看看效果。
那么这次测试在以下两个条件下,最终结果会如何?
- 图片数据使用真实视频抽帧,每帧内容不同,更接近播放场景;
- 加入 DMA 和 Block 合并策略,测试“解码”和“上屏”能否真正并行。
先说结论:在 240×240 + RGB565 + SPI 40MHz 的配置下,最终逼近链路上限,实测约 40 FPS,再往上提升空间很小了 😃。
测试视频
最终性能测试视频
默认 Block 上屏流程演示
测试方案

测试硬件:
- 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 输出块。

三个库处理 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 像素的缓冲——如果有这个需求,记得同步修改库里的这个上限值。
测试结果
整体结果对比如下:

一、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 可以更早地进入下一帧的图像解码工作。

只要 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 链路极限。
测试结论
经过这轮测试,几个结论比较清晰:
- DMA 影响巨大 — 有没有 DMA,FPS 差距可以超过一倍。不用 DMA 的 FULL IMAGE 模式,再快的解码库也被上屏拖累。
- 合并 Strip 很重要 — 特别是 Tjpg_Decoder,合并后上屏时间从 27.93ms 暴降到 2.31ms。减少 DMA 启动次数,开销差异极大。
- SPI 是最终瓶颈 — ESP_NEW_JPEG 的上屏时间 17.83ms 不是在等解码,是在等 DMA 传输完成。240×240 RGB565 画面,每帧 115,200 字节,40MHz SPI 理论极限就是这个帧率了。再怎么优化解码,SPI 这关过不去。
- ESP_NEW_JPEG 综合最强 — 解码时间最短(6.70ms),自动整行输出,在有其他任务占 CPU 的情况下优势更明显。
- 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 条评论。