性能调优指南

概述

EmbeddedGUI 的性能调优主要围绕三个维度展开:PFB(局部帧缓冲)配置、SPI 传输策略、以及绘图 API 的选择。本文基于 QEMU 实测数据,给出各场景下的优化建议。

PFB 尺寸选择策略

PFB 是 EmbeddedGUI 的核心设计:屏幕被划分为多个小块,逐块渲染后传输到显示器。PFB 尺寸直接影响内存占用和渲染效率。

尺寸与性能的关系

根据 PFB 矩阵测试数据,不同尺寸的性能表现差异显著:

PFB 配置

内存 (RGB565)

分块数

适用场景

30x30 (default)

1.8 KB

64

RAM < 4KB 的极低资源 MCU

60x30 (medium)

3.6 KB

32

通用嵌入式场景,性价比最优

120x60 (large)

14.4 KB

8

RAM 充裕时的高性能选择

240x15 (fullwidth)

7.2 KB

16

行扫描优化,适合文本密集界面

240x240 (fullscreen)

112.5 KB

1

仅限 RAM > 128KB 的高端 MCU

选择建议

对于大多数图元绘制(矩形、圆形、圆弧等),PFB 尺寸对 CPU 计算耗时的影响不大(差异 < 10%)。但以下场景差异明显:

  1. 文本渲染:PFB 越大,字体缓存命中率越高。fullscreen 模式下 TEXT 耗时仅 0.6ms,而 default 模式需要 1.9ms

  2. 填充操作:更大的 PFB 减少了跨块边界的重复计算。CIRCLE_FILL 从 2.9ms (default) 降到 2.3ms (fullscreen)

  3. 渐变和阴影:这类逐像素计算的操作受 PFB 尺寸影响较小

推荐策略:

  • RAM < 8KB:使用 30x30 默认配置

  • RAM 8-32KB:使用 60x30,兼顾内存和性能

  • RAM > 32KB:使用 120x60 或 240x15(全宽条带)

  • RAM > 128KB:可考虑全屏缓冲,但需评估是否值得

配置方法(在 app_egui_config.h 中):

#define EGUI_CONFIG_PFB_WIDTH   60
#define EGUI_CONFIG_PFB_HEIGHT  30

注意:PFB 宽度和高度建议优先选成屏幕尺寸的整数约数。例如 240x240 屏幕,PFB 宽度可选 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 30, 40, 48, 60, 80, 120, 240;如果不能整除,框架也会处理边界块。

双缓冲与三缓冲

SPI 传输与缓冲策略

SPI 传输是大多数场景下的帧率瓶颈,详见下文 SPI 速度的决定性影响。双缓冲通过让 CPU 和 SPI/DMA 并行工作来缓解这一问题。

根据 SPI 矩阵测试报告 的实测数据:

策略

说明

效果

单缓冲 (buf=1)

CPU 渲染完等 SPI 传输完再渲染下一块

帧时间 = CPU + SPI,最慢

双缓冲 (buf=2)

CPU 渲染块 N+1 的同时 SPI 传输块 N

CPU 和 SPI 并行,帧时间 = max(CPU, SPI)

三缓冲 (buf=3)

额外一个缓冲区吸收 CPU/SPI 速度波动

理论上更平滑,实测与双缓冲差异很小

实测数据显示,从单缓冲切换到双缓冲后,大部分测试项的帧时间从 40-65ms 降到 38.4-39ms,接近 SPI 传输的理论下限。三缓冲和四缓冲相比双缓冲几乎没有额外收益,但多占用一个 PFB 大小的 RAM。

建议:优先使用双缓冲。三缓冲仅在 CPU 渲染时间波动很大时才有意义。

配置方法:

#define EGUI_CONFIG_PFB_BUFFER_COUNT  2

SPI 速度的决定性影响

为什么 SPI 才是真正的瓶颈

在实际硬件上,帧刷新时间由 CPU 渲染和 SPI 传输两部分组成。启用双缓冲后,两者可以并行,帧时间取决于较慢的一方。根据 SPI 矩阵测试报告 的实测数据,绝大多数绘制操作的 CPU 耗时远低于 SPI 传输时间,这意味着 SPI 速度才是决定帧率的关键因素。

SPI Matrix Chart

下表对比了典型图元在纯 CPU 渲染、SPI 16MHz 双缓冲、SPI 64MHz 双缓冲三种条件下的帧时间(单位 ms,PFB 30x30,屏幕 240x240):

测试用例

CPU only

SPI 16MHz buf=2

SPI 64MHz buf=2

CPU 占比 (16MHz)

RECTANGLE_FILL

1.132

57.600

19.200

2.0%

CIRCLE_FILL

1.960

57.600

19.200

3.4%

IMAGE_565

0.855

57.600

19.200

1.5%

TEXT

2.456

58.200

19.800

4.3%

ARC_FILL

5.491

59.300

20.900

9.5%

CIRCLE

4.560

58.300

19.900

7.9%

GRADIENT_RECT

2.122

57.600

19.200

3.7%

SHADOW_ROUND

7.676

60.800

22.300

12.6%

GRADIENT_CIRCLE

16.355

64.700

24.300

25.3%

TEXT_ROTATE

45.469

74.000

48.300

61.4%

GRADIENT_ROUND_RECT_RING

54.944

81.500

55.200

67.4%

关键发现:

  1. SPI 16MHz 下,超过 85% 的测试项帧时间被钉死在 57.6ms。CPU 渲染只用了不到 5ms,剩余 52ms 都在等 SPI 传输完成。CPU 大部分时间处于空闲状态

  2. SPI 从 16MHz 提升到 64MHz,帧时间从 57.6ms 降到 19.2ms,提升 3 倍。而同样的 CPU 渲染优化(比如把 CIRCLE_FILL 从 2ms 优化到 1ms)对帧时间几乎没有影响

  3. 只有极少数重计算操作(旋转文字、复杂渐变)的 CPU 耗时超过 SPI 传输时间,此时 CPU 才成为瓶颈

不同屏幕尺寸下的 SPI 瓶颈分析

SPI 传输时间与屏幕总像素数成正比。下表列出常见屏幕在不同 SPI 速率下的全屏传输时间(RGB565,PFB 30x30 双缓冲,脏区域为全屏):

屏幕分辨率

总字节数

SPI 16MHz

SPI 64MHz

128x128

32 KB

~18 ms

~6 ms

240x240

112.5 KB

57.6 ms (实测)

19.2 ms (实测)

320x480

300 KB

~153 ms

~51 ms

对比 CPU 渲染耗时(大多数常规图元 < 10ms),可以看出:

  • 128x128 小屏:SPI 64MHz 以上时传输约 6ms,CPU 和 SPI 耗时接近,两者都需要优化

  • 240x240 中屏:SPI 16MHz 下帧时间被钉死在 57.6ms(约 17 FPS),即使 64MHz 也需要 19.2ms,远超大多数 CPU 渲染时间。SPI 是绝对瓶颈

  • 320x480 大屏:SPI 16MHz 下全屏刷新约 153ms(仅 6.5 FPS),大屏必须依赖脏矩形减少传输量,或使用并行接口(8080/RGB)

实际优化建议

既然 SPI 是瓶颈,优化策略的优先级应该调整:

  1. 提高 SPI 时钟频率:这是最直接有效的手段。从 16MHz 提升到 64MHz,帧率直接提升 3 倍。大多数 SPI LCD 控制器支持 40-80MHz

  2. 启用双缓冲:让 CPU 渲染和 SPI 传输并行。实测数据显示,双缓冲后帧时间从 CPU+SPI 降到 max(CPU, SPI),对于 SPI 瓶颈场景收益巨大

  3. 减少脏区域面积:SPI 只传输脏区域覆盖的 PFB 块。如果只有 1/4 屏幕需要更新,SPI 传输时间也降为 1/4

  4. CPU 优化仅在特定场景有意义:只有当 CPU 渲染时间超过 SPI 传输时间时(如旋转文字、复杂渐变),CPU 优化才能降低帧时间。对于常规 UI(按钮、文本、图片),优化 CPU 渲染几乎不影响帧率

经验法则:如果你的 UI 不包含旋转文字或复杂渐变,帧率几乎完全由 屏幕像素数 / SPI 带宽 决定。先算这个数字,再决定是否需要优化 CPU 渲染。

详细的 SPI 矩阵测试数据见 SPI 矩阵测试报告

脏矩形优化

EmbeddedGUI 使用脏矩形(dirty rectangle)机制:只有被标记为”脏”的区域才会重新渲染。

如果后续需要继续压缩控件内部的局部刷新范围,可参考脏矩形细粒度优化指南;其中包含自定义子区域、PFB 早裁剪和统计验证的完整做法。

减少 invalidate 范围

每次调用 egui_view_invalidate() 会将整个 view 的区域标记为脏。优化要点:

  1. 精确 invalidate:只对真正变化的 view 调用 invalidate,避免对父容器调用

  2. 避免全屏刷新:不要在定时器回调中对根 view 调用 invalidate

  3. 动画优化:动画框架会自动 invalidate 目标 view,不需要手动调用

// 不推荐:刷新整个页面
egui_view_invalidate(EGUI_VIEW_OF(&root_layout));

// 推荐:只刷新变化的控件
egui_view_invalidate(EGUI_VIEW_OF(&label_value));

布局技巧

将频繁更新的控件和静态控件分开放置,避免脏矩形覆盖不必要的区域:

  • 把动态数值 label 和静态标题 label 放在不同的区域

  • 避免大面积半透明叠加,因为底层控件也需要重绘

普通 vs HQ 抗锯齿

EmbeddedGUI 提供两套绘图 API:普通版和 HQ(高质量抗锯齿)版。

性能对比

图元

普通 (ms)

HQ (ms)

倍率

CIRCLE

6.19

9.23

1.5x

CIRCLE_FILL

4.48

4.40

1.0x

ARC

2.69

6.83

2.5x

ARC_FILL

6.92

18.18

2.6x

LINE

8.79

11.28

1.3x

关键发现:

  • 描边操作(CIRCLE, ARC, LINE)的 HQ 版本开销明显,约 1.3-2.6 倍

  • 填充操作(CIRCLE_FILL)的 HQ 版本几乎无额外开销

  • ARC_FILL_HQ 开销最大(2.6 倍),因为需要对弧形边缘做亚像素混合

建议:

  • 大尺寸图元(半径 > 30px)使用 HQ 版本,锯齿感明显

  • 小尺寸图元(半径 < 15px)使用普通版本,肉眼难以分辨

  • 填充操作可以放心使用 HQ 版本

  • 动画中的高频重绘优先使用普通版本

图片 vs 矢量绘制

性能对比

方式

耗时

内存

IMAGE_565 (位图)

9.1ms

图片大小 (WxHx2 bytes)

CIRCLE + RECTANGLE (矢量)

~8.6ms

几乎为零

IMAGE_565_8 (8x 压缩)

0.19ms

图片大小 / 8

GRADIENT_ROUND_RECT (矢量渐变)

7.3ms

几乎为零

关键发现:

  • 压缩图片(IMAGE_565_1/2/4/8)的绘制速度极快(< 0.2ms),因为解压后直接 memcpy

  • 未压缩的全屏图片绘制约 9ms,与复杂矢量图形相当

  • 渐变矢量图形(GRADIENT_ROUND_RECT)耗时 7.3ms,比之前的 155ms 大幅优化

建议:

  • 复杂图形(图标、照片)使用压缩位图,速度最快

  • 简单几何形状使用矢量绘制,节省 ROM 空间

  • 渐变背景优先使用矢量渐变 API,避免存储大尺寸渐变位图

文本渲染优化

文本渲染的性能与 PFB 尺寸强相关:

PFB 配置

TEXT (ms)

TEXT_RECT (ms)

15x15

7.09

7.25

30x30

2.46

2.50

240x1

16.13

16.28

240x240

0.39

0.39

优化建议:

  1. 增大 PFB 尺寸:文本渲染从中受益最大,因为减少了字形的重复解码

  2. 使用 TEXT_RECT 裁剪:当文本可能超出显示区域时,TEXT_RECT 通过裁剪避免无效像素写入

  3. 字体大小选择:较小的字体(12-16px)渲染更快,因为每个字形的像素更少

  4. 静态文本缓存:对于不变的文本,避免每帧重新设置 egui_view_label_set_text()

总结

性能调优的优先级排序:

  1. 提高 SPI 时钟频率(对帧率影响最大,大多数场景下是决定性因素)

  2. 启用双缓冲(让 CPU 和 SPI 并行,消除串行等待)

  3. 精确控制脏矩形范围(减少 SPI 传输量和 CPU 重绘量)

  4. 选择合适的 PFB 尺寸(影响 CPU 渲染效率,尤其是文本和填充操作)

  5. 根据场景选择普通/HQ 绘图 API

  6. 合理使用压缩位图 vs 矢量绘制