在前面的章节中,我们已经多次提及配置软件和维护软件,大家或许也认识到它们是整个平台架构中不可或缺的一环。然而现实经常是无奈的,我的职场生涯中,这两个软件模块也是命运多舛。
国内的软件氛围不好,企业舍得为硬件设备花钱,但都不乐意为纯软件项目花钱,会有一种想当然的感觉软件不值钱。在这种大环境下,企业也只好将更多资源往嵌入式设备领域倾斜,至于外围软件,能省则省吧。
如我早期研发经历,要做一款新产品,只要市场部认可,领导一般会大力支持。但如果想写一款维护软件,公司会立马意识到投入大且不产生直接经济效益,因此是否能争取到资源就不确定了。
记得那时做一些调试工作非常艰难,如想观察某一变量,只好临时修改液晶输出界面。不仅啰嗦,而且受限,如想同时观察较多的变量就比较困难,还想观察其他变量就需要修改程序重新来过。
在一次规约调试时,我发现自己没有了调试手段,因为是项目组自定义规约,连借助外力的渠道也被堵死了。被逼无奈,我用VC和MFC框架快速构建了一个简易对话框,虽然是手动组帧,但至少搭建出一简陋的调试环境。
再后来,需要调试的功能越来越多,对话框放不下了,我被迫重新组织了界面,左侧outlook触发控件,右侧为了省事,干脆约定为表格算了。
此时,其他项目组看到我们有这个好东东,立马想挪过去用。然而,愿望是美好的,现实时残酷的,大家发现我这个简陋版维护软件和具体产品紧紧绑定在一起,根本挪不动。
更为奇葩的是,你没有维护软件时公司不会给你资源去做,但当你弄了一简陋版本后,大家会默认你已经搞定了维护软件,各种需求立马纷至沓来,让人苦不堪言。
相对于配置软件,维护软件还算幸运,毕竟总有一些公司会给用户额外赠送免费维护软件,我们没见过猪跑但总还是吃过猪肉的,因此大家一有空还是会努力尝试去构建一款维护软件,但大家在很长一段时间内压根认识不到配置软件的价值。
早期产品研发时,也经常面临多款产品数据不一致,当时我们习惯手动构建各种数据表;后来碰到液晶模块需要提炼汉字的场合,大家在合力构建一款小程序;碰到GUI模块需要绘制液晶界面时,我们基于MFC类库drawCli例程快速构建出一个小程序;……,总之,所有的行为都是被动的下意识行为,从未主动的去思考过这些小程序对整个架构的意义。
在跨国公司研发期间,听架构师讲解程序框架时,经常会自然而然的提及配置软件和维护软件,尤其是配置软件,经常处于核心的位置。也可能是厚积薄发,过往那些零零碎碎的痛苦经历,汇成了灵感,让我突然有一种豁然顿悟的感觉。从此,在我心目中,配置软件和维护软件不再是可有可无的角色,而是架构的有机一环。
◇◇◇
从整体架构的角度思考,配置软件用于弥合统一平台之上各产品的差异部分,是属于研发阶段的配套软件。
![](/qrcode.jpg)
我们以前无意识做的各种小程序都属于配置软件的范畴,如汉字字库提炼、液晶界面绘制等。但如果将配置软件作为单独概念提炼出来后,就会衍生出很多新的价值。
一个很经典的例子是关于字符串。在嵌入式系统中,字符串长度颇让人尴尬,如下所示保护设定值结构。
struct TSetting
{
char szName[32]; /* 定值名称 */
char szDescribe[64]; /* 定值描述 */
......
}
我们约定定值名称为32个字符串,最大16个汉字。这个长度一般是够的,但每年总有一些特殊场合不够,碰到较真的客户,大家就会被折腾的颇为苦恼。仅名称还算是好的,定值描述信息更为尴尬,大部分不需要,但恰恰有少部分需要较长的字符串。(注:上述结构中名称在前描述在后包含着一个程序技巧,针对特定现场定值名称过长的情况,可以占用定值描述部分,此时可约定定值描述等于定值名称)
定值名称或描述大部分是静态字符串,一个更巧妙的策略是将所有字符串信息压缩存储起来,而以索引的方式去访问这些字符串。此时结构如下:
struct TSetting
{
WORD wName; /* 定值名称 */
WORD wDescribe; /* 定值描述 */
......
}
这种策略非常节约flash资源,但要达到这种效果,必须收集汇总去重整个产品中所有用到的静态字符串信息。字符串信息分布在几乎所有模块中,如定值、液晶界面等,在没有统一的配置软件之前,这几乎是一个不可能完成的任务。
再比如字节顺序问题,不同的芯片有不同的字节顺序,主要有大端模式和小端模式两类。原先设备接受到一个特定的参数文件后,必须重新调整字节顺序后才能使用。有配置软件支撑,我们可以直接生成特定字节顺序的参数文件,此时设备不仅不需要调整字节顺序,甚至都不需要将参数文件从flash拷贝到ram空间,仅需要完成指针映射即可。
很多国际化产品的说明书都很厚,几百上千页的,对比自己产品可怜的几十页说明书,好尴尬!在跨国公司研发的那段经历,我终于知道这些厚厚的说明书是如何泡制出来的了,大部分附录部分都是由配置软件自动生成的,真正人写的部分和我们差不了太多。
平时大家可以留意,如果哪些厂家的说明书有着厚厚的附录,那么该参加极有可能程序架构水平已经迭代到了一定的高度,这些公司的产品质量应该也是比较高的,如果是新人,或许这类公司也更适合加盟。
随着高复用严格平台化架构的持续迭代,配置软件会被赋予更多的功能。如我当初参与的项目中,抽取各种可复用模块并自动生成配置表、程序多CPU分布时自动生成各CPU之间通讯配置表、自动生成用户说明书附录部分等等。
记得那个架构师经常感叹,配置软件的高度决定了架构设计的高度。瞅一眼自己那个颇为寒酸的配置软件,两相比较,还好,我们还有比较大的成长空间,>_<。
◇◇◇
早期,维护软件在我心目中,就是我们自己人偶然用一下的辅助软件,顶多额外用于帮助工程人员现场设备调试。将维护软件作为架构设计中单独一环考虑后,维护软件的范畴就扩大了。如果说配置软件是研发阶段中的软件,那么维护软件就是针对成品的软件。
工业产品一般对可靠性要求极高,测试一直是比较头疼的一环。为了减少测试工作量,很多重复的测试工作我们会通过自动化测试完成。为了构建自动化测试框架,就需要维护软件配合。
如微机保护的自动化测试,通过维护软件触发继保测试仪加特定电气量,并读取继保遥测值,可以很容易的判断计算精度。如果能将各种电气量输出验证过程组织成脚本,并自动生成报表,就已经构建出一套自动化测试环境。
既然可以指导测试,就可以指导生产。硬件总会存在一定的差异,设备出厂时需要进行系数矫正。这是一个需要耐心且颇为繁琐的工作,如由维护软件自动触发并进行自动系数矫正,不仅效率高,精度也比人工校验的高。
这些都可以算是维护软件的初级功能,在跨国公司研发的那段经历,真正触动我的是基于维护软件的二次研发环境。基于高复用软件模块和脚本化等策略,这样做出来的产品自然具备着强大的二次工程定制化能力。基于平台、产品、工程三层研发体系,很多特殊需求都可以在现场完成。
甚至,随着硬件的快速发展,跨国公司可以在本部仅进行通用产品研发,然后由各地方公司人员完成具体产品研发,各自发挥自己的优势。我带领的团队就曾经完成过很多这类定制化开发任务。
需求的持续迭代,让维护软件从最初的一个简单对话框,演变为一个庞杂的软件模块了。
◇◇◇
厚积薄发后的瞬间顿悟,虽然可以帮助我重新理解配置软件和维护软件,但并不能帮助我自动构建出来这两个软件。现实世界经常是无奈的,人员缺、时间紧、任务重、看不到短期效益,诸多因素决定了公司不可能投入资源单独进行配置软件和维护软件的研发。所有的工作都必须在产品研发过程中见缝插针,但面对各种庞杂的功能,经常让人有一种无力感。
如何破解这个难题呢?前文已经叙述过,我采取的策略是“借人”。我平时会承担很多公司招聘、培训等工作,我很清楚,职场中最好的培训不是学,而是用。是否可以借用这些新人的力量,来构建配置软件和维护软件呢?
为了让新人快速成长,完成编程规范培训后,每个人都需要动手写一点程序以加深记忆。但新人能力有限,你不可能让他们去面对复杂的架构产品,最好是一个单纯的软件模块。维护软件和配置软件恰好是由诸多零碎的功能构成的,如果能构建出一套合适的软件架构,让新人去分别完成这些零零碎碎的功能,应该是一个好策略。
幸运的是,我做到了。
◇◇◇
当初跨国公司研发时,配置软件和维护软件是完全不同的两个软件,实际上这两个软件是由完全不同的两个小组完成的。我们没人,能否将这连个软件整合成一个呢?
要想融合配置软件和维护软件,首先要统一界面。综合各种因素,统一后的界面示意图如下:
一些具体的界面示例如下:
整个界面分为三部分,顶层为菜单栏和工具栏,侧重于一些全局功能,如配置软件的参数生成,维护软件的通讯连接等,注意不包含任何与特定功能相关的菜单项目。
左侧为树视图,用于信息组织,如配置软件的各种配置子节点,或者各种维护软件功能。树视图内部以xml格式组织,不仅能保证界面的灵活,也可以降低程序模块的耦合度。
xml格式如下示意:
<?xml version="1.0"?>
<funtree>
<fun name="对时" class="CSystemTimeFun" ……/>
<dir name="查询">
<fun name="遥测" class="CReadAIFun" ……/>
<fun name="电度" class="CCIFun" ……/>
<fun name="遥信" class="CYXFun" ……/>
</dir>
</funtree>
右侧上半截为特定界面,每个界面与左侧节点类型有关,可以是设置界面,也可以是维护界面。可以多个界面并存,以Tab方式组织。为了界面复用,默认提供表格、属性页等多种界面风格用于子类化(subClass)。一些特定的功能界面需要额外的菜单或工具栏,统一布局在该子界面顶端。
右侧下半截为日志视图,用于提示各种操作记录、参数解析错误信息、查询记录、规约帧等功能,这些功能大多由框架提供。
早年在阅读《MFC深入浅出》那本书时,了解到MFC类库中有一套宏定义,可以依据字符串信息直接创建类。这相当于在左侧点击某个树节点,就可以直接构建出一个特定界面并在右上侧显示出来,相当于框架程序部分不需要知道上层程序。大家注意到xml配置信息中class属性吗,就是用于设置类名称的。
自己手痒,将MFC这套宏机制给单独提炼了出来。当时全球都在庆祝2000年世纪跨年,我就将这套宏名称定义为了2000,N和Z都是淘气的2呢! 代码示例如下:
/*****************************************************/
/* .h文件部分 */
/*****************************************************/
//支持动态创建,做法同MFC类库
class CFunBase;
struct CRunNZNClass
{
QString m_lpszClassName;
CFunBase* (*m_pfnCreateObject)();
CRunNZNClass* m_pNextClass;
static CRunNZNClass* m_pFirstClass;
static CRunNZNClass* load(QString lpszClassName);
CFunBase* createObject();
};
struct CRunNZNClassInit{CRunNZNClassInit(CRunNZNClass* pNewClass);};
#define DECLARE_FUN(class_name) \
public: \
static CRunNZNClass _nzn_##class_name; \
virtual CRunNZNClass* getRunNZNClass() const; \
static CFunBase* createObject();
#define IMPLEMENT_FUN(class_name) \
CRunNZNClass class_name::_nzn_##class_name = { \
#class_name, \
class_name::createObject, \
NULL \
}; \
static CRunNZNClassInit _init_##class_name(&class_name::_nzn_##class_name); \
CRunNZNClass* class_name::getRunNZNClass() const \
{ return &class_name::_nzn_##class_name; } \
CFunBase* class_name::createObject() \
{ return new class_name; }
/*****************************************************/
/* .cpp文件部分 */
/*****************************************************/
CRunNZNClass* CRunNZNClass::m_pFirstClass = 0;
CRunNZNClass* CRunNZNClass::load(QString lpszClassName)
{
CRunNZNClass* pClass = 0;
for (pClass = m_pFirstClass; pClass != NULL; pClass = pClass->m_pNextClass)
{
if (lpszClassName == pClass->m_lpszClassName)
return pClass;
}
return 0;
}
CFunBase* CRunNZNClass::createObject()
{
if (m_pfnCreateObject == 0)
return 0;
CFunBase* pFunBaseObject = 0;
pFunBaseObject = (*m_pfnCreateObject)();
return pFunBaseObject;
}
CRunNZNClassInit::CRunNZNClassInit(CRunNZNClass* pNewClass)
{
pNewClass->m_pNextClass = CRunNZNClass::m_pFirstClass;
CRunNZNClass::m_pFirstClass = pNewClass;
}
限于篇幅,这套代码的具体细节我就不展开叙述了,大家如感兴趣可参考《MFC深入浅出》一书。借助这套神奇的宏,每个新人锻炼的代码可以拿过来直接用,仅需要简单修改xml文件即可。
◇◇◇
配置软件的主要功能是统一配置产品属性,验证后生成各类代码、参数、文档等。在具体实施时,我们习惯采用“统一配置,分散发布”的策略。采用这种策略的最大优点是减少了信息的交互索引,如统一组织定值和液晶界面,定值名称的字符串信息可直接用于定值液晶界面结构。如果分为多个配置软件,就需要生成参数互相索引。
配置文件原先我们采用了数据库,后期统一调整为JSON格式。因为装置的各类配置信息会自然的呈现为树状结构,采用JSON格式会比较契合。更为关键的是数据库必须统一组织,而JSON格式可以自然扩展,便于各模块独立组织整体复用。
配置软件中比较复杂的是参数合法性验证部分,各节点本身的验证不难,难的是各类需要全局信息的验证项。为了支持这类验证功能,我们借鉴了编译系统中“遍”的概念,可基于全体信息构建查询映射表,独立抽象这些复杂验证模块。
相对与配置软件,维护软件额外增加的主要功能项是通讯和规约,一般需要提供多种通讯功能和规约,如串口、以太网、虚拟设备通讯等。配置软件的整体架构图如下所示:
◇◇◇
维护软件和配置软件的设计细节,如果详细展开叙述,估计都可以独立成书了。本章限于篇幅,侧重于让大家从程序架构的角度重新认识配置软件和维护软件。只要认知到位了,配置软件和维护软件虽然颇具工作量,但其本身的架构设计并不难。
——————————————
我是小马儿,一个渴望良知与灵魂的嵌入式软件工程师,欢迎您的陪伴与同行,如感兴趣可加个人微信号nzn_xiaomaer交流,需备注“异维”二字。