减少代码Flash占用

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

226.3.25 减少代码 Flash 占用

const lv_img_dsc_t ui_img_942215904 = { // 定义仪表盘背景图的 LVGL 图像描述符,并保留 SquareLine 工程原有符号名
    .header.always_zero = 0, // LVGL 8 要求该标志为 0,用于识别变量型图像源
    .header.w = 233, // 声明图像宽度为 233 像素,必须与转换出的像素数据一致
    .header.h = 233, // 声明图像高度为 233 像素,必须与转换出的像素数据一致
    .data_size = 27325, // 声明调色板和像素索引数据合计占 27325 字节
    .header.cf = LV_IMG_CF_INDEXED_4BIT, // 指定每个像素保存 4 位调色板索引,而不再使用真彩色加透明度格式
    .data = ui_img_942215904_data // 指向本文件中转换后的小型图像数据数组
}; // 仪表盘背景图描述符定义结束

代码范围说明:本节真正需要理解的手写修改是图像描述符及其对应的数据格式。转换器生成的 ui_img_942215904_data[] 含 27,325 个字节,逐字抄入笔记既不能帮助理解,也会淹没关键逻辑,因此只保留与数据数组配套、课堂最终工程中实际启用的描述符。数组的尺寸、链接占用和文件内容均已对照工程核验。

一、代码优化减小图片占用

本节优化的对象不是 CPU 执行速度,而是固件镜像中的只读数据(RO-data)。仪表盘背景图原来以 LV_IMG_CF_TRUE_COLOR_ALPHA 数组编入程序,体积远大于普通控制代码;将它量化成 16 色调色板并以 4 位索引保存,可以显著降低 Flash 占用。

需要先纠正一个常见说法:转换后图片仍然占 Flash,只是占得少了;它并没有变成“不占 Flash”的数据。

1.1 打开之前工程

课堂先打开第 24 个工程,做两项收尾检查:删除已经不用的 needletick,并确保 time_count = time_count % 400 与 time_count++ 一起位于 LVGL 的周期分支中。归档的第 24、25 个最终源码已经体现这些结果:只保留 timetick 和 lvtick,计数自增与取模都在 HAL_GetTick() - lvtick > 5 分支中。

这里“放进分支”减少的是无意义的主循环重复取模。源码用的是 > 5,在默认 1 ms HAL 滴答下最早约 6 ms 进入一次,并非严格每 5 ms;其原理已在第 225 篇说明。

编译第 24 个工程后,视频和链接映射文件给出的结果为:

项目优化前数值
Code168,206 B
RO-data307,022 B
RW-data684 B
ZI-data62,588 B
Total RO Size475,228 B = 464.09 KiB
Total ROM Size475,432 B = 464.29 KiB
Keil 目标区 Flash 使用率90.64%(按完整 512 KiB 计算)
RAM 使用率96.55%(约 61.79/64 KiB)

课堂后续 IAP 布局把 Bootloader 放在前 32 KiB,APP 从 0x08008000 开始,长度为 0x78000,即 480 KiB。因此,虽然这个尚未搬移的普通工程按完整 512 KiB 目标区显示仍有约 47.9 KiB,放入 IAP 的 480 KiB APP 分区时实际只剩约 16 KiB。不能只看当前 Keil 进度条,而忽略未来链接分区。

课堂复制第 24 个工程并命名为第 25 个工程,再在副本上修改。这一步的价值是保留可回退、可对比的基线。实际项目更推荐同时使用版本控制;复制目录不能替代清晰的变更记录,但本节确实按复制工程的方式演示。

1.2 减少 Flash 占用

1.2.1 找到占用 Flash 的地方

定位空间热点时应看链接器的 .map 文件或构建统计,而不是只根据 C 文件“行数很多”猜测。工程映射文件显示,优化前 ui_img_942215904.o 中背景图数组本身占 162,867 B,另有 12 B 图像描述符;这证明它确实是主要目标。

工程中各 UI 图像源文件的文本大小也能辅助筛查:背景图源文件约 819 KB,而指针文件约 29 KB、四个小图标各约 16 KB。不过,主机磁盘上的 .c 文本文件大小不等于 MCU Flash 占用;十六进制文本 0xFF, 在编译后只形成一个数据字节,最终仍以链接映射为准。

原背景图为 233 × 233,共 54,289 个像素。工程设置 LV_COLOR_DEPTH 16,而 LV_IMG_CF_TRUE_COLOR_ALPHA 在该设置下为每像素 2 字节 RGB565 颜色加 1 字节 Alpha:

233 × 233 × 3 = 162,867 B

这与 .map 文件和原数组中的字节数量完全一致。因此视频中“原格式好像是 32 位/像素”的说法不适用于本工程;那只是课堂中的口头猜测。若 LV_COLOR_DEPTH 配成 32,真彩色带 Alpha 才会是每像素 4 字节。

1.2.2 减少 Flash 占用方法

本节选用 LV_IMG_CF_INDEXED_4BIT。它不是直接用 4 位完整表达任意颜色,而是先建立最多 16 种颜色的调色板,每个像素只保存一个 0~15 的调色板索引。代价是颜色被量化,可能出现渐变色带或细节损失;收益是静态、颜色相对集中的大图能显著缩小。

1.2.2.1 找到思维导图

视频通过课程思维导图中“LVGL → 仪表盘显示 → 代码优化 → 减少图片占用 Flash”的链接进入 LVGL Image Converter。思维导图只是课程导航,真正决定输出格式的是转换器中选择的 LVGL 版本、颜色格式和输出格式。

1.2.2.2 LVGL 官网

视频使用 https://lvgl.io/tools/imageconverter,界面上选择 LVGL v8。必须与工程版本匹配:本工程由 SquareLine Studio 1.5.0 生成,使用 LVGL 8.3.11;若误选 LVGL 9,生成的类型名、描述符结构和颜色格式枚举可能与 v8 工程不兼容。

Image Converter 的作用是把 PNG 等图片转换成 LVGL 可读取的 C 数组或二进制资源。选择不同格式会改变 Flash 体积、颜色精度、透明度表达和可用的图像操作,不能只选“文件最小”而不考虑功能。

1.2.2.3 图片转化步骤

1.2.2.3.1 选择图片文件

课堂选择原始资源 gauge-bg.png。该图对应 UI 目录中的大背景数组 ui_img_942215904_data,不是较小的指针数组 ui_img_1601502596_data。文件名、图像内容和目标符号要三者对应,不能仅凭数字文件名盲目替换。

1.2.2.3.2 Color format 选择

操作顺序为:

  1. 选择 LVGL v8。
  2. 选择 gauge-bg.png。
  3. 将 Color format 设为 Indexed 4 bit。
  4. Output format 保持 C array。

视频没有启用 Dither。抖动有时能减轻低色数渐变的明显色带,但也会改变像素分布;是否启用应在目标屏幕上比较,而不是默认它一定更好。

1.2.2.3.3 转化成所需数组文件

点击 Convert 后,转换器生成一个 .c 文件。对于本图,最终有效数组大小为 27,325 B。其组成可以精确算出:

  • 16 色调色板:16 × 4 = 64 B。每个调色板项用 4 字节保存颜色与透明度。
  • 每行像素索引:233 像素 × 4 bit = 932 bit,需要向整字节取整为 117 B。
  • 233 行索引:117 × 233 = 27,261 B。
  • 总计:64 + 27,261 = 27,325 B。

之所以按行向上取整,而不是简单算 54,289 ÷ 2,是因为宽度 233 为奇数,每行最后半个字节不能与下一行首像素拼接。转换器生成的 data_size = 27325 与该结果一致。

1.2.2.3.4 保存文件

课堂把下载文件保存到第 25 个工程根目录,文件名为 gauge-bg.c,再用 VS Code 与 SquareLine 生成的原图文件对照。下载文件只是中间产物,真正参与 Keil 编译的是 UI 工程中的 midware/ui/images/ui_img_942215904.c。

应注意,源文件名 gauge-bg.c 含连字符。视频所用转换器据此生成了类似 gauge-bg_map 的标识符,而 C 标识符不能包含 -;最终工程没有直接启用这些名字,而是保留原工程合法且已被引用的 ui_img_942215904_data 与 ui_img_942215904。新项目最好从一开始就用字母、数字和下划线命名资源,例如 gauge_bg.png。

1.2.2.4 打开转化后的文件

转换后的文件仍然是一段很长的十六进制数组,但有效数据从 162,867 B 降为 27,325 B。不能通过编辑器显示的“行数”判断压缩比例,因为一行可以排任意数量的字节;应比较数组元素数、对象文件的 RO Data 或最终 .map。

这也不是 PNG/JPEG 那样的通用压缩流。运行时数据是调色板加打包的像素索引,LVGL 按 LV_IMG_CF_INDEXED_4BIT 解释它。

1.2.2.5 使用该文件

课堂的实际替换过程包含四个必须配套的部分:

  1. 把新生成的调色板和索引字节作为 ui_img_942215904_data[] 的内容。
  2. 保留原工程的数组名,使 ui_img_942215904 描述符和 UI 代码仍能找到同一符号。
  3. 将 .header.cf 从 LV_IMG_CF_TRUE_COLOR_ALPHA 改为 LV_IMG_CF_INDEXED_4BIT。
  4. 让 .data_size 与新数组长度一致,并继续令 .data 指向 ui_img_942215904_data。

其中任何一项不匹配,都可能造成编译错误、花屏、越界读取或错误解码。宽高仍是 233 × 233,所以 .header.w、.header.h 无需改变。

课堂最终源码把 .data_size 写成常量 27325。这个值正确,但“这样可减少一次计算”的解释不成立:对同一编译单元中完整定义的数组,sizeof(ui_img_942215904_data) 是编译期常量,不会在 MCU 运行时遍历数组或执行尺寸计算。保留 sizeof 反而能在以后重新转换图片时自动跟随数组长度,降低忘记同步常量的风险。本笔记顶部仍严格记录课堂最终源代码,没有擅自替换。

手工修改 midware/ui/images 下的生成文件还有一个维护风险:以后从 SquareLine 重新导出 UI,文件可能被覆盖。应保留原始图片、转换参数和替换步骤,或者把资源优化纳入可重复的生成流程。

二、减少 Flash 占用

2.1 复制粘贴文件

视频先在下载的 gauge-bg.c 中注释掉与工程不兼容的包含、宏、数组声明和描述符外壳,再把原工程的合法符号名与新数据组合起来,最后用编辑后的完整内容替换目标图片源文件。

学习时不应把“Ctrl+A、Ctrl+C、Ctrl+V”当成核心。真正要核对的是以下不变量:

  • 目标工程只保留一份有效的 ui_img_942215904_data 定义,避免重复符号。
  • 新数组是 Indexed 4-bit 数据,不再是旧的 True Color Alpha 字节流。
  • 描述符的颜色格式与数组编码完全相同。
  • 描述符宽高与原图和新数组一致。
  • UI 对外使用的 ui_img_942215904 名称不变,因此无需改 SquareLine 生成的屏幕创建代码。

下载文件顶部和尾部被注释掉的代码不会进入编译;注释文本本身也不会占 MCU Flash。它们只会增加主机上的源文件文本大小,正式整理工程时可以在确认无用后删除,但课堂归档工程保留了这些对照内容。

2.2 编译并查看效果

第 25 个工程的链接结果为:

项目优化前优化后变化
Code168,206 B168,206 B0 B
RO-data307,022 B171,482 B-135,540 B
Total RO Size464.09 KiB331.73 KiB-132.36 KiB
Total ROM Size464.29 KiB331.93 KiB-132.36 KiB
RAM 使用率96.55%96.55%无变化

背景数组本身从 162,867 B 降到 27,325 B,少了 135,542 B;最终 RO-data 少 135,540 B,2 B 差异来自链接时的对齐/布局变化。这个结果同时说明:

  • 优化的是只读图片数据,不是机器指令,所以 Code 大小不变。
  • 数组是 const 数据,主要驻留 Flash;本次没有解决已经高达 96.55% 的 RAM 压力。
  • 课堂口述的“约减少 140 KB”是粗略值;按映射文件精确值为 135,540 B,即 132.36 KiB。

烧写后,视频中肉眼没有发现明显差别,说明对当前屏幕、观看距离和这张背景图,16 色量化效果可以接受。但 Indexed 4-bit 是有损颜色量化,不能据此断言图像与原图完全一致。应重点检查渐变、半透明边缘、夜间亮度和近距离色带,再决定是否采用。

2.3 继续减少 Flash 占用的方法

优化顺序应优先看 .map 中占用最大的资源,这一点正确;但不能把所有图片都机械转换成 Indexed 4-bit。

  • 静态大背景图:适合尝试 Indexed 4-bit 或 Indexed 8-bit,再比较画质与空间。
  • 仪表盘指针:本工程通过 lv_img_set_angle() 旋转。LVGL 8.3 明确说明 indexed 和 alpha-only 图像不能直接执行图像旋转/缩放,因此不能照搬本节格式,否则指针变换会失效。若要优化,应继续使用可变换的真彩色 C 数组,并从裁剪透明边界、减小尺寸或重新设计资源着手。
  • 四个单色告警图标:它们依靠 img_recolor 改色,可评估 LVGL 的 Alpha-only 格式,让像素只保存透明度、颜色由样式提供;但必须在工程中逐个验证边缘和重着色效果。
  • 外部存储:若硬件带外部 SPI Flash/SDRAM,可把部分资源移出片内 Flash,但需要相应文件系统或图像解码、读取带宽和缓存设计,本节没有实现,不能把它当作现成结果。

此外,RAM 仍接近满载。Flash 图片格式优化不等于 RAM 优化;后续还应检查 LVGL 绘图缓冲、堆大小、全局数组和栈余量。只有把 Flash、静态 RAM、运行时堆栈分别测量,才能判断项目是否真正有扩展空间。

三、本节易错点总结

  • LV_IMG_CF_INDEXED_4BIT 表示“每像素 4 位索引 + 16 色调色板”,不是每像素直接拥有 4 位任意真彩色。
  • 本工程 LV_COLOR_DEPTH=16,原 True Color Alpha 数据实际为 24 bit/像素,不是课堂猜测的 32 bit/像素。
  • .c 文件在电脑上的文本大小不等于烧入 MCU 的二进制大小;应看数组字节数和链接 .map。
  • sizeof(数组) 在这里是编译期常量,把它换成 27325 不会节省一次运行时计算。
  • .data_size、.header.cf、宽高、数据数组和符号名必须成套匹配。
  • 16 色量化是有损的;“视频里看不出差别”不等于像素完全一致。
  • LVGL 8.3 的 indexed 图像不能直接旋转或缩放,所以旋转指针不能机械改成 Indexed 4-bit。
  • 本节将 Flash 从 464.09 KiB 降到 331.73 KiB,但 RAM 使用率仍为 96.55%,两类容量问题要分别处理。

四、资料依据

  • LVGL 8.3 官方文档:Images:核对 True Color Alpha 在不同 LV_COLOR_DEPTH 下的字节布局、Indexed 4-bit 的 16 色调色板与 4 位索引,以及 indexed 图像不能旋转/缩放的限制。
  • LVGL Image Converter:课堂使用的官方在线图片转换入口;本工程应选择 LVGL v8 输出。
  • C11 草案 N1570:核对 sizeof 运算符及数组对象尺寸语义;对这里的完整数组,尺寸由编译器在编译期确定。
  • 工程构建证据:第 24、25 个工程的 12LCD_Display.map、视频编译输出和最终 ui_img_942215904.c,用于核对 162,867 B、27,325 B、464.09 KiB 与 331.73 KiB 等具体数值。