phpcmspc_base::load_app_func的簡單介紹
摘要:本文首先分析了C語言的陷阱和缺陷,對容易犯錯的地方進行歸納整理;分析了編譯器語義檢查的不足之處并給出防范措施,以Keil MDK編譯器為例,介紹了該編譯器的特性、對未定義行為的處理以及一些高級應(yīng)用;
在此基礎(chǔ)上,介紹了防御性編程的概念,提出了編程過程中就應(yīng)該防范于未然的多種措施;提出了測試對編寫優(yōu)質(zhì)嵌入式程序的重要作用以及常用測試方法;最后,本文試圖以更高的層次看待編程,討論一些通用的編程思想。
1. 簡介
市面上介紹C語言以及編程方法的書數(shù)目繁多,但對如何編寫優(yōu)質(zhì)嵌入式C程序卻鮮有介紹,特別是對應(yīng)用于單片機、ARM7、Cortex-M3這類微控制器上的優(yōu)質(zhì)C程序編寫方法幾乎是個空白。本文面向的,正是使用單片機、ARM7、Cortex-M3這類微控制器的底層編程人員。
編寫優(yōu)質(zhì)嵌入式C程序絕非易事,它跟設(shè)計者的思維和經(jīng)驗積累關(guān)系密切。嵌入式C程序員不僅需要熟知硬件的特性、硬件的缺陷等,更要深入一門語言編程,不浮于表面。為了更方便的操作硬件,還需要對編譯器進行深入的了解。
本文將從語言特性、編譯器、防御性編程、測試和編程思想這幾個方面來討論如何編寫優(yōu)質(zhì)嵌入式C程序。與很多雜志、書籍不同,本文提供大量真實實例、代碼段和參考書目,不僅介紹應(yīng)該做什么,還重點介紹如何做、以及為什么這樣做。編寫優(yōu)質(zhì)嵌入式C程序涉及面十分廣,需要程序員長時間的經(jīng)驗積累,本文希望能縮短這一過程。
2. C語言特性
語言是編程的基石,C語言詭異且有種種陷阱和缺陷,需要程序員多年歷練才能達到較為完善的地步。雖然有眾多書籍、雜志、專題討論過C語言的陷阱和缺陷,但這并不影響本節(jié)再次討論它。總是有大批的初學(xué)者,前仆后繼的倒在這些陷阱和缺陷上,民用設(shè)備、工業(yè)設(shè)備甚至是航天設(shè)備都不例外。本節(jié)將結(jié)合具體例子再次審視它們,希望引起足夠重視。深入理解C語言特性,是編寫優(yōu)質(zhì)嵌入式C程序的基礎(chǔ)。
2.1處處都是陷阱 2.1.1 無心之過
1) “=”和”==”
將比較運算符”==”誤寫成賦值運算符”=”,可能是絕大多數(shù)人都遇到過的,比如下面代碼:
1.if(x= 5)
2.{
3.//其它代碼
4.}
代碼的本意是比較變量x是否等于常量5,但是誤將”==”寫成了”=”,if語句恒為真。如果在邏輯判斷表達式中出現(xiàn)賦值運算符,現(xiàn)在的大多數(shù)編譯器會給出警告信息。比如keil MDK會給出警告提示:“warning: #187-D: use of "=" where"==" may have been intended”,但并非所有程序員都會注意到這類警告,因此有經(jīng)驗的程序員使用下面的代碼來避免此類錯誤:
展開全文
1.if( 5==x)
2.{
3.//其它代碼
4.}
將常量放在變量x的左邊,即使程序員誤將’==’寫成了’=’,編譯器會產(chǎn)生一個任誰也不能無視的語法錯誤信息:不可給常量賦值!
2) 復(fù)合賦值運算符
復(fù)合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔并有可能產(chǎn)生更高效的機器代碼,但某些復(fù)合賦值運算符也會給程序帶來隱含Bug,比如”+=”容易誤寫成”=+”,代碼如下:
1.tmp=+ 1;
代碼本意是想表達tmp=tmp+1,但是將復(fù)合賦值運算符”+=”誤寫成”=+”:將正整數(shù)常量1賦值給變量tmp。編譯器會欣然接受這類代碼,連警告都不會產(chǎn)生。
如果你能在調(diào)試階段就發(fā)現(xiàn)這個Bug,真應(yīng)該慶祝一下,否則這很可能會成為一個重大隱含Bug,且不易被察覺。
復(fù)合賦值運算符”-=”也有類似問題存在。
3) 其它容易誤寫
使用了中文標點
頭文件聲明語句最后忘記結(jié)束分號
邏輯與和位與、邏輯或||和位或|、邏輯非!和位取反~
字母l和數(shù)字1、字母O和數(shù)字0
使用了中文標點
頭文件聲明語句最后忘記結(jié)束分號
邏輯與和位與、邏輯或||和位或|、邏輯非!和位取反~
字母l和數(shù)字1、字母O和數(shù)字0
這些誤寫其實容易被編譯器檢測出,只需要關(guān)注編譯器對此的提示信息,就能很快解決。
2.1.2 數(shù)組下標
數(shù)組常常也是引起程序不穩(wěn)定的重要因素,C語言數(shù)組的迷惑性與數(shù)組下標從0開始密不可分,你可以定義int test[30],但是你絕不可以使用數(shù)組元素test [30],除非你自己明確知道在做什么。
2.1.3 容易被忽略的break關(guān)鍵字
1) 不能漏加的break
switch…case語句可以很方便的實現(xiàn)多分支結(jié)構(gòu),但要注意在合適的位置添加break關(guān)鍵字。程序員往往容易漏加break從而引起順序執(zhí)行多個case語句,這也許是C的一個缺陷之處。
對于switch…case語句,從概率論上說,絕大多數(shù)程序一次只需執(zhí)行一個匹配的case語句,而每一個這樣的case語句后都必須跟一個break。去復(fù)雜化大概率事件,這多少有些不合常情。
2) 不能亂加的break
break關(guān)鍵字用于 跳出最近的那層循環(huán)語句或者switch語句,但程序員往往不夠重視這一點。
1990年1月15日,ATT電話網(wǎng)絡(luò)位于紐約的一臺交換機宕機并且重啟,引起它鄰近交換機癱瘓,由此及彼,一個連著一個,很快,114型交換機每六秒宕機重啟一次,六萬人九小時內(nèi)不能打長途電話。當時的解決方式:工程師重裝了以前的軟件版本。。。事后的事故調(diào)查發(fā)現(xiàn),這是break關(guān)鍵字誤用造成的?!禖專家編程》提供了一個簡化版的問題源碼:
1.network code
2. {
3.switch(line)
4.{
5.caseTHING1:
6.{
7.doit1;
8.} break;
9.caseTHING2:
10.{
11.if(x==STUFF)
12.{
13.do_first_stuff;
14.if(y==OTHER_STUFF)
15.break;
16.do_later_stuff;
17.} /*代碼的意圖是跳轉(zhuǎn)到這里… …*/
18.initialize_modes_pointer;
19.} break;
20.default:
21.processing;
22.} /*… …但事實上跳到了這里。*/
23.use_modes_pointer; /*致使modes_pointer未初始化*/
24.}
那個程序員希望從if語句跳出,但他卻忘記了break關(guān)鍵字實際上跳出最近的那層循環(huán)語句或者switch語句?,F(xiàn)在它跳出了switch語句,執(zhí)行了use_modes_pointer函數(shù)。但必要的初始化工作并未完成,為將來程序的失敗埋下了伏筆。
2.1.4 意想不到的八進制
將一個整形常量賦值給變量,代碼如下所示:
1.inta= 34, b= 034;
變量a和b相等嗎?
答案是不相等的。我們知道,16進制常量以’0x’為前綴,10進制常量不需要前綴,那么8進制呢?它與10進制和16進制表示方法都不相同,它以數(shù)字’0’為前綴,這多少有點奇葩:三種進制的表示方法完全不相同。如果8進制也像16進制那樣以數(shù)字和字母表示前綴的話,或許更有利于減少軟件Bug,畢竟你使用8進制的次數(shù)可能都不會有誤使用的次數(shù)多!下面展示一個誤用8進制的例子,最后一個數(shù)組元素賦值錯誤:
1.a[ 0]= 106; /*十進制數(shù)106*/
2.a[ 1]= 112; /*十進制數(shù)112*/
3.a[ 2]= 052; /*實際為十進制數(shù)42,本意為十進制52*/
2.1.5指針加減運算
**指針的加減運算是特殊的。**下面的代碼運行在32位ARM架構(gòu)上,執(zhí)行之后,a和p的值分別是多少?
1.inta= 1;
2.int*p=( int*) 0x00001000;
3.a=a+ 1;
4.p=p+ 1;
對于a的值很容判斷出結(jié)果為2,但是p的結(jié)果卻是0x00001004。指針p加1后,p的值增加了4,這是為什么呢?原因是指針做加減運算時是以指針的數(shù)據(jù)類型為單位。p+1實際上是按照公式p+1*sizeof(int)來計算的。不理解這一點,在使用指針直接操作數(shù)據(jù)時極易犯錯。
某項目使用下面代碼對連續(xù)RAM初始化零操作,但運行發(fā)現(xiàn)有些RAM并沒有被真正清零。
1.unsignedint*pRAMaddr; //定義地址指針變量
2.for(pRAMaddr=StartAddr;pRAMaddrEndAddr;pRAMaddr+= 4)
3.{
4.*pRAMaddr= 0x00000000; //指定RAM地址清零
5.}
通過分析我們發(fā)現(xiàn),由于pRAMaddr是一個無符號int型指針變量,所以pRAMaddr+=4代碼其實使pRAMaddr偏移了4*sizeof(int)=16個字節(jié),所以每執(zhí)行一次for循環(huán),會使變量pRAMaddr偏移16個字節(jié)空間,但只有4字節(jié)空間被初始化為零。其它的12字節(jié)數(shù)據(jù)的內(nèi)容,在大多數(shù)架構(gòu)處理器中都會是隨機數(shù)。
2.1.6關(guān)鍵字sizeof
不知道有多少人最初認為sizeof是一個函數(shù)。其實它是一個關(guān)鍵字,其作用是返回一個對象或者類型所占的內(nèi)存字節(jié)數(shù),對絕大多數(shù)編譯器而言,返回值為無符號整形數(shù)據(jù)。需要注意的是, 使用sizeof獲取數(shù)組長度時,不要對指針應(yīng)用sizeof操作符,比如下面的例子:
1.voidClearRAM( chararray[])
2. {
3.inti ;
4.for(i= 0;i sizeof( array)/ sizeof( array[ 0]);i++) //這里用法錯誤,array實際上是指針
5.{
6.array[i]= 0x00;
7.}
8.}
9.
10.intmain( void)
11. {
12.charFle[ 20];
13.
14.ClearRAM(Fle); //只能清除數(shù)組Fle中的前四個元素
15.}
我們知道,對于一個數(shù)組array[20],我們使用代碼sizeof(array)/sizeof(array[0])可以獲得數(shù)組的元素(這里為20),但數(shù)組名和指針往往是容易混淆的,有且只有一種情況下數(shù)組名是可以當做指針的,那就是**數(shù)組名作為函數(shù)形參時,數(shù)組名被認為是指針,同時,它不能再兼任數(shù)組名。**注意只有這種情況下,數(shù)組名才可以當做指針,但不幸的是這種情況下容易引發(fā)風(fēng)險。在ClearRAM函數(shù)內(nèi),作為形參的array[]不再是數(shù)組名了,而成了指針。sizeof(array)相當于求指針變量占用的字節(jié)數(shù),在32位系統(tǒng)下,該值為4,sizeof(array)/sizeof(array[0])的運算結(jié)果也為4。所以在main函數(shù)中調(diào)用ClearRAM(Fle),也只能清除數(shù)組Fle中的前四個元素了。
2.1.7增量運算符’++’和減量運算符‘--‘
增量運算符”++”和減量運算符”--“既可以做前綴也可以做后綴。**前綴和后綴的區(qū)別在于值的增加或減少這一動作發(fā)生的時間是不同的。**作為前綴是先自加或自減然后做別的運算,作為后綴時,是先做運算,之后再自加或自減。許多程序員對此認識不夠,就容易埋下隱患。下面的例子可以很好的解釋前綴和后綴的區(qū)別。
1.inta= 8,b= 2,y;
2.y=a+++--b;
代碼執(zhí)行后,y的值是多少?
1.y=(a++)+(--b);
當賦值給變量y時,a的值為8,b的值為1,所以變量y的值為9;賦值完成后,變量a自加,a的值變?yōu)?,千萬不要以為y的值為10。這條賦值語句相當于下面的兩條語句:
1.y=a+(--b);
2.a=a+ 1;
2.1.8邏輯與’’和邏輯或’||’的陷阱
為了提高系統(tǒng)效率,邏輯與和邏輯或操作的規(guī)定如下:**如果對第一個操作數(shù)求值后就可以推斷出最終結(jié)果,第二個操作數(shù)就不會進行求值!**比如下面代碼:
1.if((i= 0)(i++ =max))
2.{
3.//其它代碼
4.}
在這個代碼中,只有當i=0時,i++才會被執(zhí)行。這樣,i是否自增是不夠明確的,這可能會埋下隱患。邏輯或與之類似。
2.1.9結(jié)構(gòu)體的填充
結(jié)構(gòu)體可能產(chǎn)生填充,因為對大多數(shù)處理器而言,訪問按字或者半字對齊的數(shù)據(jù)速度更快,當定義結(jié)構(gòu)體時,編譯器為了性能優(yōu)化,可能會將它們按照半字或字對齊,這樣會帶來填充問題。比如以下兩個個結(jié)構(gòu)體:
第一個結(jié)構(gòu)體:
1.struct{
2.charc;
3.short s;
4.intx;
5.}str_test1;
第二個結(jié)構(gòu)體:
1.struct{
2.charc;
3.intx;
4.short s;
5.}str_test2;
這兩個結(jié)構(gòu)體元素都是相同的變量,只是元素換了下位置,那么這兩個結(jié)構(gòu)體變量占用的內(nèi)存大小相同嗎?
其實這兩個結(jié)構(gòu)體變量占用的內(nèi)存是不同的,對于Keil MDK編譯器,默認情況下第一個結(jié)構(gòu)體變量占用8個字節(jié),第二個結(jié)構(gòu)體占用12個字節(jié),差別很大。第一個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-1所示:
img
圖2-1:結(jié)構(gòu)體變量1內(nèi)存分布
第二個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-2所示。對比兩個圖可以看出MDK編譯器是是怎么將數(shù)據(jù)對齊的,這其中的填充內(nèi)容是之前內(nèi)存中的數(shù)據(jù),是隨機的,所以不能在結(jié)構(gòu)之間逐字節(jié)比較;另外,合理的排布結(jié)構(gòu)體內(nèi)的元素位置,可以最大限度減少填充,節(jié)省RAM。
img
圖2-2 :結(jié)構(gòu)體變量2內(nèi)存分布
2.2不可輕視的優(yōu)先級
C語言有32個關(guān)鍵字,卻有34個運算符。要記住所有運算符的優(yōu)先級是困難的。稍不注意,你的代碼邏輯和實際執(zhí)行就會有很大出入。
比如下面將BCD碼轉(zhuǎn)換為十六進制數(shù)的代碼:
1.result=(uTimeValue 4)* 10+uTimeValue 0x0F;
這里uTimeValue存放的BCD碼,想要轉(zhuǎn)換成16進制數(shù)據(jù),實際運行發(fā)現(xiàn),如果uTimeValue的值為0x23,按照我設(shè)定的邏輯,result的值應(yīng)該是0x17,但運算結(jié)果卻是0x07。經(jīng)過種種排查后,才發(fā)現(xiàn)’+’的優(yōu)先級是大于’’的,相當于(uTimeValue4)*10+uTimeValue與0x0F位與,結(jié)果自然與邏輯不符。符合邏輯的代碼應(yīng)該是:
1.result=(uTimeValue 4)* 10+(uTimeValue 0x0F);
不合理的#define會加重優(yōu)先級問題,讓問題變得更加隱蔽。
1.# defineREADSDA IO0PIN(111) //讀IO口p0.11的端口狀態(tài)
2.
3.if(READSDA==( 1 11)) //判斷端口p0.11是否為高電平
4.{
5.//其它代碼
6.}
編譯器在編譯后將宏帶入,原代碼語句變?yōu)?
1.if(IO0PIN( 1 11) ==( 1 11))
2.{
3.//其它代碼
4.}
運算符'=='的優(yōu)先級是大于''的,代碼IO0PIN(111) ==(111))等效為IO0PIN0x00000001:判斷端口P0.0是否為高電平,這與原意相差甚遠。因此,使用宏定義的時候,最好將被定義的內(nèi)容用括號括起來。
按照常規(guī)方式使用時,可能引起誤會的運算符還有很多,如表2-1所示。C語言的運算符當然不會只止步于數(shù)目繁多!
有一個簡便方法可以避免優(yōu)先級問題:不清楚的優(yōu)先級就加上””,但這樣至少有會帶來兩個問題:
過多的括號影響代碼的可讀性,包括自己和以后的維護人員
別人的代碼不一定用括號來解決優(yōu)先級問題,但你總要讀別人的代碼
過多的括號影響代碼的可讀性,包括自己和以后的維護人員
別人的代碼不一定用括號來解決優(yōu)先級問題,但你總要讀別人的代碼
無論如何,在嵌入式編程方面,該掌握的基礎(chǔ)知識,偷巧不得。建議花一些時間,將優(yōu)先級順序以及容易出錯的優(yōu)先級運算符理清幾遍。
2.3隱式轉(zhuǎn)換
C語言的設(shè)計理念一直被人吐槽,因為它認為C程序員完全清楚自己在做什么,其中一個證據(jù)就是隱式轉(zhuǎn)換。C語言規(guī)定,**不同類型的數(shù)據(jù)(比如char和int型數(shù)據(jù))需要轉(zhuǎn)換成同一類型后,才可進行計算。**如果你混合使用類型,比如用char類型數(shù)據(jù)和int類型數(shù)據(jù)做減法,C使用一個規(guī)則集合來自動(隱式的)完成類型轉(zhuǎn)換。這可能很方便,但也很危險。
這就要求我們理解這個轉(zhuǎn)換規(guī)則并且能應(yīng)用到程序中去!
當出現(xiàn)在表達式里時,有符號和無符號的char和short類型都將自動被轉(zhuǎn)換為int類型,在需要的情況下,將自動被轉(zhuǎn)換為unsigned int(在short和int具有相同大小時)。這稱為類型提升。
提升在算數(shù)運算中通常不會有什么大的壞處,但如果 位運算符 ~ 和 應(yīng)用在基本類型為unsigned char或unsigned short 的操作數(shù),結(jié)果應(yīng)該立即強制轉(zhuǎn)換為unsigned char或者unsigned short類型(取決于操作時使用的類型)。
1.uint8_tport = 0x5aU;
2.uint8_tresult_8;
3.result_8= (~port) 4;
假如我們不了解表達式里的類型提升,認為在運算過程中變量port一直是unsigned char類型的。我們來看一下運算過程:~port結(jié)果為0xa5,0xa54結(jié)果為0x0a,這是我們期望的值。但實際上,result_8的結(jié)果卻是0xfa!在ARM結(jié)構(gòu)下,int類型為32位。變量port在運算前被提升為int類型:~port結(jié)果為0xffffffa5,0xa54結(jié)果為0x0ffffffa,賦值給變量result_8,發(fā)生類型截斷(這也是隱式的?。瑀esult_8=0xfa。經(jīng)過這么詭異的隱式轉(zhuǎn)換,結(jié)果跟我們期望的值,已經(jīng)大相徑庭!正確的表達式語句應(yīng)該為:
1.result_8=( unsignedchar) (~port) 4; /*強制轉(zhuǎn)換*/
在包含兩種數(shù)據(jù)類型的任何運算里,兩個值都會被轉(zhuǎn)換成兩種類型里較高的級別。類型級別從高到低的順序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。
這種類型提升通常都是件好事,但往往有很多程序員不能真正理解這句話,比如下面的例子(int類型表示16位)。
1.uint16_tu16a = 40000; /* 16位無符號變量*/
2.uint16_tu16b= 30000; /*16位無符號變量*/
3.uint32_tu32x; /*32位無符號變量 */
4.uint32_tu32y;
5.u32x = u16a +u16b; /* u32x = 70000還是4464 ? */
6.u32y =( uint32_t)(u16a + u16b); /* u32y = 70000 還是4464 ? */
u32x和u32y的結(jié)果都是4464(70000%65536)!不要認為表達式中有一個高類別uint32_t類型變量,編譯器都會幫你把所有其他低類別都提升到uint32_t類型。正確的書寫方式:
1.u32x = ( uint32_t)u16a +( uint32_t)u16b; 或者:
2.u32x = ( uint32_t)u16a + u16b;
后一種寫法在本表達式中是正確的,但是在其它表達式中不一定正確,比如:
1.uint16_tu16a,u16b,u16c;
2.uint32_tu32x;
3.u32x= u16a + u16b + ( uint32_t)u16c; /*錯誤寫法,u16a+ u16b仍可能溢出*/
在賦值語句里,計算的最后結(jié)果被轉(zhuǎn)換成將要被賦予值的那個變量的類型。這一過程可能導(dǎo)致類型提升也可能導(dǎo)致類型降級。降級可能會導(dǎo)致問題。比如將運算結(jié)果為321的值賦值給8位char類型變量。程序必須對運算時的數(shù)據(jù)溢出做合理的處理。很多其他語言,像Pascal(C語言設(shè)計者之一曾撰文狠狠批評過Pascal語言),都不允許混合使用類型,但C語言不會限制你的自由,即便這經(jīng)常引起B(yǎng)ug。
當作為函數(shù)的參數(shù)被傳遞時,char和short會被轉(zhuǎn)換為int,float會被轉(zhuǎn)換為double。
當不得已混合使用類型時,一個比較好的習(xí)慣是使用類型強制轉(zhuǎn)換。強制類型轉(zhuǎn)換可以避免編譯器隱式轉(zhuǎn)換帶來的錯誤,同時也向以后的維護人員傳遞一些有用信息。這有個前提:你要對強制類型轉(zhuǎn)換有足夠的了解!下面總結(jié)一些規(guī)則:
并非所有強制類型轉(zhuǎn)換都是由風(fēng)險的,把一個整數(shù)值轉(zhuǎn)換為一種具有相同符號的更寬類型時,是絕對安全的。
精度高的類型強制轉(zhuǎn)換為精度低的類型時,通過丟棄適當數(shù)量的最高有效位來獲取結(jié)果,也就是說會發(fā)生數(shù)據(jù)截斷,并且可能改變數(shù)據(jù)的符號位。
精度低的類型強制轉(zhuǎn)換為精度高的類型時,如果兩種類型具有相同的符號,那么沒什么問題;需要注意的是負的有符號精度低類型強制轉(zhuǎn)換為無符號精度高類型時,會不直觀的執(zhí)行符號擴展,例如:
并非所有強制類型轉(zhuǎn)換都是由風(fēng)險的,把一個整數(shù)值轉(zhuǎn)換為一種具有相同符號的更寬類型時,是絕對安全的。
精度高的類型強制轉(zhuǎn)換為精度低的類型時,通過丟棄適當數(shù)量的最高有效位來獲取結(jié)果,也就是說會發(fā)生數(shù)據(jù)截斷,并且可能改變數(shù)據(jù)的符號位。
精度低的類型強制轉(zhuǎn)換為精度高的類型時,如果兩種類型具有相同的符號,那么沒什么問題;需要注意的是負的有符號精度低類型強制轉(zhuǎn)換為無符號精度高類型時,會不直觀的執(zhí)行符號擴展,例如:
2.signedcharfred = -1;
3.
4.bob=( unsignedint)fred; /*發(fā)生符號擴展,此時bob為0xFFFFFFFF*/
3.編譯器
如果你和一個優(yōu)秀的程序員共事,你會發(fā)現(xiàn)他對他使用的工具非常熟悉,就像一個畫家了解他的畫具一樣。----比爾.蓋茨
3.1不能簡單的認為是個工具
嵌入式程序開發(fā)跟硬件密切相關(guān),需要使用C語言來讀寫底層寄存器、存取數(shù)據(jù)、控制硬件等,C語言和硬件之間由編譯器來聯(lián)系,一些C標準不支持的硬件特性操作,由編譯器提供。
匯編可以很輕易的讀寫指定RAM地址、可以將代碼段放入指定的Flash地址、可以精確的設(shè)置變量在RAM中分布等等,所有這些操作,在深入了解編譯器后,也可以使用C語言實現(xiàn)。
C語言標準并非完美,有著數(shù)目繁多的未定義行為,這些未定義行為完全由編譯器自主決定,了解你所用的編譯器對這些未定義行為的處理,是必要的。
嵌入式編譯器對調(diào)試做了優(yōu)化,會提供一些工具,可以分析代碼性能,查看外設(shè)組件等,了解編譯器的這些特性有助于提高在線調(diào)試的效率。
此外,堆棧操作、代碼優(yōu)化、數(shù)據(jù)類型的范圍等等,都是要深入了解編譯器的理由。
如果之前你認為編譯器只是個工具,能夠編譯就好。那么,是時候改變這種思想了。
嵌入式程序開發(fā)跟硬件密切相關(guān),需要使用C語言來讀寫底層寄存器、存取數(shù)據(jù)、控制硬件等,C語言和硬件之間由編譯器來聯(lián)系,一些C標準不支持的硬件特性操作,由編譯器提供。
匯編可以很輕易的讀寫指定RAM地址、可以將代碼段放入指定的Flash地址、可以精確的設(shè)置變量在RAM中分布等等,所有這些操作,在深入了解編譯器后,也可以使用C語言實現(xiàn)。
C語言標準并非完美,有著數(shù)目繁多的未定義行為,這些未定義行為完全由編譯器自主決定,了解你所用的編譯器對這些未定義行為的處理,是必要的。
嵌入式編譯器對調(diào)試做了優(yōu)化,會提供一些工具,可以分析代碼性能,查看外設(shè)組件等,了解編譯器的這些特性有助于提高在線調(diào)試的效率。
此外,堆棧操作、代碼優(yōu)化、數(shù)據(jù)類型的范圍等等,都是要深入了解編譯器的理由。
如果之前你認為編譯器只是個工具,能夠編譯就好。那么,是時候改變這種思想了。
編譯器的語義檢查很弱小,甚至還會“掩蓋”錯誤?,F(xiàn)代的編譯器設(shè)計是件浩瀚的工程,為了讓編譯器設(shè)計簡單一些,目前幾乎所有編譯器的語義檢查都比較弱小。為了獲得更快的執(zhí)行效率,C語言被設(shè)計的足夠靈活且?guī)缀醪贿M行任何運行時檢查,比如數(shù)組越界、指針是否合法、運算結(jié)果是否溢出等等。這就造成了很多編譯正確但執(zhí)行奇怪的程序。
C語言足夠靈活,對于一個數(shù)組test[30],它允許使用像test[-1]這樣的形式來快速獲取數(shù)組首元素所在地址前面的數(shù)據(jù);允許將一個常數(shù)強制轉(zhuǎn)換為函數(shù)指針,使用代碼( ((void( ))0))來調(diào)用位于0地址的函數(shù)。C語言給了程序員足夠的自由,但也由程序員承擔濫用自由帶來的責(zé)任。
3.2.1莫名的死機
下面的兩個例子都是死循環(huán),如果在不常用分支中出現(xiàn)類似代碼,將會造成看似莫名其妙的死機或者重啟。
1.unsignedchari; //例程1
2.for(i= 0;i 256;i++)
3.{
4.//其它代碼
5.}
1.unsignedchari; //例程2
2.for(i= 10;i= 0;i--)
3.{
4.//其它代碼
5.}
對于無符號char類型,表示的范圍為0~255,所以無符號char類型變量i永遠小于256(第一個for循環(huán)無限執(zhí)行),永遠大于等于0(第二個for循環(huán)無線執(zhí)行)。需要說明的是,賦值代碼i=256是被C語言允許的,即使這個初值已經(jīng)超出了變量i可以表示的范圍。C語言會千方百計的為程序員創(chuàng)造出錯的機會,可見一斑。
3.2.2不起眼的改變
假如你在if語句后誤加了一個分號,可能會完全改變了程序邏輯。編譯器也會很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:
1.if(ab); //這里誤加了一個分號
2.a=b; //這句代碼一直被執(zhí)行
不但如此,編譯器還會忽略掉多余的空格符和換行符,就像下面的代碼也不會給出足夠提示:
1.if(n 3)
2.return//這里少加了一個分號
3.logrec.data=x[ 0];
4.logrec.time=x[ 1];
5.logrec.code=x[ 2];
這段代碼的本意是n3時程序直接返回,由于程序員的失誤,return少了一個結(jié)束分號。編譯器將它翻譯成返回表達式logrec.data=x[0]的結(jié)果,return后面即使是一個表達式也是C語言允許的。這樣當n=3時,表達式logrec.data=x[0];就不會被執(zhí)行,給程序埋下了隱患。
3.2.3 難查的數(shù)組越界
上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間就會寫數(shù)組越界。
一位同事的代碼在硬件上運行,一段時間后就會發(fā)現(xiàn)LCD顯示屏上的一個數(shù)字不正常的被改變。經(jīng)過一段時間的調(diào)試,問題被定位到下面的一段代碼中:
1.intSensorData[ 30];
2.//其他代碼
3.for(i= 30;i 0;i--)
4.{
5.SensorData[i]=…;
6.//其他代碼
7.}
這里聲明了擁有30個元素的數(shù)組,不幸的是for循環(huán)代碼中誤用了本不存在的數(shù)組元素SensorData[30],但C語言卻默許這么使用,并欣然的按照代碼改變了數(shù)組元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一個LCD顯示變量,這正是顯示屏上的那個值不正常被改變的原因。真慶幸這么輕而易舉的發(fā)現(xiàn)了這個Bug。
其實很多編譯器會對上述代碼產(chǎn)生一個警告:賦值超出數(shù)組界限。但并非所有程序員都對編譯器警告保持足夠敏感,況且,編譯器也并不能檢查出數(shù)組越界的所有情況。比如下面的例子:
你在模塊A中定義數(shù)組:
1.intSensorData[ 30];
在模塊B中引用該數(shù)組,但由于你引用代碼并不規(guī)范,這里沒有顯示聲明數(shù)組大小,但編譯器也允許這么做:
1.externintSensorData[];
這次,編譯器不會給出警告信息,因為編譯器壓根就不知道數(shù)組的元素個數(shù)。所以, 當一個數(shù)組聲明為具有外部鏈接,它的大小應(yīng)該顯式聲明。
再舉一個編譯器檢查不出數(shù)組越界的例子。函數(shù)func的形參是一個數(shù)組形式,函數(shù)代碼簡化如下所示:
1.char* func( charSensorData[ 30])
2. {
3.unsignedint i;
4.for(i= 30;i 0;i--)
5.{
6.SensorData[i]=…;
7.//其他代碼
8.}
9.}
這個給SensorData[30]賦初值的語句,編譯器也是不給任何警告的。實際上,編譯器是將數(shù)組名Sensor隱含的轉(zhuǎn)化為指向數(shù)組第一個元素的指針, 函數(shù)體是使用指針的形式來訪問數(shù)組的,它當然也不會知道數(shù)組元素的個數(shù)了。造成這種局面的原因之一是C編譯器的作者們認為指針代替數(shù)組可以提高程序效率,而且,可以簡化編譯器的復(fù)雜度。
指針和數(shù)組是容易給程序造成混亂的,我們有必要仔細的區(qū)分它們的不同。其實換一個角度想想,它們也是容易區(qū)分的:可以將數(shù)組名等同于指針的情況有且只有一處,就是上面例子提到的數(shù)組作為函數(shù)形參時。其它時候,數(shù)組名是數(shù)組名,指針是指針。
下面的例子編譯器同樣檢查不出數(shù)組越界。
我們常常用數(shù)組來緩存通訊中的一幀數(shù)據(jù)。在通訊中斷中將接收的數(shù)據(jù)保存到數(shù)組中,直到一幀數(shù)據(jù)完全接收后再進行處理。即使定義的數(shù)組長度足夠長,接收數(shù)據(jù)的過程中也可能發(fā)生數(shù)組越界,特別是干擾嚴重時。這是由于外界的干擾破壞了數(shù)據(jù)幀的某些位,對一幀的數(shù)據(jù)長度判斷錯誤,接收的數(shù)據(jù)超出數(shù)組范圍,多余的數(shù)據(jù)改寫與數(shù)組相鄰的變量,造成系統(tǒng)崩潰。由于中斷事件的異步性,這類數(shù)組越界編譯器無法檢查到。
如果局部數(shù)組越界,可能引發(fā)ARM架構(gòu)硬件異常。
同事的一個設(shè)備用于接收無線傳感器的數(shù)據(jù),一次軟件升級后,發(fā)現(xiàn)接收設(shè)備工作一段時間后會死機。調(diào)試表明ARM7處理器發(fā)生了硬件異常,異常處理代碼是一段死循環(huán)(死機的直接原因)。接收設(shè)備有一個硬件模塊用于接收無線傳感器的整包數(shù)據(jù)并存在自己的緩沖區(qū)中,當硬件模塊接收數(shù)據(jù)完成后,使用外部中斷通知設(shè)備取數(shù)據(jù),外部中斷服務(wù)程序精簡后如下所示:
1.__irq ExintHandler( void)
2. {
3.unsignedchar DataBuf[ 50];
4.GetData(DataBug); //從硬件緩沖區(qū)取一幀數(shù)據(jù)
5.//其他代碼
6.}
由于存在多個無線傳感器近乎同時發(fā)送數(shù)據(jù)的可能加之GetData函數(shù)保護力度不夠,數(shù)組DataBuf在取數(shù)據(jù)過程中發(fā)生越界。由于數(shù)組DataBuf為局部變量,被分配在堆棧中,同在此堆棧中的還有中斷發(fā)生時的運行環(huán)境以及中斷返回地址。溢出的數(shù)據(jù)將這些數(shù)據(jù)破壞掉,中斷返回時PC指針可能變成一個不合法值,硬件異常由此產(chǎn)生。
如果我們精心設(shè)計溢出部分的數(shù)據(jù),化數(shù)據(jù)為指令,就可以利用數(shù)組越界來修改PC指針的值,使之指向我們希望執(zhí)行的代碼。
1988年,第一個網(wǎng)絡(luò)蠕蟲在一天之內(nèi)感染了2000到6000臺計算機,這個蠕蟲程序利用的正是一個標準輸入庫函數(shù)的數(shù)組越界Bug。起因是一個標準輸入輸出庫函數(shù)gets,原來設(shè)計為從數(shù)據(jù)流中獲取一段文本,遺憾的是,gets函數(shù)沒有規(guī)定輸入文本的長度。gets函數(shù)內(nèi)部定義了一個500字節(jié)的數(shù)組,攻擊者發(fā)送了大于500字節(jié)的數(shù)據(jù),利用溢出的數(shù)據(jù)修改了堆棧中的PC指針,從而獲取了系統(tǒng)權(quán)限。目前,雖然有更好的庫函數(shù)來代替gets函數(shù),但gets函數(shù)仍然存在著。
3.2.4神奇的volatile
做嵌入式設(shè)備開發(fā),如果不對volatile修飾符具有足夠了解,實在是說不過去。volatile是C語言32個關(guān)鍵字中的一個,屬于類型限定符,常用的const關(guān)鍵字也屬于類型限定符。
volatile限定符用來告訴編譯器,該對象的值無任何持久性,不要對它進行任何優(yōu)化;它迫使編譯器每次需要該對象數(shù)據(jù)內(nèi)容時都必須讀該對象,而不是只讀一次數(shù)據(jù)并將它放在寄存器中以便后續(xù)訪問之用(這樣的優(yōu)化可以提高系統(tǒng)速度)。
這個特性在嵌入式應(yīng)用中很有用,比如你的IO口的數(shù)據(jù)不知道什么時候就會改變,這就要求編譯器每次都必須真正的讀取該IO端口。這里使用了詞語“真正的讀”,是因為由于編譯器的優(yōu)化,你的邏輯反應(yīng)到代碼上是對的,但是代碼經(jīng)過編譯器翻譯后,有可能與你的邏輯不符。你的代碼邏輯可能是每次都會讀取IO端口數(shù)據(jù),但實際上編譯器將代碼翻譯成匯編時,可能只是讀一次IO端口數(shù)據(jù)并保存到寄存器中,接下來的多次讀IO口都是使用寄存器中的值來進行處理。因為讀寫寄存器是最快的,這樣可以優(yōu)化程序效率。與之類似的,中斷里的變量、多線程中的共享變量等都存在這樣的問題。
不使用volatile,可能造成運行邏輯錯誤,但是不必要的使用volatile會造成代碼效率低下(編譯器不優(yōu)化volatile限定的變量),因此清楚的知道何處該使用volatile限定符,是一個嵌入式程序員的必修內(nèi)容。
一個程序模塊通常由兩個文件組成,源文件和頭文件。如果你在源文件定義變量:
1.unsignedinttest;
并在頭文件中聲明該變量:
1.externunsignedlongtest;
編譯器會提示一個語法錯誤:變量’ test’聲明類型不一致。但如果你在源文件定義變量:
1.volatileunsignedinttest;
在頭文件中這樣聲明變量:
1.externunsignedinttest; /*缺少volatile限定符*/
編譯器卻不會給出錯誤信息(有些編譯器僅給出一條警告)。當你在另外一個模塊(該模塊包含聲明變量test的頭文件)使用變量test時,它已經(jīng)不再具有volatile限定,這樣很可能造成一些重大錯誤。比如下面的例子,注意該例子是為了說明volatile限定符而專門構(gòu)造出的,因為現(xiàn)實中的volatile使用Bug大都隱含,并且難以理解。
在模塊A的源文件中,定義變量:
1.volatileunsignedintTimerCount= 0;
該變量用來在一個定時器中斷服務(wù)程序中進行軟件計時:
1.TimerCount++;
在模塊A的頭文件中,聲明變量:
1.externunsignedintTimerCount; //這里漏掉了類型限定符volatile
在模塊B中,要使用TimerCount變量進行精確的軟件延時:
1.# include“…A.h” //首先包含模塊A的頭文件
2.//其他代碼
3.TimerCount= 0;
4.while(TimerCount=TIMER_VALUE); //延時一段時間(感謝網(wǎng)友chhfish指出這里的邏輯錯誤)
5.//其他代碼
實際上,這是一個死循環(huán)。由于模塊A頭文件中聲明變量TimerCount時漏掉了volatile限定符,在模塊B中,變量TimerCount是被當作unsigned int類型變量。由于寄存器速度遠快于RAM,編譯器在使用非volatile限定變量時是先將變量從RAM中拷貝到寄存器中,如果同一個代碼塊再次用到該變量,就不再從RAM中拷貝數(shù)據(jù)而是直接使用之前寄存器備份值。代碼while(TimerCount=TIMER_VALUE)中,變量TimerCount僅第一次執(zhí)行時被使用,之后都是使用的寄存器備份值,而這個寄存器值一直為0,所以程序無限循環(huán)。圖3-1的流程圖說明了程序使用限定符volatile和不使用volatile的執(zhí)行過程。
為了更容易的理解編譯器如何處理volatile限定符,這里給出未使用volatile限定符和使用volatile限定符程序的反匯編代碼:
沒有使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認優(yōu)化級別,如下所示(注意最后兩行):
沒有使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認優(yōu)化級別,如下所示(注意最后兩行):
2.123:
3.0x00002E10E59F11D4 LDR R1,[PC,# 0x01D4]
4.0x00002E14E3A05000 MOV R5,#key1( 0x00000000)
5.0x00002E18E1A00005 MOV R0,R5
6.0x00002E1CE5815000 STR R5,[R1]
7.124: while(unIdleCount!= 200); //延時2S鐘
8.125:
9.0x00002E20E35000C8 CMP R0,# 0x000000C8
10.0x00002E241AFFFFFD BNE 0x00002E20/span
使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認優(yōu)化級別,如下所示(注意最后三行):
使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認優(yōu)化級別,如下所示(注意最后三行):
2.123:
3.0x00002E10E59F01D4 LDR R0,[PC,# 0x01D4]
4.0x00002E14E3A05000 MOV R5,#key1( 0x00000000)
5.0x00002E18E5805000 STR R5,[R0]
6.124: while(unIdleCount!= 200); //延時2S鐘
7.125:
8.0x00002E1CE5901000 LDR R1,[R0]
9.0x00002E20E35100C8 CMP R1,# 0x000000C8
10.0x00002E241AFFFFFC BNE 0x00002E1C
可以看到,如果沒有使用volatile關(guān)鍵字,程序一直比較R0內(nèi)數(shù)據(jù)與0xC8是否相等,但R0中的數(shù)據(jù)是0,所以程序會一直在這里循環(huán)比較(死循環(huán));再看使用了volatile關(guān)鍵字的反匯編代碼,程序會先從變量中讀出數(shù)據(jù)放到R1寄存器中,然后再讓R1內(nèi)數(shù)據(jù)與0xC8相比較,這才是我們C代碼的正確邏輯!
3.2.5局部變量
ARM架構(gòu)下的編譯器會頻繁的使用堆棧,堆棧用于存儲函數(shù)的返回值、AAPCS規(guī)定的必須保護的寄存器以及局部變量,包括局部數(shù)組、結(jié)構(gòu)體、聯(lián)合體和C++的類。默認情況下,堆棧的位置、初始值都是由編譯器設(shè)置,因此需要對編譯器的堆棧有一定了解。從堆棧中分配的局部變量的初值是不確定的,因此需要運行時顯式初始化該變量。一旦離開局部變量的作用域,這個變量立即被釋放,其它代碼也就可以使用它,因此堆棧中的一個內(nèi)存位置可能對應(yīng)整個程序的多個變量。
局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到的溫度值跟預(yù)期會有很大差別,因為在使用局部變量sum時,并不能保證它的初值為0。編譯器會在第一次運行時清零堆棧區(qū)域,這加重了此類Bug的隱蔽性。
1.unsignedintGetTempValue( void)
2. {
3.unsignedintsum; //定義局部變量,保存總值
4.for(i= 0;i 10;i++)
5.{
6.sum+=CollectTemp; //函數(shù)CollectTemp可以得到當前的溫度值
7.}
8.return(sum/ 10);
9.}
由于一旦程序離開局部變量的作用域即被釋放,所以下面代碼返回指向局部變量的指針是沒有實際意義的,該指針指向的區(qū)域可能會被其它程序使用,其值會被改變。
1.char* GetData( void)
2. {
3.charbuffer[ 100]; //局部數(shù)組
4.…
5.returnbuffer;
6.}
3.2.6使用外部工具
由于編譯器的語義檢查比較弱,我們可以使用第三方代碼分析工具,使用這些工具來發(fā)現(xiàn)潛在的問題,這里介紹其中比較著名的是PC-Lint。
PC-Lint由Gimpel Software公司開發(fā),可以檢查C代碼的語法和語義并給出潛在的BUG報告。PC-Lint可以顯著降低調(diào)試時間。
目前公司ARM7和Cortex-M3內(nèi)核多是使用Keil MDK編譯器來開發(fā)程序,通過簡單配置,PC-Lint可以被集成到MDK上,以便更方便的檢查代碼。MDK已經(jīng)提供了PC-Lint的配置模板,所以整個配置過程十分簡單,Keil MDK開發(fā)套件并不包含PC-Lint程序,在此之前,需要預(yù)先安裝可用的PC-Lint程序,配置過程如下:
點擊菜單Tools---Set-up PC-Lint…
img
PC-Lint Include Folders:該列表路徑下的文件才會被PC-Lint檢查,此外,這些路徑下的文件內(nèi)使用#include包含的文件也會被檢查;
Lint Executable:指定PC-Lint程序的路徑
Configuration File:指定配置文件的路徑,該配置文件由MDK編譯器提供。
菜單Tools---Lint 文件路徑.c/.h
檢查當前文件。
菜單Tools---Lint All C-Source Files
檢查所有C源文件。
PC-Lint的輸出信息顯示在MDK編譯器的Build Output窗口中,雙擊其中的一條信息可以跳轉(zhuǎn)到源文件所在位置。
編譯器語義檢查的弱小在很大程度上助長了不可靠代碼的廣泛存在。隨著時代的進步,現(xiàn)在越來越多的編譯器開發(fā)商意識到了語義檢查的重要性,編譯器的語義檢查也越來越強大,比如公司使用的Keil MDK編譯器,雖然它的編輯器依然不盡人意,但在其 V4.47及以上版本中增加了動態(tài)語法檢查并加強了語義檢查,可以友好的提示更多警告信息。建議經(jīng)常關(guān)注編譯器官方網(wǎng)站并將編譯器升級到V4.47或以上版本,升級的另一個好處是這些版本的編輯器增加了標識符自動補全功能,可以大大節(jié)省編碼的時間。
3.3你覺得有意義的代碼未必正確
C語言標準特別的規(guī)定某些行為是未定義的,編寫未定義行為的代碼,其輸出結(jié)果由編譯器決定!C標準委員會定義未定義行為的原因如下:
簡化標準,并給予實現(xiàn)一定的靈活性,比如不捕捉那些難以診斷的程序錯誤;
編譯器開發(fā)商可以通過未定義行為對語言進行擴展
C語言的未定義行為,使得C極度高效靈活并且給編譯器實現(xiàn)帶來了方便,但這并不利于優(yōu)質(zhì)嵌入式C程序的編寫。因為許多 C 語言中看起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并用一系列手段進行運行時檢查,使用Java可以相對容易的寫出安全代碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代碼來。
簡化標準,并給予實現(xiàn)一定的靈活性,比如不捕捉那些難以診斷的程序錯誤;
編譯器開發(fā)商可以通過未定義行為對語言進行擴展
C語言的未定義行為,使得C極度高效靈活并且給編譯器實現(xiàn)帶來了方便,但這并不利于優(yōu)質(zhì)嵌入式C程序的編寫。因為許多 C 語言中看起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并用一系列手段進行運行時檢查,使用Java可以相對容易的寫出安全代碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代碼來。
自增自減在表達式中連續(xù)出現(xiàn)并作用于同一變量或者自增自減在表達式中出現(xiàn)一次,但作用的變量多次出現(xiàn)
自增(++)和自減(--)這一動作發(fā)生在表達式的哪個時刻是由編譯器決定的,比如:
1.r = 1* a[i++] + 2* a[i++] + 3* a[i++];
不同的編譯器可能有著不同的匯編代碼,可能是先執(zhí)行i++再進行乘法和加法運行,也可能是先進行加法和乘法運算,再執(zhí)行i++,因為這句代碼在一個表達式中出現(xiàn)了連續(xù)的自增并作用于同一變量。更加隱蔽的是自增自減在表達式中出現(xiàn)一次,但作用的變量多次出現(xiàn),比如:
1.a[i] = i++; /* 未定義行為 */
先執(zhí)行i++再賦值,還是先賦值再執(zhí)行i++是由編譯器決定的,而兩種不同的執(zhí)行順序的結(jié)果差別是巨大的。
函數(shù)實參被求值的順序
函數(shù)如果有多個實參,這些實參的求值順序是由編譯器決定的,比如:
1.printf( "%d %d\n", ++n, power( 2, n)); /* 未定義行為 */
是先執(zhí)行++n還是先執(zhí)行power(2,n)是由編譯器決定的。
有符號整數(shù)溢出
有符號整數(shù)溢出是未定義的行為,編譯器決定有符號整數(shù)溢出按照哪種方式取值。比如下面代碼:
1.intvalue1,value2,sum
2.
3.//其它操作
4.sum=value1+value; /*sum可能發(fā)生溢出*/
有符號數(shù)右移、移位的數(shù)量是負值或者大于操作數(shù)的位數(shù)
除數(shù)為零
malloc、calloc或realloc分配零字節(jié)內(nèi)存
代碼中引入未定義行為會為代碼埋下隱患,防止代碼中出現(xiàn)未定義行為是困難的,我們總能不經(jīng)意間就會在代碼中引入未定義行為。但是還是有一些方法可以降低這種事件,總結(jié)如下:
了解C語言未定義行為
了解C語言未定義行為
標準C99附錄J.2“未定義行為”列舉了C99中的顯式未定義行為,通過查看該文檔,了解那些行為是未定義的,并在編碼中時刻保持警惕;
尋求工具幫助
尋求工具幫助
編譯器警告信息以及PC-Lint等靜態(tài)檢查工具能夠發(fā)現(xiàn)很多未定義行為并警告,要時刻關(guān)注這些工具反饋的信息;
總結(jié)并使用一些編碼標準
總結(jié)并使用一些編碼標準
1)避免構(gòu)造復(fù)雜的自增或者自減表達式,實際上,應(yīng)該避免構(gòu)造所有復(fù)雜表達式;
比如a[i] = i++;語句可以改為a[i] = i; i++;這兩句代碼。
2)只對無符號操作數(shù)使用位操作;
必要的運行時檢查
必要的運行時檢查
檢查是否溢出、除數(shù)是否為零,申請的內(nèi)存數(shù)量是否為零等等,比如上面的有符號整數(shù)溢出例子,可以按照如下方式編寫,以消除未定義特性:
1.intvalue1,value2,sum;
2.
3.//其它代碼
4.if((value1 0 value2 0 value1(INT_MAX-value2))||
5.(value1 0 value2 0 value1(INT_MIN-value2)))
6.{
7.//處理錯誤
8.}
9.else
10.{
11.sum=value1+value2;
12.}
上面的代碼是通用的,不依賴于任何CPU架構(gòu),但是代碼效率很低。如果是有符號數(shù)使用補碼的CPU架構(gòu)(目前常見CPU絕大多數(shù)都是使用補碼),還可以用下面的代碼來做溢出檢查:
intvalue1, value2, sum;
unsignedintusum = ( unsignedint)value1 + value2;
if((usum ^ value1) (usum ^ value2) INT_MIN)
{
/*處理溢出情況*/
}
else
{
sum = value1 + value2;
}
使用的原理解釋一下,因為在加法運算中,操作數(shù)value1和value2只有符號相同時,才可能發(fā)生溢出,所以我們先將這兩個數(shù)轉(zhuǎn)換為無符號類型,兩個數(shù)的和保存在變量usum中。如果發(fā)生溢出,則value1、value2和usum的最高位(符號位)一定不同,表達式(usum ^ value1) (usum ^ value2) 的最高位一定為1,這個表達式位與()上INT_MIN是為了將最高位之外的其它位設(shè)置為0。
了解你所用的編譯器對未定義行為的處理策略
了解你所用的編譯器對未定義行為的處理策略
很多引入了未定義行為的程序也能運行良好,這要歸功于編譯器處理未定義行為的策略。不是你的代碼寫的正確,而是恰好編譯器處理策略跟你需要的邏輯相同。了解編譯器的未定義行為處理策略,可以讓你更清楚的認識到那些引入了未定義行為程序能夠運行良好是多么幸運的事,不然多換幾個編譯器試試!
以Keil MDK為例,列舉常用的處理策略如下:
1) 有符號量的右移是算術(shù)移位,即移位時要保證符號位不改變。
2)對于int類的值:超過31位的左移結(jié)果為零;無符號值或正的有符號值超過31位的右移結(jié)果為零。負的有符號值移位結(jié)果為-1。
3)整型數(shù)除以零返回零
3.4 了解你的編譯器
在嵌入式開發(fā)過程中,我們需要經(jīng)常和編譯器打交道,只有深入了解編譯器,才能用好它,編寫更高效代碼,更靈活的操作硬件,實現(xiàn)一些高級功能。下面以公司最常用的Keil MDK為例,來描述一下編譯器的細節(jié)。
3.4.1編譯器的一些小知識
默認情況下,char類型的數(shù)據(jù)項是無符號的,所以它的取值范圍是0~255;
在所有的內(nèi)部和外部標識符中,大寫和小寫字符不同;
通常局部變量保存在寄存器中,但當局部變量太多放到棧里的時候,它們總是字對齊的。
壓縮類型的自然對齊方式為1。使用關(guān)鍵字__packed來壓縮特定結(jié)構(gòu),將所有有效類型的對齊邊界設(shè)置為1;
整數(shù)以二進制補碼形式表示;浮點量按IEEE格式存儲;
整數(shù)除法的余數(shù)的符號于被除數(shù)相同,由ISO C90標準得出;
如果整型值被截斷為短的有符號整型,則通過放棄適當數(shù)目的最高有效位來得到結(jié)果。如果原始數(shù)是太大的正或負數(shù),對于新的類型,無法保證結(jié)果的符號將于原始數(shù)相同。
整型數(shù)超界不引發(fā)異常;像unsigned char test; test=1000;這類是不會報錯的;
對于結(jié)構(gòu)體填充,根據(jù)定義結(jié)構(gòu)的方式,keil MDK編譯器用以下方式的一種來填充結(jié)構(gòu):
I 定義為static或者extern的結(jié)構(gòu)用零填充;
II ?;蚨焉系慕Y(jié)構(gòu),例如,用malloc或者auto定義的結(jié)構(gòu),使用先前存儲在那些存儲器位置的任何內(nèi)容進行填充。不能使用memcmp來比較以這種方式定義的填充結(jié)構(gòu)!
編譯器不對聲明為volatile類型的數(shù)據(jù)進行優(yōu)化;
__nop:延時一個指令周期,編譯器絕不會優(yōu)化它。如果硬件支持NOP指令,則該句被替換為NOP指令,如果硬件不支持NOP指令,編譯器將它替換為一個等效于NOP的指令,具體指令由編譯器自己決定;
__align(n):指示編譯器在n 字節(jié)邊界上對齊變量。對于局部變量,n的值為1、2、4、8;
attribute((at(address))):可以使用此變量屬性指定變量的絕對地址;
__inline:提示編譯器在合理的情況下內(nèi)聯(lián)編譯C或C++ 函數(shù);
我們程序中的一些全局變量和靜態(tài)變量在定義時進行了初始化,經(jīng)過編譯器編譯后,這些初始值被存放在了代碼的哪里?我們舉個例子說明:
1.unsignedintg_unRunFlag= 0xA5;
2.staticunsignedints_unCountFlag= 0x5A;
我曾做過一個項目,項目中的一個設(shè)備需要在線編程,也就是通過協(xié)議,將上位機發(fā)給設(shè)備的數(shù)據(jù)通過在應(yīng)用編程(IAP)技術(shù)寫入到設(shè)備的內(nèi)部Flash中。我將內(nèi)部Flash做了劃分,一小部分運行程序,大部分用來存儲上位機發(fā)來的數(shù)據(jù)。隨著程序量的增加,在一次更新程序后發(fā)現(xiàn),在線編程之后,設(shè)備運行正常,但是重啟設(shè)備后,運行出現(xiàn)了故障!經(jīng)過一系列排查,發(fā)現(xiàn)故障的原因是一個全局變量的初值被改變了。這是件很不可思議的事情,你在定義這個變量的時候指定了初始值,當你在第一次使用這個變量時卻發(fā)現(xiàn)這個初值已經(jīng)被改掉了!這中間沒有對這個變量做任何賦值操作,其它變量也沒有任何溢出,并且多次在線調(diào)試表明,進入main函數(shù)的時候,該變量的初值已經(jīng)被改為一個恒定值。
要想知道為什么全局變量的初值被改變,就要了解這些初值編譯后被放到了二進制文件的哪里。在此之前,需要先了解一點鏈接原理。
ARM映象文件各組成部分在存儲系統(tǒng)中的地址有兩種:一種是映象文件位于存儲器時(通俗的說就是存儲在Flash中的二進制代碼)的地址,稱為加載地址;一種是映象文件運行時(通俗的說就是給板子上電,開始運行Flash中的程序了)的地址,稱為運行時地址。賦初值的全局變量和靜態(tài)變量在程序還沒運行的時候,初值是被放在Flash中的,這個時候他們的地址稱為加載地址,當程序運行后,這些初值會從Flash中拷貝到RAM中,這時候就是運行時地址了。
原來,對于在程序中賦初值的全局變量和靜態(tài)變量,程序編譯后,MDK將這些初值放到Flash中,位于緊靠在可執(zhí)行代碼的后面。在程序進入main函數(shù)前,會運行一段庫代碼,將這部分數(shù)據(jù)拷貝至相應(yīng)RAM位置。由于我的設(shè)備程序量不斷增加,超過了為設(shè)備程序預(yù)留的Flash空間,在線編程時,將一部分存儲全局變量和靜態(tài)變量初值的Flash給重新編程了。在重啟設(shè)備前,初值已經(jīng)被拷貝到RAM中,所以這個時候程序運行是正常的,但重新上電后,這部分初值實際上是在線編程的數(shù)據(jù),自然與初值不同了。
3.4.3在C代碼中使用的變量,編譯器將他們分配到RAM的哪里?
我們會在代碼中使用各種變量,比如全局變量、靜態(tài)變量、局部變量,并且這些變量時由編譯器統(tǒng)一管理的,有時候我們需要知道變量用掉了多少RAM,以及這些變量在RAM中的具體位置。這是一個經(jīng)常會遇到的事情,舉一個例子,程序中的一個變量在運行時總是不正常的被改變,那么有理由懷疑它臨近的變量或數(shù)組溢出了,溢出的數(shù)據(jù)更改了這個變量值。要排查掉這個可能性,就必須知道該變量被分配到RAM的哪里、這個位置附近是什么變量,以便針對性的做跟蹤。
其實MDK編譯器的輸出文件中有一個“工程名.map”文件,里面記錄了代碼、變量、堆棧的存儲位置,通過這個文件,可以查看使用的變量被分配到RAM的哪個位置。要生成這個文件,需要在Options for Targer窗口,Listing標簽欄下,勾選Linker Listing前的復(fù)選框,如圖3-1所示。
img
圖3-1 設(shè)置編譯器生產(chǎn)MAP文件
3.4.4默認情況下,棧被分配到RAM的哪個地方?
MDK中,我們只需要在配置文件中定義堆棧大小,編譯器會自動在RAM的空閑區(qū)域選擇一塊合適的地方來分配給我們定義的堆棧,這個地方位于RAM的那個地方呢?
通過查看MAP文件,原來MDK將堆棧放到程序使用到的RAM空間的后面,比如你的RAM空間從0x4000 0000開始,你的程序用掉了0x200字節(jié)RAM,那么堆棧空間就從0x4000 0200處開始。
使用了多少堆棧,是否溢出?
2.4.5 有多少RAM會被初始化?
在進入main函數(shù)之前,MDK會把未初始化的RAM給清零的,我們的RAM可能很大,只使用了其中一小部分,MDK會不會把所有RAM都初始化呢?
答案是否定的,MDK只是把你的程序用到的RAM以及堆棧RAM給初始化,其它RAM的內(nèi)容是不管的。如果你要使用絕對地址訪問MDK未初始化的RAM,那就要小心翼翼的了,因為這些RAM上電時的內(nèi)容很可能是隨機的,每次上電都不同。
3.4.6 MDK編譯器如何設(shè)置非零初始化變量?
對于控制類產(chǎn)品,當系統(tǒng)復(fù)位后(非上電復(fù)位),可能要求保持住復(fù)位前RAM中的數(shù)據(jù),用來快速恢復(fù)現(xiàn)場,或者不至于因瞬間復(fù)位而重啟現(xiàn)場設(shè)備。而keil mdk在默認情況下,任何形式的復(fù)位都會將RAM區(qū)的非初始化變量數(shù)據(jù)清零。
MDK編譯程序生成的可執(zhí)行文件中,每個輸出段都最多有三個屬性:RO屬性、RW屬性和ZI屬性。對于一個全局變量或靜態(tài)變量,用const修飾符修飾的變量最可能放在RO屬性區(qū),初始化的變量會放在RW屬性區(qū),那么剩下的變量就要放到ZI屬性區(qū)了。默認情況下,ZI屬性區(qū)的數(shù)據(jù)在每次復(fù)位后,程序執(zhí)行main函數(shù)內(nèi)的代碼之前,由編譯器“自作主張”的初始化為零。所以我們要在C代碼中設(shè)置一些變量在復(fù)位后不被零初始化,那一定不能任由編譯器“胡作非為”,我們要用一些規(guī)則,約束一下編譯器。
分散加載文件對于連接器來說至關(guān)重要,在分散加載文件中,使用UNINIT來修飾一個執(zhí)行節(jié),可以避免編譯器對該區(qū)節(jié)的ZI數(shù)據(jù)進行零初始化。這是要解決非零初始化變量的關(guān)鍵。因此我們可以定義一個UNINIT修飾的數(shù)據(jù)節(jié),然后將希望非零初始化的變量放入這個區(qū)域中。于是,就有了第一種方法:
修改分散加載文件,增加一個名為MYRAM的執(zhí)行節(jié),該執(zhí)行節(jié)起始地址為0x1000A000,長度為0x2000字節(jié)(8KB),由UNINIT修飾:
2: ER_IROM1 0x000000000x00080000{ ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x100000000x0000A000{ ; RW data
8: .ANY (+RW +ZI)
9: }
10: MYRAM 0x1000A000UNINIT 0x00002000{
11: .ANY (NO_INIT)
12: }
13: }
那么,如果在程序中有一個數(shù)組,你不想讓它復(fù)位后零初始化,就可以這樣來定義變量:
1.unsignedcharplc_eu_backup[ 32] __attribute__((at( 0x1000A000)));
變量屬性修飾符__attribute__((at(adde)))用來將變量強制定位到adde所在地址處。由于地址0x1000A000開始的8KB區(qū)域ZI變量不會被零初始化,所以位于這一區(qū)域的數(shù)組plc_eu_backup也就不會被零初始化了。
這種方法的缺點是顯而易見的:要程序員手動分配變量的地址。如果非零初始化數(shù)據(jù)比較多,這將是件難以想象的大工程(以后的維護、增加、修改代碼等等)。所以要找到一種辦法,讓編譯器去自動分配這一區(qū)域的變量。
分散加載文件同方法1,如果還是定義一個數(shù)組,可以用下面方法:
變量屬性修飾符__attribute__((section(“name”),zero_init))用于將變量強制定義到name屬性數(shù)據(jù)節(jié)中,zero_init表示將未初始化的變量放到ZI數(shù)據(jù)節(jié)中。因為“NO_INIT”這顯性命名的自定義節(jié),具有UNINIT屬性。
將一個模塊內(nèi)的非初始化變量都非零初始化
假如該模塊名字為test.c,修改分散加載文件如下所示:
1: LR_IROM1 0x000000000x00080000{ ; load region size_region
2: ER_IROM1 0x000000000x00080000{ ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x100000000x0000A000{ ; RW data
8: .ANY (+RW +ZI)
9: }
10: RW_IRAM2 0x1000A000UNINIT 0x00002000{
11: test.o (+ZI)
12: }
13: }
在該模塊定義時變量時使用如下方法:
這里,變量屬性修飾符__attribute__((zero_init))用于將未初始化的變量放到ZI數(shù)據(jù)節(jié)中變量,其實MDK默認情況下,未初始化的變量就是放在ZI數(shù)據(jù)區(qū)的。
4.防御性編程
嵌入式產(chǎn)品的可靠性自然與硬件密不可分,但在硬件確定、并且沒有第三方測試的前提下,使用防御性編程思想寫出的代碼,往往具有更高的穩(wěn)定性。
防御性編程首先需要認清C語言的種種缺陷和陷阱,C語言對于運行時的檢查十分弱小,需要程序員謹慎的考慮代碼,在必要的時候增加判斷;防御性編程的另一個核心思想是假設(shè)代碼運行在并不可靠的硬件上,外接干擾有可能會打亂程序執(zhí)行順序、更改RAM存儲數(shù)據(jù)等等。
4.1具有形參的函數(shù),需判斷傳遞來的實參是否合法。
程序員可能無意識的傳遞了錯誤參數(shù);外界的強干擾可能將傳遞的參數(shù)修改掉,或者使用隨機參數(shù)意外的調(diào)用函數(shù),因此在執(zhí)行函數(shù)主體前,需要先確定實參是否合法。
1.intexam_fun( unsignedchar*str )
2. {
3.if( str != NULL) // 檢查“假設(shè)指針不為空”這個條件
4.{
5.//正常處理代碼
6.}
7.else
8.{
9.//處理錯誤代碼
10.}
11.}
4.2仔細檢查函數(shù)的返回值
對函數(shù)返回的錯誤碼,要進行全面仔細處理,必要時做錯誤記錄。
1.char* DoSomething(…)
2. {
3.char* p;
4.p= malloc( 1024);
5.if(p== NULL) /*對函數(shù)返回值作出判斷*/
6.{
7.UARTprintf(…); /*打印錯誤信息*/
8.returnNULL;
9.}
10.retuen p;
11.}
4.3 防止指針越界
如果動態(tài)計算一個地址時,要保證被計算的地址是合理的并指向某個有意義的地方。特別對于指向一個結(jié)構(gòu)或數(shù)組的內(nèi)部的指針,當指針增加或者改變后仍然指向同一個結(jié)構(gòu)或數(shù)組。
4.4 防止數(shù)組越界
數(shù)組越界的問題前文已經(jīng)講述的很多了,由于C不會對數(shù)組進行有效的檢測,因此必須在應(yīng)用中顯式的檢測數(shù)組越界問題。下面的例子可用于中斷接收通訊數(shù)據(jù)。
1.# defineREC_BUF_LEN 100
2.unsignedcharRecBuf[REC_BUF_LEN];
3.//其它代碼
4.voidUart_IRQHandler( void)
5. {
6.staticRecCount= 0; //接收數(shù)據(jù)長度計數(shù)器
7.//其它代碼
8.if(RecCount REC_BUF_LEN) //判斷數(shù)組是否越界
9.{
10.RecBuf[RecCount]=…; //從硬件取數(shù)據(jù)
11.RecCount++;
12.//其它代碼
13.}
14.else
15.{
16.//錯誤處理代碼
17.}
18.//其它代碼
19.}
在使用一些庫函數(shù)時,同樣需要對邊界進行檢查,比如下面的memset(RecBuf,0,len)函數(shù)把RecBuf指指向的內(nèi)存區(qū)的前l(fā)en個字節(jié)用0填充,如果不注意len的長度,就會將數(shù)組RecBuf之外的內(nèi)存區(qū)清零:
1.# defineREC_BUF_LEN 100
2.unsignedcharRecBuf[REC_BUF_LEN];
3.
4.if(len REC_BUF_LEN)
5.{
6.memset(RecBuf, 0,len); //將數(shù)組RecBuf清零
7.}
8.else
9.{
10.//處理錯誤
11.}
4.5 數(shù)學(xué)算數(shù)運算 4.5.1除法運算,只檢測除數(shù)為零就可靠嗎?
除法運算前,檢查除數(shù)是否為零幾乎已經(jīng)成為共識,但是僅檢查除數(shù)是否為零就夠了嗎?
1.# includelimits.h
2.signedlongsl1,sl2,result;
3./*初始化sl1和sl2*/
4.if((sl2== 0)||(sl1==LONG_MIN sl2== -1))
5.{
6.//處理錯誤
7.}
8.else
9.{
10.result = sl1 / sl2;
11.}
4.5.2檢測運算溢出
整數(shù)的加減乘運算都有可能發(fā)生溢出,在討論未定義行為時,給出過一個有符號整形加法溢出判斷代碼,這里再給出一個無符號整形加法溢出判斷代碼段:
1.# includelimits.h
2.unsignedinta,b,result;
3./*初始化a,b*/
4.if(UINT_MAX-ab)
5.{
6.//處理溢出
7.}
8.else
9.{
10.result=a+b;
11.}
嵌入式硬件一般沒有浮點處理器,浮點數(shù)運算在嵌入式也比較少見并且溢出判斷嚴重依賴C庫支持,這里不討論。
4.5.3檢測移位
在討論未定義行為時,提到有符號數(shù)右移、移位的數(shù)量是負值或者大于操作數(shù)的位數(shù)都是未定義行為,也提到不對有符號數(shù)進行位操作,但要檢測移位的數(shù)量是否大于操作數(shù)的位數(shù)。下面給出一個無符號整數(shù)左移檢測代碼段:
1.unsignedintui1;
2.unsignedintui2;
3.unsignedinturesult;
4.
5./*初始化ui1,ui2*/
6.if(ui2= sizeof( unsignedint)*CHAR_BIT)
7.{
8.//處理錯誤
9.}
10.else
11.{
12.uresult=ui1ui2;
13.}
4.6如果有硬件看門狗,則使用它
在其它一切措施都失效的情況下,看門狗可能是最后的防線。它的原理特別簡單,但卻能大大提高設(shè)備的可靠性。如果設(shè)備有硬件看門狗,一定要為它編寫驅(qū)動程序。
要盡可能早的開啟看門狗
要盡可能早的開啟看門狗
這是因為從上電復(fù)位結(jié)束到開啟看門狗的這段時間內(nèi),設(shè)備有可能被干擾而跳過看門狗初始化程序,導(dǎo)致看門狗失效。盡可能早的開啟看門狗,可以降低這種概率;
不要在中斷中喂狗,除非有其他聯(lián)動措施
不要在中斷中喂狗,除非有其他聯(lián)動措施
在中斷程序喂狗,由于干擾的存在,程序可能一直處于中斷之中,這樣會導(dǎo)致看門狗失效。如果在主程序中設(shè)置標志位,中斷程序喂狗時與這個標志位聯(lián)合判斷,也是允許的;
喂狗間隔跟產(chǎn)品需求有關(guān),并非特定的時間
喂狗間隔跟產(chǎn)品需求有關(guān),并非特定的時間
產(chǎn)品的特性決定了喂狗間隔。對于不涉及安全性、實時性的設(shè)備,喂狗間隔比較寬松,但間隔時間不宜過長,否則被用戶感知到,是影響用戶體驗的。對于設(shè)計安全性、有實時控制類的設(shè)備,原則是盡可能快的復(fù)位,否則會造成事故。
克萊門汀號在進行第二階段的任務(wù)時,原本預(yù)訂要從月球飛行到太空深處的Geographos小行星進行探勘,然而這艘太空探測器在飛向小行星時卻由于一個軟件缺陷而使其中斷運作20分鐘,不但未能到達小行星,也因為控制噴嘴燃燒了11分鐘使電力供應(yīng)降低,無法再透過遠端控制探測器,最終結(jié)束這項任務(wù),但也導(dǎo)致了資源與資金的浪費。
“克萊門汀太空任務(wù)失敗這件事讓我感到十分震驚,它其實可以透過硬件中一款簡單的看門狗計時器避免掉這項意外,但由于當時的開發(fā)時間相當緊縮,程序設(shè)計人員沒時間編寫程序來啟動它,”Ganssle說。
遺憾的是,1998年發(fā)射的近地號太空船(NEAR)也遇到了相同的問題。由于編程人員并未采納建議,因此,當推進器減速器系統(tǒng)故障時,29公斤的儲備燃料也隨之報銷──這同樣是一個本來可經(jīng)由看門狗定時器編程而避免的問題,同時也證明要從其他程序設(shè)計人員的錯誤中學(xué)習(xí)并不容易。
4.7關(guān)鍵數(shù)據(jù)儲存多個備份,取數(shù)據(jù)采用“表決法”
RAM中的數(shù)據(jù)在受到干擾情況下有可能被改變,對于系統(tǒng)關(guān)鍵數(shù)據(jù)應(yīng)該進行保護。關(guān)鍵數(shù)據(jù)包括全局變量、靜態(tài)變量以及需要保護的數(shù)據(jù)區(qū)域。備份數(shù)據(jù)與原數(shù)據(jù)不應(yīng)該處于相鄰位置,因此不應(yīng)由編譯器默認分配備份數(shù)據(jù)位置,而應(yīng)該由程序員指定區(qū)域存儲??梢詫AM分為3個區(qū)域,第一個區(qū)域保存原碼,第二個區(qū)域保存反碼,第三個區(qū)域保存異或碼,區(qū)域之間預(yù)留一定量的“空白”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è)置為:
1.LR_IROM1 0x000000000x00080000{ ; load region size_region
2.ER_IROM1 0x000000000x00080000{ ; load address = execution address
3.*.o (RESET, +First)
4.*(InRoot$$Sections)
5..ANY (+RO)
6.}
7.RW_IRAM1 0x100000000x00008000{ ;保存原碼
8..ANY (+RW +ZI )
9.}
10.
11.RW_IRAM3 0x100090000x00001000{ ;保存反碼
12..ANY (MY_BK1)
13.}
14.
15.RW_IRAM2 0x1000B0000x00001000{ ;保存異或碼
16..ANY (MY_BK2)
17.}
18.}
如果一個關(guān)鍵變量需要多處備份,可以按照下面方式定義變量,將三個變量分別指定到三個不連續(xù)的RAM區(qū)中,并在定義時按照原碼、反碼、0xAA的異或碼進行初始化。
1.uint32 plc_pc= 0; //原碼
2.__attribute__((section( "MY_BK1"))) uint32 plc_pc_not=~ 0x0; //反碼
3.__attribute__((section( "MY_BK2"))) uint32 plc_pc_xor= 0x0^ 0xAAAAAAAA; //異或碼
當需要寫這個變量時,這三個位置都要更新;讀取變量時,讀取三個值做判斷,取至少有兩個相同的那個值。
為什么選取異或碼而不是補碼?這是因為MDK的整數(shù)是按照補碼存儲的,正數(shù)的補碼與原碼相同,在這種情況下,原碼和補碼是一致的,不但起不到冗余作用,反而對可靠性有害。比如存儲的一個非零整數(shù)區(qū)因為干擾,RAM都被清零,由于原碼和補碼一致,按照3取2的“表決法”,會將干擾值0當做正確的數(shù)據(jù)。
4.8對非易失性存儲器進行備份存儲
非易失性存儲器包括但不限于Flash、EEPROM、鐵電。僅僅將寫入非易失性存儲器中的數(shù)據(jù)再讀出校驗是不夠的。強干擾情況下可能導(dǎo)致非易失性存儲器內(nèi)的數(shù)據(jù)錯誤,在寫非易失性存儲器的期間系統(tǒng)掉電將導(dǎo)致數(shù)據(jù)丟失,因干擾導(dǎo)致程序跑飛到寫非易失性存儲器函數(shù)中,將導(dǎo)致數(shù)據(jù)存儲紊亂。一種可靠的辦法是將非易失性存儲器分成多個區(qū),每個數(shù)據(jù)都將按照不同的形式寫入到這些分區(qū)中,需要進行讀取時,同時讀出多份數(shù)據(jù)并進行表決,取相同數(shù)目較多的那個值。
4.9軟件鎖
對于初始化序列或者有一定先后順序的函數(shù)調(diào)用,為了保證調(diào)用順序或者確保每個函數(shù)都被調(diào)用,我們可以使用環(huán)環(huán)相扣,實質(zhì)上這也是一種軟件鎖。此外對于一些安全關(guān)鍵代碼語句(是語句,而不是函數(shù)),可以給它們設(shè)置軟件鎖,只有持有特定鑰匙的,才可以訪問這些關(guān)鍵代碼。也可以通俗的理解為,關(guān)鍵安全代碼不能按照單一條件執(zhí)行,要額外的多設(shè)置一個標志。
比如,向Flash寫一個數(shù)據(jù),我們會判斷數(shù)據(jù)是否合法、寫入的地址是否合法,計算要寫入的扇區(qū)。之后調(diào)用寫Flash子程序,在這個子程序中,判斷扇區(qū)地址是否合法、數(shù)據(jù)長度是否合法,之后就要將數(shù)據(jù)寫入Flash。由于寫Flash語句是安全關(guān)鍵代碼,所以程序給這些語句上鎖:必須具有正確的鑰匙才可以寫Flash。這樣即使是程序跑飛到寫Flash子程序,也能大大降低誤寫的風(fēng)險。
1./****************************************************************************
2. * 名稱:RamToFlash
3. * 功能:復(fù)制RAM的數(shù)據(jù)到FLASH,命令代碼51。
4. * 入口參數(shù):dst 目標地址,即FLASH起始地址。以512字節(jié)為分界
5. * src 源地址,即RAM地址。地址必須字對齊
6. * no 復(fù)制字節(jié)個數(shù),為512/1024/4096/8192
7. * ProgStart 軟件鎖標志
8. * 出口參數(shù):IAP返回值(paramout緩沖區(qū)) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
9. SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未選擇扇區(qū)
10. ****************************************************************************/
11.voidRamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)
12. {
13.PLC_ASSERT( "Sector number",(dst= 0x00040000)(dst= 0x0007FFFF));
14.PLC_ASSERT( "Copy bytes number is 512",(no== 512));
15.PLC_ASSERT( "ProgStart==0xA5",(ProgStart== 0xA5));
16.
17.paramin[ 0] = IAP_RAMTOFLASH; // 設(shè)置命令字
18.paramin[ 1] = dst; // 設(shè)置參數(shù)
19.paramin[ 2] = src;
20.paramin[ 3] = no;
21.paramin[ 4] = Fcclk/ 1000;
22.if(ProgStart== 0xA5) //只有軟件鎖標志正確時,才執(zhí)行關(guān)鍵代碼
23.{
24.iap_entry(paramin, paramout); // 調(diào)用IAP服務(wù)程序
25.ProgStart= 0;
26.}
27.else
28.{
29.paramout[ 0]=PROG_UNSTART;
30.}
31.}
該程序段是編程lpc1778內(nèi)部Flash,其中調(diào)用IAP程序的函數(shù)iap_entry(paramin, paramout)是關(guān)鍵安全代碼,所以在執(zhí)行該代碼前,先判斷一個特定設(shè)置的安全鎖標志ProgStart,只有這個標志符合設(shè)定值,才會執(zhí)行編程Flash操作。如果因為意外程序跑飛到該函數(shù),由于ProgStart標志不正確,是不會對Flash進行編程的。
4.10通信
制定協(xié)議時,限制每幀的字節(jié)數(shù);
制定協(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校驗程序;
增加額外判斷
增加額外判斷
1)增加緩沖區(qū)溢出判斷。這是因為數(shù)據(jù)接收多是在中斷中完成,編譯器檢測不出緩沖區(qū)是否溢出,需要手動檢查,在上文介紹數(shù)據(jù)溢出一節(jié)中已經(jīng)詳細說明。
2)增加超時判斷。當一幀數(shù)據(jù)接收到一半,長時間接收不到剩余數(shù)據(jù),則認為這幀數(shù)據(jù)無效,重新開始接收??蛇x,跟不同的協(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ū)很可能溢出,后果是災(zāi)難性的。
重傳機制
重傳機制
如果檢測到通訊數(shù)據(jù)發(fā)生了錯誤,則要有重傳機制重新發(fā)送出錯的幀。
4.11開關(guān)量輸入的檢測、確認
開關(guān)量容易受到尖脈沖干擾,如果不進行濾除,可能會造成誤動作。一般情況下,需要對開關(guān)量輸入信號進行多次采樣,并進行邏輯判斷直到確認信號無誤為止。
4.12開關(guān)量輸出
開關(guān)信號簡單的一次輸出是不安全的,干擾信號可能會翻轉(zhuǎn)開關(guān)量輸出的狀態(tài)。采取重復(fù)刷新輸出可以有效防止電平的翻轉(zhuǎn)。
4.13初始化信息的保存和恢復(fù)
微處理器的寄存器值也可能會因外界干擾而改變,外設(shè)初始化值需要在寄存器中長期保存,最容易被破壞。由于Flash中的數(shù)據(jù)相對不易被破壞,可以將初始化信息預(yù)先寫入Flash,待程序空閑時比較與初始化相關(guān)的寄存器值是否被更改,如果發(fā)現(xiàn)非法更改則使用Flash中的值進行恢復(fù)。
公司目前使用的4.3寸LCD顯示屏抗干擾能力一般。如果顯示屏與控制器之間的排線距離過長或者對使用該顯示屏的設(shè)備打靜電或者脈沖群,顯示屏有可能會花屏或者白屏。對此,我們可以將初始化顯示屏的數(shù)據(jù)保存在Flash中,程序運行后,每隔一段時間從顯示屏的寄存器讀出當前值和Flash存儲的值相比較,如果發(fā)現(xiàn)兩者不同,則重新初始化顯示屏。下面給出校驗源碼,僅供參考。
定義數(shù)據(jù)結(jié)構(gòu):
1.typedefstruct{
2.uint8_tlcd_command; //LCD寄存器
3.uint8_tlcd_get_value[ 8]; //初始化時寫入寄存器的值
4.uint8_tlcd_value_num; //初始化時寫入寄存器值的數(shù)目
5.}lcd_redu_list_struct;
定義const修飾的結(jié)構(gòu)體變量,存儲LCD部分寄存器的初始值,這個初始值跟具體的應(yīng)用初始化有關(guān),不一定是表中的數(shù)據(jù),通常情況下,這個結(jié)構(gòu)體變量被存儲到Flash中。
1./*LCD部分寄存器設(shè)置值列表*/
2.lcd_redu_list_struct constlcd_redu_list_str[]=
3.{
4.{SSD1963_Get_Address_Mode,{ 0x20} , 1}, /*1*/
5.{SSD1963_Get_Pll_Mn ,{ 0x3b, 0x02, 0x04} , 3}, /*2*/
6.{SSD1963_Get_Pll_Status ,{ 0x04} , 1}, /*3*/
7.{SSD1963_Get_Lcd_Mode ,{ 0x24, 0x20, 0x01, 0xdf, 0x01, 0x0f, 0x00} , 7}, /*4*/
8.{SSD1963_Get_Hori_Period ,{ 0x02, 0x0c, 0x00, 0x2a, 0x07, 0x00, 0x00, 0x00}, 8}, /*5*/
9.{SSD1963_Get_Vert_Period ,{ 0x01, 0x1d, 0x00, 0x0b, 0x09, 0x00, 0x00} , 7}, /*6*/
10.{SSD1963_Get_Power_Mode ,{ 0x1c} , 1}, /*7*/
11.{SSD1963_Get_Display_Mode,{ 0x03} , 1}, /*8*/
12.{SSD1963_Get_Gpio_Conf ,{ 0x0F, 0x01} , 2}, /*9*/
13.{SSD1963_Get_Lshift_Freq ,{ 0x00, 0xb8} , 2}, /*10*/
14.};
實現(xiàn)函數(shù)如下所示,函數(shù)會遍歷結(jié)構(gòu)體變量中的每一個命令,以及每一個命令下的初始值,如果有一個不正確,則跳出循環(huán),執(zhí)行重新初始化和恢復(fù)措施。這個函數(shù)中的MY_DEBUGF宏是我自己的調(diào)試函數(shù),使用串口打印調(diào)試信息,在接下來的第五部分將詳細敘述。通過這個函數(shù),我可以長時間監(jiān)控顯示屏的哪些命令、哪些位容易被干擾。程序里使用了一個被妖魔化的關(guān)鍵字:goto。大多數(shù)C語言書籍對goto關(guān)鍵字談之色變,但你應(yīng)該有自己的判斷。在函數(shù)內(nèi)部跳出多重循環(huán),除了goto關(guān)鍵字,又有哪種方法能如此簡潔高效!
1./**
2. * lcd 顯示冗余
3. * 每隔一段時間調(diào)用該程序一次
4. */
5.voidlcd_redu( void)
6. {
7.uint8_ttmp[ 8];
8.uint32_ti,j;
9.uint32_tlcd_init_flag;
10.
11.lcd_init_flag = 0;
12.for(i= 0;i sizeof(lcd_redu_list_str)/ sizeof(lcd_redu_list_str[ 0]);i++)
13.{
14.LCD_SendCommand(lcd_redu_list_str[i].lcd_command);
15.uyDelay( 10);
16.for(j= 0;jlcd_redu_list_str[i].lcd_value_num;j++)
17.{
18.tmp[j]=LCD_ReadData;
19.if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])
20.{
21.lcd_init_flag= 0x55;
22.MY_DEBUGF(MENU_DEBUG,( "讀lcd寄存器值與預(yù)期不符,命令為:0x%x,第%d個參數(shù),
23. 該參數(shù)正確值為:0x%x,實際讀出值為:0x%x\n" ,lcd_redu_list_str[i].lcd_command,j+ 1,
24.lcd_redu_list_str[i].lcd_get_value[j],tmp[j]));
25.gotohandle_lcd_init;
26.}
27.}
28.}
29.
30.handle_lcd_init:
31.if(lcd_init_flag== 0x55)
32.{
33.//重新初始化LCD
34.//一些必要的恢復(fù)措施
35.}
36.}
4.14陷阱
對于8051內(nèi)核單片機,由于沒有相應(yīng)的硬件支持,可以用純軟件設(shè)置軟件陷阱,用來攔截一些程序跑飛。對于ARM7或者Cortex-M系列單片機,硬件已經(jīng)內(nèi)建了多種異常,軟件需要根據(jù)硬件異常來編寫陷阱程序,用來快速定位甚至恢復(fù)錯誤。
4.15阻塞處理
有時候程序員會使用while(!flag);語句阻塞在此等待標志flag改變,比如串口發(fā)送時用來等待一字節(jié)數(shù)據(jù)發(fā)送完成。這樣的代碼時存在風(fēng)險的,如果因為某些原因標志位一直不改變則會造成系統(tǒng)死機。
一個良好冗余的程序是設(shè)置一個超時定時器,超過一定時間后,強制程序退出while循環(huán)。
2003年8月11日發(fā)生的W32.Blaster.Worm蠕蟲事件導(dǎo)致全球經(jīng)濟損失高達5億美元,這個漏洞是利用了Windows分布式組件對象模型的遠程過程調(diào)用接口中的一個邏輯缺陷:在調(diào)用GetMachineName函數(shù)時,循環(huán)只設(shè)置了一個不充分的結(jié)束條件。
原代碼簡化如下所示:
1.HRESULT GetMachineName( WCHAR *pwszPath,
2.WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+ 1])
3. {
4.WCHAR *pwszServerName = wszMachineName;
5.WCHAR *pwszTemp = pwszPath + 2;
6.while( *pwszTemp != L’\\’ ) /* 這句代碼循環(huán)結(jié)束條件不充分 */
7.*pwszServerName++= *pwszTemp++;
8./*… */
9.}
微軟發(fā)布的安全補丁MS03-026解決了這個問題,為GetMachineName函數(shù)設(shè)置了充分終止條件。一個解決代碼簡化如下所示(并非微軟補丁代碼):
1.HRESULT GetMachineName( WCHAR *pwszPath,
2.WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+ 1])
3. {
4.WCHAR *pwszServerName = wszMachineName;
5.WCHAR *pwszTemp = pwszPath + 2;
6.WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
7.while((*pwszTemp != L’\\’ ) (*pwszTemp != L’\ 0’)
8. (pwszServerNameend_addr)) /*充分終止條件*/
9.*pwszServerName++= *pwszTemp++;
10./*… */
11.}
5.測試,再測試
思維再縝密的程序員也不可能編寫完全無缺陷的程序,測試的目的正是盡可能多的發(fā)現(xiàn)這些缺陷并改正。這里說的測試,是指程序員的自測試。前期的自測試能夠更早的發(fā)現(xiàn)錯誤,相應(yīng)的修復(fù)成本也會很低,如果你不徹底測試自己的代碼,恐怕你開發(fā)的就不只是代碼,可能還會聲名狼藉。
優(yōu)質(zhì)嵌入式C程序跟優(yōu)質(zhì)的基礎(chǔ)元素關(guān)系密切,可以將函數(shù)作為基礎(chǔ)元素,我們的測試正是從最基本的函數(shù)開始。判斷哪些函數(shù)需要測試需要一定的經(jīng)驗積累,雖然代碼行數(shù)跟邏輯復(fù)雜度并不成正比,但如果你不能判斷某個函數(shù)是否要測試,一個簡單粗暴的方法是:當函數(shù)有效代碼超過20行,就測試它。
程序員對自己的代碼以及邏輯關(guān)系十分清楚,測試時,按照每一個邏輯分支全面測試。很多錯誤發(fā)生在我們認為不會出錯的地方,所以即便某個邏輯分支很簡單,也建議測試一遍。第一個原因是我們自己看自己的代碼總是不容易發(fā)現(xiàn)錯誤,而測試能暴露這些錯誤;另一方面,語法正確、邏輯正確的代碼,經(jīng)過編譯器編譯后,生成的匯編代碼很可能與你的邏輯相差甚遠。比如我們前文提及的使用volatile以及不使用volatile關(guān)鍵字編譯后生成的匯編代碼,再比如我們用低優(yōu)化級別編譯和使用高優(yōu)化級別編譯后生成的匯編代碼,都可能相差很大,實際運行測試,可以暴漏這些隱含錯誤。最后,雖然可能性極小,編譯器本身也可能有BUG,特別是構(gòu)造復(fù)雜表達式的情況下(應(yīng)極力避免復(fù)雜表達式)。
5.1使用硬件調(diào)試器測試
使用硬件調(diào)試器(比如J-link)測試是最通用的手段??梢詥尾竭\行、設(shè)置斷點,可以很方便的查看當前寄存器、變量的值。在尋找缺陷方面,使用硬件調(diào)試器測試是最簡單卻又最有效的手段。
硬件調(diào)試器已經(jīng)在公司普遍使用,這方面的測試不做介紹,想必大家都已經(jīng)很熟悉了。
5.2有些缺陷很難纏
就像沒有一種方法能完美解決所有問題,在實際項目中,硬件調(diào)試器也有難以觸及的地方??梢耘e幾個例子說明:
使用了比較大的協(xié)議棧,需要跟進到協(xié)議棧內(nèi)部調(diào)試的缺陷
使用了比較大的協(xié)議棧,需要跟進到協(xié)議棧內(nèi)部調(diào)試的缺陷
比如公司使用lwIP協(xié)議棧,如果跟蹤數(shù)據(jù)的處理過程,需要從接收數(shù)據(jù)開始一直到應(yīng)用層處理數(shù)據(jù),之間會經(jīng)過驅(qū)動層、IP層、TCP層和應(yīng)用層,會經(jīng)過十幾個文件幾十個函數(shù),使用硬件調(diào)試器跟蹤費時費力;
具有隨機性的缺陷
具有隨機性的缺陷
有一些缺陷,可能是不定時出現(xiàn)的,有可能是幾分鐘出現(xiàn),也有可能是幾個小時甚至幾天才出現(xiàn),像這樣的缺陷很難用硬件調(diào)試器捕捉到;
需要外界一系列有時間限制的輸入條件觸發(fā),但這一過程中有缺陷
比如我們用組合鍵來完成某個功能,規(guī)定按下按鍵1不小于3秒后松開,然后在6秒內(nèi)分別按下按鍵2、按鍵3、按鍵4這三個按鍵來執(zhí)行我們的特定程序,要測試類似這種過程,硬件調(diào)試器很難做到;
除了測試缺陷需要,有時候我們在做穩(wěn)定性測試時,需要知道軟件每時每刻運行到那些分支、執(zhí)行了哪些操作、我們關(guān)心的變量當前值是什么等等,這些都表明,我們還需要一種和硬件調(diào)試器互補的測試手段。
這個測試手段就是在程序中增加額外調(diào)試語句,當程序運行時,通過這些調(diào)試語句將運行信息輸出到可以方便查看的設(shè)備上,可以是PC機、LCD顯示屏、存儲卡等等。
以串口輸出到PC機為例,下面提供完整的測試思路。在此之前,我們先對這種測試手段提一些要求:
必須簡單易用
需要外界一系列有時間限制的輸入條件觸發(fā),但這一過程中有缺陷
比如我們用組合鍵來完成某個功能,規(guī)定按下按鍵1不小于3秒后松開,然后在6秒內(nèi)分別按下按鍵2、按鍵3、按鍵4這三個按鍵來執(zhí)行我們的特定程序,要測試類似這種過程,硬件調(diào)試器很難做到;
除了測試缺陷需要,有時候我們在做穩(wěn)定性測試時,需要知道軟件每時每刻運行到那些分支、執(zhí)行了哪些操作、我們關(guān)心的變量當前值是什么等等,這些都表明,我們還需要一種和硬件調(diào)試器互補的測試手段。
這個測試手段就是在程序中增加額外調(diào)試語句,當程序運行時,通過這些調(diào)試語句將運行信息輸出到可以方便查看的設(shè)備上,可以是PC機、LCD顯示屏、存儲卡等等。
以串口輸出到PC機為例,下面提供完整的測試思路。在此之前,我們先對這種測試手段提一些要求:
必須簡單易用
我們在初學(xué)C語言的時候,都接觸過printf函數(shù),這個函數(shù)可以方便的輸出信息,并可以將各種變量格式化為指定格式的字符串,我們應(yīng)當提供類似的函數(shù);
調(diào)試語句必須方便的從代碼中移除
調(diào)試語句必須方便的從代碼中移除
在編碼階段,我們可能會往程序中加入大量的調(diào)試語句,但是程序發(fā)布時,需要將這些調(diào)試語句從代碼中移除,這將是件恐怖的過程。我們必須提供一種策略,可以方便的移除這些調(diào)試語句。
5.2.1簡單易用的調(diào)試函數(shù)
使用庫函數(shù)printf。以MDK為例,方法如下:
I初始化串口
II重構(gòu)fputc函數(shù),printf函數(shù)會調(diào)用fputc函數(shù)執(zhí)行底層串口的數(shù)據(jù)發(fā)送。
1./**
2. * @brief 將C庫中的printf函數(shù)重定向到指定的串口.
3. * @param ch:要發(fā)送的字符
4. * @param f :文件指針
5. */
6.intfputc( intch, FILE *f)
7. {
8.
9./*這里是一個跟硬件相關(guān)函數(shù),將一個字符寫到UART */
10.//舉例:USART_SendData(UART_COM1, (uint8_t) ch);
11.
12.returnch;
13.}
III 在Options for Targer窗口,Targer標簽欄下,勾選Use MicroLIB前的復(fù)選框以便避免使用半主機功能。(注:標準C庫printf函數(shù)默認開啟半主機功能,如果非要使用標準C庫,請自行查閱資料)
構(gòu)建自己的調(diào)試函數(shù)
使用庫函數(shù)比較方便,但也少了一些靈活性,不利于隨心所欲的定制輸出格式。自己編寫類似printf函數(shù)則會更靈活一些,而且不依賴任何編譯器。下面給出一個完整的類printf函數(shù)實現(xiàn),該函數(shù)支持有限的格式參數(shù),使用方法與庫函數(shù)一致。同庫函數(shù)類似,該也需要提供一個底層串口發(fā)送函數(shù)(原型為:int32_t UARTwrite(const uint8_t *pcBuf, uint32_t ulLen)),用來發(fā)送指定數(shù)目的字符,并返回最終發(fā)送的字符個數(shù)。
1.# includestdarg.h /*支持函數(shù)接收不定量參數(shù)*/
2.
3.constchar* constg_pcHex = "0123456789abcdef";
4.
5./**
6. * 簡介: 一個簡單的printf函數(shù),支持\%c, \%d, \%p, \%s, \%u,\%x, and \%X.
7. */
8.voidUARTprintf( constuint8_t*pcString, ...)
9. {
10.uint32_tulIdx;
11.uint32_tulValue; //保存從不定量參數(shù)堆棧中取出的數(shù)值型變量
12.uint32_tulPos, ulCount;
13.uint32_tulBase; //保存進制基數(shù),如十進制則為10,十六進制數(shù)則為16
14.uint32_tulNeg; //為1表示從變量為負數(shù)
15.uint8_t*pcStr; //保存從不定量參數(shù)堆棧中取出的字符型變量
16.uint8_tpcBuf[ 32]; //保存數(shù)值型變量字符化后的字符
17.uint8_tcFill; //'%08x'-不足8個字符用'0'填充,cFill='0';
18.//'%8x '-不足8個字符用空格填充,cFill=' '
19.va_list vaArgP;
20.
21.va_start(vaArgP, pcString);
22.while(*pcString)
23.{
24.// 首先搜尋非%核字符串結(jié)束字符
25.for(ulIdx = 0; (pcString[ulIdx] != '%') (pcString[ulIdx] != '\0'); ulIdx++)
26.{ }
27.UARTwrite(pcString, ulIdx);
28.
29.pcString += ulIdx;
30.if(*pcString == '%')
31.{
32.pcString++;
33.
34.ulCount = 0;
35.cFill = ' ';
36.again:
37.switch(*pcString++)
38.{
39.case'0': case'1': case'2': case'3': case'4':
40.case'5': case'6': case'7': case'8': case'9':
41.{
42.// 如果第一個數(shù)字為0, 則使用0做填充,則用空格填充)
43.if((pcString[ -1] == '0') (ulCount == 0))
44.{
45.cFill = '0';
46.}
47.ulCount *= 10;
48.ulCount += pcString[ -1] - '0';
49.gotoagain;
50.}
51.case'c':
52.{
53.ulValue = va_arg(vaArgP, unsignedlong);
54.UARTwrite(( unsignedchar*)ulValue, 1);
55.break;
56.}
57.case'd':
58.{
59.ulValue = va_arg(vaArgP, unsignedlong);
60.ulPos = 0;
61.
62.if(( long)ulValue 0)
63.{
64.ulValue = -( long)ulValue;
65.ulNeg = 1;
66.}
67.else
68.{
69.ulNeg = 0;
70.}
71.ulBase = 10;
72.gotoconvert;
73.}
74.case's':
75.{
76.pcStr = va_arg(vaArgP, unsignedchar*);
77.
78.for(ulIdx = 0; pcStr[ulIdx] != '\0'; ulIdx++)
79.{
80.}
81.UARTwrite(pcStr, ulIdx);
82.
83.if(ulCount ulIdx)
84.{
85.ulCount -= ulIdx;
86.while(ulCount--)
87.{
88.UARTwrite( " ", 1);
89.}
90.}
91.break;
92.}
93.case'u':
94.{
95.ulValue = va_arg(vaArgP, unsignedlong);
96.ulPos = 0;
97.ulBase = 10;
98.ulNeg = 0;
99.gotoconvert;
100.}
101.case'x': case'X': case'p':
102.{
103.ulValue = va_arg(vaArgP, unsignedlong);
104.ulPos = 0;
105.ulBase = 16;
106.ulNeg = 0;
107.convert: //將數(shù)值轉(zhuǎn)換成字符
108.for(ulIdx = 1; (((ulIdx * ulBase) = ulValue) (((ulIdx * ulBase) / ulBase) == ulIdx)); ulIdx *= ulBase, ulCount--)
109.{ }
110.if(ulNeg)
111.{
112.ulCount--;
113.}
114.if(ulNeg (cFill == '0'))
115.{
116.pcBuf[ulPos++] = '-';
117.ulNeg = 0;
118.}
119.if((ulCount 1) (ulCount 16))
120.{
121.for(ulCount--; ulCount; ulCount--)
122.{
123.pcBuf[ulPos++] = cFill;
124.}
125.}
126.
127.if(ulNeg)
128.{
129.pcBuf[ulPos++] = '-';
130.}
131.
132.for(; ulIdx; ulIdx /= ulBase)
133.{
134.pcBuf[ulPos++] = g_pcHex[(ulValue / ulIdx) % ulBase];
135.}
136.UARTwrite(pcBuf, ulPos);
137.break;
138.}
139.case'%':
140.{
141.UARTwrite(pcString - 1, 1);
142.break;
143.}
144.default:
145.{
146.UARTwrite( "ERROR", 5);
147.break;
148.}
149.}
150.}
151.}
152.//可變參數(shù)處理結(jié)束
153.va_end(vaArgP);
154.}
5.2.2對調(diào)試函數(shù)進一步封裝
上文說到,我們增加的調(diào)試語句應(yīng)能很方便的從最終發(fā)行版中去掉,因此我們不能直接調(diào)用printf或者自定義的UARTprintf函數(shù),需要將這些調(diào)試函數(shù)做一層封裝,以便隨時從代碼中去除這些調(diào)試語句。參考方法如下:
1.# ifdefMY_DEBUG
2.# defineMY_DEBUGF(message) do { \
3.{UARTprintf message;} \
4.} while( 0)
5.# else
6.# defineMY_DEBUGF(message)
7.# endif/* PLC_DEBUG */
在我們編碼測試期間,定義宏MY_DEBUG,并使用宏MY_DEBUGF(注意比前面那個宏多了一個‘F’)輸出調(diào)試信息。經(jīng)過預(yù)處理后,宏MY_DEBUGF(message)會被UARTprintf message代替,從而實現(xiàn)了調(diào)試信息的輸出;當正式發(fā)布時,只需要將宏MY_DEBUG注釋掉,經(jīng)過預(yù)處理后,所有MY_DEBUGF(message)語句都會被空格代替,而從將調(diào)試信息從代碼中去除掉。
6.編程思想 6.1編程風(fēng)格
《計算機程序的構(gòu)造和解釋》一書在開篇寫到:程序?qū)懗鰜硎墙o人看的,附帶能在機器上運行。
6.1.1 整潔的樣式
使用什么樣的編碼樣式一直都頗具爭議性的,比如縮進和大括號的位置。因為編碼的樣式也會影響程序的可讀性,面對一個亂放括號、對齊都不一致的源碼,我們很難提起閱讀它的興趣。我們總要看別人的程序,如果彼此編碼樣式相近,讀起源碼來會覺得比較舒適。但是編碼風(fēng)格的問題是主觀的,永遠不可能在編碼風(fēng)格上達成統(tǒng)一意見。因此只要你的編碼樣式整潔、結(jié)構(gòu)清晰就足夠了。除此之外,對編碼樣式再沒有其它要求。
提出匈牙利命名法的程序員、前微軟首席架構(gòu)師Charles Simonyi說:我覺得代碼清單帶給人的愉快同整潔的家差不多。你一眼就能分辨出家里是雜亂無章還是整潔如新。這也許意義不大。因為光是房子整潔說明不了什么,它仍可能藏污納垢!但是第一印象很重要,它至少反映了程序的某些方面。我敢打賭,我在3米開外就能看出程序拙劣與否。我也許沒法保證它很不錯,但如果從3米外看起來就很糟,我敢保證這程序?qū)懙貌挥眯?。如果寫得不用心,那它在邏輯上也許就不會優(yōu)美。
6.1.2清晰的命名
變量、函數(shù)、宏等等都需要命名,清晰的命名是優(yōu)秀代碼的特點之一。命名的要點之一是名稱應(yīng)能清晰的描述這個對象,以至于一個初級程序員也能不費力的讀懂你的代碼邏輯。我們寫的代碼主要給誰看是需要思考的:給自己、給編譯器還是給別人看?我覺得代碼最主要的是給別人看,其次是給自己看。如果沒有一個清晰的命名,別人在維護你的程序時很難在整個全貌上看清代碼,因為要記住十多個以上的糟糕命名的變量是件非常困難的事;而且一段時間之后你回過頭來看自己的代碼,很有可能不記得那些糟糕命名的變量是什么意思。
為對象起一個清晰的名字并不是簡單的事情。首先能認識到名稱的重要性需要有一個過程,這也許跟譚式C程序教材被大學(xué)廣泛使用有關(guān):滿書的a、b、c、x、y、z變量名是很難在關(guān)鍵的初學(xué)階段給人傳達優(yōu)秀編程思想的;其次如何恰當?shù)臑閷ο竺埠苡刑魬?zhàn)性,要準確、無歧義、不羅嗦,要對英文有一定水平,所有這些都要滿足時,就會變得很困難;此外,命名還需要考慮整體一致性,在同一個項目中要有統(tǒng)一的風(fēng)格,堅持這種風(fēng)格也并不容易。
關(guān)于如何命名,Charles Simonyi說:面對一個具備某些屬性的結(jié)構(gòu),不要隨隨便便地取個名字,然后讓所有人去琢磨名字和屬性之間有什么關(guān)聯(lián),你應(yīng)該把屬性本身,用作結(jié)構(gòu)的名字。
6.1.3恰當?shù)淖⑨?
注釋向來也是爭議之一,不加注釋和過多的注釋我都是反對的。不加注釋的代碼顯然是很糟糕的,但過多的注釋也會妨礙程序的可讀性,由于注釋可能存在的歧義,有可能會誤解程序真實意圖,此外,過多的注釋會增加程序員不必要的時間。如果你的編碼樣式整潔、命名又很清晰,那么,你的代碼可讀性不會差到哪去,而注釋的本意就是為了便于理解程序。
這里建議使用良好的編碼樣式和清晰的命名來減少注釋,對模塊、函數(shù)、變量、數(shù)據(jù)結(jié)構(gòu)、算法和關(guān)鍵代碼做注釋,應(yīng)重視注釋的質(zhì)量而不是數(shù)量。如果你需要一大段注釋才能說清楚程序做什么,那么你應(yīng)該注意了:是否是因為程序變量命名不夠清晰,或者代碼邏輯過于混亂,這個時候你應(yīng)該考慮的可能就不是注釋,而是如何精簡這個程序了。
6.2數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)是程序設(shè)計的基礎(chǔ)。在設(shè)計程序之前,應(yīng)該先考慮好所需要的數(shù)據(jù)結(jié)構(gòu)。
前微軟首席架構(gòu)師Charles Simonyi:編程的第一步是想象。就是要在腦海中對來龍去脈有極為清晰的把握。在這個初始階段,我會使用紙和鉛筆。我只是信手涂鴉,并不寫代碼。我也許會畫些方框或箭頭,但基本上只是涂鴉,因為真正的想法在我腦海里。我喜歡想象那些有待維護的結(jié)構(gòu),那些結(jié)構(gòu)代表著我想編碼的真實世界。一旦這個結(jié)構(gòu)考慮得相當嚴謹和明確,我便開始寫代碼。我會坐到終端前,或者換在以前的話,就會拿張白紙,開始寫代碼。這相當容易。我只要把頭腦中的想法變換成代碼寫下來,我知道結(jié)果應(yīng)該是什么樣的。大部分代碼會水到渠成,不過我維護的那些數(shù)據(jù)結(jié)構(gòu)才是關(guān)鍵。我會先想好數(shù)據(jù)結(jié)構(gòu),并在整個編碼過程中將它們牢記于心。
開發(fā)過以太網(wǎng)和操作系統(tǒng)SDS 940的Butler Lampson:(程序員)最重要的素質(zhì)是能夠把問題的解決方案組織成容易操控的結(jié)構(gòu)。
開發(fā)CP/M操作系統(tǒng)的Gary.A:如果不能確認數(shù)據(jù)結(jié)構(gòu)是正確的,我是決不會開始編碼的。我會先畫數(shù)據(jù)結(jié)構(gòu),然后花很長時間思考數(shù)據(jù)結(jié)構(gòu)。在確定數(shù)據(jù)結(jié)構(gòu)之后我就開始寫一些小段的代碼,并不斷地改善和監(jiān)測。在編碼過程中進行測試可以確保所做的修改是局部的,并且如果有什么問題的話,能夠馬上發(fā)現(xiàn)。
微軟創(chuàng)始人比爾**·**蓋茨:編寫程序最重要的部分是設(shè)計數(shù)據(jù)結(jié)構(gòu)。接下來重要的部分是分解各種代碼塊。
編寫世界上第一個電子表格軟件的Dan Bricklin:在我看來,寫程序最重要的部分是設(shè)計數(shù)據(jù)結(jié)構(gòu),此外,你還必須知道人機界面會是什么樣的。
我們舉個例子來說明。在介紹防御性編程的時候,提到公司使用的LCD顯示屏抗干擾能力一般,為了提高LCD的穩(wěn)定性,需要定期讀出LCD內(nèi)部的關(guān)鍵寄存器值,然后跟存在Flash中的初始值相比較。需要讀出的LCD寄存器有十多個,從每個寄存器讀出的值也不盡相同,從1個到8個字節(jié)都有可能。如果不考慮數(shù)據(jù)結(jié)構(gòu),編寫出的程序?qū)苋唛L。
1.voidlcd_redu( void)
2. {
3.讀第一個寄存器值;
4.if(第一個寄存器值==Flash存儲值)
5.{
6.讀第二個寄存器值;
7.if(第二個寄存器值==Flash存儲值)
8.{
9....
10.
11.讀第十個寄存器值;
12.if(第十個寄存器值==Flash存儲值)
13.{
14.返回;
15.}
16.else
17.{
18.重新初始化LCD;
19.}
20.}
21.else
22.{
23.重新初始化LCD;
24.}
25.}
26.else
27.{
28.重新初始化LCD;
29.}
30.}
我們分析這個過程,發(fā)現(xiàn)能提取出很多相同的元素,比如每次讀LCD寄存器都需要該寄存器的命令號,都會經(jīng)過讀寄存器、判斷值是否相同、處理異常情況這一過程。所以我們可以提取一些相同的元素,組織成數(shù)據(jù)結(jié)構(gòu),用統(tǒng)一的方法去處理這些數(shù)據(jù),將數(shù)據(jù)與處理過程分開來。
我們可以先提取相同的元素,將之組織成數(shù)據(jù)結(jié)構(gòu):
1.typedefstruct{
2.uint8_tlcd_command; //LCD寄存器
3.uint8_tlcd_get_value[ 8]; //初始化時寫入寄存器的值
4.uint8_tlcd_value_num; //初始化時寫入寄存器值的數(shù)目
5.}lcd_redu_list_struct;
這里lcd_command表示的是LCD寄存器命令號;lcd_get_value是一個數(shù)組,表示寄存器要初始化的值,這是因為對于一個LCD寄存器,可能要初始化多個字節(jié),這是硬件特性決定的;lcd_value_num是指一個寄存器要多少個字節(jié)的初值,這是因為每一個寄存器的初值數(shù)目是不同的,我們用同一個方法處理數(shù)據(jù)時,是需要這個信息的。
就本例而言,我們將要處理的數(shù)據(jù)都是事先固定的,所以定義好數(shù)據(jù)結(jié)構(gòu)后,我們可以將這些數(shù)據(jù)組織成表格:
1./*LCD部分寄存器設(shè)置值列表*/
2.lcd_redu_list_struct constlcd_redu_list_str[]=
3.{
4.{SSD1963_Get_Address_Mode,{ 0x20} , 1}, /*1*/
5.{SSD1963_Get_Pll_Mn ,{ 0x3b, 0x02, 0x04} , 3}, /*2*/
6.{SSD1963_Get_Pll_Status ,{ 0x04} , 1}, /*3*
7. {SSD1963_Get_Lcd_Mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /*4*/
8.{SSD1963_Get_Hori_Period ,{ 0x02, 0x0c, 0x00, 0x2a, 0x07, 0x00, 0x00, 0x00}, 8}, /*5*/
9.{SSD1963_Get_Vert_Period ,{ 0x01, 0x1d, 0x00, 0x0b, 0x09, 0x00, 0x00} , 7}, /*6*/
10.{SSD1963_Get_Power_Mode ,{ 0x1c} , 1}, /*7*/
11.{SSD1963_Get_Display_Mode,{ 0x03} , 1}, /*8*/
12.{SSD1963_Get_Gpio_Conf ,{ 0x0F, 0x01} , 2}, /*9*/
13.{SSD1963_Get_Lshift_Freq ,{ 0x00, 0xb8} , 2}, /*10*
14. };
至此,我們就可以用一個處理過程來完成數(shù)十個LCD寄存器的讀取、判斷和異常處理了:
1./**
2. * lcd 顯示冗余
3. * 每隔一段時間調(diào)用該程序一次
4. */
5.voidlcd_redu( void)
6. {
7.uint8_ttmp[ 8];
8.uint32_ti,j;
9.uint32_tlcd_init_flag;
10.
11.lcd_init_flag = 0;
12.for(i= 0;i sizeof(lcd_redu_list_str)/ sizeof(lcd_redu_list_str[ 0]);i++)
13.{
14.LCD_SendCommand(lcd_redu_list_str[i].lcd_command);
15.uyDelay( 10);
16.for(j= 0;jlcd_redu_list_str[i].lcd_value_num;j++)
17.{
18.tmp[j]=LCD_ReadData;
19.if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])
20.{
21.lcd_init_flag= 0x55;
22.//一些調(diào)試語句,打印出錯的具體信息
23.gotohandle_lcd_init;
24.}
25.}
26.}
27.
28.handle_lcd_init:
29.if(lcd_init_flag== 0x55)
30.{
31.//重新初始化LCD
32.//一些必要的恢復(fù)措施
33.}
34.}
通過合理的數(shù)據(jù)結(jié)構(gòu),我們可以將數(shù)據(jù)和處理過程分開,LCD冗余判斷過程可以用很簡潔的代碼來實現(xiàn)。更重要的是,將數(shù)據(jù)和處理過程分開更有利于代碼的維護。比如,通過實驗發(fā)現(xiàn),我們還需要增加一個LCD寄存器的值進行判斷,這時候只需要將新增加的寄存器信息按照數(shù)據(jù)結(jié)構(gòu)格式,放到LCD寄存器設(shè)置值列表中的任意位置即可,不用增加任何處理代碼即可實現(xiàn)!這僅僅是數(shù)據(jù)結(jié)構(gòu)的優(yōu)勢之一,使用數(shù)據(jù)結(jié)構(gòu)還能簡化編程,使復(fù)雜過程變的簡單,這個只有實際編程后才會有更深的理解。
7.總結(jié)和閱讀書目
本文介紹了編寫優(yōu)質(zhì)嵌入式C程序涉及的多個方面。每年都有億萬計的C程序運行在單片機、ARM7、Cortex-M3這些微處理器上,但在這些處理器上如何編寫優(yōu)質(zhì)高效的C程序,幾乎沒有書籍做專門介紹。本文試圖在這方面做一些努力。編寫優(yōu)質(zhì)嵌入式C程序需要大量的專業(yè)知識,本文雖盡力描述編寫嵌入式C程序所需要的各種技能,但本文卻無力將每一個方面都面面俱到的描述出來,所以本文最后會列舉一些閱讀書目,這些書大多都是真正大師的經(jīng)驗之談。站在巨人的肩膀上,可以看的更遠。
7.1關(guān)于語言特性
Stephen Prata 著 云巔工作室 譯 《C Primer Plus(第五版)中文版》
Andrew Koenig 著 高巍 譯 《C陷阱與缺陷》
Peter Van Der Linden 著 徐波 譯 《C專家編程》
陳正沖 編著 《C語言深度解剖》
Stephen Prata 著 云巔工作室 譯 《C Primer Plus(第五版)中文版》
Andrew Koenig 著 高巍 譯 《C陷阱與缺陷》
Peter Van Der Linden 著 徐波 譯 《C專家編程》
陳正沖 編著 《C語言深度解剖》
杜春雷 編著 《ARM體系結(jié)構(gòu)與編程》
Keil MDK 編譯器幫助手冊
杜春雷 編著 《ARM體系結(jié)構(gòu)與編程》
Keil MDK 編譯器幫助手冊
MISRA-C-:2004 Guidelines for the use of the C language in criticalsystems
Robert C.Seacord 著 徐波 譯 《C安全編碼標準》
MISRA-C-:2004 Guidelines for the use of the C language in criticalsystems
Robert C.Seacord 著 徐波 譯 《C安全編碼標準》
Pete Goodliffe 著 韓江、陳玉 譯 《編程匠藝---編寫卓越的代碼》
Susan Lammers 著 李琳驍、吳詠煒、張菁《編程大師訪談錄》
Pete Goodliffe 著 韓江、陳玉 譯 《編程匠藝---編寫卓越的代碼》
Susan Lammers 著 李琳驍、吳詠煒、張菁《編程大師訪談錄》
來源:嵌入式ARM
1.嵌入式行業(yè)那些坑與出路
2.一文搞懂 | Linux 驅(qū)動的來龍去脈
3.英偉達并購Arm要失??!老黃還要給孫正義12.5億美元分手費
4.Intel加入RISC-V國際基金會~
5.這才是像樣的C語言編程規(guī)范~~
6.為什么招聘單片機工程師的時候要求精通C/C++?
掃描二維碼推送至手機訪問。
版權(quán)聲明:本文由飛速云SEO網(wǎng)絡(luò)優(yōu)化推廣發(fā)布,如需轉(zhuǎn)載請注明出處。