設(shè)備的可靠性涉及多個方面:穩(wěn)定的硬件、優(yōu)秀的軟件架構(gòu)、嚴格的測試以及市場和時間的檢驗等等。這里著重談一下作者自己對嵌入式軟件可靠性設(shè)計的一些理解,通過一定的技巧和方法提高軟件可靠性。
1、判錯
工欲善其事必先利其器。判錯的最終目的是用來暴露設(shè)計中的Bug并加以改正,所以將錯誤信息提供給編程者是必要的。
有時候需要將故障信息儲存于非易失性存儲器中,便于查看。這里以使用串口打印錯誤信息到PC顯示屏為例,來說明一般需要顯示什么信息。
編寫或移植一個類似C標準庫中的printf函數(shù),可以格式化打印字符、字符串、十進制整數(shù)、十六進制整數(shù)。這里稱為UARTprintf()。
unsignedintWriteData(unsignedintaddr) { if((addr>=BASE_ADDR)&&(addr<=END_ADDR))? ????{ ????????…/*地址合法,進行處理*/ ?}? ????else? ????{?/*地址錯誤,打印錯誤信息*/ ??UARTprintf?("文件%s的第?%d?行寫數(shù)據(jù)時發(fā)生地址錯誤,錯誤地址為:0x%x ",__FILE__,__LINE__,addr); ??…/*錯誤處理代碼*/ ?}
假設(shè)UARTprintf()函數(shù)位于main.c模塊的第256行,并且WriteData()函數(shù)在讀數(shù)據(jù)時傳遞了錯誤地址0x00000011,則會執(zhí)行UARTprintf()函數(shù),打印如下所示的信息:
文件main.c的第256行寫數(shù)據(jù)時發(fā)生地址錯誤,錯誤地址為:0x00000011。類似這樣的信息會有助于程序員定位分析錯誤產(chǎn)生的根源,更快的消除Bug。
2、判斷實參是否合法
程序員可能無意識的傳遞了錯誤參數(shù);外界的強干擾可能將傳遞的參數(shù)修改掉,或者使用隨機參數(shù)意外的調(diào)用函數(shù),因此在執(zhí)行函數(shù)主體前,需要先確定實參是否合法。
intexam_fun(unsignedchar*str) { if(str!=NULL) {//檢查“假設(shè)指針不為空”這個條件 ...//正常處理代碼 } else { UARTprintf(…);//打印錯誤信息 …//處理錯誤代碼 } }
3、仔細檢查函數(shù)的返回值
對函數(shù)返回的錯誤碼,要進行全面仔細處理,必要時做錯誤記錄。
char*DoSomething(…) { char*p; p=malloc(1024); if(p==NULL) {/*對函數(shù)返回值作出判斷*/ UARTprintf(…);/*打印錯誤信息*/ returnNULL; } retuenp; }
4、防止指針越界
如果動態(tài)計算一個地址時,要保證被計算的地址是合理的并指向某個有意義的地方。特別對于指向一個結(jié)構(gòu)或數(shù)組的內(nèi)部的指針,當指針增加或者改變后仍然指向同一個結(jié)構(gòu)或數(shù)組。
5、防止數(shù)組越界
數(shù)組越界的問題前文已經(jīng)講述的很多了,由于C不會對數(shù)組進行有效的檢測,因此必須在應(yīng)用中顯式的檢測數(shù)組越界問題。下面的例子可用于中斷接收通訊數(shù)據(jù)。
#defineREC_BUF_LEN100 unsignedcharRecBuf[REC_BUF_LEN]; …//其它代碼 voidUart_IRQHandler(void) { staticRecCount=0;//接收數(shù)據(jù)長度計數(shù)器 …//其它代碼 if(RecCount
在使用一些庫函數(shù)時,同樣需要對邊界進行檢查:
#defineREC_BUF_LEN100 unsignedcharRecBuf[REC_BUF_LEN]; if(len
6、數(shù)學算數(shù)運算
檢測除數(shù)是否為零
檢測運算溢出情況
「有符號整數(shù)除法,僅檢測除數(shù)為零就夠了嗎?」
兩個整數(shù)相除,除了要檢測除數(shù)是否為零外,還要檢測除法是否溢出。對于一個signed long類型變量,它能表示的數(shù)值范圍為:-2147483648 ~ +2147483647,如果讓-2147483648 / -1,那么結(jié)果應(yīng)該是+ 2147483648,但是這個結(jié)果已經(jīng)超出了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、其它可能出現(xiàn)運行時錯誤的地方
運行時錯誤檢查是C 程序員需要加以特別的注意的,這是因為C語言在提供任何運行時檢測方面能力較弱。對于要求可靠性較高的軟件來說,動態(tài)檢測是必需的。
因此C 程序員需要謹慎考慮的問題是,在任何可能出現(xiàn)運行時錯誤的地方增加代碼的動態(tài)檢測。大多數(shù)的動態(tài)檢測與應(yīng)用緊密相關(guān),在程序設(shè)計過程中要根據(jù)系統(tǒng)需求設(shè)置動態(tài)代碼檢測。
8、編譯器語義檢查
為了更簡單的設(shè)計編譯器,目前幾乎所有編譯器的語義檢查都比較弱小,加之為了獲得更快的執(zhí)行效率,C語言被設(shè)計的足夠靈活且?guī)缀醪贿M行任何運行時檢查,比如數(shù)組越界、指針是否合法、運算結(jié)果是否溢出等等。
C語言足夠靈活,對于一個數(shù)組a[30],它允許使用像a[-1]這樣的形式來快速獲取數(shù)組首元素所在地址前面的數(shù)據(jù);允許將一個常數(shù)強制轉(zhuǎn)換為函數(shù)指針,使用代碼( * ((void( * )())0))()來調(diào)用位于0地址的函數(shù)。
C語言給了程序員足夠的自由,但也由程序員承擔濫用自由帶來的責任。下面的兩個例子都是死循環(huán),如果在不常用分支中出現(xiàn)類似代碼,將會造成看似莫名其妙的死機或者重啟。
a.unsignedchari; for(i=0;i<256;i++)??{…?}?????????????? b.?unsigned?chari; ???for(i=10;i>=0;i--){…}
對于無符號char類型,表示的范圍為0~255,所以無符號char類型變量i永遠小于256(第一個for循環(huán)無限執(zhí)行),永遠大于等于0(第二個for循環(huán)無線執(zhí)行)。需要說明的是,賦值代碼i=256是被C語言允許的,即使這個初值已經(jīng)超出了變量i可以表示的范圍。C語言會千方百計的為程序員創(chuàng)造出錯的機會,可見一斑。
假如你在if語句后誤加了一個分號改變了程序邏輯,編譯器也會很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:
if(a>b);//這里誤加了一個分號 a=b;//這句代碼一直被執(zhí)行
不但如此,編譯器還會忽略掉多余的空格符和換行符,就像下面的代碼也不會給出足夠提示:
if(n<3) return????//這里少加了一個分號 logrec.data=x[0]; logrec.time=x[1]; logrec.code=x[2];
這段代碼的本意是n<3時程序直接返回,由于程序員的失誤,return少了一個結(jié)束分號。編譯器將它翻譯成返回表達式logrec.data=x[0]的結(jié)果,return后面即使是一個表達式也是C語言允許的。這樣當n>=3時,表達式logrec.data=x[0];就不會被執(zhí)行,給程序埋下了隱患。
可以毫不客氣的說,弱小的編譯器語義檢查在很大程度上縱容了不可靠代碼可以肆無忌憚的存在。
上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間就會寫數(shù)組越界。一位同事的代碼在硬件上運行,一段時間后就會發(fā)現(xiàn)LCD顯示屏上的一個數(shù)字不正常的被改變。經(jīng)過一段時間的調(diào)試,問題被定位到下面的一段代碼中:
intSensorData[30]; for(i=30;i>0;i--) { SensorData[i]=…; … }
這里聲明了擁有30個元素的數(shù)組,不幸的是for循環(huán)代碼中誤用了本不存在的數(shù)組元素SensorData[30],但C語言卻默許這么使用,并欣然的按照代碼改變了數(shù)組元素SensorData[30]所在位置的值。
SensorData[30]所在的位置原本是一個LCD顯示變量,這正是顯示屏上的那個值不正常被改變的原因。真慶幸這么輕而易舉的發(fā)現(xiàn)了這個Bug。
9、關(guān)鍵數(shù)據(jù)多區(qū)備份,取數(shù)據(jù)采用“表決法”
RAM中的數(shù)據(jù)在受到干擾情況下有可能被改變,對于系統(tǒng)關(guān)鍵數(shù)據(jù)必須進行保護。關(guān)鍵數(shù)據(jù)包括全局變量、靜態(tài)變量以及需要保護的數(shù)據(jù)區(qū)域。數(shù)據(jù)備份與原數(shù)據(jù)不應(yīng)該處于相鄰位置,因此不應(yīng)由編譯器默認分配備份數(shù)據(jù)位置,而應(yīng)該由程序員指定區(qū)域存儲。
可以將RAM分為3個區(qū)域,第一個區(qū)域保存原碼,第二個區(qū)域保存反碼,第三個區(qū)域保存異或碼,區(qū)域之間預留一定量的“空白”RAM作為隔離。
可以使用編譯器的“分散加載”機制將變量分別存儲在這些區(qū)域。需要進行讀取時,同時讀出3份數(shù)據(jù)并進行表決,取至少有兩個相同的那個值。
假如設(shè)備的RAM從0x1000_0000開始,我需要在RAM的0x1000_0000~0x10007FFF內(nèi)存儲原碼,在0x1000_9000~0x10009FFF內(nèi)存儲反碼,在0x1000_B000~0x1000BFFF內(nèi)存儲0xAA的異或碼,編譯器的分散加載可以設(shè)置為:
LR_IROM10x000000000x00080000{;loadregionsize_region ER_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) } }
如果一個關(guān)鍵變量需要多處備份,可以按照下面方式定義變量,將三個變量分別指定到三個不連續(xù)的RAM區(qū)中,并在定義時按照原碼、反碼、0xAA的異或碼進行初始化。
uint32plc_pc=0;//原碼 __attribute__((section("MY_BK1")))uint32plc_pc_not=~0x0;//反碼 __attribute__((section("MY_BK2")))uint32plc_pc_xor=0x0^0xAAAAAAAA;//異或碼
當需要寫這個變量時,這三個位置都要更新;讀取變量時,讀取三個值做判斷,取至少有兩個相同的那個值。
為什么選取異或碼而不是補碼?這是因為MDK的整數(shù)是按照補碼存儲的,正數(shù)的補碼與原碼相同,在這種情況下,原碼和補碼是一致的,不但起不到冗余作用,反而對可靠性有害。
比如存儲的一個非零整數(shù)區(qū)因為干擾,RAM都被清零,由于原碼和補碼一致,按照3取2的“表決法”,會將干擾值0當做正確的數(shù)據(jù)。
10、非易失性存儲器的數(shù)據(jù)存儲
非易失性存儲器包括但不限于Flash、EEPROM、鐵電。僅僅將寫入非易失性存儲器中的數(shù)據(jù)再讀出校驗是不夠的。強干擾情況下可能導致非易失性存儲器內(nèi)的數(shù)據(jù)錯誤,在寫非易失性存儲器的期間系統(tǒng)掉電將導致數(shù)據(jù)丟失,因干擾導致程序跑飛到寫非易失性存儲器函數(shù)中,將導致數(shù)據(jù)存儲紊亂。
一種可靠的辦法是將非易失性存儲器分成多個區(qū),每個數(shù)據(jù)都將按照不同的形式寫入到這些分區(qū)中,需要進行讀取時,同時讀出多份數(shù)據(jù)并進行表決,取相同數(shù)目較多的那個值。
對于因干擾導致程序跑飛到寫非易失性存儲器函數(shù),還應(yīng)該配合軟件鎖以及嚴格的入口檢驗,單單依靠寫數(shù)據(jù)到多個區(qū)是不夠的也是不明智的,應(yīng)該在源頭進行阻截。
11、軟件鎖
軟件鎖可以實現(xiàn)但不局限于環(huán)環(huán)相扣。對于初始化序列或者有一定先后順序的函數(shù)調(diào)用,為了保證調(diào)用順序或者確保每個函數(shù)都被調(diào)用,我們可以使用環(huán)環(huán)相扣,實質(zhì)上這也是一種軟件鎖。此外對于一些安全關(guān)鍵代碼語句(是語句,而不是函數(shù)),可以給它們設(shè)置軟件鎖,只有持有特定鑰匙的,才可以訪問這些關(guān)鍵代碼。
比如,向Flash寫一個數(shù)據(jù),我們會判斷數(shù)據(jù)是否合法、寫入的地址是否合法,計算要寫入的扇區(qū)。之后調(diào)用寫Flash子程序,在這個子程序中,判斷扇區(qū)地址是否合法、數(shù)據(jù)長度是否合法,之后就要將數(shù)據(jù)寫入Flash。
由于寫Flash語句是安全關(guān)鍵代碼,所以程序給這些語句上鎖:必須具有正確的鑰匙才可以寫Flash。這樣即使是程序跑飛到寫Flash子程序,也能大大降低誤寫的風險。
/*************************************************************** *名稱:RamToFlash() *功能:復制RAM的數(shù)據(jù)到FLASH,命令代碼51。 *入口參數(shù):dst 目標地址,即FLASH起始地址。以512字節(jié)為分界 * src 源地址,即RAM地址。地址必須字對齊 *no復制字節(jié)個數(shù),為512/1024/4096/8192 *ProgStart軟件鎖標志 *出口參數(shù):IAP返回值(paramout緩沖區(qū)) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR, SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未選擇扇區(qū) ****************************************************************/ voidRamToFlash(uint32dst,uint32src,uint32no,uint8ProgStart) { PLC_ASSERT("Sectornumber",(dst>=0x00040000)&&(dst<=0x0007FFFF)); ????PLC_ASSERT("Copy?bytes?number?is?512",(no==512)); ????PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5)); ????paramin[0]?=?IAP_RAMTOFLASH;?//?設(shè)置命令字 ????paramin[1]?=?dst;?//?設(shè)置參數(shù) ????paramin[2]?=?src; ????paramin[3]?=?no; ????paramin[4]?=?Fcclk/1000; ????if(ProgStart==0xA5)?//只有軟件鎖標志正確時,才執(zhí)行關(guān)鍵代碼 ????{ ????????iap_entry(paramin,?paramout);?//?調(diào)用IAP服務(wù)程序 ????????ProgStart=0; ????} ????else ????{ ?????paramout[0]=PROG_UNSTART; ????} }
該程序段是編程lpc1778內(nèi)部Flash,其中調(diào)用IAP程序的函數(shù)iap_entry(paramin, paramout)是關(guān)鍵安全代碼,所以在執(zhí)行該代碼前,先判斷一個特定設(shè)置的安全鎖標志ProgStart,只有這個標志符合設(shè)定值,才會執(zhí)行編程Flash操作。
如果因為意外程序跑飛到該函數(shù),由于ProgStart標志不正確,是不會對Flash進行編程的。
12、通信數(shù)據(jù)的檢錯
通訊線上的數(shù)據(jù)誤碼相對嚴重,通訊線越長,所處的環(huán)境越惡劣,誤碼會越嚴重。拋開硬件和環(huán)境的作用,我們的軟件應(yīng)能識別錯誤的通訊數(shù)據(jù)。對此有一些應(yīng)用措施:
制定協(xié)議時,限制每幀的字節(jié)數(shù);
每幀字節(jié)數(shù)越多,發(fā)生誤碼的可能性就越大,無效的數(shù)據(jù)也會越多。對此以太網(wǎng)規(guī)定每幀數(shù)據(jù)不大于1500字節(jié),高可靠性的CAN收發(fā)器規(guī)定每幀數(shù)據(jù)不得多于8字節(jié),對于RS485,基于RS485鏈路應(yīng)用最廣泛的Modbus協(xié)議一幀數(shù)據(jù)規(guī)定不超過256字節(jié)。因此,建議制定內(nèi)部通訊協(xié)議時,使用RS485時規(guī)定每幀數(shù)據(jù)不超過256字節(jié);
使用多種校驗
編寫程序時應(yīng)使能奇偶校驗,每幀超過16字節(jié)的應(yīng)用,建議至少編寫CRC16校驗程序。
增加額外判斷
增加緩沖區(qū)溢出判斷。這是因為數(shù)據(jù)接收多是在中斷中完成,編譯器檢測不出緩沖區(qū)是否溢出,需要手動檢查,在上文介紹數(shù)據(jù)溢出一節(jié)中已經(jīng)詳細說明。
增加超時判斷。當一幀數(shù)據(jù)接收 到一半,長時間接收不到剩余數(shù)據(jù),則認為這幀數(shù)據(jù)無效,重新開始接收。
可選,跟不同的協(xié)議有關(guān),但緩沖區(qū)溢出判斷必須實現(xiàn)。這是因為對于需要幀頭判斷的協(xié)議,上位機可能發(fā)送完幀頭后突然斷電,重啟后上位機是從新的幀開始發(fā)送的,但是下位機已經(jīng)接收到了上次未發(fā)送完的幀頭,所以上位機的這次幀頭會被下位機當成正常數(shù)據(jù)接收。
這有可能造成數(shù)據(jù)長度字段為一個很大的值,填滿該長度的緩沖區(qū)需要相當多的數(shù)據(jù)(比如一幀可能1000字節(jié)),影響響應(yīng)時間;另一方面,如果程序沒有緩沖區(qū)溢出判斷,那么緩沖區(qū)很可能溢出,后果是災難性的。
重傳機制
如果檢測到通訊數(shù)據(jù)發(fā)生了錯誤,則要有重傳機制重新發(fā)送出錯的幀。
13、開關(guān)量輸入的檢測、確認
開關(guān)量容易受到尖脈沖干擾,如果不進行濾除,可能會造成誤動作。一般情況下,需要對開關(guān)量輸入信號進行多次采樣,并進行邏輯判斷直到確認信號無誤為止。多次采樣之間需要有一定時間間隔,具體跟開關(guān)量的最大切換頻率有關(guān),一般不小于1ms。
14、開關(guān)量輸出
開關(guān)信號簡單的一次輸出是不安全的,干擾信號可能會翻轉(zhuǎn)開關(guān)量輸出的狀態(tài)。采取重復刷新輸出可以有效防止電平的翻轉(zhuǎn)。
15、初始化信息的保存與恢復
微處理器的寄存器值也可能會因外界干擾而改變,外設(shè)初始化值需要在寄存器中長期保存,最容易被破壞。由于Flash中的數(shù)據(jù)相對不易被破壞,可以將初始化信息預先寫入Flash,待程序空閑時比較與初始化相關(guān)的寄存器值是否被更改,如果發(fā)現(xiàn)非法更改則使用Flash中的值進行恢復。
16、while循環(huán)
有時候程序員會使用while(!flag);語句來等待標志flag改變,比如串口發(fā)送時用來等待一字節(jié)數(shù)據(jù)發(fā)送完成。這樣的代碼時存在風險的,如果因為某些原因標志位一直不改變則會造成系統(tǒng)死機。良好冗余的程序是設(shè)置一個超時定時器,超過一定時間后,強制程序退出while循環(huán)。
2003年8月11日發(fā)生的W32.Blaster.Worm蠕蟲事件導致全球經(jīng)濟損失高達5億美元,這個漏洞是利用了Windows分布式組件對象模型的遠程過程調(diào)用接口中的一個邏輯缺陷:在調(diào)用GetMachineName()函數(shù)時,循環(huán)只設(shè)置了一個不充分的結(jié)束條件。
原代碼簡化如下所示:
HRESULTGetMachineName(WCHAR*pwszPath, WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1]) { WCHAR*pwszServerName=wszMachineName; WCHAR*pwszTemp=pwszPath+2; while(*pwszTemp!=L’\’)/*這句代碼循環(huán)結(jié)束條件不充分*/ *pwszServerName++=*pwszTemp++; /*…*/ }
微軟發(fā)布的安全補丁MS03-026解決了這個問題,為GetMachineName()函數(shù)設(shè)置了充分終止條件。一個解決代碼簡化如下所示(并非微軟補丁代碼):
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、系統(tǒng)自檢
對CPU、RAM、Flash、外部掉電保存存儲器以及其他線路自檢。
18、其它一些編程建議:
深入理解嵌入式C語言以及編譯器
細致、謹慎的編程
使用好的風格和合理的設(shè)計
不要倉促編寫代碼,寫每一行的代碼時都要三思而后行:可能會出現(xiàn)什么樣的錯誤?是否考慮了所有的邏輯分支?
打開編譯器所有警告開關(guān)
使用靜態(tài)分析工具分析代碼
安全的讀寫數(shù)據(jù)(檢查所有數(shù)組邊界…)
檢查指針的合法性
檢查函數(shù)入口參數(shù)合法性
檢查所有返回值
在聲明變量位置初始化所有變量
合理的使用括號
謹慎的進行強制轉(zhuǎn)換
使用好的診斷信息日志和工具
來源:https://blog.csdn.net/zhzht19861011/article/details/17117819
審核編輯:劉清
-
C語言
+關(guān)注
關(guān)注
180文章
7632瀏覽量
141759 -
嵌入式軟件
+關(guān)注
關(guān)注
4文章
245瀏覽量
27332 -
LCD顯示屏
+關(guān)注
關(guān)注
1文章
92瀏覽量
14032 -
非易失性存儲器
+關(guān)注
關(guān)注
0文章
109瀏覽量
23714
原文標題:嵌入式軟件可靠性設(shè)計的編程要點
文章出處:【微信號:工程師進階筆記,微信公眾號:工程師進階筆記】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
嵌入式軟件可靠性設(shè)計要注意的問題

如何對嵌入式軟件進行可靠性測試
嵌入式軟件可靠性測試方法是什么
嵌入式軟件的安全可靠性控制
嵌入式系統(tǒng)軟件可靠性設(shè)計
軍用嵌入式系統(tǒng)的可靠性考慮因素
嵌入式系統(tǒng)的可靠性設(shè)計

軍用嵌入式系統(tǒng)的可靠性考慮因素
嵌入式系統(tǒng)硬件可靠性分析

嵌入式軟件怎樣測試,如何對嵌入式軟件進行可靠性測試

評論