傻孩子(GorgonMeducer) · 2021年10月19日

【例说Arm-2D界面设计】从不规则图标的显示说起

【说在前面的话】

Arm-2D是Arm公司为Cortex-M处理器平台量身打造的一款2D图形处理方案。针对已有的经典Cortex-M内核,诸如Cortex-M0/M0+/M3/M4/M7/M33等,Arm-2D提供了经过优化了的软件加速库——虽然在资源丰富的环境下,Arm-2D在这些传统处理器上无法与市面上各类GUI在同等条件下拉开性能差距,但在大部分GUI都无法覆盖的小资源处理器上,Arm-2D却提供了以极其低廉的手段实现智能手机级别GUI的可能性。当然更不用说在最新问世的Cortex-M55处理器上,借助Helium技术的加持,Arm-2D可以提供相较传统方案4倍以上的加速能力。

从这一系列文章开始,我将以范例的形式,一小步一小步的带领大家实现“从入门到精通”的跨越。每篇文章都不会追求内容的广度,但力求在一类具体的问题上为大家讲懂、讲透。

【前提和约定】

在开始今天的内容之前,为了让大家能站在一个较为统一的视角上看待问题,我觉得很有必要在出现因为立场不同而产生不必要的误解和争论之前,做出一些必要的约定:

  • 虽然Arm-2D本身同时支持资源丰富和资源受限的环境,但这一系列的文章主要从资源受限系统的角度出发做出设计上的考量
  • 目标处理器的系统频率在72MHz以内,覆盖从Cortex-M0到Cortex-M4F的处理器范围;
  • Flash的大小在128KByte以内;
  • SRAM的大小在4~32K的范围内;
  • 允许使用XIP将外部的SPI-FLASH映射到4G地址空间中
  • 目标应用场景主要为各类对动态效果没有要求的界面
  • 各类仪器仪表
  • 智能家居的控制面板
  • 手持设备的菜单界面
  • ……
  • 关于刷新率约定:
  • 当整个屏幕都进行完整刷新时,我们用 Frame-per-sec, FPS 来描述;
  • 当我们只刷新屏幕的局部时,我们用 Update-per-sec, UPS 来描述;
  • 目标应用场景在大部分情况下对帧率没有或只有较低的要求
  • 大部分情况下 1~3 FPS 即可满足要求
  • 部分场景下,在屏幕的局部可能会要求达到30 UPS的刷新率(比如动态进度条等等)
  • 在效果允许的情况下,尽可能减小 PFB的尺寸——在相同PFB面积的情况下,尽可能在确保Height不小于8时取Width所允许的最大值。
  • 本系列介绍的各类方法主要适用于无法负担起常规GUI协议栈(比如LVGL)的环境,如果您的条件允许,还是推荐直接使用常规GUI进行界面设计(这类GUI在底层仍然可以使用Arm-2D对一些算法进行加速)。
  • 本文假设读者已经完成了Arm-2D在本地平台的移植:
  • 如果您还没有完成这一步骤,请先移步《【教程更新】一网打尽Arm-2D的资料和傻瓜部署教程
  • 如果您想跳过移植的步骤,直接进入Arm-2D的使用和学习环节,可以参考文章《懒人玩Arm-2D究竟有几种姿势

    为了便于讲解,本文将主要使用 example 目录下的 [template][cmsis-rtos2][pfb] 模板作为起点。截图来自FastModel。
    image.png

  • 因为我们主要考虑资源有限的环境,因此推荐使用 -Omin(或-Oz) 优化等级、开启 Link-Time-Optimisation、并使用microLib:
    image.png

注意

当你使用 [template][cmsis-rtos2][pfb] 或 [template][bare-metal][pfb] 模板时,别忘记正确设置 Dirty Region List哦,否则你将永远只能看到屏幕中央一块 80*80 见方大小的区域。具体方法如下:

1、找到 main.c
2、在函数 display_task() 中找到以下内容:

void display_task(void)
{
    ...
//! call partial framebuffer helper service
while(arm_fsm_rt_cpl != arm_2d_helper_pfb_task( 
                                &s_tExamplePFB, 
                                (arm_2d_region_list_item_t *)s_tDirtyRegions));
    ...
}

3、将其修改为:

void display_task(void)
{
    ...
//! call partial framebuffer helper service
while(arm_fsm_rt_cpl != arm_2d_helper_pfb_task( 
                                &s_tExamplePFB, 
NULL));
    ...
}

其本质是完全不使用Dirty Region List,从而迫使 PFB Helper服务完整的刷新整个屏幕。

【API的异步工作模式】

Arm-2D的API同时支持RTOS和裸机环境。

RTOS环境下,用户以异步的方式使用API——简单来理解,当用户调用API时,只是给后台的2D流水线下达了一个任务,且还未等任务执行完成就从函数已经退出了。任务的实际内容是在另外一个线程上来实现的。

在这种情况下,如果一个API要使用一些具有“时效性”的资源——比如某些局部变量,显然,就要把这些变量的生命周期考虑在内——不能只在调用API的时候内容有效,结果当另外一个线程实际干活的时候却发现对应的内容已经“物是人非”了。

为了解决这类问题,最无脑的方式就是在调用API后通过下面的函数来等待任务完成。

arm_2d_op_wait_async(NULL);

这里,arm_2d是函数前缀,op是操作(operation)的缩写,wait_async是等待异步任务完成(wait-asynchronous)的意思。至于括号里的NULL,说来话长,暂时就当做格式来记忆吧。

image.png
举个例子:

/*! \note arm_2d_align_centre generate a region '__centre_region' based
 *!       on the given target tile (frame buffer) and the size of the 
 *!       target area of your following drawing.
 *!       Use '__centre_region' when required as the target region in
 *!       2D operations inside the {...} .
 */
 arm_2d_align_centre(tTempPanel, c_tileCMSISLogo.tRegion.tSize) {
    //!< copy CMSIS logo (with masks) to the centre of the right panel
    arm_2d_rgb565_tile_copy_with_masks(
                                &c_tileCMSISLogo,
                                &c_tileCMSISLogoMask,
                                &tTempPanel,
                                &c_tileFadeMask,
                                &__centre_region,                               //!< generated by arm_2d_align_centre()

                                /*! remove ARM_2D_CP_MODE_FILL and only keeps
                                 *! mirroring mode
                                 */
                                ptLayers[0].wMode &~ ARM_2D_CP_MODE_FILL);

    arm_2d_op_wait_async(NULL);
}

上述例子中,arm_2d_align_centre()的作用是根据用户给定的目标显示缓冲区(比如tTempPanel)和区域大小(比如c_tileCMSISLogo.tRegion.tSize)生成一个能将给定大小的区域居中的_arm_2d_region_t对象——局部变量 __centre_region——它的生命周期仅限于arm_2d_align_centre()后的花括号内。

姑且不论 arm_2d_rgb565_tile_copy_with_masks()实际用 __centre_region做了什么,但考虑到它的生命周期有限,因此必须在退出花括号之前调用 arm_2d_op_wait_async(NULL)来等待操作完成。

虽然裸机环境下,所有arm-2d的API都是以同步模式来工作的(即退出API就意味着任务完成或者出现了错误),理论上的确完全无需arm_2d_op_wait_async(NULL)调用来实现所谓的同步,但以“异步工作模式”使用API写出来的代码拥有最高的兼容性——可以同时在RTOS环境和裸机环境下使用,因此,本系列文章统一以异步模式为蓝本来讲解后续的内容

【背景和命题】

随便打开一部手机或是平板电脑,我们就可以很容易的看到:在现代的界面设计中,圆角矩形是搭建界面所需的重要基础要素。无论是凸显面板上重要的区域(如下图所示):
image.png
(图片来自我自己iPad的截图)

还是作为图表的背景(如下图所示):
image.png
(图片资源来自网络:侵删)

很多情况下,我们都可以从目标控件中拆解出圆角矩形来:
image.png
(图片资源来自网络:侵删)

聪明的小伙伴很快会发现,其实借助贴图技术,只要我们事先规定好背景的颜色(比如黑色),再准备好对应控件的图片,那么无论你是圆角矩形还是普通四边形,其实都没有区别——直接进行像素级的拷贝即可——对应到Arm-2D的API就是 arm_2d_rgb565_tile_copy()。此前的文章《【Arm-2D】不整活儿玩啥GUI?》对具体的API和使用细节已经介绍的非常详细,这里就不再赘述了。

然而,上述方法的弊端也已经写的非常清楚,即:圆角矩形范围以外的部分不应该覆盖背景。这么说也许有些抽象,我们不妨以一个简单的例子加以说明。

已知一个白底的圆角矩形:
image.png

它其实保存在一个四方四正的像素数组里(红色边框是我加的,用来让指示范围更加清晰):
image.png
当目标背景的颜色也是白色时,复制该贴图,并无异样。就像你们现在看到的那样(假设您阅读本文时使用的是白色背景)。但假设背景是一个不同于白色的其它颜色,甚至是一个墙纸时:
image.png

使用直接贴图的方式,就会显露出它的弊端:
image.png
因为我们期望的效果是这样的:
image.png
简单说,就是我们希望圆角矩形的贴图中,原本背景的白色不是真正的白色,而是所谓的“透明色”:
image.png
也就是这里,红色边界范围内的“白色”应该是一种类似透明玻璃的颜色——目标背景是什么像素内容,这里就能“透过去”。

然而,“透明色”是一种不存在的颜色。在传统的RGB体系中,无论你是16bit的全彩色还是24bit的真彩色,都没有“透明色”的生存空间——每一个bit都被用来编码颜色还嫌不够呢。实际上“透明”不仅仅只有“全透明”这一种情况,还有以百分比计的透明度的概念——比如以下是25%、50%、75%透明的效果:
image.png

所以,透明度(Transparency)实际上是一类与颜色独立的信息,在传统的RGB体系中,往往会占用独立的通道来保存,如下图所示:
image.png
(图片来自维基百科:https://en.wikipedia.org/wiki/RGBA\_color\_model

一个经典的 32位 ARGB32 颜色,实际上除了R、G、B三个颜色通道分别占用了一个字节外,还利用最高字节保存了一个所谓的Alpha通道——这个Alpha就是透明度相关的信息,只不过这里保存的不是透明度(Transparency),而是不透明度(Opacity):

  • Transparency:0表示完全不透明,255表示完全透明;
  • Opacity:0表示完全透明,255表示完全不透明。

此外,Alpha也并不固定的保存在最高字节,在不同的尾端下,Alpha通道也可能保存在最低字节——此时同样是32bit的RGB颜色体系,就被称为RGBA32:
image.png

(图片来自维基百科:https://en.wikipedia.org/wiki/RGBA\_color\_model

搞清楚了上述原理后,我们实际上很容易发现,对实现各类圆角矩形来说,具体像素里的RGB部分其实我们并不关心——真正有用的是Alpha通道。一般来说,PNG格式是携带Alpha通道信息的常见格式。大量界面设计的素材也是以PNG来保存的。

那么Arm-2D是如何应对PNG类的图片素材的呢?

【神奇的Alpha-Mask】

前面我们提到,对常见的携带了Alpha通道的颜色格式来说,无论是ARGB32还是RGBA32,Alpha信息都是与RGB颜色信息放在一起的。如果我们的屏幕所使用的像素是32bit的还好说,但对成本极其敏感的嵌入式系统中广泛使用的LCD屏幕很多都是 使用RGB565(16bit)颜色格式的,这意味着:

  • 每个像素是16bit,其中R、G、B分别对应5、6、5个二进制位
  • ARGB32中,RGB888占用24个bit,相对RGB565来说,每个像素多占用了8个bit。
  • 在同时需要保留Alpha信息的情况下,RGB565比RGB888节省了25%的空间。

问题来了:PNG格式的文件在解码后往往以ARGB32形式保存,我们如何将其连同Alpha信息一起应用到一个RGB565的屏幕上呢

为了解决这一问题,Arm-2D的思路非常直接:

  • 将Alpha信息从像素中提取出来,单独作为一个数组保存;
  • 在设计阶段,将RGB888转化为RGB565形式保存,从而节省资源的存储空间,也避免了在运行时刻进行颜色格式转换所造成的不必要的性能损失

这里,由单独提取出来的Alpha信息所构成的位图,我们称之为透明度蒙版(Alpha-Mask),很多时候简称蒙版(Mask)。

为了简化用户的设计工序,Arm-2D在仓库的tools文件夹下提供了一个专门的python脚本,用于帮助用户直接将给定的图片文件转化为Arm-2D可以直接使用的tile数据结构。
image.png

这是一个命令行工具,需要python3.x 版本,并安装以下的依赖:

pip install Pillow
pip install numpy

具体使用方式如下:

python img2c.py <命令行选项>

image.png

比如,在 examples/benchmark/asset 目录下有一个png图片 CMSIS_Logo_Final.png,我们可以借助命令行将其转化为 tile 数据结构:

python img2c.py -i ..\examples\benchmark\asset\CMSIS_Logo_Final.png --name CMSISLogo

运行成功后,由于我们没有指定输出的路径,因此直接在tools所在目录下生成了一个与图片文件同名(扩展名不同)的c文件 CMSIS_Logo_Final.c
image.png
打开文件,我们可以看到img2c.py按照默认的颜色格式 RGB565自动生成了对应的像素数组 c_bmpCMSISLogoarm-2d的API可以直接使用的arm_2d_tile_t对象c_tileCMSISLogo

static const uint16_t c_bmpCMSISLogo[163*65] = {
    ...
};

extern const arm_2d_tile_t c_tileCMSISLogo;
const arm_2d_tile_t c_tileCMSISLogo = {
    .tRegion = {
        .tSize = {
            .iWidth = 163,
            .iHeight = 65,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_RGB565,
        },
    },
    .phwBuffer = (uint16_t*)c_bmpCMSISLogo,
};

关于 arm_2d_tile_t 数据结构的详细内容,大家可以参考文章《【Arm-2D】不整活儿玩啥GUI?》,这里大家“望文生义”即可,容易发现c_tileCMSISLogo:

  • 描述了图片的尺寸信息:163 * 65
  • 描述了图片的颜色信息:ARM_2D_COLOUR_RGB565
  • 由于直接提供了像素数组的地址,因此这是一个Root tile。

既然拿到了Tile,我们不妨赶快试它一试:

1、将生成的CMSIS_Logo_Final.c加入MDK工程参与编译

2、在example_gui.c中加入对应的引用声明:

extern const arm_2d_tile_t c_tileCMSISLogo;

3、在修改 example_gui.c 中的界面绘制函数 example_gui_refresh():

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_fill_colour(ptFrameBuffer, NULL, GLCD_COLOR_WHITE);

    arm_2d_rgb16_tile_copy( &c_tileCMSISLogo,
                            ptFrameBuffer,
                            NULL,
                            ARM_2D_CP_MODE_COPY);
}

这里,我们首先通过 arm_2d_rgb16_fill_colour() 向整个屏幕填充了白色(GLCD_COLOUR_WHITE);紧接着以 arm_2d_rgb16_tile_copy()将我们的额 c_tileCMSISLogo拷贝到了屏幕左上角。运行结果如下:
image.png
怎么说呢……运行结果正常,却并不能让我们满意——由于缺乏透明度信息,原本应该是完全透明的部分,由于对应像素值为0x0000正好对应了RGB565下的黑色,因此呈现出一个黑色的背景色——这当然不是我们所需要的。

重新打开此前生成的 CMSIS_Logo_Final.c,我们注意到,其实Alpha信息已经被单独提取出来、独立保存并生成了专门的arm_2d_tile_t对象c_tileCMSISLogoMask

static const uint8_t c_bmpCMSISLogoAlpha[163*65] = {
    ...
};

extern const arm_2d_tile_t c_tileCMSISLogoMask;
const arm_2d_tile_t c_tileCMSISLogoMask = {
    .tRegion = {
        .tSize = {
            .iWidth = 163,
            .iHeight = 65,
        },
    },
    .tInfo = {
        .bIsRoot = true,
        .bHasEnforcedColour = true,
        .tColourInfo = {
            .chScheme = ARM_2D_COLOUR_8BIT,
        },
    },
    .pchBuffer = (uint8_t *)c_bmpCMSISLogoAlpha,
};

通过观察,容易发现:

  • c_tileCMSISLogoMask拥有与c_tileCMSISLogo相同的像素尺寸,都是163*65;
  • c_tileCMSISLogoMask的颜色是 8bit的 ARM_2D_COLOUR_8BIT
  • 它也是一个root tile。

借助专门的函数 arm_2d_rgb565_tile_copy_with_src_mask(),我们就可以轻松达成所需的效果。具体操作如下:

1、在example_gui.c中加入对应的引用声明:

extern const arm_2d_tile_t c_tileCMSISLogoMask;

2、在修改 example_gui.c 中的界面绘制函数 example_gui_refresh():

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_fill_colour(ptFrameBuffer, NULL, GLCD_COLOR_WHITE);
     
    arm_2d_rgb565_tile_copy_with_src_mask(  &c_tileCMSISLogo,
                                            &c_tileCMSISLogoMask, 
                                            ptFrameBuffer,
                                            NULL,
                                            ARM_2D_CP_MODE_COPY);
}

编译后运行效果如下:
image.png
如果你还记得本文开篇时的那个辅助函数 arm_2d_align_centre()的话,我们还可以借助它实现Logo居中的效果:

1、确保目标 c 文件中增加了 arm_2d_helper.h 的包含:

#include "arm_2d_helper.h"

2、修改 example_gui_refresh() 函数,加入 arm_2d_align_entre():

void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_fill_colour(ptFrameBuffer, NULL, GLCD_COLOR_WHITE);

    arm_2d_align_centre(*ptFrameBuffer, c_tileCMSISLogo.tRegion.tSize) {
        arm_2d_rgb565_tile_copy_with_src_mask(  &c_tileCMSISLogo,
                                                &c_tileCMSISLogoMask, 
                                                ptFrameBuffer,
                                                &__centre_region,
                                                ARM_2D_CP_MODE_COPY);
    }

    arm_2d_op_wait_async(NULL);
}

运行效果如下:
image.png

关于 arm_2d_align_centre() 值得说明的有两点:

  • arm_2d_align_centre() 需要用户传递两类信息
  • 目标缓冲区的实例(注意是实例,不是地址)。在本例中,显示缓冲区由指针 ptFrameBuffer 保存,因此,传递给 arm_2d_align_centre() 时需要加入"*" 运算,即"(*ptFrameBuffer)"
  • 目标区域的大小信息。在本例中,因为我们希望将CMSIS Logo居中,因此目标区域的大小信息就保存在 c_tileCMSISLogo 中。具体写为 c_tileCMSISLogo.tRegion.tSize
  • 最终结果就是代码中所呈现的那样:

    arm_2d_align_centre(*ptFrameBuffer, 
                     c_tileCMSISLogo.tRegion.tSize) {
    ...
    }
  • arm_2d_align_centre() 会产生一个局部变量 __centre_region,它的生命周期仅限于 arm_2d_align_centre() 的花括号内,因此,当我们使用 _centre_region进行绘图时,要将其生命周期考虑在内——必须在其生命结束前加入 arm_2d_op_wait_async() 以确保API执行的有效性:
void example_gui_refresh(const arm_2d_tile_t *ptFrameBuffer, bool bIsNewFrame)
{
    arm_2d_rgb16_fill_colour(ptFrameBuffer, NULL, GLCD_COLOR_WHITE);

    arm_2d_align_centre(*ptFrameBuffer, c_tileCMSISLogo.tRegion.tSize) {
        arm_2d_rgb565_tile_copy_with_src_mask(  &c_tileCMSISLogo,
                                                &c_tileCMSISLogoMask, 
                                                ptFrameBuffer,
                                                &__centre_region,
                                                ARM_2D_CP_MODE_COPY);
        arm_2d_op_wait_async(NULL);
    }
}

【说在后面的话】

回顾这篇文章,我们从“圆角矩形直接贴图那差强人意的问题”引入了充当“透明色”效果的“透明信息(Alpha)”的概念;介绍了arm-2d中所使用的透明蒙版(Alpha-Mask),并介绍了Arm-2D对PNG图片的支持方式。

然而这只是个开始。

透明蒙版的应用远比你想想的要广泛,其通用性和灵活性远远超越了RGBA32这类支持Alpha信息的图片格式。基于篇幅的限制,我们今天就暂时先讲到这里。在随后的文章中,我们将为您详细介绍透明蒙版所带来的的“无限可能”——为你展示其在现代界面设计中所占有的举足轻重的作用。

首发:裸机思维
作者: GorgonMeducer 傻孩子

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。
推荐阅读
关注数
1480
内容数
118
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息