2016年嵌入式開發C語言問題詳解

才智咖 人氣:2.44W

嵌入式系統的C語言開發中,經常遇到這樣那樣的問題。有些問題可能很快就能找到原因,但是有些問題必須有一定的經驗積累才能快速找到原因。

本站小編整理了本人所瞭解的和經常遇到的嵌入式開發中的C語言典型問題,不足之處歡迎各位專家指摘賜教。

一、由編譯優化引起的問題

例1、編譯後的組合語言處理邏輯跟C語言處理邏輯不一致

由於編譯器的原因,在設定了編譯優化的情況下,編譯後有些程式碼的邏輯會發生變化。這種情況下會發生很奇怪的問題,一些函式的處理結果跟預想的不一致,而檢查程式碼又看不出什麼問題。

這種問題的解決辦法一般是在充分分析軟體處理邏輯,確認處理上沒問題後,把編譯後的列表檔案(*)和C語言處理邏輯逐行對照。把不一致的地方找出來,並尋找修正對策。

例2、編譯後的一些處理被優化了

這種問題經常發生在硬體暫存器的操作上。對於硬體而言,每一次讀寫操作可能都有特定的含義:某些硬體暫存器要求讀一下才能做後續其他處理;而某些暫存器要連續寫幾次。比如下面的情形:

#define TSTREG (unsigned char *) 0x00C00032 /*TEST REGISTER */

unsigned char *pTSTR;

pTSTR = TSTREG;

*pTSTR = 0x01; //這個操作很容易被編譯器優化掉。

*pTSTR = 0x02;

……

作為對策之一,可以在定義變數時加上volatile關鍵字。比如:

volatile unsigned char *pTSTR;

volatile關鍵詞影響編譯器編譯的結果,用volatile宣告的變量表示該變數隨時可能發生變化,與該變數有關的運算,不要進行編譯優化,以免出錯。使用volatile變數的幾個場景:

1)中斷服務程式中修改的供其它程式檢測的變數需要加volatile。

2)多工環境下各任務間共享的標誌應該加volatile。

3)儲存器對映的硬體暫存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義。

二、由位元組對齊引起的問題

一個結構體變數定義完之後,其在記憶體中的儲存並不一定等於其所包含元素的寬度之和。因為這裡涉及到位元組對齊的問題。

結構體中元素的對齊基本上遵循兩個基本原則:

原則一:結構體中元素是按照定義順序一個一個放到記憶體中去的,但並不是緊密排列的。從結構體儲存的首地址開始,每一個元素放置到記憶體中時,它都會認為記憶體是以它自己的大小來劃分的,因此元素放置的位置一定會在自己寬度的整數倍上開始(以結構體變數首地址為0計算)。

原則二:在經過第一原則分析後,檢查計算出的儲存單元是否為所有元素中最寬的元素的長度的整數倍。若是,則結束;若不是,則補齊為它的整數倍。

每個特定平臺上的編譯器都有自己的預設“對齊係數”(也叫對齊模數或邊界調整數)。通常,可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。

#pragma pack(n) //編譯器將按照n位元組對齊

#pragma pack() //編譯器將取消自定義位元組對齊方式

在#pragma pack(n)和#pragma pack()之間的程式碼按n位元組對齊。但是成員對齊有一個重要的條件,即每個成員按照自己的對齊方式對齊; 也就是說雖然指定了按n位元組對齊,但並不是所有的成員都以n位元組對齊。對齊的規則是:每個成員按其型別的對齊引數(通常是這個型別的大小)和指定對齊引數(這裡是n位元組)中較小的一個對齊,即min(n,sizeof(item)),並且結構的長度必須為所用過的所有對齊引數的整數倍,不夠就補空位元組。

以瑞薩SH7145F CPU和XASS-V編譯器為例,根據XASS-V幫助檔案,對於SH7145F,其結構體成員的預設對齊係數如下:

char:1

short:2

int:4

long:4

float:4

long long:4

double:4

指標:4

結構體/聯合體:4

陣列:根據型別而定

在編譯的`時候可以通過設定 /bond = n, n=4(一般情況),來指定邊界調整係數。因此,實際採用/bond=n中的n和上述預設對齊係數中的較小者。

由於跟特定編譯器有關,所以下面的例子僅限XASS-V編譯器,目標CPU是瑞薩SH7145F。

例1、假設 /bond=4,進行如下定義:

typedef unsigned char UCHAR;

/* 結構體定義*/

typedef struct{

union{

UCHAR SO_bit7;

UCHAR DMY0_bit56;

UCHAR LNO_bit04;

} DL; /* 預想BYTE0 */

UCHAR ORDN;

union{

UCHAR RW_bit7;

UCHAR DTNO_bit06;

}RD; /* 預想BYTE1 */

UCHAR ADRH; /* 預想BYTE2 */

UCHAR ADRL; /* 預想BYTE3 */

UCHAR DATA; /* 預想BYTE4 */

}stTEST;

/* 變數定義*/

UCHAR TEST[6];

stTEST *pTEM=( stTEST *) TEST;

這樣執行後,pTEM->ADRH並不是對應TEST [3],導致了資料處理錯誤。

原因分析:

由邊界調整數決定,union只能在4的倍數的地址上存放;且UNION型別要佔用4*X Byte,故後面有3Byte的Dummy。即UNION{xxx}DL佔用了4Byte。

依次類推,整個結構體元素的記憶體分佈如下:

UNION{xxx}DL; /* Byte0~Byte3: 後3Byte Dummy*/

UCHAR ORDN; /* Byte4~Byte7: 後3Byte Dummy*/

UNION{xxx}RD; /* Byte8~Byte11: 後3Byte Dummy*/

UCHAR ADRH; /* Byte12*/

UCHAR ADRL; /* Byte13*/

UCHAR DATA; /* Byte14~Byte15: 後1Byte Dummy*/

Byte5~Byte7的Dummy是因為UNION成員必須在4的倍數的地址上存放。

Byte15的Dummy是整個結構體大小必須是4的倍數。

所以sizeof(stTEST)=16, pTEM->ADRH對應為Byte12,不是預想的TEST[3]。

三、由變數型別不匹配引起的問題

例1、迴圈變數溢位

UCHAR i = 0x00;

/* 版本1:100個迴圈 */

for(i=0;i<100;i++) { /* 處理:略*/}

/* 版本2:1000個迴圈 */

for(i=0;i<1000;i++) { /* 處理:略*/}

一開始需求是100個迴圈,而後面需求變更為1000個迴圈,但忘記修改迴圈變數型別。以為UCHAR的有效範圍是0~255,顯然不滿足版本2的要求。這種情況會發生在迴圈變數定義的位置距離迴圈體比較遠的時候,在無意識中忽略了。

四、由陣列下標越界引起的問題

常見的是指定的陣列下標超過了陣列最大有效下標。很多情況下不會導致程式奔潰,但是取出的資料顯然是不正確的。

五、由位元組序引起的問題

位元組序主要體現大於1Byte的資料的儲存方式上。對於CPU而言,有MSB FIRST和LSB FIRST兩種儲存方式。MSB指Most Significant Bit,即最高有效位;LSB指Lest Significant Bit,即最低有效位。簡單地說,MSB FIRST就是高位優先儲存,即高位儲存在低地址上,低位儲存在高地址上,簡稱“高低低高”。LSB FIRST則相反,即低位優先儲存,高位儲存在高地址上,低位儲存在低地址上,簡稱“高高低低”。大部分嵌入式系統的CPU是MSB FIRST的,少部分是LSB FIRST的。常見的LSB FIRST的CPU是INTEL的。

類似的,在網路通訊方面有兩種位元組序:“Big-Endian”和“Small-Endian”。 指的都是對於多位元組的資料型別(比如4位元組的32位整數),其多個位元組的順序問題,是最高位元組在前(Big-Endian)還是最低位元組在前(Small-Endian)。 比如對於123456789這個整數,其16進製為0x075BCD15,那麼按照Big-Endian的方式,它在網路上傳輸(或者在記憶體裡儲存)的4個位元組依次是:07 5B CD 15,而Small-Endian的順序正相反,是:15 CD 5B 07。處於通訊的雙方必須按相同的位元組序進行收發資料處理,才能得出正常的結果。

例1、應用程式A以075BCD15的位元組序(Big-Endian)傳送資料123456789給應用程式B,但應用程式B卻以15CD5B07的位元組序(Small-Endian)處理,則雙方沒法正常通訊。

六、由UNION元素賦值引起的問題

一個UNION元素的值由最後那次設定決定的。有些時候,無意中對一個UNION元素連續賦值,就會發生意料之外的問題。

例1、以前面的結構體型別stTEST 為例,做如下設定。

stTEST tTst;

_bit7=0x80; /* SO使用bit7 */

. LNO_bit04=0x01; /* LNO使用bit0~bit4 */

這樣設定後,最終SO=0,而不是預先希望的SO=1。

七、由運算子優先順序引起的問題

運算子優先順序處理不好也會引入一些潛在的問題。

例1、邏輯運算子與條件運算子

int a=0x02;

if((a&0x03)!=0x00) /*表示式1*/

if(a&0x03!=0x00) /*表示式2*/

表示式1:先執行0x02&0x03=>0x02,再執行0x02 !=0x00,故結果為TRUE。

表示式2:先執行0x03 !=0x00=>TRUE(結果是0x01),再執行0x02&0x01,結果為0x00即FALSE。

顯然,表示式1才是正確的寫法。如果把用表示式2的形式就會引入一些潛在的問題。

例2、指標取值運算,邏輯運算子與條件運算子

if((*pSTSRG&0x01)==0x00) /*表示式1*/

if(((*pSTSRG)&0x01)==0x00) /*表示式2*/

if(*pSTSRG&0x01==0x00) /*表示式3*/

顯然,表示式1和表示式2是正確寫法,而且最保險的寫法是表示式2。而表示式3是錯誤寫法。

從上面的例子可以看出,由於運算子優先順序不太方便記憶,也沒必要去記憶,最規避這類問題的最好辦法就是給表示式強制加上括號。

八、由中斷優先順序引起的問題

在多執行緒應用程式開發中,經常會用訊號量等方式來保證共享資源的訪問。但是在有多箇中斷的應用程式中,經常會無意識地略掉中斷優先順序,導致引入一些潛在的問題。

例1、中斷IRQ1每1ms發生1次,中斷IRQ4每4ms發生1次,且IRQ4的優先順序高於IRQ1。在IRQ1和IRQ4的處理過程都會設定全域性變數tData。該如何安排IRQ1和IRQ4的處理邏輯?

因為低優先順序的中斷IRQ1未處理完成時,如果發生高優先順序的中斷IRQ4,則IRQ1的處理會把相關上下文壓棧,暫時掛起,等IRQ4處理完成後才繼續處理IRQ1。由於IRQ4也會修改全域性變數tData,如果沒有任何保護措施,則IRQ1的處理可能會不完全正確。作為對策之一就是在IRQ1的處理中加入中斷遮蔽。

/* IRQ1的中斷處理函式 */

IRQ1_Handler()

{

xxx; /* 遮蔽所有中斷 */

yyy; /* 相關處理 */

zzz; /* 解除中斷遮蔽 */

}

當然,上述處理的前提是IRQ4是可遮蔽中斷。

九、由組合語言轉C語言引起的問題

由於CPU的更換,原先用匯編語言開發的系統轉換為用C語言開發的情形也是存在的。這種情況也經常會引入一些問題。

例1、CPU位元組序不同而引起的問題

關於位元組序參考前面的內容。當用到一個大於1Byte的變數的時候,必須瞭解新舊CPU的位元組序,正確操作大於1Byte的變數,才能保證不會因為高低位倒置而引入問題。

例2、CPU頻率不同而引起的問題

在組合語言開發的系統中,經常會用一些迴圈來實現微秒級的延時。特別在串列埠通訊中,硬體暫存器對時間非常敏感,如果在轉換成C語言時沒有考慮到這點,沒有及時調整迴圈次數,就會因為CPU頻率變高而導致延時不足。