【说在前面的话】
Cortex-M0+ 实时地将背景图片、圆角矩形半透明方框和位于前景的直方图合成到了一起; 该实例未使用任何图形加速硬件,甚至连SPI也是CPU自己去填充数据的(未借助DMA); 虽然使用了最新的Arm Compiler 6,但优化等级使用的却不是“以最大化牺牲代码尺寸为代价换取最大化性能”的-Omax——相反,这里用到的是“追求尺寸和性能相平衡”的-Os加Link Time Optimization。 除去背景图片后的代码尺寸为40K Flash,项目整体SRAM占用为12K(其中还包含 3K STACK和1K HEAP);PFB的尺寸为 320*8
无论此前是否接触过Arm-2D,上述环境数据与最终达成的视觉效果让很多朋友一脸懵逼。
那么,这背后究竟隐藏着怎样的黑科技呢?其实,对大多数朋友来说,要理解这一切,一张图足以:
这是前述例子在开启脏矩阵调试模式后的效果(为了方便观察,我们将直方图的颜色改成了灰色),这里可以清晰的看到:虽然在我们肉眼中画面在较短时间内呈现巨大变化,但相对前一帧,改变的内容就只有红色方框所标记的那一小部分而已——这就可以合理的解释为什么在LCD带宽较低、CPU性能较弱(且未开启编译器最高性能优化)的情况下,我们仍然可以获得流畅的动态画面。
【什么是脏矩阵】
脏矩阵不是什么新兴的科技,它的基本思路也非常的简单,即:只在需要的时候重绘画面中变化的部分。由于基于光栅的绘图技术在数据结构上总表现为一个矩形区域,且画面中变化的内容往往又被称为“弄脏了”的部分——“脏矩阵”故此得名。
画面上有哪些内容需要重绘 这些区域何时需要重绘
如果你对上述问题感到困惑,不妨来看一个简单且极端的例子:
如上图所示,假设相对前一帧,屏幕上突然出现了一个横跨对角的直线,很多较为初级的脏矩阵算法可能会“无奈”的做出将屏幕完全重绘的决策——而在我们看来,只因为1%都不到的像素变化就重绘整个屏幕,显示然是不合理的。
受制于脏矩阵必须是矩形区域的数据结构限制,我们其实还可以通过下面的方法来缩小重绘的面积:
有些朋友可能立马会提出这样的疑问:可不可以用一连串宽度(Width)和高度(Height)都为1个像素的脏矩阵来精确逼近整个斜线呢?理论上可行,但从时间成本角度出发,这种做法却往往不是最优解。原因有三:
一个可以根据目标内容自己“想出”最优的脏矩阵拆分策略的智能算法,其时间和空间开销通常都不可小觑; 底层的每一次脏矩阵刷新都有固定的开支,即便你拥有完整的显示缓冲(Framebuffer)保存着当前帧的所有像素,用 1*1的脏矩阵来覆盖一条斜线就意味着将这一固定开支直接放大数百倍(具体取决于这条斜线所涉及的像素数量)——在把这一固定成本考虑进去后,很可能1*1的极限逼近方案就已经不是性能上的最优解了; 在无法使用完整显示缓冲(Full Framebuffer)的环境下,Partial Framebuffer(PFB) 就成了唯一的选择。而PFB的实现原理不可避免的涉及“对画面内容的重复绘制”——对每一次绘制来说,哪怕当前PFB区域以外的部分都会被忽略,但这一过程中“用户代码的执行”和“区域的裁剪”仍然伴随着不可忽略的时间开销——当使用1*1的极限逼近方案时,就意味着同样的画面绘制函数会被重复上百次——最终的时间成本就会积累到不可接受的程度。
从这个例子可以看出,对于需要重绘的区域,如何合理的设置脏矩阵——在性能收益与时间成本之间做权衡——是一个颇具挑战的难题。
【Arm-2D的脏矩阵策略】
为什么生成脏矩阵的算法消耗巨大?
为什么它需要足够聪明?
为什么它不知道所需更新的内容是什么?
既然关键信息掌握在用户手里,为什么不直接让用户去设置脏矩阵方案呢?
很好!出院!
Arm-2D也是这么想的。
我们前面说过,Arm-2D本身并不是GUI(缺乏正常GUI所必须的控件管理、消息处理、控件库和设计器)——只有在你的芯片资源负担不起一个正常GUI协议栈的情况下,才推荐使用Arm-2D直接开发图形应用。在这一前提条件下,其实并非Arm-2D偷懒,而是那种“用户你只管绘图,最小更新区域我来猜”的“智能友好”算法压根就是负担不起的。
事实上,让掌握应用信息的一方去设置脏矩阵区域(而非建立一个智能算法去揣测用户的实际意图)是嵌入式行业的通解。
以普通GUI为例,当用户通过某种方式(比如设计器、数据结构或者API调用)将控件之间的隶属和层次关系告知GUI协议栈后,负责控件树管理的服务就掌握了所有的应用信息。这里,控件哪怕看起来与一个圆角矩形无异,它本身也包含着额外的应用信息——比如当你按下一个按钮的时候,协议栈知道只有边框的阴影部分需要重绘,而按钮的表面却无需更新。正因如此,GUI协议栈可以根据用户与控件的交互轻松地生成最优的脏矩阵覆盖方案,而无需什么智能算法的加持——掌握信息的一方直接设置脏矩阵不存在信息差,自然也不存在“猜测”的成本。
当我们无法负担一个普通GUI协议栈的成本时,用户除了要直接使用Arm-2D所提供的图形API来绘制界面,还应该(根据自己对界面的设计理解)承担起设置脏矩阵的职能。
那么?让用户自己手动设置脏矩阵容易么?不必担心,从信息的角度来说,实际过程不应该非常复杂,原因有二:
如前面所说,用户原本就对自己设计的界面了如指掌——哪里需要更新、哪里保持不变根本无需过多得思考。 在《【玩转Arm-2D】Arm-2D应用开发入门》中,我们介绍过一种基于面板还状态拆分的界面设计范式。借助场景播放器的帮助,复杂的界面可以被拆分成“代表不同状态的简单面板”——正因为每个面板都很简单,要覆盖更新区域所需的脏矩阵配置也不会复杂。
正因如此,为了满足用户不同场景下不同层次的需求、为了最大限度提供傻瓜化的脏矩阵描述方式,Arm-2D提供以下的方案:
静态脏矩阵 动态脏矩阵傻瓜化服务模块(arm_2d_helper_dirty_region) 旋转和缩放脏矩阵服务模块(arm_2d_helper_transform) 自定义动态脏矩阵(arm_2d_dynamic_dirty_region)
本着“从易到难、循序渐进”的原则,我们将结合实例,以多个篇幅深入浅出地为大家介绍Arm-2D下的脏矩阵技术。
【针对固定区域静态脏矩阵】
即,下图中红色方框所标注的区域:
...
/*============================ LOCAL VARIABLES ===============================*/
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)
/* a dirty region for hour */
ADD_REGION_TO_LIST(s_tDirtyRegions,
...
),
...
/* a dirty region for TenMs */
ADD_REGION_TO_LIST(s_tDirtyRegions,
...
),
/* add the last region for ECG */
ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
...
),
END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)
/*============================ IMPLEMENTATION ================================*/
...
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>, static)
/* 一个编译时刻位置和尺寸就已经固定下来的脏矩阵矩形区域 */
ADD_REGION_TO_LIST(<脏矩阵列表名称>,
.tLocation = {
.iX = <X轴坐标>,
.iY = <Y轴坐标>
},
.tSize = {
.iWidth = <宽度>,
.iHeight = <高度>
},
),
...
/* 一个需要在运行时刻才能确定其位置或大小的脏矩阵矩形区域 */
ADD_REGION_TO_LIST(<脏矩阵列表名称>,
0
),
/* 列表的最后一个元素 */
ADD_LAST_REGION_TO_LIST(<脏矩阵列表名称>,
...
),
END_IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>)
这里,关键字IMPL_ARM_2D_REGION_LIST() 和配套的 END_IMPL_ARM_2D_REGION_LIST() 定义了一个用户指定名称的脏矩阵列表。在它们包裹的区域内,我们只能通过关键字ADD_REGION_TO_LIST()和ADD_LAST_REGION_TO_LIST()来添加具体的矩形区域到列表中。
其中,列表的最后一个元素由ADD_LAST_REGION_TO_LIST() 来添加,而其它元素则需要通过ADD_REGION_TO_LIST()来添加。值得强调的是:当列表中有且仅有一个元素的时候,也应该使用ADD_LAST_REGION_TO_LIST() 来添加。
脏矩阵列表中元素的数据类型是 arm_2d_region_list_item_t,其定义如下:
/*!
* \brief the node of a region list
*
*/
typedef struct arm_2d_region_list_item_t {
struct arm_2d_region_list_item_t *ptNext; //!< the next node
ARM_PRIVATE(
struct arm_2d_region_list_item_t *ptInternalNext; //!< the next node in the internal list
)
arm_2d_region_t tRegion; //!< the region
ARM_PROTECTED(
uint8_t chUserRegionIndex; //!< User Region Index, used to indicate updating which dynamic dirty regions
uint8_t bIgnore : 1; //!< ignore this region
uint8_t bUpdated : 1; //!< this region item has been updated, PFB helper should refresh it again.
uint8_t : 6; //!< reserved for the future
uint16_t bFromInternalPool : 1; //!< a flag indicating whether this list item coming from the internal pool
uint16_t bFromHeap : 1; //!< whether this item comes from the HEAP
uint16_t u2UpdateState : 2; //!< reserved for internal FSM
uint16_t u12KEY : 12; //!< KEY
)
} arm_2d_region_list_item_t;
/*!
* \brief a type for coordinates (integer)
*
*/
typedef struct arm_2d_location_t {
int16_t iX; //!< x in Cartesian coordinate system
int16_t iY; //!< y in Cartesian coordinate system
} arm_2d_location_t;
/*!
* \brief a type for the size of an rectangular area
*
*/
typedef struct arm_2d_size_t {
int16_t iWidth; //!< width of an rectangular area
int16_t iHeight; //!< height of an rectangular area
} arm_2d_size_t;
/*!
* \brief a type for an rectangular area
*
*/
typedef struct arm_2d_region_t {
implement_ex(arm_2d_location_t, tLocation); //!< the location (top-left corner)
implement_ex(arm_2d_size_t, tSize); //!< the size
} arm_2d_region_t;
typedef struct arm_2d_region_t {
struct arm_2d_location_t {
int16_t iX; //!< x in Cartesian coordinate system
int16_t iY; //!< y in Cartesian coordinate system
} tLocation;
struct arm_2d_size_t {
int16_t iWidth; //!< width of an rectangular area
int16_t iHeight; //!< height of an rectangular area
} tSize;
} arm_2d_region_t;
至此,我们能够清晰的理解到,本质上前面所提到的关键字IMPL_ARM_2D_REGION_LIST()、 END_IMPL_ARM_2D_REGION_LIST()、ADD_REGION_TO_LIST() 和 ADD_LAST_REGION_TO_LIST() 实际上只是通过定义和初始化静态结构体的方法在编译时刻创建了一个静态的链表。
这里,ADD_REGION_TO_LIST() 设置ptNext指针指向下一个元素,而ADD_LAST_REGION_TO_LIST() 则会将 ptNext设置为NULL——以指示链表的末尾。
除此差别之外,ADD_REGION_TO_LIST()和ADD_LAST_REGION_TO_LIST() 就再无半点不同——实现的都是对结构体成员变量 tRegion 的初始化——方便我们指定目标区域,其语法细节如下图所示:
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>, static)
/* 一个编译时刻位置和尺寸就已经固定下来的脏矩阵矩形区域 */
ADD_REGION_TO_LIST(<脏矩阵列表名称>,
.tLocation = {
.iX = <X轴坐标>,
.iY = <Y轴坐标>
},
.tSize = {
.iWidth = <宽度>,
.iHeight = <高度>
},
),
...
/* 列表的最后一个元素 */
ADD_LAST_REGION_TO_LIST(<脏矩阵列表名称>,
...
),
END_IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>)
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)
/* a dirty region for hour */
ADD_REGION_TO_LIST(s_tDirtyRegions,
...
),
...
/* add the last region for ECG */
ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
...
),
END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)
...
/* 场景初始化函数 */
ARM_NONNULL(1)
user_scene_alarm_clock_t *__arm_2d_scene_alarm_clock_init(
arm_2d_scene_player_t *ptDispAdapter,
user_scene_alarm_clock_t *ptThis)
{
bool bUserAllocated = false;
assert(NULL != ptDispAdapter);
s_tDirtyRegions[dimof(s_tDirtyRegions)-1].ptNext = NULL;
...
*ptThis = (user_scene_alarm_clock_t){
.use_as__arm_2d_scene_t = {
...
/* 将我们定义的静态脏矩阵列表赋值给目标场景 */
.ptDirtyRegion = (arm_2d_region_list_item_t *)s_tDirtyRegions,
...
},
.bUserAllocated = bUserAllocated,
};
...
return ptThis;
}
【“静中有动”】
就以前面的例子场景(arm_2d_scene_alarm_clock)为例,其布局示意图如下所示:
对应的布局代码如下:
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_alarm_clock_handler)
{
...
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_dock_vertical(__top_canvas, 64+c_tileECGMask.tRegion.tSize.iHeight) {
arm_2d_layout(__vertical_region) {
/* Draw Clock */
__item_line_dock_vertical(64) {
arm_2d_size_t tStringSize = arm_lcd_get_string_line_box("00:00:00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tTwoDigitsSizeSmall = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
arm_2d_size_t tTwoDigitsSizeBig = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tCommaSizeBig = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tCommaSizeSmall = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
tStringSize.iWidth += tTwoDigitsSizeSmall.iWidth + tCommaSizeSmall.iWidth;
arm_lcd_text_set_font((arm_2d_font_t *)&ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_lcd_text_set_colour(GLCD_COLOR_WHITE, GLCD_COLOR_BLACK);
arm_2d_dock_horizontal(__item_region, tStringSize.iWidth) {
arm_2d_layout(__horizontal_region) {
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
/* 绘制小时数 */
...
}
__item_line_dock_horizontal(tCommaSizeBig.iWidth) {
/* 绘制冒号 */
...
}
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
/* 绘制分钟数 */
...
}
__item_line_dock_horizontal(tCommaSizeBig.iWidth) {
/* 绘制冒号 */
...
}
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
/* 绘制秒数 */
...
}
__item_line_dock_horizontal(tCommaSizeSmall.iWidth) {
}
__item_line_dock_horizontal(tTwoDigitsSizeSmall.iWidth) {
arm_2d_align_centre(__item_region, tTwoDigitsSizeSmall) {
/* 绘制10毫秒数 */
...
}
}
}
}
}
/* ECG Scanning Animation */
__item_line_dock_vertical() {
arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
arm_2d_container(ptTile, __ecg, &__centre_region ) {
/* Draw ECG waveform as background */
...
}
}
}
}
}
...
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
同样的代码在 480*480 的分辨率下,依然可以很好的呈现出所需的视觉效果:
enum {
DIRTY_REGION_IDX_HOUR,
DIRTY_REGION_IDX_MIN,
DIRTY_REGION_IDX_SEC,
DIRTY_REGION_IDX_TENMS,
DIRTY_REGION_IDX_ECG,
};
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)
/* a dirty region for hour */
ADD_REGION_TO_LIST(s_tDirtyRegions,
0 /* initialize at runtime later */
),
/* a dirty region for mins */
ADD_REGION_TO_LIST(s_tDirtyRegions,
0 /* initialize at runtime later */
),
/* a dirty region for second */
ADD_REGION_TO_LIST(s_tDirtyRegions,
0 /* initialize at runtime later */
),
/* a dirty region for TenMs */
ADD_REGION_TO_LIST(s_tDirtyRegions,
0 /* initialize at runtime later */
),
/* add the last region for ECG */
ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
0
),
END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)
ARM_NONNULL(1)
user_scene_alarm_clock_t *__arm_2d_scene_alarm_clock_init(
arm_2d_scene_player_t *ptDispAdapter,
user_scene_alarm_clock_t *ptThis)
{
bool bUserAllocated = false;
assert(NULL != ptDispAdapter);
s_tDirtyRegions[dimof(s_tDirtyRegions)-1].ptNext = NULL;
/* get the screen region */
arm_2d_region_t tScreen
= arm_2d_helper_pfb_get_display_area(
&ptDispAdapter->use_as__arm_2d_helper_pfb_t);
/*--------------initialize static dirty region items: begin---------------*/
arm_2d_dock_vertical(tScreen, 64+c_tileECGMask.tRegion.tSize.iHeight) {
arm_2d_layout(__vertical_region) {
__item_line_dock_vertical(64) {
arm_2d_size_t tStringSize = arm_lcd_get_string_line_box("00:00:00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tTwoDigitsSizeSmall = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
arm_2d_size_t tTwoDigitsSizeBig = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tCommaSizeBig = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_2d_size_t tCommaSizeSmall = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
tStringSize.iWidth += tTwoDigitsSizeSmall.iWidth + tCommaSizeSmall.iWidth;
arm_lcd_text_set_font((arm_2d_font_t *)&ARM_2D_FONT_ALARM_CLOCK_64_A4);
arm_lcd_text_set_colour(GLCD_COLOR_WHITE, GLCD_COLOR_BLACK);
arm_2d_dock_horizontal(__item_region, tStringSize.iWidth) {
arm_2d_layout(__horizontal_region) {
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
s_tDirtyRegions[DIRTY_REGION_IDX_HOUR].tRegion = __item_region;
}
__item_line_dock_horizontal(tCommaSizeBig.iWidth) {
}
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
s_tDirtyRegions[DIRTY_REGION_IDX_MIN].tRegion = __item_region;
}
__item_line_dock_horizontal(tCommaSizeBig.iWidth) {
}
__item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
s_tDirtyRegions[DIRTY_REGION_IDX_SEC].tRegion = __item_region;
}
__item_line_dock_horizontal(tCommaSizeSmall.iWidth) {
}
__item_line_dock_horizontal(tTwoDigitsSizeSmall.iWidth) {
arm_2d_align_centre(__item_region, tTwoDigitsSizeSmall) {
s_tDirtyRegions[DIRTY_REGION_IDX_TENMS].tRegion = __centre_region;
}
}
}
}
}
/* ECG Scanning Animation */
__item_line_dock_vertical() {
arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
s_tDirtyRegions[DIRTY_REGION_IDX_ECG].tRegion = __centre_region;
}
}
}
}
/*--------------initialize static dirty region items: end ---------------*/
...
*ptThis = (user_scene_alarm_clock_t){
.use_as__arm_2d_scene_t = {
...
.ptDirtyRegion = (arm_2d_region_list_item_t *)s_tDirtyRegions,
...
},
.bUserAllocated = bUserAllocated,
};
...
return ptThis;
}
enum {
DIRTY_REGION_IDX_HOUR,
DIRTY_REGION_IDX_MIN,
DIRTY_REGION_IDX_SEC,
DIRTY_REGION_IDX_TENMS,
DIRTY_REGION_IDX_ECG,
};
...
/* ECG Scanning Animation */
__item_line_dock_vertical() {
arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
s_tDirtyRegions[DIRTY_REGION_IDX_ECG].tRegion = __centre_region;
}
}
...
相对在编译时刻(compile-time)以常量赋值的方法直接指定静态脏矩阵的位置和大小;这种在运行时刻(runtime)以布局辅助的方式计算出脏矩阵区域并初始化静态脏矩阵的做法就不可谓不是“静中有动”了。
其实,这里的“动中有静”还有另外一种体现方式。在前面介绍脏矩阵基本概念的时候我们提出过脏矩阵设置的要点,总结来说就是一句话:在“什么时候”刷新“哪个区域”。对静态脏矩阵来说,更新“哪个区域”的问题我们已经解决,剩下就是决定“什么时候”了。
观察应用场景容易发现,很多时候,我们并不需要一直不停地重复刷新某一区域。就以时间显示为例,秒表数字只会一秒钟更新一次、分钟数字一分钟更新一次而表示小时的数字则更是一小时更新一次了。这种情况下,我们只需要在数字变化的瞬间更新一次即可。
为了实现这一功能,Arm-2D 为每个脏矩阵对象都提供了一个API函数:
/*!
* \brief decide whether ignore the specified dirty region item
*
* \param[in] ptThis the target dirty region item object
* \param[in] bIgnore whether ignore
* \return bool the previous ignore status
*/
extern
ARM_NONNULL(1)
bool arm_2d_dirty_region_item_ignore_set(arm_2d_region_list_item_t *ptThis, bool bIgnore);
static void __on_scene_alarm_clock_frame_start(arm_2d_scene_t *ptScene)
{
user_scene_alarm_clock_t *ptThis = (user_scene_alarm_clock_t *)ptScene;
ARM_2D_UNUSED(ptThis);
int64_t lTimeStampInMs = arm_2d_helper_convert_ticks_to_ms(
arm_2d_helper_get_system_timestamp());
/* calculate the hours */
do {
uint_fast8_t chHour = lTimeStampInMs / (3600ul * 1000ul);
chHour %= 24;
arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_HOUR],
(chHour == this.chHour));
this.chHour = chHour;
lTimeStampInMs %= (3600ul * 1000ul);
} while(0);
/* calculate the Minutes */
do {
uint_fast8_t chMin = lTimeStampInMs / (60ul * 1000ul);
arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_MIN],
(chMin == this.chMin));
this.chMin = chMin;
lTimeStampInMs %= (60ul * 1000ul);
} while(0);
/* calculate the Seconds */
do {
uint_fast8_t chSec = lTimeStampInMs / (1000ul);
arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_SEC],
(chSec == this.chSec));
this.chSec = chSec;
lTimeStampInMs %= (1000ul);
} while(0);
/* calculate the Ten-Miliseconds */
do {
uint_fast8_t chTenMs = lTimeStampInMs / (10ul);
arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_TENMS],
(chTenMs == this.chTenMs));
this.chTenMs = chTenMs;
} while(0);
...
}
int64_t lTimeStampInMs
= arm_2d_helper_convert_ticks_to_ms(
arm_2d_helper_get_system_timestamp());
/* calculate the Seconds */
do {
uint_fast8_t chSec = lTimeStampInMs / (1000ul);
arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_SEC],
(chSec == this.chSec));
this.chSec = chSec;
lTimeStampInMs %= (1000ul);
} while(0);
为了观察结果,我们可以在 arm_2d_disp_adapter_0.h 中打开脏矩阵调试模式:
// <q> Enable Dirty Region Debug Mode
// <i> Draw dirty regions on the screen for debug.
可以看到,读秒的区域只会在数字变化的那一帧产生一个红色的方框——标明对应的区域进行了刷新;而10毫秒对应的区域则长期被红色方框标出——这是因为在当前帧率下,该区域经常性的处于变化状态,因此几乎一直处于刷新状态(其实很有可能存在一些帧数字并未发生变化,但我们肉眼就看不到了)。同理,由于底部的波形部分一直处于刷新状态,因此红框也牢牢的锁定了这一区域。
也可以在MDK的工程管理器中,选择任意用户创建的Group后单击右键,在弹出菜单中选择 “Add New Item to Group”:
在窗口左侧的列表中选择最下方的“User Code Template”,然后在右边的窗口中展开 Acceleration,找到 Arm-2D Helper PFB 中的 Scene Template Alarm Clock。单击OK后就可以添加到工程中了。
感兴趣的朋友可以自行尝试,这里就不再赘述啦。
【追踪动态变化区域的动态脏矩阵】
这虽然并不难理解,但却立即带来了至少三种情况,如图所示:
对于图示的三种情况:
新老位置完全分离:我们需要用两个脏矩阵分别覆盖新老两个位置;
新老位置部分重合:我们需要用一个足以覆盖新老两个位置的最小脏矩阵;
新老位置交错:此时我们要计算足以包含新老位置的“最小覆盖脏矩阵(Minimal Enclosure)”的面积(像素数量),然后与新老两个区域面积的总和进行比较:
如果“最小覆盖脏矩阵”的面积小于新老区域面积总和,则使用最小覆盖脏矩阵进行刷新;反之,
如果“最小覆盖脏矩阵”的面积大于新老区域面积的总和,则维持原来的方案——依次刷新新老两个位置——这里我们不得不接受一定程度的浪费。
新增一个脏矩阵带来的RAM开销 选择拆方案的决策开销 PFB模式下过于细碎的拆分导致“重复执行用户绘图函数导致的时间成本积累”,抵消了(部分)脏矩阵带来的性能优势 产生更多细碎的脏矩阵,从而更加依赖脏矩阵合并算法。但脏矩阵合并算法本身也存在时间成本随着候选脏矩阵数量增加而快速暴涨的问题。
综合来看,上述成本问题在画面较为简单时还不突出,当画面中要处理的区域增多时,其带来的“时间成本(CPU性能消耗)”和“空间成本(RAM开销)”都会显著增加。
因此选择不对已有的脏矩阵进行拆分——而忍受一定程度浪费的存在——实属权衡利弊后的无奈之选。
【动态脏矩阵辅助服务模块(arm_2d_helper_dirty_region)】
类(控制块):arm_2d_helper_dirty_region_t 目标区域更新函数:arm_2d_helper_dirty_region_update_item()
从API的基本构成可以看出,该服务的使用极其简单——直接更新目标区域即可。
static void __on_scene_atom_frame_start(arm_2d_scene_t *ptScene)
{
user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
ARM_2D_UNUSED(ptThis);
/* update core and electronics coordinates */
do {
int32_t nResult;
const int16_t iRadiusX = 110;
const int16_t iRadiusY = 110;
/* calculate core vibration */
arm_2d_helper_time_cos_slider(
-5, 5, /* 振动幅度 */
100, /* 振动周期 */
ARM_2D_ANGLE(0.0f), /* 相位 */
&nResult, &this.lTimestamp[1]);
this.Core.tVibration.iX = nResult;
arm_2d_helper_time_cos_slider(
-5, 5, /* 振动幅度 */
150, /* 振动周期 */
ARM_2D_ANGLE(30.0f), /* 相位 */
&nResult, &this.lTimestamp[2]);
this.Core.tVibration.iY = nResult;
...
} while(0);
...
}
ARM_NONNULL(1)
user_scene_atom_t *__arm_2d_scene_atom_init( arm_2d_scene_player_t *ptDispAdapter,
user_scene_atom_t *ptThis)
{
...
*ptThis = (user_scene_atom_t){
.use_as__arm_2d_scene_t = {
...
.bUseDirtyRegionHelper = true,
},
...
};
...
return ptThis;
}
第二步:在场景绘图函数中更新目标区域。
/* update dirty region */
arm_2d_helper_dirty_region_update_item(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper.tDefaultItem,
(arm_2d_tile_t *)< 目标 tile 指针 >,
< 脏矩阵目标区域所在的画布的地址 >,
< 脏矩阵目标区域的地址 >);
其中:
<目标 tile 指针> 指向当前绘图的目标tile
< 脏矩阵目标区域所在的画布的地址 > :当我们提供画布的地址后,如果脏矩阵超出了该画布就会被自动裁剪(甚至忽略掉)。该参数可以为NULL
< 脏矩阵目标区域的地址 > : 这就是当前帧内我们要追踪的目标所在的区域信息。
在这个氦原子核振动的例子中,我们可以使用下面的实现脏矩阵对原子核的追踪:
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_atom_handler)
{
ARM_2D_PARAM(pTarget);
ARM_2D_PARAM(ptTile);
ARM_2D_PARAM(bIsNewFrame);
user_scene_atom_t *ptThis = (user_scene_atom_t *)pTarget;
arm_2d_size_t tScreenSize = ptTile->tRegion.tSize;
ARM_2D_UNUSED(tScreenSize);
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
arm_2d_size_t tCharSize
= ARM_2D_FONT_A4_DIGITS_ONLY
.use_as__arm_2d_user_font_t
.use_as__arm_2d_font_t.tCharSize;
arm_2d_size_t tAtomCoreSize = {
.iWidth = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iWidth * 2,
.iHeight = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2
+ tCharSize.iHeight,
};
/* draw atom core */
arm_2d_align_centre(__top_canvas, tAtomCoreSize) {
arm_2d_layout(__centre_region) {
__item_line_dock_vertical(c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2) {
/* apply vibration */
__item_region.tLocation.iX += this.Core.tVibration.iX;
__item_region.tLocation.iY += this.Core.tVibration.iY;
arm_2d_region_t tDirtyRegion = __item_region;
tDirtyRegion.tSize.iHeight += tCharSize.iHeight;
/* update dirty region */
arm_2d_helper_dirty_region_update_item(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper.tDefaultItem,
(arm_2d_tile_t *)ptTile,
&__top_canvas,
&tDirtyRegion);
...
}
...
}
}
...
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
至此,大功告成。是不是非常简单呢?
【当你要更新多个运动对象时该怎么办?】
这里,电子在二维平面上的运动轨迹仍然是通过x轴和y轴上的简谐振动叠加而成:
static void __on_scene_atom_frame_start(arm_2d_scene_t *ptScene)
{
user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
ARM_2D_UNUSED(ptThis);
/* update core and electronics coordinates */
do {
int32_t nResult;
const int16_t iRadiusX = 110;
const int16_t iRadiusY = 110;
...
/* calculate electronic0 vibration */
arm_2d_helper_time_cos_slider(
-iRadiusX, iRadiusX, /* 振动幅度 */
1300, /* 周期 */
ARM_2D_ANGLE(0.0f), /* 相位 */
&nResult, &this.lTimestamp[3]);
this.Electronic[0].tOffset.iX = nResult;
arm_2d_helper_time_cos_slider(
-iRadiusY, iRadiusY, /* 振动幅度 */
1300, /* 周期 */
ARM_2D_ANGLE(45.0f), /* 相位 */
&nResult, &this.lTimestamp[4]);
this.Electronic[0].tOffset.iY = nResult;
arm_2d_helper_time_cos_slider(
128, 255,
1300,
ARM_2D_ANGLE(45.0f),
&nResult, &this.lTimestamp[7]);
this.Electronic[0].chOpacity = nResult;
...
} while(0);
}
值得说明的是,我们甚至使用相同周期变化的 透明度 模拟了电子在Z轴上的远近变化。完成了电子运动轨迹的生成,接下来就是脏矩阵的覆盖和更新了。
第一步:在用户的场景对象定义中添加 arm_2d_helper_dirty_region_item_t 成员。
/*!
* \brief a user class for scene atom
*/
typedef struct user_scene_atom_t user_scene_atom_t;
struct user_scene_atom_t {
implement(arm_2d_scene_t); //! derived from class: arm_2d_scene_t
ARM_PRIVATE(
...
struct {
arm_2d_helper_dirty_region_item_t tDirtyRegionItem;
arm_2d_location_t tOffset;
uint8_t chOpacity;
} Electronic[2];
)
/* place your public member here */
};
第二步:向脏矩阵辅助模块添加 arm_2d_helper_dirty_region_item_t 对象。
static void __on_scene_atom_load(arm_2d_scene_t *ptScene)
{
user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
ARM_2D_UNUSED(ptThis);
...
}
ARM_NONNULL(1)
user_scene_atom_t *__arm_2d_scene_atom_init( arm_2d_scene_player_t *ptDispAdapter,
user_scene_atom_t *ptThis)
{
...
*ptThis = (user_scene_atom_t){
.use_as__arm_2d_scene_t = {
...
/* Please uncommon the callbacks if you need them
*/
.fnOnLoad = &__on_scene_atom_load,
...
.bUseDirtyRegionHelper = true,
},
...
};
...
}
接下来在场景 on load 事件处理程序中添加代码:
static void __on_scene_atom_load(arm_2d_scene_t *ptScene)
{
user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
ARM_2D_UNUSED(ptThis);
arm_2d_helper_dirty_region_add_items(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.Electronic[0].tDirtyRegionItem,
1);
arm_2d_helper_dirty_region_add_items(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.Electronic[1].tDirtyRegionItem,
1);
}
这里,函数 arm_2d_helper_dirty_region_add_items() 的原型如下:
extern
ARM_NONNULL(1,2)
/*!
* \brief add an array of region items to a dirty region helper
*
* \param[in] ptThis the target helper
* \param[in] ptItems the array of the region items
* \param[in] hwCount the number of items in the array
*/
void arm_2d_helper_dirty_region_add_items(
arm_2d_helper_dirty_region_t *ptThis,
arm_2d_helper_dirty_region_item_t *ptItems,
uint_fast16_t hwCount)
extern
ARM_NONNULL(1,2)
/*!
* \brief remove an array of region items to a dirty region helper
*
* \param[in] ptThis the target helper
* \param[in] ptItems the array of the region items
* \param[in] hwCount the number of items in the array
*/
void arm_2d_helper_dirty_region_remove_items(
arm_2d_helper_dirty_region_t *ptThis,
arm_2d_helper_dirty_region_item_t *ptItems,
uint_fast16_t hwCount);
观察函数原型容易发现,通过 ptItems 和 hwCount,我们可以轻松的一次性添加多个对象。在需要的时候,我们也可以通过函数 arm_2d_helper_dirty_region_remove_item() 将此前加入的一些对象移除,这里就不再赘述。
第三步:在场景绘图函数中更新脏矩阵区域。
其实,arm_2d_helper_dirty_region_t 控制块中自带了一个默认的 arm_2d_helper_dirty_region_item_t 对象 tDefaultItem,这也是为什么之前追踪原子核的例子中会使用下面的代码来更新目标区域:
...
arm_2d_helper_dirty_region_update_item(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this
.use_as__arm_2d_scene_t
.tDirtyRegionHelper
.tDefaultItem,
(arm_2d_tile_t *)ptTile,
&__top_canvas,
&tDirtyRegion);
...
因此,两个电子对应区域的更新,本质上并无不同:
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_atom_handler)
{
...
user_scene_atom_t *ptThis = (user_scene_atom_t *)pTarget;
arm_2d_size_t tScreenSize = ptTile->tRegion.tSize;
arm_2d_canvas(ptTile, __top_canvas) {
/*-----------------------draw the foreground begin-----------------------*/
arm_2d_size_t tCharSize = ARM_2D_FONT_A4_DIGITS_ONLY
.use_as__arm_2d_user_font_t
.use_as__arm_2d_font_t.tCharSize;
arm_2d_size_t tAtomCoreSize = {
.iWidth = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iWidth * 2,
.iHeight = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2
+ tCharSize.iHeight,
};
。。。
/* draw electronics */
do {
arm_2d_align_centre(__top_canvas,
220,
220) {
arm_2d_size_t tElectronicSize = {
.iWidth = c_tileWhiteDotMask.tRegion.tSize.iWidth
+ tCharSize.iWidth,
.iHeight = c_tileWhiteDotMask.tRegion.tSize.iWidth
+ tCharSize.iHeight,
};
/* electronic 0 */
arm_2d_align_centre(__centre_region, tElectronicSize) {
__centre_region.tLocation.iX += this.Electronic[0].tOffset.iX;
__centre_region.tLocation.iY += this.Electronic[0].tOffset.iY;
/* update dirty region */
arm_2d_helper_dirty_region_update_item(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.Electronic[0].tDirtyRegionItem,
(arm_2d_tile_t *)ptTile,
&__top_canvas,
&__centre_region);
...
}
/* electronic 1 */
arm_2d_align_centre(__centre_region, tElectronicSize) {
__centre_region.tLocation.iX += this.Electronic[1].tOffset.iX;
__centre_region.tLocation.iY += this.Electronic[1].tOffset.iY;
/* update dirty region */
arm_2d_helper_dirty_region_update_item(
&this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
&this.Electronic[1].tDirtyRegionItem,
(arm_2d_tile_t *)ptTile,
&__top_canvas,
&__centre_region);
...
}
}
} while(0);
/*-----------------------draw the foreground end -----------------------*/
}
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
【动态脏矩阵辅助服务模块的其它操作】
/*!
* \brief force the arm_2d_helper_dirty_region_item_t object to suspend the
* dirty region update.
*
* \param[in] ptThis the target item
* \param[in] bEnable whether enable this feature.
* \return boolean the original setting
*/
ARM_NONNULL(1)
bool arm_2d_helper_dirty_region_item_suspend_update(
arm_2d_helper_dirty_region_item_t *ptThis,
bool bEnable);
该函数与静态脏矩阵的 arm_2d_dirty_region_item_ignore_set() 函数功能类似——都是迫使目标 arm_2d_helper_dirty_region_item_t 所对应的区域放弃更新。
/*!
* \brief force an arm_2d_helper_dirty_region_item_t object to use the minimal
* enclosure region to update.
*
* \param[in] ptThis the target item
* \param[in] bEnable whether enable this feature.
* \return boolean the original setting
*/
ARM_NONNULL(1)
bool arm_2d_helper_dirty_region_item_force_to_use_minimal_enclosure(
arm_2d_helper_dirty_region_item_t *ptThis,
bool bEnable);
该函数会强迫目标 arm_2d_helper_dirty_region_item_t 对象使用“最小覆盖脏矩阵”来作为刷新区域。这在某些应用场景下非常有用。比如在下面的进度条例子中,假设进度条发生了突变(图中从右向左出现了位移),相比原本只覆盖圆形断点一前一后两个位置,要想正确的更新进度条凹槽内的背景,我们就必须要使用能同时覆盖新老两个区域的“最小覆盖脏矩阵”。
【说在后面的话】