目录
LVGL 显示缓冲区
LVGL 显示驱动
小结
lv_disp_drv_register 分析
lv_disp_drv_init 分析
lv_disp_drv_register 分析
注意
LVGL 是 GUI 的图形绘制库,既然是图形绘制,那么就需要考虑 2 点:
- 开辟绘制的 buffer;
- 对接底层实际绘制到屏幕的驱动;
LVGL 显示缓冲区
在 LVGL v8 上,用于绘制的 buffer,使用 lv_disp_draw_buf_t 结构体进行描述:
typedef struct _lv_disp_draw_buf_t {
void * buf1;
void * buf2;
void * buf_act;
uint32_t size;
volatile int flushing;
volatile int flushing_last;
volatile uint32_t last_area : 1;
volatile uint32_t last_part : 1;
} lv_disp_draw_buf_t;
绘制缓冲区是 LVGL 用来渲染屏幕内容的简单数组。 一旦渲染准备就绪,绘制缓冲区的内容将使用显示驱动程序中设置的 flush_cb 函数发送到显示器;
在 lv_port_disp.c 文件中 (xxx_port_xxx 这种文件,都是需要根据实际的芯片、板卡、和外设来进行移植),有一个函数 lv_port_disp_init;用于初始化显示相关的内容,可以在里面进行绘制缓冲区的配置:
void lv_port_disp_init(void)
{
disp_init();
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 10];
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10);
//
// static lv_disp_draw_buf_t draw_buf_dsc_2;
// static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];
// static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];
// lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);
//
// static lv_disp_draw_buf_t draw_buf_dsc_3;
// static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];
// static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES];
// lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, MY_DISP_VER_RES * LV_VER_RES_MAX);
............
}
代码中可以看到,有 3 种配置,这里选了第 1 种,配置的绘图缓冲区为:MY_DISP_HOR_RES * 10;也就是 10 行数据;
个 lv_port_disp_init 函数,是 LVGL 在初始化的时候,需要显式调用的;
注意,这里的 lv_disp_draw_buf_t ,是 static 的,也就是说它是静态的,全局的变量,不能也不要被销毁;
绘制缓冲区可以小于屏幕。在这种情况下,较大的区域将被重新绘制为适合绘制缓冲区的较小部分。 如果只有一个小区域发生变化(例如按下按钮),则只会刷新该区域。
更大的缓冲区会导致更好的性能,但超过 1/10 屏幕大小的缓冲区没有显着的性能改进。 因此,建议选择绘制缓冲区的大小至少为屏幕大小的 1/10。
如果只使用一个缓冲区,LVGL 将屏幕内容绘制到该绘制缓冲区中并将其发送到显示器。 这样 LVGL 需要等到缓冲区的内容发送到显示器,然后再在其中绘制新内容。
如果使用两个缓冲区,LVGL 可以绘制到一个缓冲区中,而另一个缓冲区的内容被发送到后台显示。 应使用 DMA 或其他硬件将数据传输到显示器,让 MCU 同时绘制。 这样,显示的渲染和刷新变得并行。
LVGL 显示驱动
LVGL 使用 lv_disp_drv_t 来表示一个显示驱动,它的定义如下:
typedef struct _lv_disp_drv_t {
lv_coord_t hor_res;
lv_coord_t ver_res;
lv_coord_t
physical_hor_res;
lv_coord_t
physical_ver_res;
lv_coord_t
offset_x;
lv_coord_t offset_y;
lv_disp_draw_buf_t * draw_buf;
uint32_t direct_mode : 1;
uint32_t full_refresh : 1;
uint32_t sw_rotate : 1;
uint32_t antialiasing : 1;
uint32_t rotated : 2;
uint32_t screen_transp : 1;
uint32_t dpi : 10;
void (*flush_cb)(struct _lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
void (*rounder_cb)(struct _lv_disp_drv_t * disp_drv, lv_area_t * area);
void (*set_px_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
lv_color_t color, lv_opa_t opa);
void (*clear_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, uint32_t size);
void (*monitor_cb)(struct _lv_disp_drv_t * disp_drv, uint32_t time, uint32_t px);
void (*wait_cb)(struct _lv_disp_drv_t * disp_drv);
void (*clean_dcache_cb)(struct _lv_disp_drv_t * disp_drv);
void (*drv_update_cb)(struct _lv_disp_drv_t * disp_drv);
lv_color_t color_chroma_key;
lv_draw_ctx_t * draw_ctx;
void (*draw_ctx_init)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);
void (*draw_ctx_deinit)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);
size_t draw_ctx_size;
#if LV_USE_USER_DATA
void * user_data;
#endif
} lv_disp_drv_t;
在 lv_port_disp_init 函数中,会调用 lv_disp_drv_init 函数,来进行显示驱动部分的初始化:
void lv_port_disp_init(void)
{
.........
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 128;
disp_drv.ver_res = 128;
disp_drv.flush_cb = disp_flush;
disp_drv.draw_buf = &draw_buf_dsc_1;
//disp_drv.full_refresh = 1
//disp_drv.gpu_fill_cb = gpu_fill;
lv_disp_drv_register(&disp_drv);
}
这里可以看到,定义了一个 disp_drv 的 driver,而且是静态的;传入到 lv_disp_drv_init 函数进行最基本的结构体初始化:
void lv_disp_drv_init(lv_disp_drv_t * driver)
{
lv_memset_00(driver, sizeof(lv_disp_drv_t));
driver->hor_res = 320;
driver->ver_res = 240;
driver->physical_hor_res = -1;
driver->physical_ver_res = -1;
driver->offset_x = 0;
driver->offset_y = 0;
driver->antialiasing = LV_COLOR_DEPTH > 8 ? 1 : 0;
driver->screen_transp = LV_COLOR_SCREEN_TRANSP;
driver->dpi = LV_DPI_DEF;
driver->color_chroma_key = LV_COLOR_CHROMA_KEY;
#if LV_USE_GPU_STM32_DMA2D
driver->draw_ctx_init = lv_draw_stm32_dma2d_ctx_init;
driver->draw_ctx_deinit = lv_draw_stm32_dma2d_ctx_init;
driver->draw_ctx_size = sizeof(lv_draw_stm32_dma2d_ctx_t);
#elif LV_USE_GPU_NXP_PXP
driver->draw_ctx_init = lv_draw_nxp_pxp_init;
driver->draw_ctx_deinit = lv_draw_nxp_pxp_init;
driver->draw_ctx_size = sizeof(lv_draw_nxp_pxp_t);
#elif LV_USE_GPU_NXP_VG_LITE
driver->draw_ctx_init = lv_draw_nxp_vglite_init;
driver->draw_ctx_deinit = lv_draw_nxp_vglite_init;
driver->draw_ctx_size = sizeof(lv_draw_nxp_vglite_t);
#elif LV_USE_GPU_SDL
driver->draw_ctx_init = lv_draw_sdl_init_ctx;
driver->draw_ctx_deinit = lv_draw_sdl_deinit_ctx;
driver->draw_ctx_size = sizeof(lv_draw_sdl_ctx_t);
#else
driver->draw_ctx_init = lv_draw_sw_init_ctx;
driver->draw_ctx_deinit = lv_draw_sw_init_ctx;
driver->draw_ctx_size = sizeof(lv_draw_sw_ctx_t);
#endif
}
接着,在外部,显示的配置了 hor_res 和 ver_res(这里,我使用的屏是 128x128 的);
接着配置了 disp_drv.flush_cb = disp_flush; 这个是用于对接到 platform 的刷新数据的函数,(这个函数需要自行实现);
然后再将前面初始化完成的 draw_buf 挂接到 display 上:disp_drv.draw_buf = &draw_buf_dsc_1;
最后调用 lv_disp_drv_register,来将初始化完毕的 display driver 注册给 LVGL;
这里需要注意的是,disp_drv.flush_cb = disp_flush; 函数的移植,是需要我们自行根据屏来实现的:
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
LCD_DrawPoint(x, y, color_p->full);
color_p++;
}
}
// LCD_Fill(area->x1, area->y1, area->x2, area->y2, *((uint16_t *)color_p));
lv_disp_flush_ready(disp_drv);
}
这里的 LCD_DrawPoint 就是自行实现的,这里使用 STM32 + ST7735S(SPI方式驱屏),所以这里的 LCD_DrawPoint 是对接的这部分驱动实现:
void LCD_DrawPoint(u16 x,u16 y,u16 color)
{
LCD_Address_Set(x,y,x,y);//设置光标位置
LCD_WriteData_16Bit(color);
}
小结
应用层调用 lv_init,在调用 lv_port_disp_init 函数,来完成 LVGL 的初始化动作:
application
|-------> lv_init()
|-------> lv_port_disp_init()
|------------------> lv_disp_draw_buf_init() 配置绘制 buffer;
|------------------> lv_disp_drv_init() 配置底层驱动,挂接绘制函数 flush_cb;
|-------------------------------> lv_disp_drv_register() 注册驱动;
lv_disp_drv_register 分析
lv_disp_drv_init 分析
要说 lv_disp_drv_register,那么就要先说在他之前调用的 lv_disp_drv_init:
void lv_disp_drv_init(lv_disp_drv_t * driver)
{
lv_memset_00(driver, sizeof(lv_disp_drv_t));
driver->hor_res = 320;
driver->ver_res = 240;
driver->physical_hor_res = -1;
driver->physical_ver_res = -1;
driver->offset_x = 0;
driver->offset_y = 0;
driver->antialiasing = LV_COLOR_DEPTH > 8 ? 1 : 0;
driver->screen_transp = LV_COLOR_SCREEN_TRANSP;
driver->dpi = LV_DPI_DEF;
driver->color_chroma_key = LV_COLOR_CHROMA_KEY;
#if LV_USE_GPU_STM32_DMA2D
driver->draw_ctx_init = lv_draw_stm32_dma2d_ctx_init;
driver->draw_ctx_deinit = lv_draw_stm32_dma2d_ctx_init;
driver->draw_ctx_size = sizeof(lv_draw_stm32_dma2d_ctx_t);
#elif LV_USE_GPU_NXP_PXP
driver->draw_ctx_init = lv_draw_nxp_pxp_init;
driver->draw_ctx_deinit = lv_draw_nxp_pxp_init;
driver->draw_ctx_size = sizeof(lv_draw_nxp_pxp_t);
#elif LV_USE_GPU_NXP_VG_LITE
driver->draw_ctx_init = lv_draw_nxp_vglite_init;
driver->draw_ctx_deinit = lv_draw_nxp_vglite_init;
driver->draw_ctx_size = sizeof(lv_draw_nxp_vglite_t);
#elif LV_USE_GPU_SDL
driver->draw_ctx_init = lv_draw_sdl_init_ctx;
driver->draw_ctx_deinit = lv_draw_sdl_deinit_ctx;
driver->draw_ctx_size = sizeof(lv_draw_sdl_ctx_t);
#else
driver->draw_ctx_init = lv_draw_sw_init_ctx;
driver->draw_ctx_deinit = lv_draw_sw_init_ctx;
driver->draw_ctx_size = sizeof(lv_draw_sw_ctx_t);
#endif
}
初始化了一些关键的结构,这里,关系最后几个:
- draw_ctx_init;
- draw_ctx_deini;
- draw_ctx_size;
我们没定义 STM32_DMA2D 以及其他的,所以默认就走了最后一个,也就是纯软件的 draw context;
void lv_disp_drv_init(lv_disp_drv_t * driver)
{
......
driver->draw_ctx_init = lv_draw_sw_init_ctx;
driver->draw_ctx_deinit = lv_draw_sw_init_ctx;
driver->draw_ctx_size = sizeof(lv_draw_sw_ctx_t);
......
}
OK,看起来只有这个 lv_draw_sw_init_ctx,这个,记住他;
lv_disp_drv_register 分析lv_disp_t * lv_disp_drv_register(lv_disp_drv_t * driver)
{
// 首先生成一个 _lv_dis_ll 链表的头,disp
lv_disp_t * disp = _lv_ll_ins_head(&LV_GC_ROOT(_lv_disp_ll));
if(!disp) {
LV_ASSERT_MALLOC(disp);
return NULL;
}
// 这里没有指定 draw_ctx
if(driver->draw_ctx == NULL) {
// 分配一个 draw_ctx
lv_draw_ctx_t * draw_ctx = lv_mem_alloc(driver->draw_ctx_size);
LV_ASSERT_MALLOC(draw_ctx);
if(draw_ctx == NULL) return NULL;
// 调用 draw_ctx_init,也就是 lv_disp_drv_init 函数挂接的 lv_draw_sw_init_ctx
driver->draw_ctx_init(driver, draw_ctx);
// 将 draw_ctx 挂接到 drvier->draw_ctx
driver->draw_ctx = draw_ctx;
}
lv_memset_00(disp, sizeof(lv_disp_t));
// 将 driver 挂接到 disp->drvier
disp->driver = driver;
// 至此
disp 结构,已经包含的几乎所有的 port 相关的操作;
// 包含 flush_cb 即,直接绘制到屏的驱动,和平台相关;
// 包含绘制相关的接口,在 draw_ctx 中;
// 赋值给全局变量 disp_def
lv_disp_t * disp_def_tmp = disp_def;
disp_def = disp;
// 创建一个用于 refresh 的 lv_timer,使用默认的 LV_DISP_DEF_REFR_PERIOD=30ms 为周期,lv_timer 的回调函数为 _lv_disp_refr_timer
disp->refr_timer = lv_timer_create(_lv_disp_refr_timer, LV_DISP_DEF_REFR_PERIOD, disp);
LV_ASSERT_MALLOC(disp->refr_timer);
if(disp->refr_timer == NULL) {
lv_mem_free(disp);
return NULL;
}
// 判断分配的 buffer 是否可以支持全刷新模式
if(driver->full_refresh && driver->draw_buf->size < (uint32_t)driver->hor_res * driver->ver_res) {
driver->full_refresh = 0;
LV_LOG_WARN("full_refresh requires at least screen sized draw buffer(s)");
}
// 设置背景颜色为白色
disp->bg_color = lv_color_white();
// 设置不透明度
#if LV_COLOR_SCREEN_TRANSP
disp->bg_opa = LV_OPA_TRANSP;
#else
disp->bg_opa = LV_OPA_COVER;
#endif
#if LV_USE_THEME_DEFAULT
//
设置 theme
if(lv_theme_default_is_inited() == false) {
disp->theme = lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),
LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT);
}
else {
disp->theme = lv_theme_default_get();
}
#endif
//
创建系统的 3 个 screen
disp->act_scr = lv_obj_create(NULL);
disp->top_layer = lv_obj_create(NULL);
disp->sys_layer = lv_obj_create(NULL);
lv_obj_remove_style_all(disp->top_layer);
lv_obj_remove_style_all(disp->sys_layer);
lv_obj_clear_flag(disp->top_layer, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(disp->sys_layer, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_scrollbar_mode(disp->top_layer, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_scrollbar_mode(disp->sys_layer, LV_SCROLLBAR_MODE_OFF);
lv_obj_invalidate(disp->act_scr);
disp_def = disp_def_tmp;
if(disp_def == NULL) disp_def = disp;
lv_timer_ready(disp->refr_timer);
return disp;
}
lv_draw_sw_init_ctx 函数实现,其中包含了绘制 rect,line ,background 等等;
void lv_draw_sw_init_ctx(lv_disp_drv_t * drv, lv_draw_ctx_t * draw_ctx)
{
LV_UNUSED(drv);
lv_draw_sw_ctx_t * draw_sw_ctx = (lv_draw_sw_ctx_t *) draw_ctx;
lv_memset_00(draw_sw_ctx, sizeof(lv_draw_sw_ctx_t));
draw_sw_ctx->base_draw.draw_arc = lv_draw_sw_arc;
draw_sw_ctx->base_draw.draw_rect = lv_draw_sw_rect;
draw_sw_ctx->base_draw.draw_bg = lv_draw_sw_bg;
draw_sw_ctx->base_draw.draw_letter = lv_draw_sw_letter;
draw_sw_ctx->base_draw.draw_img_decoded = lv_draw_sw_img_decoded;
draw_sw_ctx->base_draw.draw_line = lv_draw_sw_line;
draw_sw_ctx->base_draw.draw_polygon = lv_draw_sw_polygon;
draw_sw_ctx->base_draw.wait_for_finish = lv_draw_sw_wait_for_finish;
draw_sw_ctx->blend = lv_draw_sw_blend_basic;
}
相关结构体和函数的关系如下所示:
如图所示, lv_disp_t 结构几乎包含了所有和显示相关的内容(直接或者间接的),包括 draw_buffer,flush_cb,绘制接口,refresh timer,和 LVGL 的几层基本 Layer;
注意
适配 lv_disp_drv_t 的时候,有些必须要指定的结构:
- 指向初始化的 lv_disp_draw_buf_t 变量的 draw_buf 指针;
- hor_res 显示器的水平分辨率(以像素为单位)
- ver_res 显示器的垂直分辨率(以像素为单位)
- flush_cb 一个回调函数,用于将缓冲区的内容复制到显示器的特定区域
lv_disp_flush_ready(&disp_drv) 需要在刷新准备好时调用。 LVGL 可能会以多个块呈现屏幕,因此多次调用 flush_cb。要查看当前是否是渲染的最后一个块,请使用 lv_disp_flush_is_last(&disp_drv)。
一些其他可选的回调,使处理单色、灰度或其他非标准 RGB 显示器更容易、更优化:
- rounder_cb 四舍五入要重绘的区域的坐标。例如。 2x2 px 可以转换为 2x8。 如果显示控制器只能刷新具有特定高度或宽度的区域(单色显示器通常为 8 像素高度),则可以使用它。
- set_px_cb 一个自定义函数来写入绘制缓冲区。如果显示器具有特殊的颜色格式,它可用于将像素更紧凑地存储在绘图缓冲区中。 (例如 1 位单色、2 位灰度等) 这样,lv_disp_draw_buf_t 中使用的缓冲区可以更小,以仅容纳给定区域大小所需的位数。请注意,使用set_px_cb 渲染比普通渲染慢。
- monitor_cb 一个回调函数,告诉我们在多长时间内刷新了多少像素。当最后一个块被渲染并发送到显示器时调用。
- clean_dcache_cb 用于清理与显示相关的任何缓存的回调。



