嵌入式软件可靠性设计需要注意些什么?

2023-07-06 21:14:13 | 来源:面包芯语

设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。


(相关资料图)

这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。

1、判错

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

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

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

unsignedintWriteData(unsignedintaddr){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、判断实参是否合法

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

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

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

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

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

4、防止指针越界

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

5、防止数组越界

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

#defineREC_BUF_LEN100unsignedcharRecBuf[REC_BUF_LEN];…//其它代码voidUart_IRQHandler(void){staticintRecCount=0;//接收数据长度计数器…//其它代码if(RecCount

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

#defineREC_BUF_LEN100unsignedcharRecBuf[REC_BUF_LEN];if(len

6、数学算数运算

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

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

#includesignedlongsl1,sl2,result;/*初始化sl1和sl2*/if((sl2==0)||((sl1==LONG_MIN)&&(sl2==-1))){//处理错误}else{result=sl1/sl2;}

加法溢出检测:

a)无符号加法

#includeunsignedinta,b,result;/*初始化a,b*/if(UINT_MAX-a

b)有符号加法

#includesignedinta,b,result;/*初始化a,b*/if((a>0&&INT_MAX-ab)){//处理溢出}else{result=a+b;}

乘法溢出检测:

a)无符号乘法

#includeunsignedinta,b,result;/*初始化a,b*/if((a!=0)&&(UINT_MAX/a

b)有符号乘法

#includesignedinta,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.unsignedchari;for(i=0;i<256;i++){…}b.unsignedchari;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显示屏上的一个数字不正常的被改变。

经过一段时间的调试,问题被定位到下面的一段代码中:

intSensorData[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_0000~0x10007FFF内存储原码,在0x1000_9000~0x10009FFF内存储反码,在0x1000_B000~0x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:

LR_IROM10x000000000x00080000{;loadregionsize_regionER_IROM10x000000000x00080000{;loadaddress=executionaddress*.o(RESET,+First)*(InRoot$$Sections).ANY(+RO)}RW_IRAM10x100000000x00008000{;保存原码.ANY(+RW+ZI)}RW_IRAM30x100090000x00001000{;保存反码.ANY(MY_BK1)}RW_IRAM20x1000B0000x00001000{;保存异或码.ANY(MY_BK2)}}

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

uint32plc_pc=0;//原码__attribute__((section("MY_BK1")))uint32plc_pc_not=~0x0;//反码__attribute__((section("MY_BK2")))uint32plc_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,未选择扇区****************************************************************/voidRamToFlash(uint32dst,uint32src,uint32no,uint8ProgStart){PLC_ASSERT("Sectornumber",(dst>=0x00040000)&&(dst<=0x0007FFFF));PLC_ASSERT("Copybytesnumberis512",(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()函数时,循环只设置了一个不充分的结束条件。

原代码简化如下所示:

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

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

HRESULTGetMachineName(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

17、系统自检

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

18、其它一些编程建议:

本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

往期推荐

《嵌入式Linux驱动大全》

嵌入式软件可靠性设计需要注意些什么?

【寻道大千】开服活动攻略

7省区近百件(组)文物精品亮相山西 展现黄河流域根祖文化

疑似曹操出行车辆行驶中电池掉落?官方回应:系换电站测试车,工作人员操作失误所致

TCL官宣企业“3050”碳中和目标及行动计划 李东生:把握机遇实现企业高质量发展

脑机融合智能升级 脑虎科技发布多项阶段性科研新成果|2023世界人工智能大会

国家自然科学基金委员会:女性科研人员申请“杰青”年龄限制放宽至48岁

德迈仕(301007)龙虎榜数据(07-06)

俄媒公布搜查普里戈任住所和办公室画面:发现假发、武器等-聚焦

中国在人工智能领域优势在何处? 关注

北京市元宇宙产业创新中心启动筹建|速看料

中校的媳妇(中校的温存小娇妻)|环球新要闻

双预警!又一次较强降水过程,并伴有短时强降水和雷暴大风! 当前看点

【全球速看料】梅雨期新一轮强降水将至 湖北发布风险提示

天天资讯:弘扬农耕文化 传承乡村技艺——第一届全国农民技能大赛见闻

既没在手订单也未形成收入,两个交易日却涨超30%!“减速器”大牛股发声了

环球关注:长春专治男科医院排行榜【公开宣布排名】长春看男科有哪些医院好

“夜游”美术馆真美妙! 当前快播

湖北潜江:二孩及以上家庭最高可贷72万 “带押过户”业务将推行

智利发生5.9级地震 震源深度130千米

河口区开展公交客运行业综合应急演练

致远互联签约甘肃建材集团 构建一体化协同平台 世界讯息

中钢国际(000928.SZ):公司在国内的主要竞争对手为中冶系的工程公司

175dt梦幻西游答题器封号吗 175dt梦幻西游答题器封号介绍

世界即时:如何选择三星Galaxy S20和Galaxy S20以及Galaxy S20 Super

申万宏源傅静涛:A股下半年延续震荡市,主题行情或愈加频繁

天天速讯:联想发力智慧医疗领域 探索慢病管理新路径

智信精密(301512.SZ):网上路演时间为7月7日14:00-17:00

上海打造“15分钟就业服务圈” 高质量服务毕业生就业_环球微动态

天天视讯!开润股份部分董事及高管完成减持151.03万股

Copyright   2015-2022 太平洋运动网 版权所有  备案号:豫ICP备2022016495号-17   联系邮箱:93 96 74 66 9@qq.com