lv_display

lv_display 是什么

lv_display_t 是代表物理显示面板的核心对象,相当于 LVGL 与底层屏幕硬件之间的桥梁。lv_display对象还管理着分辨率、DPI、颜色格式(如 RGB565/RGB888)、旋转角度以及该显示器上的各层 Screen 等属性。

要让 lvgl 显示画面,我们需要执行以下几个步骤:

  • 创建 lv_display 对象
  • 设置绘图缓冲区。
  • 注册刷新回调

创建 lv_display 对象

cpp 复制代码
lv_display_t * disp = lv_display_create(HOR_RES, VER_RES);

lv_display_t * lv_display_create(int32_t hor_res, int32_t ver_res) 用于创建一个 lv_display 对象,hor_res 和 ver_res 分别对应屏幕的宽高。当一个 lv_display 被创建时,系统会自动地创建 4 个辅助层,并将其附加到 lv_display 对象上。这个四个辅助层分别为 Bottom Layer (底层),Active Screen (活动屏幕),Top Layer(顶层)以及 System Layer(系统层)。我们可以通过这几个辅助层实现遮罩,模态,菜单等。

绘图缓冲区

lvgl 不直接绘制到硬件,而是先渲染到内存缓冲区,再通过回调刷入屏幕。

cpp 复制代码
lv_display_set_buffers(display1, buf1, buf2, buf_size_in_bytes, render_mode)

void lv_display_set_buffers(lv_display_t * disp, void * buf1, void * buf2, uint32_t buf_size, lv_display_render_mode_t render_mode); 用于设置绘制缓冲区以供LVGL使用。

buf1 是LVGL可以渲染像素的缓冲区,buf2 是可选缓冲区,buf_size_in_bytes 是缓冲区大小,render_mode 是渲染模式。

  • LV_DISPLAY_RENDER_MODE_PARTIAL(局部渲染模式)
    使用小于屏幕尺寸的缓冲区进行渲染。
    建议缓冲区大小至少为屏幕尺寸的 1/10。
    在刷新回调中,需要将渲染完成的图像复制到显示屏的对应区域。
    仅会重绘更新的区域,其余部分不会重新绘制。
  • LV_DISPLAY_RENDER_MODE_DIRECT(直接渲染模式)
    缓冲区大小必须与屏幕尺寸一致。
    LVGL 会直接在缓冲区的正确位置进行渲染。
    使用该方式时,缓冲区始终保存完整的屏幕图像。
    同样,仅会重绘更新的区域,其余部分不会重新绘制。
  • LV_DISPLAY_RENDER_MODE_FULL(全屏渲染模式)
    缓冲区大小必须与屏幕尺寸一致。
    LVGL 始终重绘整个屏幕,即使只有一个像素发生变化。

计算绘图缓冲区大小

当使用 LV_DISPLAY_RENDER_MODE_DIRECT 或者 LV_DISPLAY_RENDER_MODE_FULL 时,绘图缓冲区必须和屏幕尺寸一致。

内存中,像素数据按行存储(行优先,row-major)

buffer 内存布局:

行0的像素...padding ← 一个 stride

行1的像素...padding ← 一个 stride

行2的像素...\]\[padding

lv_draw_buf_width_to_stride 会根据像素宽度和颜色格式,计算一行在内存中实际占用的字节数。

cpp 复制代码
lv_color_format_t cf = lv_display_get_color_format(disp);
uint32_t stride = lv_draw_buf_width_to_stride(HOR_RES, cf);
uint32_t buf_size = stride * VER_RES

设置绘图缓冲区

cpp 复制代码
uint8_t* fb_buf1 = new uint8_t[buf_size];   /* framebuffer 1 */
uint8_t* fb_buf2 = new uint8_t[buf_size];   /* framebuffer 2 */
lv_memset(fb_buf1, 0xFF, buf_size);
lv_memset(fb_buf2, 0xFF, buf_size);
lv_display_set_buffers(disp, fb_buf1, fb_buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL);

当 fb_buf2 设置为 nullptr 时,lvgl 使用单缓冲绘制,否则使用双缓冲。

多缓冲

  • 单缓冲
    时间线:

    渲染到buf\]\[传输buf到屏幕\]\[渲染到buf\]\[传输

    CPU忙 CPU等 CPU忙 CPU等
    只有 1 个 buffer
    渲染和传输串行:渲染完才能传,传完才能渲染下一帧
  • 双缓冲
    时间线:

    渲染buf1\]\[渲染buf2\]\[渲染buf1

    传输buf1\]\[传输buf2

    CPU忙 CPU忙 CPU忙
    DMA忙 DMA忙
    2 个 buffer,交替使用
    渲染和传输并行:CPU 渲染 buf2 的同时 DMA 传输 buf1
    CPU 利用率高,帧率翻倍帧
  • 三缓冲
    时间线:

    渲染buf1\]\[渲染buf2\]\[渲染buf3\]\[渲染buf1

    传输buf1\]\[传输buf2\]\[传输buf3

    CPU忙 CPU忙 CPU忙 CPU忙
    DMA忙 DMA忙 DMA忙

刷新回调

void lv_display_set_flush_cb(lv_display_t * disp, lv_display_flush_cb_t flush_cb) 用于设置刷新回调函数

typedef void (*lv_display_flush_cb_t)(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map) disp 表示当前显示设备对象,area 表示需要刷新的屏幕区域,px_map 则是该区域的像素数据。

创建 sdl 窗口

在 windows/linux 上,我们通过 sdl 来把 lvgl 的像素数据发送给屏幕,因此需要先创建 sdl 窗口

cpp 复制代码
static SDL_Window* sdl_window;
static SDL_Renderer* sdl_renderer;
static SDL_Texture* sdl_texture;

sdl_window 表示屏幕上的一个窗口对象,sdl_renderer 用于绘制画面,sdl_texture 则存放像素数据。

cpp 复制代码
SDL_Init(SDL_INIT_VIDEO);

sdl_window = SDL_CreateWindow("LVGL Display",
                                  SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                                  HOR_RES, VER_RES, 0);
sdl_renderer = SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED);

#if LV_COLOR_DEPTH == 32
    SDL_PixelFormatEnum px_format = SDL_PIXELFORMAT_RGB888;
#elif LV_COLOR_DEPTH == 24
    SDL_PixelFormatEnum px_format = SDL_PIXELFORMAT_BGR24;
#elif LV_COLOR_DEPTH == 16
    SDL_PixelFormatEnum px_format = SDL_PIXELFORMAT_RGB565;
#else
    #error "Unsupported LV_COLOR_DEPTH"
#endif

sdl_texture = SDL_CreateTexture(sdl_renderer, px_format,
                                    SDL_TEXTUREACCESS_STATIC, HOR_RES, VER_RES);

把 lvgl 的像素数据更新到 sdl

cpp 复制代码
static void my_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map)
{
    /* SDL: synchronous flush, transfer completes before returning */
    uint32_t stride = lv_draw_buf_width_to_stride(HOR_RES, lv_display_get_color_format(disp));
    SDL_Rect rect;
    rect.x = area->x1;
    rect.y = area->y1;
    rect.w = lv_area_get_width(area);
    rect.h = lv_area_get_height(area);
    SDL_UpdateTexture(sdl_texture, &rect, px_map, (int)stride);
    SDL_RenderClear(sdl_renderer);
    SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, NULL);
    SDL_RenderPresent(sdl_renderer);

    lv_display_flush_ready(disp);
}

SDL_UpdateTexture 用于把 px_map 的数据更新到 sdl_texture,SDL_RenderClear

则用于清空当前渲染目标,SDL_RenderCopy 把 sdl_texture 传给 sdl_render 用于绘制,SDL_RenderPresent 把画好的内容显示到屏幕上。

lv_display_flush_ready 用于清除 flushing 标志,表示刷新完成。

同步刷新和异步刷新

  • 同步 flush
    传输数据到屏幕; // CPU 在这里等着,直到传完
    调用 lv_display_flush_ready 清除 LVGL flushing 标志。
  • 异步 flush
    在嵌入式硬件上,像素数据推送到屏幕上通过 DMA 异步传输,my_flush_cb 发起屏幕数据传输请求,LVGL 调 flush_wait_cb → 在这里阻塞等待 DMA 完成。
    lv_display_set_flush_wait_cb 用于设置刷新等待回调,刷新等待回调返回,LVGL 自动清标志,表示刷新完成。

读取 SDL 事件

我们要做的最后一件事情是读取 sdl 的事件,在 Windows/Linux /macOS 系统下,如果程序没有取消息,会认为窗口卡死,未响应,因此需要设置一个回调去读取事件。

cpp 复制代码
lv_timer_create(sdl_event_handler, 5, NULL);

我们创建一个 5ms 的定时任务去处理 sdl 事件。

cpp 复制代码
static void sdl_event_handler(lv_timer_t * timer)
{
    (void)timer;
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            exit(0);
        }
    }
}

这个回调函数里只处理了程序退出事件,其他事件暂时没有用到,先不处理。

小结

这里 是本篇文章的源码