嵌入式软件可靠性设计的编程要点有哪些?
本文整理来自网络
设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。1、判错
工欲善其事必先利其器。判错的最终目的是用来暴露设计中的Bug并加以改正,所以将错误信息提供给编程者是必要的。有时候需要将故障信息储存于非易失性存储器中,便于查看。这里以使用串口打印错误信息到PC显示屏为例,来说明一般需要显示什么信息。
编写或移植一个类似C标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为UARTprintf()。unsignedintWriteData(unsignedintaddr){if((addrBASEADDR)(addrENDADDR)){地址合法,进行处理}else{地址错误,打印错误信息UARTprintf(文件s的第d行写数据时发生地址错误,错误地址为:0xx,FILE,LINE,addr);错误处理代码}
假设UARTprintf()函数位于main。c模块的第256行,并且WriteData()函数在读数据时传递了错误地址0x00000011,则会执行UARTprintf()函数,打印如下所示的信息:
文件main。c的第256行写数据时发生地址错误,错误地址为:0x00000011。类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除Bug。2、判断实参是否合法
程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。intexamfun(unsignedcharstr){if(str!NULL){检查假设指针不为空这个条件。。。正常处理代码}else{UARTprintf();打印错误信息处理错误代码}}3、仔细检查函数的返回值
对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。charDoSomething(){charp;pmalloc(1024);if(pNULL){对函数返回值作出判断UARTprintf();打印错误信息returnNULL;}retuenp;}4、防止指针越界
如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。5、防止数组越界
数组越界的问题前文已经讲述的很多了,由于C不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。defineRECBUFLEN100unsignedcharRecBuf〔RECBUFLEN〕;其它代码voidUartIRQHandler(void){staticRecCount0;接收数据长度计数器其它代码if(RecCountRECBUFLEN){RecBuf〔RecCount〕;从硬件取数据RecCount;其它代码}else{UARTprintf();打印错误信息其它错误处理代码}}
在使用一些库函数时,同样需要对边界进行检查:defineRECBUFLEN100unsignedcharRecBuf〔RECBUFLEN〕;if(lenRECBUFLEN){memset(RecBuf,0,len);将数组RecBuf清零}else{处理错误}6、数学算数运算检测除数是否为零检测运算溢出情况
有符号整数除法,仅检测除数为零就够了吗?
两个整数相除,除了要检测除数是否为零外,还要检测除法是否溢出。对于一个signedlong类型变量,它能表示的数值范围为:21474836482147483647,如果让21474836481,那么结果应该是2147483648,但是这个结果已经超出了signedlong所能表示的范围了。includelimits。hsignedlongsl1,sl2,result;初始化sl1和sl2if((sl20)((sl1LONGMIN)(sl21))){处理错误}else{resultsl1sl2;}
加法溢出检测:
a)无符号加法includelimits。hunsignedinta,b,result;初始化a,bif(UINTMAXab){处理溢出}else{resultab;}
b)有符号加法includelimits。hsignedinta,b,result;初始化a,bif((a0INTMAXab)(a0)(INTMINab)){处理溢出}else{resultab;}
乘法溢出检测:
a)无符号乘法includelimits。hunsignedinta,b,result;初始化a,bif((a!0)(UINTMAXab)){}else{resultab;}
b)有符号乘法includelimits。hsignedinta,b,tmp,result;初始化a,btmpab;if(a!0tmpa!b){}else{resulttmp;}7、其它可能出现运行时错误的地方
运行时错误检查是C程序员需要加以特别的注意的,这是因为C语言在提供任何运行时检测方面能力较弱。对于要求可靠性较高的软件来说,动态检测是必需的。因此C程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。8、编译器语义检查
为了更简单的设计编译器,目前几乎所有编译器的语义检查都比较弱小,加之为了获得更快的执行效率,C语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。
C语言足够灵活,对于一个数组a〔30〕,它允许使用像a〔1〕这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码(((void()())0))()来调用位于0地址的函数。C语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。下面的两个例子都是死循环,如果在不常用分支中出现类似代码,将会造成看似莫名其妙的死机或者重启。a。unsignedchari;for(i0;i256;i){}b。unsignedchari;for(i10;i0;i){}
对于无符号char类型,表示的范围为0255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无线执行)。需要说明的是,赋值代码i256是被C语言允许的,即使这个初值已经超出了变量i可以表示的范围。C语言会千方百计的为程序员创造出错的机会,可见一斑。假如你在if语句后误加了一个分号改变了程序逻辑,编译器也会很配合的帮忙掩盖,甚至连警告都不提示。代码如下:if(ab);这里误加了一个分号ab;这句代码一直被执行
不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:if(n3)return这里少加了一个分号logrec。datax〔0〕;logrec。timex〔1〕;logrec。codex〔2〕;
这段代码的本意是n3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec。datax〔0〕的结果,return后面即使是一个表达式也是C语言允许的。这样当n3时,表达式logrec。datax〔0〕;就不会被执行,给程序埋下了隐患。可以毫不客气的说,弱小的编译器语义检查在很大程度上纵容了不可靠代码可以肆无忌惮的存在。
上文曾提到数组常常是引起程序不稳定的重要因素,程序员往往不经意间就会写数组越界。一位同事的代码在硬件上运行,一段时间后就会发现LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:intSensorData〔30〕;for(i30;i0;i){SensorData〔i〕;}
这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素SensorData〔30〕,但C语言却默许这么使用,并欣然的按照代码改变了数组元素SensorData〔30〕所在位置的值,SensorData〔30〕所在的位置原本是一个LCD显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个Bug。9、关键数据多区备份,取数据采用表决法
RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。
可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的空白RAM作为隔离。可以使用编译器的分散加载机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。
假如设备的RAM从0x10000000开始,我需要在RAM的0x100000000x10007FFF内存储原码,在0x100090000x10009FFF内存储反码,在0x1000B0000x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:LRIROM10x000000000x00080000{;loadregionsizeregionERIROM10x000000000x00080000{;loadaddressexecutionaddress。o(RESET,First)(InRootSections)。ANY(RO)}RWIRAM10x100000000x00008000{;保存原码。ANY(RWZI)}RWIRAM30x100090000x00001000{;保存反码。ANY(MYBK1)}RWIRAM20x1000B0000x00001000{;保存异或码。ANY(MYBK2)}}
如果一个关键变量需要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码进行初始化。uint32plcpc0;原码attribute((section(MYBK1)))uint32plcpcnot0x0;反码attribute((section(MYBK2)))uint32plcpcxor0x00xAAAAAAAA;异或码
当需要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做判断,取至少有两个相同的那个值。
为什么选取异或码而不是补码?这是因为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复制字节个数,为512102440968192ProgStart软件锁标志出口参数:IAP返回值(paramout缓冲区)CMDSUCCESS,SRCADDRERROR,DSTADDRERROR,SRCADDRNOTMAPPED,DSTADDRNOTMAPPED,COUNTERROR,BUSY,未选择扇区voidRamToFlash(uint32dst,uint32src,uint32no,uint8ProgStart){PLCASSERT(Sectornumber,(dst0x00040000)(dst0x0007FFFF));PLCASSERT(Copybytesnumberis512,(no512));PLCASSERT(ProgStart0xA5,(ProgStart0xA5));paramin〔0〕IAPRAMTOFLASH;设置命令字paramin〔1〕dst;设置参数paramin〔2〕src;paramin〔3〕no;paramin〔4〕Fcclk1000;if(ProgStart0xA5)只有软件锁标志正确时,才执行关键代码{iapentry(paramin,paramout);调用IAP服务程序ProgStart0;}else{paramout〔0〕PROGUNSTART;}}
该程序段是编程lpc1778内部Flash,其中调用IAP程序的函数iapentry(paramin,paramout)是关键安全代码,所以在执行该代码前,先判断一个特定设置的安全锁标志ProgStart,只有这个标志符合设定值,才会执行编程Flash操作。如果因为意外程序跑飞到该函数,由于ProgStart标志不正确,是不会对Flash进行编程的。12、通信数据的检错
通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:制定协议时,限制每帧的字节数;
每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的CAN收发器规定每帧数据不得多于8字节,对于RS485,基于RS485链路应用最广泛的Modbus协议一帧数据规定不超过256字节。因此,建议制定内部通讯协议时,使用RS485时规定每帧数据不超过256字节;使用多种校验
编写程序时应使能奇偶校验,每帧超过16字节的应用,建议至少编写CRC16校验程序。增加额外判断
1)增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。
2)增加超时判断。当一帧数据接收到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。重传机制
如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。13、开关量输入的检测、确认
开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于1ms。14、开关量输出
开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。15、初始化信息的保存与恢复
微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于Flash中的数据相对不易被破坏,可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用Flash中的值进行恢复。16、while循环
有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。
2003年8月11日发生的W32。Blaster。Worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了Windows分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用GetMachineName()函数时,循环只设置了一个不充分的结束条件。
原代码简化如下所示:HRESULTGetMachineName(WCHARpwszPath,WCHARwszMachineName〔MAXCOMPUTTERNAMELENGTHFQDN1〕){WCHARpwszServerNamewszMachineName;WCHARpwszTemppwszPath2;while(pwszTemp!L’’)这句代码循环结束条件不充分pwszServerNamepwszTemp;}
微软发布的安全补丁MS03026解决了这个问题,为GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):HRESULTGetMachineName(WCHARpwszPath,WCHARwszMachineName〔MAXCOMPUTTERNAMELENGTHFQDN1〕){WCHARpwszServerNamewszMachineName;WCHARpwszTemppwszPath2;WCHARendaddrpwszServerNameMAXCOMPUTTERNAMELENGTHFQDN;while((pwszTemp!L’’)(pwszTemp!L’’)(pwszServerNameendaddr))充分终止条件pwszServerNamepwszTemp;}17、系统自检
对CPU、RAM、Flash、外部掉电保存存储器以及其他线路自检。18、其它一些编程建议:深入理解嵌入式C语言以及编译器细致、谨慎的编程使用好的风格和合理的设计不要仓促编写代码,写每一行的代码时都要三思而后行:可能会出现什么样的错误?是否考虑了所有的逻辑分支?打开编译器所有警告开关使用静态分析工具分析代码安全的读写数据(检查所有数组边界)检查指针的合法性检查函数入口参数合法性检查所有返回值在声明变量位置初始化所有变量合理的使用括号谨慎的进行强制转换使用好的诊断信息日志和工具猜你喜欢:
嵌入式必备之Git的使用
C语言、嵌入式中几个非常实用的宏技巧
小技巧数据抽象思想在嵌入式中的应用
1024G嵌入式资源大放送!包括但不限于CC、单片机、Linux等。私信回复1024,即可免费获取!
聊聊如何判断笔记本电脑散热的好坏散热,可以说是笔吧评测室第二在意的参数(排第一的是屏幕),一台电脑散热差劲,各方面体验都会下滑,可谓是牵一发动全身。根据我的印象,大概从Intel第8代酷睿开始,散热极差……
意料之外!AppleWatchS7升级信息一览,再现挤牙膏行苹果秋季发布会顺利召开,更新了iPhone13系列,新品整体还是挺有看点的,直接128G起步,加量不加价。倒是苹果的智能手表AppleWatchS7挺让人意外的,并没有网传的直……
360度无死角家用监控器怎么挑选,选购费用与效果如何家用监控器的选择有很多,不管是传统意义上的布线监控器还是现如今技术更新换代的无线WiFi网络摄像机,综合来说都适用于家庭之用,只是使用场景不同而已。如果想要一款360度无死角监……
iPhoneXsMax更新14。8系统的个人体验感受往下看是我个人的体验原本是14。6的,手机不发烫,打一把王者耗电百分10,更新14。8耗电百分13明显增多了还比14。6烫一点,画面也变暗了很多,颜色也没有以前那么光亮了……
联发科或于年底出货天玑2000,还有5nm次旗舰芯片作为安卓芯片厂商之一,如今不少人都在期待着联发科能够提高自己的竞争力,毕竟如今华为的海思麒麟由于种种原因只能遗憾退场,三星的Exynos应用范围不广,安卓阵营离有实力能够与高通……
120下海底微生物生存秘诀揭示英国《自然通讯》杂志近日发表的一项微生物学研究显示,较高的能量代谢率使一个微生物种群能够生活在海床下1000多米深、温度高达120的沉积物中。研究结果有助于阐释生物在被认为生命……
中亚商品交易中心沥青内容分发工具互联网发展到今天,各类信息已严重过载,用户的消费行为也慢慢开始发生变化。以前用户更多是主动去发现内容,所以搜索引擎的作用很关键,这个时候推荐算法的作用就很关键。为什……
午评港股迎来虎年首个交易日恒生科技指数涨超2金融界2月4日消息港股迎来虎年首个交易日,恒指高开2。48后涨幅扩大,截至午盘,恒生指数涨2。71,报24447。31点;国企指数涨2。22,红筹指数涨2。01。恒生科技指数涨……
揭秘手机隐私泄露背后的真相,荣耀Magic3系列让你从此不再现实生活中,你一定遇到过这些情况:打开购物APP,发现推荐的产品不偏不倚正合你意;打开生活APP,发现推荐的旅游城市,正是你的心之所向。接下来,你会惊奇的发现,似乎每一个手机A……
雨露均沾?vivoX70将至,同时采用联发科高通三星芯片近日网上曝光了vivo即将推出的旗舰手机X70系列的参数配置,这个系列虽然没有发布,但配置已经被曝光差不多了;作为vivo的年度旗舰手机,该系列机器的配置究竟如何呢?首先……
5G网络专用缩略语介绍3GPPThirdGenerationPartnershipProject第三代移动通信伙伴组织5G5thGeneration第五代移动通信技术……
元宇宙中的NFT,到底是什么?什么是NFT非同质化代币(NonFungibleToken),即NFT,它的字面意思是世界上独一无二、其他无法替代的币。独一无二也就意味着不可能被复制。更使其不能复制。……