【喂到嘴边了的模块】准备徒手撸GUI?用Arm-2D三分钟就够了

2022-07-30 13:53:49 浏览数 (1)

【说在前面的话】


裸机思维的老读者们想必对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)和体积信息(widthheight)共同确定。

很多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-2Dcmsis-pack后,可以直接双击进行安装:

如果你手上的MDK是较新的版本,比如 5.36 或者更新,一般无脑Next安装即可。反之,则强烈推荐你安装最新的 MDK 以避免一系列不必要的麻烦——这也是社区中很多前人用血泪史总结出的共同建议——如果一开始就用最新的 MDK 就可以节省大量的时间。

【傻瓜部署教程】


Arm-2D 的部署不可谓不简单,基本可以通过在 RTE 中勾选对应选项完成大部分工作。具体请参考下面的手把手教程吧:

步骤一:加入组件

在 MDK 工程中依次选择 Project -> Manage -> Run-Time Environment 来打开 RTE 配置窗口:

其实,也可以通过工具栏中的快捷按钮来实现同样的目标:

在 窗口中找到 Acceleration,并将其展开:

如图所示,勾选以下组件:

  • CoreArm-2D的核心(必选)
  • Alpha-Blending:大部分与透明度相关的操作,比如基于蒙版的拷贝、透明图层合成、透明色块填充等等
  • Transform:旋转、缩放等操作(支持蒙版、抠图和透明度)

为了简化我们与LCD显示驱动进行对接,需要勾选以下服务:

  • FPBPartial Frame-Buffer模块,支持部分刷新的核心组件
  • Display Adapter:一个使用 PFB 来适配 LCD 底层驱动的代码模板,帮我我们快速在上层绘图和底层LCD刷新之间建立桥梁。一般来说 Display Adapter 与 屏幕是一一对应关系:如果你有一块屏幕,这里就选“1”,如果你有两块屏幕,这里就选“2”,以此类推。

由于 Display Adapter 依赖了一些额外(Extra)的模块,因此,如果你只勾选了上述的部分,你会在窗口中看到橙色的警告:

这里警告的含义是说:Display Adapter依赖了模块 ControlsLCD ASCII Printf,但你没有勾选它们。简单的单击左下角的 Resolve 按钮,RTE会自动帮你勾选上所依赖的模块。

单击“OK” 按钮完成组件的添加。


为了方便后续的开发,强烈推荐下载并安装 perf_counter 模块,具体步骤请参考文章《【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱》,这里简述下关键步骤:

  1. 关注【裸机思维】公众号后发送关键字“perf_counter”来获取对应的 cmsis-pack
  2. 下载并安装 cmsis-pack
  3. 打开RTE后找到 Utilities,并勾选 perf_counter 中的 Core,推荐以 Library形式进行部署
  4. 如果出现橙色警告,单击Resolve按钮来解决

步骤二:配置编译环境

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

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

此外,由于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,并勾选上COREDSP。这里需要注意的是,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中的一些模块,比如 ContolsLCD 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掉某些可能用不到的特性或者功能来换取性能的选项。推荐做法是:

  1. 不要勾选
  2. 在完成应用后依次尝试:如果对应用没有明显的影响,就勾选以换取一定的性能提升。

步骤四:添加代码

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 的关键代码
  • 背景是那种只绘制一次以后不再改变的部分,你可以在下面这个函数绘制背景:
代码语言:javascript复制
static
IMPL_PFB_ON_DRAW(__pfb_draw_background_handler)
{
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    ...
}
  • 前景是哪种内容不断改变的部分,你可以在下面这个函数中绘制前景:
代码语言:javascript复制
static
IMPL_PFB_ON_DRAW(__pfb_draw_handler)
{
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    ...
}
  • 上述两个函数都涉及到两个重要参数 ptTilebIsNewFrame
    • ptTile 其实就是虚拟屏幕,是我们进行2D操作的目标Tile(Target Tile)
    • 由于上述两个函数在完成一帧的绘制之前,际上会被重复调用多次,因此bIsNewFrame 用于指示绘制一帧内容时的第一次调用——我们一般通过检测这个标志为true 的时候才允许调整各类绘制参数(比如位置和大小之类的)——如若不然,则一定会出现图像撕裂的问题。
  • 前景与背景不同的地方在于:它只刷新指定的部分。而指定哪些部分要刷新,是通过脏矩阵来实现的。关于脏矩阵的定义,可以在函数disp_adapter0_init() 中找到:
代码语言:javascript复制
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()

这里,“列表名称”实际上就是列表的变量名,而“列表变量的修饰” 则是大家熟悉的类型修饰符,比如 staticconst 一类——正确使用修饰符既可以节省RAM消耗,也可以在需要的情况下建立允许动态修改内容的列表。

  • 与 LCD 底层刷新有关的操作在函数 __glcd0_pfb_render_handler中进行:
代码语言:javascript复制
__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设计方式

尽情期待。

0 人点赞