傻孩子(GorgonMeducer) · 2023年04月24日

【玩转Arm-2D】十一、酷炫汽车仪表盘是怎么实现的

image.png

【说在前面的话】

随着科技的发展,汽车也几乎达到了普及,家家都有辆小车,汽车上酷炫的仪表盘界面大家应该也不陌生,大致如下

image.png

你想知道这个仪表界面是怎么实现的吗?

image.png

哈哈,下面我们就从原理讲起一步一步实现这个酷炫的界面。在讲制作原理之前,有必要先说一下这个板子的软硬件配置,如下

image.png
配置讲完之后,就来看看我们是怎么实现这个酷炫的界面。

【表针的旋转】

首先,仪表盘中有一个表针,看到表针那肯定就需要旋转了,Arm-2D的旋转我们之前已经讲过了,不清楚的可以看下面这篇文章

【玩转Arm-2D】四、旋转与抗锯齿(美颜功能)

值得一提的就是之前我们讲的旋转圆心的坐标都是在素材里面,其实圆心也可以在素材的外面,如下图所示

image.png

当我们把表针的旋转圆心设置成(3,50)即在素材的外面,它就会沿着半径为50的圆进行旋转,和视频中看到的效果一样。

image.png

哈哈,当然没有这么简单。如果只让表针旋转,这样就可以了,但是,如果在资源紧张的单片机(比如m0的内核)中,实现表针的流畅旋转,还需要一些特殊的技巧,那就是我们之前讲的脏矩阵(即局部刷新)。

【脏矩阵的使用】

视频中,我们很容易发现表针的旋转区域为橙色半圆环的区域,如下图所示

image.png

所以我们只要刷新这片区域就可以了,不需要整个屏幕全刷。

那怎么把旋转区域设置成半圆环呢?

哈哈,这个是办不到的,因为Arm-2D的脏矩阵只能设置成矩形区域。

image.png

不过,有了这个矩形区域,我们就可以实现表针的流畅旋转了,因为我们可以动态地修改这个区域,如下图所示:

image.png

图中,表针从位置1运动到位置2,我们只需要把脏矩阵的区域设置成位置1和位置2这两个矩形区域就可以了。位置1的区域是为了擦除原来的表针,而位置2的区域就是绘制表针。这个就是视频中表针旋转非常流畅的原因,因为我们每次只刷了两块小区域。

image.png
那么,问题又来了,位置2的区域该怎么计算呢

这个也简单,因为我们旋转的时候是知道要旋转多少度角的,根据这个角度用三角函数sin和cos就可以计算出来了,如下图

image.png

我们只需要根据矩形的对角线坐标(x1,y1)、(x2,y2)还有圆的半径R和旋转角度θ 就可以计算出旋转后的坐标(x3,y3)、(x4,y4),知道了对角线坐标矩形区域就可以确定了。这个计算过程我们就不写了,相信大家都会(* ̄︶ ̄)

不写也不是为了偷懒,那是因为如果使用Arm-2D,这个动态更新的脏矩阵区域Arm-2D已经为我们计算好了,我只要简单地使用就可以了,下面就讲一下怎么用Arm-2D来动态更新旋转区域,让我们的旋转更流畅。

【如何用Arm-2D动态更新脏矩阵】

使用Arm-2D来实现视频中的旋转表针也很简单(官方已经提供了模板),如下图

image.png

然后添加Meter模板就可以,如下图所示

image.png

此时,我们的工程就会出现这两个文件,如下图

image.png

这时我们就把模板添加成功了,在主函数中调用就可以,如下

int main(){
    ...
    __arm_2d_scene_meter_init(   &DISP0_ADAPTER,  NULL);
    ...
}
  • 因为meter模板也是一个scene,所以和scene的使用是一样的。

到这里,视频中表针的旋转就实现了,是不是很简单。

transform helper的使用

不过我们在使用transform helper(为旋转或缩放提供辅助的服务)时,还有几点注意事项需要说一下

image.png

一、这里用 transform 的时候,一定要有一个独立的 OP,我们可以根据具体使用的transform类型在场景类中添加:


/*!
 * \brief a user class for scene meter
 */
typedef struct user_scene_meter_t user_scene_meter_t;

struct user_scene_meter_t {
    ...

ARM_PRIVATE(
    /* place your private member here, following two are examples */
    ...
    
    struct {
        /* transform 的 OP */
        arm_2d_op_fill_cl_msk_opa_trans_t tOP;
        arm_2d_helper_transform_t tHelper;
    } Pointer;
)
    /* place your public member here */
    
};

且一定要用 ARM_2D_OP_INIT 初始化一下,如下所示

 /* initialize op */
 ARM_2D_OP_INIT(this.Pointer.tOP);

二、初始化完OP后,接着就是初始化transform helper,程序如下

 /* initialize transform helper */
arm_2d_helper_transform_init(&this.Pointer.tHelper,
   (arm_2d_op_t *)&this.Pointer.tOP,
   0.01f,
   0.1f,
   &this.use_as__arm_2d_scene_t.ptDirtyRegion);
  • 其中0.01f是设置旋转角度的门限值,也就是旋转角度大于0.01f时才会重新绘制表针。这个在Arm-2D内部使用了双缓冲技术,不仅可以让用户在任意地方安全的更新角度,而且使得旋转效率进一步提升,真是为了旋转的性能无所不用其极啊(* ̄︶ ̄)
  • 同样的,0.1f是设置图片缩放的门限值,其内部也使用了双缓冲技术
  • 最后一个参数是传入脏矩阵列表,这里要特别注意传入的dirty region list至少要有一个元素(不能为空),否则会导致图片不能正确被旋转哦。比如:
ARM_NONNULL(1)
user_scene_meter_t *__arm_2d_scene_meter_init(   arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_meter_t *ptThis)
{
    bool bUserAllocated = false;
    assert(NULL != ptDispAdapter);

    /*! define dirty regions */
    IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)

        /* the dirty region for text display*/
        ADD_REGION_TO_LIST(s_tDirtyRegions,
            0  /* initialize at runtime later */
        ),

        /* add the last region:
         * it is the top left corner for text display
         */
        ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
            .tLocation = {
                .iX = 0,
                .iY = 0,
            },
            .tSize = {
                .iWidth = __GLCD_CFG_SCEEN_WIDTH__,
                .iHeight = 8,
            },
        ),

    END_IMPL_ARM_2D_REGION_LIST()
    ...
    

    /* initialize transform helper */
    arm_2d_helper_transform_init(&this.Pointer.tHelper,
                                 (arm_2d_op_t *)&this.Pointer.tOP,
                                 0.01f,
                                 0.1f,
                                 &this.use_as__arm_2d_scene_t.ptDirtyRegion);
    
    ...
}
  • 三、当我们使用完OP后(界面切换走),记得要把它释放掉,否则会导致内存泄漏,程序如下

static void __on_scene_meter_depose(arm_2d_scene_t *ptScene){
    ...
    /* depose op */
    ARM_2D_OP_DEPOSE(this.Pointer.tOP);
    ...
}

好,注意事项我们就讲完了。不过,还有一个重要的问题,那就是如何按照应用需求更新表针的角度呢?

其实也简单,借助 _arm_2d_helper_transform_update_value()_函数 我们可以在工程的任意位置来更新角度和缩放比例,但这里,我们偷懒就一起放在一帧开始绘制之前的事件处理程序里了,也就是___on_scene_meter_frame_start_,如下所示


static void __on_scene_meter_frame_start(arm_2d_scene_t *ptScene)
{
    /* 你的用来更新角度的代码,保存在iResult里上 */

    float fAngle = ARM_2D_ANGLE((float)iResult / 10.0f);   
    /* update helper with new values*/
    arm_2d_helper_transform_update_value(&this.Pointer.tHelper, 
        fAngle,
        1.0f);
     /* call helper's on-frame-begin event handler */
    arm_2d_helper_transform_on_frame_begin(&this.Pointer.tHelper);
}    
  • 定义一个变量fAngle 来存放要更新的角度值(注意是弧度)
  • 然后调用arm_2d_helper_transform_update_value函数更新角度。注意第3个参数为缩放倍数,1.0f即原图大小,不进行缩放。
  • 最后在绘制函数开始前调用arm_2d_helper_transform_on_frame_begin函数就可以了(这样才能确保角度和比例能正确更新)。

角度更新完成后,就可以调用旋转函数对表针进行旋转了,如下所示

static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_meter_handler)
{
    ...
    /* draw pointer */
    arm_2dp_fill_colour_with_mask_opacity_and_transform(
        &this.Pointer.tOP,
        &c_tilePointerMask,
        ptTile,
        NULL, //&__centre_region,
        s_tPointerCenter,
        this.Pointer.tHelper.fAngle,
        this.Pointer.tHelper.fScale,
        GLCD_COLOR_RED,
        255);

    arm_2d_helper_transform_update_dirty_regions(
        &this.Pointer.tHelper,
        bIsNewFrame);

    arm_2d_op_wait_async((arm_2d_op_core_t *)&this.Pointer.tOP);
}
  • 我们使用arm_2dp_fill_colour_with_mask_opacity_and_transform函数对表针进行旋转。
  • 旋转完成后记得调用arm_2d_helper_transform_update_dirty_regions函数来更新脏矩阵,此函数必须放在自己所要服务的 transform 操作函数的后面哦。

【修改图片资源

当然,如果你觉得官方的背景图片不好看(或者有更酷的),也可以改成自己喜欢的背景,让仪表盘界面显示更炫酷一些。

那该怎么修改模板例子中的背景图片呢?

其实也简单,官方已经帮我们提供了宏,只要替换掉这个宏就可以,如下所示

#define c_tileMeterPanel         c_tileMeterPanelRGB565

只要把 c_tileMeterPanelRGB565替换成自己喜欢的背景tile就可以,简单替换后的效果如下

image.png

那表针可以替换吗?

答案是肯定的:我们只需要定义一个叫做c_tilePointerMask的宏, 并把它跟我们自己的指针关联起来就行:


/* 背景图片的宏 */
#define c_tileMeterPanel         c_tileMeterPanelRGB565

/* 指针Mask的宏 */
#define c_tilePointerMask        c_tileMyPointerMask

我们把c_tilePointerMask替换成我们自己的表针素材就可以了,如果要更改指针的旋转半径,可以修改__arm_2d_scene_meter_init()的代码:

ARM_NONNULL(1)
user_scene_meter_t *__arm_2d_scene_meter_init(   arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_meter_t *ptThis)
{
    ...
    /* initialize op */
    ARM_2D_OP_INIT(this.Pointer.tOP);

    /* initialize transform helper */
    arm_2d_helper_transform_init(&this.Pointer.tHelper,
                                 (arm_2d_op_t *)&this.Pointer.tOP,
                                 0.01f,
                                 0.1f,
                                 &this.use_as__arm_2d_scene_t.ptDirtyRegion);
    

    s_tPointerCenter.iX = c_tilePointerMask.tRegion.tSize.iWidth >> 1;
    
    
    /* 我们在这里更新指针的旋转半径 */
    s_tPointerCenter.iY = 100; /* radius */
    
    ...
}

那要是多个图片旋转效果怎么样呢?

【制作手表界面】

哈哈,上面的仪表界面只有一个表针(图片)在旋转,其实我们也可以实现多个表针的旋转,比如有时、分、秒的表盘界面。只要使用transform helper就可以很方便地实现动态脏矩阵,让我们的表针旋转更流畅。

        贴心的官方也提供了表盘的显示界面给我们测试,我们只要添加watch模板就可以,如下图

image.png

在syd8810单片机上的运行效果如下

image.png
怎么样,使用了脏矩阵,运行效果还是很流畅吧。

【小结】

使用Arm-2D实现图片的旋转很简单,在旋转时加入动态脏矩阵也很简单(有transform helper的帮助),即有几个要旋转的图片分别定义几个OP与之对应就可以,需要注意的就是OP定义完一定要用 ARM_2D_OP_INIT 初始化一下,并且用完了要用 ARM_2D_OP_DEPOSE 进行释放(申请的资源要释放掉)。

如果你在开发界面时想要自己手动调试脏矩阵区域,官方也给我们提供了一个调试的黑科技,就是可以把我们的脏矩阵区域显示出来方便我们观察设置的区域是否正确,显示脏矩阵区域也很简单,如下图所示

image.png

到此,今天的文章就讲完了,如果大家对仪表界面感兴趣,那就赶快动手试试吧,添加官方的模板文件来测试是真的非常简单哦(* ̄︶ ̄)


原文:裸机思维
作者:GorgonMeducer 傻孩子

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1480
内容数
118
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息