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);
}
}
}
这个回调函数里只处理了程序退出事件,其他事件暂时没有用到,先不处理。
小结
这里 是本篇文章的源码