【教程更新】Arm-2D的公开课你错过了么?
【Arm-2D的部署很简单】
Arm-2D的基本设计理念是“傻瓜化”,它表现在部署上就是:
- 支持“无脑”添加所有 C 源文件;
- 默认情况下无需复杂配置;
- 使用前,调用 arm_2d_init() 即可。
- 本身占用RAM极小;
- 支持最高优化等级(-O3,-Os,-Oz,-Ofast,-Omax,-Omin)
- 支持Arm Compiler 5、Arm Compiler 6、GCC和LLVM(理论上也支持IAR)。
废话少说,下面我们就来实际动手进行Arm-2D的部署吧。
准备阶段:
1、准备一个已有的工程,确保该工程已经能够实现基础的LCD初始化,并能提供一个向LCD指定区域传送位图的函数,其原型如下:
/** \fn int32_t GLCD_DrawBitmap (uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap) \brief Draw bitmap (bitmap from BMP file without header) \param[in] x Start x position in pixels (0 = left corner) \param[in] y Start y position in pixels (0 = upper corner) \param[in] width Bitmap width in pixels \param[in] height Bitmap height in pixels \param[in] bitmap Bitmap data \returns - \b 0: function succeeded - \b -1: function failed*/int32_t GLCD_DrawBitmap (uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap)这里,5个参数之间的关系如下图所示:

简单来说,这个函数就是把 bitmap 指针所指向的“连续存储区域” 中保存的像素信息拷贝到LCD的一个指定矩形区域内,这一矩形区域由位置信息(x,y)和体积信息(width,height)共同确定。
很多LCD都支持一个叫做“操作窗口”的概念,这里的窗口其实就是上图中的矩形区域——一旦你通过指令设置好了窗口,随后连续写入的像素就会被依次自动填充到指定的矩形区域内(而无需用户去考虑何时进行折行的问题)。
此外,如果你有幸使用带LCD控制器的芯片——LCD的显示缓冲区被直接映射到Cortex-M芯片的4GB地址空间中,则我们可以使用简单的存储器读写操作来实现上述函数,以STM32F746G-Discovery开发板为例:
//! STM32F746G-Discovery#define GLCD_WIDTH 480#define GLCD_HEIGHT 272
#define LCD_DB_ADDR 0xC0000000#define LCD_DB_PTR ((volatile uint16_t *)LCD_DB_ADDR)
int32_t GLCD_DrawBitmap (uint32_t x, uint32_t y, uint32_t width, uint32_t height, const uint8_t *bitmap) { volatile uint16_t *phwDes = LCD_DB_PTR + y * GLCD_WIDTH + x; const uint16_t *phwSrc = (const uint16_t *)bitmap; for (int_fast16_t i = 0; i < height; i++) { memcpy ((uint16_t *)phwDes, phwSrc, width * 2); phwSrc += width; phwDes += GLCD_WIDTH; }
return 0;}2、获取Arm-2D库:
访问网址或者在【公众号中发送关键字“arm-2d”获取对应压缩包。
需要说明的是,Arm-2D是Arm仓库EndpointAI的一部分。目前与Arm-2D相关的分支有4个:
master——主分支,包含了最简的arm-2d库
main-arm-2d-developing——主分支对应的开发分支
main-arm-2d-more-examples——包含了与主分支一样的内容,并提供了额外的例子(推荐尝鲜的小伙伴使用)
main-arm-2d-more-example-developing——上述分支的开发分支
后续内容,我们将假设下载的是main-arm-2d-more-examples分支中的内容。
部署阶段:
1、提取Arm-2D
解压缩压缩包,然后顺着以下路径找到Arm-2D目录:
"\Kernels\Research\"
将Arm-2D目录整体拷贝出来,放置到你的目标工程目录下,比如:

2、将Arm-2D添加到MDK工程中
在工程管理器中新建一个名为“Arm-2D”的分组,并将文件夹“Arm-2D/Library”下“Include”和“Source”中所有内容都添加到分组中:

为了获取PFB支持,我们还需要再添加对应的Helper服务到工程中来。同样新建一个分组,名为“Arm-2D-Helper”,并将“Arm-2D/Helper”目录下“Include”和“Source”中的所有内容都添加到分组中:

3、配置编译环境
将“Arm-2D/Library/Include”和“Arm-2D/Helper/Include”添加到Include搜索路径列表里:

如果你使用Arm Compiler 6(armclang),则需要打开对C11和GNU扩展的支持,即直接在"Language C"中选择“gnu11”:

如果你使用的是Arm Compiler 5(armcc),则需要打开对C99和GNU扩展的支持,如下图所示:

此外,由于Arm-2D依赖CMSIS ,可以通过配置MDK的RTE的方式来获得最新版本CMSIS的支持。1、如下图所示,通过工具栏最右边的按钮打开Pack Installer

我们会看到类似这样的窗口:

在右半部分的Packs选项卡中,找到ARM::CMSIS,确保它显示“Up to date”,如果没有就单击对应的按钮进行更新。目前我使用的版本就是5.7.0。
如果你的MDK版本较老,同时因为某些原因又不想更新MDK版本,可以通过Pack Installer导入仓库的办法获取最新的CMSIS。具体步骤如下:
1、通过git工具将最新版本的CMSIS从https://github.com/ARM-software/CMSIS_5 的develop 分支下载到本地。比如,我使用的工具就是Github Desktop:


2、打开Pack Installer,并通过菜单File->Manage Local Repository 打开仓库管理窗口:

3、单击Add,并把刚刚从Github上获取的CMSIS加入仓库中:

4、成功后,我们会看到最新的CMSIS已经被加入到Pack列表中了:

此时,单击OK。经过一番等待,我们发现最新的CMSIS 5.8.0(还没有release哦)已经被加入到我们的MDK环境中了:

2、通过如下图所示工具栏正中间的按钮打开RTE配置窗口:

在Software Component列表中,展开CMSIS,并勾选上CORE和DSP。这里需要注意的是,DSP部分如果有Source的选项请选择Source选项——这将允许我们直接使用源代码的形式来编译CMSIS-DSP的库。

此外,如果你不确定RTE中所使用的CMSIS是否为最新的版本的话,可以单击Select Packs按钮:

看到窗体顶部 “Use latest Software Packs for Target” 被勾选,基本上就可以高枕无忧了。依次单击OK关闭对话框后,我们就成功的将CMSIS加入到了编译中。这里,由于我们选择了使用源代码的方式来编译CMSIS,因此可能还需要对CMSIS-DSP的源代码进行额外的设置。
在工程管理器中,找到CMSIS,在右键的弹出菜单中选择“Options for Component Class 'CMSIS'”:

在弹出窗口中选中DSP,并切换到 C/C++选项卡,如果你使用的是Arm Compiler 6,推荐将Optimisation Level设置为 -Ofast,并在Misc Controls中加入小写的“-w” 选项以屏蔽所有的Warning(这一屏蔽效果仅对CMSIS-DSP的源代码有效):

如果你使用的是Arm Compiler 5,则推荐将优化等级设置为Level 3(-O3),并确保旁边的 Optimize for Time处于明确的勾选状态。最后在Misc Controls里加入大写的“-W”来屏蔽所有的Warning。

有的小伙伴可能会对“明确的勾选状态”,下面的截图就是一个“非明确的勾选状态”:

仔细对比,你会发现,不光勾选的颜色是灰色的,其背景色也不是白色——这表示它会根据用户总体的工程设置来编译CMSIS-DSP。
至此,我们就应该能够成功的完成编译了。
仔细想想,部署Arm-2D我们其实也没做啥特别的事情,是不是特别简单?
使用准备阶段:
1、包含头文件
在要使用Arm-2D的地方直接包含“arm_2d.h”,比如:
#include "arm_2d.h"2、初始化Arm-2D
在使用任何Arm-2D服务之前,需要对库进行初始化,比如:
void main(void){ ... arm_2d_init(); ... while(1) { ... }}如果你的芯片SRAM财大气粗——不需要使用PFB,则至此我们已经完成了Arm-2D的全部部署工作。你可以着手第一个“Hello Arm-2D”啦。
PFB Helper 服务的部署:
1、包含头文件
在要使用PFB Helper服务的地方直接包含“arm_2d_helper.h”,比如:
#include "arm_2d_helper.h"2、建立对象
理论上,我们可以建立多个PFB Helper对象——依据应用实际情况而定。这里,我们可以直接使用类型 arm_2d_helper_pfb_t 来建立一个静态实例:
static arm_2d_helper_pfb_t s_tPFBHelper;3、初始化PFB服务:
在使用 PFB Helper之前,我们需要对其进行必要的初始化。Arm-2D提供了一个宏模板,可以帮我们简化必要的步骤:
//! initialise FPB helper if (ARM_2D_HELPER_PFB_INIT( , //!< FPB Helper object , //!< screen width , //!< screen height <像素的数据类型,比如uint16_t>, //!< colour date type , //!< PFB block width , //!< PFB block height , //!< number of PFB in the PFB pool { .evtOnLowLevelRendering = { //! callback for low level rendering .fnHandler = &<底层绘图函数>, }, .evtOnDrawing = { //! callback for drawing GUI .fnHandler = &<图形面绘制函数>, }, } ) < 0) { //! error detected assert(false); }比如,一个典型的例子是:
//! initialise FPB helper if (ARM_2D_HELPER_PFB_INIT( &s_tPFBHelper, //!< FPB Helper object 320, //!< screen width 240, //!< screen height uint16_t, //!< colour date type 16, //!< PFB block width 16, //!< PFB block height 1, //!< number of PFB in the PFB pool { .evtOnLowLevelRendering = { //! callback for low level rendering .fnHandler = &__pfb_render_handler, }, .evtOnDrawing = { //! callback for drawing GUI .fnHandler = &__pfb_draw_handler, }, } ) < 0) { //! error detected assert(false); }其中,底层LCD像素绘制函数__pfb_render_handler()负责将PFB中的像素发送给LCD:
static IMPL_PFB_ON_LOW_LV_RENDERING(__pfb_render_handler){ const arm_2d_tile_t *ptTile = &(ptPFB->tTile);
ARM_2D_UNUSED(pTarget); ARM_2D_UNUSED(bIsNewFrame);
GLCD_DrawBitmap(ptTile->tRegion.tLocation.iX, ptTile->tRegion.tLocation.iY, ptTile->tRegion.tSize.iWidth, ptTile->tRegion.tSize.iHeight, ptTile->pchBuffer);
arm_2d_helper_pfb_report_rendering_complete(&s_tExamplePFB, (arm_2d_pfb_t *)ptPFB);}
这里的arm_2d_helper_report_rendering_complete() 负责释放从PFB池中分配到的 arm_2d_pfb_t 对象——这点非常关键。
对于使用DMA来异步刷新LCD的系统来说,用户就需要对上述过程做一个修改:
在 __pfb_render_handler() 中向DMA发送刷新请求;
当DMA完成刷新后,在对应的完成中断处理程序中调用用 arm_2d_helper_pfb_report_rendering_complete() 来释放 PFB对象;
这里的 __pfb_draw_handler_t() 就是我们绘制图形界面的函数:
static IMPL_PFB_ON_DRAW(__pfb_draw_handler_t){ ARM_2D_UNUSED(pTarget); ARM_2D_UNUSED(bIsNewFrame);
arm_2d_region_t tBox = { .tLocation = {50,50}, .tSize = {200, 100}, }; //! 背景填充白色 arm_2d_rgb16_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE); //! 在box指定的区域绘制黑色影子 arm_2d_rgb16_fill_colour(ptTile, &tBox, GLCD_COLOR_BLACK); //! 适当向左上角移动box tBox.tLocation.iX -= 10; tBox.tLocation.iY -= 10; //! 在box指定的区域填充蓝色,并且使用 50%(128/255)的透明效果 arm_2d_rgb565_fill_colour_with_alpha( ptTile, &tBox, (arm_2d_color_rgb565_t){GLCD_COLOR_BLUE}, 128); //!< 透明度
return arm_fsm_rt_cpl;}在这个例子中,我们简单的实现了一个半透明浮动窗口的效果,(这篇文章实在太长了,就简单做个例子凑个数吧):

4、调用 PFB Helper服务任务:
要想使用PFB,还需要在超级循环或者某个RTOS任务里调用PFB的服务函数 arm_2d_helper_pfb_task(),由于它是非阻塞的、返回值为状态机的状态 arm_fsm_rt_t,因此使用方法非常灵活,例如:
int main (void) { lcd_init(); arm_2d_init(); //! initialise FPB helper if (ARM_2D_HELPER_PFB_INIT( &s_tPFBHelper, //!< FPB Helper object 320, //!< screen width 240, //!< screen height uint16_t, //!< colour date type 320, //!< PFB block width 1, //!< PFB block height 1, //!< number of PFB in the PFB pool { .evtOnLowLevelRendering = { //! callback for low level rendering .fnHandler = &__pfb_render_handler, }, .evtOnDrawing = { //! callback for drawing GUI .fnHandler = &__pfb_draw_handler_t, }, } ) < 0) { //! error detected assert(false); }
while(1) { //! call partial framebuffer helper service while(arm_fsm_rt_cpl != arm_2d_helper_pfb_task(&s_tPFBHelper, NULL)); }
}值得特别说明的是,函数arm_2d_helper_pfb_task() 的第二个参数是脏矩阵列表(的地址),简单说就是一个由用户指定的刷新区域列表——你让PFB只刷哪些区域,它就只刷哪些区域。为了方便用户,Arm-2D还专门提供了一套宏模板来简化用户的脏矩阵列表定义工作,例如:
/*! define dirty regions */ IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static const)
/* a region for the busy wheel */ ADD_REGION_TO_LIST(s_tDirtyRegions, .tLocation = {(APP_SCREEN_WIDTH - 80) / 2, (APP_SCREEN_HEIGHT - 80) / 2}, .tSize = { .iWidth = 80, .iHeight = 80, }, ),
/* a region for the status bar on the bottom of the screen */ ADD_LAST_REGION_TO_LIST(s_tDirtyRegions, .tLocation = {0,APP_SCREEN_HEIGHT - 8}, .tSize = { .iWidth = APP_SCREEN_WIDTH, .iHeight = 8, }, ),
END_IMPL_ARM_2D_REGION_LIST()
//! call partial framebuffer helper service while(arm_fsm_rt_cpl != arm_2d_helper_pfb_task( &s_tExamplePFB, (arm_2d_region_list_item_t *)s_tDirtyRegions));在这个例子中,代码定义了两个区域:一个是屏幕正中央一块 80*80 的区域,以及屏幕底部一个高度为8像素的条状区域(可以用于状态信息的显示)——最终的效果是,每次使用PFB进行刷新,这两个区域以外的部分都会被跳过(保持不变),从而节省了大量的处理时间,客观上提高了用户实际可见的帧率(Arm-2D中对于这种情况使用 Update per second而不是Frame per second 进行描述)。
借助这一范例很容易发现:通过宏 ADD_REGION_TO_LIST()我们可以几乎毫无限制的向列表中添加任意数量的区域,其语法为:
ADD_REGION_TO_LIST(<列表名称>, .tLocation = {<坐标信息>}, .tSize = {<尺寸信息>}),需要注意的是,列表的最后一个元素一定要用 ADD_LAST_REGION_TO_LIST()来添加,否则代码一定会出现内存溢出的惨状。
整个列表的语法为:
/*! define dirty regions */IMPL_ARM_2D_REGION_LIST(列表名称, <列表变量的修饰>) ...END_IMPL_ARM_2D_REGION_LIST()这里,“列表名称”实际上就是列表的变量名,而“列表变量的修饰” 则是大家熟悉的类型修饰符,比如 static、const 一类——正确使用修饰符既可以节省RAM消耗,也可以在需要的情况下建立允许动态修改内容的列表。
【说在后面的话】
至此,我们完成了Arm-2D在工程中的部署,赋予了那些资源极端受限的单片机以“低帧率换低资源消耗”的方式 实现较为华丽图形界面的“人权”。其实,不光是小资源系统可以使用PFB来解决“从无到有”的问题,资源较为宽裕的芯片也可以使用1/2 甚至是1/4的PFB来换取更多的 SRAM 用于改善或者拓展其它应用性能,比如,改善音频处理类应用的缓冲效果等等。
另一方面,如果将PFB大小设置为完整的屏幕尺寸,实际上就可以将PFB Helper服务当做一个帧缓冲池来使用;此外,倘若上层的GUI软件能向PFB Helper传递脏矩阵列表,就能在刷新帧率上获得极大的优化空间。
作为本系列的第二篇,我们介绍了Arm-2D对普通单片机的意义,并提供了一个手把手的部署教程。后续内容,我们将在PFB平台的基础上以一个个具体的控件特效为例,详细为您介绍Arm-2D API的使用和技巧——什么进度条啊,滑动列表啊,菜单啊,统统都会安排上。如果你想一起追剧,就赶快搭建好测试平台吧。





