不知道大家是否还记得之前分享过的一篇文章:如何在STM32C0旋钮屏上设计并制作TouchGFX GUI? 当时那个演示版的流畅度只能说是差强人意,拿给家里人体验时,那种明显的卡顿感立刻就被吐槽了。
说实话,即便界面布局和色彩搭配得再精致,上手操作时一卡一顿的感觉,立刻就暴露了资源有限硬件的短板,满满的廉价感扑面而来。
不过好在,我从 TouchGFX 官方文档里找到了不少实用的优化思路,又结合了原厂工程师在原版演示中的设计巧思做了针对性调整。现在新版演示的流畅度有了肉眼可见的提升,在体验上已经能和 STM32H7 搭配 DMA2D 渲染的同款 UI 不相上下了!
接下来,我将通过三篇文章分享这些优化思路,并提供可以落地的实操步骤:
- 上篇:介绍图像的 L8 格式以及 RLE 压缩方式,以及如何将经过压缩的图像部署到 STM32C0 的 TouchGFX 应用层。
- 中篇:带领大家使用设计好的 L8 图像重构过往文章中设计的旋转菜单,并添加必要的编译插件实现图像自动压缩,让 GUI 变得更流畅。
- 下篇:介绍在 TouchGFX 中设计两种流畅的遮罩效果,为大家带来一些新的思考和启示。
01 图像的 L8 格式及其压缩
1.1 RGB色彩模式
在认识 L8 色彩格式前,我们需要先了解什么是 RGB 色彩模式。
RGB 色彩模式是工业界的一种颜色标准,通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色。这个标准几乎涵盖了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。常见的模式包括 RGB16(如 RGB555、RGB565)、RGB24(RGB888)、RGB32(ARGB8888),分别代表使用 16 位、24 位、32 位来表示一个像素。在 TouchGFX 中,我们常接触的是 RGB565 和 ARGB8888。
- RGB565:常见于不透明图像,使用 16 位表示一个像素。这 16 位中,5 位用于红色(R),6 位用于绿色(G),5 位用于蓝色(B),故称 RGB565。
- ARGB8888:常见于带有透明度(Alpha通道)的图像,使用 32 位表示一个像素。红、绿、蓝分量各占 8 位,另外 8 位用作 Alpha 通道来表示不透明度,因此可以支持透明或半透明图像。
1.2 L8格式
在 TouchGFX 官方的文档中,对于 L8 的解释可能不够清晰。在嵌入式 GUI 场景下,L8 指的是 8 位调色板索引 CLUT(Color Look-Up Table,颜色查找表)格式。
这时就有朋友要问了:我们为什么要设计 L8 格式?这就需要先了解它的工作原理。
L8 的整套工作流由两部分构成:像素数据和调色板。按照传统的 RGB 色彩模式来理解,像素数据就是色彩数据本身?错!在 L8 中,每个像素的数据仅占 8 位(1 字节),这个值并非直接代表 RGB 颜色,而是代表调色板数组的索引。
所谓的调色板,就是一个最多包含 256 个颜色项的数组,其中的每个颜色项可以是 RGB565、RGB888 或 ARGB8888 等任意格式的真实颜色值。这样一来,显示的流程就变了:GUI 渲染时,先读取像素的索引值,然后根据这个索引到调色板中查找对应的真实颜色,最后再输出到 LCD 显示。这种方法使得图像本身的存储体积大大降低!
让我们来直观对比一下不同格式下,一张 240x240 分辨率图像的理论大小:

可见,L8 对于图像体积的压缩效果是巨大的。
于是聪明的你又要问了:那么代价是什么呢?代价就是图像的色彩种类会被严格限制在 256 种以内,否则就无法被成功压缩为 L8 图像!让我们举个例子,请看下图:

这是一张没有 Alpha 通道的 117×150 大小图像,是我使用 Photoshop 制作的。它保留了色彩梯度,以 #000000 为背景色,#00a2e8 为前景色。大家猜猜,这张图能直接压缩成 L8 格式吗?
答案是:不能。为什么明明只有黑色和蓝色两种颜色却不能被压缩?请注意,在制图时我保留了“色彩梯度”,且以黑色为背景。当我们绘制前景的几何图案时,几何边缘线段的倾斜会导致 PS 软件内部的抗锯齿(AA)算法自动运行,在边缘生成了介于前景色和背景色之间的过渡色(色彩梯度)。这些新生成的颜色使得该图像实际使用的色彩种类超出了 CLUT 的 256 色限制。因此,L8 图像的应用场景在嵌入式 GUI 中较多,且一般用于制作按钮、图标等色彩相对单一的动态控件图像。
1.3 L8图像的制作
那么,我们应该如何制作适合 L8 格式的图标图像呢?经验告诉我主要有两种方法:
- 取消抗锯齿制图:这是最直接的方法,从源头上杜绝新颜色的产生。
- 对称设计:如果必须开启抗锯齿,应尽可能使图像对称,这样可以减少因抗锯齿而产生的新颜色种类。
在制作 L8 图像时,应使用 8 位色彩空间,并尽可能让图像由纯色块堆叠而成。即使成功将带有复杂色彩梯度的图像转换成了 L8 格式,色彩通常也会有非常严重的分层现象,可用性大大降低。下图为我正在使用 Photoshop 设计本次演示要用到的图标。

在设计并制作图标时,推荐在未定稿前保留几何元素的“矢量”属性(如形状图层),不要过早地将图层栅格化,因为这可能导致更严重的色彩越界问题。
按照上述注意事项设计出的 GUI 图标,就可以在导出后通过 RLE 进行高效压缩了。以下是我为本次演示制作的一组图标示例。

1.4 RLE压缩
针对这种由大面积相同颜色区块构成的图标图片,采用 RLE 压缩是非常有效的。那么什么是 RLE 压缩呢?
或许大家在初学编程时就已经接触过字符串压缩的小案例了。是的,RLE 就是其中最简单的 Run-Length Encoding(行程长度编码),属于无损压缩。
具体举个例子:一个未压缩的 L8 图像,每个像素是 1 字节的索引值。如果连续 100 个像素都是黑色(假设索引为 0),那么存储为:0, 0, 0, ..., 0 (共100字节)。
但是在 RLE 压缩后,存储格式变为“重复次数(1字节),索引值(1字节)”,即:100, 0。仅用 2 个字节就表示了原来 100 字节的内容,压缩比极高。
在新版本的 TouchGFX Designer 中,已经集成了 L8 + RLE 的压缩选项,甚至在 RLE 之外,还提供了 L4、LZW9 等其他压缩方式。

02 L8图像在STM32C0上的部署
由于我们使用的是官方开发套件,可以直接通过 TouchGFX Board Setup (TBS) 来创建新工程。

进入工程后,添加我们制作好的、准备转为 L8 格式的图标图片。


根据 TouchGFX 文档,L8 图像格式需要在 Image Format 列中设置。这里我们选择体积最小的 L8_RGB565 格式。

一个关键设置:L8 图像的 Extra Section 必须设置为 IntFlashSection(内部 Flash),否则 L8 图像将无法被正确渲染。

对于不需要设置为 L8 格式的图像,可以选择标准的 RGB 色彩模式,例如 RGB565。

当然,我们也可以直接在项目的 application.config 文件中直接指定所有图像的配置。这里以 info.png 的配置为例:
{
"image_configuration": {
"dither_algorithm": "2",
"alpha_dither": "yes",
"layout_rotation": "0",
"opaque_image_format": "RGB565",
"nonopaque_image_format": "ARGB8888",
"l8_compression": "no",
"rgb_compression": "no",
"section": "ExtFlashSection",
"extra_section": "ExtFlashSection",
"images": {
"info.png": {
"format": "L8_RGB565",
"extra_section": "IntFlashSection"
}
}
}
}
配置文件的上半部分是全局默认配置,images 对象内则是针对每张图片的详细配置。如果某张图片未在 images 中提及,TouchGFX 会按照上半部分的全局默认配置来处理它。
接着,我们尝试在界面上添加一个 Image 控件,关联我们设置好的 L8 图片,生成代码后烧录到设备,查看图片是否被正确渲染。


可以看到,图像显示正确。
但是,请注意,事情并没有这么简单就结束了。当前这个版本的 TBS 模板存在一个已知问题:在这个套件的 TBS 工程中,L8 图像如果被放置到 Container(容器)控件内部,是无法被渲染出来的。 不过,这个问题经过我们手动调整项目代码后是可以解决的。
如何解决这个问题,并利用 L8 和 RLE 压缩,最终让整个 GUI 流畅地跑起来?我们将在中篇文章里详细分享。欢迎持续关注,在云栈社区与我们一同探讨更多嵌入式开发的实战技巧。