添加自定义 GPU

LVGL 拥有灵活且可扩展的绘图管线。您可以通过 GPU 实现部分渲染,甚至完全替换内置的软件渲染器。

绘图上下文

绘图的核心结构是 lv_draw_ctx_t。 它包含一个指向绘图缓冲区的指针,以及用于绘制矩形、文本和其他图形元素的回调函数。

字段

lv_draw_ctx_t 包含以下字段:

  • void * buf 指向绘图缓冲区的指针

  • lv_area_t * buf_area 缓冲区的位置和大小(绝对坐标)

  • const lv_area_t * clip_area 当前裁剪区域的绝对坐标,总是与 buf_area 相同或更小。所有绘图都应裁剪到此区域。

  • void (*draw_rect)() 绘制带阴影、渐变、边框等的矩形

  • void (*draw_arc)() 绘制弧线

  • void (*draw_img_decoded)() 绘制已由 LVGL 解码的 (A)RGB 图像

  • lv_res_t (*draw_img)() 绘制图像(在解码之前,绕过 LVGL 的内部图像解码器)

  • void (*draw_letter)() 绘制字母

  • void (*draw_line)() 绘制线条

  • void (*draw_polygon)() 绘制多边形

  • void (*draw_bg)() 用没有装饰(如圆角或边框)的矩形替换缓冲区

  • void (*wait_for_finish)() 等待所有后台操作完成(例如 GPU 操作)

  • void * user_data 用于任意目的的自定义用户数据

(为简洁起见,这里未显示回调函数的参数。)

所有 draw_* 回调的第一个参数是指向当前 draw_ctx 的指针。其他参数中包含一个描述符,用于指示绘制内容, 例如,对于 draw_rect,描述符是 lv_draw_rect_dsc_t, 对于 lv_draw_line,描述符是 lv_draw_line_dsc_t 等。

要根据 draw_dsc 正确渲染,您需要熟悉 LVGL 的 盒模型 以及字段的含义。字段的名称和含义与 样式属性 的名称和含义相同。

初始化

lv_disp_drv_t 有 4 个与绘图上下文相关的字段:

  • lv_draw_ctx_t * draw_ctx 指向此显示器的 draw_ctx 的指针

  • void (*draw_ctx_init)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx) 初始化 draw_ctx 的回调

  • void (*draw_ctx_deinit)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx) 反初始化 draw_ctx 的回调

  • size_t draw_ctx_size 绘图上下文结构的大小。例如 sizeof(lv_draw_sw_ctx_t)

如果忽略这些字段,LVGL 会根据 lv_conf.h 中的配置在 lv_disp_drv_init() 中设置默认值。 lv_disp_drv_register() 会根据 draw_ctx_size 分配一个 draw_ctx,并调用 draw_ctx_init() 对其进行初始化。

但是,您可以在调用 lv_disp_drv_register() 之前覆盖回调和大小值。 这使得可以使用您自己的 draw_ctx 和自定义回调。

软件渲染器

LVGL 内置的软件渲染器扩展了基本的 lv_draw_ctx_t 结构并设置了绘图回调。它看起来像这样:

typedef struct {
   /** 包含基本的 draw_ctx 类型 */
    lv_draw_ctx_t base_draw;

    /** 将颜色或图像混合到一个区域 */
    void (*blend)(lv_draw_ctx_t * draw_ctx, const lv_draw_sw_blend_dsc_t * dsc);
} lv_draw_sw_ctx_t;

draw_ctx_init() 中设置绘图回调,如下所示:

draw_sw_ctx->base_draw.draw_rect = lv_draw_sw_rect;
draw_sw_ctx->base_draw.draw_letter = lv_draw_sw_letter;
...

混合回调

如上所述,软件渲染器添加了 blend 回调字段。这是一个与软件渲染器工作方式相关的特殊回调。 所有绘图操作最终都会调用 blend 回调,该回调可以通过考虑可选的遮罩来填充一个区域或将图像复制到一个区域。

lv_draw_sw_blend_dsc_t 参数描述了混合的内容和方式。它具有以下字段:

  • const lv_area_t * blend_area 要在 draw_ctx->buf 上绘制的区域的绝对坐标。如果设置了 src_buf,则为要混合的图像的坐标。

  • const lv_color_t * src_buf 指向要混合的图像的指针。如果设置了此字段,则忽略 color。如果未设置,则用 color 填充 blend_area

  • lv_color_t color 填充颜色。仅在 src_buf == NULL 时使用。

  • lv_opa_t * mask_buf 如果忽略则为 NULL,或者为要应用于 blend_area 的 alpha 遮罩。

  • lv_draw_mask_res_t mask_res 先前遮罩操作的结果。(LV_DRAW_MASK_RES_...

  • const lv_area_t * mask_area mask_buf 的绝对坐标区域。

  • lv_opa_t opa 总体不透明度。

  • lv_blend_mode_t blend_mode 例如 LV_BLEND_MODE_ADDITIVE

扩展软件渲染器

新的混合回调

让我们举一个实际的例子:您希望仅使用 MCU 的 GPU 进行颜色填充操作。

由于所有绘图回调最终都会调用 blend 回调来填充一个区域,因此只需要覆盖 blend 回调。

首先扩展 lv_draw_sw_ctx_t


/*我们不添加新字段,因此仅为了清晰起见添加新类型*/
typedef lv_draw_sw_ctx_t my_draw_ctx_t;

void my_draw_ctx_init(lv_disp_drv_t * drv, lv_draw_ctx_t * draw_ctx)
{
    /*首先初始化父类型*/
    lv_draw_sw_init_ctx(drv, draw_ctx);

    /*更改一些回调*/
    my_draw_ctx_t * my_draw_ctx = (my_draw_ctx_t *)draw_ctx;

    my_draw_ctx->blend = my_draw_blend;
    my_draw_ctx->base_draw.wait_for_finish = my_gpu_wait;
}

在调用 lv_disp_draw_init(&drv) 后,您可以分配新的 draw_ctx_init 回调并设置 draw_ctx_size 以覆盖默认值:

static lv_disp_drv_t drv;
lv_disp_draw_init(&drv);
drv->hor_res = my_hor_res;
drv->ver_res = my_ver_res;
drv->flush_cb = my_flush_cb;

/*新的绘图上下文设置*/
drv->draw_ctx_init = my_draw_ctx_init;
drv->draw_ctx_size = sizeof(my_draw_ctx_t);

lv_disp_drv_register(&drv);

这样,当 LVGL 调用 blend 时,它将调用 my_draw_blend,我们可以执行自定义 GPU 操作。以下是一个完整的示例:

void my_draw_blend(lv_draw_ctx_t * draw_ctx, const lv_draw_sw_blend_dsc_t * dsc)
{
    /*让我们获取混合区域,它是要填充的区域和裁剪区域的交集。*/
    lv_area_t blend_area;
    if(!_lv_area_intersect(&blend_area, dsc->blend_area, draw_ctx->clip_area)) return;  /*完全裁剪,无需操作*/

    /*仅填充非遮罩、完全不透明、正常混合且不太小的区域*/
    if(dsc->src_buf == NULL && dsc->mask == NULL && dsc->opa >= LV_OPA_MAX &&
       dsc->blend_mode == LV_BLEND_MODE_NORMAL && lv_area_get_size(&blend_area) > 100) {

        /*获取缓冲区中的第一个像素*/
        lv_coord_t dest_stride = lv_area_get_width(draw_ctx->buf_area); /*目标缓冲区的宽度*/
        lv_color_t * dest_buf = draw_ctx->buf;
        dest_buf += dest_stride * (blend_area.y1 - draw_ctx->buf_area->y1) + (blend_area.x1 - draw_ctx->buf_area->x1);

        /*使混合区域相对于缓冲区*/
        lv_area_move(&blend_area, -draw_ctx->buf_area->x1, -draw_ctx->buf_area->y1);
       
        /*调用您的自定义 GPU 填充函数以填充 blend_area,位于 dest_buf 上,使用 dsc->color*/
        my_gpu_fill(dest_buf, dest_stride, &blend_area, dsc->color);
    }
    /*回退:GPU 不支持这些设置。调用软件渲染器。*/
    else {
      lv_draw_sw_blend_basic(draw_ctx, dsc);
    }
}

等待回调的实现要简单得多:

void my_gpu_wait(lv_draw_ctx_t * draw_ctx)
{
    while(my_gpu_is_working());
   
    /*也调用软件渲染器的等待回调*/
    lv_draw_sw_wait_for_finish(draw_ctx);
}

新的矩形绘制器

如果您的 MCU 拥有更强大的 GPU,可以绘制例如圆角矩形,您也可以替换原始的软件绘制器。 一个自定义的 draw_rect 回调可能如下所示:

void my_draw_rect(lv_draw_ctx_t * draw_ctx, const lv_draw_rect_dsc_t * dsc, const lv_area_t * coords)
{
  if(lv_draw_mask_is_any(coords) == false && dsc->grad == NULL && dsc->bg_img_src == NULL &&
     dsc->shadow_width == 0 && dsc->blend_mode = LV_BLEND_MODE_NORMAL)
  {
    /*绘制背景*/
    my_bg_drawer(draw_ctx, coords, dsc->bg_color, dsc->radius);
   
    /*如果有边框则绘制*/
    if(dsc->border_width) {
      my_border_drawer(draw_ctx, coords, dsc->border_width, dsc->border_color, dsc->border_opa)
    }
   
    /*如果有轮廓则绘制*/
    if(dsc->outline_width) {
      my_outline_drawer(draw_ctx, coords, dsc->outline_width, dsc->outline_color, dsc->outline_opa, dsc->outline_pad)
    }
  }
  /*回退*/
  else {
    lv_draw_sw_rect(draw_ctx, dsc, coords);
  }
}

my_draw_rect 如果需要可以完全绕过 blend 回调的使用。

完全自定义绘图引擎

例如,如果您的 MCU/MPU 支持强大的矢量图形引擎,您可能只使用它而不是 LVGL 的软件渲染器。 在这种情况下,您需要基于基本的 lv_draw_ctx_t(而不是 lv_draw_sw_ctx_t)并根据需要扩展/初始化它。