【例说Arm-2D界面设计】片内Flash不够怎么办?
扫描二维码
随时随地手机看文章
【说在前面的话】
在前面的文章《【例说Arm-2D界面设计】“手撸GUI”的利器——场景播放器》中,我们详细介绍了智能设备时代一种“基于面板(Panel)的嵌入式界面设计范式”,并以Arm-2D的场景播放器(Scene Player)为例,介绍了小资源环境下具体“手搓GUI”的方式。
很多小伙伴看了以后大为震撼,并纷纷发出了叩问灵魂的拷问:
我芯片内部Flash已经很紧张了,随便一个背景图你让我存哪里?
一般来说,很多芯片会提供一个叫做 XIP 的外设(也许你的芯片中对应的外设并不叫这个名字,但你可以根据下面的功能描述来对号入座),以便:
- 通过QSPI接口连接外部的SPI Flash芯片;
- 将外部Flash芯片中的内容映射到4G地址空间中
换句话说,我们可以像访问内部Flash那样使用芯片外部的SPI Flash,这其中不仅包括存储数据(比如图片),甚至还可以执行代码。
如果你芯片拥有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)。
也许你已经从根贴图的指针看出了端倪:普通的根贴图要求其指向的图片资源必须存在于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 );};
【如何使用虚拟资源?】
这里,我们假设你已经按照文章《【喂到嘴边了的模块】准备徒手撸GUI?用Arm-2D三分钟就够了》的步骤完成了Arm-2D的部署。
准备阶段:
在工程管理器中展开 Acceleration,并找到你的LCD驱动模板 arm_2d_disp_adapter_0.h(这里假设你只有一个屏幕):
通过Configuraion Wizard打开图形配置界面:
假设你已经配置好了其它部分,勾选这里的“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 __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;}
也许你要问,如果我要处理多个图片该怎么办呢?别着急,后面会有专门的章节详细介绍。现阶段我们先专注于完成一个最简单的例子。
完成了上述准备工作,再次编译就应该毫无问题了。
创建自己的虚拟资源:
在要创建虚拟资源的源代码中加入对 Display Adapter 0 的头文件引用:
定义一个 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);
效果如下:
正如我们前面说过的,虚拟素材(virtual resource)也是贴图(Tile)的一种,因此,也可以在它的基础上创建子贴图(Child Tile),比如:
staticconst 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,};
这里:
-
bIsRoot为false,清晰的标明了 c_tChildImage 的子贴图身份;
-
创建子贴图作为素材时,bDerivedResource一定要设置为 true,切记切记!
-
这个例子中,观察 tLocation和tSize容易发现:我们实际上是取了原图右下角的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),因此被默认放置到了屏幕的左上角,形成了如下的效果:
【我们有多个图片该怎么办?】
前面的例子中,为了让小伙伴们快速的体验虚拟资源的爽快,因此我们对内容作了简化——只演示了一个图片的情况——实际应用中,显然这是无法满足要求的。
聪明的小伙伴也许已经注意到了,当存在多个图片资源的时候,决定我们实际读取那一张图片的关键就是函数 __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)
-
所见即所得的方式
剩下的篇幅,我们将着重介绍“所见即所得”的方法:
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;}