【Arm-2D】不整活儿玩啥GUI?

2021-05-27 15:57:57 浏览数 (1)

上回我们说到:Arm-2D是小资源单片机

之前,我们说过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); //!< 就是单纯的拷贝,不做作

}

容易注意到两点:

  1. 既然已经是全屏幕无缝填充了,自然不需要背景色了;
  2. 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追更吧

0 人点赞