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

2021-07-06 15:51:24 浏览数 (1)

【说在前面的话】


最近受到极客社区的邀请,我有幸为大家献上了一期名为“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)和体积信息(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)

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),则需要打开对C11GNU扩展的支持,即直接在"Language C"中选择“gnu11”:

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

此外,由于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”,比如:

代码语言: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 = &<图形面绘制函数>, 
                },
            }
        ) < 0) {
        //! error detected
        assert(false);
    }

比如,一个典型的例子是:

代码语言: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_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() 就是我们绘制图形界面的函数:

代码语言:javascript复制
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,因此使用方法非常灵活,例如:

代码语言: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_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()我们可以几乎毫无限制的向列表中添加任意数量的区域,其语法为:

代码语言: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消耗,也可以在需要的情况下建立允许动态修改内容的列表。

0 人点赞