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

文摘   2024-04-15 11:56   英国  

【说在前面的话】

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

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

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

单片机
syd8810(m0内核)主频64M
屏幕
240*240像素、spi接口
Arm-2D的PFB大小

240*30

MDK编译优化等级
-Omax

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

【表针的旋转】

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

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

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

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

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

【脏矩阵的使用】

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

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

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

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

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

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

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

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

【从Arm-2D提供的例子模板开始】

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

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

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

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

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

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

transform helper的使用

模板中让指针流畅运动的关键是一个名为transform helper的服务,它以极傻瓜的方式实现了前面文中介绍过的“跟着指针走的动态脏矩阵”关于这个Transform Helper 几点注意事项需要说一下:

一、这里用 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是设置图片缩放的门限值,其内部也使用了双缓冲技术

  • 最后一个参数是传入脏矩阵列表,一般就是当前scene的ptDirtyRegion 指针。

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);    ...}


  • 四、在场景模板的 __on_scene_xxxx_start() 处理程序的最后插入transform_helper 对应的时间处理程序 arm_2d_helper_transform_on_frame_begin() 例如:

static void __on_scene_meter_frame_start(arm_2d_scene_t *ptScene){    user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;        ...
/* call helper's on-frame-begin event handler */ arm_2d_helper_transform_on_frame_begin(&this.Pointer.tHelper);}


注意事项我们就讲完了,那么如何按照应用需求更新表针的角度呢?借助 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即原图大小,不进行缩放。


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

staticIMPL_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 操作函数的后面哦。


【修改 meter scene模板的图片资源

当我们想直接修改官方提供的 meter 场景模板来实现自己的界面时,通常需要更换背景图片——好让仪表盘界面显示更炫酷一些。

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

模板中针对不同颜色格式为宏c_tileMeterPanel提供了不同的定义,只要更新它们,就可以替换背景:
#if __GLCD_CFG_COLOUR_DEPTH__ == 8
# define c_tileMeterPanel c_tileMeterPanelGRAY8
#elif __GLCD_CFG_COLOUR_DEPTH__ == 16
# define c_tileMeterPanel c_tileMeterPanelRGB565
#elif __GLCD_CFG_COLOUR_DEPTH__ == 32
# define c_tileMeterPanel c_tileMeterPanelCCCA8888#else# error Unsupported colour depth!#endif

具体来说,我们需要:

  1. 使用 img2c.py 将新的背景转换成 arm-2d 可以使用的 tile 对象;

  2. 按照屏幕的颜色格式把 c_tileMeterPanelGRAY8c_tileMeterPanelRGB565或者c_tileMeterPanelCCCA8888替换成新生成的tile

下面是一个替换了背景后的效果,看起来不错吧?

既然背景可以替换,那么指针可以更换吗?

答案是肯定的:我们只需要定义一个叫做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模板就可以,如下图

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

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

【小结】

使用Arm-2D实现图片的旋转很简单,在旋转时加入动态脏矩阵也很简单(有transform helper的帮助),即有几个要旋转的图片分别定义几个OP与之对应就可以,需要注意的就是OP定义完一定要用 ARM_2D_OP_INIT 初始化一下,并且用完了要用 ARM_2D_OP_DEPOSE 进行释放(申请的资源要释放掉)。
如果你在开发界面时想要自己手动调试脏矩阵区域,官方也给我们提供了一个调试的黑科技,就是可以把我们的脏矩阵区域显示出来方便我们观察设置的区域是否正确,显示脏矩阵区域也很简单,如下图所示

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


原创不易,如果你喜欢我的公众号、觉得我的文章对你有所启发,

请务必“点赞、收藏、转发”,这对我很重要,谢谢!

欢迎订阅    嵌入式小书虫


裸机思维
傻孩子图书工作室。探讨嵌入式系统开发的相关思维、方法、技巧。
 最新文章