# 4. 显示设备适配

系统中存放 LCD 显示数据的显存被称为 Framebuffer(简称 fb),AWTK 设计了一个名为 lcd_t 的类型,把 fb 和 lcd_t 对象关联起来,AWTK 操作 lcd_t 对象等同于直接操作 LCD 显示的数据,达到控制 LCD 显示画面的目的。

大部分嵌入式平台都可以直接操作 fb,特殊小资源的平台没有 fb,则需要通过 SPI 或 I2C 等手段去控制 LCD 显示。

显示设备适配的主要步骤如下图所示:

图4.1 显示设备适配流程
图4.1 显示设备适配流程
  1. 根据平台显存的大小,选择开辟多少块 Framebuffer。

  2. 根据 LCD 的颜色格式和 Framebuffer 的个数,确定相关的 lcd_t 类型。

  3. 实现 platform_create_lcd 函数,在 platform_create_lcd 函数中创建该类型的 lcd_t 对象

  4. 根据 Framebuffer 的个数,选择刷新 LCD 的方式(分别为 Flush 模式和 Swap 模式),并重载 lcd_t 对象中平台相关的函数,实现显示模式的逻辑代码

  5. 在 platform_create_lcd 函数中返回初始化完毕的 lcd_t 对象给 AWTK 使用。

创建 lcd_t 对象后,将默认使用 Flush 刷新模式,用户可以通过重载 lcd_t 的以下函数实现不同的刷新模式:

函数名称 说明 备注
flush 每帧画面绘制结束时被调用,将图像从 offline_fb 拷贝到 online_fb 通常在使用特殊的 LCD 类型时重载
swap 每帧画面绘制结束时被调用,交换 offline_fb 与 online_fb 的地址 通常在使用 Swap 模式刷新 LCD 时重载

在 GUI 主循环中,AWTK 总是把界面先绘制到 offline_fb,再根据移植层实现的不同刷新策略,把 offline_fb 的内容送到 LCD 显示。刷新 LCD 模式如图所示:

图4.2 LCD刷新模式
图4.2 LCD刷新模式
  1. Flush 模式:是指把离线显存(offline_fb)中的数据拷贝在线显存(online_fb)中的操作,LCD 会把 online_fb 中的数据显示出来。Flush 过程中 LCD 显存指针一直保持不变。
  2. Swap 模式:是指离线显存(offline_fb)和在线显存(online_fb)的地址交换,LCD 再把新的 online_fb 数据显示出来。Swap 过程中 LCD 显存指针会切换到新的 online_fb 显存上。

这里不同的刷新策略由用户在移植层重载 lcd_t 的 flush/swap 接口去实现,比如缓冲区数量、如何拷贝或交换、如何实现垂直同步等。更多两种显示模式的介绍详见本章中的以下小节。

# 4.1 评估 Framebuffer 数量

根据不同平台的显存大小,用户可以选择以下四种 Framebuffer 的方案:

  1. 单 Framebuffer:平台的显存比较小,只能开辟一块屏幕大小的显存,LCD 显示和 AWTK 绘图都使用块显存。
  2. 双 Framebuffer:平台的显存较大,可以开辟两块屏幕大小的内存,一块用于 LCD 显示(online_fb),另一块用于 AWTK 绘图使用(offline_fb)。
  3. 三 Framebuffer:平台的显存很大,可以开辟三块屏幕大小的内存,一块用于 LCD 显示(online_fb),一块用于 AWTK 绘图使用(offline_fb),另外一块用于等到交换显存(next_fb)。
  4. 片段式 Framebuffer:平台的显存非常小,无法开辟一块完整屏幕大小的显存,只能开辟一小块内存作为显存的情况。

一块屏幕大小的显存所占内存为: LCD 的宽度 * LCD 的高度 * LCD的每像素占用的字节数。

# 4.2 创建 lcd_t 对象

AWTK 的 lcd_t 类型有一个子类 lcd_mem_t,该子类用于和 Framebuffer 绑定,并且该子类提供了嵌入式下常用的 LCD 颜色格式的实现,同时每种颜色格式的 lcd_mem_t 类型都有单/双/三 Framebuffer 的构造函数,让用户适配 LCD 变得非常简单。

这些适配代码实现详见 awtk/src/lcd 目录下前缀为 "lcd_mem_" 的文件,其中 lcd_mem_rgb565.c 提供了 RGB565 格式的 lcd_mem_t 类型的实现,其他文件名以此类推,用户只需要根据系统的 LCD 颜色格式找到对应的 lcd_mem_t 类型使用即可。

AWTK 的所有颜色格式布局为小端内存排序,例如 AWTK 的 lcd_t 类型颜色格式为 RGBA,fb 中的内存布局由低位往高位的顺序是 R,G,B,A

# 4.2.1 16 位色 lcd_t 类型

AWTK 提供了 BGR565 和 RGB565 两种 16 位色的 lcd_mem_t 类型,创建方法如下代码:

/* 该函数创建一个颜色格式为 rgb565 的 lcd_t 对象并将其返回给 AWTK 使用 */
lcd_t* platform_create_lcd(wh_t w, wh_t h) {
  /* w 和 h 为 LCD 的分辨率 */
  /* online_fb 为 LCD 正在显示的 fb;offline_fb 为 AWTK 用于绘制的 fb */
  /* 此处把两个 fb 地址传给 lcd_t 对象实现关联 */
  return lcd_mem_rgb565_create_double_fb(w, h, online_fb, offline_fb);
}

以下为 AWTK 提供的 16 位色 lcd_mem_t 类型的所有创建函数,请用户根据需求使用:

/* 颜色格式:bgr565 */
lcd_t* lcd_mem_bgr565_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_bgr565_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_bgr565_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb, uint8_t* next_fb);

/* 颜色格式:rgb565 */
lcd_t* lcd_mem_rgb565_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_rgb565_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_rgb565_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb,  uint8_t* next_fb);

# 4.2.2 32 位色 lcd_t 类型

AWTK 提供了 BGRA8888 和 RGBA8888 两种 32 位色的 lcd_mem_t 类型,创建方法和上面的 16 位色的 lcd_mem_t 类似。以下为所有创建函数,请用户根据需要选择使用:

/* 颜色格式:bgra8888 */
lcd_t* lcd_mem_bgra8888_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_bgra8888_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_bgra8888_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb, uint8_t* next_fb);

/* 颜色格式:rgba8888 */
lcd_t* lcd_mem_rgba8888_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_rgba8888_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_rgba8888_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb, uint8_t* next_fb);

# 4.2.3 24 位色 lcd_t 类型

AWTK 提供了 BGR888 和 RGB888 两个 24 位色的 lcd_mem_t 类型,创建方法和上面的 16 位色的 lcd_mem_t 类似。由于 24 位色内存不对齐,拷贝速度较慢,影响绘制效率,因此如果硬件允许,建议使用 32 位色的 LCD。以下为所有创建函数,请用户根据需要选择使用:

/* 颜色格式为:bgr888 */
lcd_t* lcd_mem_bgr888_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_bgr888_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_bgr888_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb, uint8_t* next_fb);

/* 颜色格式为:rgb888 */
lcd_t* lcd_mem_rgb888_create_single_fb(wh_t w, wh_t h, uint8_t* fbuff);
lcd_t* lcd_mem_rgb888_create_double_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb);
lcd_t* lcd_mem_rgb888_create_three_fb(wh_t w, wh_t h, uint8_t* online_fb, uint8_t* offline_fb, uint8_t* next_fb);

# 4.2.4 特殊 lcd_t 类型

AWTK 目前仅提供了常见的 16/24/32 位颜色的 LCD 创建函数,而在某些硬件平台上,还有特殊的 LCD 类型,比如单色屏、5551 的屏等。AWTK 提供了一个名为 lcd_mem_special 的 lcd_mem_t 类型,用于解决该情况。创建方法和上面的 16 位色的 lcd_mem_t 类似,还需要用户提供额外的几个回调函数,由用户实现 Flush 操作,将离线的 offline_fb 内容拷贝到特殊格式的 LCD 在线显存上。

离线 offline_fb 仅支持 565、888 和 8888 这几种标准格式。

/* awtk/lcd/lcd_mem_special.h 文件 */
/**
 * @method lcd_mem_special_create
 *
 * 创建lcd对象。
 *
 * @param {wh_t} w 宽度。
 * @param {wh_t} h 高度。
 * @param {bitmap_format_t} format 离线lcd的格式。一般用 BITMAP_FMT_BGR565 或 BITMAP_FMT_RGBA8888。
 * @param {lcd_flush_t} flush 回调函数,用于刷新GUI数据到实际的LCD。
 * @param {lcd_resize_t} on_resize 用于调整LCD的大小。一般用NULL即可。
 * @param {lcd_destroy_t} on_destroy lcd销毁时的回调函数。
 * @param {void*} ctx 回调函数的上下文。
 *
 * @return {lcd_t*} 返回lcd对象。
 */
lcd_t* lcd_mem_special_create(wh_t w, wh_t h, bitmap_format_t fmt, lcd_flush_t on_flush, lcd_resize_t on_resize, lcd_destroy_t on_destroy, void* ctx);

# 4.2.5 AWTK 内部位图颜色格式

确定了 lcd_t 类型的颜色格式后,还需要明确 AWTK 内部解码图片时,缓存的位图颜色格式。如果 AWTK 解码图片的颜色格式和 LCD 的颜色格式一致,那么像素点在显示时就不需要做颜色转换,可以提高渲染图片的效率。

AWTK 默认将所有图片解码为 RBGA8888 格式的位图,并提供了以下宏定义来设置解码位图的颜色格式:

宏定义 不透明图片解码为 透明图片解码为
缺省宏定义 RBGA8888 RBGA8888
WITH_BITMAP_BGR565 BGR565 无影响
WITH_BITMAP_RGB565 RGB565 无影响
WITH_BITMAP_BGR888 BGR888 无影响
WITH_BITMAP_RGB888 RGB888 无影响
WITH_BITMAP_BGRA 无影响 BGRA8888

需要注意的是,AWTK 在绘图的时候会自动检测是否需要颜色转化,就算不定义这些宏或者随便定义宏也可以正常显示,只是效率变低而已

# 4.3 使用 Flush 模式的双 Framebuffer 移植案例

此处假设平台的 LCD 颜色格式为 BGR565,平台显存可以分配出双 Framebuffer 。

1、LCD 初始化

AWTK 程序启动时,会在 tk_init 函数中调用 platform_create_lcd 函数初始化 LCD,该函数与平台相关,一般在移植层上面实现,AWTK 运行流程详见本文 1.3 章节的内容。GUI 主循环 main_loop 的基本功能通过 main_loop_raw.inc 来实现,用户只需实现 platform_create_lcd 函数初始化 LCD 即可,代码如下:

extern uint8_t* online_fb;   /* 系统 LCD 使用的显存 */
extern uint8_t* offline_fb;  /* 给 AWTK 绘图的显存 */
lcd_t* platform_create_lcd(wh_t w, wh_t h) {
  /* 根据 LCD 类型调用对应 lcd_mem_t 构造函数创建 lcd_t 对象 */
  return lcd_mem_bgr565_create_double_fb(w, h, online_fb, offline_fb);
}
#include "main_loop/main_loop_raw.inc"

2、LCD 的刷新流程

本案例中 lcd_mem_bgr565_create_double_fb函数创建的 lcd_t 对象默认使用 Flush 模式刷新图像,无需用户实现额外代码,即 GUI 通过拷贝内存的方式刷新 LCD,绘图流程如下:

图4.3 Flush模式的双Framebuffer刷新流程
图4.3 Flush模式的双Framebuffer刷新流程

(1) AWTK 调用 lcd_begin_frame 函数获取脏矩形,并获取 offline_fb 地址,准备绘制。

(2) AWTK 调用 lcd_t 对象的相关接口将界面变化的部分绘制到 offline_fb 上。

(3) AWTK 调用 lcd_end_frame 函数完成绘制,该函数中会调用 lcd_mem_flush 函数将 offline_fb 中脏矩形部分的图像数据拷贝到 online_fb 上完成 LCD 的刷新。

上述的流程或者函数被封装在 lcd_mem.inc 中,所以用户不需要重载任何函数。

本方案使用简单,无需写额外的刷新逻辑代码,但无法实现垂直同步功能,因此在显示时可能会出画面现撕裂现象。

# 4.4 使用 Swap 模式的双 Framebuffer 移植案例

此处假设平台的 LCD 颜色格式为 BGR565,平台显存可以分配出双 Framebuffer 。

显存地址(online_fb)交换操作一般要调用具体平台的 API,所以用户除了需要创建 lcd_mem_t 对象,还需要重载实现对象的 swap 函数,代码如下:

extern uint8_t* online_fb;    /* 系统 LCD 使用的显存 */
extern uint8_t* offline_fb;   /* 给 AWTK 绘图的显存 */

static ret_t lcd_mem_swap(lcd_t* lcd) {
  lcd_mem_t* mem = (lcd_mem_t*)lcd;
  uint8_t* tmp_fb = mem->offline_fb;

  system_wait_vbi();  /* 调用系统API加入垂直同步等待来保证画面没有撕裂 */

  /* 
   * 调用系统API把 offline_fb 设置为系统 LCD 使用的显存地址, 
   * 随后交换 offline_fb 和 online_fb 地址。
   */
  system_reg.fbaddr = mem->offline_fb;
  lcd_mem_set_offline_fb(mem, mem->online_fb);
  lcd_mem_set_online_fb(mem, tmp_fb);
  return RET_OK;
}

lcd_t* platform_create_lcd(wh_t w, wh_t h) {
  /* 根据 LCD 类型调用对应 lcd_mem_t 构造函数创建 lcd_t 对象 */
  lcd_t* lcd = lcd_mem_bgr565_create_double_fb(w, h, online_fb, offline_fb);
  lcd->swap = lcd_mem_swap;         /* 重载 swap 函数 */
  lcd->support_dirty_rect = FALSE;  /* 关闭脏矩形机制(新版本AWTK可以不关闭) */
  return lcd;
}
#include "main_loop/main_loop_raw.inc"

Swap 模式和 Flush 模式唯一的不同就是 Swap 模式重载了 lcd_t 对象的 swap 函数,让原本 LCD 的刷新流程的最后一步 lcd_mem_flush 函数改为调用 lcd_mem_swap 函数完成显存地址交换,并且重新设置系统 LCD 使用的显存地址,其刷新流程如下图所示:

图4.4 Swap模式的双Framebuffer刷新流程
图4.4 Swap模式的双Framebuffer刷新流程

在使用 Swap 模式的时候,一般会加入垂直同步等待来保证画面没有撕裂。

# 4.5 使用 Swap 模式的三 Framebuffer 移植案例

此处假设平台的 LCD 颜色格式为 BGR565,平台显存可以分配出三 Framebuffer ,并且加入垂直同步机制。

1、LCD 的刷新流程如下:

图4.5 Sawp模式的三Framebuffer刷新流程
图4.5 Sawp模式的三Framebuffer刷新流程

(1) AWTK 调用 lcd_begin_frame 函数获取脏矩形区域和准备绘制。

(2) AWTK 将界面变化部分绘制到 offline_fb 上面。

(3) AWTK 调用 lcd_end_frame 函数,完成绘制,其中会调用重载的 swap 函数。

(4) 移植层在 swap 函数中等待完成垂直同步的消息后,把 offline_fb 地址交给 next_fb 保存,并且把全局变量中的 online_fb 地址作为 offline_fb 地址。

(5) 移植层另外一个时刻,垂直同步回调函数触发后,会先把 online_fb 地址保存在全局变量中,把 online_fb 地址设置为 next_fb 的地址,并且对外通知完成垂直同步的消息。

2、伪代码实现

static uint8_t* volatile s_next_fb = NULL; /* next_fb地址 */
static uint8_t* volatile s_next_offline_fb = NULL; /* 上一帧online_fb,也是下一帧offline_fb */
extern uint8_t* s_framebuffers[3];         /* 三个Framebuffer的地址 */

/* 重载 swap 函数 */
ret_t lcd_mem_swap(lcd_t* lcd) {
  lcd_mem_t* mem = (lcd_mem_t*)lcd;
  /* 等到s_next_fb空闲同步已完成 */
  while (s_next_fb != NULL) {};
  /* 将绘制好的 offline_fb 设置为 next_fb */
  s_next_fb = lcd_mem_get_offline_fb(mem);
  /* 把上一帧的 online_fb 作为下一帧的 offline_fb */
  lcd_mem_set_offline_fb(mem, s_next_offline_fb);
  return RET_OK;
}

/* LCD 扫描完成后会触发垂直同步回调函数 */
void LTDC_IRQHandler(void) {
    if (s_next_fb != NULL) {
      /* s_next_offline_fb 变量保存上一帧的 online_fb 地址 */
	  s_next_offline_fb = system_reg.fbaddr;
      /* 调用系统API把 s_online_fb 的地址设置为当前 LCD 的显示显存 */
      system_reg.fbaddr = s_next_fb;
      s_next_fb = NULL;
    }
}

/* 创建 lcd_t 对象 */
lcd_t* platform_create_lcd(wh_t w, wh_t h) {
  /* s_next_offline_fb 变量保存下一帧的 offline_fb 地址  */
  s_next_offline_fb = s_framebuffers[2];
  /* 调用系统API把 s_online_fb 的地址设置为当前 LCD 的显示显存 */
  system_reg.fbaddr = s_framebuffers[0];

  /* 根据 LCD 类型调用对应构造函数创建 lcd_t 对象, s_framebuffers[1] 为 offline_fb 地址。 */
  lcd_t* lcd = lcd_mem_bgr565_create_three_fb(w, h, s_framebuffers[0], s_framebuffers[1], 
                                              s_framebuffers[2]);
  /* 关闭脏矩形机制和重载 swap 函数 */
  lcd->swap = lcd_mem_swap;
  /* 调用系统API配置屏幕中断*/
  system_enable_ltdc_irq();
  return lcd;
}

#include "main_loop/main_loop_raw.inc"

三 Framebuffer 实现的 LCD 相比于双 Framebuffer,多了一个等待显示的缓冲 next_fb,这样可以缓解双 Framebuffer 每帧画面要等待垂直同步信号,效率降低的问题,并且通过交换显存地址刷新 LCD,可以有效解决图像撕裂的问题。

# 4.6 使用 Swap + Flush 移植案例

Swap 和 Flush 各有各的优势(具体详见下一章节),使用 Swap + Flush 来刷新 LCD 可以结合这两种模式的优点,解决垂直同步问题的同时也可以启用脏矩形机制,并且还能支持 LCD 旋转(默认情况下只有 Flush 模式支持 LCD 旋转)。但两种模式的结合会消耗更多的内存,也会多一次显存拷贝操作,请酌情使用。

此处采用 awtk-linux-fb (opens new window) 项目中的双 fb 刷新模式作为案例来讲解如何同时使用两种刷新模式。注意这里说的双 fb 指的是一个离线 fb 和 两个在线 fb,可以在目标板上执行 fbset 命令设置虚拟高=2倍屏幕高来设置在线 fb 数量。如果显存足够大,甚至可以设置成三 fb。

1、awtk-linux-fb 中 fb 的刷新流程如下:

图4.6 Swap+Flush的刷新流程
图4.6 Swap+Flush的刷新流程

(1) AWTK 调用 lcd_begin_frame 函数获取脏矩形区域和准备绘制。

(2) AWTK 将界面变化部分绘制到 offline_fb 上面,其中 offline_fb 是 malloc 出来的一块与在线 fb 同大小的常规内存。

(3) AWTK 调用 lcd_end_frame 函数完成绘制,中间会调用 swap/flash 函数,这两个函数均可被重载(案例中 swap/flash 被重载为 lcd_mem_linux_wirte_buff 函数)。

(4) lcd_mem_linux_wirte_buff 函数中会获取脏矩形列表,并根据这些脏矩形把 offline_fb 中的数据拷贝到空闲的 next_fb 中,拷贝完成后将这个空闲的 next_fb 放入繁忙队列。

(5) 线程 fbswap_thread 会等待 next_fb 准备好以及屏幕垂直同步中断,中断到来时交换 online_fb 和 next_fb。即把上一个的 online_fb 的地址放入空闲队列变成 next_fb,然后把绘制好的 next_fb 将其设置为当前 online_fb 显示到 LCD 上。

2、关键代码分析:

/* awtk-linux-fb/awtk-port/lcd_linux/lcd_linux_fb.c */
/* 以下将省略大量无关代码以及线程安全锁代码,只为演示整个 fb 操作流程 */

/* 把offline fb的内容拷贝到指定的在线fb(fbid),其他RTOS平台移植时可参考本函数 */
static ret_t lcd_linux_flush(lcd_t* base, int fbid) {
  buff = fb->fbmem0 + size * fbid; /* 获得要进行拷贝的在线 fb 显存地址 */

  /* 分别创建 offline_fb 和 online_fb 对象,用于后续的脏矩形拷贝操作,固定的套路 */
  lcd_linux_init_drawing_fb(lcd, &offline_fb);
  lcd_linux_init_online_fb(lcd, &online_fb, buff, fb_width(fb), fb_height(fb), fb_line_length(fb));

  /* 把在线 fb 显存地址放到脏矩形管理器中管理, 并更新脏矩形管理器中的所有地址的脏矩形信息 */
  lcd_fb_dirty_rects_add_fb_info(&(lcd->fb_dirty_rects_list), buff);
  lcd_fb_dirty_rects_update_all_fb_dirty_rects(&(lcd->fb_dirty_rects_list), base->dirty_rects);

  /* 从脏矩形管理器获取这个在线 fb 显存的脏矩形, 并根据脏矩形来拷贝对应的数据 */
  dirty_rects = lcd_fb_dirty_rects_get_dirty_rects_by_fb(&(lcd->fb_dirty_rects_list), buff);
  if (dirty_rects != NULL && dirty_rects->nr > 0) {
    for (int i = 0; i < dirty_rects->nr; i++) {
      const rect_t* dr = (const rect_t*)dirty_rects->rects + i;
      /* 根据应用是否旋转 LCD 进行直接拷贝或旋转拷贝 */
      if (o == LCD_ORIENTATION_0) {
        image_copy(&online_fb, &offline_fb, dr, dr->x, dr->y);
      } else {
        image_rotate(&online_fb, &offline_fb, dr, o);
      }
    }
  }

  /* 重置脏矩形管理器中这个在线 fb 显存的脏矩形 */
  lcd_fb_dirty_rects_reset_dirty_rects_by_fb(&(lcd->fb_dirty_rects_list), buff);
  return RET_OK;
}

/* AWTK 往 offline fb 绘制完一帧画面时,将进入该函数,该函数在 GUI 线程运行 */
static ret_t lcd_mem_linux_wirte_buff(lcd_t* lcd) {
  /* 等待并获得下一个空闲的在线 fb(next_fb)  */
  tk_semaphore_wait(s_sem_spare, -1);
  fb_taged_t* spare_fb = get_spare_fb();

  /* 把 offline fb 的内容拷贝到这个空闲的在线 fb(next_fb)  */
  ret = lcd_linux_flush(lcd, spare_fb->fbid);

  /* 把 next_fb 的标记修改为数据已准备好,发信号通知 fbswap_thread 交换显存 */
  spare_fb->tags = FB_TAG_READY;
  tk_semaphore_post(s_sem_ready);
}

/* 两个在线 fb 交换线程 */
static void* fbswap_thread(void* ctx) {
  /* 等待 next_fb 数据准备好(填充完成), 把 next_fb 设置为当前在线 online_fb */
  tk_semaphore_wait(s_sem_ready, -1);

  fb_taged_t* ready_fb = get_ready_fb();
  int ready_fbid = ready_fb->fbid;
  vi.yoffset = ready_fbid * fb_height(fb);

  /* 发出切换显存地址请求 FBIOPAN_DISPLAY,并等交换完成(垂直同步到来时交换完成) */
  ioctl(fb->fd, FBIOPAN_DISPLAY, &vi);
  ioctl(fb->fd, FBIO_WAITFORVSYNC, &dummy);

  /* 把之前的的 online_fb 变为下一个空闲状态(next_fb),并发信号通知 GUI 线程 */
  fb_taged_t* last_busy_fb = get_busy_fb();
  last_busy_fb->tags = FB_TAG_SPARE;

  tk_semaphore_post(s_sem_spare);
  ready_fb->tags = FB_TAG_BUSY;
}

/* lcd 适配层初始化 */
static lcd_t* lcd_linux_create_swappable(fb_info_t* fb) {
  uint8_t* offline_fb = (uint8_t*)(malloc(fb_size()));
  lcd = lcd_mem_XXXXX_create_single_fb(w, h, offline_fb);

  /* 重载 swap 函数和 flush 函数,LCD 不旋转时将进入 swap 函数,LCD 旋转时将进入 flush 函数 */
  lcd->swap = lcd_mem_linux_wirte_buff;
  lcd->flush = lcd_mem_linux_wirte_buff;
}

上述内容可以总结为以下两点:

  • 在 lcd_mem_linux_wirte_buff 函数中,根据脏矩形的区域把 offline_fb 的数据拷贝(同时旋转)到空闲的 next_fb。
  • 垂直同步信号到来时,把 next_fb 和 online_fb 进行交换,实现屏幕显示帧更新,并解决刷新画面撕裂问题。

示例代码详见:awtk-linux-fb/awtk-port/lcd_linux/lcd_linux_fb.c。

# 4.7 理解的 Flush 和 Swap 的本质

在 AWTK 整个绘图流程中,有两个非常重要的 Framebuffer 指针。一个是在 lcd_t 对象中记录的当前的离线显存指针 offline_fb,AWTK 会在该指针指向的缓冲区中做所有的绘图操作;另外一个是 LCD 控制器记录的当前在线显存指针 online_fb(通常是一个硬件寄存器),控制器会不断的扫描该指针指向的缓冲区,把内容映射到显示屏上。 Flush 和 Swap 要做的核心工作就是如何有效的使用和控制这两个指针,使得 AWTK 绘制的画面可以及时的完美的显示到屏幕上。

mem->offline_fb      /* offline_fb */
system_reg.fbaddr    /* online_fb  */

Flush 通常指的是拷贝操作,把离线显存(offline_fb)的数据拷贝到 LCD 显示显存(online_fb)中,但如果拷贝过程中刚好 LCD 也在刷新 online_fb,相当于一边在写 online_fb,另外一边同时在读,速度不可能完全同步,很容易出画面撕裂感。

Swap 通常指的是交换指针操作,LCD 控制器每扫描完一帧画面后会触发一次中断(垂直同步信号),这时马上的把离线显存(offline_fb)和在线显存(online_fb)的两个指针做交换,由于地址的设置几乎是瞬间就完成,所以 LCD 画面不会出现撕裂感。但由于中断处理过程与 GUI 主循环是异步执行的,两个指针应在合适的时机做交换,即必须保证将要切换到 online_fb 的是一帧绘制完整的缓冲区。实践中可以通过多缓冲和信号量同步的机制解决。

在某些应用场合,甚至可以把 Flush 和 Swap 机制结合起来使用,比如 awtk-linux-fb,通过 Flush 实现带 cache 的可旋转的 offline_fb,通过多个 online_fb 缓冲 Swap 实现垂直同步交换。AWTK 并没有限制用户如何使用这两种模式,而取决于用户如何实现 lcd_t 的 flush 或 swap 接口,如何有效的使用和控制 online_fb、offline_fb 这两个指针,移植时应根据使用场景需求灵活取舍。

# 4.7.1 Flush 的优点和缺点总结

Flush 是把离线显存(offline_fb)的数据拷贝到其他显存中,无法实现垂直同步,而拷贝势必会消耗性能。同时 Flush 最坏的情况是既要全屏画,又要全屏拷贝,这样会导致双倍的消耗时间,但是 Flush 也不是一无是处的,下面是 Flush 的优点:

  1. 拷贝就意味着离线显存(offline_fb)的地址是不会变,AWTK 可以一直使用这块显存,那么这块内存就可以是 cache 类型的,有 CPU 的 cache 机制支持,可以提高内存的读写速度,即可以提高 AWTK 绘图的效率。
  2. 由于通过拷贝内存刷新图像,所以可以在拷贝的时候给显存数据做旋转,达到屏幕旋转的效果,其中 AWTK 的 tk_set_lcd_orientation 函数(LCD 旋转机制)就是使用 Flush 模式来实现的。
  3. 由于通过拷贝内存刷新图像,所以可以在拷贝的过程中,做颜色转化,比如本文 3.2.3 章节中说的要在特殊颜色格式的 LCD 上(lcd_mem_special 类型)显示,也是使用到 Flush 模式。

# 4.7.2 Swap 的优点和缺点总结

Swap 是把离线显存(offline_fb)的地址和其他的显存地址进行交换,而交换地址是不需要耗费时间的,所以 Swap 机制最大的好处就是省下拷贝数据的时间,实现垂直同步。但是这也会带来另外一个问题,就是离线显存(offline_fb)的属性一般是 no cache,这样会让 AWTK 在绘图时,尤其是颜色混合计算时,频繁的读取离线显存(offline_fb)的数据,导致性能下降。

同时因为 Swap 的显存地址交换,无法实现画面旋转功能,无法启用 AWTK 的脏矩形机制,即 Swap 模式下每一帧都需要全屏绘图。

在 AWTK 1.7 及其以上版本中,三 Framebuffer 的 Swap 模式支持脏矩形刷新机制,但必须使用 lcd_mem_set_offline_fb 去修改 offline_fb 指针。

# 4.8 片段式 Framebuffer 移植案例

在低端的嵌入式平台上,可能没有足够的空间创建一屏的 Framebuffer,而直接使用寄存器写入颜色数据容易出现屏幕闪烁和画面撕裂的现象,比较好的方法是创建一小块 Framebuffer,把需要绘制的区域分成多个小块,一次绘制一小块,由于 AWTK 有脏矩阵机制,在大多数情况下只需要刷新一小块屏幕,依然可以保持比较快的速度,且解决寄存器写入的闪烁问题。

此处假设平台的 LCD 颜色格式为 BGR565,平台显存无法分配出单个完整的 Framebuffer。

1、LCD 初始化

只需在 platform_create_lcd 函数中调用 lcd_mem_fragment_create 函数创建 lcd_t 对象即可,代码如下:

static lcd_t *platform_create_lcd(wh_t w, wh_t h) {
  /* 创建基于片段式 Framebuffer 实现的 lcd_t 对象 */
  return lcd_mem_fragment_create(w, h);  
}

#include "main_loop/main_loop_raw.inc"   /* 添加 main_loop_raw.inc 中的代码 */

2、LCD 的工作原理

片段式 Framebuffer 的工作原理其实就是创建一小块内存作为片段式 Framebuffer ,其大小为 FRAGMENT_FRAME_BUFFER_SIZE * 单位像素大小,在绘制图像时,先通过 AWTK 的脏矩形机制找到重绘区域,然后根据重绘区域的大小判断是否需要进行切片:

  • 若无需切片,则直接将重绘区域的图像拷贝到片段式显存中,再通过 Flush 函数绘制到屏幕的对应区域;
  • 若需要切片,则在切片后按照顺序将每一片重绘区域先拷贝到片段式显存中,再通过 Flush 函数绘制到屏幕的对应区域。

绘制流程图如下:

图4.7 片段式显存绘制流程图
图4.7 片段式显存绘制流程图

3、LCD 的实现

AWTK 提供了基于片段式 Framebuffer 的 LCD 缺省实现,只需要定义宏 FRAGMENT_FRAME_BUFFER_SIZE 并提供 set_window_func 和 write_data_func 两个函数/宏即可。它们的用法如下:

  • set_window_func 函数用于设置 LCD 上要写入颜色数据的区域,该区域对 AWTK 来讲实际上是就脏矩形区域,以矩形左上角为起始坐标,向右延伸宽度,向下延伸高度。设置区域相较于每次写入颜色时设置坐标,可以极大提高工作效率。
  • write_data_func 函数用于向 LCD 上写入颜色数据,从设置区域的起始坐标开始,从左往右、从上往下每次写入一个像素点。

首先,通过定义宏 FRAGMENT_FRAME_BUFFER_SIZE 设置片段式显存的大小,代码如下:

 #define FRAGMENT_FRAME_BUFFER_SIZE 8 * 1024

备注:宏 FRAGMENT_FRAME_BUFFER_SIZE 的单位是像素个数,其大小必须大于等于 LCD 的宽度,这是由于片段式 Framebuffer 的实现原理导致的。

接下来定义宏 set_window_func 和宏 write_data_func,代码如下:

#define set_window_func LCD_Set_Window       /* 使用 LCD_Set_Window 函数设置绘制区域 */
#define write_data_func LCD_WriteData_Color  /* 使用 LCD_WriteData_Color 函数写入颜色数据 */

除了以上方式之外,在特殊情况下(比如 SPI 接口的 LCD 或特殊的 LCD 格式类型),也可以选择实现 lcd_draw_bitmap_impl 函数/宏来绘制图像,它负责把变化的部分更新到物理设备,参考片段式 Framebuffer 中的 Flush 函数(lcd_mem_fragment_flush)

/* awtk/src/lcd/lcd_mem_fragment.inc */
...
static ret_t lcd_mem_fragment_flush(lcd_t* lcd) {
  lcd_mem_fragment_t* mem = (lcd_mem_fragment_t*)lcd;

  int32_t x = mem->x;
  int32_t y = mem->y;
  uint32_t w = mem->fb.w;
  uint32_t h = mem->fb.h;
  pixel_t* p = mem->buff;
  
  /** 
   * 如果定义了宏 lcd_draw_bitmap_impl,则使用该宏来刷新屏幕(将片段式显存中的图像数据拷贝到屏幕上),
   * 否则使用宏 set_window_func 和宏 write_data_func 来刷新屏幕。
  */
#ifdef lcd_draw_bitmap_impl
  lcd_draw_bitmap_impl(x, y, w, h, p);
#else
  uint32_t nr = w * h;
  set_window_func(x, y, x + w - 1, y + h - 1);
  while (nr-- > 0) {
    write_data_func(*p++);
  }
#endif
	
  return RET_OK;
}
...

4、 片段式 Framebuffer 的优缺点

片段式 Framebuffer 主要用在内存不足以提供完整 Framebuffer 的低端平台上,其优点相当明显。

  • 大幅度减少了 Framebuffer 的内存开销,支持脏矩阵机制,提高了低端平台上的渲染性能;
  • 能有效解决基于寄存器实现的 LCD 在屏幕较大时出现的闪烁和画面撕裂问题。

由于内存和 CPU 性能问题,片段式 Framebuffer 的缺点如下:

  • 不支持窗口动画;
  • 不支持离线画布(canvas_offline_t);
  • 不支持控件截图,即无法调用 widget_take_snapshot_rect 接口;
  • 脏矩形区域较大时,重绘区域切片较多,绘制速度比较慢。

片段式 Framebuffer 的具体实现,可以参考 AWTK 针对 STM32f103ze 的移植 (opens new window)

# 4.9 LCD 显示异常时的排查方法

在移植 AWTK 后,如果 LCD 显示异常,比如出现颜色异常、花屏、画面错位等情况,通常可以按照以下顺序排查问题:

  1. 先不运行 AWTK,直接向显存中写入数据,检查硬件 LCD 是否能正常显示,颜色是否正常;
  2. 确保硬件 LCD 显示正常后,检查移植层中的 AWTK lcd_t 对象的颜色格式是否与硬件 LCD 一致;
  3. 确保颜色格式一致后,检查 lcd_t 对象使用的 Framebuffer 地址是否正确;
  4. 以上都确认无误后,可以调试移植层 lcd_t 对象的重载函数,比如重载的 flush 函数或 swap 函数,查看绘制数据是否正常。