小议软件架构设计要点

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小议软件架构设计要点相关的知识,希望对你有一定的参考价值。

参考技术A

  如何更好地进行软件架构设计 这是软件工程领域中一个永恒的重点话题 过去几十年来 国际软件工程界在软件架构设计方面已经获得了长足发展 大量图书 文章和文献记载了这方面的成熟经验与成果 软件架构设计往往是一件非常复杂的工作 涉及到很多细节和方方面面 可探讨的话题也非常之多 囿于篇幅限制 以下只能根据笔者个人理解 遴选出软件架构设计的个别要点 结合当前流行的敏捷软件工程思想 与大家分享一下自己在软件架构设计方面的心得和体会

   架构决定成败

  软件架构是软件产品 软件系统设计当中的主体结构和主要矛盾 任何软件都有架构 哪怕一段短小的HelloWorld程序 软件架构设计的成败决定了软件产品和系统研发的成败 软件架构自身所具有的属性和特点 决定了软件架构设计的复杂性和难度

  这几年流行一个说法(管理谚语) 细节决定成败 这句话其实只说对了一半 细节确实很重要 很多项目 产品就输在细节的执行上 一方面 战术细节固然很重要 但另一方面 战略全局也同样重要 对应的我们可以说 战略决定成败 战略性失败 就好比下一盘围棋 局部下得再漂亮 再凌厉 如果罔顾大盘 己方连空都不够了 还有官子(细节)获胜的机会吗?必然是中盘告负

  类似地 正确的软件架构设计 应该既包括战略全局上的设计 也包括战术细节(关键路径)上的设计 有一种错误的观点认为 软件架构设计只要分分层和包 画一个大体的轮廓草图 就完事了 这种 纸上谈兵 型的架构师行为是非常有害的 事实上 既然软件架构是软件建筑的主体结构 隐蔽工程 承重墙和要害部位 那么软件架构也必然要落实到实际的算法和代码 不但要有实现代码 还要包括对这部分架构进行测试的代码 以保证获得高质量的 满足各种功能和非功能质量属性要求的架构 除了完成概念 模型设计外 软件架构师一定要参与实际的编码 测试和调试 做一位真正的hands on practitioner 这已经成为了敏捷软件工程所倡导的主流文化

   两个架构

  我们在日常的软件产品和系统开发中 实际上会遇到两种 两个部分的软件架构 即待开发的应用部分的软件架构(简称 应用架构 ) 以及既有的基础平台部分的软件架构(简称 基础架构 ) 这两部分架构之间是互为依赖 相辅相成的关系 它们共同组成了整个软件产品和系统的架构

  基础架构的例子包括 NET和J EE等主流的基础平台和各种公共应用框架 由基础库API 对象模型 事件模型 各种开发和应用的扩展规则等内容组成 我们只有熟悉基础架构的构造细节 应用机理 才能有效地开发出高质量 高性能的上层应用 然而 开发一个面向最终用户的软件应用系统和产品 仅仅掌握一般的计算机高级编程语言知识和基础平台架构 API的使用知识显然是不够的 我们还需要根据客户应用的类型和特点 在基础架构之上 设计出符合用户要求的高质量应用软件

  熟悉OOA OOD抽象建模技术 设计原则以及架构模式和设计模式等等方法技术 不但有助于我们更好地理解和利用基础平台架构 也有助于我们设计开发出更高质量的应用软件架构

   风险驱动 敏捷迭代的架构设计与开发

  软件架构将随着软件产品和系统的生命周期而演化 其生命期往往超过了一个项目 一次发布 甚至有可能长达数年之久 因而软件架构无论对于客户还是开发商来说都是一项极其重要的资产

  软件架构的设计应该遵循什么样的开发过程?或者说 有没有更好的 成熟的软件架构设计和开发过程?回答是 世纪的软件架构设计应该优先采用敏捷迭代的开发方式和方法 与传统做法不同 敏捷迭代开发主张软件架构采用演进式设计(evolutionary design) 一个软件产品或系统的架构是通过多次迭代 乃至多次发布 在开发生命周期中逐步建立和完善起来的

  好的软件架构不是一蹴而就的 在架构设计开发过程中 我们应该尽量避免瀑布式思维 通过一个 架构设计阶段 来完成系统的架构设计乃至详细设计 然后再根据架构图纸和模型 在 编码实现阶段 按图索骥进行架构的编码与实现 这种传统做法的错误在于认为软件架构就是图纸上的模型 而不是真正可以高质量执行的源代码 几十年的软件工程实践表明 没有经过代码实现 测试 用户确认过的架构设计 往往会存在着不可靠的臆想 猜测和过度设计 过度工程 极易造成浪费和返工 导致较高的失败率

  风险是任何可能阻碍和导致软件产品/系统研发失败的潜在因素和问题 软件架构是软件产品和系统研发的主要矛盾和主要技术风险 软件架构的质量决定了整个软件系统和产品的质量 不确定性往往是软件架构设计当中一种最大的潜在风险 因此 软件架构的设计与开发应该遵循风险驱动的原则 在整个开发生命周期内至始至终维护一张风险问题清单 随着迭代的前进 根据风险的实时动态变化 首先化解和处理最主要的架构风险 再依次化解和处理次要的架构风险

   架构设计的可视化建模

  软件架构设计的难度源于软件设计问题本身的复杂性 一个复杂的软件系统往往存在大量复杂的 难于被人类所理解的细节和不确定因素 抽象与建模是人类自诞生以来就已掌握的理解复杂事物的方法 因而人类所从事的软件设计工作本质上也是一个不断建模的过程 我们可以通过各种抽象的模型和视图 从各个不同层次 宏观和微观的角度来理解复杂的软件架构 以保证作出正确和有效的设计

  有人认为 软件架构就是源代码(source codes) 以及 源代码就是设计 这种说法其实是片面的 什么是真正的软件?我们知道 最终可以在电脑上执行的真正的软件其实是二进制代码 和 借助编译器我们把高级编程语言翻译成底层的汇编语言 机器语言等 没有人能直接 完整地看到二进制程序在CPU上的实际运行状况(runtime) 人们大多只能通过各种调试工具 窗口视图等方式来间接地动态观察这些真正的软件的运行片段 因此 Java C# C++ 等等设计时(design time)源代码在本质上也是一种模型 虽然是一种经处理后可执行的静态模型 但显然它们并不是真实软件和软件架构的全部 可见 源代码模型(有时也叫实现模型)与UML模型其实都是软件架构的一种模型(逻辑反映) 差别就在于抽象层次的不同 完整的软件架构(建筑)不仅仅包括源代码(实现模型) 还包括了需求模型 分析模型 设计模型 实现模型和测试模型等等许多模型 软件架构本身就是一组模型的集合

  UML SysML是当前国际上流行的软件/系统架构可视化建模语言 在编写实际的代码之前 利用包图 类图 活动图 交互图 状态图等等各种标准图形符号对软件架构进行建模 探讨和交流各种可行的设计方案 发现潜在的设计问题 保证具体编码实现之前抽象设计的正确性 被实践证明是一种非常有效和高效 敏捷的工作方式

   架构设计的重用

  重用(Reuse)是在软件工程实践中获得高效率 高质量产品和系统开发的一种基本手段和主要途径 通过有组织的 系统和有效的重用 我们往往可以获得 倍率以上的效率提升 而一个优秀的 有长久生命力的软件架构(比方主流的一些框架软件) 其本身或其组件被重用的次数越多 其体现的价值也就越大

  软件重用有各种不同的范围 层次 粒度和类型 从函数重用 类重用 构件/组件重用 库(API)重用 到框架重用 架构重用 模式重用 再到软件设计知识 思想的重用等等 重用的效能和效果各有不同

  软件工程经过几十年的发展 已经积累了大量的软件架构模式和设计模式 它们记载 蕴藏了大量成熟 已经验证的软件设计知识 思想和经验 我们平时对各种基础平台 主流框架和API的应用和调用 本身就是一种最为普遍的重用形式 而一个优秀 成熟的软件研发组织 必然会在日常开发中注意收集各种软件设计知识和经验 建立和维护基于架构模式和设计模式等内容的软件重用知识库 积极主动和频繁地运用各种软件模式来解决实际工程问题

  框架(Framework)是一类具有高可重用度的软件 针对某一类应用或领域 它们具有非常灵活的 高度可扩展的软件架构 那么 如何才能设计出可重用的软件架构或其组件?借助于OOA OOD等抽象分析和设计技术是一种重要的方法 人们在实践中发现 往往越抽象的东西 其适应面也就越广 可重用度也就越高 相反 越具体的东西 其适应面也就越窄 可重用度也就越低 重用 意味着充分利用现成 既有的东西 成果来解决新问题或重复的问题 以 不变 应 万变 在软件架构设计中 应该主动地区分软件架构中的 不变 与 可变 之处 系统地管理好这些稳定点和变化点以适应未来的变化 这也是提高软件架构重用度 获得高质量框架设计的一种重要方法

   架构设计的权衡

  与其它所有工程行业一样 软件工程本质上也是一门讲究权衡的科学和艺术 软件架构设计的最难之处往往在于如何在各种相互竞争 矛盾的制约条件之下 作出巧妙的最佳权衡 软件架构设计的权衡水平 也是最能体现软件架构师的设计经验 能力和技巧的地方

  在软件开发和软件架构的设计过程中 从选择平台 到选择语言 选择框架 选择设计模式 选择工具…等等 我们无时不刻都需要权衡 对各种候选项作出合理评判 在架构师带领下 软件研发团队往往还需要对近期目标与远期目标 质量与速度和效率 质量与成本 功能与性能 灵活性与复杂性…等等许多彼此矛盾的设计选项 因素和约束进行细致 小心和理性的权衡

lishixinzhi/Article/program/Java/gj/201311/27294

嵌入式软件可靠性设计的编程要点

设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。

1、判错

工欲善其事必先利其器。判错的最终目的是用来暴露设计中的Bug并加以改正,所以将错误信息提供给编程者是必要的。

有时候需要将故障信息储存于非易失性存储器中,便于查看。这里以使用串口打印错误信息到PC显示屏为例,来说明一般需要显示什么信息。

编写或移植一个类似C标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为UARTprintf()。

unsigned int WriteData(unsigned int addr)
{
 	if((addr>= BASE_ADDR)&&(addr<=END_ADDR)) 
    {
		/*地址合法,进行处理*/
 	} 
    else 
    {
    	/*地址错误,打印错误信息*/
  		UARTprintf ("文件%s的第 %d 行写数据时发生地址错误,错误地址为:0x%x\\n",__FILE__,__LINE__,addr);
  		/*错误处理代码*/
  	}
}

假设 UARTprintf( ) 函数位于main.c模块的第256行,并且 WriteData() 函数在读数据时传递了错误地址0x00000011,则会执行UARTprintf()函数,打印如下所示的信息:

文件main.c的第256行写数据时发生地址错误,错误地址为:0x00000011。类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除Bug。

2、判断实参是否合法

程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。

int exam_fun( unsigned char *str ) 
{ 
    if( str != NULL )
    { 
    	//  检查“假设指针不为空”这个条件 
        //正常处理代码
    } 
    else 
    {
        UARTprintf(); // 打印错误信息
        //处理错误代码
    }
}

3、仔细检查函数的返回值

对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。

char *DoSomething()
{
    char * p;
    p=malloc(1024);
    if(p==NULL) 
    { 
    	/*对函数返回值作出判断*/
        UARTprintf(); /*打印错误信息*/
        return NULL;
    }
    retuen p;
}

4、防止指针越界

如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。

5、防止数组越界

数组越界的问题前文已经讲述的很多了,由于C不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。

#define REC_BUF_LEN 100
unsigned char RecBuf[REC_BUF_LEN];
//其它代码
void Uart_IRQHandler(void)
{
    static RecCount=0;   //接收数据长度计数器
    //其它代码
    if(RecCount < REC_BUF_LEN)
    {
        RecBuf[RecCount]=;  //从硬件取数据
        RecCount++;
        //其它代码
    } 
    else 
    {
        UARTprintf(); //打印错误信息
        //其它错误处理代码
    }}

在使用一些库函数时,同样需要对边界进行检查:

#define REC_BUF_LEN 100
unsigned char RecBuf[REC_BUF_LEN];

if(len< REC_BUF_LEN)
{
	 memset(RecBuf,0,len);  //将数组RecBuf清零
} 
else 
{
	//处理错误
}

6、数学算数运算

  • 检测除数是否为零
  • 检测运算溢出情况

有符号整数除法,仅检测除数为零就够了吗?

两个整数相除,除了要检测除数是否为零外,还要检测除法是否溢出。对于一个signed long类型变量,它能表示的数值范围为:-2147483648 ~ +2147483647,如果让-2147483648 / -1,那么结果应该是+ 2147483648,但是这个结果已经超出了signed long所能表示的范围了。

#include <limits.h>
signed long sl1,sl2,result;
/*初始化sl1和sl2*/
if((sl2==0)||((sl1==LONG_MIN) && (sl2==-1)))
{
    //处理错误
} 
else 
{
    result = sl1 / sl2;
}

加法溢出检测:

  • a)无符号加法
#include <limits.h>
unsigned int a,b,result;
/*初始化a,b*/
if(UINT_MAX-a<b)
{
	//处理溢出
} 
else 
{
	result=a+b;
}
  • b)有符号加法
#include <limits.h>
signed int a,b,result;
/*初始化a,b */
if((a>0 && INT_MAX-a<b)||(a<0) && (INT_MIN-a>b))
{
	//处理溢出
} 
else 
{
	result=a+b;
}

乘法溢出检测:

  • a)无符号乘法
#include <limits.h>
unsigned int a,b,result;
/*初始化a,b*/
if((a!=0) && (UINT_MAX/a<b)) 
{
	//
} 
else 
{
	result=a*b;
}
  • b)有符号乘法
#include <limits.h>
signed int a,b,tmp,result;
/*初始化a,b*/
tmp=a * b;
if(a!=0 && tmp/a!=b)
{
	//
} 
else 
{
	result=tmp;
}

7、其它可能出现运行时错误的地方

运行时错误检查是C 程序员需要加以特别的注意的,这是因为C语言在提供任何运行时检测方面能力较弱。对于要求可靠性较高的软件来说,动态检测是必需的。

因此C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。

8、编译器语义检查

为了更简单的设计编译器,目前几乎所有编译器的语义检查都比较弱小,加之为了获得更快的执行效率,C语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。

C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码( * ((void( * )())0))()来调用位于0地址的函数。

C语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。下面的两个例子都是死循环,如果在不常用分支中出现类似代码,将会造成看似莫名其妙的死机或者重启。

a. unsigned char i;                  
   for(i=0;i<256;i++)  {}              
b. unsigned chari;
   for(i=10;i>=0;i--) {}

对于无符号char类型,表示的范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无线执行)。需要说明的是,赋值代码i=256是被C语言允许的,即使这个初值已经超出了变量i可以表示的范围。C语言会千方百计的为程序员创造出错的机会,可见一斑。

假如你在if语句后误加了一个分号改变了程序逻辑,编译器也会很配合的帮忙掩盖,甚至连警告都不提示。代码如下:

if(a>b);          	   //这里误加了一个分号
	a=b;               //这句代码一直被执行

不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:

if(n<3)
return    //这里少加了一个分号
logrec.data=x[0];
logrec.time=x[1];
logrec.code=x[2];

这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。

可以毫不客气的说,弱小的编译器语义检查在很大程度上纵容了不可靠代码可以肆无忌惮的存在。

上文曾提到数组常常是引起程序不稳定的重要因素,程序员往往不经意间就会写数组越界。一位同事的代码在硬件上运行,一段时间后就会发现LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:

int SensorData[30];
for(i=30;i>0;i--)
{
    SensorData[i]=;}

这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素SensorData[30],但C语言却默许这么使用,并欣然的按照代码改变了数组元素SensorData[30]所在位置的值。

SensorData[30]所在的位置原本是一个LCD显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个Bug。

9、关键数据多区备份,取数据采用“表决法”

RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。

可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。

可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。

假如设备的RAM从0x1000_0000开始,我需要在RAM的0x1000_00000x10007FFF内存储原码,在0x1000_90000x10009FFF内存储反码,在0x1000_B000~0x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:

LR_IROM1 0x00000000 0x00080000 { ; load region size_region
ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}

RW_IRAM1 0x10000000 0x00008000 { ;保存原码
.ANY (+RW +ZI )
}

RW_IRAM3 0x10009000 0x00001000{ ;保存反码
.ANY (MY_BK1)
}

RW_IRAM2 0x1000B000 0x00001000 { ;保存异或码
.ANY (MY_BK2)
}
}

如果一个关键变量需要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码进行初始化。

uint32 plc_pc=0; //原码
__attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0; //反码
__attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //异或码

当需要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做判断,取至少有两个相同的那个值。

为什么选取异或码而不是补码?这是因为MDK的整数是按照补码存储的,正数的补码与原码相同,在这种情况下,原码和补码是一致的,不但起不到冗余作用,反而对可靠性有害。

比如存储的一个非零整数区因为干扰,RAM都被清零,由于原码和补码一致,按照3取2的“表决法”,会将干扰值0当做正确的数据。

10、非易失性存储器的数据存储

非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。

一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。

对于因干扰导致程序跑飞到写非易失性存储器函数,还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的,应该在源头进行阻截。

11、软件锁

软件锁可以实现但不局限于环环相扣。对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。

比如,向Flash写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写Flash子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入Flash。

由于写Flash语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序,也能大大降低误写的风险。

/***************************************************************
* 名称:RamToFlash()
* 功能:复制RAM的数据到FLASH,命令代码51。
* 入口参数:dst 目标地址,即FLASH起始地址。以512字节为分界
* src 源地址,即RAM地址。地址必须字对齐
* no 复制字节个数,为512/1024/4096/8192
* ProgStart 软件锁标志
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区
****************************************************************/
void RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)
{
    PLC_ASSERT("Sector number",(dst>=0x00040000)&&(dst<=0x0007FFFF));
    PLC_ASSERT("Copy bytes number is 512",(no==512));
    PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5));
    paramin[0] = IAP_RAMTOFLASH; // 设置命令字
    paramin[1] = dst; // 设置参数
    paramin[2] = src;
    paramin[3] = no;
    paramin[4] = Fcclk/1000;
    if(ProgStart==0xA5) //只有软件锁标志正确时,才执行关键代码
    {
        iap_entry(paramin, paramout); // 调用IAP服务程序
        ProgStart=0;
    }
    else
    {
		paramout[0]=PROG_UNSTART;
    }
}

该程序段是编程lpc1778内部Flash,其中调用IAP程序的函数iap_entry(paramin, paramout)是关键安全代码,所以在执行该代码前,先判断一个特定设置的安全锁标志ProgStart,只有这个标志符合设定值,才会执行编程Flash操作。

如果因为意外程序跑飞到该函数,由于ProgStart标志不正确,是不会对Flash进行编程的。

12、通信数据的检错

通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:

  • 制定协议时,限制每帧的字节数;
    每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的CAN收发器规定每帧数据不得多于8字节,对于RS485,基于RS485链路应用最广泛的Modbus协议一帧数据规定不超过256字节。因此,建议制定内部通讯协议时,使用RS485时规定每帧数据不超过256字节;

  • 使用多种校验
    编写程序时应使能奇偶校验,每帧超过16字节的应用,建议至少编写CRC16校验程序。

  • 增加额外判断

    • 增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。

    • 增加超时判断。当一帧数据接收 到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。

    可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。

    这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。

  • 重传机制
    如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。

13、开关量输入的检测、确认

开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于1ms。

14、开关量输出

开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。

15、初始化信息的保存与恢复

微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于Flash中的数据相对不易被破坏,可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用Flash中的值进行恢复。

16、while循环

有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。

2003年8月11日发生的W32.Blaster.Worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了Windows分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用GetMachineName()函数时,循环只设置了一个不充分的结束条件。

原代码简化如下所示:

HRESULT GetMachineName ( WCHAR *pwszPath,
WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
{
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    while ( *pwszTemp != L’\\\\’ )               /* 这句代码循环结束条件不充分 */
        *pwszServerName++= *pwszTemp++;
    /*… */
}

微软发布的安全补丁MS03-026解决了这个问题,为GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):

HRESULT GetMachineName( WCHAR *pwszPath,
WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
{
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
    while (*pwszTemp != L’\\\\’ ) && (*pwszTemp != L’\\0)
            && (pwszServerName<end_addr))  /*充分终止条件*/
            *pwszServerName++= *pwszTemp++;
    /*… */
}

17、系统自检

对CPU、RAM、Flash、外部掉电保存存储器以及其他线路自检。

18、其它一些编程建议:

  • 深入理解嵌入式C语言以及编译器
  • 细致、谨慎的编程
  • 使用好的风格和合理的设计
  • 不要仓促编写代码,写每一行的代码时都要三思而后行:可能会出现什么样的错误?是否考虑了所有的逻辑分支?
  • 打开编译器所有警告开关
  • 使用静态分析工具分析代码
  • 安全的读写数据(检查所有数组边界…)
  • 检查指针的合法性
  • 检查函数入口参数合法性
  • 检查所有返回值
  • 在声明变量位置初始化所有变量
  • 合理的使用括号
  • 谨慎的进行强制转换
  • 使用好的诊断信息日志和工具

以上是关于小议软件架构设计要点的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式软件可靠性设计的编程要点?

嵌入式软件可靠性设计的编程要点

SoC嵌入式软件架构设计之七:嵌入式系统固件的系统区文件系统设计

系统架构设计师第七章 软件架构设计

软考 系统架构设计师软件架构设计① 软件架构的概念

软件架构设计-软件架构风格分层架构