38

傻孩子(GorgonMeducer) · 2022年09月07日 · 北京市

【例说Arm-2D界面设计】片内Flash不够怎么办?

bu'z

image.png

【说在前面的话】


在前面的文章《【例说Arm-2D界面设计】“手撸GUI”的利器——场景播放器》中,我们详细介绍了智能设备时代一种“基于面板(Panel)的嵌入式界面设计范式”,并以Arm-2D的场景播放器(Scene Player)为例,介绍了小资源环境下具体“手搓GUI”的方式。

很多小伙伴看了以后大为震撼,并纷纷发出了叩问灵魂的拷问:

image.png

我芯片内部Flash已经很紧张了,随便一个背景图你让我存哪里?

这是个好问题。一般来说,我们所说的小资源环境是指:内部FLASH空间小于64K,当然也包括那些虽然芯片Flash较大(比如有128K)但原本的应用代码已经非常庞大——“留给GUI的空间已经不多”的情况。总之,留给Arm-2D的代码空间已经捉襟见肘,图片资源又该保存到哪里呢?

一般来说,很多芯片会提供一个叫做XIP的外设(也许你的芯片中对应的外设并不叫这个名字,但你可以根据下面的功能描述来对号入座),以便:

  • 通过QSPI接口连接外部的SPI Flash芯片;
  • 将外部Flash芯片中的内容映射到4G地址空间中

换句话说,我们可以像访问内部Flash那样使用芯片外部的SPI Flash,这其中不仅包括存储数据(比如图片),甚至还可以执行代码。

image.png

如果你芯片拥有XIP,那么你的芯片已经不属于我们所讨论的“资源受限”的环境了——因为大容量的片外Flash不仅长见而且廉价。相信很多人对此都非常熟悉,我就不再赘述了。

如果你的芯片没有XIP,而你也只能通过外设 SPI 和自己编写的驱动来访问外部 Flash,此时,我们如何使用 Arm-2D 来简化“手搓GUI” 的开发过程呢?不要急,本文就将为你揭晓谜底。

【什么是虚拟资源(Virtual Resource)】


Arm-2D几乎所有API的基本操作单位都是“贴图(Tile)”,它的数据结构定义如下:


/*!
 * \brief a type for tile
 * 
 */
typedef struct arm_2d_tile_t arm_2d_tile_t;
struct arm_2d_tile_t {
    implement_ex(struct {
        uint8_t    bIsRoot              : 1;                                    //!< is this tile a root tile
        uint8_t    bHasEnforcedColour   : 1;                                    //!< does this tile contains enforced colour info
        uint8_t    bDerivedResource     : 1;                                    //!< indicate whether this is a derived resources (when bIsRoot == 0)
        uint8_t    bVirtualResource     : 1;                                    //!< indicate whether the resource should be loaded on-demand
        uint8_t                         : 4;
        uint8_t                         : 8;
        uint8_t                         : 8;
        arm_2d_color_info_t    tColourInfo;                                     //!< enforced colour
    }, tInfo);

    implement_ex(arm_2d_region_t, tRegion);                                     //!< the region of the tile

    union {
        /* when bIsRoot is true, phwBuffer is available,
         * otherwise ptParent is available
         */
        arm_2d_tile_t       *ptParent;                                          //!< a pointer points to the parent tile
        uint8_t             *pchBuffer;                                         //!< a pointer points to a buffer in a 8bit colour type
        uint16_t            *phwBuffer;                                         //!< a pointer points to a buffer in a 16bit colour type
        uint32_t            *pwBuffer;                                          //!< a pointer points to a buffer in a 32bit colour type
        
        intptr_t            nAddress;                                           //!< a pointer in integer
    };
};

如果看着比较晕,不要紧,其实它就只有三大部分而已:

  • 贴图的各类属性描述信息:_tInfo_
  • 贴图的尺寸和位置信息:    _tRegion_
  • 贴图的指针或引用

贴图从种类上来说分类两种:根贴图(Root Tile)子贴图(Child Tile)。其中,根贴图是指那些直接“拥有”具体图片资源(或者是显示缓冲区)的贴图(Tile),这表现在:

  • 根贴图的属性 _tInfo.bIsRoot_一定为true
  • 根贴图的指针直接指向具体的资源数组或者现实缓冲区

从前面的数据结构中,我们可以看到union中有很多指针,比如 pchBuffer、phwBuffer和pwBuffer。由于他们都是共用体,因此这些指针保存的地址值都是相同的,而具体使用哪个类型的指针则取决于目标资源的颜色格式,这一信息可以是省略的,但一般通过 img2c.py 脚本转换出来的tile都会在tInfo.tColourInfo中包含具体的颜色信息。

值得强调的是 ptParent 仅在子贴图中有意义,用于指向自己的父贴图(Parent Tile),而 nAddress 仅仅是方便对地址值进行四则运算的一个整形变量(uintptr_t)。


推论1:我们所有的图片资源都是用根贴图来描述的。

也许你已经从根贴图的指针看出了端倪:普通的根贴图要求其指向的图片资源必须存在于4G地址空间中——换句话说就是普通指针可以访问的地方——保存在外部Flash中的图片资源(在未经XIP帮助的情况下)则无法满足上述要求,因此无法直接用 arm_2d_tile_t 进行描述

为了解决这一问题,arm-2d在基类arm_2d_tile_t的基础上派生出了一个新的类:虚拟资源(Virtual Resource),arm_2d_vres_t——专门用于描述这类无法直接访问的图片资源。其数据结构如下:


/*!
 * \brief a type for virtual resource
 *
 * \note the flag tTile.tInfo.bVirtualResource must be true (1)
 */
typedef struct arm_2d_vres_t arm_2d_vres_t;
struct arm_2d_vres_t {

    /*! base class: tTile */
    implement_ex( arm_2d_tile_t, tTile);
    
    /*!  a reference of an user object  */
    uintptr_t pTarget;
    
    /*!
     *  \brief a method to load a specific part of an image
     *  \param[in] pTarget a reference of an user object 
     *  \param[in] ptVRES a reference of this virtual resource
     *  \param[in] ptRegion the target region of the image
     *  \return intptr_t the address of a resource buffer which holds the content
     */
    intptr_t  (*Load)   (   uintptr_t pTarget, 
                            arm_2d_vres_t *ptVRES, 
                            arm_2d_region_t *ptRegion);
    
    /*!
     *  \brief a method to despose the buffer
     *  \param[in] pTarget a reference of an user object 
     *  \param[in] ptVRES a reference of this virtual resource
     *  \param[in] pBuffer the target buffer
     */
    void      (*Depose) (   uintptr_t pTarget, 
                            arm_2d_vres_t *ptVRES, 
                            intptr_t pBuffer );
};

image.png

对上述结构提描述感到一头雾水的小伙伴不要慌张——实际使用中,我们并不需要与arm_2d_vres_t 的内部结构打交道——arm-2d为我们提供了傻瓜式的封装服务,使用起来依然非常简单

【如何使用虚拟资源?】


这里,我们假设你已经按照文章《【喂到嘴边了的模块】准备徒手撸GUI?用Arm-2D三分钟就够了》的步骤完成了Arm-2D的部署。

准备阶段:

在工程管理器中展开 Acceleration,并找到你的LCD驱动模板 arm_2d_disp_adapter_0.h(这里假设你只有一个屏幕):

image.png

通过Configuraion Wizard打开图形配置界面:

image.png

假设你已经配置好了其它部分,勾选这里的“Enable the virtual resoure helper service” 选项后保存——至此,我们就为 Display Adapter 0 开启了其专属的虚拟资源辅助服务(Helper Service)。需要强调的是,每个Display Adapter 都有自己独立的虚拟资源辅助服务,需要独立的打开

此时,如果直接编译,会看到如下的错误:


Error: L6218E: Undefined symbol __disp_adapter0_vres_get_asset_address (referred from arm_2d_disp_adapter_0.o).
Error: L6218E: Undefined symbol __disp_adapter0_vres_read_memory (referred from arm_2d_disp_adapter_0.o).

不要慌,这是我们有意为之——它提醒我们作为用户需要提供(实现)两个最基本额接口函数:

  • __disp_adapter0_vres_read_memory()

一个专门用于从外部存储器的指定地址读取指定长度字节的函数,其原型如下:


void __disp_adapter0_vres_read_memory(
     intptr_t pObj, 
     void *pBuffer,
     uintptr_t pAddress,
     size_t nSizeInByte);

这里:

    • pObj 我们可以暂时忽略
    • pBuffer指向一块缓冲区,用于保存我们从外部存储器中读取到的内容;
    • pAddress保存的是目标内容在外部存储器中的地址;
    • nSizeInByte 保存的是要读取的字节数

一般来说,如果我们已经事先调试好了一个SPI Flash读取函数,就可以轻松的实现这一函数,比如:

extern void spi_flash_read(void *pBuffer, 
                           uint32_t nAddressInFlash,
                           size_t nSize);
                           
void __disp_adapter0_vres_read_memory(intptr_t pObj, 
                                    void *pBuffer,
                                    uintptr_t pAddress,
                                    size_t nSizeInByte)
{
    ARM_2D_UNUSED(pObj);
    /* it is just a demo, in real application, you can place a function to 
     * read SPI Flash 
     */
    spi_flash_read(pBuffer, (void * const)pAddress, nSizeInByte);
} 
  • __disp_adapter0_vres_get_asset_address()

一个专门用于返回当前虚拟资源起始地址的函数。需要注意的是,它的返回类型是uintptr_t,在Cortex-M环境下是一个32位的无符号整形(uint32_t),我们用它来返回目标图片在SPI Flash中的起始地址绰绰有余。其函数原型是:


uintptr_t __disp_adapter0_vres_get_asset_address(
    uintptr_t pObj,
    arm_2d_vres_t *ptVRES);

这里:

    • pObj我们可以暂时忽略
    • ptVRES 指向的是我们的目标虚拟资源

在最简单的情况下,假设你的系统只有一背景图保存在外部SPI Flash中,且地址为0x00000000,那么这个函数就极其简单了:


uintptr_t __disp_adapter0_vres_get_asset_address(uintptr_t pObj,
                                               arm_2d_vres_t *ptVRES)
{
    ARM_2D_UNUSED(ptVRES);
    ARM_2D_UNUSED(pObj);
    
    return 0x00000000;
}

也许你要问,如果我要处理多个图片该怎么办呢?别着急,后面会有专门的章节详细介绍。现阶段我们先专注于完成一个最简单的例子。

完成了上述准备工作,再次编译就应该毫无问题了。

image.png

创建自己的虚拟资源:

在要创建虚拟资源的源代码中加入对Display Adapter 0的头文件引用:

#include "arm_2d_disp_adapter_0.h"

定义一个arm_2d_vres_t类型的静态变量(或者全局变量),并使用专门的宏 disp_adapter0_impl_vres来描述资源的颜色和尺寸信息,比如:


static arm_2d_vres_t s_tMyVirtualRes = 
    disp_adapter0_impl_vres(   
        ARM_2D_COLOUR_RGB565,    // 图片的颜色格式
        320,                     // 图片的宽度
        256,                     // 图片的高度
    );

其中,宏disp_adapter0_impl_vres()Display Adapter 0专用的,以此类推,如果你的虚拟资源要在Display Adapter 1上使用,则对应的描述宏为 disp_adapter1_impl_vres()。不管如何,它们的原型是一样的:


disp_adapter0_impl_vres(__COLOUR_FORMAT, __WIDTH, __HEIGHT,...)

这里:

    • ___COLOUR_FORMAT_是目标素材的颜色格式,具体可用的颜色在 _arm_2d_type.h_中定义,都以_ARM_2D_COLOUR__作为前缀。
    • ___WIDTH_是目标素材的像素宽度
    • ___HEIGHT_是目标素材的像素高度
    • ...是一系列可选的参数,主要用于初始化_arm_2d_vres_t_中的一些特殊成员(比如_pTarget_),这个在随后的章节中会用到。

在上述例子中,我们创建了一个虚拟资源 s_tMyVirtualRes,由于它是 arm_2d_tile_t的派生类,因此可以像普通的贴图那样在arm-2d的API中作为素材(source tile)蒙版(mask)来直接使用,比如:


/* 把 虚拟素材 显示在屏幕上 */
arm_2d_tile_copy(   &s_tMyVirtualRes.tTile,  /* 素材 */
                    ptTile,                  /* 目标缓冲区 */
                    NULL, 
                    ARM_2D_CP_MODE_COPY);

效果如下:

image.png

正如我们前面说过的,虚拟素材(virtual resource)也是贴图(Tile)的一种,因此,也可以在它的基础上创建子贴图(Child Tile),比如:


static
const arm_2d_tile_t c_tChildImage = {
    .tRegion = {
        .tLocation = {
            .iX = 160,
            .iY = 128,
        },
        .tSize = {
            .iWidth = 160,
            .iHeight = 128,
        },
    },
    .tInfo = {
        .bIsRoot = false,
        .bDerivedResource = true,
    },
    .ptParent = (arm_2d_tile_t *)&s_tMyVirtualRes.tTile,
};

这里:

  • bIsRootfalse,清晰的标明了c_tChildImage的子贴图身份;
  • 创建子贴图作为素材时,bDerivedResource一定要设置为_true_,切记切记!
  • 这个例子中,观察tLocationtSize容易发现:我们实际上是取了原图右下角的1/4作为新的素材

修改代码,将新的素材也拷贝到屏幕上:


/* 把 虚拟素材 显示在屏幕上 */
arm_2d_tile_copy(   &s_tMyVirtualRes.tTile,  /* 素材 */
                    ptTile,                  /* 目标缓冲区 */
                    NULL, 
                    ARM_2D_CP_MODE_COPY);
/* 把 子贴图 显示在屏幕上 */
arm_2d_tile_copy(   &c_tChildImage,          /* 素材 */
                    ptTile,                  /* 目标缓冲区 */
                    NULL, 
                    ARM_2D_CP_MODE_COPY);

由于我们在拷贝子贴图时没有指定要复制的位置(给了NULL),因此被默认放置到了屏幕的左上角,形成了如下的效果:

image.png

【我们有多个图片该怎么办?】


前面的例子中,为了让小伙伴们快速的体验虚拟资源的爽快,因此我们对内容作了简化——只演示了一个图片的情况——实际应用中,显然这是无法满足要求的。

聪明的小伙伴也许已经注意到了,当存在多个图片资源的时候,决定我们实际读取那一张图片的关键就是函数 __disp_adapter0_vres_get_asset_address()的返回值——它返回谁的地址,读取的就是谁的图片。

观察它的函数原型,容易发现两个形参都很有潜质。


uintptr_t __disp_adapter0_vres_get_asset_address(
    uintptr_t pObj,
    arm_2d_vres_t *ptVRES)

换句话说,支持多图片的关键就在于如何使用传递进来的参数返回对应图片在外部存储器中的地址

进一步观察arm_2d_vres_t的结构,我们可以注意到一个有趣的成员pTarget


typedef struct arm_2d_vres_t arm_2d_vres_t;
struct arm_2d_vres_t {
    ...
    /*!  a reference of an user object  */
    uintptr_t pTarget;
    ...
};
...

无论我们给它赋任何内容,它的值都会作为第一个实参传递给接口函数

__disp_adapter0_vres_read_memory(intptr_t pObj, …… ) 

和 

__disp_adapter0_vres_get_asset_address(uintptr_t pObj, ……)

也就是这里的 pObj。

至此,对多图片的支持实际上就形成了两种方式:

  • 面向对象的方式(OOPC)
  • 所见即所得的方式

对于熟悉使用C语言进行面向对象开发(OOPC)的小伙伴来说,恐怕看了上面的描述就已经心领神会了吧。这里就不再赘述。

image.png

剩下的篇幅,我们将着重介绍“所见即所得”的方法:

步骤一:在建立(描述)虚拟资源时,将目标图片在外部存储器中的地址直接赋值给pTarget。比如:


static arm_2d_vres_t s_tVRes0 = 
    disp_adapter0_impl_vres(   
        ARM_2D_COLOUR_RGB565,
        32,
        32,
        .pTarget = <这个资源在外部存储器中的地址>
    );

static arm_2d_vres_t s_tVRes1 = 
    disp_adapter0_impl_vres(   
        ARM_2D_COLOUR_RGB565,
        32,
        32,
        .pTarget = <这个资源在外部存储器中的地址>
    );
...

步骤二:在函数 _\_\_disp\_adapter0\_vres\_get\_asset\_address_里直接将pObj的值(也就是_ptVRES->pTarget_)的值返回:


uintptr_t __disp_adapter0_vres_get_asset_address(uintptr_t pObj,
                                               arm_2d_vres_t *ptVRES)
{
    ARM_2D_UNUSED(ptVRES);
    return pObj;
}

怎么样,没法更简单了吧?是不是所见即所得?

image.png

【一些优化SRAM占用的建议】


虚拟资源本质上是通过将外部存储器中的图片资源载入到一块芯片内部的缓冲区(RAM)中,并以此来完成后续的贴图操作。换句话说:

  1. 如果一个API中用到了一个虚拟素材,就需要一块缓冲;
  2. 如果一个API中用到了两个虚拟素材,就需要两快缓冲,比如:

    1. RGB565的图片的像素数组(Source)
    2. 图片对应的蒙版(Source Mask)
  3. 如果一个API中用到了三个虚拟素材,就需要三块缓冲,比如:

    1. RGB565的图片的像素数组(Source)
    2. 图片对应的蒙版(Source Mask)
    3. 目标缓冲对应的蒙版(Target Mask)

当然,目前arm-2d的API最多也就只有涉及三个虚拟素材的情况。因此,在默认情况下,Display Adapter在编译阶段会多准备3个PFB Block供虚拟资源当做缓冲使用:

static void __user_scene_player_init(void)
{
    ...
    //! initialise FPB helper
    if (ARM_2D_HELPER_PFB_INIT(
       ...
        __DISP0_CFG_PFB_HEAP_SIZE__                                    //!< number of PFB in the PFB pool

#if     __DISP0_CFG_VIRTUAL_RESOURCE_HELPER__                          \
    &&  !__DISP0_CFG_USE_HEAP_FOR_VIRTUAL_RESOURCE_HELPER__
        + 3
#endif
       ...

#if     __DISP0_CFG_VIRTUAL_RESOURCE_HELPER__                          \
    &&  !__DISP0_CFG_USE_HEAP_FOR_VIRTUAL_RESOURCE_HELPER__
        .FrameBuffer.u4PoolReserve = 3,                                         // reserve 3 PFB blocks for the virtual resource service
#endif
    ) < 0) {
        //! error detected
        assert(false);
    }
}

换句话说,哪怕你在配置阶段为了节省RAM——将PFB池中PFB Block的数量设置为1,上述代码也会因为用户(你)开启了虚拟资源的支持而追加3个PFB Block到池中(并且通过 .FrameBuffer.u4PoolReserve来保留指定数量的PFB Block 给虚拟资源使用)。

这么做当然是为了最坏情况下(一个API用了三个虚拟资源时)每个虚拟资源都能获得自己的缓冲,但显然也占用了大量的RAM资源——这就是需要用户自己平衡的事情了——如果你能拍胸脯说:我的应用绝对不会在一个API中同时用到两个以上的虚拟资源,你就可以把这里的+3连同后面通过.FrameBuffer.u4PoolReserve保留的PFB数量都改为2。同理,如果你可以确保你的应用不会在同一个API中使用1个以上的虚拟资源,就可以将上述两处设置都修改为1——改成0虚拟资源就不工作了——不能马儿跑马儿不吃草,对不对?

image.png

当然,如果你的芯片RAM“相对比较大”,可以负担得起一定尺寸的堆分配(HEAP),则可以通过配置强令虚拟资源的辅助服务使用堆来分配,从而绕过了上述烦恼。

具体做法为在Display Adapter的配置文件中勾选 “Use heap to allocate buffer in the virtual resource helper service”

image.png

这样,Display Adapter不会自说自话的追加池中PBF Block的数量,你也不用手动修改前面提到的设置了——不过我还是要提醒一句,小心HEAP尺寸不足,导致虚拟素材分配空间时候被assert抓住哦:

image.png

image.png

【说在后面的话】


从使用极小的缓冲区实现整块大屏幕刷新的“部分缓冲(PFB)” 技术,到帮助用户直接使用外部存储器中图片资源的“虚拟素材(Virtual Resource)”,Arm-2d可谓是在帮助小资源环境下手搓GUI的小伙伴们操碎了心。

image.png

如果上述技术确实帮助到了你,还请不吝惜你的小⭐️⭐️,给Arm-2D这个开源项目一个Star吧。

https://github.com/ARM-software/Arm-2D

如果能留下你的意见和建议就最好了,这对我们很重要。谢谢啦。

原文:裸机思维
作者:GorgonMeducer 傻孩子

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1466
内容数
108
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息