【说在前面的话】
随着时间的推移,从4月份更新第一个版本以来,Arm-2D也逐渐走入成熟期,截止到我编写这篇文稿的时间,其版本已经来到了0.9.8,而Github开发分支上的版本也进入了0.9.9 dev。
在社区的帮助下,如今的版本不仅修复了一些不容易发现的bug,还追加了一些喜闻乐见的新特性,比如:
- 支持将目标贴图进行任意角度旋转的rotation类API
- 角度是浮点数
- 用户可以指定贴图饶哪个坐标进行旋转(而不是默认绕贴图中心旋转)
- 支持可选的抗锯齿效果
- 默认支持抠图
- 支持透明度效果
- 提供了一个python脚本 img2c.py用来帮助用户将给定的图片转换成Arm-2D格式的tile。
作为一个资料综合贴,本文希望能够帮您解决以下问题:
- Arm-2D公开课和对应的资料贴从哪里找
- Arm-2D的傻瓜图文部署教程
- Arm-2D依赖哪个版本的CMSIS?以及如何在MDK环境下安装和部署最新的CMSIS
- 如何利用Arm-2D提供的 PFB Helper 来降低资源消耗
- 如何利用PFB Helper将 RGB16 的高低字节交换
如果你对Arm-2D的话题感兴趣,并且不想错过历史上以及未来所有我会发布的相关文章,欢迎单击文章最后的“阅读原文”来订阅话题#Arm-2D
【公开课和资料下载】
此前,受到极客社区的邀请,我有幸为大家献上了一期名为“Arm-2D初探——填补空白还是屋上架屋”的公开课。原本计划是1个小时,无奈说的太嗨了,一不小心就讲了3个小时……
如果你错过了这次直播,可以通过下面的连接进行回看(记得要完成新手任务):
https://aijishu.com/l/1110000000204649
讲课所使用的PPT可以通过下面的连接来下载:
https://cdn-file.aijishu.com/261/007/2610079804-60ab64d80a07e?_upt=31db16cd1622241529
虽然公开课用了3个小时里里外外详细的介绍了Arm-2D的方方面面,然而,为了节省时间,Arm-2D的移植却故意没有过多提及——这里我们就必须要做一下补充。手把手的教程是在此前的文章《为什么说Arm-2D是小资源单片机的GUI人权卡!》的基础上扩展而来:
- 增加了CMSIS配置的流程
- 按照最新版本的要求加入了一些API接口依赖的描述
希望大家喜欢。
【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指定区域传送位图的函数,其原型如下:
代码语言:javascript复制/**
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开发板为例:
代码语言:javascript复制//! 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库:
访问网址:
代码语言:javascript复制https://github.com/ARM-software/EndpointAI
或者在【裸机思维】公众号中发送关键字“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目录:
代码语言:javascript复制"KernelsResearch"
将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”,如果没有就单击对应的按钮进行更新。Arm-2D所依赖的CMSIS版本不得低于5.7.0(如果你要用Cortex-M55,则版本不得低于5.8.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”,比如:
代码语言:javascript复制#include "arm_2d.h"
2、初始化Arm-2D
在使用任何Arm-2D服务之前,需要对库进行初始化,比如:
代码语言:javascript复制void main(void)
{
...
arm_2d_init();
...
while(1) {
...
}
}
如果你的芯片SRAM财大气粗——不需要使用PFB,则至此我们已经完成了Arm-2D的全部部署工作。你可以着手第一个“Hello Arm-2D”啦。
PFB Helper 服务的部署:
1、包含头文件
在要使用PFB Helper服务的地方直接包含“arm_2d_helper.h”,比如:
代码语言:javascript复制#include "arm_2d_helper.h"
2、建立对象
理论上,我们可以建立多个PFB Helper对象——依据应用实际情况而定。这里,我们可以直接使用类型 arm_2d_helper_pfb_t 来建立一个静态实例:
代码语言:javascript复制static arm_2d_helper_pfb_t s_tPFBHelper;
3、初始化PFB服务:
在使用 PFB Helper之前,我们需要对其进行必要的初始化。Arm-2D提供了一个宏模板,可以帮我们简化必要的步骤:
代码语言:javascript复制 //! initialise FPB helper
if (ARM_2D_HELPER_PFB_INIT(
<PFB Helper对象的地址>, //!< FPB Helper object
<LCD的像素宽度>, //!< screen width
<LCD的像素高度>, //!< screen height
<像素的数据类型,比如uint16_t>, //!< colour date type
<PFB的像素宽度>, //!< PFB block width
<PBF的像素高度>, //!< PFB block height
<PFB池中PFB的数量,一般写1>, //!< number of PFB in the PFB pool
{
.evtOnLowLevelRendering = {
//! callback for low level rendering
.fnHandler = &<底层绘图函数>,
},
.evtOnDrawing = {
//! callback for drawing GUI
.fnHandler = &<图形面绘制函数>,
},
},
//.FrameBuffer.bSwapRGB16 = true,
) < 0) {
//! error detected
assert(false);
}
注意,如果你的屏幕需要将RGB16的高低字节进行交换,则只需要把第20行的注释符号"//"删除即可,这样我们就可以通过选项:
代码语言:javascript复制.FrameBuffer.bSwapRGB16 = true,
让 PFB Helper帮我们自动进行高低字节的交换。
一个典型的例子是:
代码语言:javascript复制 //! 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:
代码语言:javascript复制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_tPFBHelper,
(arm_2d_pfb_t *)ptPFB);
}
这里的arm_2d_helper_pfb_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() 就是我们绘制图形界面的函数:
代码语言:javascript复制static
IMPL_PFB_ON_DRAW(__pfb_draw_handler)
{
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,因此使用方法非常灵活,例如:
代码语言:javascript复制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还专门提供了一套宏模板来简化用户的脏矩阵列表定义工作,例如:
代码语言:javascript复制 /*! 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_tPFBHelper,
(arm_2d_region_list_item_t *)s_tDirtyRegions));
在这个例子中,代码定义了两个区域:一个是屏幕正中央一块 80*80 的区域,以及屏幕底部一个高度为8像素的条状区域(可以用于状态信息的显示)——最终的效果是,每次使用PFB进行刷新,这两个区域以外的部分都会被跳过(保持不变),从而节省了大量的处理时间,客观上提高了用户实际可见的帧率(Arm-2D中对于这种情况使用 Update per second而不是Frame per second 进行描述)。
借助这一范例很容易发现:通过宏 ADD_REGION_TO_LIST()我们可以几乎毫无限制的向列表中添加任意数量的区域,其语法为:
代码语言:javascript复制ADD_REGION_TO_LIST(<列表名称>,
.tLocation = {<坐标信息>},
.tSize = {<尺寸信息>}
),
需要注意的是,列表的最后一个元素一定要用 ADD_LAST_REGION_TO_LIST()来添加,否则代码一定会出现内存溢出的惨状。
整个列表的语法为:
代码语言:javascript复制/*! 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的使用和技巧——什么进度条啊,滑动列表啊,菜单啊,统统都会安排上。如果你想一起追剧,就赶快搭建好测试平台吧。
原创不易,
如果你喜欢我的思维、觉得我的文章对你有所启发,
请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!
欢迎订阅 裸机思维