【说在前面的话】
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】入门和移植从未如此简单》完成了Arm-2D到本地硬件的部署,通过《【玩转Arm-2D】Arm-2D应用开发入门》学习了使用Arm-2D直接开发应用的基本方式后,我将以范例的形式,一小步一小步的带领大家实现“从入门到精通”的跨越。虽然每篇文章都不会追求内容的广度,但一定力求在一类具体的问题上为大家讲懂、讲透。
【前提和约定】
在开始今天的内容之前,为了让大家能站在一个较为统一的视角上看待问题,我觉得很有必要在出现因为立场不同而产生不必要的误解和争论之前,做出一些必要的约定:
为了便于讲解,本文将主要使用 example 目录下的 [template][cmsis-rtos2][pfb] 模板作为起点。
截图来自FastModel
或者 AN547-Cortex-M55_r0(使用免费的Corstone-300-FVP)
详细配置方法,请参考文章《懒人玩Arm-2D究竟有几种姿势》
【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,说来话长,暂时就当做格式来记忆吧。
举个例子:
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
user_scene_0_t *ptThis = (user_scene_0_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_align_centre(__top_canvas, c_tileCMSISLogoRGB565.tRegion.tSize) {
/* draw the cmsis logo in the centre of the screen */
arm_2d_rgb565_tile_copy_with_src_mask_only(
&c_tileCMSISLogoRGB565,
&c_tileCMSISLogoMask,
ptTile,
&__centre_region);
arm_2d_op_wait_async(NULL);
}
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
上述例子中,arm_2d_align_centre()的作用是根据用户给定的画布或者区域(比如这里的__top_canvas)和区域大小(比如c_tileCMSISLogo.tRegion.tSize)生成一个能将给定大小的区域居中的arm_2d_region_t对象——局部变量 __centre_region——它的生命周期被限制在arm_2d_align_centre()后的花括号内。
姑且不论 arm_2d_rgb565_tile_copy_with_src_mask_only() 实际用 __centre_region 做了什么,但考虑到它的生命周期有限,因此必须在退出花括号之前调用 arm_2d_op_wait_async(NULL) 来等待操作完成。
虽然裸机环境下,所有arm-2d的API都是以同步模式来工作的(即退出API就意味着任务完成或者出现了错误)——理论上的确完全无需arm_2d_op_wait_async(NULL)调用来实现所谓的同步,但以“异步工作模式”使用API写出来的代码拥有最高的兼容性——可以同时在RTOS环境和裸机环境下使用,因此,本系列文章统一以异步模式为蓝本来讲解后续的内容。
【背景和命题】
随便打开一部手机或是平板电脑,我们就可以很容易的看到:在现代的界面设计中,圆角矩形是搭建界面所需的重要基础要素。无论是凸显面板上重要的区域(如下图所示):
(图片来自我自己iPad的截图)
还是作为图表的背景(如下图所示):
(图片资源来自网络:侵删)
很多情况下,我们都可以从目标控件中拆解出圆角矩形来:
(图片资源来自网络:侵删)
聪明的小伙伴很快会发现,其实借助贴图技术,只要我们事先规定好背景的颜色(比如黑色),再准备好对应控件的图片,那么无论你是圆角矩形还是普通四边形,其实都没有区别——直接进行像素级的拷贝即可——对应到Arm-2D的API就是 arm_2d_rgb16_tile_copy_only()。
然而,上述方法的弊端也已经写的非常清楚,即:圆角矩形范围以外的部分不应该覆盖背景。这么说也许有些抽象,我们不妨以一个简单的例子加以说明。
已知一个白底的圆角矩形:
它其实保存在一个四方四正的像素数组里(红色边框是我加的,用来让指示范围更加清晰):
当目标背景的颜色也是白色时,复制该贴图,并无异样。就像你们现在看到的那样(假设您阅读本文时使用的是白色背景)。但假设背景是一个不同于白色的其它颜色,甚至是一个墙纸时:
使用直接贴图的方式,就会显露出它的弊端:
因为我们期望的效果是这样的:
简单说,就是我们希望圆角矩形的贴图中,原本背景的白色不是真正的白色,而是所谓的“透明色”:
也就是这里,红色边界范围内的“白色”应该是一种类似透明玻璃的颜色——目标背景是什么像素内容,这里就能“透过去”。
然而,“透明色”是一种不存在的颜色。在传统的RGB体系中,无论你是16bit的全彩色还是24bit的真彩色,都没有“透明色”的生存空间——每一位都被用来编码颜色还嫌不够呢。实际上“透明”不仅仅只有“全透明”这一种情况,还有以百分比计的透明度的概念——比如以下是25%、50%、75%透明的效果:
所以,透明度(Transparency)实际上是一类与颜色独立的信息,在传统的RGB体系中,往往会占用独立的通道来保存,如下图所示:
(图片来自维基百科:https://en.wikipedia.org/wiki/RGBA_color_model)
一个经典的 32位 ARGB32 颜色,实际上除了R、G、B三个颜色通道分别占用了一个字节外,还利用最高字节保存了一个所谓的Alpha通道——这个Alpha就是透明度相关的信息,只不过这里保存的不是透明度(Transparency),而是不透明度(Opacity):
此外,Alpha也并不固定的保存在最高字节,在不同的尾端下,Alpha通道也可能保存在最低字节——此时同样是32bit的RGB颜色体系,就被称为RGBA32:
(图片来自维基百科:https://en.wikipedia.org/wiki/RGBA_color_model)
搞清楚了上述原理后,我们实际上很容易发现,对实现各类圆角矩形来说,具体像素里的RGB部分其实我们并不关心——真正有用的是Alpha通道。一般来说,PNG格式是携带Alpha通道信息的常见格式。大量界面设计的素材也是以PNG来保存的。
那么Arm-2D是如何应对PNG类的图片素材的呢?
【神奇的Alpha-Mask】
问题来了:PNG格式的文件在解码后往往以ARGB32形式保存,我们如何将其连同Alpha信息一起应用到一个RGB565的屏幕上呢?
为了解决这一问题,Arm-2D的思路非常直接:
这里,由单独提取出来的Alpha信息所构成的位图,我们称之为透明度蒙版(Alpha-Mask),很多时候简称蒙版(Mask)。
为了简化用户的设计工序,Arm-2D在仓库的tools文件夹下提供了一个专门的python脚本,用于帮助用户直接将给定的图片文件转化为Arm-2D可以直接使用的tile数据结构。
这是一个命令行工具,需要python3.x 版本,并安装以下的依赖:
pip install Pillow
pip install numpy
具体使用方式如下:
python img2c.py <命令行选项>
选项 | 描述 | |
---|---|---|
-h, --help | 显示命令行使用方法 | |
-i <路径> | 输入图片文件的路径(png, bmp, jpeg...) | |
-o <路径> | 输出c源文件的路径 | 可选 |
-name <名称> | 数组的名字 | |
-format <格式> | 输出的颜色格式:rgb32, rgb565, gray8, all(默认) | 可选 |
-dim <宽度>,<高度> | 在输出前修改图片的尺寸 | 可选 |
-rot <角度> | 在输出前将图片旋转指定的角度 | 可选 |
比如,在 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:
打开文件,我们可以看到img2c.py按照默认的颜色格式 RGB565 自动生成了对应的像素数组 c_bmpCMSISLogo 和arm-2d的API可以直接使用的arm_2d_tile_t对象c_tileCMSISLogo。
static const uint16_t c_bmpCMSISLogo[163*65] = {
...
};
extern const arm_2d_tile_t c_tileCMSISLogoRGB565;
const arm_2d_tile_t c_tileCMSISLogoRGB565 = {
.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 数据结构的详细内容,本身并不复杂,这里大家“望文生义”即可:容易发现c_tileCMSISLogoRGB565:
既然拿到了Tile,我们不妨赶快试它一试:
1、将生成的CMSIS_Logo_Final.c加入MDK工程参与编译(其实,部署Arm-2D后,其实一个包含相同内容的文件 cmsis_logo.c 已经加入了编译)
2、在对应的场景源代码中加入引用声明:
extern const arm_2d_tile_t c_tileCMSISLogoRGB565;
3、在修改场景绘制函数制函数 __pfb_draw_scene0_handler():
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
user_scene_0_t *ptThis = (user_scene_0_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_rgb16_tile_copy_only( &c_tileCMSISLogoRGB565,
ptTile,
&__top_canvas);
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
这里,我们首先通过 arm_2d_rgb16_fill_colour() 向整个屏幕填充了白色(GLCD_COLOUR_WHITE);紧接着以 arm_2d_rgb16_tile_copy()将我们的额 c_tileCMSISLogoRGB565 拷贝到了屏幕左上角。运行结果如下:
怎么说呢……运行结果正常,却并不能让我们满意——由于缺乏透明度信息,原本应该是完全透明的部分,由于对应像素值为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,
};
通过观察,容易发现:
借助专门的函数 arm_2d_rgb565_tile_copy_with_src_mask(),我们就可以轻松达成所需的效果。具体操作如下:
1、在对应的场景源代码中加入对应的引用声明:
extern const arm_2d_tile_t c_tileCMSISLogoMask;
2、修改场景绘制函数制函数 __pfb_draw_scene0_handler():
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
user_scene_0_t *ptThis = (user_scene_0_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_rgb565_tile_copy_with_src_mask_only(
&c_tileCMSISLogoRGB565,
&c_tileCMSISLogoMask,
ptTile,
&__top_canvas);
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
编译后运行效果如下:
如果你还记得本文开篇时的那个辅助函数 arm_2d_align_centre()的话,我们还可以借助它实现Logo居中的效果:
1、确保目标 c 文件中增加了 arm_2d_helper.h 的包含:
#include "arm_2d_helper.h"
2、修改场景绘制函数制函数 __pfb_draw_scene0_handler(),加入 arm_2d_align_entre():
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
user_scene_0_t *ptThis = (user_scene_0_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_align_centre(__top_canvas, c_tileCMSISLogoRGB565.tRegion.tSize) {
/* draw the cmsis logo in the centre of the screen */
arm_2d_rgb565_tile_copy_with_src_mask_only(
&c_tileCMSISLogoRGB565,
&c_tileCMSISLogoMask,
ptTile,
&__centre_region);
arm_2d_op_wait_async(NULL);
}
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
运行效果如下:
关于 arm_2d_align_centre() 值得说明的有两点:
arm_2d_canvas(ptTile, __top_canvas) {
arm_2d_align_centre(__top_canvas, c_tileCMSISLogoRGB565.tRegion.tSize) {
...
}
}
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
user_scene_0_t *ptThis = (user_scene_0_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_align_centre(__top_canvas, c_tileCMSISLogoRGB565.tRegion.tSize) {
/* draw the cmsis logo in the centre of the screen */
arm_2d_rgb565_tile_copy_with_src_mask_only(
&c_tileCMSISLogoRGB565,
&c_tileCMSISLogoMask,
ptTile,
&__centre_region);
arm_2d_op_wait_async(NULL);
}
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
【说在后面的话
回顾这篇文章,我们从“圆角矩形直接贴图那差强人意的问题”引入了充当“透明色”效果的“透明信息(Alpha)”的概念;介绍了arm-2d中所使用的透明蒙版(Alpha-Mask),并介绍了Arm-2D对PNG图片的支持方式。
然而这只是个开始。透明蒙版的应用远比你想想的要广泛,其通用性和灵活性远远超越了RGBA32这类支持Alpha信息的图片格式。基于篇幅的限制,我们今天就暂时先讲到这里。在随后的文章中,我们将为您详细介绍透明蒙版所带来的的“无限可能”——为你展示其在现代界面设计中所占有的举足轻重的作用。
另外,你也许注意到了,本文在例子中出现了一些新的概念,比如:“画布(Canvas)”、“区域(Region)”,并涉及到了一些诸如“居中对齐”一类基本的图形布局手段(Layout)。第一次接触他们可能会让你手足无措,甚至开始好奇这些看起来完全不像C语言函数的宏究竟是如何实现的,关于这一点,我有重要的话要说: