之前,我们说过Arm-2D虽然本意是在底层默默的为各类商用和开源GUI软件协议栈提供加速服务,但考虑到在资源受限的深度嵌入式系统环境下,仍然有一大批贫下中农不辞辛劳的在 32~64K Flash、4~32K SRAM的单片机里“螺蛳壳里做道场”——“妄图染指”一般只有高端处理器才能触碰的“华丽”图形界面,Arm-2D也为这些享受不起哪怕是起码LVGL恩惠的资源难民,提供了一系列享受浪漫的机会。
既然是“嵌入式工程师的浪漫”,哪有不整活儿的呢?
今天我们就在上一篇文章移植好的平台上为大家“正经”的介绍一下“某些”常用API的使用和技巧。
【从“正事儿”开始】
说起Arm-2D的正事儿,那自然是贴图(Tile)了。其实在Arm-2D眼中,一切似乎都是贴图:
- 保存在ROM里的图片是贴图;
- RAM里的作图缓存(Frame buffer)也是贴图;
- 甚至还可以从一个已有的贴图里派生出无数的子贴图(Child-Tile)。
为此,Arm-2D专门引入了一个数据结构 arm_2d_tile_t,它定义在 arm_2d_tile_types.h 中,具体内容如下:
代码语言:javascript复制typedef struct arm_2d_tile_t arm_2d_tile_t;
struct arm_2d_tile_t {
implement_ex(struct {
uint8_t bIsRoot : 1;
uint8_t bHasEnforcedColour : 1;
uint8_t : 6;
uint8_t : 8;
uint8_t : 8;
arm_2d_color_info_t tColourInfo;
}, tInfo);
implement_ex(arm_2d_region_t, tRegion);
union {
/*! when bIsRoot is true, phwBuffer is available,
*! otherwise ptParent is available
*/
arm_2d_tile_t *ptParent;
uint16_t *phwBuffer;
uint32_t *pwBuffer;
uint8_t *pchBuffer;
};
};
如果看着比较晕,不要紧,其实它就只有三大部分而已:
- 贴图的各类属性描述信息:tInfo
- 贴图的尺寸和位置信息: tRegion
- 贴图的指针或引用
为了方便大家理解,我们不妨举个例子。
假设我们想在屏幕上显示一个让人“印象深刻”的图片,比如这个:
由于傻孩子小学六年级暑假花300RMB学的PS已经忘得差不多了,所以只能祭出“简历上人均精通”的工具——PowerPoint——在空白页面上插入以上图片,截取“灵魂”,并缩放到普通LCD看起来尺寸适中的大小:
使用工具将其转化为C语言数组:
代码语言:javascript复制#include <stdint.h>
//! 没有这个,有些编译器会产生 warning,神烦!
__attribute__((aligned(2)))
extern const uint8_t c_bmpDodgy[];
__attribute__((aligned(2)))
const uint8_t c_bmpDoge[] = {
...
};
由于我的屏幕使用的是RGB565的颜色格式,因此保存在数组 c_bmpDoge[]里的像素是16位的,需要2字节对齐;同理,如果你使用的颜色格式是32位的,则需要使用__attribute__((aligned(4)))来确保一个不低于4字节的对齐。
关于转化工具,我推荐LVGL的在线工具:https://lvgl.io/tools/imageconverter 非常好用。
接下来,我们就可以使用 arm_2d_tile_t 来描述这一图片资源:
代码语言:javascript复制extern const uint8_t c_bmpDoge[];
const arm_2d_tile_t c_tileDoge = {
.tRegion = {
.tSize = {
.iWidth = 112, //!< 素材的宽度
.iHeight = 108 //!< 素材d额高度
},
},
.tInfo.bIsRoot = true, //!< 说明这个贴图拥有资源
.phwBuffer = (uint16_t *)c_bmpDoge, //!< 指向资源的指针
};
这里,我们使用了C99标准引入的结构体初始化方式(可惜这一方式C 并不认可),并分别提供了以下信息:
- 素材的长宽尺寸;
- 当前的贴图是一个“根”贴图(bIsRoot = true),意思是说,这个贴图真正拥有自己的像素数组;与之相对,有些贴图是从别的贴图那里派生出来的,因此并不拥有自己的像素数组;
- 由于当前贴图是一个根贴图,因此“指针引用区”的phwBuffer被赋值——指向了我们此前建立的常量数组 c_bmpDoge[]。
值得说明的是,由于c_bmpDoge[]和新建立的贴图对象 c_tileDoge 都使用了 const 进行修饰,因而在编译时都算作 RO数据(Read Only Data),一般来说会被放置在Flash中,因而并不占用宝贵的RAM资源,当然也不可以在运行时刻对其内容进行修改——否则会立即导致 Busfault(或者Hardfault)。
对大部分Arm-2D的API来说,其操作的最基本单位是贴图(tile),现在我们的手中已经有了一个专门的贴图对象 c_tileDoge,剩下的事情就变得非常简单了。
比如,我们可以借助 arm_2d_rgb16_tile_copy() 函数将其拷贝到显示缓冲区中(显示缓冲区自然也是用 arm_2d_tile_t 数据结构来表示的):
代码语言:javascript复制#include "arm_2d.h"
#include "arm_2d_helper.h"
...
static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
ARM_2D_UNUSED(pTarget);
arm_2d_rgb16_fill_colour(
ptTile, //!< 目标缓冲区
NULL, //!< 填充目标缓冲区的哪个区域
GLCD_COLOR_WHITE); //!< 白色
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
NULL, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_COPY); //!< 就是单纯的拷贝,不做作
}
...
static ARM_NOINIT arm_2d_helper_pfb_t s_tExamplePFB;
...
void main(void)
{
lcd_init();
arm_2d_init();
//! initialise FPB helper
if (ARM_2D_HELPER_PFB_INIT(
&s_tExamplePFB, //!< FPB Helper object
...
{
...
.evtOnDrawing = {
//! callback for drawing GUI
.fnHandler = &__pfb_draw_background_handler,
},
}
) < 0) {
//! error detected
assert(false);
}
//! call partial framebuffer helper service to draw background
while(arm_fsm_rt_cpl != arm_2d_helper_pfb_task(&s_tExamplePFB,NULL));
}
这里 __pfb_draw_background_handler() 就是我们的界面绘制函数,其中我们做了以下操作:
- 通过 arm_2d_rgb16_fill_colour() 在整个目标缓冲区内填充白色;
- 通过 arm_2d_rgb16_tile_copy() 将贴图拷贝到目标缓冲区中。
效果如下:
值得说明的是,两个函数都在涉及“目标缓冲区中具体哪个位置”的地方给了NULL,这个意思就是:“我也不指定目标缓冲区具体哪里了,你就默认为是目标缓冲区的整个区域好了”。
看起来效果不错——这小眼神怎么看都是那么的有“神韵”——然而,为了把节目效果拉满,我们完全可以这样整活儿:
怎么样,是不是血压一下就上来了?这里我们只是简单的使用了贴图填充功能(也就是大家熟悉的纹理填充),代码如下:
代码语言:javascript复制static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
ARM_2D_UNUSED(pTarget);
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
NULL, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_FILL); //!< 就是单纯的拷贝,不做作
}
容易注意到两点:
- 既然已经是全屏幕无缝填充了,自然不需要背景色了;
- 将 ARM_2D_CP_MODE_COPY 改为 ARM_2D_CP_MODE_FILL 就可以实现填充功能——非常简单。
考虑到,如果你真的把这样的效果作为界面背景呈现给你的用户,我估计屏幕的损坏率可能会出奇的高:
为了安抚一下观众们的情绪,我们不妨用一个万能的背景美化效果——加半透明蒙版:
代码语言:javascript复制static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
ARM_2D_UNUSED(pTarget);
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
NULL, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_FILL); //!< 就是单纯的拷贝,不做作
arm_2d_rgb565_fill_colour_with_alpha(
ptTile, //!< 目标缓冲区
NULL, //!< 填充目标缓冲区的哪个区域
//!< 特别指明是 rgb565 的白色
(arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
200); //!< 不透明度 (200/255) * 100%
}
实际效果如下:
是不是突然就觉得好像还挺不错的?
认真说起来,其实同样的方法也可以用来给界面打公司水印——即用公司logo填充背景,然后加上白色蒙版——完美。
【“整活儿要有技术含量”】
前面的整活儿贴图,虽然效果不错,但总觉得过于缺乏技术含量,比如最简单的问题,如果我想做个“水边的神烦狗”,应该如何实现呢?
说到水边,其关键就在于一种意境,具体说就是一种若有似无的倒影。要做到这一点其实并不难。首先,我们要将狗头居中,并留下倒影的位置:
代码如下:
代码语言:javascript复制static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
arm_2d_rgb16_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
arm_2d_region_t tDogRegion = {
.tLocation = {
.iX = (APP_SCREEN_WIDTH - c_tileDoge.tRegion.tSize.iWidth) >> 1,
.iY = (APP_SCREEN_HEIGHT - c_tileDoge.tRegion.tSize.iHeight * 2) >> 1,
},
.tSize = c_tileDoge.tRegion.tSize,
};
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_COPY);
}
与此前不同的是,这次我们通过必要的计算,1)将狗头居中;2)并预留出倒影的位置。计算的结果保存在 arm_2d_region_t 类型的结构 tDogRegion中。很容易注意到,类型 arm_2d_region_t 由尺寸和位置两部分信息组成,它们的定义如下:
代码语言:javascript复制typedef struct arm_2d_location_t {
int16_t iX;
int16_t iY;
} arm_2d_location_t;
typedef struct arm_2d_point_float_t {
float fX;
float fY;
} arm_2d_point_float_t;
typedef struct arm_2d_size_t {
int16_t iWidth;
int16_t iHeight;
} arm_2d_size_t;
typedef struct arm_2d_region_t {
implement_ex(arm_2d_location_t, tLocation);
implement_ex(arm_2d_size_t, tSize);
} arm_2d_region_t;
这里OOPC辅助宏 implement_ex() 的意思是:arm_2d_region_t 继承了 arm_2d_location_t 和 arm_2d_size_t,并分配给与它们对应的名字 tLocation和tSize。
接下来是重头戏,翻转狗头——这次必须要请出 arm_2d_rgb16_tile_copy()的特殊模式:Y轴镜像(ARM_2D_CP_MODE_Y_MIRROR)——加入代码如下:
代码语言:javascript复制static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
//! 填充白色背景
arm_2d_rgb16_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
//! 计算狗头的位置(居中,并预留倒影)
arm_2d_region_t tDogRegion = {
.tLocation = {
.iX = (APP_SCREEN_WIDTH - c_tileDoge.tRegion.tSize.iWidth) >> 1,
.iY = (APP_SCREEN_HEIGHT - c_tileDoge.tRegion.tSize.iHeight * 2) >> 1,
},
.tSize = c_tileDoge.tRegion.tSize,
};
//! 画狗头
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_COPY);
//! 更新要拷贝狗头的位置
tDogRegion.tLocation.iY = c_tileDoge.tRegion.tSize.iHeight;
//! 以镜像的方式拷贝狗头
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到倒影的位置
ARM_2D_CP_MODE_Y_MIRROR);
}
嗯!嗯!
我知道!
是的……是的……
我知道很魔性……
“我们受过专业的训练,无论到好笑,我们都不会笑……除非忍不住……”
接下来,我们继续严肃认真地解决“倒影不够有意境的问题”——加上半透明蒙版:
代码语言:javascript复制static arm_fsm_rt_t __pfb_draw_background_handler( void *pTarget,
const arm_2d_tile_t *ptTile)
{
//! 填充白色背景
arm_2d_rgb16_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
//! 计算狗头的位置(居中,并预留倒影)
arm_2d_region_t tDogRegion = {
.tLocation = {
.iX = (APP_SCREEN_WIDTH - c_tileDoge.tRegion.tSize.iWidth) >> 1,
.iY = (APP_SCREEN_HEIGHT - c_tileDoge.tRegion.tSize.iHeight * 2) >> 1,
},
.tSize = c_tileDoge.tRegion.tSize,
};
//! 画狗头
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到目标缓冲区的那个区域
ARM_2D_CP_MODE_COPY);
//! 更新要拷贝狗头的位置
tDogRegion.tLocation.iY = c_tileDoge.tRegion.tSize.iHeight;
//! 以镜像的方式拷贝狗头
arm_2d_rgb16_tile_copy(
&c_tileDoge, //!< 我们的素材
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到倒影的位置
ARM_2D_CP_MODE_Y_MIRROR);
//! 给倒影加半透明蒙版
arm_2d_rgb565_fill_colour_with_alpha(
ptTile, //!< 目标缓冲区
&tDogRegion, //!< 拷贝到倒影的位置
//! 特别指明是 rgb565 的白色
(arm_2d_color_rgb565_t){GLCD_COLOR_WHITE},
200); //!< 不透明度 (200/255) * 100%
}
最终效果如下:
【It's Only Begin!】
这一篇,我们通过“怒刷狗头”的方式介绍了Arm-2D一些常用API的使用,并给出了这些方法在日常应用中如何“狗头保命”的建议——比如如何实现水影背景。
然而,整活儿的步伐怎么能说停就停呢?下一篇,我们将详细介绍一种实现“一狗映月进度条”的方式:
感兴趣的小伙伴、还没搭建平台的小伙伴,赶快动起手来,一起来Arm-2D追更吧!