6.7 GUI人机接口

6.7.1 缘起

平时交流,很多人都会感觉单个模块接口设计和整体产品架构设计之间跨越有点大。大家普遍感觉单个模块的接口设计相对容易,但一旦开始整体产品架构设计,就无从下手。前面几章讲解到的静态和动态程序组织结构,应该算是架构设计中比较入门的知识了,但你是否已经开始有一点点迷糊的感觉了呢!又如何破解呢?

在多年的实践过程中,我们找到了一个很好的中间抓手:复杂模块的整体设计。所谓复杂模块,并非一个确定性的概念,因人而异,如对刚入职的菜鸟来说,定值模块可能已经是复杂模块了,但对工作多年的老手来说,本章的GUI模块都算不得多复杂。

复杂模块一般内部会分为多层,由多个软件模块组成。为了让大家体会这个中间抓手,我挑选一个中等复杂且有趣的软件模块:GUI模块。

◇◇◇

工业产品的液晶尺寸一般比较小,功能少且操作简单,因此早期大多没有明确的GUI模块,液晶界面程序先是通过一些列绘点绘线绘字符串等接口函数绘制出各个界面,然后通过按键切换来实现的。

这种程序结构将界面逻辑和功能逻辑混杂在一起,完成一个界面不仅需要考虑具体显示的信息内容,如定值名称单位等,还需要考虑光标切换、滚屏等界面问题。不仅导致每一个液晶界面程序很复杂,而且各界面程序大量冗余,如遥测的滚屏程序和定值的滚屏程序。更为严重的是,这类液晶界面程序和液晶尺寸紧紧绑定在一起,程序中包含着大量的位置硬编码。

早期的保护设备功能都比较简单,采取这种策略简单可靠,但随着保护功能的持续增加,液晶界面数量也在快速增加。更为痛苦的是,为了迎合市场和用户需求,液晶界面也越来越大,而且有不同尺寸的液晶适应不同的用户需求。这些需求导致原先的程序策略不可行了。

我参与的某一项目,不仅液晶变大了,点阵变密了,而且有多种液晶尺寸,原有的液晶界面程序没有一点复用价值,需要重写了。借着这个机会,是否可以重构液晶界面模块呢?

◇◇◇

如何构建可复用的液晶界面程序,计算机行业早就给出了标准答案:GUI模块。如下图所示:
在这里插入图片描述
左侧图是一液晶界面运行图,用于查看元件状态。元件状态以表格方式组织,第一列为名称,第二列为元件状态。基于GUI模块构建这类程序时,伪代码如下:

void onCreate(void)
{
	获取表格控件控制句柄
	for (i = 0; i < 元件个数; i++)
	{
		通过元件接口函数获取第i个元件名称及状态信息;
		表格第一列填充元件名称;
		表格第二列填充元件状态;
	}
}

这段伪代码不仅未包含任何液晶位置硬编码信息,而且也不需要处理光标移动、翻屏等界面细节,各界面程序不仅简单了,而且也容易复用了。

GUI界面程序经常和资源绑定,如windows程序的对话框资源等。这类资源隔离了各种液晶尺寸的差异,不同的液晶尺寸对应不同的界面资源,界面程序对控件编程,而所有的位置信息都被隐藏在资源配置文件中。

因此,采用GUI模块成为我们的首选方案。

◇◇◇

市面上的GUI模块很多,应该选用哪个呢?反复权衡,最终我们团队选择了ucGUI模块,然而没想到一切才刚刚开始。(注:本书描述的ucGUI模块是2006年的ucGUI版本,因此很多内容会有出入,建议大家侧重关注迭代经历)

使用ucGUI,首要问题是响应慢。当时我们用的还是68332cpu,在保证保护任务优先级的情况下,CPU资源已经有点紧张了,此时运行ucGUI模块,可以很明显的看到液晶屏的刷新过程,而且按键之后需要等待1~2s液晶界面才有反应,用户体验较差。

其次是关于汉字的支持,继保设备的flash资源非常紧张,不可能放下整个汉字库,需要挑选装置中用到的汉字信息重新编码,这部分功能ucGUI模块支持比较弱。

再次是关于窗口和控件,使用ucGUI构建窗口,不仅需要手动构建一系列资源表,还存在界面程序实现繁琐,不美观,很多特定应用功能难以实现等缺点。

深入分析ucGUI模块具体实现,并综合权衡时间因素和产品需求,我们团队做了一个慎重决定:重构ucGUI模块。

6.7.2 解剖ucGUI

重构的前提是整体分析ucGUI模块。ucGUI是一个高效的,可裁减GUI软件模块,属于相对轻量级软件模块,主要用于嵌入式系统中。记得当时完成ucGUI代码阅读后,我绘制了一张体系结构图,如下:
在这里插入图片描述
ucGUI简单的分为三层:驱动层,GUI库和窗口管理模块。

最底层是驱动层,包含多个模块:

  1. ucGui支持三种LCD硬件体系:simple-bus,full-bus和none-bus,然后基于这三种硬件构建统一的LCD驱动接口。
  2. ucGui包含OS抽象层,使其可以支持多种操作系统,甚至包含window平台仿真,同时也支持多任务环境。
  3. ucGUI支持点输入设备(主要包括鼠标和触摸屏)和键盘,它们都为两层结构。上层为General layer,用于为应用提供服务,下层为Driver Layer:实现硬件驱动程序。

基于驱动层提供的各种封装接口之上,ucGUI构建了GUI库,其中包含color,font,bitmap,2Dlibary等多个子模块,构成了ucGUI的核心。

最上面一层是窗口管理模块,给应用程序提供窗口和控件支撑。window manager是一个可裁减模块,以窗口方式组织液晶屏幕,以简化界面程序。基于window manager,ucGUI也提供了对控件和对话框的支持。

为了支持window manager模块,ucGUI模块还额外提供了一个有趣的memdev模块,该提供了内存设备映射机制,主要用于优化屏幕绘制。memdev模块设计比较精巧,为了是ucGUI适用于较小内存的嵌入式系统,memdev支持Banding Memory Device,其核心思想是将一个大的memdev切割成多个小的Memdev,每次仅绘制一部分,通过多次绘制完成整个界面绘制。额外,为了提高绘制效率,支持Auto Device Object,每次绘制时动态计算实际需要绘制的区域,然后才执行绘制操作。

6.7.3 重构GUI模块

ucGUI模块是一个很优秀的嵌入式软件模块,很多设计理念让人大开眼界。遗憾的是,优秀不等同于适用,ucGUI模块为了适应较宽应用场合,反而和我们的继保设备存在诸多不匹配点。优化迭代这些不匹配点,成为我们重构GUI模块的切入点。

我们主要对ucGUI模块做了如下优化:

  1. 简化LCD驱动层。不再区分多种LCD类型,并考虑分布式液晶模块等需求,重新构建lcd驱动接口。
  2. 结合动态执行框架,限制GUI任务仅允许在单一任务环境中执行。该策略导致界面程序不需要考虑互斥锁等问题,简化了整个GUI软件模块。
  3. 结合微机保护单色液晶特征,简化GUI核心模块,包括color,font,bitmap,2DLibary等子模块。
  4. 重写windows manager模块,增加窗口之间的跳转机制,去除窗口裁剪机制,优化控件。
  5. 增加配置软件支撑,不仅增支持界面绘制功能,而且收集整理系统中用到的所有字符串信息,并进行重新编码,然后统一生成界面资源文件。
  6. 去除memdev模块,将其内化到驱动层。
  7. ……

经过一系列调整后,GUI体系结构如下图所示:
在这里插入图片描述
整个GUI模块分为三层,最低层为驱动抽象层,便于硬件移植,目前仅考虑液晶、key、触摸屏三类驱动。在驱动抽象层之上是GUI的基本支撑环境,主要包括color,font,bitmap,2d Libary四部分,接口类同于ucGUI,但基于黑白单色液晶进行了优化。在基本支撑环境之上是窗口管理系统,主要包括“控件,窗口管理结构”,“节点结构信息”,“节点执行环境”三部分。

在GUI模块之下主要为各种驱动的具体实现,在GUI模块之上,为具体的应用程序,其主要表现为窗口和控件的具体消息响应函数,类似于vb编程。

◇◇◇

可能是为了支持更多的液晶设备吧,ucGUI的LCD驱动层做的比较复杂,层次结构比较多。然而即使如此,我们需求依然无法满足。在前文分布式模型中提及,人机接口会分布到另外一个CPU上,此时ucGUI提供的接口就不合适了。

如何构建更合适的驱动接口呢?ucGUI的memdev模块给我们提供了灵感。既然可以基于内存绘制,那么为何不降整个映射内存区作为LCD绘制驱动接口呢!基于这样的理念,优化后的LCD驱动接口如下:

/* 获取液晶内存映射缓冲区,该缓冲区由驱动程序维护 */
BYTE *hwGetLcdMap(void);

/* 整屏数据输出 */
void hwOutputAllLCD(void);
	
/*
 *  Description: LCD带状数据输出
 *  Input: 
 *    DWORD y0, y1: 位置
 *  Others: 范围(y0 <= y1 <= range)
 */
void hwOutputBandLCD(DWORD y0, DWORD y1);

/*
 *  Description: 块状数据输出
 *  Input: 
 *    DWORD x0,y0,x1,y1: 矩阵
 *  Return: 成功返回TRUE,否则返回FALSE
 *  Others: 范围(y0 < y1 <= range, x0 < x1 <= range)
 */
void hwOutputBlockLCD(DWORD x0, DWORD y0, DWORD x1, DWORD y1);

采用这样的接口模式,不仅传统的液晶驱动容易编写,假如液晶界面需要被分布到其他CPU上,也仅需要在驱动程序中将LCD映射内存传输到其他CPU即可。

分布式系统中,如果液晶点阵较大,传输整个点阵数据耗时较多,可能会影响液晶界面按键体验。此时可以在驱动程序中增加各种压缩策略,如经典的RLE(Run-Length Encoding)编码,或者将字符串等静态信息预先传输过去等策略。总之,问题被约束在驱动层单点上。

采用基于memdev的LCD接口,自然而然的省略了ucGUI模块中的memdev模块,有助于进一步简化ucGUI程序结构。

◇◇◇

使用ucGUI模块时,最大的痛点就是慢,不仅窗口绘制慢,用户按键响应也慢,导致用户体验较差。拜各种早期程序动态执行架构所赐,我们有措施详细分析ucGUI模块的堵点在哪儿,最终定位到ucGUI的窗口裁剪绘制机制。

为了完成多个层叠的窗口绘制,ucGUI需要将整个液晶窗口分割成了多个矩形区域,然后依次绘制。假设桌面窗口上有两个重叠窗口,坐标为(10,10,109,109)和(50,50,149,149),此时,整个液晶窗口被分割为多个矩形裁剪区,如下图各彩色窗口所示:

在这里插入图片描述
坐标罗列如下:

1.(0,0,319,9)
2.(0,10,9,49)
3.(110,10,319,49)
4.(0,50,9,109)
5.(150,50,319,109)
6.(0,110,49,149)
7.(150,110,319,149)
8.(0,150,319,239)

这种策略适用面广,但也导致了液晶界面绘制效率低下。微机保护设备液晶尺寸较小,不需要复杂的窗口层叠。如何充分利用微机保护液晶尺寸较小的特征,进一步优化这一过程呢?

在ucGUI模块中,每个窗口不仅有父窗口,还有兄弟窗口。为了去掉复杂的窗口裁剪策略,我们限定所有窗口只能以父子窗口的形式层叠排列,每个窗口都铺满整个液晶界面,此时,窗口自然以堆栈方式依次创建取消。

该策略还有助于提高内存的利用率。构建一执行堆栈,统一组织所有的窗口、控件、字符串等信息,表格条目等也在执行堆栈中分配。在窗口创建时,一次性预分配该窗口需要的所有信息。在窗口撤消后,释放所有信息。

执行堆栈示意图如下:
在这里插入图片描述
当然采用该策略后,也会带来了一些额外约束,如不支持窗口隐藏,两个兄弟窗口不能同时显示,表格初始化时必须先确定其大小等。所幸,这些限制容易回避,但由此带来了用户体验显著提升。

◇◇◇

整个GUI模块通过消息方式来驱动,GUI模块主流程如下:

void guiMainLoop(void)
{
	if (有按键消息)
	{
		打开背光灯
		按键消息处理
		return
	}
	else
	{
		if (超时时间到达)
			关闭背光灯
	}
	时间消息处理
}

按键消息分为两种,一种为系统识别按键消息,一种为系统不识别按键消息。

  1. 对于系统不识别按键消息,处理流程比较简单,将当前按键消息直接发送给root节点,如果上层应用希望对该按键进行处理,只需要重载root节点的处理函数即可。该机制有助于实现特定行业按键功能,如微机保护的信号复归按键、分合闸按键等。
  2. 系统识别的按键主要是上、下、左、右、确定、取消等按键,这类按键是GUI模块内部约定使用的按键,处理流程如下:
if (存在当前控件)
{
	发送给当前控件
	if (当前控件未处理)
	{
		发送给当前窗口
		if (当前窗口未处理)
		{
			发送给root节点,上层应用可以重载root节点的处理流程.
		}
	}
}

定时消息采用注册方式进行,当一个窗口打开和关闭时,系统定时消息函数更新为当前窗口及其包含控件的消息函数列表,窗口销毁时恢复为父窗口的定时消息函数。

除了定时、按键消息外,GUI系统中还包括其它消息:

  1. 窗口创建消息,在一个窗口及其包含的所有控件创建完毕后调用,用户可以重载该函数以完成额外的初始化信息,一般情况下,该函数会被重载。
  2. 窗口撤消消息,在一个窗口即将被撤消前调用。
  3. 窗口重新绘制消息。
  4. ……

基于消息系统,各界面程序由一系列消息函数组成,整体结构如下所示:

窗口列表
{
	{窗口ID, 消息函数列表地址, 消息函数个数},
	{窗口ID, 消息函数列表地址, 消息函数个数},
	{窗口ID, 消息函数列表地址, 消息函数个数},
	……
}

窗口A消息函数列表
{
	{控件或窗口ID, 消息类型,消息响应函数},
	{控件或窗口ID, 消息类型,消息响应函数},
	{控件或窗口ID, 消息类型,消息响应函数},
	……
}

消息响应函数A
消息响应函数B
消息响应函数C
……

◇◇◇

为了提升用户体验,液晶界面支持汉子是最基本的需求,但很遗憾的是很多工业嵌入式系统中flash资源很小,即使最基本的16*16字库都放不下。此时,需要提炼所有用到的汉字字库,并重新进行编码。

考虑静态组织架构,这部分工作属于配置软件范畴。实际上配置软件中恰好包含了微机保护各型号装置所有字符串信息,然后重新组织并编码。

同理,各窗口界面绘制也属于配置软件范畴,然后生成界面资源文件,便于界面程序访问。配置软件中液晶界面绘制部分如下图所示:
在这里插入图片描述

6.7.4 GUI模块和整体架构的关系

限于篇幅,本章未能详细叙述GUI模块迭代过程的诸多细节,但即使如此,大家也容易理解GUI模块的迭代路径:需求推动,持续适应性改造。时间会帮助我们走出很远,一套适用于微机保护的GUI模块被我们慢慢的构建成功了。类同于GUI模块,很多优秀模块引入时都需要进行类似改造。

在构建GUI模块过程中,多处和整体架构存在关联关系,如:

  1. 液晶界面资源,字符串信息,汉字重新编码等功能需要配置软件配合,而这些需求会进一步推动配置软件迭代升级。
  2. 现场用户经常都有一些特殊的界面需求,采取硬编码策略会导致现场工程版本繁多。类似组态软件,可通过维护软件组织这类特殊界面。
  3. 界面程序编写时需要反复调试,构建虚拟设备驱动层,基于windows虚拟环境仿真调试,可以加快研发进度。
  4. 功能按键模块脚本化改造。
  5. ……

因此,构建复杂模块,会强迫我们考虑全局,会进一步提升我们对架构的感觉,复杂模块构建多了,自然也就会具备架构设计能力。

——————————————

返回目录

我是小马儿,一个渴望良知与灵魂的嵌入式软件工程师,欢迎您的陪伴与同行,如感兴趣可加个人微信号nzn_xiaomaer交流,需备注“异维”二字。

猜你喜欢

转载自blog.csdn.net/zhangmalong/article/details/106864758
6.7