資料指標
在嵌入式系統的編程中,常常要求在特定的記憶體單元讀寫內容,彙編有對應的MOV指令,而除C/C++以外的其它程式設計語言基本沒有直接存取絕對位址的能力。在嵌入式系統的實際調試中,多藉助C語言指標所具有的對絕對位址單元內容的讀寫能力。以指標直接操作記憶體多發生在如下幾種情況:
(1) 某I/O晶片被定位在CPU的儲存空間而非I/O空間,而且寄存器對應於某特定地址;
(2) 兩個CPU之間以雙連接埠RAM通訊,CPU需要在雙連接埠RAM的特定單元(稱為mail box)書寫內容以在對方CPU產生中斷;
(3) 讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00; *p=11; |
以上程式的意義為在絕對位址0xF0000+0xFF00(80186使用16位段地址和16位位移地址)寫入11。
在使用絕對位址指標時,要注意指標自增自減操作的結果取決於指標指向的資料類別。上例中p++後的結果是p= 0xF000FF01,若p指向int,即:
| int *p = (int *)0xF000FF00; |
p++(或++p)的結果等同於:p = p+sizeof(int),而p-(或-p)的結果是p = p-sizeof(int)。
同理,若執行:
| long int *p = (long int *)0xF000FF00; |
則p++(或++p)的結果等同於:p = p+sizeof(long int) ,而p-(或-p)的結果是p = p-sizeof(long int)。
記住:CPU以位元組為單位編址,而C語言指標以指向的資料類型長度作自增和自減。理解這一點對於以指標直接操作記憶體是相當重要的。
函數指標
首先要理解以下三個問題:
(1)C語言中函數名直接對應於函數產生的指令代碼在記憶體中的地址,因此函數名可以直接賦給指向函數的指標;
(2)調用函數實際上等同於"調轉指令+參數傳遞處理+迴歸位置入棧",本質上最核心的操作是將函數產生的目標代碼的首地址賦給CPU的PC寄存器;
(3)因為函數調用的本質是跳轉到某一個地址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:
請拿出你可以獲得的任何一本大學《微電腦原理》教材,書中講到,186 CPU啟動後跳轉至絕對位址0xFFFF0(對應C語言指標是0xF000FFF0,0xF000為段地址,0xFFF0為段內位移)執行,請看下面的代碼:
typedef void (*lpFunction) ( ); /* 定義一個無參數、無傳回型別的 */ /* 函數指標類型 */ lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定義一個函數指標,指向*/ /* CPU啟動後所執行第一條指令的位置 */ lpReset(); /* 調用函數 */ |
在以上的程式中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟開機"的作用,跳轉到CPU啟動後第一條要執行的指令的位置。
記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!
數組vs.動態申請
在嵌入式系統中動態記憶體申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的記憶體空間往往是十分有限的,不經意的記憶體泄露會很快導致系統的崩潰。
所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程式:
char * function(void) { char *p; p = (char *)malloc(…); if(p==NULL) …; … /* 一系列針對p的操作 */ return p; } |
在某處調用function(),用完function中動態申請的記憶體後將其free,如下:
char *q = function(); … free(q); |
上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即"誰申請,就由誰釋放"原則。不滿足這個原則,會導致代碼的耦合度增大,因為使用者在調用function函數時需要知道其內部細節!
正確的做法是在調用處申請記憶體,並傳入function函數,如下:
char *p=malloc(…); if(p==NULL) …; function(p); … free(p); p=NULL; |
而函數function則接收參數p,如下:
void function(char *p) { … /* 一系列針對p的操作 */ } |
基本上,動態申請記憶體方式可以用較大的數組替換。對於編程新手,筆者推薦你盡量採用數組!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻範政治錯誤走反革命道路的楊康。
給出原則:
(1)儘可能的選用數組,數組不能越界訪問(真理越過一步就是謬誤,數組越過界限就光榮地成全了一個混亂的嵌入式系統);
(2)如果使用動態申請,則申請後一定要判斷是否申請成功了,並且malloc和free應成對出現!
即:
| int *p = (int *)0xF000FF00; |
p++(或++p)的結果等同於:p = p+sizeof(int),而p-(或-p)的結果是p = p-sizeof(int)。
同理,若執行:
| long int *p = (long int *)0xF000FF00; |
則p++(或++p)的結果等同於:p = p+sizeof(long int) ,而p-(或-p)的結果是p = p-sizeof(long int)。
記住:CPU以位元組為單位編址,而C語言指標以指向的資料類型長度作自增和自減。理解這一點對於以指標直接操作記憶體是相當重要的。
函數指標
首先要理解以下三個問題:
(1)C語言中函數名直接對應於函數產生的指令代碼在記憶體中的地址,因此函數名可以直接賦給指向函數的指標;
(2)調用函數實際上等同於"調轉指令+參數傳遞處理+迴歸位置入棧",本質上最核心的操作是將函數產生的目標代碼的首地址賦給CPU的PC寄存器;
(3)因為函數調用的本質是跳轉到某一個地址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:
請拿出你可以獲得的任何一本大學《微電腦原理》教材,書中講到,186 CPU啟動後跳轉至絕對位址0xFFFF0(對應C語言指標是0xF000FFF0,0xF000為段地址,0xFFF0為段內位移)執行,請看下面的代碼:
typedef void (*lpFunction) ( ); /* 定義一個無參數、無傳回型別的 */ /* 函數指標類型 */ lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定義一個函數指標,指向*/ /* CPU啟動後所執行第一條指令的位置 */ lpReset(); /* 調用函數 */ |
在以上的程式中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟開機"的作用,跳轉到CPU啟動後第一條要執行的指令的位置。
記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!
數組vs.動態申請
在嵌入式系統中動態記憶體申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的記憶體空間往往是十分有限的,不經意的記憶體泄露會很快導致系統的崩潰。
所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程式:
char * function(void) { char *p; p = (char *)malloc(…); if(p==NULL) …; … /* 一系列針對p的操作 */ return p; } |
在某處調用function(),用完function中動態申請的記憶體後將其free,如下:
char *q = function(); … free(q); |
上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即"誰申請,就由誰釋放"原則。不滿足這個原則,會導致代碼的耦合度增大,因為使用者在調用function函數時需要知道其內部細節!
正確的做法是在調用處申請記憶體,並傳入function函數,如下:
char *p=malloc(…); if(p==NULL) …; function(p); … free(p); p=NULL; |
而函數function則接收參數p,如下:
void function(char *p) { … /* 一系列針對p的操作 */ } |
基本上,動態申請記憶體方式可以用較大的數組替換。對於編程新手,筆者推薦你盡量採用數組!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻範政治錯誤走反革命道路的楊康。
給出原則:
(1)儘可能的選用數組,數組不能越界訪問(真理越過一步就是謬誤,數組越過界限就光榮地成全了一個混亂的嵌入式系統);
(2)如果使用動態申請,則申請後一定要判斷是否申請成功了,並且malloc和free應成對出現!
關鍵字const
const意味著"唯讀"。區別如下代碼的功能非常重要,也是老生長歎,如果你還不知道它們的區別,而且已經在程式界摸爬滾打多年,那隻能說這是一個悲哀:
const int a; int const a; const int *a; int * const a; int const * a const; |
(1) 關鍵字const的作用是為給讀你代碼的人傳達非常有用的資訊。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬於"輸入參數"。在有多個形參的時候,函數的調用者可以憑藉參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。
(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。
const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:"只能讀的普通變數",可以稱其為"不能改變的變數"(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在C語言中如下程式是非法的:
const int SIZE = 10; char a[SIZE]; /* 非法:編譯階段不能用到變數 */ |
關鍵字volatile
C語言編譯器會對使用者書寫的代碼進行最佳化,譬如如下代碼:
int a,b,c; a = inWord(0x100); /*讀取I/O空間0x100連接埠的內容存入a變數*/ b = a; a = inWord (0x100); /*再次讀取I/O空間0x100連接埠的內容存入a變數*/ c = a; |
很可能被編譯器最佳化為:
int a,b,c; a = inWord(0x100); /*讀取I/O空間0x100連接埠的內容存入a變數*/ b = a; c = a; |
但是這樣的最佳化結果可能導致錯誤,如果I/O空間0x100連接埠的內容在執行第一次讀操作後被其它程式寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變數a的定義前加上volatile關鍵字可以防止編譯器的類似最佳化,正確的做法是:
volatile變數可能用於如下幾種情況:
(1) 平行裝置的硬體寄存器(如:狀態寄存器,例中的代碼屬於此類);
(2) 一個中斷服務子程式中會訪問到的非自動變數(也就是全域變數);
(3) 多線程應用中被幾個任務共用的變數。
CPU字長與儲存空間位寬不一致處理
在背景篇中提到,本文特意選擇了一個與CPU字長不一致的儲存晶片,就是為了進行本節的討論,解決CPU字長與儲存空間位寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫位元組、字的介面,如下:
typedef unsigned char BYTE; typedef unsigned int WORD; /* 函數功能:讀NVRAM中位元組 * 參數:wOffset,讀取位置相對NVRAM基地址的位移 * 返回:讀取到的位元組值 */ extern BYTE ReadByteNVRAM(WORD wOffset) { LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼位移要×2? */ return *lpAddr; } /* 函數功能:讀NVRAM中字 * 參數:wOffset,讀取位置相對NVRAM基地址的位移 * 返回:讀取到的字 */ extern WORD ReadWordNVRAM(WORD wOffset) { WORD wTmp = 0; LPBYTE lpAddr; /* 讀取高位位元組 */ lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼位移要×2? */ wTmp += (*lpAddr)*256; /* 讀取低位位元組 */ lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 為什麼位移要×2? */ wTmp += *lpAddr; return wTmp; } /* 函數功能:向NVRAM中寫一個位元組 *參數:wOffset,寫入位置相對NVRAM基地址的位移 * byData,欲寫入的位元組 */ extern void WriteByteNVRAM(WORD wOffset, BYTE byData) { … } /* 函數功能:向NVRAM中寫一個字 */ *參數:wOffset,寫入位置相對NVRAM基地址的位移 * wData,欲寫入的字 */ extern void WriteWordNVRAM(WORD wOffset, WORD wData) { … } |
子貢問曰:Why位移要乘以2?
子曰:請看圖1,16位80186與8位NVRAM之間互連只能以地址線A1對其A0,CPU本身的A0與NVRAM不串連。因此,NVRAM的地址只能是偶數地址,故每次以0x10為單位前進!
子貢再問:So why 80186的地址線A0不與NVRAM的A0串連?
子曰:請看《IT論語》之《微機原理篇》,那裡面講述了關於電腦群組成的聖人之道。
總結
本篇主要講述了嵌入式系統C編程中記憶體操作的相關技巧。掌握並深入理解關於資料指標、函數指標、動態申請記憶體、const及volatile關鍵字等的相關知識,是一個優秀的C語言程式設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在記憶體操作中體現。
我們之所以在嵌入式系統中使用C語言進行程式設計,99%是因為其強大的記憶體操作能力!
如果你愛編程,請你愛C語言;
如果你愛C語言,請你愛指標;
如果你愛指標,請你愛指標的指標!