1. LVGL图像基础:图像数据类型与解码器工作原理

各位同学好,我是老张。今天咱们来聊聊LVGL里图像这块儿最基础的东西。说实话,图像处理在嵌入式UI里是个大头,搞不好就容易出问题。我刚开始用LVGL那会儿,就因为没搞懂图像类型,踩了不少坑。

1.1 图像数据类型:两种存储方式

LVGL支持两种图像数据来源,说白了就是:变量文件。咱们一个个来看。

LV_IMG_SRC_VARIABLE:变量图像

这种图像数据直接编译到固件里,存在Flash或RAM中。我习惯叫它「内嵌图像」。它的结构体长这样:

typedef struct {
    lv_img_header_t header;  // 图像头信息
    uint32_t data_size;      // 数据大小
    const uint8_t *data;     // 像素数据指针
} lv_img_dsc_t;

其中lv_img_header_t包含了宽、高、颜色格式等关键信息。举个例子:

// 定义一个32x32的ARGB8888图像
const lv_img_dsc_t my_image = {
    .header.always_zero = 0,
    .header.w = 32,
    .header.h = 32,
    .header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA,  // 带透明通道
    .data_size = 32 * 32 * 4,                  // 每个像素4字节
    .data = my_image_data,                     // 原始像素数组
};
我的经验:变量图像适合小图标、Logo这类固定资源。我曾经在一个项目里把一张800x480的背景图也做成变量,结果Flash直接爆了。小图用变量,大图用文件,这是基本原则。

LV_IMG_SRC_FILE:文件图像

文件图像从存储设备读取,比如SD卡、SPI Flash或者文件系统。LVGL通过lv_fs模块来访问文件。使用时只需要提供路径:

lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, "S:/images/background.bin");

注意路径前缀S:,这是LVGL文件系统的驱动器标识符。你可以在lv_conf.h里配置:

#define LV_USE_FS_FATFS  1
#define LV_FS_FATFS_LETTER 'S'  // 驱动器字母
避坑指南:文件图像虽然省Flash,但读取速度慢。我曾经在刷新率要求高的界面上用了文件图像,结果卡成PPT。后来改成预加载到RAM才解决。记住:速度换空间,空间换速度。

1.2 图像解码器工作原理

LVGL的图像解码器,你可以把它想象成一个「图像翻译官」。它负责把原始数据转换成屏幕上能显示的像素格式。嗯,这里要注意,解码器不是万能的,它只认识它注册过的格式。

解码器注册机制

LVGL内置了几个解码器,默认就注册好了:

  • 内置解码器:处理LVGL原生格式(如LV_IMG_CF_TRUE_COLOR
  • PNG解码器:需要开启LV_USE_PNG
  • BMP解码器:需要开启LV_USE_BMP
  • JPEG解码器:需要开启LV_USE_JPEG
  • GIF解码器:需要开启LV_USE_GIF

解码器的核心结构体是lv_img_decoder_t

typedef struct _lv_img_decoder_t {
    lv_img_decoder_info_cb_t info_cb;    // 获取图像信息
    lv_img_decoder_open_cb_t open_cb;    // 打开解码器
    lv_img_decoder_read_cb_t read_cb;    // 读取像素数据
    lv_img_decoder_close_cb_t close_cb;  // 关闭解码器
    // ...
} lv_img_decoder_t;

解码流程:三步走

当LVGL需要显示一张图像时,它会按顺序做三件事:

  1. 信息查询:调用info_cb获取图像宽高、颜色格式。这一步不加载数据,只是「看一眼」。
  2. 打开解码器:调用open_cb准备解码。对于文件图像,这里会打开文件句柄;对于变量图像,直接指向内存地址。
  3. 读取像素:调用read_cb逐行或逐块读取像素数据。LVGL会按需读取,不是一次性全加载。

关键点:LVGL采用「按需解码」策略。它不会把整张图都解码到内存里,而是只解码当前需要显示的那部分。这对大图显示特别重要,能省下大量RAM。

自定义解码器

如果你有特殊格式的图像,比如公司自研的压缩格式,可以自己注册解码器。我去年就做过一个:

// 注册自定义解码器
lv_img_decoder_t *decoder = lv_img_decoder_create();
lv_img_decoder_set_info_cb(decoder, my_decoder_info);
lv_img_decoder_set_open_cb(decoder, my_decoder_open);
lv_img_decoder_set_read_cb(decoder, my_decoder_read);
lv_img_decoder_set_close_cb(decoder, my_decoder_close);

注册后,LVGL会自动识别并调用你的解码器。优先级高的解码器会先被尝试。

解码器类型 内存占用 解码速度 适用场景
内置(原生格式) 极快 小图标、固定资源
PNG解码器 较慢 需要透明通道的图片
JPEG解码器 中等 照片、大背景图
BMP解码器 简单位图
我的建议:如果对性能有要求,尽量用LVGL原生格式。PNG虽然通用,但解码慢。我一般把UI里常用的图标转成C数组,背景图用JPEG。这样既省Flash,又保证速度。

1.3 实际项目中的选择策略

说了这么多,到底怎么选?我总结了几条经验:

  • 小于64x64的图标:用变量图像,直接编译进固件
  • 全屏背景图:用文件图像,存成JPEG或压缩格式
  • 需要频繁切换的图片:预加载到RAM,用变量图像
  • 动画序列帧:用文件图像,配合缓存管理

你想想看,如果每个小图标都从文件读,那界面响应得多慢?反过来,如果大背景也塞进Flash,那存储空间肯定不够。说白了就是权衡,没有银弹。

好了,这一章就到这里。下一章咱们聊聊缓存管理,那可是提升UI流畅度的关键。到时候我会分享一个我踩过的坑——因为缓存没配好,整个界面卡了三天才找到原因。