【Arm-2D】不整活儿玩啥GUI?
【书接上回】
上回我们说到:Arm-2D是小资源单片机的GUI人权卡、按部就班的介绍了如何在MDK工程中部署Arm-2D库,并进一步介绍了如何使用PFB Helper在“帧率和RAM占用间”实现“无级变速”。今天我们就来继续说说嵌入式工程师的浪漫之二——“裸机徒手撸GUI”。
之前,我们说过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 中,具体内容如下:
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语言数组:
//! 没有这个,有些编译器会产生 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字节的对齐。
接下来,我们就可以使用 arm_2d_tile_t 来描述这一图片资源:
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 数据结构来表示的):
...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,这个意思就是:“我也不指定目标缓冲区具体哪里了,你就默认为是目标缓冲区的整个区域好了”。
看起来效果不错——这小眼神怎么看都是那么的有“神韵”——然而,为了把节目效果拉满,我们完全可以这样整活儿:

怎么样,是不是血压一下就上来了?这里我们只是简单的使用了贴图填充功能(也就是大家熟悉的纹理填充),代码如下:
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 就可以实现填充功能——非常简单。
为了安抚一下观众们的情绪,我们不妨用一个万能的背景美化效果——加半透明蒙版:
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填充背景,然后加上白色蒙版——完美。
【“整活儿要有技术含量”】
前面的整活儿贴图,虽然效果不错,但总觉得过于缺乏技术含量,比如最简单的问题,如果我想做个“水边的神烦狗”,应该如何实现呢?
说到水边,其关键就在于一种意境,具体说就是一种若有似无的倒影。要做到这一点其实并不难。首先,我们要将狗头居中,并留下倒影的位置:

代码如下:
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 由尺寸和位置两部分信息组成,它们的定义如下:
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)——加入代码如下:
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);}
嗯!嗯!
我知道!
是的……是的……
我知道很魔性……
接下来,我们继续严肃认真地解决“倒影不够有意境的问题”——加上半透明蒙版:
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追更吧!





