【说在前面的话】
裸机思维的老读者们想必对Arm-2D已经不再陌生,通过前面一系列文章,我们知道:
- Arm-2D的本质是一个针对微控制器GUI生态的“显卡驱动”
- 它为原本玩不起GUI的小资源MCU(64K Flash 4K SRAM)用“低帧率”换取“低资源消耗”提供了可能
- 我们可以借助Arm-2D轻松整活儿
- 做出漂亮的水印效果
- 实现不规则窗体
- 制作剪影风格的界面
- 用 极小的资源资源实现任意大小的圆角矩形界面
- 用极小资源实现酷炫的动态进度条
- 显示文字
更不用说:
- 制作拥有多层景深效果的横版过关游戏
- 实现酷炫的智能手表表盘
甚至,即便是作为懒汉的你,也能从众多方案中找到一种直接尝试Arm-2D例程的方法。
从2021年3月31日在Github上公开算起,Arm-2D已经从青涩中逐渐成熟(从0.9.x版本一路进化到 1.0.0-preview)、从寄人篱下到自立门户(拥有了不久即将公开的独立的仓库 https://github.com/ARM-software/Arm-2D)。
在部署方式上,经过社区的大量反馈和测试,终于来到了“点几下鼠标”就能轻松部署的时代——如何使用CMSIS-Pack在三分钟内将 Arm-2D 部署到位,就是本文将要介绍的主要内容。
【准备工作】
准备一个已有的工程,确保该工程已经能够实现基础的LCD初始化,并能提供一个向LCD指定区域传送位图的函数,其原型如下:
代码语言:javascript复制
void Disp0_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)
void Disp0_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;
}
}
【如何获取安装包】
在 MDK 中部署 Arm-2D的第一步是获取对应的 cmsis-pack,对于可以流畅访问 Github 的朋友来说,通过下面的网址直接找到最新的 .pack 文件是最直接的方式:
- 目前(在Arm-2D的专用仓库公开前),使用网址
https://github.com/ARM-software/EndpointAI/tree/master/Kernels/Research/Arm-2D/cmsis-pack
- 未来,使用网址:
https://github.com/ARM-software/Arm-2D/tree/main/cmsis-pack
为了方便国内用户,对于无法访问Github的朋友来说,可以在关注公众号【裸机思维】后发送关键字“arm-2d”来获取网盘链接。
获得 Arm-2D 的cmsis-pack后,可以直接双击进行安装:
如果你手上的MDK是较新的版本,比如 5.36 或者更新,一般无脑Next安装即可。反之,则强烈推荐你安装最新的 MDK 以避免一系列不必要的麻烦——这也是社区中很多前人用血泪史总结出的共同建议——如果一开始就用最新的 MDK 就可以节省大量的时间。
【傻瓜部署教程】
Arm-2D 的部署不可谓不简单,基本可以通过在 RTE 中勾选对应选项完成大部分工作。具体请参考下面的手把手教程吧:
步骤一:加入组件
在 MDK 工程中依次选择 Project -> Manage -> Run-Time Environment 来打开 RTE 配置窗口:
其实,也可以通过工具栏中的快捷按钮来实现同样的目标:
在 窗口中找到 Acceleration,并将其展开:
如图所示,勾选以下组件:
- Core:Arm-2D的核心(必选)
- Alpha-Blending:大部分与透明度相关的操作,比如基于蒙版的拷贝、透明图层合成、透明色块填充等等
- Transform:旋转、缩放等操作(支持蒙版、抠图和透明度)
为了简化我们与LCD显示驱动进行对接,需要勾选以下服务:
- FPB:Partial Frame-Buffer模块,支持部分刷新的核心组件
- Display Adapter:一个使用 PFB 来适配 LCD 底层驱动的代码模板,帮我我们快速在上层绘图和底层LCD刷新之间建立桥梁。一般来说 Display Adapter 与 屏幕是一一对应关系:如果你有一块屏幕,这里就选“1”,如果你有两块屏幕,这里就选“2”,以此类推。
由于 Display Adapter 依赖了一些额外(Extra)的模块,因此,如果你只勾选了上述的部分,你会在窗口中看到橙色的警告:
这里警告的含义是说:Display Adapter依赖了模块 Controls 和 LCD ASCII Printf,但你没有勾选它们。简单的单击左下角的 Resolve 按钮,RTE会自动帮你勾选上所依赖的模块。
单击“OK” 按钮完成组件的添加。
为了方便后续的开发,强烈推荐下载并安装 perf_counter 模块,具体步骤请参考文章《【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱》,这里简述下关键步骤:
- 关注【裸机思维】公众号后发送关键字“perf_counter”来获取对应的 cmsis-pack
- 下载并安装 cmsis-pack
- 打开RTE后找到 Utilities,并勾选 perf_counter 中的 Core,推荐以 Library形式进行部署
- 如果出现橙色警告,单击Resolve按钮来解决
步骤二:配置编译环境
如果你使用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)。
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的源代码进行额外的设置。
至此,我们就应该能够成功的完成编译了。
步骤三:模块配置
在工程管理器中展开 Acceleration,并找到新加入的显示驱动适配器文件(arm_2d_disp_adapter_0.h)
双击打开后,在编辑器的左下角选择 Configuration Wizard
进入图形配置界面:
根据你的屏幕填写正确的信息:
- 颜色位数(Screen Colour Depth)
- 横向分辨率(Width of the screen)
- 纵向分辨率(Height of the Screen)
- 部分刷新缓冲块的宽度(Width of the PFB Block),一般优先考虑为整行或者1/2行像素的宽度
- 部分刷新缓冲块的高度(Height of the PFB Block),一般推荐为1/10屏幕像素高度。在RAM较为紧缺时,考虑8或者1。
- 进行帧率计算时,平均多少帧做一次数据更新(Number of iterations),默认是30,选0将关闭帧率实时计算功能。
保存后关闭窗口。
在Acceleration中找到 arm_2d_cfg.h,
同样打开它的 Configuration Wizard 图形配置界面:
由于我们用到了 Extra中的一些模块,比如 Contols 和 LCD ASCII Printf,因此需要提供对应的信息:比如屏幕的颜色位数、分辨率和 printf 打印行缓冲的大小(默认值是64个ASCII字符)。
对于 Arm-2D General Configuration 中的一些选项,这里有必要做一下说明:
- Enable Asynchronous Programmers' model support:目前推荐关闭
- Enable anti-alias support for all transform operations:使能旋转、缩放操作的抗锯齿功能
- Enable Support for accessing individual Colour channels:当你的目标屏幕是 RGB888,而你又需要支持 PNG 图片时推荐打开。
Patches for improving performance 中都是一些通过Hack掉某些可能用不到的特性或者功能来换取性能的选项。推荐做法是:
- 不要勾选
- 在完成应用后依次尝试:如果对应用没有明显的影响,就勾选以换取一定的性能提升。
步骤四:添加代码
在 main() 函数所在的源代码文件中包含头文件:
代码语言:javascript复制#include "arm_2d.h"
并在 main()函数中完成对 arm-2d 的初始化:
代码语言:javascript复制int main(void)
{
system_init(); // 包括 LCD 在内的系统初始化
...
arm_irq_safe {
arm_2d_init(); // 初始化 arm-2d
}
...
while(1) {
...
}
}
以上就完成了对 arm-2d 模块的初始化。
如果你使用了 Display Adapter 来辅助移植,则还需要包含下面的头文件:
代码语言:javascript复制#include "arm_2d_disp_adapter_0.h"
并在 main() 函数中加入代码:
代码语言:javascript复制int main(void)
{
system_init(); // 包括 LCD 在内的系统初始化
...
arm_irq_safe {
arm_2d_init(); // 初始化 arm-2d
}
// 初始化 Display Adapter 0
disp_adapter0_init();
while (true) {
...
// 执行 Display Adapter 的刷新任务
disp_adapter0_task();
...
}
}
此外,由于 Display Adapter 需要用户提供性能测量相关的服务,这里我们通过 perf_counter 来提供,因此需要插入如下的代码:
代码语言:javascript复制#include "perf_counter.h"
...
static volatile int64_t s_lTimeStamp;
__OVERRIDE_WEAK
void arm_2d_helper_perf_counter_start(void)
{
s_lTimeStamp = get_system_ticks();
}
__OVERRIDE_WEAK
int32_t arm_2d_helper_perf_counter_stop(void)
{
return (int32_t)(get_system_ticks() - s_lTimeStamp);
}
编译、下载,如果一切顺利,你应该可以在屏幕上看到类似如下的画面:
屏幕当中是一个不停旋转的“载入圈”,屏幕底部是当前的帧率信息——这里,FPS后面的数字表示“绘图”的帧率(冒号后面的数字是绘图所消耗的时间),而只有把 LCD 刷新所消耗的时间(LCD Latency)与绘图所消耗的时间加在一起,才能计算出实际帧率。这种分开显示的方式完全是为了方便我们寻找性能瓶颈而准备的。
至此,我们就完成了裸机环境下整个 Arm-2D的部署。
【如何开始玩耍?】
相信很多小伙伴看到这里在高兴之余,其实也是一脸懵逼的:
我该怎么玩?
这里的文字怎么打印出来的?
圆角矩形怎么画出来的?
死亡小圈圈怎么画出来的?
我自己的界面该怎么办?
这里设计的内容就太多了,完全值得为之专门再写一篇文章。但在那之前,请允许我给聪明的小伙伴提供一点思路和提示:
- 在 Acceleration 中可以找到 arm_2d_disp_adapter_0.c ,打开之后可以找到使用 arm-2d 的关键代码
- 背景是那种只绘制一次以后不再改变的部分,你可以在下面这个函数绘制背景:
static
IMPL_PFB_ON_DRAW(__pfb_draw_background_handler)
{
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
...
}
- 前景是哪种内容不断改变的部分,你可以在下面这个函数中绘制前景:
static
IMPL_PFB_ON_DRAW(__pfb_draw_handler)
{
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
...
}
- 上述两个函数都涉及到两个重要参数 ptTile 和 bIsNewFrame:
- ptTile 其实就是虚拟屏幕,是我们进行2D操作的目标Tile(Target Tile)
- 由于上述两个函数在完成一帧的绘制之前,际上会被重复调用多次,因此bIsNewFrame 用于指示绘制一帧内容时的第一次调用——我们一般通过检测这个标志为true 的时候才允许调整各类绘制参数(比如位置和大小之类的)——如若不然,则一定会出现图像撕裂的问题。
- 前景与背景不同的地方在于:它只刷新指定的部分。而指定哪些部分要刷新,是通过脏矩阵来实现的。关于脏矩阵的定义,可以在函数disp_adapter0_init() 中找到:
void disp_adapter0_init(void)
{
...
do {
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, const static)
/* a region for the busy wheel */
ADD_REGION_TO_LIST(s_tDirtyRegions,
.tLocation = {
.iX = ((__DISP0_CFG_SCEEN_WIDTH__ - 100) >> 1),
.iY = ((__DISP0_CFG_SCEEN_HEIGHT__ - 100) >> 1),
},
.tSize = {
.iWidth = 100,
.iHeight = 100,
},
),
/* a region for the status bar on the top of the screen */
ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
.tLocation = {
.iX = 0,
.iY = __DISP0_CFG_SCEEN_HEIGHT__ - 9},
.tSize = {
.iWidth = __DISP0_CFG_SCEEN_WIDTH__,
.iHeight = 9,
},
),
END_IMPL_ARM_2D_REGION_LIST()
static const arm_2d_scene_t c_tBenchmarkScene[] = {
[0] = {
.fnBackground = &__pfb_draw_background_handler,
.fnScene = &__pfb_draw_handler,
.ptDirtyRegion = (arm_2d_region_list_item_t *)s_tDirtyRegions,
...
},
};
arm_2d_user_scene_player_append_scenes(
&DISP0_ADAPTER,
(arm_2d_scene_t *)c_tBenchmarkScene);
} while(0);
}
在这个例子中,代码定义了两个区域:一个是屏幕正中央一块 100*100 的区域,以及屏幕底部一个高度为9像素的条状区域(可以用于状态信息的显示)——最终的效果是,每次使用PFB进行刷新,这两个区域以外的部分都会被跳过(保持不变),从而节省了大量的处理时间,客观上提高了用户实际可见的帧率。
借助这一范例很容易发现:通过宏 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消耗,也可以在需要的情况下建立允许动态修改内容的列表。
- 与 LCD 底层刷新有关的操作在函数 __glcd0_pfb_render_handler中进行:
__WEAK
IMPL_PFB_ON_LOW_LV_RENDERING(__glcd0_pfb_render_handler)
{
const arm_2d_tile_t *ptTile = &(ptPFB->tTile);
ARM_2D_UNUSED(pTarget);
ARM_2D_UNUSED(bIsNewFrame);
Disp0_DrawBitmap(ptTile->tRegion.tLocation.iX,
ptTile->tRegion.tLocation.iY,
ptTile->tRegion.tSize.iWidth,
ptTile->tRegion.tSize.iHeight,
(const uint8_t *)ptTile->pchBuffer);
arm_2d_helper_pfb_report_rendering_complete(
&DISP0_ADAPTER.use_as__arm_2d_helper_pfb_t,
(arm_2d_pfb_t *)ptPFB);
}
仔细观察容易发现,该函数实际上一前一后分别调用了 Disp0_DrawBitmap() 和 arm_2d_helper_pfb_report_rendering_complete() 函数。如果你的底层刷新使用了 DMA来辅助,则可以在任意其它c源文件中通过 __OVERRIDE_WEAK 关键字来覆盖这个默认的实现,比如:
代码语言:javascript复制static volatile struct {
arm_2d_helper_pfb_t *ptHelper;
arm_2d_pfb_t *ptPFB;
} DMA_Helper = {NULL, NULL};
__OVERRIDE_WEAK
IMPL_PFB_ON_LOW_LV_RENDERING(__glcd0_pfb_render_handler)
{
const arm_2d_tile_t *ptTile = &(ptPFB->tTile);
ARM_2D_UNUSED(pTarget);
ARM_2D_UNUSED(bIsNewFrame);
// 启动 DMA 传输
Disp0_DrawBitmap(ptTile->tRegion.tLocation.iX,
ptTile->tRegion.tLocation.iY,
ptTile->tRegion.tSize.iWidth,
ptTile->tRegion.tSize.iHeight,
(const uint8_t *)ptTile->pchBuffer);
DMA_Helper.ptHelper = &DISP0_ADAPTER.use_as__arm_2d_helper_pfb_t;
DMA_Helper.ptPFB = ptPFB;
}
// DMA 传输完成处理程序
void DMA_Handler(void)
{
// 报告 PFB helper 操作完成
arm_2d_helper_pfb_report_rendering_complete(
DMA_Helper.ptHelper,
DMA_Helper.ptPFB);
}
值得注意的是,这里,我们在__glcd0_pfb_render_handler中发起DMA异步传输请求,在DMA的传输完成中断中调用arm_2d_helper_pfb_report_rendering_complete()来报告异步刷新过程结束。
在使用DMA来进行辅助刷新的时候,推荐将 PFB 池中的PFB数量修改为2或者3,以获得双缓冲的效果:
代码语言:javascript复制static void __user_scene_player_init(void)
{
memset(&DISP0_ADAPTER, 0, sizeof(DISP0_ADAPTER));
//! initialise FPB helper
if (ARM_2D_HELPER_PFB_INIT(
&DISP0_ADAPTER.use_as__arm_2d_helper_pfb_t, //!< FPB Helper object
__DISP0_CFG_SCEEN_WIDTH__, //!< screen width
__DISP0_CFG_SCEEN_HEIGHT__, //!< screen height
uint16_t, //!< colour date type
__DISP0_CFG_PFB_BLOCK_WIDTH__, //!< PFB block width
__DISP0_CFG_PFB_BLOCK_HEIGHT__, //!< PFB block height
// PFB 池中 PFB的数量
2, //!< number of PFB in the PFB pool
{
.evtOnLowLevelRendering = {
//! callback for low level rendering
.fnHandler = &__glcd0_pfb_render_handler,
},
},
//.FrameBuffer.bSwapRGB16 = true,
) < 0) {
//! error detected
assert(false);
}
}
【跑个Benchmark看看?】
也许你已经注意到了,在 RTE 中,Arm-2D 还提供了两个 Benchmark选项:
这里:
- Benchmark: Generic 是 Arm-2D 的综合 Benchmark,用于评估目标系统的综合2D图形性能,是一个重要的参考指标。
- Benchmark: WatchPanel 是 Arm-2D 的表盘 Benchmark,专门用于评估智能手表一类应用的2D图形性能
值得注意的是:这两个Benchmark所使用的图片素材文件都挺大的,没有个512K Flash就别考虑了,而且目前也仅支持 RGB565 的屏幕。
如果你的目标芯片确实有足够的 Flash 来运行这两个 Benchmark,那么在RTE选中后,需要在 main() 函数中添加如下的代码:
代码语言:javascript复制#include "arm_2d_benchmark.h"
#include "perf_counter.h"
...
static volatile int64_t s_lTimeStamp;
__OVERRIDE_WEAK
void arm_2d_helper_perf_counter_start(void)
{
s_lTimeStamp = get_system_ticks();
}
__OVERRIDE_WEAK
int32_t arm_2d_helper_perf_counter_stop(void)
{
return (int32_t)(get_system_ticks() - s_lTimeStamp);
}
int main (void)
{
system_init(); // 包括 LCD 在内的系统初始化
arm_irq_safe {
arm_2d_init();
}
// 运行在 RTE中选中的 benchmark
arm_2d_run_benchmark();
...
}
这里,arm_2d_run_benchmark() 就是核心代码,头文件 arm_2d_benchark.h 提供了它的函数原型。它是一个有去无回的函数,因此 main() 函数中在它之后的代码都没有实际意义。
在开始编译之前,我们还需要打开 arm_2d_cfg.h,为 benchmark 提供必要的信息:
除了此前就已经配置好的屏幕信息外,还需要为 Benchmark 配置对应的 PFB 尺寸——当然是尺寸越大帧率越高啦。此外,Number of iterations 用来选择使用“多少帧的性能信息做平均”来计算跑分结果,推荐保持默认值1000即可。
完成配置后,就可以编译啦。如果你的编译器提示找不到函数GLCD_DrawBitmap(),不要奇怪——因为 benchmark 也不知道你要测量哪个屏幕的性能,因此它需要用户提供一个底层刷新函数,与前面的 Disp0_DrawBitmap() 相同,当你只有一块屏幕时,我们可以通过下面的代码来解决问题:
代码语言:javascript复制void GLCD_DrawBitmap ( uint32_t x,
uint32_t y,
uint32_t width,
uint32_t height,
const uint8_t *bitmap) {
Disp0_DrawBitmap(x,y,width, height, bitmap);
}
编译下载,大功告成!
由于要运行 1000 帧后才能得出结果,因此需要耐心等一段时间(如果你等候的时间过长仍然没有获得结果,可以检查下你的栈是否太小,为了保险起见,起码设置 0x800)
【常见问题】
问题一:安装 CMSIS-Pack 时失败
如果,你在安装 cmsis-pack 的时候就会遇到如下所示的问题:
不要慌,通常安装最新MDK、且避免修改默认安装目录就可以解决。怕麻烦的小伙伴可以在关注【裸机思维】后发送关键字“MDK”获取最新MDK的网盘链接。
值得强调的是,MDK为开源社区提供了 Community 版本,除了不能商用,几乎没有任何限制(对芯片、代码尺寸、调试均没有限制)。Community 版本的本质是一种 License,使用的安装文件与其它版本并无不同。对这一“官方白嫖版”感兴趣的小伙伴可以通过下面的链接来获取:
https://www.keil.arm.com/mdk-community/
如果你的运气特别差,安装了最新MDK也无法解决上述问题,还可以通过Pack-Installer的导入功能最后“搏一搏”——打开 Pack Installer 后依次单击 File->Import:
在弹出窗口中选中下载获得的 cmsis-pack 进行安装。
如果还不能解决,请确认你的 MDK 是否安装在默认的安装目录下(C:Keil_v5),如果不是,尝试重新安装到默认目录下。
再不行……再不行就换台电脑吧。
问题二:编译时报告与 ARM_PRIVATE() 相关的错误
这类问题是由于你的 MDK 工程中存在独立的 CMSIS,且该 CMSIS 与 RTE中所添加的 CMSIS 存在冲突(工程中的 CMSIS 版本过于老旧),具体解决方案请参考文章《CMSIS玩家的“阴间成就”指南》,这里就不再赘述。
此外,要检查你是否正确开启了 GNU 扩展和对应的C标准(Arm Compiler 5要开启 C99,Arm Compiler 6要开启 gnu99)
问题三:提示找不到__aeabi_assert
这是由于我们在工程中选择了 microLib,而 microLib 没有为 assert.h 提供底层实现导致的。添加如下代码即可:
代码语言:javascript复制#include "arm_2d.h"
#include "cmsis_compiler.h"
#if defined(__MICROLIB)
void __aeabi_assert(const char *chCond, const char *chLine, int wErrCode)
{
ARM_2D_UNUSED(chCond);
ARM_2D_UNUSED(chLine);
ARM_2D_UNUSED(wErrCode);
while(1) {
__NOP();
}
}
#endif
问题四:提示找不到 Disp0_DrawBitmap
当你选择 Display Adapter 服务时,需要用户提供一个向 LCD 刷新数据的函数。当你有多个屏幕时,需要在 RTE 里为 Display Adapter 选择对应的数量:
此时,我们可以在 Acceleration 中看到添加的代码文件:
注意到这里每个文件后面都有一个对应的数字,指代对应的 Display Adapter 模板。而每个 Display Adapter 都需要一个属于自己的底层刷新函数:Dispn_DrawBitmap(),具体请参考本文的【准备工作】章节。
问题五:出现Hardfault
检查栈的大小,推荐在 0x800(2K)以上为易。
【说在后面的话】
只要你安装好了 arm-2d 的cmsis-pack,并准备好了LCD底层驱动函数 Disp0_DrawBitmap() (记得事先测试满足要求),那么整个Arm-2D的部署工作几乎可以在3分钟之内轻松完成。
本文看起来这么长,实际上是因为包含了大量“解惑”的讲解和很多避免用户踩坑的步骤细节。当你跟着教程成功做过一遍以后,下次再部署时就可谓轻车熟路了——如果可能,尽可能使用最新的MDK来尝试,这是社区中众多前辈的吐血推荐,他可以让你避免90%的坑,从而避免大量不必要的时间浪费。
新版的 cmsis-pack 除了简化用户部署外,还引入了一个方便裸机用户开发简易 GUI 应用的服务:场景播放器(scene player)——它允许我们将界面拆分成若干场景:
- 每个场景都由(可选的)背景和前景组成
- 用户可以
- 事先设定好一连串场景然后依次切换
- 也可以在运行时刻通过API追加(Append)新的场景
- 场景的切换会自动避免帧撕裂的问题
虽然 arm_2d_disp_adapter_0.c 已经为我们演示了场景播放器的使用,但为了降低大家的学习门槛,我将在下一篇文章中详细为大家介绍这种“基于场景”的低成本GUI设计方式。
尽情期待。