傻孩子(GorgonMeducer) · 2021年05月31日

【教程更新】Arm-2D的公开课你错过了么?

首发:裸机思维
作者: GorgonMeducer 傻孩子

image.png

【说在前面的话】


最近受到极术社区的邀请,我有幸为大家献上了一期名为“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指定区域传送位图的函数,其原型如下:


/**
  \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个参数之间的关系如下图所示:

image.png
简单来说,这个函数就是把 _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库:

访问网址:

https://github.com/ARM-software/EndpointAI

或者在【裸机思维】公众号中发送关键字“arm-2d”获取对应压缩包。

image.png

需要说明的是,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\"

image.png

将Arm-2D目录整体拷贝出来,放置到你的目标工程目录下,比如:

image.png

2、将Arm-2D添加到MDK工程中

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

image.png

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

image.png

3、配置编译环境

将“Arm-2D/Library/Include”和“Arm-2D/Helper/Include”添加到Include搜索路径列表里:

image.png

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

image.png

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

image.png

此外,由于Arm-2D依赖CMSIS ,可以通过配置MDK的RTE的方式来获得最新版本CMSIS的支持。

1、如下图所示,通过工具栏最右边的按钮打开Pack Installer

image.png

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

在右半部分的Packs选项卡中,找到ARM::CMSIS,确保它显示“Up to date”,如果没有就单击对应的按钮进行更新。目前我使用的版本就是5.7.0。


如果你的MDK版本较老,同时因为某些原因又不想更新MDK版本,可以通过Pack Installer导入仓库的办法获取最新的CMSIS。具体步骤如下:

1、通过git工具将最新版本的CMSIS从https://github.com/ARM-softwa...\_5 的_develop_ 分支下载到本地。比如,我使用的工具就是Github Desktop:
image.png

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

image.png

3、单击Add,并把刚刚从Github上获取的CMSIS加入仓库中:
image.png
4、成功后,我们会看到最新的CMSIS已经被加入到Pack列表中了:

image.png

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

image.png


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

image.png

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

image.png

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

image.png

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

在工程管理器中,找到CMSIS,在右键的弹出菜单中选择“Options for Component Class 'CMSIS'”:
image.png

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

image.png

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

image.png


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

image.png

仔细对比,你会发现,不光勾选的颜色是灰色的,其背景色也不是白色——这表示它会根据用户总体的工程设置来编译CMSIS-DSP。


至此,我们就应该能够成功的完成编译了。

image.png

仔细想想,部署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( 
            <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 = &<图形面绘制函数>, 
                },
            }
        ) < 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的系统来说,用户就需要对上述过程做一个修改:

  1. 在 _\_\_pfb\_render\_handler()_ 中向DMA发送刷新请求;
  2. 当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;
}

在这个例子中,我们简单的实现了一个半透明浮动窗口的效果,(这篇文章实在太长了,就简单做个例子凑个数吧):
image.png

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的使用和技巧——什么进度条啊,滑动列表啊,菜单啊,统统都会安排上。如果你想一起追剧,就赶快搭建好测试平台吧。

相关链接:


专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。
推荐阅读
关注数
1480
内容数
118
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息