亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

嵌入式 C/C++語言精華文章集錦

2019-11-14 08:44:02
字體:
來源:轉載
供稿:網友
C/C+語言 struct 深層探索 ............................................................................2C++中 extern "C"含義深層探索........................................................................7C 語言高效編程的幾招...............................................................................11想成為嵌入式程序員應知道的 0x10 個基本問題 .........................................................15C 語言嵌入式系統編程修煉...........................................................................22C 語言嵌入式系統編程修煉之一:背景篇............................................................22C 語言嵌入式系統編程修煉之二:軟件架構篇........................................................24C 語言嵌入式系統編程修煉之三:內存操作..........................................................30C 語言嵌入式系統編程修煉之四:屏幕操作..........................................................36C 語言嵌入式系統編程修煉之五:鍵盤操作..........................................................43C 語言嵌入式系統編程修煉之六:性能優化..........................................................46C/C++語言 void 及 void 指針深層探索 .................................................................50C/C++語言可變參數表深層探索 .......................................................................54C/C++數組名與指針區別深層探索 .....................................................................60C/C++程序員應聘常見面試題深入剖析(1) ..............................................................62C/C++程序員應聘常見面試題深入剖析(2) ..............................................................67一道著名外企面試題的抽絲剝繭 ......................................................................74C/C++結構體的一個高級特性――指定成員的位數 .......................................................78C/C++中的近指令、遠指針和巨指針 ...................................................................80從兩道經典試題談 C/C++中聯合體(union)的使用......................................................81基于 ARM 的嵌入式 linux 移植真實體驗 ................................................................83基于 ARM 的嵌入式 Linux 移植真實體驗(1)――基本概念 ...........................................83基于 ARM 的嵌入式 Linux 移植真實體驗(2)――BootLoader .........................................96基于 ARM 的嵌入式 Linux 移植真實體驗( 3)――操作系統 ..........................................111基于 ARM 的嵌入式 Linux 移植真實體驗(4)――設備驅動 ..........................................120基于 ARM 的嵌入式 Linux 移植真實體驗(5)――應用實例 ..........................................135深入淺出 Linux 設備驅動編程 .......................................................................1441.Linux 內核模塊..............................................................................1442.字符設備驅動程序 ...........................................................................1463.設備驅動中的并發控制 .......................................................................1514.設備的阻塞與非阻塞操作 .....................................................................157C/C+語言 struct 深層探索出處:PConline 作者:宋寶華1. struct 的巨大作用面對一個人的大型 C/C++程序時,只看其對 struct 的使用情況我們就可以對其編寫者的編程經驗進行評估。因為一個大型的 C/C++程序,勢必要涉及一些(甚至大量)進行數據組合的結構體,這些結構體可以將原本意義屬于一個整體的數據組合在一起。從某種程度上來說,會不會用 struct,怎樣用struct 是區別一個開發人員是否具備豐富開發經歷的標志。在網絡協議、 通信控制、 嵌入式系統的 C/C++編程中, 我們經常要傳送的不是簡單的字節流 (char型數組),而是多種數據組合起來的一個整體,其表現形式是一個結構體。經驗不足的開發人員往往將所有需要傳送的內容依順序保存在 char 型數組中,通過指針偏移的方法傳送網絡報文等信息。這樣做編程復雜,易出錯,而且一旦控制方式及通信協議有所變化,程序就要進行非常細致的修改。一個有經驗的開發者則靈活運用結構體,舉一個例子,假設網絡或控制協議中需要傳送三種報文,其格式分別為 packetA、packetB、packetC:struct structA{int a;char b;};struct structB{char a;short b;};struct structC{int a;char b;float c;}優秀的程序設計者這樣設計傳送的報文:struct CommuPacket{3int ipacketType; //報文類型標志union //每次傳送的是三種報文中的一種,使用 union{struct structA packetA; struct structB packetB;struct structC packetC;}};在進行報文傳送時,直接傳送 struct CommuPacket 一個整體。假設發送函數的原形如下:// pSendData:發送字節流的首地址,iLen:要發送的長度Send(char * pSendData, unsigned int iLen);發送方可以直接進行如下調用發送 struct CommuPacket 的一個實例 sendCommuPacket:Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );假設接收函數的原形如下:// PRecvData:發送字節流的首地址,iLen:要接收的長度//返回值:實際接收到的字節數unsigned int Recv(char * pRecvData, unsigned int iLen);接收方可以直接進行如下調用將接收到的數據保存在 struct CommuPacket 的一個實例 recvCommuPacket 中:Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );接著判斷報文類型進行相應處理:switch(recvCommuPacket. iPacketType){case PACKET_A:… //A 類報文處理break;case PACKET_B:… //B 類報文處理break;case PACKET_C:… //C 類報文處理break;}以上程序中最值得注意的是Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );中的強制類型轉換:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再轉化為 char 型指針,這樣就可以直接利用處理字節流的函數。利用這種強制類型轉化,我們還可以方便程序的編寫,例如要對 sendCommuPacket 所處內存初始化為 0,可以這樣調用標準庫函數 memset():memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));2. struct的成員對齊Intel、微軟等公司曾經出過一道類似的面試題:#include <iostream.h>4#pragma pack(8)struct example1{short a;long b;};struct example2{char c;example1 struct1;short e;};#pragma pack()int main(int argc, char* argv[]){example2 struct2;cout << sizeof(example1) << endl;cout << sizeof(example2) << endl;cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;return 0;}問程序的輸入結果是什么?答案是:8164不明白?還是不明白?下面一一道來:2.1 自然對界struct 是一種復合數據類型,其構成元素既可以是基本數據類型(如 int、long、float 等)的變量,也可以是一些復合數據類型(如 array、struct、union 等)的數據單元。對于結構體,編譯器會自動進行成員變量的對齊,以提高運算效率。缺省情況下,編譯器為結構體的每個成員按其自然對界(natural alignment)條件分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。自然對界(natural alignment)即默認對齊方式,是指按結構體的成員中 size 最大的成員對齊。例如:struct naturalalign{char a;short b;char c;};在上述結構體中,size 最大的是 short,其長度為 2 字節,因而結構體中的 char 成員 a、c 都以 2 為單位對齊,sizeof(naturalalign)的結果等于 6;如果改為:struct naturalalign5{char a;int b;char c;};其結果顯然為 12。2.2 指定對界一般地,可以通過下面的方法來改變缺省的對界條件:· 使用偽指令#pragma pack (n),編譯器將按照 n 個字節對齊;· 使用偽指令#pragma pack (),取消自定義字節對齊方式。注意: 如果#pragma pack (n)中指定的 n 大于結構體中最大成員的 size,則其不起作用,結構體仍然按照 size 最大的成員進行對界。例如:#pragma pack (n)struct naturalalign{char a;int b;char c;};#pragma pack ()當 n 為 4、8、16 時,其對齊方式均一樣,sizeof(naturalalign)的結果都等于 12。而當 n 為 2時,其發揮了作用,使得 sizeof(naturalalign)的結果為 6。在 VC++ 6.0 編譯器中,我們可以指定其對界方式(見圖 1),其操作方式為依次選擇 projetct >setting > C/C++菜單,在 struct member alignment 中指定你要的對界方式。圖 1 在 VC++ 6.0 中指定對界方式6另外,通過__attribute((aligned (n)))也可以讓所作用的結構體成員對齊在 n 字節邊界上,但是它較少被使用,因而不作詳細講解。2.3 面試題的解答至此,我們可以對 Intel、微軟的面試題進行全面的解答。程序中第 2 行#pragma pack (8)雖然指定了對界為 8,但是由于 struct example1 中的成員最大size 為 4(long 變量 size 為 4),故 struct example1 仍然按 4 字節對界,struct example1 的 size為 8,即第 18 行的輸出結果;struct example2 中包含了 struct example1,其本身包含的簡單數據成員的最大 size 為 2 (short變量 e),但是因為其包含了 struct example1,而 struct example1 中的最大成員 size 為 4,structexample2 也應以 4 對界,#pragma pack (8)中指定的對界對 struct example2 也不起作用,故 19 行的輸出結果為 16;由于 struct example2 中的成員以 4 為單位對界,故其 char 變量 c 后應補充 3 個空,其后才是成員 struct1 的內存空間,20 行的輸出結果為 4。3. C 和 C++間 struct 的深層區別在 C++語言中 struct 具有了“類” 的功能,其與關鍵字 class 的區別在于 struct 中成員變量和函數的默認訪問權限為 public,而 class 的為 private。例如,定義 struct 類和 class 類:struct structA{char a;…}class classB{char a;…}則:structA a;a.a = 'a'; //訪問 public 成員,合法classB b;b.a = 'a'; //訪問 private 成員,不合法許多文獻寫到這里就認為已經給出了 C++中 struct 和 class 的全部區別,實則不然,另外一點需要注意的是:C++中的 struct 保持了對 C 中 struct 的全面兼容(這符合 C++的初衷——“a better c”),因而,下面的操作是合法的://定義 structstruct structA{char a;char b;int c;};7structA a = {'a' , 'a' ,1}; // 定義時直接賦初值即 struct 可以在定義的時候直接以{ }對其成員變量賦初值,而 class 則不能,在經典書目《thinking C++ 2nd edition》中作者對此點進行了強調。4. struct 編程注意事項看看下面的程序:1. #include <iostream.h>2. struct structA3. {4. int iMember;5. char *cMember;6. };7. int main(int argc, char* argv[])8.{9. structA instant1,instant2;10. char c = 'a';11. instant1.iMember = 1;12. instant1.cMember = &c;13. instant2 = instant1;14. cout << *(instant1.cMember) << endl;15. *(instant2.cMember) = 'b';16. cout << *(instant1.cMember) << endl;17. return 0;}14 行的輸出結果是:a16 行的輸出結果是:bWhy?我們在 15 行對 instant2 的修改改變了 instant1 中成員的值!原因在于 13 行的 instant2 = instant1 賦值語句采用的是變量逐個拷貝,這使得 instant1 和instant2 中的 cMember 指向了同一片內存,因而對 instant2 的修改也是對 instant1 的修改。在 C 語言中,當結構體中存在指針型成員時,一定要注意在采用賦值語句時是否將 2 個實例中的指針型成員指向了同一片內存。在 C++語言中,當結構體中存在指針型成員時,我們需要重寫 struct 的拷貝構造函數并進行“=”操作符重載。C++中 extern "C"含義深層探索作者: 宋寶華 e-mail:21cnbao@21cn.com出處: 太平洋電腦網1.引言C++語言的創建初衷是“a better C”,但是這并不意味著 C++中類似 C 語言的全局變量和函數所采用的編譯和連接方式與 C 語言完全相同。作為一種欲與 C 兼容的語言,C++保留了一部分過程式語言的特點(被世人稱為“不徹底地面向對象”),因而它可以定義不屬于任何類的全局變量和函數。8但是,C++畢竟是一種面向對象的程序設計語言,為了支持函數的重載,C++對全局函數的處理方式與 C有明顯的不同。2.從標準頭文件說起某企業曾經給出如下的一道面試題:面試題為什么標準頭文件都有類似以下的結構?#ifndef __INCvxWorksh#define __INCvxWorksh#ifdef __cplusplusextern "C" {#endif/*...*/#ifdef __cplusplus}#endif#endif /* __INCvxWorksh */分析顯然,頭文件中的編譯宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止該頭文件被重復引用。那么#ifdef __cplusplusextern "C" {#endif#ifdef __cplusplus}#endif的作用又是什么呢?我們將在下文一一道來。3.深層揭密 extern "C"extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。讓我們來詳細解讀這兩重含義。(1)被 extern "C"限定的函數或變量是 extern 類型的;extern 是 C/C++語言中表明函數和全局變量作用范圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。記住,下列語句:extern int a;僅僅是一個變量的聲明,其并不是在定義變量 a,并未為 a 分配內存空間。變量 a 在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字 extern 聲明。例如,如果模塊 B 欲引用該模塊 A 中定義的全局變量和函數時只需包含模塊 A 的頭文件即可。這樣,模塊 B 中調用模塊 A 中的函數時,在編譯階段,模塊 B 雖然找不到該函數,但是并不會報錯;它會在連接階段中從模塊 A 編譯生成的目標代碼中找到此函數。與 extern 對應的關鍵字是 static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被 extern “C”修飾。(2)被 extern "C"修飾的變量和函數是按照 C 語言方式編譯和連接的;未加 extern “C”聲明時的編譯方式9首先看看 C++中對類似 C 的函數是怎樣編譯的。作為一種面向對象的語言,C++支持函數重載,而過程式語言 C 則不支持。函數被 C++編譯后在符號庫中的名字與 C 語言的不同。例如,假設某個函數的原型為:void foo( int x, int y );該函數被 C 編譯器編譯后在符號庫中的名字為_foo,而 C++編譯器則會產生像_foo_int_int 之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為“mangledname”)。_foo_int_int 這樣的名字包含了函數名、函數參數數量及類型信息,C++就是靠這種機制來實現函數重載的。例如,在 C++中,函數 void foo( int x, int y )與 void foo( int x, float y )編譯生成的符號是不相同的,后者為_foo_int_float。同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也為類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。未加 extern "C"聲明時的連接方式假設在 C++中,模塊 A 的頭文件如下:// 模塊 A 頭文件 moduleA.h#ifndef MODULE_A_H#define MODULE_A_Hint foo( int x, int y );#endif在模塊 B 中引用該函數:// 模塊 B 實現文件 moduleB.cpp#include "moduleA.h"foo(2,3);實際上,在連接階段,連接器會從模塊 A 生成的目標文件 moduleA.obj 中尋找_foo_int_int 這樣的符號!加 extern "C"聲明后的編譯和連接方式加 extern "C"聲明后,模塊 A 的頭文件變為:// 模塊 A 頭文件 moduleA.h#ifndef MODULE_A_H#define MODULE_A_Hextern "C" int foo( int x, int y );#endif在模塊 B 的實現文件中仍然調用 foo( 2,3 ),其結果是:(1)模塊 A 編譯生成 foo 的目標代碼時,沒有對其名字進行特殊處理,采用了 C 語言的方式;(2)連接器在為模塊 B 的目標代碼尋找 foo(2,3)調用時,尋找的是未經修改的符號名_foo。如果在模塊 A 中函數聲明了 foo 為 extern "C"類型,而模塊 B 中包含的是 extern int foo( int x,int y ) ,則模塊 B 找不到模塊 A 中的函數;反之亦然。所以,可以用一句話概括 extern “C”這個聲明的真實目的(任何語言中的任何語法特性的誕生都不是隨意而為的,來源于真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎么做的,還要問一問它為什么要這么做,動機是什么,這樣我們可以更深入地理解許多問題):實現 C++與 C 及其它語言的混合編程。明白了 C++中 extern "C"的設立動機,我們下面來具體分析 extern "C"通常的使用技巧。4.extern "C"的慣用法10(1)在 C++中引用 C 語言中的函數和變量,在包含 C 語言頭文件(假設為 cExample.h)時,需進行下列處理:extern "C"{#include "cExample.h"}而在 C 語言的頭文件中,對其外部函數只能指定為 extern 類型,C 語言中不支持 extern "C"聲明,在.c 文件中包含了 extern "C"時會出現編譯語法錯誤。筆者編寫的 C++引用 C 函數例子工程中包含的三個文件的源代碼如下:/* c 語言頭文件:cExample.h */#ifndef C_EXAMPLE_H#define C_EXAMPLE_Hextern int add(int x,int y);#endif/* c 語言實現文件:cExample.c */#include "cExample.h"int add( int x, int y ){return x + y;}// c++實現文件,調用 add:cppFile.cppextern "C"{#include "cExample.h"}int main(int argc, char* argv[]){add(2,3);return 0;}如果 C++調用一個 C 語言編寫的.DLL 時,當包括.DLL 的頭文件或聲明接口函數時,應加 extern "C"{ }。(2)在 C 中引用 C++語言中的函數和變量時,C++的頭文件需添加 extern "C",但是在 C 語言中不能直接引用聲明了 extern "C"的該頭文件,應該僅將 C 文件中將 C++中定義的 extern "C"函數聲明為extern 類型。筆者編寫的 C 引用 C++函數例子工程中包含的三個文件的源代碼如下://C++頭文件 cppExample.h#ifndef CPP_EXAMPLE_H#define CPP_EXAMPLE_Hextern "C" int add( int x, int y );#endif//C++實現文件 cppExample.cpp#include "cppExample.h"int add( int x, int y )11{return x + y;}/* C 實現文件 cFile.c/* 這樣會編譯出錯:#include "cExample.h" */extern int add( int x, int y );int main( int argc, char* argv[] ){add( 2, 3 );return 0;}如果深入理解了第 3 節中所闡述的 extern "C"在編譯和連接階段發揮的作用,就能真正理解本節所闡述的從 C++引用 C 函數和 C 引用 C++函數的慣用法。對第 4 節給出的示例代碼,需要特別留意各個細節。C 語言高效編程的幾招編寫高效簡潔的 C語言代碼,是許多軟件工程師追求的目標。本文就工作中的一些體會和經驗做相關的闡述,不對的地方請各位指教。第 1 招:以空間換時間計算機程序中最大的矛盾是空間和時間的矛盾,那么,從這個角度出發逆向思維來考慮程序的效率問題,我們就有了解決問題的第 1 招--以空間換時間。例如:字符串的賦值。方法 A,通常的辦法:#define LEN 32char string1 [LEN];memset (string1,0,LEN);strcpy (string1,"This is an example!!"方法 B:const char string2[LEN]="This is an example!"char*cp;cp=string2;(使用的時候可以直接用指針來操作。 )從上面的例子可以看出, A和 B的效率是不能比的。在同樣的存儲空間下, B 直接使用指針就可以操作了,而A 需要調用兩個字符函數才能完成。 B 的缺點在于靈活性沒有A 好。在需要頻繁更改一個字符串內容的時候,A 具有更好的靈活性;如果采用方法 B,則需要預存許多字符串,雖然占用了 大量的內存,但是獲得了程序執行的高效率。如果系統的實時性要求很高,內存還有一些,那我推薦你使用該招數。12該招數的邊招--使用宏函數而不是函數。舉例如下:方法 C:#define bwMCDR2_ADDRESS 4#define bsMCDR2_ADDRESS 17int BIT_MASK (int_bf){return ((IU<<(bw##_bf))-1)<<(bs##_bf);}void SET_BITS(int_dst,int_bf,int_val){_dst=((_dst) & ~ (BIT_MASK(_bf)))I/(((_val)<<<(bs##_bf))&(BIT_MASK(_bf)))}SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumber);方法 D:#define bwMCDR2_ADDRESS 4#define bsMCDR2_ADDRESS 17#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)#define BIT_MASK(_bf)(((1U<<(bw##_bf))-1)<<(bs##_bf)#define SET_BITS(_dst,_bf,_val)/((_dst)=((_dst)&~(BIT_MASK(_bf)))I(((_val)<<(bs##_bf))&(BIT_MASK(_bf))))SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumber);函數和宏函數的區別就在于,宏函數占用了大量的空間,而函數占用了時間。大家要知道的是,函數調用是要使用系統的棧來保存數據的,如果編譯器里有棧檢查選項,一般在函數的頭會嵌入一些匯編語句對當前棧進行檢查;同時, CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操作,所以,函數調用需要一些 CPU時間。而宏函數不存在這個問題。宏函數僅僅作為預先寫好的代碼嵌入到當前程序,不會產生函數調用,所以僅僅是占用了空間,在頻繁調用同一個宏函數的時候,該現象尤其突出。D 方法是我看到的最好的置位操作函數,是ARM 公司源碼的一部分,在短短的三行內實現了很多功能,幾乎涵蓋了所有的位操作功能。 C 方法是其變體,其中滋味還需大家仔細體會。第 2 招:數學方法解決問題現在我們演繹高效 C 語言編寫的第二招--采用數學方法來解決問題。數學是計算機之母,沒有數學的依據和基礎,就沒有計算機的發展,所以在編寫程序的時候,采用一些數學方法會對程序的執行效率有數量級的提高。舉例如下,求 1~100 的和。
方法 Eint I,j;方法 Fint I;
13
for (I=1; I<=100; I++){j+=I;}I=(100*(1+100))/2
這個例子是我印象最深的一個數學用例,是我的餓計算機啟蒙老師考我的。當時我只有小學三年級,可惜我當時不知道用公式 Nx(N+1)/2 來解決這個問題。方法E 循環了100 次才解決問題,也就是說最少用了100 個賦值、100 個判斷、200個加法(I和 j);而方法F 僅僅用了1 個加法、1 個乘法、1 次除法。效果自然不言而喻。所以,現在我在編程序的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提高程序運行的效率。第 3 招:使用位操作實現高效的 C 語言編寫的第三招--使用位操作,減少除法和取模的運算。在計算機程序中,數據的位是可以操作的最小數據單位,理論上可以用“位運算”來完成所有的運算和操作。一般的位操作是用來控制硬件的,或者做數據變換使用,但是,靈活的位操作可以有效地提高程序運行的效率。舉例臺如下:方法 Gint I,J;I=257/8;J=456%32;方法 Hint I,J;I=257>>3;J=456-(456>>4<<4);在字面上好象 H比 G麻煩了好多,但是,仔細查看產生的匯編代碼就會明白,方法G 調用了基本的取模函數和除法函數,既有函數調用,還有很多匯編代碼和寄存器參與運算;而方法 H則僅僅是幾句相關的匯編,代碼更簡潔、效率更高。當然,由于編譯器的不同,可能效率的差距不大,但是,以我目前遇到的 MS C,ARM C來看,效率的差距還是不小。相關匯編代碼就不在這里列舉了。運用這招需要注意的是,因為 CPU 的不同而產生的問題。比如說,在PC 上用這招編寫的程序,并在PC 上調試通過,在移植到一個 16 位機平臺上的時候,可能會產生代碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。第 4 招:匯編嵌入高效 C 語言編程的必殺技,第四招--嵌入匯編?!霸谑煜R編語言的人眼里,C 語言編寫的程序都是垃圾”。這種說法雖然偏激了一些,但是卻有它的道理。匯編語言是效率最高的計算機語言,但是,不可能靠著它來寫一個操作系統吧?所以,為了獲得程序的高效率,我們只好采用變通的方法--嵌入匯編、混合編程。舉例如下,將數組一賦值給數組二,要求每一個字節都相符。 char string1[1024], string2[1024];14方法 Iint I;for (I=0; I<1024; I++)*(string2+I)=*(string1+I)方法 J#int I;for(I=0; I<1024; I++)*(string2+I)=*(string1+I);#else#ifdef_ARM__asm{MOV R0,string1MOV R1,string2MOV R2,#0loop:LDMIA R0!,[R3-R11]STMIA R1!,[R3-R11]ADD R2,R2,#8CMP R2, #400BNE loop}#endif方法 I是最常見的方法,使用了 1024次循環;方法 J則根據平臺不同做了區分,在 ARM平臺下,用嵌入匯編僅用 128次循環就完成了同樣的操作。這里有朋友會說,為什么不用標準的內存拷貝函數呢?這是因為在源數據里可能含有數據為0 的字節,這樣的話,標準庫函數會提前結束而不會完成我們要求的操作。這個例程典型應用于LCD 數據的拷貝過程。根據不同的 CPU,熟練使用相應的嵌入匯編,可以大大提高程序執行的效率。雖然是必殺技,但是如果輕易使用會付出慘重的代價。這是因為,使用了嵌入匯編,便限制了程序的可移植性,使程序在不同平臺移植的過程中,臥虎藏龍、險象環生!同時該招數也與現代軟件工程的思想相違背,只有在迫不得已的情況下才可以采用。切記。使用 C 語言進行高效率編程,我的體會僅此而已。在此已本文拋磚引玉,還請各位高手共同切磋。希望各位能給出更好的方法,大家一起提高我們的編程技巧。摘自《單片機與嵌入式系統應用》 2003.915想成為嵌入式程序員應知道的 0x10 個基本問題-|endeaver發表于 2006-3-8 16:16:00C 語言測試是招聘嵌入式系統程序員過程中必須而且有效的方法。這些年,我既參加也組織了許多這種測試,在這過程中我意識到這些測試能為帶面試者和被面試者提供許多有用信息,此外,撇開面試的壓力不談,這種測試也是相當有趣的。從被面試者的角度來講,你能了解許多關于出題者或監考者的情況。這個測試只是出題者為顯示其對 ANSI標準細節的知識而不是技術技巧而設計嗎?這個愚蠢的問題嗎?如要你答出某個字符的 ASCII值。這些問題著重考察你的系統調用和內存分配策略方面的能力嗎?這標志著出題者也許花時間在微機上而不上在嵌入式系統上。如果上述任何問題的答案是"是"的話,那么我知道我得認真考慮我是否應該去做這份工作。從面試者的角度來講,一個測試也許能從多方面揭示應試者的素質:最基本的,你能了解應試者 C語言的水平。不管怎么樣,看一下這人如何回答他不會的問題也是滿有趣。應試者是以好的直覺做出明智的選擇,還是只是瞎蒙呢?當應試者在某個問題上卡住時是找借口呢,還是表現出對問題的真正的好奇心,把這看成學習的機會呢?我發現這些信息與他們的測試成績一樣有用。有了這些想法,我決定出一些真正針對嵌入式系統的考題,希望這些令人頭痛的考題能給正在找工作的人一點幫住。這些問題都是我這些年實際碰到的。其中有些題很難,但它們應該都能給你一點啟迪。這個測試適于不同水平的應試者,大多數初級水平的應試者的成績會很差,經驗豐富的程序員應該有很好的成績。為了讓你能自己決定某些問題的偏好,每個問題沒有分配分數,如果選擇這些考題為你所用,請自行按你的意思分配分數。預處理器( Preprocessor)1 . 用預處理指令#define聲明一個常數,用以表明 1年中有多少秒(忽略閏年問題)#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL我在這想看到幾件事情:?; #define 語法的基本知識(例如:不能以分號結束,括號的使用,等等)?; 懂得預處理器將為你計算常數表達式的值,因此,直接寫出你是如何計算一年中有多少秒而不是計算出實際的值,是更清晰而沒有代價的。?; 意識到這個表達式將使一個16 位機的整型數溢出-因此要用到長整型符號L,告訴編譯器這個常數是的長整型數。?; 如果你在你的表達式中用到UL(表示無符號長整型),那么你有了一個好的起點。記住,第一印象很重要。2 . 寫一個"標準"宏MIN ,這個宏輸入兩個參數并返回較小的一個。#define MIN(A,B) ((A)<= (B) ? (A) : (B))這個測試是為下面的目的而設的:?; 標識#define在宏中應用的基本知識。這是很重要的,因為直到嵌入(inline)操作符變為標準C 的一部分,宏是方便產生嵌入代碼的唯一方法,對于嵌入式系統來說,為了能達到要求的性能,嵌入代碼經常是必須的方法。?; 三重條件操作符的知識。這個操作符存在C 語言中的原因是它使得編譯器能產生比if-then-else 更優化的代碼,了解這個用法是很重要的。?; 懂得在宏中小心地把參數用括號括起來?; 我也用這個問題開始討論宏的副作用,例如:當你寫下面的代碼時會發生什么事?least = MIN(*p++, b);3. 預處理器標識#error的目的是什么?如果你不知道答案,請看參考文獻 1。這問題對區分一個正常的伙計和一個書呆子是很有用的。只有書呆子才會讀C 語言課本的附錄去找出象這種問題的答案。當然如果你不是在找一個書呆子,那么應試者最好希望自己不要知道答案。死循環( Infinite loops)4. 嵌入式系統中經常要用到無限循環,你怎么樣用C 編寫死循環呢?16這個問題用幾個解決方案。我首選的方案是:while(1){?}一些程序員更喜歡如下方案:for(;;){?}這個實現方式讓我為難,因為這個語法沒有確切表達到底怎么回事。如果一個應試者給出這個作為方案,我將用這個作為一個機會去探究他們這樣做的基本原理。如果他們的基本答案是: "我被教著這樣做,但從沒有想到過為什么。"這會給我留下一個壞印象。第三個方案是用 gotoLoop:...goto Loop;應試者如給出上面的方案,這說明或者他是一個匯編語言程序員(這也許是好事)或者他是一個想進入新領域的BASIC/FORTRAN 程序員。數據聲明( Data declarations)5. 用變量a 給出下面的定義a) 一個整型數(An integer)b)一個指向整型數的指針(A pointer to an integer)c)一個指向指針的的指針,它指向的指針是指向一個整型數(A pointer to a pointer to an intege)rd)一個有 10個整型數的數組(An array of 10 integers)e) 一個有10 個指針的數組,該指針是指向一個整型數的。(An array of 10 pointers to integers)f) 一個指向有10 個整型數數組的指針(A pointer to an array of 10 integers)g) 一個指向函數的指針,該函數有一個整型參數并返回一個整型數(A pointer to a function that takes an integer as an argumentand returns an integer)h)一個有10 個指針的數組,該指針指向一個函數,該函數有一個整型參數并返回一個整型數(An array of ten pointers to functions that take an integer argument and return an integer )答案是:a) int a; // An integerb) int *a; // A pointer to an integerc) int **a; // A pointer to a pointer to an integerd) int a[10]; // An array of 10 integerse) int *a[10]; // An array of 10 pointers to integersf) int (*a)[10]; // A pointer to an array of 10 integersg) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integerh) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer人們經常聲稱這里有幾個問題是那種要翻一下書才能回答的問題,我同意這種說法。當我寫這篇文章時,為了確定語法的正確性,我的確查了一下書。但是當我被面試的時候,我期望被問到這個問題(或者相近的問題)。因為在被面試的這段時間里,我確定我知道這個問題的答案。應試者如果不知道所有的答案(或至少大部分答案),那么也就沒有為這次面試做準備,如果該面試者沒有為這次面試做準備,那么他又能為什么出準備呢?17Static6. 關鍵字 static的作用是什么?這個簡單的問題很少有人能回答完全。在 C 語言中,關鍵字 static有三個明顯的作用:?; 在函數體,一個被聲明為靜態的變量在這一函數被調用過程中維持其值不變。?; 在模塊內(但在函數體外),一個被聲明為靜態的變量可以被模塊內所用函數訪問,但不能被模塊外其它函數訪問。它是一個本地的全局變量。?; 在模塊內,一個被聲明為靜態的函數只可被這一模塊內的其它函數調用。那就是,這個函數被限制在聲明它的模塊的本地范圍內使用。大多數應試者能正確回答第一部分,一部分能正確回答第二部分,同是很少的人能懂得第三部分。這是一個應試者的嚴重的缺點,因為他顯然不懂得本地化數據和代碼范圍的好處和重要性。Const7.關鍵字 const有什么含意?我只要一聽到被面試者說: "const 意味著常數",我就知道我正在和一個業余者打交道。去年Dan Saks 已經在他的文章里完全概括了const的所有用法,因此 ESP(譯者:Embedded Systems Programming)的每一位讀者應該非常熟悉const 能做什么和不能做什么.如果你從沒有讀到那篇文章,只要能說出 const 意味著"只讀"就可以了。盡管這個答案不是完全的答案,但我接受它作為一個正確的答案。(如果你想知道更詳細的答案,仔細讀一下 Saks 的文章吧。)如果應試者能正確回答這個問題,我將問他一個附加的問題:下面的聲明都是什么意思?const int a;int const a;const int *a;int * const a;int const * a const;/******/前兩個的作用是一樣, a是一個常整型數。第三個意味著 a是一個指向常整型數的指針(也就是,整型數是不可修改的,但指針可以)。第四個意思 a 是一個指向整型數的常指針(也就是說,指針指向的整型數是可以修改的,但指針是不可修改的)。最后一個意味著a 是一個指向常整型數的常指針(也就是說,指針指向的整型數是不可修改的,同時指針也是不可修改的)。如果應試者能正確回答這些問題,那么他就給我留下了一個好印象。順帶提一句,也許你可能會問,即使不用關鍵字 const,也還是能很容易寫出功能正確的程序,那么我為什么還要如此看重關鍵字 const 呢?我也如下的幾下理由:?; 關鍵字const 的作用是為給讀你代碼的人傳達非常有用的信息,實際上, 聲明一個參數為常量是為了告訴了用戶這個參數的應用目的。如果你曾花很多時間清理其它人留下的垃圾,你就會很快學會感謝這點多余的信息。(當然,懂得用 const的程序員很少會留下的垃圾讓別人來清理的。)?; 通過給優化器一些附加的信息,使用關鍵字const 也許能產生更緊湊的代碼。?; 合理地使用關鍵字const 可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改。簡而言之,這樣可以減少bug的出現。Volatile8. 關鍵字 volatile有什么含意?并給出三個不同的例子。一個定義為 volatile 的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。下面是 volatile變量的幾個例子:18?; 并行設備的硬件寄存器(如:狀態寄存器)?; 一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)?; 多線程應用中被幾個任務共享的變量回答不出這個問題的人是不會被雇傭的。我認為這是區分 C程序員和嵌入式系統程序員的最基本的問題。搞嵌入式的家伙們經常同硬件、中斷、RTOS 等等打交道,所有這些都要求用到volatile 變量。不懂得volatile 的內容將會帶來災難。假設被面試者正確地回答了這是問題(嗯,懷疑是否會是這樣),我將稍微深究一下,看一下這家伙是不是直正懂得 volatile完全的重要性。?; 一個參數既可以是const 還可以是volatile 嗎?解釋為什么。?; 一個指針可以是volatile 嗎?解釋為什么。?; 下面的函數有什么錯誤:int square(volatile int *ptr){return *ptr * *ptr;}下面是答案:?; 是的。一個例子是只讀的狀態寄存器。它是volatile 因為它可能被意想不到地改變。它是const 因為程序不應該試圖去修改它。?; 是的。盡管這并不很常見。一個例子是當一個中服務子程序修該一個指向一個buffer 的指針時。?; 這段代碼有點變態。這段代碼的目的是用來返指針*ptr指向值的平方,但是,由于*ptr指向一個 volatile型參數,編譯器將產生類似下面的代碼:int square(volatile int *ptr){int a,b;a = *ptr;b = *ptr;return a * b;}由于*ptr的值可能被意想不到地該變,因此 a和 b可能是不同的。結果,這段代碼可能返不是你所期望的平方值!正確的代碼如下:long square(volatile int *ptr){int a;a = *ptr;return a * a;}位操作( Bit manipulation)9. 嵌入式系統總是要用戶對變量或寄存器進行位操作。給定一個整型變量a,寫兩段代碼,第一個設置a 的bit 3,第二個清除a 的bit 3。在以上兩個操作中,要保持其它位不變。19對這個問題有三種基本的反應?; 不知道如何下手。該被面者從沒做過任何嵌入式系統的工作。?; 用bit fields。Bit fields 是被扔到C 語言死角的東西,它保證你的代碼在不同編譯器之間是不可移植的,同時也保證了的你的代碼是不可重用的。我最近不幸看到 Infineon 為其較復雜的通信芯片寫的驅動程序,它用到了 bit fields因此完全對我無用,因為我的編譯器用其它的方式來實現 bit fields 的。從道德講:永遠不要讓一個非嵌入式的家伙粘實際硬件的邊。?; 用#defines 和bit masks 操作。這是一個有極高可移植性的方法,是應該被用到的方法。最佳的解決方案如下:#define BIT3 (0x1 << 3)static int a;void set_bit3(void) {a |= BIT3;}void clear_bit3(void) {a &= ~BIT3;}一些人喜歡為設置和清除值而定義一個掩碼同時定義一些說明常數,這也是可以接受的。我希望看到幾個要點:說明常數、|=和&=~操作。訪問固定的內存位置( accessing fixed memory locations)10. 嵌入式系統經常具有要求程序員去訪問某特定的內存位置的特點。在某工程中,要求設置一絕對地址為0x67a9 的整型變量的值為0xaa66。編譯器是一個純粹的 ANSI編譯器。寫代碼去完成這一任務。這一問題測試你是否知道為了訪問一絕對地址把一個整型數強制轉換( typecast)為一指針是合法的。這一問題的實現方式隨著個人風格不同而不同。典型的類似代碼如下:int *ptr;ptr = (int *)0x67a9;*ptr = 0xaa55;A more obscure approach is:一個較晦澀的方法是:*(int * const)(0x67a9) = 0xaa55;即使你的品味更接近第二種方案,但我建議你在面試時使用第一種方案。中斷( Interrupts)11. 中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴展―讓標準C 支持中斷。具代表事實是,產生了一個新的關鍵字__interrupt。下面的代碼就使用了__interrupt關鍵字去定義了一個中斷服務子程序(ISR),請評論一下這段代碼的。__interrupt double compute_area (double radius){20double area = PI * radius * radius;printf("/nArea = %f", area);return area;}這個函數有太多的錯誤了,以至讓人不知從何說起了:?; ISR 不能返回一個值。如果你不懂這個,那么你不會被雇用的。?; ISR 不能傳遞參數。如果你沒有看到這一點,你被雇用的機會等同第一項。?; 在許多的處理器/編譯器中,浮點一般都是不可重入的。有些處理器/編譯器需要讓額處的寄存器入棧,有些處理器/編譯器就是不允許在ISR 中做浮點運算。此外, ISR應該是短而有效率的,在 ISR中做浮點運算是不明智的。?; 與第三點一脈相承,printf()經常有重入和性能上的問題。如果你丟掉了第三和第四點,我不會太為難你的。不用說,如果你能得到后兩點,那么你的被雇用前景越來越光明了。*****代碼例子( Code examples)12 . 下面的代碼輸出是什么,為什么?void foo(void){unsigned int a = 6;int b = -20;(a+b > 6) ? puts("> 6") : puts("<= 6");}這個問題測試你是否懂得 C語言中的整數自動轉換原則,我發現有些開發者懂得極少這些東西。不管如何,這無符號整型問題的答案是輸出是">6"。原因是當表達式中存在有符號類型和無符號類型時所有的操作數都自動轉換為無符號類型。 因此-20變成了一個非常大的正整數,所以該表達式計算出的結果大于 6。這一點對于應當頻繁用到無符號數據類型的嵌入式系統來說是豐常重要的。如果你答錯了這個問題,你也就到了得不到這份工作的邊緣。13. 評價下面的代碼片斷:unsigned int zero = 0;unsigned int compzero = 0xFFFF;/*1's complement of zero */對于一個 int型不是 16位的處理器為說,上面的代碼是不正確的。應編寫如下:unsigned int compzero = ~0;這一問題真正能揭露出應試者是否懂得處理器字長的重要性。在我的經驗里,好的嵌入式程序員非常準確地明白硬件的細節和它的局限,然而PC 機程序往往把硬件作為一個無法避免的煩惱。到了這個階段,應試者或者完全垂頭喪氣了或者信心滿滿志在必得。如果顯然應試者不是很好,那么這個測試就在這里結束了。但如果顯然應試者做得不錯,那么我就扔出下面的追加問題,這些問題是比較難的,我想僅僅非常優秀的應試者能做得不錯。提出這些問題,我希望更多看到應試者應付問題的方法,而不是答案。不管如何,你就當是這個娛樂吧...21動態內存分配( Dynamic memory allocation)14. 盡管不像非嵌入式計算機那么常見,嵌入式系統還是有從堆(heap)中動態分配內存的過程的。那么嵌入式系統中,動態分配內存可能發生的問題是什么?這里,我期望應試者能提到內存碎片,碎片收集的問題,變量的持行時間等等。這個主題已經在 ESP雜志中被廣泛地討論過了(主要是 P.J.Plauger, 他的解釋遠遠超過我這里能提到的任何解釋),所有回過頭看一下這些雜志吧!讓應試者進入一種虛假的安全感覺后,我拿出這么一個小節目:下面的代碼片段的輸出是什么,為什么?char *ptr;if ((ptr = (char *)malloc(0)) ==NULL)elseputs("Got a null pointer");puts("Got a valid pointer");這是一個有趣的問題。最近在我的一個同事不經意把 0值傳給了函數 malloc,得到了一個合法的指針之后,我才想到這個問題。這就是上面的代碼,該代碼的輸出是"Got a valid pointer"。我用這個來開始討論這樣的一問題,看看被面試者是否想到庫例程這樣做是正確。得到正確的答案固然重要,但解決問題的方法和你做決定的基本原理更重要些。Typedef:15 Typedef 在 C語言中頻繁用以聲明一個已經存在的數據類型的同義字。也可以用預處理器做類似的事。例如,思考一下下面的例子:#define dPS struct s *typedef struct s * tPS;以上兩種情況的意圖都是要定義 dPS和 tPS作為一個指向結構 s指針。哪種方法更好呢?(如果有的話)為什么?這是一個非常微妙的問題,任何人答對這個問題(正當的原因)是應當被恭喜的。答案是: typedef更好。思考下面的例子:dPS p1,p2;tPS p3,p4;第一個擴展為struct s * p1, p2;.上面的代碼定義 p1為一個指向結構的指, p2為一個實際的結構,這也許不是你想要的。第二個例子正確地定義了 p3和 p4兩個指針?;逎恼Z法16 . C 語言同意一些令人震驚的結構,下面的結構是合法的嗎,如果是它做些什么?int a = 5, b = 7, c;c = a+++b;22這個問題將做為這個測驗的一個愉快的結尾。不管你相不相信,上面的例子是完全合乎語法的。問題是編譯器如何處理它?水平不高的編譯作者實際上會爭論這個問題,根據最處理原則,編譯器應當能處理盡可能所有合法的用法。因此,上面的代碼被處理成:c = a++ + b;因此,這段代碼持行后 a = 6, b = 7, c = 12。如果你知道答案,或猜出正確答案,做得好。如果你不知道答案,我也不把這個當作問題。我發現這個問題的最大好處是這是一個關于代碼編寫風格,代碼的可讀性,代碼的可修改性的好的話題。好了,伙計們,你現在已經做完所有的測試了。這就是我出的 C語言測試題,我懷著愉快的心情寫完它,希望你以同樣的心情讀完它。如果是認為這是一個好的測試,那么盡量都用到你的找工作的過程中去吧。天知道也許過個一兩年,我就不做現在的工作,也需要找一個。Nigel Jones 是一個顧問,現在住在Maryland,當他不在水下時,你能在多個范圍的嵌入項目中找到他。 他很高興能收到讀者的來信,他的email 地址是: NAJones@compuserve.com。References?; Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.?; Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66C 語言嵌入式系統編程修煉C 語言嵌入式系統編程修煉之一:背景篇作者:宋寶華 更新日期:2005-08-30來源:yesky.com不同于一般形式的軟件編程,嵌入式系統編程建立在特定的硬件平臺上,勢必要求其編程語言具備較強的硬件直接操作能力。無疑,匯編語言具備這樣的特質。但是,歸因于匯編語言開發過程的復雜性,它并不是嵌入式系統開發的一般選擇。而與之相比,C 語言--一種"高級的低級"語言,則成為嵌入式系統開發的最佳選擇。筆者在嵌入式系統項目的開發過程中,一次又一次感受到 C 語言的精妙,沉醉于 C 語言給嵌入式開發帶來的便利。圖 1 給出了本文的討論所基于的硬件平臺,實際上,這也是大多數嵌入式系統的硬件平臺。它包括兩部分:(1) 以通用處理器為中心的協議處理模塊,用于網絡控制協議的處理;(2) 以數字信號處理器(DSP)為中心的信號處理模塊,用于調制、解調和數/模信號轉換。本文的討論主要圍繞以通用處理器為中心的協議處理模塊進行,因為它更多地牽涉到具體的 C 語言編程技巧。而 DSP編程則重點關注具體的數字信號處理算法,主要涉及通信領域的知識,不是本文的討論重點。23著眼于討論普遍的嵌入式系統 C 編程技巧,系統的協議處理模塊沒有選擇特別的 CPU,而是選擇了眾所周知的 CPU 芯片--80186,每一位學習過《微機原理》的讀者都應該對此芯片有一個基本的認識,且對其指令集比較熟悉。80186 的字長是 16 位,可以尋址到的內存空間為 1MB,只有實地址模式。C 語言編譯生成的指針為 32 位(雙字),高 16 位為段地址,低16 位為段內偏移,一段最多 64KB。圖 1 系統硬件架構協議處理模塊中的 Flash 和 RAM 幾乎是每個嵌入式系統的必備設備,前者用于存儲程序,后者則是程序運行時指令及數據的存放位置。系統所選擇的 FLASH 和 RAM 的位寬都為 16 位,與 CPU 一致。實時鐘芯片可以為系統定時,給出當前的年、月、日及具體時間(小時、分、秒及毫秒),可以設定其經過一段時間即向 CPU 提出中斷或設定報警時間到來時向 CPU 提出中斷(類似鬧鐘功能)。NVRAM(非易失去性 RAM)具有掉電不丟失數據的特性,可以用于保存系統的設置信息,譬如網絡協議參數等。在系統掉電或重新啟動后,仍然可以讀取先前的設置信息。其位寬為 8 位,比 CPU 字長小。文章特意選擇一個與 CPU 字長不一致的存儲芯片,為后文中一節的討論創造條件。UART 則完成 CPU 并行數據傳輸與 RS-232 串行數據傳輸的轉換,它可以在接收到[1~MAX_BUFFER]字節后向 CPU 提出中斷,MAX_BUFFER 為 UART 芯片存儲接收到字節的最大緩沖區。鍵盤控制器和顯示控制器則完成系統人機界面的控制。以上提供的是一個較完備的嵌入式系統硬件架構,實際的系統可能包含更少的外設。之所以選擇一個完備的系統,是為了后文更全面的討論嵌入式系統 C 語言編程技巧的方方面面,所有設備都會成為后文的分析目標。嵌入式系統需要良好的軟件開發環境的支持,由于嵌入式系統的目標機資源受限,不可能在其上建立龐大、復雜的開發環境,因而其開發環境和目標運行環境相互分離。因此,嵌入式應用軟件的開發方式一般是,在宿主機(Host)上建立開發環境,進行應用程序編碼和交叉編譯,然后宿主機同目標機(Target)建立連接,將應用程序下載到目標機上進行交叉調試,經過調試和優化,最后將應用程序固化到目標機中實際運行。CAD-UL 是適用于 x86 處理器的嵌入式應用軟件開發環境,它運行在 Windows 操作系統之上,可生成 x86 處理器的目標24代碼并通過 PC 機的 COM 口(RS-232 串口)或以太網口下載到目標機上運行,如圖 2。其駐留于目標機 FLASH 存儲器中的monitor 程序可以監控宿主機 Windows 調試平臺上的用戶調試指令,獲取 CPU 寄存器的值及目標機存儲空間、I/O 空間的內容。圖 2 交叉開發環境后續章節將從軟件架構、內存操作、屏幕操作、鍵盤操作、性能優化等多方面闡述 C 語言嵌入式系統的編程技巧。軟件架構是一個宏觀概念,與具體硬件的聯系不大;內存操作主要涉及系統中的 FLASH、 RAM 和 NVRAM 芯片;屏幕操作則涉及顯示控制器和實時鐘;鍵盤操作主要涉及鍵盤控制器;性能優化則給出一些具體的減小程序時間、空間消耗的技巧。在我們的修煉旅途中將經過 25 個關口,這些關口主分為兩類,一類是技巧型,有很強的適用性;一類則是常識型,在理論上有些意義。C 語言嵌入式系統編程修煉之二:軟件架構篇作者:宋寶華 更新日期:2005-07-22模塊劃分模塊劃分的"劃"是規劃的意思, 意指怎樣合理的將一個很大的軟件劃分為一系列功能獨立的部分合作完成系統的需求。C 語言作為一種結構化的程序設計語言,在模塊的劃分上主要依據功能(依功能進行劃分在面向對象設計中成為一個錯誤,牛頓定律遇到了>相對論),C 語言模塊化程序設計需理解如下概念:(1) 模塊即是一個.c 文件和一個.h 文件的結合,頭文件(.h)中是對于該模塊接口的聲明;(2) 某模塊提供給其它模塊調用的外部函數及數據需在.h 中文件中冠以 extern 關鍵字聲明;(3) 模塊內的函數和全局變量需在.c 文件開頭冠以 static 關鍵字聲明;(4) 永遠不要在.h 文件中定義變量!定義變量和聲明變量的區別在于定義會產生內存分配的操作,是匯編階段的概念;而聲明則只是告訴包含該聲明的模塊在連接階段從其它模塊尋找外部函數和變量。如:/*module1.h*/int a = 5; /* 在模塊 1 的.h 文件中定義 int a *//*module1 .c*/#include "module1.h" /* 在模塊 1 中包含模塊 1 的.h 文件 */25/*module2 .c*/#include "module1.h" /* 在模塊 2 中包含模塊 1 的.h 文件 *//*module3 .c*/#include "module1.h" /* 在模塊 3 中包含模塊 1 的.h 文件 */以上程序的結果是在模塊 1、2、3 中都定義了整型變量 a,a 在不同的模塊中對應不同的地址單元,這個世界上從來不需要這樣的程序。正確的做法是:/*module1.h*/extern int a; /* 在模塊 1 的.h 文件中聲明 int a *//*module1 .c*/#include "module1.h" /* 在模塊 1 中包含模塊 1 的.h 文件 */int a = 5; /* 在模塊 1 的.c 文件中定義 int a *//*module2 .c*/#include "module1.h" /* 在模塊 2 中包含模塊 1 的.h 文件 *//*module3 .c*/#include "module1.h" /* 在模塊 3 中包含模塊 1 的.h 文件 */這樣如果模塊 1、2、3 操作 a 的話,對應的是同一片內存單元。一個嵌入式系統通常包括兩類模塊:(1)硬件驅動模塊,一種特定硬件對應一個模塊;(2)軟件功能模塊,其模塊的劃分應滿足低偶合、高內聚的要求。多任務還是單任務所謂"單任務系統"是指該系統不能支持多任務并發操作,宏觀串行地執行一個任務。而多任務系統則可以宏觀并行(微觀上可能串行)地"同時"執行多個任務。多任務的并發執行通常依賴于一個多任務操作系統 (OS), 多任務 OS 的核心是系統調度器, 它使用任務控制塊 (TCB)來管理任務調度功能。TCB 包括任務的當前狀態、優先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。調度器在任務被激活時,要用到這些信息。此外,TCB 還被用來存放任務的"上下文"(context)。任務的上下文就是當一個執行中的任務被停止時,所要保存的所有信息。通常,上下文就是計算機當前的狀態,也即各個寄存器的內容。 當發生任務切換時, 當前運行的任務的上下文被存入 TCB, 并將要被執行的任務的上下文從它的 TCB 中取出,放入各個寄存器中。嵌入式多任務 OS 的典型例子有 Vxworks、ucLinux 等。嵌入式 OS 并非遙不可及的神壇之物,我們可以用不到 100026行代碼實現一個針對 80186 處理器的功能最簡單的 OS 內核,作者正準備進行此項工作,希望能將心得貢獻給大家。究竟選擇多任務還是單任務方式,依賴于軟件的體系是否龐大。例如,絕大多數手機程序都是多任務的,但也有一些小靈通的協議棧是單任務的,沒有操作系統,它們的主程序輪流調用各個軟件模塊的處理程序,模擬多任務環境。單任務程序典型架構(1)從 CPU 復位時的指定地址開始執行;(2)跳轉至匯編代碼 startup 處執行;(3)跳轉至用戶主程序 main 執行,在 main 中完成:a.初試化各硬件設備;b.初始化各軟件模塊;c.進入死循環(無限循環),調用各模塊的處理函數用戶主程序和各模塊的處理函數都以 C 語言完成。用戶主程序最后都進入了一個死循環,其首選方案是:while(1){}有的程序員這樣寫:for(;;){}這個語法沒有確切表達代碼的含義,我們從 for(;;)看不出什么,只有弄明白 for(;;)在 C 語言中意味著無條件循環才明白其意。下面是幾個"著名"的死循環:(1)操作系統是死循環;(2)WIN32 程序是死循環;(3)嵌入式系統軟件是死循環;(4)多線程程序的線程處理函數是死循環。你可能會辯駁,大聲說:"凡事都不是絕對的,2、3、4 都可以不是死循環"。Yes,you are right,但是你得不到鮮27花和掌聲。實際上,這是一個沒有太大意義的牛角尖,因為這個世界從來不需要一個處理完幾個消息就喊著要 OS 殺死它的WIN32 程序,不需要一個剛開始 RUN 就自行了斷的嵌入式系統,不需要莫名其妙啟動一個做一點事就干掉自己的線程。有時候,過于嚴謹制造的不是便利而是麻煩。君不見,五層的 TCP/IP 協議棧超越嚴謹的 ISO/OSI 七層協議棧大行其道成為事實上的標準?經常有網友討論:printf("%d,%d",++i,i++); /* 輸出是什么?*/c = a+++b; /* c=? */等類似問題。面對這些問題,我們只能發出由衷的感慨:世界上還有很多有意義的事情等著我們去消化攝入的食物。實際上,嵌入式系統要運行到世界末日。中斷服務程序中斷是嵌入式系統中重要的組成部分,但是在標準 C 中不包含中斷。許多編譯開發商在標準 C 上增加了對中斷的支持,提供新的關鍵字用于標示中斷服務程序 (ISR),類似于__interrupt、#program interrupt 等。當一個函數被定義為 ISR的時候,編譯器會自動為該函數增加中斷服務程序所需要的中斷現場入棧和出棧代碼。中斷服務程序需要滿足如下要求:(1)不能返回值;(2)不能向 ISR 傳遞參數;(3) ISR 應該盡可能的短小精悍;(4) printf(char * lpFormatString,…)函數會帶來重入和性能問題,不能在 ISR 中采用。在某項目的開發中,我們設計了一個隊列,在中斷服務程序中,只是將中斷類型添加入該隊列中,在主程序的死循環中不斷掃描中斷隊列是否有中斷,有則取出隊列中的第一個中斷類型,進行相應處理。/* 存放中斷的隊列 */typedef struct tagIntQueue{int intType; /* 中斷類型 */struct tagIntQueue *next;}IntQueue;IntQueue lpIntQueueHead;__interrupt ISRexample (){28int intType;intType = GetSystemType();QueueAddTail(lpIntQueueHead, intType);/* 在隊列尾加入新的中斷 */}在主程序循環中判斷是否有中斷:While(1){If( !IsIntQueueEmpty() ){intType = GetFirstInt();switch(intType) /* 是不是很象 WIN32 程序的消息解析函數? */{/* 對,我們的中斷類型解析很類似于消息驅動 */case xxx: /* 我們稱其為"中斷驅動"吧? */…break;case xxx:…break;…}}}按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執行了。硬件驅動模塊一個硬件驅動模塊通常應包括如下函數:(1)中斷服務程序 ISR(2)硬件初始化a.修改寄存器,設置硬件參數(如 UART 應設置其波特率,AD/DA 設備應設置其采樣速率等);b.將中斷服務程序入口地址寫入中斷向量表:/* 設置中斷向量表 */m_myPtr = make_far_pointer(0l); /* 返回 void far 型指針 void far * */m_myPtr += ITYPE_UART; /* ITYPE_UART: uart 中斷服務程序 *//* 相對于中斷向量表首地址的偏移 */29*m_myPtr = &UART _Isr; /* UART _Isr:UART 的中斷服務程序 */(3)設置 CPU 針對該硬件的控制線a.如果控制線可作 PIO(可編程 I/O)和控制信號用,則設置 CPU 內部對應寄存器使其作為控制信號;b.設置 CPU 內部的針對該設備的中斷屏蔽位,設置中斷方式(電平觸發還是邊緣觸發)。( 4)提供一系列針對該設備的操作接口函數。例如,對于 LCD,其驅動模塊應提供繪制像素、畫線、繪制矩陣、顯示字符點陣等函數;而對于實時鐘,其驅動模塊則需提供獲取時間、設置時間等函數。C 的面向對象化在面向對象的語言里面,出現了類的概念。類是對特定數據的特定操作的集合體。類包含了兩個范疇:數據和操作。而 C 語言中的 struct 僅僅是數據的集合,我們可以利用函數指針將 struct 模擬為一個包含數據和操作的"類"。下面的 C程序模擬了一個最簡單的"類":#ifndef C_Class#define C_Class struct#endifC_Class A{C_Class A *A_this; /* this 指針 */void (*Foo)(C_Class A *A_this); /* 行為:函數指針 */int a; /* 數據 */int b;};我們可以利用 C 語言模擬出面向對象的三個特性:封裝、繼承和多態,但是更多的時候,我們只是需要將數據與行為封裝以解決軟件結構混亂的問題。C 模擬面向對象思想的目的不在于模擬行為本身,而在于解決某些情況下使用 C 語言編程時程序整體框架結構分散、數據和函數脫節的問題。我們在后續章節會看到這樣的例子??偨Y本篇介紹了嵌入式系統編程軟件架構方面的知識,主要包括模塊劃分、多任務還是單任務選取、單任務程序典型架構、中斷服務程序、硬件驅動模塊設計等,從宏觀上給出了一個嵌入式系統軟件所包含的主要元素。請記?。很浖Y構是軟件的靈魂!結構混亂的程序面目可憎,調試、測試、維護、升級都極度困難。小力力力 2005-09-21 17:2930C 語言嵌入式系統編程修煉之三:內存操作作者:宋寶華 更新日期:2005-07-22數據指針在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的 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 語言指針以指向的數據類型長度作自增和自減。理解這一點對于以指針直接操作內存是相當重要的。函數指針首先要理解以下三個問題:31(1)C 語言中函數名直接對應于函數生成的指令代碼在內存中的地址,因此函數名可以直接賦給指向函數的指針;(2)調用函數實際上等同于"調轉指令+參數傳遞處理+回歸位置入棧",本質上最核心的操作是將函數生成的目標代碼的首地址賦給 CPU 的 PC 寄存器;(3)因為函數調用的本質是跳轉到某一個地址單元的 code 去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:請拿出你可以獲得的任何一本大學《微型計算機原理》教材,書中講到,186 CPU 啟動后跳轉至絕對地址 0xFFFF0(對應 C 語言指針是 0xF000FFF0,0xF000 為段地址,0xFFF0 為段內偏移)執行,請看下面的代碼:typedef void (*lpFunction) ( ); /* 定義一個無參數、無返回類型的函數指針類型*//* 定義一個函數指針,指向 CPU 啟動后所執行第一條指令的位置*/lpFunction lpReset = (lpFunction)0xF000FFF0;lpReset(); /* 調用函數 */在以上的程序中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟重啟"的作用,跳轉到 CPU 啟動后第一條要執行的指令的位置。記?。?函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!數組 vs.動態申請在嵌入式系統中動態內存申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的內存空間往往是十分有限的,不經意的內存泄露會很快導致系統的崩潰。所以一定要保證你的 malloc 和 free 成對出現,如果你寫出這樣的一段程序:char * function(void){char *p;p = (char *)malloc(…);if(p==NULL)…;… /* 一系列針對 p 的操作 */return p;}在某處調用 function(),用完 function 中動態申請的內存后將其 free,如下:32char *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 應成對出現!關鍵字 constconst 意味著"只讀"。區別如下代碼的功能非常重要,也是老生長嘆,如果你還不知道它們的區別,而且已經在程序界摸爬滾打多年,那只能說這是一個悲哀:const int a;int const a;const int *a;int * const a;33int const * a const;(1)關鍵字 const 的作用是為給讀你代碼的人傳達非常有用的信息。例如,在函數的形參前添加 const 關鍵字意味著這個參數在函數體內不會被修改,屬于"輸入參數"。在有多個形參的時候,函數的調用者可以憑借參數前是否有 const 關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。(2)合理地使用關鍵字 const 可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少 bug 的出現。const 在 C++語言中則包含了更豐富的含義,而在 C 語言中僅意味著:"只能讀的普通變量",可以稱其為"不能改變的變量"(這個說法似乎很拗口,但卻最準確的表達了 C 語言中 const 的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在 C 語言中如下程序是非法的:const int SIZE = 10;char a[SIZE]; /* 非法:編譯階段不能用到變量 */關鍵字 volatileC 語言編譯器會對用戶書寫的代碼進行優化,譬如如下代碼: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 int a;volatile 變量可能用于如下幾種情況:(1) 并行設備的硬件寄存器(如:狀態寄存器,例中的代碼屬于此類);34(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,欲寫入的字節*/35extern 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 為單位前進!圖 1 CPU 與 NVRAM 地址線連接子貢再問:So why 80186 的地址線 A0 不與 NVRAM 的 A0 連接?子曰:請看《IT 論語》之《微機原理篇》,那里面講述了關于計算機組成的圣人之道??偨Y本篇主要講述了嵌入式系統 C 編程中內存操作的相關技巧。掌握并深入理解關于數據指針、函數指針、動態申請內存、const 及 volatile 關鍵字等的相關知識, 是一個優秀的 C 語言程序設計師的基本要求。 當我們已經牢固掌握了上述技巧后,我們就已經學會了 C 語言的 99%,因為 C 語言最精華的內涵皆在內存操作中體現。我們之所以在嵌入式系統中使用 C 語言進行程序設計,99%是因為其強大的內存操作能力!如果你愛編程,請你愛 C 語言;如果你愛 C 語言,請你愛指針;36如果你愛指針,請你愛指針的指針!C 語言嵌入式系統編程修煉之四:屏幕操作作者:宋寶華 更新日期:2005-07-22漢字處理現在要解決的問題是,嵌入式系統中經常要使用的并非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的 LCD 上沒有必要提供顯示"電子郵件"的功能;一個提供漢字顯示功能的空調的 LCD 上不需要顯示一條"短消息",諸如此類。但是一部手機、小靈通則通常需要包括較完整的漢字庫。如果包括的漢字庫較完整,那么,由內碼計算出漢字字模在庫中的偏移是十分簡單的:漢字庫是按照區位的順序排列的,前一個字節為該漢字的區號,后一個字節為該字的位號。每一個區記錄 94 個漢字,位號則為該字在該區中的位置。因此,漢字在漢字庫中的具體位置計算公式為:94*(區號-1)+位號-1。減 1 是因為數組是以 0 為開始而區號位號是以 1 為開始的。只需乘上一個漢字字模占用的字節數即可,即:(94*(區號-1)+位號-1)*一個漢字字模占用字節數,以 16*16 點陣字庫為例,計算公式則為:(94*(區號-1)+(位號-1))*32。漢字庫中從該位置起的 32 字節信息記錄了該字的字模信息。對于包含較完整漢字庫的系統而言,我們可以以上述規則計算字模的位置。但是如果僅僅是提供少量漢字呢?譬如幾十至幾百個?最好的做法是:定義宏:# define EX_FONT_CHAR(value)# define EX_FONT_UNICODE_VAL(value) (value),# define EX_FONT_ANSI_VAL(value) (value),定義結構體:typedef struct _wide_unicode_font16x16{WORD value; /* 內碼 */BYTE data[32]; /* 字模點陣 */}Unicode;#define CHINESE_CHAR_NUM … /* 漢字數量 */字模的存儲用數組:Unicode chinese[CHINESE_CHAR_NUM] ={{EX_FONT_CHAR("業")EX_FONT_UNICODE_VAL(0x4e1a)37{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50,0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}},{EX_FONT_CHAR("中")EX_FONT_UNICODE_VAL(0x4e2d){0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,0x21, 0x08,0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}},{EX_FONT_CHAR("云")EX_FONT_UNICODE_VAL(0x4e91){0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00,0x07, 0x00,0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}},{EX_FONT_CHAR("件")EX_FONT_UNICODE_VAL(0x4ef6){0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40,0x2f, 0xfe,0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}}}要顯示特定漢字的時候,只需要從數組中查找內碼與要求漢字內碼相同的即可獲得字模。如果前面的漢字在數組中以內碼大小順序排列,那么可以以二分查找法更高效的查找到漢字的字模。這是一種很有效的組織小漢字庫的方法,它可以保證程序有很好的結構。系統時間顯示從 NVRAM 中可以讀取系統的時間,系統一般借助 NVRAM 產生的秒中斷每秒讀取一次當前時間并在 LCD 上顯示。關于時間的顯示,有一個效率問題。因為時間有其特殊性,那就是 60 秒才有一次分鐘的變化,60 分鐘才有一次小時變化,如果我們每次都將讀取的時間在屏幕上完全重新刷新一次,則浪費了大量的系統時間。一個較好的辦法是我們在時間顯示函數中以靜態變量分別存儲小時、分鐘、秒,只有在其內容發生變化的時候才更新其顯示。extern void DisplayTime(…){38static BYTE byHour,byMinute,bySecond;BYTE byNewHour, byNewMinute, byNewSecond;byNewHour = GetSysHour();byNewMinute = GetSysMinute();byNewSecond = GetSysSecond();if(byNewHour!= byHour){… /* 顯示小時 */byHour = byNewHour;}if(byNewMinute!= byMinute){… /* 顯示分鐘 */byMinute = byNewMinute;}if(byNewSecond!= bySecond){… /* 顯示秒鐘 */bySecond = byNewSecond;}}這個例子也可以順便作為 C 語言中 static 關鍵字強大威力的證明。當然,在 C++語言里,static 具有了更加強大的威力,它使得某些數據和函數脫離"對象"而成為"類"的一部分,正是它的這一特點,成就了軟件的無數優秀設計。動畫顯示動畫是無所謂有,無所謂無的,靜止的畫面走的路多了,也就成了動畫。隨著時間的變更,在屏幕上顯示不同的靜止畫面,即是動畫之本質。所以,在一個嵌入式系統的 LCD 上欲顯示動畫,必須借助定時器。沒有硬件或軟件定時器的世界是無法想像的:(1) 沒有定時器,一個操作系統將無法進行時間片的輪轉,于是無法進行多任務的調度,于是便不再成其為一個多任務操作系統;(2) 沒有定時器,一個多媒體播放軟件將無法運作,因為它不知道何時應該切換到下一幀畫面;(3) 沒有定時器,一個網絡協議將無法運轉,因為其無法獲知何時包傳輸超時并重傳之,無法在特定的時間完成特定的任務。因此,沒有定時器將意味著沒有操作系統、沒有網絡、沒有多媒體,這將是怎樣的黑暗?所以,合理并靈活地使用各種定時器,是對一個軟件人的最基本需求!在 80186 為主芯片的嵌入式系統中,我們需要借助硬件定時器的中斷來作為軟件定時器,在中斷發生后變更畫面的顯示內容。在時間顯示"xx:xx"中讓冒號交替有無,每次秒中斷發生后,需調用 ShowDot:39void ShowDot(){static BOOL bShowDot = TRUE; /* 再一次領略 static 關鍵字的威力 */if(bShowDot){showChar(’:’,xPos,yPos);}else{showChar(’ ’,xPos,yPos);}bShowDot = ! bShowDot;}菜單操作無數人為之絞盡腦汁的問題終于出現了,在這一節里,我們將看到,在 C 語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!筆者曾經是個笨蛋,被菜單搞暈了,給出這樣的一個系統:圖 1 菜單范例要求以鍵盤上的"← →"鍵切換菜單焦點,當用戶在焦點處于某菜單時,若敲擊鍵盤上的 OK、CANCEL 鍵則調用該焦點菜單對應之處理函數。我曾經傻傻地這樣做著:/* 按下 OK 鍵 */void onOkKey(){/* 判斷在什么焦點菜單上按下 Ok 鍵,調用相應處理函數 */Switch(currentFocus){case MENU1:menu1OnOk();break;case MENU2:menu2OnOk();break;…}}40/* 按下 Cancel 鍵 */void onCancelKey(){/* 判斷在什么焦點菜單上按下 Cancel 鍵,調用相應處理函數 */Switch(currentFocus){case MENU1:menu1OnCancel();break;case MENU2:menu2OnCancel();break;…}}終于有一天,我這樣做了:/* 將菜單的屬性和操作"封裝"在一起 */typedef struct tagSysMenu{char *text; /* 菜單的文本 */BYTE xPos; /* 菜單在 LCD 上的 x 坐標 */BYTE yPos; /* 菜單在 LCD 上的 y 坐標 */void (*onOkFun)(); /* 在該菜單上按下 ok 鍵的處理函數指針 */void (*onCancelFun)(); /* 在該菜單上按下 cancel 鍵的處理函數指針 */}SysMenu, *LPSysMenu;當我定義菜單時,只需要這樣:static SysMenu menu[MENU_NUM] ={{"menu1", 0, 48, menu1OnOk, menu1OnCancel},{" menu2", 7, 48, menu2OnOk, menu2OnCancel},{" menu3", 7, 48, menu3OnOk, menu3OnCancel}41,{" menu4", 7, 48, menu4OnOk, menu4OnCancel}…};OK 鍵和 CANCEL 鍵的處理變成:/* 按下 OK 鍵 */void onOkKey(){menu[currentFocusMenu].onOkFun();}/* 按下 Cancel 鍵 */void onCancelKey(){menu[currentFocusMenu].onCancelFun();}程序被大大簡化了,也開始具有很好的可擴展性!我們僅僅利用了面向對象中的封裝思想,就讓程序結構清晰,其結果是幾乎可以在無需修改程序的情況下在系統中添加更多的菜單,而系統的按鍵處理函數保持不變。面向對象,真神了!模擬 MessageBox 函數MessageBox 函數,這個 Windows 編程中的超級猛料,不知道是多少入門者第一次用到的函數。還記得我們第一次在Windows 中利用 MessageBox 輸出 "Hello,World!"對話框時新奇的感覺嗎?無法統計,這個世界上究竟有多少程序員學習Windows 編程是從 MessageBox ("Hello,World!",…)開始的。在我本科的學校,廣泛流傳著一個詞匯,叫做"’Hello,World’級程序員",意指入門級程序員,但似乎"’Hello,World’級"這個說法更搞笑而形象。圖 2 經典的 Hello,World!圖 2 給出了兩種永恒經典的 Hello,World 對話框,一種只具有"確定",一種則包含"確定"、"取消"。是的,MessageBox的確有,而且也應該有兩類!這完全是由特定的應用需求決定的。嵌入式系統中沒有給我們提供 MessageBox,但是鑒于其功能強大,我們需要模擬之,一個模擬的 MessageBox 函數為:42/******************************************/* 函數名稱: MessageBox/* 功能說明: 彈出式對話框,顯示提醒用戶的信息/* 參數說明: lpStr --- 提醒用戶的字符串輸出信息/* TYPE --- 輸出格式(ID_OK = 0, ID_OKCANCEL = 1)/* 返回值: 返回對話框接收的鍵值,只有兩種 KEY_OK, KEY_CANCEL/******************************************typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE){BYTE keyValue = -1;ClearScreen(); /* 清除屏幕 */DisplayString(xPos,yPos,lpStr,TRUE); /* 顯示字符串 *//* 根據對話框類型決定是否顯示確定、取消 */switch (TYPE){case ID_OK:DisplayString(13,yPos+High+1, " 確定 ", 0);break;case ID_OKCANCEL:DisplayString(8, yPos+High+1, " 確定 ", 0);DisplayString(17,yPos+High+1, " 取消 ", 0);break;default:break;}DrawRect(0, 0, 239, yPos+High+16+4); /* 繪制外框 *//* MessageBox 是模式對話框,阻塞運行,等待按鍵 */while( (keyValue != KEY_OK) || (keyValue != KEY_CANCEL) ){keyValue = getSysKey();}/* 返回按鍵類型 */if(keyValue== KEY_OK){return ID_OK;}else{return ID_CANCEL;}}43上述函數與我們平素在 VC++等中使用的 MessageBox 是何等的神似???實現這個函數,你會看到它在嵌入式系統中的妙用是無窮的??偨Y本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統屏幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被 LCD 上凌亂不堪的顯示內容所困擾。屏幕乃嵌入式系統生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不系統、最混亂的部分,筆者曾深受其害。C 語言嵌入式系統編程修煉之五:鍵盤操作作者:宋寶華 更新日期:2005-07-22處理功能鍵功能鍵的問題在于,用戶界面并非固定的,用戶功能鍵的選擇將使屏幕畫面處于不同的顯示狀態下。例如,主畫面如圖 1:圖 1 主畫面當用戶在設置 XX 上按下 Enter 鍵之后,畫面就切換到了設置 XX 的界面,如圖 2:圖 2 切換到設置 XX 畫面程序如何判斷用戶處于哪一畫面,并在該畫面的程序狀態下調用對應的功能鍵處理函數,而且保證良好的結構,是一個值得思考的問題。讓我們來看看 WIN32 編程中用到的"窗口"概念,當消息(message)被發送給不同窗口的時候,該窗口的消息處理函數44(是一個 callback 函數)最終被調用, 而在該窗口的消息處理函數中, 又根據消息的類型調用了該窗口中的對應處理函數。通過這種方式,WIN32 有效的組織了不同的窗口,并處理不同窗口情況下的消息。我們從中學習到的就是:(1)將不同的畫面類比為 WIN32 中不同的窗口,將窗口中的各種元素(菜單、按鈕等)包含在窗口之中;(2)給各個畫面提供一個功能鍵"消息"處理函數,該函數接收按鍵信息為參數;(3)在各畫面的功能鍵"消息"處理函數中,判斷按鍵類型和當前焦點元素,并調用對應元素的按鍵處理函數。/* 將窗口元素、消息處理函數封裝在窗口中 */struct windows{BYTE currentFocus;ELEMENT element[ELEMENT_NUM];void (*messageFun) (BYTE keyValue);…};/* 消息處理函數 */void messageFunction(BYTE keyValue){BYTE i = 0;/* 獲得焦點元素 */while ( (element [i].ID!= currentFocus)&& (i < ELEMENT_NUM) ){i++;}/* "消息映射" */if(i < ELEMENT_NUM){switch(keyValue){case OK:element[i].OnOk();break;…}}}在窗口的消息處理函數中調用相應元素按鍵函數的過程類似于"消息映射",這是我們從 WIN32 編程中學習到的。編程到了一個境界,很多東西都是相通的了。其它地方的思想可以拿過來為我所用,是為編程中的"拿來主義"。45在這個例子中,如果我們還想玩得更大一點,我們可以借鑒 MFC 中處理 MESSAGE_MAP 的方法,我們也可以學習 MFC 定義幾個精妙的宏來實現"消息映射"。處理數字鍵用戶輸入數字時是一位一位輸入的,每一位的輸入都對應著屏幕上的一個顯示位置(x 坐標,y 坐標)。此外,程序還需要記錄該位置輸入的值,所以有效組織用戶數字輸入的最佳方式是定義一個結構體,將坐標和數值捆綁在一起:/* 用戶數字輸入結構體 */typedef struct tagInputNum{BYTE byNum; /* 接收用戶輸入賦值 */BYTE xPos; /* 數字輸入在屏幕上的顯示位置 x 坐標 */BYTE yPos; /* 數字輸入在屏幕上的顯示位置 y 坐標 */}InputNum, *LPInputNum;那么接收用戶輸入就可以定義一個結構體數組,用數組中的各位組成一個完整的數字:InputNum inputElement[NUM_LENGTH]; /* 接收用戶數字輸入的數組 *//* 數字按鍵處理函數 */extern void onNumKey(BYTE num){if(num==0|| num==1) /* 只接收二進制輸入 */{/* 在屏幕上顯示用戶輸入 */DrawText(inputElement[currentElementInputPlace].xPos,inputElement[currentElementInputPlace].yPos, "%1d", num);/* 將輸入賦值給數組元素 */inputElement[currentElementInputPlace].byNum = num;/* 焦點及光標右移 */moveToRight();}}將數字每一位輸入的坐標和輸入值捆綁后,在數字鍵處理函數中就可以較有結構的組織程序,使程序顯得很緊湊。整理用戶輸入繼續第 2 節的例子,在第 2 節的 onNumKey 函數中, 只是獲取了數字的每一位,因而我們需要將其轉化為有效數據,譬如要轉化為有效的 XXX 數據,其方法是:/* 從 2 進制數據位轉化為有效數據:XXX */void convertToXXX(){46BYTE i;XXX = 0;for (i = 0; i < NUM_LENGTH; i++){XXX += inputElement[i].byNum*power(2, NUM_LENGTH - i - 1);}}反之,我們也可能需要在屏幕上顯示那些有效的數據位,因為我們也需要能夠反向轉化:/* 從有效數據轉化為 2 進制數據位:XXX */void convertFromXXX(){BYTE i;XXX = 0;for (i = 0; i < NUM_LENGTH; i++){inputElement[i].byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;}}當然在上面的例子中,因為數據是 2 進制的,用 power 函數不是很好的選擇,直接用"<< >>"移位操作效率更高,我們僅是為了說明問題的方便。 試想,如果用戶輸入是十進制的,power 函數或許是唯一的選擇了??偨Y本篇給出了鍵盤操作所涉及的各個方面:功能鍵處理、數字鍵處理及用戶輸入整理,基本上提供了一個全套的按鍵處理方案。對于功能鍵處理方法,將 LCD 屏幕與 Windows 窗口進行類比,提出了較新穎地解決屏幕、鍵盤繁雜交互問題的方案。計算機學的許多知識都具有相通性,因而,不斷追趕時髦技術而忽略基本功的做法是徒勞無意的。我們最多需要"精通"三種語言(精通,一個在如今的求職簡歷里泛濫成災的詞語),最佳拍檔是匯編、C、C++(或 java),很顯然,如果你"精通"了這三種語言,其它語言你應該是可以很快"熟悉"的,否則你就沒有"精通"它們.C 語言嵌入式系統編程修煉之六:性能優化作者:宋寶華 更新日期:2005-07-22使用宏定義在 C 語言中,宏是產生內嵌代碼的唯一方法。對于嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法。47寫一個"標準"宏 MIN ,這個宏輸入兩個參數并返回較小的一個:錯誤做法:#define MIN(A,B) ( A <= B ? A : B )正確做法:#define MIN(A,B) ( (A)<= (B) ? (A) : (B) )對于宏,我們需要知道三點:(1)宏定義"像"函數;(2)宏定義不是函數,因而需要括上所有"參數";(3)宏定義可能產生副作用。下面的代碼:least = MIN(*p++, b);將被替換為:( (*p++) <= (b) ?(*p++):(b) )發生的事情無法預料。因而不要給宏定義傳入有副作用的"參數"。使用寄存器變量當對一個變量頻繁被讀寫時,需要反復訪問內存,從而花費大量的存取時間。為此,C 語言提供了一種變量,即寄存器變量。這種變量存放在 CPU 的寄存器中,使用時,不需要訪問內存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的說明符是 register。對于循環次數較多的循環控制變量及循環體內反復使用的變量均可定義為寄存器變量,而循環計數是應用寄存器變量的最好候選者。(1) 只有局部自動變量和形參才可以定義為寄存器變量。因為寄存器變量屬于動態存儲方式,凡需要采用靜態存儲方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部 static 變量;(2) register 是一個"建議"型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因為條件不滿足并未成為寄存器變量,而是被放在了存儲器中,但編譯器中并不報錯(在 C++語言中有另一個"建議"型關鍵字:inline)。48下面是一個采用寄存器變量的例子:/* 求 1+2+3+….+n 的值 */WORD Addition(BYTE n){register i,s=0;for(i=1;i<=n;i++){s=s+i;}return s;}本程序循環 n 次,i 和 s 都被頻繁使用,因此可定義為寄存器變量。內嵌匯編程序中對時間要求苛刻的部分可以用內嵌匯編來重寫,以帶來速度上的顯著提高。但是,開發和測試匯編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用匯編的部分。在程序中,存在一個 80-20 原則,即 20%的程序消耗了 80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。嵌入式 C 程序中主要使用在線匯編,即在 C 程序中直接插入_asm{ }內嵌匯編語句:/* 把兩個輸入參數的值相加,結果存放到另外一個全局變量中 */int result;void Add(long a, long *b){_asm{MOV AX, aMOV BX, bADD AX, [BX]MOV result, AX}}利用硬件特性首先要明白 CPU 對各種存儲器的訪問速度,基本上是:CPU 內部 RAM > 外部同步 RAM > 外部異步 RAM > FLASH/ROM49對于程序代碼,已經被燒錄在 FLASH 或 ROM 中,我們可以讓 CPU 直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啟動后將 FLASH 或 ROM 中的目標代碼拷貝入 RAM 中后再執行以提高取指令速度;對于 UART 等設備,其內部有一定容量的接收 BUFFER,我們應盡量在 BUFFER 被占滿后再向 CPU 提出中斷。例如計算機終端在向目標機通過 RS-232 傳遞數據時, 不宜設置 UART 只接收到一個 BYTE 就向 CPU 提中斷, 從而無謂浪費中斷處理時間;如果對某設備能采取 DMA 方式讀取,就采用 DMA 讀取,DMA 讀取方式在讀取目標中包含的存儲信息較大時效率較高,其數據傳輸的基本單位是塊,而所傳輸的數據是從設備直接送入內存的(或者相反)。DMA 方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了 CPU 與外設的并行操作程度?;钣梦徊僮魇褂?C 語言的位操作可以減少除法和取模的運算。在計算機程序中數據的位是可以操作的最小數據單位,理論上可以用"位運算"來完成所有的運算和操作,因而,靈活的位操作可以有效地提高程序運行的效率。舉例如下:/* 方法 1 */int i,j;i = 879 / 16;j = 562 % 32;/* 方法 2 */int i,j;i = 879 >> 4;j = 562 - (562 >> 5 << 5);對于以 2 的指數次方為"*"、"/"或"%"因子的數學運算,轉化為移位運算"<< >>"通??梢蕴岣咚惴ㄐ?。因為乘除運算指令周期通常比移位運算大。C 語言位運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關系。我們通常要對硬件寄存器進行位設置,譬如,我們通過將 AM186ER 型 80186 處理器的中斷屏蔽控制寄存器的第低 6 位設置為 0(開中斷 2),最通用的做法是:#define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);outword(INT_MASK, wTemp &~INT_I2_MASK);而將該位設置為 1 的做法是:#define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);outword(INT_MASK, wTemp | INT_I2_MASK);判斷該位是否為 1 的做法是:50#define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);if(wTemp & INT_I2_MASK){… /* 該位為 1 */}上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握??偨Y在性能優化方面永遠注意 80-20 準備,不要優化程序中開銷不大的那 80%,這是勞而無功的。宏定義是 C 語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但宏在本質上不是函數,因而要防止宏展開后出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準 C 至今沒有包括 C++中 inline 函數的功能,inline 函數兼具無調用開銷和安全的優點。使用寄存器變量、內嵌匯編和活用位操作也是提高程序效率的有效方法。除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬件設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用 DMA 傳輸方式等。C/C++語言 void 及 void 指針深層探索1.概述許多初學者對 C/C++語言中的 void 及 void 指針類型不甚理解,因此在使用上出現了一些錯誤。本文將對 void 關鍵字的深刻含義進行解說,并詳述 void 及 void 指針類型的使用方法與技巧。2.void 的含義void 的字面意思是“無類型”,void *則為“無類型指針”,void *可以指向任何類型的數據。void 幾乎只有“注釋”和限制程序的作用,因為從來沒有人會定義一個 void 變量,讓我們試著來定義:void a;這行語句編譯時會出錯,提示“illegal use of type ‘void‘”。不過,即使 void a 的編譯不會出錯, 它也沒有任何實際意義。void 真正發揮的作用在于:(1) 對函數返回的限定;(2) 對函數參數的限定。我們將在第三節對以上二點進行具體說明。眾所周知,如果指針 p1 和 p2 的類型相同,那么我們可以直接在 p1 和 p2 間互相賦值;如果 p1 和 p2 指向不同的數據類型,則必須使用強制類型轉換運算符把賦值運算符右邊的指針類型轉換為左邊指針的類型。例如:float *p1;int *p2;p1 = p2;51其中 p1 = p2 語句會編譯出錯,提示“‘=‘ : cannot convert from ‘int *‘ to ‘float *‘”,必須改為:p1 = (float *)p2;而 void *則不同,任何類型的指針都可以直接賦值給它,無需進行強制類型轉換:void *p1;int *p2;p1 = p2;但這并不意味著,void *也可以無需強制類型轉換地賦給其它類型的指針。因為“無類型”可以包容“有類型”,而“有類型”則不能包容“無類型”。道理很簡單,我們可以說“男人和女人都是人”,但不能說“人是男人”或者“人是女人”。下面的語句編譯出錯:void *p1;int *p2;p2 = p1;提示“‘=‘ : cannot convert from ‘void *‘ to ‘int *‘”。3.void 的使用下面給出 void 關鍵字的使用規則:規則一 如果函數沒有返回值,那么應聲明為 void 類型在 C 語言中,凡不加返回值類型限定的函數,就會被編譯器作為返回整型值處理。但是許多程序員卻誤以為其為 void 類型。例如:add ( int a, int b ){return a + b;}int main(int argc, char* argv[]){printf ( "2 + 3 = %d", add ( 2, 3) );}程序運行的結果為輸出:2 + 3 = 5這說明不加返回值說明的函數的確為 int 函數。林銳博士《高質量 C/C++編程》中提到:“C++語言有很嚴格的類型安全檢查,不允許上述情況(指函數不加類型聲明)發生”。可是編譯器并不一定這么認定,譬如在 Visual C++6.0 中上述 add 函數的編譯無錯也無警告且運行正確,所以不能寄希望于編譯器會做嚴格的類型檢查。因此,為了避免混亂,我們在編寫 C/C++程序時,對于任何函數都必須一個不漏地指定其類型。如果函數沒有返回值,一定要聲明為 void 類型。這既是程序良好可讀性的需要,也是編程規范性的要求。另外,加上 void 類型聲明后,也可以發揮代碼的“自注釋”作用。代碼的“自注釋”即代碼能自己注釋自己。規則二 如果函數無參數,那么應聲明其參數為 void52在 C++語言中聲明一個這樣的函數:int function(void){return 1;}則進行下面的調用是不合法的:function(2);因為在 C++中,函數參數為 void 的意思是這個函數不接受任何參數。我們在 Turbo C 2.0 中編譯:#include "stdio.h"fun(){return 1;}main(){printf("%d",fun(2));getchar();}編譯正確且輸出 1,這說明,在 C 語言中,可以給無參數的函數傳送任意類型的參數, 但是在 C++編譯器中編譯同樣的代碼則會出錯。在 C++中,不能向無參數的函數傳送任何參數,出錯提示“‘fun‘ : function does not take 1 parameters”。所以,無論在 C 還是 C++中,若函數不接受任何參數,一定要指明參數為 void。規則三 小心使用 void 指針類型按照 ANSI(American National Standards Institute)標準,不能對 void 指針進行算法操作,即下列操作都是不合法的:void * pvoid;pvoid++; //ANSI:錯誤pvoid += 1; //ANSI:錯誤//ANSI 標準之所以這樣認定,是因為它堅持:進行算法操作的指針必須是確定知道其指向數據類型大小的。//例如:int *pint;pint++; //ANSI:正確pint++的結果是使其增大 sizeof(int)。但是大名鼎鼎的 GNU(GNU‘s Not Unix 的縮寫)則不這么認定,它指定 void *的算法操作與 char *一致。因此下列語句在 GNU 編譯器中皆正確:53pvoid++; //GNU:正確pvoid += 1; //GNU:正確pvoid++的執行結果是其增大了 1。在實際的程序設計中,為迎合 ANSI 標準,并提高程序的可移植性,我們可以這樣編寫實現同樣功能的代碼:void * pvoid;(char *)pvoid++; //ANSI:正確;GNU:正確(char *)pvoid += 1; //ANSI:錯誤;GNU:正確GNU 和 ANSI 還有一些區別,總體而言,GNU 較 ANSI 更“開放”,提供了對更多語法的支持。但是我們在真實設計時,還是應該盡可能地迎合 ANSI 標準。規則四 如果函數的參數可以是任意類型指針,那么應聲明其參數為 void *典型的如內存操作函數 memcpy 和 memset 的函數原型分別為:void * memcpy(void *dest, const void *src, size_t len);void * memset ( void * buffer, int c, size_t num );這樣,任何類型的指針都可以傳入 memcpy 和 memset 中,這也真實地體現了內存操作函數的意義,因為它操作的對象僅僅是一片內存,而不論這片內存是什么類型。如果 memcpy 和 memset 的參數類型不是 void *,而是 char *,那才叫真的奇怪了!這樣的 memcpy 和memset 明顯不是一個“純粹的,脫離低級趣味的”函數!下面的代碼執行正確://示例:memset 接受任意類型指針int intarray[100];memset ( intarray, 0, 100*sizeof(int) ); //將 intarray 清 0//示例:memcpy 接受任意類型指針int intarray1[100], intarray2[100];memcpy ( intarray1, intarray2, 100*sizeof(int) ); //將 intarray2 拷貝給 intarray1有趣的是,memcpy 和 memset 函數返回的也是 void *類型,標準庫函數的編寫者是多么地富有學問?。∫巹t五 void 不能代表一個真實的變量下面代碼都企圖讓 void 代表一個真實的變量,因此都是錯誤的代碼:void a; //錯誤function(void a); //錯誤void 體現了一種抽象,這個世界上的變量都是“有類型”的,譬如一個人不是男人就是女人(還有人妖?)。void 的出現只是為了一種抽象的需要,如果你正確地理解了面向對象中“抽象基類”的概念,也很容易理解 void 數據類型。正如不能給抽象基類定義一個實例,我們也不能定義一個 void(讓我們類比的稱 void 為“抽象數據類型”)變量。4.總結小小的 void 蘊藏著很豐富的設計哲學,作為一名程序設計人員,對問題進行深一個層次的思考必然使我們受益匪淺。54C/C++語言可變參數表深層探索作者: 宋寶華 e-mail:21cnbao@21cn.com1.引言C/C++語言有一個不同于其它語言的特性,即其支持可變參數,典型的函數如 printf、scanf 等可以接受數量不定的參數。如:printf ( "I love you" );printf ( "%d", a );printf ( "%d,%d", a, b );第一、二、三個 printf 分別接受 1、2、3 個參數,讓我們看看 printf 函數的原型:int printf ( const char *format, ... );從函數原型可以看出, 其除了接收一個固定的參數 format 以外, 后面的參數用 “…”表示。 在 C/C++語言中,“…”表示可以接受不定數量的參數,理論上來講,可以是 0 或 0 以上的 n 個參數。本文將對 C/C++可變參數表的使用方法及 C/C++支持可變參數表的深層機理進行探索。2.可變參數表的用法2.1 相關宏標準 C/C++包含頭文件 stdarg.h,該頭文件中定義了如下三個宏:void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */type va_arg ( va_list arg_ptr, type );void va_end ( va_list arg_ptr );在這些宏中,va 就是 variable argument(可變參數)的意思;arg_ptr 是指向可變參數表的指針;prev_param 則指可變參數表的前一個固定參數;type 為可變參數的類型。va_list 也是一個宏,其定義為 typedef char * va_list,實質上是一 char 型指針。char 型指針的特點是++、--操作對其作用的結果是增 1 和減 1(因為 sizeof(char)為 1),與之不同的是 int 等其它類型指針的++、--操作對其作用的結果是增 sizeof(type)或減 sizeof(type),而且 sizeof(type)大于 1。通過 va_start 宏我們可以取得可變參數表的首指針,這個宏的定義為:#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )顯而易見,其含義為將最后那個固定參數的地址加上可變參數對其的偏移后賦值給 ap,這樣 ap 就是可變參數表的首地址。其中的_INTSIZEOF 宏定義為:#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) – 1 ) & ~( sizeof( int ) – 1 ) )va_arg 宏的意思則指取出當前 arg_ptr 所指的可變參數并將 ap 指針指向下一可變參數,其原型為:#define va_arg(list, mode) ((mode *)(list =/(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &/(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]對這個宏的具體含義我們將在第 3 節深入討論。而 va_end 宏被用來結束可變參數的獲取,其定義為:#define va_end ( list )可以看出,va_end ( list )實際上被定義為空,沒有任何真實對應的代碼,用于代碼對稱,與 va_start對應;另外,它還可能發揮代碼的“自注釋”作用。所謂代碼的“自注釋”,指的是代碼能自己注釋自己。下面我們以具體的例子來說明以上三個宏的使用方法。552.2 一個簡單的例子#include <stdarg.h>/* 函數名:max* 功能:返回 n 個整數中的最大值* 參數:num:整數的個數 ...:num 個輸入的整數* 返回值:求得的最大整數*/int max ( int num, ... ){int m = -0x7FFFFFFF; /* 32 系統中最小的整數 */va_list ap;va_start ( ap, num );for ( int i= 0; i< num; i++ ){int t = va_arg (ap, int);if ( t > m ){m = t;}}va_end (ap);return m;}/* 主函數調用 max */int main ( int argc, char* argv[] ){int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求 5 個整數中的最大值 */cout << n;return 0;}函數 max 中首先定義了可變參數表指針 ap,而后通過 va_start ( ap, num )取得了參數表首地址(賦給了 ap),其后的 for 循環則用來遍歷可變參數表。這種遍歷方式與我們在數據結構教材中經??吹降谋闅v方式是類似的。函數 max 看起來簡潔明了,但是實際上 printf 的實現卻遠比這復雜。max 函數之所以看起來簡單,是因為:(1) max 函數可變參數表的長度是已知的,通過 num 參數傳入;(2) max 函數可變參數表中參數的類型是已知的,都為 int 型。而 printf 函數則沒有這么幸運。首先,printf 函數可變參數的個數不能輕易的得到,而可變參數的類型也不是固定的,需由格式字符串進行識別(由%f、%d、%s 等確定),因此則涉及到可變參數表的更復雜應用。下面我們以實例來分析可變參數表的高級應用。2.3 高級應用下面這個程序是我們為某嵌入式系統(該系統中 CPU 的字長為 16 位)編寫的在屏幕上顯示格式字符串56的函數 DrawText,它的用法類似于 int printf ( const char *format, ... )函數,但其輸出的目標為嵌入式系統的液晶顯示屏幕(LED)。///////////////////////////////////////////////////////////////////////////////// 函數名稱: DrawText// 功能說明: 在顯示屏上繪制文字// 參數說明: xPos ---橫坐標的位置 [0 .. 30]// yPos ---縱坐標的位置 [0 .. 64]// ... 可以同數字一起顯示,需設置標志(%d、%l、%x、%s)///////////////////////////////////////////////////////////////////////////////extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... ){BYTE lpData[100]; //緩沖區BYTE byIndex;BYTE byLen;DWORD dwTemp;WORD wTemp;int i;va_list lpParam;memset( lpData, 0, 100);byLen = strlen( lpStr );byIndex = 0;va_start ( lpParam, lpStr );for ( i = 0; i < byLen; i++ ){if( lpStr[i] != '%' ) //不是格式符開始{lpData[byIndex++] = lpStr[i];}else{switch (lpStr[i+1]){//整型case 'd':case 'D':wTemp = va_arg ( lpParam, int );byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp );i++;break;//長整型case 'l':case 'L':57dwTemp = va_arg ( lpParam, long );byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );i++;break;//16 進制(長整型)case 'x':case 'X':dwTemp = va_arg ( lpParam, long );byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp );i++;break;default:lpData[byIndex++] = lpStr[i];break;}}}va_end ( lpParam );lpData[byIndex] = '/0';DisplayString ( xPos, yPos, lpData, TRUE); //在屏幕上顯示字符串 lpData}在這個函數中,需通過對傳入的格式字符串(首地址為 lpStr)進行識別來獲知可變參數個數及各個可變參數的類型,具體實現體現在 for 循環中。譬如,在識別為%d 后,做的是 va_arg ( lpParam, int ),而獲知為%l 和%x 后則進行的是 va_arg ( lpParam, long )。格式字符串識別完成后,可變參數也就處理完了。在項目的最初,我們一直苦于不能找到一個好的辦法來混合輸出字符串和數字,我們采用了分別顯示數字和字符串的方法,并分別指定坐標,程序條理被破壞。而且,在混合顯示的時候,要給各類數據分別人工計算坐標,我們感覺頭疼不已。以前的函數為://顯示字符串showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )//顯示數字showNum ( BYTE xPos, BYTE yPos, int num )//以 16 進制方式顯示數字showHexNum ( BYTE xPos, BYTE yPos, int num )最終,我們用 DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )函數代替了原先所有的輸出函數,程序得到了簡化。就這樣,兄弟們用得爽翻了。3.運行機制探索通過第 2 節我們學會了可變參數表的使用方法,相信喜歡拋根問底的讀者還不甘心,必然想知道如下問題:(1)為什么按照第 2 節的做法就可以獲得可變參數并對其進行操作?(2)C/C++在底層究竟是依靠什么來對這一語法進行支持的,為什么其它語言就不能提供可變參數表呢?我們帶著這些疑問來一步步進行摸索。583.1 調用機制反匯編反匯編是研究語法深層特性的終極良策,先來看看 2.2 節例子中主函數進行 max ( 5, 5, 6 ,3 ,8 ,5)調用時的反匯編:1. 004010C8 push 52. 004010CA push 83. 004010CC push 34. 004010CE push 65. 004010D0 push 56. 004010D2 push 57. 004010D4 call @ILT+5(max) (0040100a)從上述反匯編代碼中我們可以看出,C/C++函數調用的過程中:第一步:將參數從右向左入棧(第 1~6 行);第二步:調用 call 指令進行跳轉(第 7 行)。這兩步包含了深刻的含義,它說明 C/C++默認的調用方式為由調用者管理參數入棧的操作,且入棧的順序為從右至左,這種調用方式稱為_cdecl 調用。x86 系統的入棧方向為從高地址到低地址,故第 1 至 n個參數被放在了地址遞增的堆棧內。在被調用函數內部,讀取這些堆棧的內容就可獲得各個參數的值,讓我們反匯編到 max 函數的內部:int max ( int num, ...){1. 00401020 push ebp2. 00401021 mov ebp,esp3. 00401023 sub esp,50h4. 00401026 push ebx5. 00401027 push esi6. 00401028 push edi7. 00401029 lea edi,[ebp-50h]8. 0040102C mov ecx,14h9. 00401031 mov eax,0CCCCCCCCh10. 00401036 rep stos dword ptr [edi]va_list ap;int m = -0x7FFFFFFF; /* 32 系統中最小的整數 */11. 00401038 mov dword ptr [ebp-8],80000001hva_start ( ap, num );12. 0040103F lea eax,[ebp+0Ch]13. 00401042 mov dword ptr [ebp-4],eaxfor ( int i= 0; i< num; i++ )14. 00401045 mov dword ptr [ebp-0Ch],015. 0040104C jmp max+37h (00401057)16. 0040104E mov ecx,dword ptr [ebp-0Ch]17. 00401051 add ecx,118. 00401054 mov dword ptr [ebp-0Ch],ecx19. 00401057 mov edx,dword ptr [ebp-0Ch]20. 0040105A cmp edx,dword ptr [ebp+8]21. 0040105D jge max+61h (00401081)59{int t= va_arg (ap, int);22. 0040105F mov eax,dword ptr [ebp-4]23. 00401062 add eax,424. 00401065 mov dword ptr [ebp-4],eax25. 00401068 mov ecx,dword ptr [ebp-4]26. 0040106B mov edx,dword ptr [ecx-4]27. 0040106E mov dword ptr [t],edxif ( t > m )28. 00401071 mov eax,dword ptr [t]29. 00401074 cmp eax,dword ptr [ebp-8]30. 00401077 jle max+5Fh (0040107f)m = t;31. 00401079 mov ecx,dword ptr [t]32. 0040107C mov dword ptr [ebp-8],ecx}33. 0040107F jmp max+2Eh (0040104e)va_end (ap);34. 00401081 mov dword ptr [ebp-4],0return m;35. 00401088 mov eax,dword ptr [ebp-8]}36. 0040108B pop edi37. 0040108C pop esi38. 0040108D pop ebx39. 0040108E mov esp,ebp40. 00401090 pop ebp41. 00401091 ret分析上述反匯編代碼,對于一個真正的程序員而言,將是一種很大的享受;而對于初學者,也將使其受益良多。所以請一定要賴著頭皮認真研究,千萬不要被嚇倒!行 1~10 進行執行函數內代碼的準備工作,保存現場。第 2 行對堆棧進行移動;第 3 行則意味著 max函數為其內部局部變量準備的堆??臻g為 50h 字節;第 11 行表示把變量 n 的內存空間安排在了函數內部局部棧底減 8 的位置(占用 4 個字節)。第 12~13 行非常關鍵,對應著 va_start ( ap, num ),這兩行將第一個可變參數的地址賦值給了指針ap。另外,從第 12 行可以看出 num 的地址為 ebp+0Ch;從第 13 行可以看出 ap 被分配在函數內部局部棧底減 4 的位置上(占用 4 個字節)。第 22~27 行最為關鍵,對應著 va_arg (ap, int)。其中,22~24 行的作用為將 ap 指向下一可變參數(可變參數的地址間隔為 4 個字節,從 add eax,4 可以看出);25~27 行則取當前可變參數的值賦給變量 t。這段反匯編很奇怪,它先移動可變參數指針,再在賦值指令里面回過頭來取先前的參數值賦給t(從 mov edx,dword ptr [ecx-4]語句可以看出)。Visual C++同學玩得有意思,不知道碰見同樣的情況 Visual Basic 等其它同學怎么玩?第 36~41 行恢復現場和堆棧地址,執行函數返回操作。痛苦的反匯編之旅差不多結束了,看了這段反匯編我們總算弄明白了可變參數的存放位置以及它們被讀取的方式,頓覺全省輕松!603.2 特殊的調用約定除此之外,我們需要了解 C/C++函數調用對參數占用空間的一些特殊約定,因為在_cdecl 調用協議中,有些變量類型是按照其它變量的尺寸入棧的。例如,字符型變量將被自動擴展為一個字的空間,因為入棧操作針對的是一個字。參數 n 實際占用的空間為( ( sizeof(n) + sizeof(int) – 1 ) & ~( sizeof(int) – 1 ) ),這就是第 2.1 節_INTSIZEOF(v)宏的來歷!既然如此,2.1 節給出的 va_arg ( list, mode )宏為什么玩這么大的飛機就很清楚了。這個問題就留個讀者您來分析。C/C++數組名與指針區別深層探索作者: 宋寶華 e-mail:21cnbao@21cn.com1. 引言指針是 C/C++語言的特色,而數組名與指針有太多的相似,甚至很多時候,數組名可以作為指針使用。于是乎,很多程序設計者就被搞糊涂了。 而許多的大學老師, 他們在 C語言的教學過程中也錯誤得給學生講解:“數組名就是指針”。很幸運,我的大學老師就是其中之一。時至今日,我日復一日地進行著 C/C++項目的開發,而身邊還一直充滿這樣的程序員,他們保留著“數組名就是指針”的誤解。想必這種誤解的根源在于國內某著名的 C 程序設計教程。如果這篇文章能夠糾正許多中國程序員對數組名和指針的誤解,筆者就不甚欣慰了。借此文,筆者站在無數對知識如饑似渴的中國程序員之中,深深寄希望于國內的計算機圖書編寫者們,能以“深入探索”的思維方式和精益求精的認真態度來對待圖書編寫工作,但愿市面上多一些融入作者思考結晶的心血之作!2. 魔幻數組名請看程序(本文程序在 WIN32 平臺下編譯):1. #include <iostream.h>2. int main(int argc, char* argv[])3. {4. char str[10];5. char *pStr = str;6. cout << sizeof(str) << endl;7. cout << sizeof(pStr) << endl;8. return 0;9. }2.1 數組名不是指針我們先來推翻“數組名就是指針”的說法,用反證法。證明 數組名不是指針假設:數組名是指針;則:pStr 和 str 都是指針;因為:在 WIN32 平臺下,指針長度為 4;所以:第 6 行和第 7 行的輸出都應該為 4;實際情況是:第 6 行輸出 10,第 7 行輸出 4;所以:假設不成立,數組名不是指針2.2 數組名神似指針上面我們已經證明了數組名的確不是指針,但是我們再看看程序的第 5 行。該行程序將數組名直接賦值給指針,這顯61得數組名又的確是個指針!我們還可以發現數組名顯得像指針的例子:1. #include <string.h>2. #include <iostream.h>3. int main(int argc, char* argv[])4. {5. char str1[10] = "I Love U";6. char str2[10];7. strcpy(str2,str1);8. cout << "string array 1: " << str1 << endl;9. cout << "string array 2: " << str2 << endl;10. return 0;11. }標準 C 庫函數 strcpy 的函數原形中能接納的兩個參數都為 char 型指針,而我們在調用中傳給它的卻是兩個數組名!函數輸出:string array 1: I Love Ustring array 2: I Love U數組名再一次顯得像指針!既然數組名不是指針, 而為什么到處都把數組名當指針用?于是乎, 許多程序員得出這樣的結論: 數組名 (主)是 (謂)不是指針的指針(賓)。整個一魔鬼。3. 數組名大揭密那么,是揭露數組名本質的時候了,先給出三個結論:(1)數組名的內涵在于其指代實體是一種數據結構,這種數據結構就是數組;(2)數組名的外延在于其可以轉換為指向其指代實體的指針,而且是一個指針常量;(3)指向數組的指針則是另外一種變量類型(在 WIN32 平臺下,長度為 4),僅僅意味著數組的存放地址!3.1 數組名指代一種數據結構:數組現在可以解釋為什么第 1 個程序第 6 行的輸出為 10 的問題,根據結論 1,數組名 str 的內涵為一種數據結構,即一個長度為 10 的 char 型數組,所以 sizeof(str)的結果為這個數據結構占據的內存大小:10 字節。再看:1. int intArray[10];2. cout << sizeof(intArray) ;第 2 行的輸出結果為 40(整型數組占據的內存空間大小)。如果 C/C++程序可以這樣寫:1. int[10] intArray;2. cout << sizeof(intArray) ;我們就都明白了,intArray 定義為 int[10]這種數據結構的一個實例,可惜啊, C/C++目前并不支持這種定義方式。3.2 數組名可作為指針常量根據結論 2,數組名可以轉換為指向其指代實體的指針,所以程序 1 中的第 5 行數組名直接賦值給指針,程序 2 第 7行直接將數組名作為指針形參都可成立。下面的程序成立嗎?1. int intArray[10];2. intArray++;讀者可以編譯之,發現編譯出錯。原因在于,雖然數組名可以轉換為指向其指代實體的指針,但是它只能被看作一個62指針常量,不能被修改。而指針, 不管是指向結構體、 數組還是基本數據類型的指針, 都不包含原始數據結構的內涵, 在 WIN32 平臺下, sizeof操作的結果都是 4。順便糾正一下許多程序員的另一個誤解。 許多程序員以為 sizeof 是一個函數,而實際上,它是一個操作符,不過其使用方式看起來的確太像一個函數了。語句 sizeof(int)就可以說明 sizeof 的確不是一個函數,因為函數接納形參(一個變量),世界上沒有一個 C/C++函數接納一個數據類型(如 int)為“形參”。3.3 數據名可能失去其數據結構內涵到這里似乎數組名魔幻問題已經宣告圓滿解決,但是平靜的湖面上卻再次掀起波浪。請看下面一段程序:1. #include <iostream.h>2. void arrayTest(char str[])3. {4. cout << sizeof(str) << endl;5. }6. int main(int argc, char* argv[])7. {8. char str1[10] = "I Love U";9. arrayTest(str1);10. return 0;11. }程序的輸出結果為 4。不可能吧?4,一個可怕的數字,前面已經提到其為指針的長度!結論 1 指出,數據名內涵為數組這種數據結構,在 arrayTest 函數體內,str 是數組名,那為什么 sizeof 的結果卻是指針的長度?這是因為:(1)數組名作為函數形參時,在函數體內,其失去了本身的內涵,僅僅只是一個指針;(2)很遺憾,在失去其內涵的同時,它還失去了其常量特性,可以作自增、自減等操作,可以被修改。所以,數據名作為函數形參時,其全面淪落為一個普通指針!它的貴族身份被剝奪,成了一個地地道道的只擁有 4個字節的平民。以上就是結論 4。4. 結論本文以打破沙鍋問到底的探索精神用數段程序實例論證了數據名和指針的區別。最后,筆者再次表達深深的希望,愿我和我的同道中人能夠真正以謹慎的研究態度來認真思考開發中的問題,這樣才能在我們中間產生大師級的程序員,頂級的開發書籍。每次拿著美國鬼子的開發書籍,我們不免發出這樣的感慨:我們落后太遠了。C/C++程序員應聘常見面試題深入剖析(1)作者: 宋寶華 e-mail:21cnbao@21cn.com出處: 軟件報1.引言本文的寫作目的并不在于提供 C/C++程序員求職面試指導,而旨在從技術上分析面試題的內涵。文中的大多數面試題來自各大論壇,部分試題解答也參考了網友的意見。許多面試題看似簡單,卻需要深厚的基本功才能給出完美的解答。企業要求面試者寫一個最簡單的strcpy 函數都可看出面試者在技術上究竟達到了怎樣的程度,我們能真正寫好一個 strcpy 函數嗎?我們都覺得自己能,可是我們寫出的 strcpy 很可能只能拿到 10 分中的 2 分。讀者可從本文看到 strcpy63函數從 2 分到 10 分解答的例子,看看自己屬于什么樣的層次。此外,還有一些面試題考查面試者敏捷的思維能力。分析這些面試題,本身包含很強的趣味性;而作為一名研發人員,通過對這些面試題的深入剖析則可進一步增強自身的內功。2.找錯題試題1:void test1(){char string[10];char* str1 = "0123456789";strcpy( string, str1 );}試題 2:void test2(){char string[10], str1[10];int i;for(i=0; i<10; i++){str1[i] = 'a';}strcpy( string, str1 );}試題 3:void test3(char* str1){char string[10];if( strlen( str1 ) <= 10 ){strcpy( string, str1 );}}解答:試題 1 字符串 str1 需要 11 個字節才能存放下(包括末尾的’/0’),而 string 只有 10 個字節的空間,strcpy 會導致數組越界;對試題2,如果面試者指出字符數組str1不能在數組內結束可以給3分;如果面試者指出strcpy(string,str1)調用使得從 str1 內存起復制到 string 內存起所復制的字節數具有不確定性可以給 7 分,在此基礎上指出庫函數 strcpy 工作方式的給 10 分;對試題 3,if(strlen(str1) <= 10)應改為 if(strlen(str1) < 10),因為 strlen 的結果未統計’/0’所占用的 1 個字節。剖析:考查對基本功的掌握:(1)字符串以’/0’結尾;64(2)對數組越界把握的敏感度;(3)庫函數 strcpy 的工作方式,如果編寫一個標準 strcpy 函數的總分值為 10,下面給出幾個不同得分的答案:2 分void strcpy( char *strDest, char *strSrc ){while( (*strDest++ = * strSrc++) != ‘/0’ );}4 分void strcpy( char *strDest, const char *strSrc )//將源字符串加 const,表明其為輸入參數,加 2 分{while( (*strDest++ = * strSrc++) != ‘/0’ );}7 分void strcpy(char *strDest, const char *strSrc){//對源地址和目的地址加非 0 斷言,加 3 分assert( (strDest != NULL) && (strSrc != NULL) );while( (*strDest++ = * strSrc++) != ‘/0’ );}10 分//為了實現鏈式操作,將目的地址返回,加 3 分!char * strcpy( char *strDest, const char *strSrc ){assert( (strDest != NULL) && (strSrc != NULL) );char *address = strDest;while( (*strDest++ = * strSrc++) != ‘/0’ );return address;}從 2 分到 10 分的幾個答案我們可以清楚的看到,小小的 strcpy 竟然暗藏著這么多玄機,真不是蓋的!需要多么扎實的基本功才能寫一個完美的 strcpy ?。。ǎ矗?strlen 的掌握,它沒有包括字符串末尾的'/0'。讀者看了不同分值的 strcpy 版本,應該也可以寫出一個 10 分的 strlen 函數了,完美的版本為:int strlen( const char *str ) //輸入參數 const{assert( strt != NULL ); //斷言字符串地址非 0int len;while( (*str++) != '/0' ){len++;}return len;}65試題 4:void GetMemory( char *p ){p = (char *) malloc( 100 );}void Test( void ){char *str = NULL;GetMemory( str );strcpy( str, "hello world" );printf( str );}試題 5:char *GetMemory( void ){char p[] = "hello world";return p;}void Test( void ){char *str = NULL;str = GetMemory();printf( str );}試題 6:void GetMemory( char **p, int num ){*p = (char *) malloc( num );}void Test( void ){char *str = NULL;GetMemory( &str, 100 );strcpy( str, "hello" );printf( str );}試題 7:void Test( void ){char *str = (char *) malloc( 100 );strcpy( str, "hello" );free( str );... //省略的其它語句}66解答:試題 4 傳入中 GetMemory( char *p )函數的形參為字符串指針,在函數內部修改形參并不能真正的改變傳入形參的值,執行完char *str = NULL;GetMemory( str );后的 str 仍然為 NULL;試題 5 中char p[] = "hello world";return p;的 p[]數組為函數內的局部自動變量,在函數返回后,內存已經被釋放。這是許多程序員常犯的錯誤,其根源在于不理解變量的生存期。試題 6 的 GetMemory 避免了試題 4 的問題,傳入 GetMemory 的參數為字符串指針的指針,但是在GetMemory 中執行申請內存及賦值語句*p = (char *) malloc( num );后未判斷內存是否申請成功,應加上:if ( *p == NULL ){...//進行申請內存失敗處理}試題 7 存在與試題 6 同樣的問題,在執行char *str = (char *) malloc(100);后未進行內存是否申請成功的判斷;另外,在 free(str)后未置 str 為空,導致可能變成一個“野”指針,應加上:str = NULL;試題 6 的 Test 函數中也未對 malloc 的內存進行釋放。剖析:試題 4~7 考查面試者對內存操作的理解程度,基本功扎實的面試者一般都能正確的回答其中 50~60 的錯誤。但是要完全解答正確,卻也絕非易事。對內存操作的考查主要集中在:(1)指針的理解;(2)變量的生存期及作用范圍;(3)良好的動態內存申請和釋放習慣。在看看下面的一段程序有什么錯誤:swap( int* p1,int* p2 ){int *p;*p = *p1;*p1 = *p2;*p2 = *p;}在 swap 函數中,p 是一個“野”指針,有可能指向系統區,導致程序運行的崩潰。在 VC++中 DEBUG 運行時提示錯誤“Access Violation”。該程序應該改為:swap( int* p1,int* p2 ){67int p;p = *p1;*p1 = *p2;*p2 = p;}C/C++程序員應聘常見面試題深入剖析(2)作者: 宋寶華 e-mail:21cnbao@21cn.com出處: 軟件報3.內功題試題 1: 分別給出 BOOL,int,float,指針變量 與“零值”比較的 if 語句(假設變量名為 var)解答:BOOL 型變量:if(!var)int 型變量: if(var==0)float 型變量:const float EPSINON = 0.00001;if ((x >= - EPSINON) && (x <= EPSINON)指針變量: if(var==NULL)剖析:考查對 0 值判斷的“內功”,BOOL 型變量的 0 判斷完全可以寫成 if(var==0),而 int 型變量也可以寫成 if(!var),指針變量的判斷也可以寫成 if(!var),上述寫法雖然程序都能正確運行,但是未能清晰地表達程序的意思。一般的,如果想讓 if 判斷一個變量的“真”、“假”,應直接使用 if(var)、if(!var),表明其為“邏輯”判斷;如果用 if 判斷一個數值型變量(short、int、long 等),應該用 if(var==0),表明是與 0進行“數值”上的比較;而判斷指針則適宜用 if(var==NULL),這是一種很好的編程習慣。浮點型變量并不精確,所以不可將 float 變量用“==”或“!=”與數字比較,應該設法轉化成“>=”或“<=”形式。如果寫成 if (x == 0.0),則判為錯,得 0 分。試題 2: 以下為 Windows NT 下的 32 位 C++程序,請計算 sizeof 的值void Func ( char str[100] ){sizeof( str ) = ?}void *p = malloc( 100 );sizeof ( p ) = ?解答:sizeof( str ) = 4sizeof ( p ) = 4剖析:Func ( char str[100] )函數中數組名作為函數形參時,在函數體內,數組名失去了本身的內涵,僅僅只是一個指針;在失去其內涵的同時,它還失去了其常量特性,可以作自增、自減等操作,可以被修改。數組名的本質如下:68(1)數組名指代一種數據結構,這種數據結構就是數組;例如:char str[10];cout << sizeof(str) << endl;輸出結果為 10,str 指代數據結構 char[10]。(2)數組名可以轉換為指向其指代實體的指針,而且是一個指針常量,不能作自增、自減等操作,不能被修改;char str[10];str++; //編譯出錯,提示 str 不是左值(3)數組名作為函數形參時,淪為普通指針。Windows NT 32 位平臺下,指針的長度(占用內存的大?。?4 字節,故 sizeof( str ) 、sizeof ( p ) 都為 4。試題 3: 寫一個“標準”宏 MIN,這個宏輸入兩個參數并返回較小的一個。另外,當你寫下面的代碼時會發生什么事?least = MIN(*p++, b);解答:#define MIN(A,B) ((A) <= (B) ? (A) : (B))MIN(*p++, b)會產生宏的副作用剖析:這個面試題主要考查面試者對宏定義的使用,宏定義可以實現類似于函數的功能,但是它終歸不是函數,而宏定義中括弧中的“參數”也不是真的參數,在宏展開的時候對“參數”進行的是一對一的替換。程序員對宏定義的使用要非常小心,特別要注意兩個問題:(1)謹慎地將宏定義中的“參數”和整個宏用用括弧括起來。所以,嚴格地講,下述解答:#define MIN(A,B) (A) <= (B) ? (A) : (B)#define MIN(A,B) (A <= B ? A : B )都應判 0 分;(2)防止宏的副作用。宏定義#define MIN(A,B) ((A) <= (B) ? (A) : (B))對 MIN(*p++, b)的作用結果是:((*p++) <= (b) ? (*p++) : (b))這個表達式會產生副作用,指針 p 會作兩次++自增操作。除此之外,另一個應該判 0 分的解答是:#define MIN(A,B) ((A) <= (B) ? (A) : (B));這個解答在宏定義的后面加“;”,顯示編寫者對宏的概念模糊不清,只能被無情地判 0 分并被面試官淘汰。試題 4: 為什么標準頭文件都有類似以下的結構?#ifndef __INCvxWorksh#define __INCvxWorksh#ifdef __cplusplusextern "C" {#endif/*...*/#ifdef __cplusplus}69#endif#endif /* __INCvxWorksh */解答:頭文件中的編譯宏#ifndef __INCvxWorksh#define __INCvxWorksh#endif的作用是防止被重復引用。作為一種面向對象的語言,C++支持函數重載,而過程式語言 C 則不支持。函數被 C++編譯后在 symbol庫中的名字與 C 語言的不同。例如,假設某個函數的原型為:void foo(int x, int y);該函數被 C 編譯器編譯后在 symbol 庫中的名字為_foo,而 C++編譯器則會產生像_foo_int_int 之類的名字。_foo_int_int 這樣的名字包含了函數名和函數參數數量及類型信息,C++就是考這種機制來實現函數重載的。為了實現 C 和 C++的混合編程,C++提供了 C 連接交換指定符號 extern "C"來解決名字匹配問題,函數聲明前加上 extern "C"后,則編譯器就會按照 C 語言的方式將該函數編譯為_foo,這樣 C 語言中就可以調用 C++的函數了。試題 5: 編寫一個函數,作用是把一個 char 組成的字符串循環右移 n 個。比如原來是“abcdefghi”如果 n=2,移位后應該是“hiabcdefgh”函數頭是這樣的://pStr 是指向以'/0'結尾的字符串的指針//steps 是要求移動的 nvoid LoopMove ( char * pStr, int steps ){//請填充...}解答:正確解答 1:void LoopMove ( char *pStr, int steps ){int n = strlen( pStr ) - steps;char tmp[MAX_LEN];strcpy ( tmp, pStr + n );strcpy ( tmp + steps, pStr);*( tmp + strlen ( pStr ) ) = '/0';strcpy( pStr, tmp );}正確解答 2:void LoopMove ( char *pStr, int steps ){int n = strlen( pStr ) - steps;char tmp[MAX_LEN];memcpy( tmp, pStr + n, steps );memcpy(pStr + steps, pStr, n );70memcpy(pStr, tmp, steps );}剖析:這個試題主要考查面試者對標準庫函數的熟練程度,在需要的時候引用庫函數可以很大程度上簡化程序編寫的工作量。最頻繁被使用的庫函數包括:(1)strcpy(2)memcpy(3)memset試題 6: 已知 WAV 文件格式如下表,打開一個 WAV 文件,以適當的數據結構組織 WAV 文件頭并解析 WAV格式的各項信息。WAVE 文件格式說明表
偏移地址 字節數 數據類型 內 容
00H 4 Char "RIFF"標志文 件頭
04H 4 int32 文件長度
08H 4 Char "WAVE"標志
0CH 4 Char "fmt"標志
10H 4 過渡字節(不定)
14H 2 int16 格式類別
16H 2 int16 通道數
18H 2 int16采樣率(每秒樣本數),表示每個通道的播放速度
1CH 4 int32 波形音頻數據傳送速率
20H 2 int16 數據塊的調整數(按字節算的)
22H 2 每樣本的數據位數
24H 4 Char 數據標記符"data"
28H 4 int32 語音數據的長度
解答:將 WAV 文件格式定義為結構體 WAVEFORMAT:typedef struct tagWaveFormat{char cRiffFlag[4];UIN32 nFileLen;char cWaveFlag[4];char cFmtFlag[4];char cTransition[4];UIN16 nFormatTag ;UIN16 nChannels;UIN16 nSamplesPerSec;UIN32 nAvgBytesperSec;UIN16 nBlockAlign;UIN16 nBitNumPerSample;char cDataFlag[4];71UIN16 nAudioLength;} WAVEFORMAT;假設 WAV 文件內容讀出后存放在指針 buffer 開始的內存單元內,則分析文件格式的代碼很簡單,為:WAVEFORMAT waveFormat;memcpy( &waveFormat, buffer,sizeof( WAVEFORMAT ) );直接通過訪問 waveFormat 的成員,就可以獲得特定 WAV 文件的各項格式信息。剖析:試題 6 考查面試者組織數據結構的能力,有經驗的程序設計者將屬于一個整體的數據成員組織為一個結構體,利用指針類型轉換,可以將 memcpy、memset 等函數直接用于結構體地址,進行結構體的整體操作。透過這個題可以看出面試者的程序設計經驗是否豐富。試題 7: 編寫類 String 的構造函數、析構函數和賦值函數,已知類 String 的原型為:class String{public:String(const char *str = NULL); // 普通構造函數String(const String &other); // 拷貝構造函數~ String(void); // 析構函數String & Operate =(const String &other); // 賦值函數private:char *m_data; // 用于保存字符串};解答://普通構造函數String::String(const char *str){if(str==NULL){m_data = new char[1]; // 得分點:對空字符串自動申請存放結束標志'/0'的空//加分點:對 m_data 加 NULL 判斷*m_data = '/0';}else{int length = strlen(str);m_data = new char[length+1]; // 若能加 NULL 判斷則更好strcpy(m_data, str);}}// String 的析構函數String::~String(void){delete [] m_data; // 或 delete m_data;}72//拷貝構造函數String::String(const String &other) // 得分點:輸入參數為 const 型{int length = strlen(other.m_data);m_data = new char[length+1]; //加分點:對 m_data 加 NULL 判斷strcpy(m_data, other.m_data);}//賦值函數String & String::operate =(const String &other) // 得分點:輸入參數為 const 型{if(this == &other) //得分點:檢查自賦值return *this;delete [] m_data; //得分點:釋放原有的內存資源int length = strlen( other.m_data );m_data = new char[length+1]; //加分點:對 m_data 加 NULL 判斷strcpy( m_data, other.m_data );return *this; //得分點:返回本對象的引用}剖析:能夠準確無誤地編寫出 String 類的構造函數、拷貝構造函數、賦值函數和析構函數的面試者至少已經具備了 C++基本功的 60%以上!在這個類中包括了指針類成員變量 m_data,當類中包括指針類成員變量時,一定要重載其拷貝構造函數、賦值函數和析構函數,這既是對 C++程序員的基本要求,也是《Effective C++》中特別強調的條款。仔細學習這個類,特別注意加注釋的得分點和加分點的意義,這樣就具備了 60%以上的 C++基本功!試題 8: 請說出 static 和 const 關鍵字盡可能多的作用解答:static 關鍵字至少有下列 n 個作用:(1)函數體內 static 變量的作用范圍為該函數體,不同于 auto 變量,該變量的內存只被分配一次,因此其值在下次調用時仍維持上次的值;(2)在模塊內的 static 全局變量可以被模塊內所用函數訪問,但不能被模塊外其它函數訪問;(3)在模塊內的 static 函數只可被這一模塊內的其它函數調用,這個函數的使用范圍被限制在聲明它的模塊內;(4)在類中的 static 成員變量屬于整個類所擁有,對類的所有對象只有一份拷貝;(5)在類中的 static 成員函數屬于整個類所擁有,這個函數不接收 this 指針,因而只能訪問類的static 成員變量。const 關鍵字至少有下列 n 個作用:(1)欲阻止一個變量被改變,可以使用 const 關鍵字。在定義該 const 變量時,通常需要對它進行初始化,因為以后就沒有機會再去改變它了;(2)對指針來說,可以指定指針本身為 const,也可以指定指針所指的數據為 const,或二者同時指定為 const;(3)在一個函數聲明中,const 可以修飾形參,表明它是一個輸入參數,在函數內部不能改變其值;(4)對于類的成員函數,若指定其為 const 類型,則表明其是一個常函數,不能修改類的成員變量;(5)對于類的成員函數,有時候必須指定其返回值為 const 類型,以使得其返回值不為“左值”。例73如:const classA operator*(const classA& a1,const classA& a2);operator*的返回結果必須是一個 const 對象。如果不是,這樣的變態代碼也不會編譯出錯:classA a, b, c;(a * b) = c; // 對 a*b 的結果賦值操作(a * b) = c 顯然不符合編程者的初衷,也沒有任何意義。剖析:驚訝嗎?小小的 static 和 const 居然有這么多功能,我們能回答幾個?如果只能回答 1~2 個,那還真得閉關再好好修煉修煉。這個題可以考查面試者對程序設計知識的掌握程度是初級、中級還是比較深入,沒有一定的知識廣度和深度, 不可能對這個問題給出全面的解答。 大多數人只能回答出 static 和 const 關鍵字的部分功能。4.技巧題試題 1: 請寫一個 C 函數,若處理器是 Big_endian 的,則返回 0;若是 Little_endian 的,則返回 1解答:int checkCPU(){{union w{int a;char b;} c;c.a = 1;return (c.b == 1);}}剖析:嵌入式系統開發者應該對 Little-endian 和 Big-endian 模式非常了解。采用 Little-endian 模式的 CPU對操作數的存放方式是從低字節到高字節,而 Big-endian 模式對操作數的存放方式是從高字節到低字節。例如,16bit 寬的數 0x1234 在 Little-endian 模式 CPU 內存中的存放方式(假設從地址 0x4000開始存放)為:
內 存 地址0x4000 0x4001
存 放 內容0x34 0x12
而在 Big-endian 模式 CPU 內存中的存放方式則為:
內 存 地址0x4000 0x4001
存 放 內容0x12 0x34
32bit 寬的數 0x12345678 在 Little-endian 模式 CPU 內存中的存放方式 (假設從地址 0x4000 開始存放)為:
內 存 地 0x4000 0x4001 0x4002 0x4003
74
存 放 內容0x78 0x56 0x34 0x12
而在 Big-endian 模式 CPU 內存中的存放方式則為:
內 存 地址0x4000 0x4001 0x4002 0x4003
存 放 內容0x12 0x34 0x56 0x78
聯合體 union 的存放順序是所有成員都從低地址開始存放,面試者的解答利用該特性,輕松地獲得了CPU 對內存采用 Little-endian 還是 Big-endian 模式讀寫。如果誰能當場給出這個解答,那簡直就是一個天才的程序員。試題 2: 寫一個函數返回 1+2+3+…+n 的值(假定結果不會超過長整型變量的范圍)解答:int Sum( int n ){return ( (long)1 + n) * n / 2; //或 return (1l + n) * n / 2;}剖析:對于這個題,只能說,也許最簡單的答案就是最好的答案。下面的解答,或者基于下面的解答思路去優化,不管怎么“折騰”,其效率也不可能與直接 return ( 1 l + n ) * n / 2 相比!int Sum( int n ){long sum = 0;for( int i=1; i<=n; i++ ){sum += i;}return sum;}所以程序員們需要敏感地將數學等知識用在程序設計中。一道著名外企面試題的抽絲剝繭宋寶華 21cnbao@21cn.com軟件報問題:對于一個字節( 8bit)的數據,求其中“1”的個數,要求算法的執行效率盡可能地高。分析:作為一道著名外企的面試題,看似簡單,實則可以看出一個程序員的基本功底的扎實程度。你或許已經想到很多方法,譬如除、余操作,位操作等,但都不是最快的。本文一步步分析,直到最后給出一個最快的方法,相信你看到本文最后的那個最快的方法時會有驚詫的感覺。解答:首先,很自然的,你想到除法和求余運算,并給出了如下的答案:方法 1:使用除、余操作75#include#define BYTE unsigned charint main(int argc, char *argv[]){int i, num = 0;BYTE a;/* 接收用戶輸入 */printf("/nPlease Input a BYTE(0~255):");scanf("%d", &a);/* 計算 1的個數 */for (i = 0; i < 8; i++){if (a % 2 == 1){num++;}a = a / 2;}printf("/nthe num of 1 in the BYTE is %d", num);return 0;}很遺憾,眾所周知,除法操作的運算速率實在是很低的,這個答案只能意味著面試者被淘汰!好,精明的面試者想到了以位操作代替除法和求余操作,并給出如下答案:方法 2:使用位操作#include#define BYTE unsigned charint main(int argc, char *argv[]){int i, num = 0;BYTE a;/* 接收用戶輸入 */printf("/nPlease Input a BYTE(0~255):");scanf("%d", &a);/* 計算 1的個數 */for (i = 0; i < 8; i++){num += (a >> i) &0x01;}/*或者這樣計算 1的個數: *//* for(i=0;i<8;i++){if((a>>i)&0x01)num++;}76*/printf("/nthe num of 1 in the BYTE is %d", num);return 0;}方法二中 num += (a >> i) &0x01;操作的執行效率明顯高于方法一中的if (a % 2 == 1){num++;}a = a / 2;到這個時候,面試者有被錄用的可能性了,但是,難道最快的就是這個方法了嗎?沒有更快的了嗎?方法二真的高山仰止了嗎?能不能不用做除法、位操作就直接得出答案的呢?于是你想到把 0~255 的情況都羅列出來,并使用分支操作,給出如下答案:方法 3:使用分支操作#include#define BYTE unsigned charint main(int argc, char *argv[]){int i, num = 0;BYTE a;/* 接收用戶輸入 */printf("/nPlease Input a BYTE(0~255):");scanf("%d", &a);/* 計算 1的個數 */switch (a){case 0x0:num = 0;break;case 0x1:case 0x2:case 0x4:case 0x8:case 0x10:case 0x20:case 0x40:case 0x80:num = 1;break;case 0x3:case 0x6:case 0xc:case 0x18:77case 0x30:case 0x60:case 0xc0:num = 2;break;//...}printf("/nthe num of 1 in the BYTE is %d", num);return 0;}方法三看似很直接,實際執行效率可能還會小于方法二,因為分支語句的執行情況要看具體字節的值,如果a=0,那自然在第 1 個case 就得出了答案,但是如果a=255,則要在最后一個case 才得出答案,即在進行了255次比較操作之后!看來方法三不可?。〉欠椒ㄈ峁┝艘粋€思路,就是羅列并直接給出值,離最后的方法四只有一步之遙。眼看著就要被這家著名外企錄用,此時此刻,絕不對放棄尋找更快的方法。終于,靈感一現,得到方法四,一個令你心潮澎湃的答案,快地令人咋舌,算法中不需要進行任何的運算。你有足夠的信心了,把下面的答案遞給面試官:方法 4:直接得到結果#include#define BYTE unsigned char/* 定義查找表 */BYTE numTable[256] ={0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3,3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3,4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4,3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3,4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6,6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4,5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3,4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4,4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6,7, 6, 7, 7, 8};int main(int argc, char *argv[]){int i, num = 0;BYTE a = 0;/* 接收用戶輸入 */printf("/nPlease Input a BYTE(0~255):");scanf("%d", &a);/* 計算 1的個數 *//* 用 BYTE直接作為數組的下標取出 1的個數,妙哉! */78printf("/nthe num of 1 in the BYTE is %d", checknum[a]);return 0;}這是個典型的空間換時間算法,把0~255中1的個數直接存儲在數組中,字節a作為數組的下標,checknum[a]直接就是 a中“1”的個數!算法的復雜度如下:時間復雜度: O(1)空間復雜度: O(2n)恭喜你,你已經被這家著名的外企錄用!老總向你伸出手,說:“Welcome to our company”。C/C++結構體的一個高級特性――指定成員的位數宋寶華 21cnbao@21cn.comsweek在大多數情況下,我們一般這樣定義結構體:struct student{unsigned int sex;unsigned int age;};對于一般的應用,這已經能很充分地實現數據了的“封裝”。但是,在實際工程中,往往碰到這樣的情況:那就是要用一個基本類型變量中的不同的位表示不同的含義。譬如一個 cpu 內部的標志寄存器,假設為16 bit,而每個bit 都可以表達不同的含義,有的表示結果是否為0,有的表示是否越界等等。這個時候我們用什么數據結構來表達這個寄存器呢?答案還是結構體!為達到此目的,我們要用到結構體的高級特性,那就是在基本成員變量的后面添加:: 數據位數組成新的結構體:struct xxx{成員 1類型成員 1 :成員 1位數;成員 2類型成員 2 :成員 2位數;成員 3類型成員 3 :成員 3位數;};基本的成員變量就會被拆分!這個語法在初級編程中很少用到,但是在高級程序設計中不斷地被用到!例如:struct student{unsigned int sex : 1;unsigned int age : 15;};上述結構體中的兩個成員 sex和 age加起來只占用了一個 unsigned int的空間 (假設 unsigned int為 16位)?;境蓡T變量被拆分后,訪問的方法仍然和訪問沒有拆分的情況是一樣的,例如:79struct student sweek;sweek.sex = MALE;sweek.age = 20;雖然拆分基本成員變量在語法上是得到支持的,但是并不等于我們想怎么分就怎么分,例如下面的拆分顯然是不合理的:struct student{unsigned int sex : 1;unsigned int age : 12;};這是因為 1+12 = 13,不能再組合成一個基本成員,不能組合成char、int 或任何類型,這顯然是不能“自圓其說”的。在拆分基本成員變量的情況下,我們要特別注意數據的存放順序,這還與 CPU 是Big endian 還是Little endian來決定。 Little endian和 Big endian是 CPU存放數據的兩種不同順序。對于整型、長整型等數據類型,Bigendian 認為第一個字節是最高位字節(按照從低地址到高地址的順序存放數據的高位字節到低位字節);而Little endian 則相反,它認為第一個字節是最低位字節(按照從低地址到高地址的順序存放數據的低位字節到高位字節)。我們定義 IP 包頭結構體為:struct iphdr {#if defined(__LITTLE_ENDIAN_BITFIELD)__u8 ihl:4,version:4;#elif defined (__BIG_ENDIAN_BITFIELD)__u8 version:4,ihl:4;#else#error "Please fix <asm/byteorder.h>"#endif__u8 tos;__u16 tot_len;__u16 id;__u16 frag_off;__u8 ttl;__u8 protocol;__u16 check;__u32 saddr;__u32 daddr;/*The options start here. */};在 Little endian模式下, iphdr中定義:__u8 ihl:4,version:4;其存放方式為:第 1 字節低4 位ihl80第 1字節高 4位 version( IP的版本號)若在 Big endian 模式下還這樣定義,則存放方式為:第 1 字節低4 位version (IP 的版本號)第 1 字節高4 位ihl這與實際的 IP協議是不匹配的,所以在 Linux內核源代碼中, IP包頭結構體的定義利用了宏:#if defined(__LITTLE_ENDIAN_BITFIELD)…#elif defined (__BIG_ENDIAN_BITFIELD)…#endif來區分兩種不同的情況。由此我們總結全文的主要觀點:( 1)C/C++語言的結構體支持對其中的基本成員變量按位拆分;( 2)拆分的位數應該是合乎邏輯的,應仍然可以組合為基本成員變量;要特別注意拆分后的數據的存放順序,這一點要結合具體的CPU 的結構。C/C++中的近指令、遠指針和巨指針宋寶華 email:21cnbao@21cn.com sweek在我們的 C/C++學習生涯中、在我們大腦的印象里,通常只有指針的概念,很少聽說指針還有遠、近、巨之分的,從沒聽說過什么近指針、遠指針和巨指針??梢?,某年某月的某一天,你突然看到這樣的語句:char near *p; /*定義一個字符型“近”指針*/char far *p; /*定義一個字符型“遠”指針*/char huge *p; /*定義一個字符型“巨”指針*/實在不知道語句中的“near”、“far”、“huge”是從哪里冒出來的,是個什么概念!本文試圖對此進行解答,解除許多人的困惑。這一點首先要從 8086 處理器體系結構和匯編淵源講起。大家知道,8086 是一個16 位處理器,它設定了四個段寄存器,專門用來保存段地址: CS(Code Segment):代碼段寄存器;DS(Data Segment):數據段寄存器; SS(Stack Segment):堆棧段寄存器;ES(Extra Segment):附加段寄存器。8086 采用段式訪問,訪問本段( 64K 范圍內)的數據或指令時,不需要變更段地址(意味著段地址寄存器不需修改),而訪問本段范圍以外的數據或指令時,則需要變更段地址(意味著段地址寄存器需要修改)。因此,在 16 位處理器環境下,如果訪問本段內地址的值,用一個16 位的指針(表示段內偏移)就可以訪問到;而要訪問本段以外地址的值,則需要用 16 位的段內偏移+16位的段地址,總共 32位的指針。這樣,我們就知道了遠、近指針的區別:? 近指針是只能訪問本段、只包含本段偏移的、位寬為16 位的指針;? 遠指針是能訪問非本段、包含段偏移和段地址的、位寬為32 位的指針。近指針只能對64k 字節數據段內的地址進行存取,如:char near *p;p=(char near *)0xffff;遠指針是 32 位指針,它表示段地址:偏移地址,遠指針可以進行跨段尋址,可以訪問整個內存的地址。如定81義遠程指針 p指向 0x1000段的 0x2號地址,即 1000:0002,則可寫作:char far *p;p=(char far *)0x10000002;除了遠指針和近指針外,還有一個巨指針的概念。和遠指針一樣,巨指針也是 32 位的指針,指針也表示為16 位段:16 位偏移,也可以尋址任何地址。它和遠指針的區別在于進行了規格化處理。遠指針沒有規格化,可能存在兩個遠指針實際指向同一個物理地址,但是它們的段地址和偏移地址不一樣,如 23B0:0004 和23A1:00F4 都指向同一個物理地址23604!巨指針通過特定的例程保證:每次操作完成后其偏移量均小于 10h,即只有最低4 位有數值,其余數值都被進位到段地址上去了,這樣就可以避免 Far 指針在64K 邊界時出乎意料的回繞的行為。當然,一次操作必須小于64K。下面的函數可以將遠指針轉換為巨指針:void normalize(void far ** p){*p=(void far *)(((long)*p&0xffff000f)+(((long)*p&0x0000fff00<<12));}從上面的函數中我們再一次看到了指針之指針的使用,這個函數要修改指針的值,因此必須傳給它的指針的指針作為參數。講到這里,筆者要強調的是: 近指針、遠指針、巨指針是段尋址的16bit 處理器的產物(如果處理器是16 位的,但是不采用段尋址的話,也不存在近指針、遠指針、巨指針的概念),當前普通 PC 所使用的32bit 處理器( 80386 以上)一般運行在保護模式下的,指針都是32 位的,可平滑地址,已經不分遠、近指針了。但是在嵌入式系統領域下, 8086 的處理器仍然有比較廣泛的市場,如AMD 公司的AM186ED、AM186ER 等處理器,開發這些系統的程序時,我們還是有必要弄清楚指針的尋址范圍。如果讀者還想更透徹地理解本文講解的內容,不妨再溫習一下微機原理、 8086 匯編,并參考C/C++高級編程書籍的相關內容。從兩道經典試題談 C/C++中聯合體(union)的使用宋寶華 21cnbaosweek@21cn.com試題一: 編寫一段程序判斷系統中的 CPU是 Little endian還是 Big endian模式?分析:作為一個計算機相關專業的人,我們應該在計算機組成中都學習過什么叫 Little endian 和 Big endian。Littleendian 和 Big endian是 CPU存放數據的兩種不同順序。對于整型、長整型等數據類型,Big endian 認為第一個字節是最高位字節(按照從低地址到高地址的順序存放數據的高位字節到低位字節);而 Little endian則相反,它認為第一個字節是最低位字節(按照從低地址到高地址的順序存放數據的低位字節到高位字節)。例如,假設從內存地址 0x0000 開始有以下數據:
0x0000 0x0001 0x0002 0x0003
0x12 0x34 0xab 0xcd
如果我們去讀取一個地址為 0x0000的四個字節變量,若字節序為 big-endian,則讀出結果為0x1234abcd;若字節序位 little-endian,則讀出結果為0xcdab3412。如果我們將0x1234abcd 寫入到以0x0000 開始的內存中,則 Little endian 和Big endian 模式的存放結果如下:
地址 0x0000 0x0001 0x0002 0x0003
82
big-endian 0x12 0x34 0xab 0xcd
little-endian 0xcd 0xab 0x34 0x12
一般來說, x86系列 CPU都是 little-endian的字節序, PowerPC通常是 Big endian,還有的CPU 能通過跳線來設置 CPU 工作于Little endian 還是Big endian 模式。解答:顯然,解答這個問題的方法只能是將一個字節( CHAR/BYTE 類型)的數據和一個整型數據存放于同樣的內存開始地址,通過讀取整型數據,分析 CHAR/BYTE 數據在整型數據的高位還是低位來判斷CPU 工作于Littleendian 還是 Big endian模式。得出如下的答案:typedef unsigned char BYTE;int main(int argc, char* argv[]){unsigned int num,*p;p = #num = 0;*(BYTE *)p = 0xff;if(num == 0xff){printf("The endian of cpu is little/n");}else //num == 0xff000000{printf("The endian of cpu is big/n");}return 0;}除了上述方法(通過指針類型強制轉換并對整型數據首字節賦值,判斷該賦值賦給了高位還是低位)外,還有沒有更好的辦法呢?我們知道, union 的成員本身就被存放在相同的內存空間(共享內存,正是union 發揮作用、做貢獻的去處),因此,我們可以將一個 CHAR/BYTE 數據和一個整型數據同時作為一個union 的成員,得出如下答案:int checkCPU(){{union w{int a;char b;} c;c.a = 1;return (c.b == 1);}}實現同樣的功能,我們來看看 Linux操作系統中相關的源代碼是怎么做的:static union { char c[4]; unsigned long l; } endian_test = { { 'l', '?', '?', 'b' } };83#define ENDIANNESS ((char)endian_test.l)Linux 的內核作者們僅僅用一個union 變量和一個簡單的宏定義就實現了一大段代碼同樣的功能!由以上一段代碼我們可以深刻領會到 Linux 源代碼的精妙之處!(如果ENDIANNESS=’l’表示系統為little endian,為’b’表示big endian)試題二: 假設網絡節點 A和網絡節點 B中的通信協議涉及四類報文,報文格式為“報文類型字段+報文內容的結構體”,四個報文內容的結構體類型分別為STRUCTTYPE1~ STRUCTTYPE4,請編寫程序以最簡單的方式組織一個統一的報文數據結構。分析:報文的格式為“報文類型+報文內容的結構體”,在真實的通信中,每次只能發四類報文中的一種,我們可以將四類報文的結構體組織為一個 union(共享一段內存,但每次有效的只是一種),然后和報文類型字段統一組織成一個報文數據結構。解答:根據上述分析,我們很自然地得出如下答案:typedef unsigned char BYTE;//報文內容聯合體typedef union tagPacketContent{STRUCTTYPE1 pkt1;STRUCTTYPE2 pkt2;STRUCTTYPE3 pkt1;STRUCTTYPE4 pkt2;}PacketContent;//統一的報文數據結構typedef struct tagPacket{BYTE pktType;PacketContent pktContent;}Packet;總結在 C/C++程序的編寫中,當多個基本數據類型或復合數據結構要占用同一片內存時,我們要使用聯合體(試題一是這樣的例證);當多種類型,多個對象,多個事物只取其一時(我們姑且通俗地稱其為“n 選 1”),我們也可以使用聯合體來發揮其長處(試題二是這樣的例證)。基于 ARM 的嵌入式 Linux 移植真實體驗基于 ARM 的嵌入式 Linux 移植真實體驗(1) ――基本概念宋寶華 21cnbao@21cn.com出處:dev.yesky.com841.引言ARM 是 Advanced RISC Machines(高級精簡指令系統處理器)的縮寫,是 ARM 公司提供的一種微處理器知識產權(IP)核。ARM 的應用已遍及工業控制、消費類電子產品、通信系統、網絡系統、無線系統等各類產品市場?;?ARM 技術的微處理器應用約占據了 32 位 RISC 微處理器 75%以上的市場份額。揭開你的手機、MP3、PDA,嘿嘿,里面多半藏著一個基于 ARM 的微處理器!ARM 內核的數個系列(ARM7、ARM9、ARM9E、ARM10E、SecurCore、Xscale、StrongARM), 各自滿足不同應用領域的需求,無孔不入的滲入嵌入式系統各個角落的應用。這是一個 ARM 的時代!下面的圖片顯示了 ARM 的隨處可見:有人的地方就有江湖(《武林外傳》),有嵌入式系統的地方就有 ARM。構建一個復雜的嵌入式系統,僅有硬件是不夠的,我們還需要進行操作系統的移植。我們通常在 ARM平臺上構建 Windows CE、Linux、Palm OS 等操作系統,其中 Linux 具有開放源代碼的優點。下圖顯示了基于 ARM 嵌入式系統中軟件與硬件的關系:日前,筆者作為某嵌入式 ARM(硬件)/Linux(軟件)系統的項目負責人,帶領項目組成員進行了下述85工作:(1)基于 ARM920T 內核 S3C2410A CPU 的電路板設計;(2)ARM 處理下底層軟件平臺搭建:a.Bootloader 的移植;b.嵌入式 Linux 操作系統內核的移植;c.嵌入式 Linux 操作系統根文件系統的創建;d.電路板上外設 Linux 驅動程序的編寫。本文將真實地再現本項目開發過程中作者的心得,以便與廣大讀者共勉。第一章將簡單地介紹本 ARM開發板的硬件設計,第二章分析 Bootloader 的移植方法,第三章敘述嵌入式 Linux 的移植及文件系統的構建方法,第四章講解外設的驅動程序設計,第五章給出一個已構建好的軟硬件平臺上應用開發的實例。如果您有良好的嵌入式系統開發基礎,您將非常容易領會本文講解地內容。即便是您從來沒有嵌入式系統的開發經歷,本文也力求讓您讀起來不覺得生澀。 您可以通過如下 email 與作者聯系:21cnbao@21cn.com。2.ARM 體系結構作為一種 RISC 體系結構的微處理器,ARM 微處理器具有 RISC 體系結構的典型特征。還具有如下增強特點:(l)在每條數據處理指令當中,都控制算術邏輯單元(ALU)和移位器,以使 ALU 和移位器獲得最大的利用率;(2)自動遞增和自動遞減的尋址模式,以優化程序中的循環;(3)同時 Load 和 Store 多條指令,以增加數據吞吐量;(4)所有指令都條件執行,以增大執行吞吐量。ARM 體系結構的字長為 32 位,它們都支持 Byte(8 位)、Halfword(16 位)和 Word(32 位)3 種數據類型。ARM 處理器支持 7 種處理器模式,如下表:大部分應用程序都在 User 模式下運行。當處理器處于 User 模式下時,執行的程序無法訪問一些被保護的系統資源,也不能改變模式,否則就會導致一次異常。對系統資源的使用由操作系統來控制。User 模式之外的其它幾種模式也稱為特權模式,它們可以完全訪問系統資源,可以自由地改變模式。其中的 FIQ、IRQ、supervisor、Abort 和 undefined 5 種模式也被稱為異常模式。在處理特定的異常時,系統進入這幾種模式。這 5 種異常模式都有各自的額外的寄存器,用于避免在發生異常的時候與用戶模式下的程序發生沖突。還有一種模式是 system 模式,任何異常都不會導致進入這一模式,而且它使用的寄存器和 User 模式下基本相同。它是一種特權模式,用于有訪問系統資源請求而又需要避免使用額外的寄存器的操作系統任務。程序員可見的 ARM 寄存器共有 37 個:31 個通用寄存器以及 6 個針對 ARM 處理器的不同工作模式所設立的專用狀態寄存器,如下圖:86ARM9 采用 5 級流水線操作:指令預取、譯碼、執行、數據緩沖、寫回。ARM9 設置了 16 個字的數據緩沖和 4 個字的地址緩沖。這 5 級流水已被很多的 RISC 處理器所采用,被看作 RISC 結構的“經典”。3.硬件設計3.1 S3C2410A 微控制器電路板上的 ARM 微控制器 S3C2410A 采用了 ARM920T 核,它由 ARM9TDMI、存儲管理單元 MMU 和高速緩存三部分組成。 其中, MMU 可以管理虛擬內存, 高速緩存由獨立的 16KB 地址和 16KB 數據高速 Cache 組成。ARM920T 有兩個內部協處理器:CP14 和 CP15。CP14 用于調試控制,CP15 用于存儲系統控制以及測試控制。S3C2410A 集成了大量的內部電路和外圍接口:? LCD 控制器(支持 STN 和 TFT 帶有觸摸屏的液晶顯示屏)? SDRAM 控制器? 3 個通道的 UART? 4 個通道的 DMA? 4 個具有 PWM 功能的計時器和一個內部時鐘? 8 通道的 10 位 ADC? 觸摸屏接口? I2C 總線接口? 12S 總線接口? 兩個 USB 主機接口? 一個 USB 設備接口? 兩個 SPI 接口? SD 接口? MMC 卡接口S3C2410A 集成了一個具有日歷功能的 RTC 和具有 PLL(MPLL 和 UPLL)的芯片時鐘發生器。MPLL 產生主時鐘,能夠使處理器工作頻率最高達到 203MHz。這個工作頻率能夠使處理器輕松運行 WinCE、Linux 等操作系統以及進行較為復雜的信息處理。UPLL 則產生實現 USB 模塊的時鐘。下圖顯示了 S3C2410A 的集成資源和外圍接口:87我們需要對上圖中的 AHB 總線和 APB 總線的概念進行一番解釋。ARM 核開發的目的,是使其作為復雜片上系統的一個處理單元來應用的,所以還必須提供一個 ARM 與其它片上宏單元通信的接口。為了減少不必要的設計資源的浪費,ARM 公司定義了 AMBA(Advanced Microcontroller Bus Architecture)總線規范,它是一組針對基于 ARM 核的、片上系統之間通信而設計的、標準的、開放協議。在 AMBA 總線規范中,定義了 3 種總線:(l)AHB—Advanced High Performace Bus,用于高性能系統模塊的連接,支持突發模式數據傳輸和事務分割;(2)ASB—Advanced System Bus,也用于高性能系統模塊的連接,支持突發模式數據傳輸,這是較老的系統總線格式,后來由 AHB 總線替代;(3)APB—Advanced PeriPheral Bus,用于較低性能外設的簡單連接,一般是接在 AHB 或 ASB 系統總線上的第二級總線。典型的 AMBA 總線系統如下圖:S3C2410A 將系統的存儲空間分成 8 個 bank, 每個 bank 的大小是 128M 字節, 共 1G 字節。Bank0 到 bank5的開始地址是固定的,用于 ROM 或 SRAM。bank6 和 bank7 可用于 ROM、SRAM 或 SDRAM。所有內存塊的訪問周期都可編程,外部 Wait 也能擴展訪問周期。下圖給出了 S3C2410A 的內存組織:88下圖給出了 S3C2410A 的數據總線、地址總線和片選電路:SDRAM 控制信號、集成 USB 接口電路:89內核與存儲單元供電電路 (S3C2410A 對于片內的各個部件采用了獨立的電源供給,內核采用 1.8V 供電,存儲單元采用 3.3V 獨立供電):JTAG 標準通過邊界掃描技術提供了對電路板上每一元件的功能、互聯及相互間影響進行測試的方法,極大地方便了系統電路的調試。測試接入端口 TAP 的管腳定義如下:? TCK:專用的邏輯測試時鐘,時鐘上升沿按串行方式對測試指令、數據及控制信號進行移位操作,下降沿用于對輸出信號移位操作;? TMS:測試模式選擇,在 TCK 上升沿有效的邏輯測試控制信號;? TDI:測試數據輸入,用于接收測試數據與測試指令;? TDO:測試數據輸出,用于測試數據的輸出。S3C2410A 調試用 JTAG 接口電路:903.2 SDRAM 存儲器SDRAM 被用來存放操作系統(從 FLASH 解壓縮拷入)以及存放各類動態數據,采用 SAMSUNG 公司的K4S561632,它是 4Mxl6bitx4bank 的同步 DRAM,容量為 32MB。 用 2 片 K4S561632 實現位擴展,使數據總線寬度達到 32bit,總容量達到 64MB,將其地址空間映射在 S3C2410A 的 bank6。SDRAM 所有的輸入和輸出都與系統時鐘 CL K 上升沿同步,由輸入信號 RA S、CA S、WE 組合產生 SDRAM控制命令,其基本的控制命令如下:SDRAM 在具體操作之前首先必須通過 MRS 命令設置模式寄存器,以便確定 SDRAM 的列地址延遲、突發類型、突發長度等工作模式;再通過 ACT 命令激活對應地址的組,同時輸入行地址;然后通過 RD 或WR 命令輸入列地址,將相應數據讀出或寫入對應的地址;操作完成后用 PCH 命令或 BT 命令中止讀或寫操作。在沒有操作的時候,每隔一段時間必須用 ARF 命令刷新數據,防止數據丟失。下圖給出了 SDRAM 的連接電路:913.3 FLASH 存儲器NOR 和 NAND 是現在市場上兩種主要的非易失閃存技術。NOR 的特點是芯片內執行(XIP,Execute In Place),即應用程序可直接在 Flash 閃存內運行,不必把代碼讀到系統 RAM 中。NOR 的傳輸效率很高,在 1~4MB 的小容量時具有很高的成本效益,但是很低的寫入和擦除速度大大影響了它的性能。NAND 結構能提供極高的單元密度,可以達到高存儲密度,并且寫入和擦除的速度也很快。應用 NAND的困難在于 Flash 的管理和需要特殊的系統接口,S3C2410A 內嵌了 NAND FLASH 控制器。S3C2410A 支持從 GCS0 上的 NOR FLASH 啟動(16 位或 32 位)或從 NAND FLASH 啟動,需要通過 OM0 和OM1 上電時的上下拉來設置:在系統中分別采用了一片 NOR FLASH(28F640)和 NAND FLASH(K9S1208),電路如下圖:923.4 串口S3C2410 內部集成了 UART 控制器,實現了并串轉換。外部還需提供 CMOS/TTL 電平與 RS232 之間的轉換:3.5 以太網以太網控制芯片采用 CIRRUS LOGIC 公司生產的 CS8900A,其突出特點是使用靈活,其物理層接口、數據傳輸模式和工作模式等都能根據需要而動態調整,通過內部寄存器的設置來適應不同的應用環境。它符合 IEEE803.3 以太網標準, 帶有傳送、 接收低通濾波的 10Base-T 連接端口, 支持 10Base2, 10Base5和 10Base-F 的 AUI 接口,并能自動生成報頭,自動進行 CRC 檢驗,在沖突后自動重發。CS8900A 支持的傳輸模式有 I/O 和 Memory 模式。當 CS8900A 有硬件復位或軟件復位時,它將默認成為 8位工作模式。因此,要使 CS8900A 工作于 16 位模式,系統必須在訪問之前提供給總線高位使能管腳(/SBHE)一個由高到低、再由低到高變化的電平。933.6 USB 接口USB 系統由 USB 主機(USB Host)、USB 集線器(USB Hub)和 USB 設備(USB Device)組成。USB 和主機系統的接口稱作主機控制器(Host Controller),它是由硬件和軟件結合實現的。根集線器是綜合于主機系統內部的,用以提供 USB 的連接點。USB 的設備包括集線器(Hub)和功能器件(Function)。S3C2410A 集成了 USB host 和 USB device,外部連接電路如下圖:3.7 電源LDO(Low Dropout)屬于 DC/DC 變換器中的降壓變換器,它具有低成本、低噪聲、低功耗等突出優點,另外它所需要的外圍器件也很少,通常只有 1~2 個旁路電容。在電路板上我們分別用兩個 LDO 來實現 5V 向 3.3V(存儲接口電平)和 1.8V(ARM 內核電平)的轉換。94up 監控電路采用 MAX708 芯片,提供上電、掉電以及降壓情況下的復位輸出及低電平有效的人工復位輸出:3.8 其它SN74LVTH62245A 提供總線驅動和緩沖能力:S3C2410A 集成 LCD 液晶顯示器控制電路,外部引出接口:95觸摸屏有電阻式、電容式等,其本質是一種將手指在屏幕上的觸點位置轉化為電信號的傳感器。手指觸到屏幕,引起觸點位置電阻或電容的變化,再通過檢測這一電性變化,從而獲得手指的坐標位置。通過 S3C2410A 集成的 AD 功能,完成電信號向屏幕坐標的轉化,觸摸屏接口如下:鍵盤則直接利用 CPU 的可編程 I/O 口,若連接 mxn 鍵盤,則需要 m+n 個可編程 I/O 口,由軟件實現鍵盤掃描,識別按鍵:3.9 整體架構下圖呈現了 ARM 處理器及外圍電路的整體設計框架:964.小結本章講解了基于 S3C2410A ARM 處理器電路板硬件設計的基本組成,為后續各章提供了總體性的準備工作?;?ARM 的嵌入式 Linux 移植真實體驗(2)――BootLoader宋寶華 21cnbao@21cn.com 出處:dev.yesky.comBootLoader 指系統啟動后,在操作系統內核運行之前運行的一段小程序。通過 BootLoader,我們可以初始化硬件設備、建立內存空間的映射圖,從而將系統的軟硬件環境帶到一個合適的狀態,以便為最終調用操作系統內核準備好正確的環境。通常,BootLoader 是嚴重地依賴于硬件而實現的,特別是在嵌入式世界。因此, 在嵌入式世界里建立一個通用的 BootLoader 幾乎是不可能的。盡管如此,我們仍然可以對 BootLoader 歸納出一些通用的概念來,以指導用戶特定的 BootLoader 設計與實現。BootLoader 的實現依賴于 CPU 的體系結構,因此大多數 BootLoader 都分為 stage1 和 stage2 兩大部分。依賴于CPU 體系結構的代碼,比如設備初始化代碼等,通常都放在 stage1 中,而且通常都用匯編語言來實現,以達到短小精悍的目的。而 stage2 則通常用 C 語言來實現,這樣可以實現更復雜的功能,而且代碼會具有更好的可讀性和可移植性。BootLoader 的 stage1 通常包括以下步驟:? 硬件設備初始化;? 為加載 Boot Loader 的 stage2 準備 RAM 空間;? 拷貝 Boot Loader 的 stage2 到 RAM 空間中;? 設置好堆棧;97? 跳轉到 stage2 的 C 入口點。Boot Loader 的 stage2 通常包括以下步驟:? 初始化本階段要使用到的硬件設備;? 檢測系統內存映射(memory map);? 將 kernel 映像和根文件系統映像從 flash 上讀到 RAM 空間中;? 為內核設置啟動參數;? 調用內核。本系統中的 BootLoader 參照韓國 mizi 公司的 vivi 進行修改。1.開發環境我們購買了武漢創維特信息技術有限公司開發的具有自主知識產權的應用于嵌入式軟件開發的集成軟、硬件開發平臺ADT(ARM Development Tools)它為基于 ARM 核的嵌入式應用提供了一整套完備的開發方案,包括程序編輯、工程管理和設置、程序編譯、程序調試等。ADT 嵌入式開發環境由 ADT Emulator for ARM 和 ADT IDE for ARM 組成。 ADT Emulator for ARM 通過 JTAG 實現主機和目標機之間的調試支持功能。它無需目標存儲器,不占用目標系統的任何端口資源。目標程序直接在目標板上運行,通過 ARM 芯片的 JTAG 邊界掃描口進行調試,屬于完全非插入式調試,其仿真效果接近真實系統。ADT IDE for ARM 為用戶提供高效明晰的圖形化嵌入式應用軟件開發環境,包括一整套完備的面向嵌入式系統的開發和調試工具:源碼編輯器、工程管理器、工程編譯器(編譯器、匯編器和連接器)、集成調試環境、ADT Emulator forARM 調試接口等。其界面同 Microsoft Visual Studio 環境相似,用戶可以在 ADT IDE for ARM 集成開發環境中創建工程、打開工程,建立、打開和編輯文件,編譯、連接、設置、運行、調試嵌入式應用程序。ADT 嵌入式軟件開發環境采用主機-目標機交叉開發模型。ADT IDE for ARM 運行于主機端,而 ADT Emulator for ARM實現 ADT IDE for ARM 與目標機之間的連接。開發時,首先由 ADT IDE for ARM 編譯連接生成目標代碼,然后建立與 ADT Emulator for ARM 之間的調試通道,調試通道建立成功后,就可以在 ADT IDE for ARM 中通過 ADT Emulatorfor ARM 控制目標板實現目標程序的調試,包括將目標代碼下載到目標機中,控制程序運行,調試信息觀察等等。2.ARM 匯編ARM 本身屬于 RISC 指令系統,指令條數就很少,而其編程又以 C 等高級語言為主,我們僅需要在 Bootloader 的第一階段用到少量匯編指令:(1)+-運算ADD r0, r1, r298―― r0 := r1 + r2SUB r0, r1, r2―― r0 := r1 - r2其中的第二個操作數可以是一個立即數:ADD r3, r3, #1―― r3 := r3 + 1第二個操作數還可以是位移操作后的結果:ADD r3, r2, r1, LSL #3―― r3 := r2 + 8.r1(2)位運算AND r0, r1, r2―― r0 := r1 and r2ORR r0, r1, r2―― r0 := r1 or r2EOR r0, r1, r2―― r0 := r1 xor r2BIC r0, r1, r2―― r0 := r1 and not r2(3)寄存器搬移MOV r0, r2―― r0 := r2MVN r0, r2―― r0 := not r2(4)比較CMP r1, r2―― set cc on r1 - r2CMN r1, r2―― set cc on r1 + r2TST r1, r2―― set cc on r1 and r2TEQ r1, r2―― set cc on r1 or r2這些指令影響 CPSR 寄存器中的 (N, Z, C, V) 位(5)內存操作LDR r0, [r1]―― r0 := mem [r1]STR r0, [r1]― mem [r1] := r0LDR r0, [r1, #4]―― r0 := mem [r1+4]LDR r0, [r1, #4] !―― r0 := mem [r1+4] r1 := r1 + 4LDR r0, [r1], #4―― r0 := mem [r1] r1 := r1 +499LDRB r0 , [r1]―― r0 := mem8 [r1]LDMIA r1, {r0, r2, r5}―― r0 := mem [r1] r2 := mem [r1+4] r5 := mem [r1+8]{..} 可以包括 r0~r15 中的所有寄存器,若包括 r15 (PC)將導致程序的跳轉。(6)控制流例 1:MOV r0, #0 ; initialize counterLOOP:ADD r0, r0, #1 ; increment counterCMP r0, #10 ; compare with limitBNE LOOP ; repeat if not equal例 2:CMP r0, #5ADDNE r1, r1, r0SUBNE r1, r1, r2――if (r0 != 5) {r1 := r1 + r0 - r2}3.BootLoader 第一階段3.1 硬件設備初始化基本的硬件初始化工作包括:? 屏蔽所有的中斷;? 設置 CPU 的速度和時鐘頻率;? RAM 初始化;? 初始化 LEDARM 的中斷向量表設置在 0 地址開始的 8 個字空間中,如下表:每當其中的某個異常發生后即將 PC 值置到相應的中斷向量處,每個中斷向量處放置一個跳轉指令到相應的中斷服務程序去進行處理,中斷向量表的程序如下:@ 0x00: Resetb Reset@ 0x04: Undefined instruction exceptionUndefEntryPoint:b HandleUndef@ 0x08: Software interrupt exceptionSWIEntryPoint:b HandleSWI@ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort)PrefetchAbortEnteryPoint:b HandlePrefetchAbort@ 0x10: Data Access Memory AbortDataAbortEntryPoint:b HandleDataAbort100@ 0x14: Not usedNotUsedEntryPoint:b HandleNotUsed@ 0x18: IRQ(Interrupt Request) exceptionIRQEntryPoint:b HandleIRQ@ 0x1c: FIQ(Fast Interrupt Request) exceptionFIQEntryPoint:b HandleFIQ復位時關閉看門狗定時器、屏蔽所有中斷:Reset:@ disable watch dog timermov r1, #0x53000000mov r2, #0x0str r2, [r1]@ disable all interruptsmov r1, #INT_CTL_BASEmov r2, #0xffffffffstr r2, [r1, #oINTMSK]ldr r2, =0x7ffstr r2, [r1, #oINTSUBMSK]設置系統時鐘:@init clk@ 1:2:4mov r1, #CLK_CTL_BASEmov r2, #0x3str r2, [r1, #oCLKDIVN]mrc p15, 0, r1, c1, c0, 0 @ read ctrl registerorr r1, r1, #0xc0000000 @ Asynchronousmcr p15, 0, r1, c1, c0, 0 @ write ctrl register@ now, CPU clock is 200 Mhzmov r1, #CLK_CTL_BASEldr r2, mpll_200mhzstr r2, [r1, #oMPLLCON]點亮所有的用戶 LED:@ All LED onmov r1, #GPIO_CTL_BASEadd r1, r1, #oGPIO_Fldr r2,=0x55aastr r2, [r1, #oGPIO_CON]mov r2, #0xffstr r2, [r1, #oGPIO_UP]mov r2, #0x00str r2, [r1, #oGPIO_DAT]101設置(初始化)內存映射:ENTRY(memsetup)@ initialise the static memory@ set memory control registersmov r1, #MEM_CTL_BASEadrl r2, mem_cfg_valadd r3, r1, #521: ldr r4, [r2], #4str r4, [r1], #4cmp r1, r3bne 1bmov pc, lr設置(初始化)UART:@ set GPIO for UARTmov r1, #GPIO_CTL_BASEadd r1, r1, #oGPIO_Hldr r2, gpio_con_uartstr r2, [r1, #oGPIO_CON]ldr r2, gpio_up_uartstr r2, [r1, #oGPIO_UP]bl InitUART@ Initialize UART@@ r0 = number of UART portInitUART:ldr r1, SerBasemov r2, #0x0str r2, [r1, #oUFCON]str r2, [r1, #oUMCON]mov r2, #0x3str r2, [r1, #oULCON]ldr r2, =0x245str r2, [r1, #oUCON]#define UART_BRD ((50000000 / (UART_BAUD_RATE * 16)) - 1)mov r2, #UART_BRstr r2, [r1, #oUBRDIV]mov r3, #100mov r2, #0x01: sub r3, r3, #0x1tst r2, r3bne 1b#if 0mov r2, #'U'str r2, [r1, #oUTXHL]1021: ldr r3, [r1, #oUTRSTAT]and r3, r3, #UTRSTAT_TX_EMPTYtst r3, #UTRSTAT_TX_EMPTYbne 1bmov r2, #'0'str r2, [r1, #oUTXHL]1: ldr r3, [r1, #oUTRSTAT]and r3, r3, #UTRSTAT_TX_EMPTYtst r3, #UTRSTAT_TX_EMPTYbne 1b#endifmov pc, lr此外,vivi 還提供了幾個匯編情況下通過串口打印字符的函數 PrintChar、PrintWord 和 PrintHexWord:@ PrintChar : prints the character in R0@ r0 contains the character@ r1 contains base of serial port@ writes ro with XXX, modifies r0,r1,r2@ TODO : write ro with XXX reg to error handlingPrintChar:TXBusy:ldr r2, [r1, #oUTRSTAT]and r2, r2, #UTRSTAT_TX_EMPTYtst r2, #UTRSTAT_TX_EMPTYbeq TXBusystr r0, [r1, #oUTXHL]mov pc, lr@ PrintWord : prints the 4 characters in R0@ r0 contains the binary word@ r1 contains the base of the serial port@ writes ro with XXX, modifies r0,r1,r2@ TODO : write ro with XXX reg to error handlingPrintWord:mov r3, r0mov r4, lrbl PrintCharmov r0, r3, LSR #8 /* shift word right 8 bits */bl PrintCharmov r0, r3, LSR #16 /* shift word right 16 bits */bl PrintCharmov r0, r3, LSR #24 /* shift word right 24 bits */bl PrintCharmov r0, #'/r'bl PrintChar103mov r0, #'/n'bl PrintCharmov pc, r4@ PrintHexWord : prints the 4 bytes in R0 as 8 hex ascii characters@ followed by a newline@ r0 contains the binary word@ r1 contains the base of the serial port@ writes ro with XXX, modifies r0,r1,r2@ TODO : write ro with XXX reg to error handlingPrintHexWord:mov r4, lrmov r3, r0mov r0, r3, LSR #28bl PrintHexNibblemov r0, r3, LSR #24bl PrintHexNibblemov r0, r3, LSR #20bl PrintHexNibblemov r0, r3, LSR #16bl PrintHexNibblemov r0, r3, LSR #12bl PrintHexNibblemov r0, r3, LSR #8bl PrintHexNibblemov r0, r3, LSR #4bl PrintHexNibblemov r0, r3bl PrintHexNibblemov r0, #'/r'bl PrintCharmov r0, #'/n'bl PrintCharmov pc, r43.2Bootloader 拷貝配置為從 NAND FLASH 啟動,需要將 NAND FLASH 中的 vivi 代碼 copy 到 RAM 中:#ifdef CONFIG_S3C2410_NAND_BOOTbl copy_myself@ jump to ramldr r1, =on_the_ramadd pc, r1, #0nopnop1: b 1b @ infinite loop#ifdef CONFIG_S3C2410_NAND_BOOT104@@ copy_myself: copy vivi to ram@copy_myself:mov r10, lr@ reset NANDmov r1, #NAND_CTL_BASEldr r2, =0xf830 @ initial valuestr r2, [r1, #oNFCONF]ldr r2, [r1, #oNFCONF]bic r2, r2, #0x800 @ enable chipstr r2, [r1, #oNFCONF]mov r2, #0xff @ RESET commandstrb r2, [r1, #oNFCMD]mov r3, #0 @ wait1: add r3, r3, #0x1cmp r3, #0xablt 1b2: ldr r2, [r1, #oNFSTAT] @ wait readytst r2, #0x1beq 2bldr r2, [r1, #oNFCONF]orr r2, r2, #0x800 @ disable chipstr r2, [r1, #oNFCONF]@ get read to call C functions (for nand_read())ldr sp, DW_STACK_START @ setup stack pointermov fp, #0 @ no previous frame, so fp=0@ copy vivi to RAMldr r0, =VIVI_RAM_BASEmov r1, #0x0mov r2, #0x20000bl nand_read_lltst r0, #0x0beq ok_nand_read#ifdef CONFIG_DEBUG_LLbad_nand_read:ldr r0, STR_FAILldr r1, SerBasebl PrintWord1: b 1b @ infinite loop#endifok_nand_read:#ifdef CONFIG_DEBUG_LLldr r0, STR_OK105ldr r1, SerBasebl PrintWord#endif@ verifymov r0, #0ldr r1, =0x33f00000mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytesgo_next:ldr r3, [r0], #4ldr r4, [r1], #4teq r3, r4bne notmatchsubs r2, r2, #4beq done_nand_readbne go_nextnotmatch:#ifdef CONFIG_DEBUG_LLsub r0, r0, #4ldr r1, SerBasebl PrintHexWordldr r0, STR_FAILldr r1, SerBasebl PrintWord#endif1: b 1bdone_nand_read:#ifdef CONFIG_DEBUG_LLldr r0, STR_OKldr r1, SerBasebl PrintWord#endifmov pc, r10@ clear memory@ r0: start address@ r1: lengthmem_clear:mov r2, #0mov r3, r2mov r4, r2mov r5, r2mov r6, r2mov r7, r2mov r8, r2mov r9, r2106clear_loop:stmia r0!, {r2-r9}subs r1, r1, #(8 * 4)bne clear_loopmov pc, lr#endif @ CONFIG_S3C2410_NAND_BOOT3.3 進入 C 代碼首先要設置堆棧指針 sp,堆棧指針的設置是為了執行 C 語言代碼作好準備。設置好堆棧后,調用 C 語言的 main 函數:@ get read to call C functionsldr sp, DW_STACK_START @ setup stack pointermov fp, #0 @ no previous frame, so fp=0mov a2, #0 @ set argv to NULLbl main @ call mainmov pc, #FLASH_BASE @ otherwise, reboot4. BootLoader 第二階段vivi Bootloader 的第二階段又分成了八個小階段,在 main 函數中分別調用這幾個小階段的相關函數:int main(int argc, char *argv[]){int ret;/** Step 1:*/putstr("/r/n");putstr(vivi_banner);reset_handler();/** Step 2:*/ret = board_init();if (ret) {putstr("Failed a board_init() procedure/r/n");error();}/** Step 3:*/mem_map_init();mmu_init();putstr("Succeed memory mapping./r/n");/** Now, vivi is running on the ram. MMU is enabled.*//** Step 4:107*//* initialize the heap area*/ret = heap_init();if (ret) {putstr("Failed initailizing heap region/r/n");error();}/* Step 5:*/ret = mtd_dev_init();/* Step 6:*/init_priv_data();/* Step 7:*/misc();init_builtin_cmds();/* Step 8:*/boot_or_vivi();return 0;}STEP1 的 putstr(vivi_banner)語句在串口輸出一段字符說明 vivi 的版本、作者等信息,vivi_banner 定義為:const char *vivi_banner ="VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "/r/n";reset_handler 進行相應的復位處理:voidreset_handler(void){int pressed;pressed = is_pressed_pw_btn();if (pressed == PWBT_PRESS_LEVEL) {DPRINTK("HARD RESET/r/n");hard_reset_handle();} else {DPRINTK("SOFT RESET/r/n");soft_reset_handle();}}hard_reset_handle 會 clear 內存,而軟件復位處理則什么都不做:static voidhard_reset_handle(void){108clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE);}STEP2 進行板初始化,設置時間和可編程 I/O 口:int board_init(void){init_time();set_gpios();return 0;}STEP3 進行內存映射及 MMU 初始化:void mem_map_init(void){#ifdef CONFIG_S3C2410_NAND_BOOTmem_map_nand_boot();#elsemem_map_nor();#endifcache_clean_invalidate();tlb_invalidate();}S3C2410A 的 MMU 初始化只需要調用通用的 arm920 MMU 初始化函數:static inline void arm920_setup(void){unsigned long ttb = MMU_TABLE_BASE;__asm__(/* Invalidate caches */"mov r0, #0/n""mcr p15, 0, r0, c7, c7, 0/n" /* invalidate I,D caches on v4 */"mcr p15, 0, r0, c7, c10, 4/n" /* drain write buffer on v4 */"mcr p15, 0, r0, c8, c7, 0/n" /* invalidate I,D TLBs on v4 *//* Load page table pointer */"mov r4, %0/n""mcr p15, 0, r4, c2, c0, 0/n" /* load page table pointer *//* Write domain id (cp15_r3) */"mvn r0, #0/n" /* Domains 0, 1 = client */"mcr p15, 0, r0, c3, c0, 0/n" /* load domain access register *//* Set control register v4 */"mrc p15, 0, r0, c1, c0, 0/n" /* get control register v4 *//* Clear out 'unwanted' bits (then put them in if we need them) *//* .RVI ..RS B... .CAM */"bic r0, r0, #0x3000/n" /* ..11 .... .... .... */"bic r0, r0, #0x0300/n" /* .... ..11 .... .... */"bic r0, r0, #0x0087/n" /* .... .... 1... .111 *//* Turn on what we want */109/* Fault checking enabled */"orr r0, r0, #0x0002/n" /* .... .... .... ..1. */#ifdef CONFIG_CPU_D_CACHE_ON"orr r0, r0, #0x0004/n" /* .... .... .... .1.. */#endif#ifdef CONFIG_CPU_I_CACHE_ON"orr r0, r0, #0x1000/n" /* ...1 .... .... .... */#endif/* MMU enabled */"orr r0, r0, #0x0001/n" /* .... .... .... ...1 */"mcr p15, 0, r0, c1, c0, 0/n" /* write control register */: /* no outputs */: "r" (ttb) );}STEP4 設置堆棧;STEP5 進行 mtd 設備的初始化,記錄 MTD 分區信息;STEP6 設置私有數據;STEP7 初始化內建命令。STEP8 啟動一個 SHELL,等待用戶輸出命令并進行相應處理。在 SHELL 退出的情況下,啟動操作系統:#define DEFAULT_BOOT_DELAY 0x30000000void boot_or_vivi(void){char c;int ret;ulong boot_delay;boot_delay = get_param_value("boot_delay", &ret);if (ret) boot_delay = DEFAULT_BOOT_DELAY;/* If a value of boot_delay is zero,* unconditionally call vivi shell */if (boot_delay == 0) vivi_shell();/** wait for a keystroke (or a button press if you want.)*/printk("Press Return to start the LINUX now, any other key for vivi/n");c = awaitkey(boot_delay, NULL);if (((c != '/r') && (c != '/n') && (c != '/0'))) {printk("type /"help/" for help./n");vivi_shell();}run_autoboot();return;}SHELL 中讀取用戶從串口輸出的命令字符串,執行該命令:voidvivi_shell(void)110{#ifdef CONFIG_SERIAL_TERMserial_term();#else#error there is no terminal.#endif}void serial_term(void){char cmd_buf[MAX_CMDBUF_SIZE];for (;;) {printk("%s> ", prompt);getcmd(cmd_buf, MAX_CMDBUF_SIZE);/* execute a user command */if (cmd_buf[0])exec_string(cmd_buf);}}5.電路板調試在電路板的調試過程中,我們首先要在 ADT 新建的工程中添加第一階段的匯編代碼 head.S 文件,修改 Link 腳本,將代碼和數據映射到 S3C2410A 自帶的 0x40000000 開始的 4KB 內存空間內:SECTIONS{. = 0x40000000;.text : { *(.text) }Image_RO_Limit = .;Image_RW_Base = .;.data : { *(.data) }.rodata : { *(.rodata) }Image_ZI_Base = .;.bss : { *(.bss) }Image_ZI_Limit = .;__bss_start__ = .;__bss_end__ = .;__EH_FRAME_BEGIN__ = .;__EH_FRAME_END__ = .;PROVIDE (__stack = .);end = .;_end = .;.debug_info 0 : { *(.debug_info) }.debug_line 0 : { *(.debug_line) }.debug_abbrev 0 : { *(.debug_abbrev)}.debug_frame 0 : { *(.debug_frame) }111}借助萬用表、示波器等儀器儀表,調通 SDRAM,并將 vivi 中自帶的串口、NAND FLASH 驅動添加到工程中,調試通過板上的串口和 FLASH。如果板電路的原理與三星公司 DEMO 板有差距,則 vivi 中硬件的操作要進行相應的修改。全部調試通過后,修改 vivi 源代碼,重新編譯 vivi,將其燒錄入 NAND FLASH 就可以在復位后啟動這個 Bootloader 了。調試板上的新增硬件時,宜在 ADT 中添加相應的代碼,在不加載操作系統的情況下,單純地操作這些硬件。如果電路板設計有誤,要進行飛線和割線等處理。6.小結本章講解了 ARM 匯編、Bootloader 的功能,Bootloader 的調試環境及 ARM 電路板的調試方法。基于 ARM 的嵌入式 Linux 移植真實體驗( 3)――操作系統宋寶華 21cnbao@21cn.com出處:dev.yesky.com在筆者撰寫的《 C語言嵌入式系統編程修煉之道》一文中,主要陳訴的軟件架構是單任務無操作系統平臺的,而本文的側重點則在于講述操作系統嵌入的軟件架構,二者的區別如下圖:嵌入式操作系統并不總是必須的,因為程序完全可以在裸板上運行。盡管如此,但對于復雜的系統,為使其具有任務管理、定時器管理、存儲器管理、資源管理、事件管理、系統管理、消息管理、隊列管理和中斷處理的能力,提供多任務處理,更好的分配系統資源的功能,很有必要針對特定的硬件平臺和實際應用移植操作系統。鑒于 Linux 的源代碼開放性,它成為嵌入式操作系統領域的很好選擇。國內外許多知名大學、公司、研究機構都加入了嵌入式 Linux 的研究行列,推出了一些著名的版本:? RT-Linux 提供了一個精巧的實時內核, 把標準的Linux 核心作為實時核心的一個進程同用戶的實時進程一起調度。 RT-Linux 已成功地應用于航天飛機的空間數據采集、科學儀器測控和電影特技圖像處理等廣泛的應用領域。如 NASA(美國國家宇航局)將裝有RT-Linux 的設備放在飛機上,以測量Georage 咫風的風速;? uCLinux(Micro-Control-Linux,u 表示Micro,C 表示Control)去掉了MMU(內存管理)功能,應用于沒有虛擬內存管理的微處理器/微控制器,它已經被成功地移植到了很多平臺上。本章涉及的 mizi-linux 由韓國mizi 公司根據Linux 2.4 內核移植而來,支持S3C2410A 處理器。1.Linux內核要點和其他操作系統一樣, Linux包含進程調度與進程間通信(IPC)、內存管理(MMU)、虛擬文件系統(VFS)、網絡接口等,下圖給出了 Linux 的組成及其關系:112Linux 內核源代碼包括多個目錄:( 1)arch:包括硬件特定的內核代碼,如arm、mips、i386 等;( 2)drivers:包含硬件驅動代碼,如char、cdrom、scsi、mtd 等;( 3)include:通用頭文件及針對不同平臺特定的頭文件,如asm-i386、asm-arm 等;( 4)init:內核初始化代碼;( 5)ipc:進程間通信代碼;( 6)kernel:內核核心代碼;( 7)mm:內存管理代碼;( 8)net:與網絡協議棧相關的代碼,如ipv4、ipv6、ethernet 等;( 9)fs:文件系統相關代碼,如nfs、vfat 等;( 10)lib:庫文件,與平臺無關的strlen、strcpy 等,如在string.c 中包含:char * strcpy(char * dest,const char *src){char *tmp = dest;while ((*dest++ = *src++) != '/0')/* nothing */;return tmp;}( 11)Documentation:文檔。在 Linux 內核的實現中,有一些數據結構使用非常頻繁,對研讀內核的人來說至為關鍵,它們是:1.task_structLinux 內核利用task_struct 數據結構代表一個進程,用task_struct 指針形成一個task 數組。當建立新進程的時候, Linux 為新的進程分配一個task_struct 結構,然后將指針保存在task 數組中。調度程序維護current 指針,它指向當前正在運行的進程。2.mm_struct每個進程的虛擬內存由 mm_struct結構代表。該結構中包含了一組指向 vm-area_struct結構的指針,vm-area_struct 結構描述了虛擬內存的一個區域。3.inodeLinux 虛擬文件系統中的文件、目錄等均由對應的索引節點(inode)代表。2.Linux移植項目mizi-linux 已經根據Linux 2.4 內核針對S3C2410A 這一芯片進行了有針對性的移植工作,包括:( 1)修改根目錄下的Makefile 文件113a.指定目標平臺為ARM:#ARCH := $(shell uname -m | sed -e s/i.86/i386/ -e s/sun4u/sparc64/ -e s/arm.*/arm/ -es/sa110/arm/)ARCH := armb.指定交叉編譯器:CROSS_COMPILE = arm-linux-( 2)修改arch 目錄中的文件根據本章第一節可知, Linux 的arch 目錄存放硬件相關的內核代碼,因此,在Linux 內核中增加對S3C2410的支持,最主要就是要修改 arch目錄中的文件。a.在arch/arm/Makefile 文件中加入:ifeq ($(CONFIG_ARCH_S3C2410),y)TEXTADDR = 0xC0008000MACHINE = s3c2410Endifb.在 arch/arm/config.in文件中加入:if [ "$CONFIG_ARCH_S3C2410" = "y" ]; thencomment 'S3C2410 Implementation'dep_bool ' SMDK (MERI TECH BOARD)' CONFIG_S3C2410_SMDK $CONFIG_ARCH_S3C2410dep_bool ' change AIJI' CONFIG_SMDK_AIJIdep_tristate 'S3C2410 USB function support' CONFIG_S3C2410_USB $CONFIG_ARCH_S3C2100dep_tristate ' Support for S3C2410 USB character device emulation'CONFIG_S3C2410_USB_CHAR $CONFIG_S3C2410_USBfi # /* CONFIG_ARCH_S3C2410 */arch/arm/config.in 文件還有幾處針對S3C2410 的修改。c.在arch/arm/boot/Makefile 文件中加入:ifeq ($(CONFIG_ARCH_S3C2410),y)ZTEXTADDR = 0x30008000ZRELADDR = 0x30008000endifd.在 linux/arch/arm/boot/compressed/Makefile文件中加入:ifeq ($(CONFIG_ARCH_S3C2410),y)OBJS += head-s3c2410.oendif加入的結果是 head-s3c2410.S文件被編譯為 head-s3c2410.o。e.加入arch/arm/boot/compressed/ head-s3c2410.S 文件#include <linux/config.h>#include <linux/linkage.h>#include <asm/mach-types.h>.section ".start", #alloc, #execinstr__S3C2410_start:@ Preserve r8/r7 i.e. kernel entry values114@ What is it?@ Nandy@ Data cache, Intstruction cache, MMU might be active.@ Be sure to flush kernel binary out of the cache,@ whatever state it is, before it is turned off.@ This is done by fetching through currently executed@ memory to be sure we hit the same cachebic r2, pc, #0x1fadd r3, r2, #0x4000 @ 16 kb is quite enough...1: ldr r0, [r2], #32teq r2, r3bne 1bmcr p15, 0, r0, c7, c10, 4 @ drain WBmcr p15, 0, r0, c7, c7, 0 @ flush I & D caches#if 0@ disabling MMU and cachesmrc p15, 0, r0, c1, c0, 0 @ read control registerbic r0, r0, #0x05 @ disable D cache and MMUbic r0, r0, #1000 @ disable I cachemcr p15, 0, r0, c1, c0, 0#endif/** Pause for a short time so that we give enough time* for the host to start a terminal up.*/mov r0, #0x002000001: subs r0, r0, #1bne 1b該文件中的匯編代碼完成 S3C2410特定硬件相關的初始化。f.在arch/arm/def-configs 目錄中增加配置文件g.在arch/arm/kernel/Makefile 中增加對S3C2410 的支持no-irq-arch := $(CONFIG_ARCH_INTEGRATOR) $(CONFIG_ARCH_CLPS711X) /$(CONFIG_FOOTBRIDGE) $(CONFIG_ARCH_EBSA110) /$(CONFIG_ARCH_SA1100) $(CONFIG_ARCH_CAMELOT) /$(CONFIG_ARCH_S3C2400) $(CONFIG_ARCH_S3C2410) /$(CONFIG_ARCH_MX1ADS) $(CONFIG_ARCH_PXA)obj-$(CONFIG_MIZI) += event.oobj-$(CONFIG_APM) += apm2.oh.修改 arch/arm/kernel/debug-armv.S文件,在適當的位置增加如下關于 S3C2410的代碼:#elif defined(CONFIG_ARCH_S3C2410)115.macro addruart,rxmrc p15, 0, /rx, c1, c0tst /rx, #1 @ MMU enabled ?moveq /rx, #0x50000000 @ physical base addressmovne /rx, #0xf0000000 @ virtual address.endm.macro senduart,rd,rxstr /rd, [/rx, #0x20] @ UTXH.endm.macro waituart,rd,rx.endm.macro busyuart,rd,rx1001: ldr /rd, [/rx, #0x10] @ read UTRSTATtst /rd, #1 << 2 @ TX_EMPTY ?beq 1001b.endmi.修改 arch/arm/kernel/setup.c文件此文件中的 setup_arch 非常關鍵,用來完成與體系結構相關的初始化:void __init setup_arch(char **cmdline_p){struct tag *tags = NULL;struct machine_desc *mdesc;char *from = default_command_line;ROOT_DEV = MKDEV(0, 255);setup_processor();mdesc = setup_machine(machine_arch_type);machine_name = mdesc->name;if (mdesc->soft_reboot)reboot_setup("s");if (mdesc->param_offset)tags = phys_to_virt(mdesc->param_offset);/** Do the machine-specific fixups before we parse the* parameters or tags.*/116if (mdesc->fixup)mdesc->fixup(mdesc, (struct param_struct *)tags,&from, &meminfo);/** If we have the old style parameters, convert them to* a tag list before.*/if (tags && tags->hdr.tag != ATAG_CORE)convert_to_tag_list((struct param_struct *)tags,meminfo.nr_banks == 0);if (tags && tags->hdr.tag == ATAG_CORE)parse_tags(tags);if (meminfo.nr_banks == 0) {meminfo.nr_banks = 1;meminfo.bank[0].start = PHYS_OFFSET;meminfo.bank[0].size = MEM_SIZE;}init_mm.start_code = (unsigned long) &_text;init_mm.end_code = (unsigned long) &_etext;init_mm.end_data = (unsigned long) &_edata;init_mm.brk = (unsigned long) &_end;memcpy(saved_command_line, from, COMMAND_LINE_SIZE);saved_command_line[COMMAND_LINE_SIZE-1] = '/0';parse_cmdline(&meminfo, cmdline_p, from);bootmem_init(&meminfo);paging_init(&meminfo, mdesc);request_standard_resources(&meminfo, mdesc);/** Set up various architecture-specific pointers*/init_arch_irq = mdesc->init_irq;#ifdef CONFIG_VT#if defined(CONFIG_VGA_CONSOLE)conswitchp = &vga_con;#elif defined(CONFIG_DUMMY_CONSOLE)conswitchp = &dummy_con;#endif117#endif}j.修改 arch/arm/mm/mm-armv.c文件( arch/arm/mm/目錄中的文件完成與ARM 相關的MMU 處理)修改init_maps->bufferable = 0;為init_maps->bufferable = 1;要輕而易舉地進行上述馬拉松式的內核移植工作并非一件輕松的事情,需要對Linux 內核有很好的掌握,同時掌握硬件特定的知識和相關的匯編。幸而 mizi 公司的開發者們已經合力為我們完成了上述工作,這使得小弟們在將 mizi-linux 移植到自身開發的電路板的過程中只需要關心如下幾點:( 1)內核初始化:Linux 內核的入口點是start_kernel()函數。它初始化內核的其他部分,包括捕獲,IRQ通道,調度,設備驅動,標定延遲循環,最重要的是能夠 fork“init”進程,以啟動整個多任務環境。我們可以在 init 中加上一些特定的內容。(2)設備驅動:設備驅動占據了Linux 內核很大部分。同其他操作系統一樣,設備驅動為它們所控制的硬件設備和操作系統提供接口。本文第四章將單獨講解驅動程序的編寫方法。(3)文件系統:Linux 最重要的特性之一就是對多種文件系統的支持。這種特性使得Linux 很容易地同其他操作系統共存。文件系統的概念使得用戶能夠查看存儲設備上的文件和路徑而無須考慮實際物理設備的文件系統類型。 Linux 透明的支持許多不同的文件系統,將各種安裝的文件和文件系統以一個完整的虛擬文件系統的形式呈現給用戶。我們可以在 K9S1208 NAND FLASH 上移植cramfs、jfss2、yaffs 等FLASH 文件系統。3. init進程在 init函數中“加料”,可以使得Linux 啟動的時候做點什么,例如廣州友善之臂公司的demo 板在其中加入了公司信息:static int init(void * unused){lock_kernel();do_basic_setup();prepare_namespace();/** Ok, we have completed the initial bootup, and* we're essentially up and running. Get rid of the* initmem segments and start the user-mode stuff..*/free_initmem();unlock_kernel();if (open("/dev/console", O_RDWR, 0) < 0)printk("Warning: unable to open an initial console./n");(void) dup(0);118(void) dup(0);/** We try each of these until one succeeds.** The Bourne shell can be used instead of init if we are* trying to recover a really broken machine.*/printk("========================================/n");printk("= Friendly-ARM Tech. Ltd. =/n");printk("= http://www.arm9.net =/n");printk("= http://www.arm9.com.cn =/n");printk("========================================/n");if (execute_command)execve(execute_command,argv_init,envp_init);execve("/sbin/init",argv_init,envp_init);execve("/etc/init",argv_init,envp_init);execve("/bin/init",argv_init,envp_init);execve("/bin/sh",argv_init,envp_init);panic("No init found. Try passing init= option to kernel.");}這樣在 Linux的啟動過程中,會額外地輸出:========================================= Friendly-ARM Tech. Ltd. == http://www.arm9.net == http://www.arm9.com.cn =========================================4.文件系統移植文件系統是基于被劃分的存儲設備上的邏輯上單位上的一種定義文件的命名、存儲、組織及取出的方法。如果一個 Linux 沒有根文件系統,它是不能被正確的啟動的。因此,我們需要為Linux 創建根文件系統,我們將其創建在 K9S1208 NAND FLASH 上。Linux 的根文件系統可能包括如下目錄(或更多的目錄):( 1)/bin (binary):包含著所有的標準命令和應用程序;( 2)/dev (device):包含外設的文件接口,在Linux 下,文件和設備采用同種地方法訪問的,系統上的每個設備都在/dev 里有一個對應的設備文件;( 3)/etc (etcetera):這個目錄包含著系統設置文件和其他的系統文件,例如/etc/fstab(file system table)記錄了啟動時要 mount的 filesystem;( 4)/home:存放用戶主目錄;( 5)/lib(library):存放系統最基本的庫文件;( 6)/mnt:用戶臨時掛載文件系統的地方;( 7)/proc:linux 提供的一個虛擬系統,系統啟動時在內存中產生,用戶可以直接通過訪問這些文件來獲得119系統信息;( 8)/root:超級用戶主目錄;( 9)/sbin:這個目錄存放著系統管理程序,如fsck、mount 等;( 10)/tmp(temporary):存放不同的程序執行時產生的臨時文件;( 11)/usr(user):存放用戶應用程序和文件。采用 BusyBox 是縮小根文件系統的好辦法,因為其中提供了系統的許多基本指令但是其體積很小。眾所周知,瑞士軍刀以其小巧輕便、功能眾多而聞名世界,成為各國軍人的必備工具,并廣泛應用于民間,而 BusyBox也被稱為嵌入式 Linux領域的“瑞士軍刀”。此地址可以下載 BusyBox:http://www.busybox.net,當前最新版本為1.1.3。編譯好busybox 后,將其放入/bin 目錄,若要使用其中的命令,只需要建立link,如:ln -s ./busybox lsln -s ./busybox mkdir4.1 cramfs在根文件系統中,為保護系統的基本設置不被更改,可以采用cramfs 格式,它是一種只讀的閃存文件系統。制作cramfs 文件系統的方法為:建立一個目錄,將需要放到文件系統的文件copy到這個目錄,運行“mkcramfs目錄名 image名”就可以生成一個cramfs 文件系統的image 文件。例如如果目錄名為rootfs,則正確的命令為:mkcramfs rootfs rootfs.ramfs我們使用下面的命令可以 mount生成的 rootfs.ramfs文件,并查看其中的內容:mount -o loop -t cramfs rootfs.ramfs /mount/point此地址可以下載 mkcramfs工具: http://sourceforge.net/projects/cramfs/。4.2 jfss2對于 cramfs閃存文件系統,如果沒有 ramfs的支持則只能讀,而采用 jfss2(The Journalling Flash FileSystem version 2)文件系統則可以直接在閃存中讀、寫數據。jfss2 是一個日志結構(log-structured)的文件系統,包含數據和原數據(meta-data)的節點在閃存上順序地存儲。jfss2 記錄了每個擦寫塊的擦寫次數,當閃存上各個擦寫塊的擦寫次數的差距超過某個預定的閥值,開始進行磨損平衡的調整。調整的策略是,在垃圾回收時將擦寫次數小的擦寫塊上的數據遷移到擦寫次數大的擦寫塊上以達到磨損平衡的目的。與 mkcramfs 類似,同樣有一個mkfs.jffs2 工具可以將一個目錄制作為jffs2 文件系統。假設把/bin目錄制作為 jffs2 文件系統,需要運行的命令為:mkfs.jffs2 -d /bin -o jffs2.img4.3 yaffsyaffs 是一種專門為嵌入式系統中常用的閃存設備設計的一種可讀寫的文件系統,它比jffs2 文件系統具有更快的啟動速度,對閃存使用壽命有更好的保護機制。為使 Linux 支持yaffs 文件系統,我們需要將其對應的驅動加入到內核中 fs/yaffs/,并修改內核配置文件。使用我們使用mkyaffs 工具可以將NAND FLASH 中的分區格式化為 yaffs 格式(如/bin/mkyaffs /dev/mtdblock/0命令可以將第 1個 MTD塊設備分區格式化為yaffs),而使用mkyaffsimage(類似于mkcramfs、mkfs.jffs2)則可以將某目錄生成為yaffs 文件系統鏡像。嵌入式 Linux 還可以使用NFS(網絡文件系統)通過以太網掛接根文件系統,這是一種經常用來作為調試使用的文件系統啟動方式。通過網絡掛接的根文件系統,可以在主機上生成 ARM 交叉編譯版本的目標文件或二進制可執行文件,然后就可以直接裝載或執行它,而不用頻繁地寫入 flash。采用不同的文件系統啟動時,要注意通過第二章介紹的 BootLoader 修改啟動參數,如廣州友善之臂的demo提供如下三種啟動方式:( 1)從cramfs 掛接根文件系統:root=/dev/bon/2();( 2)從移植的yaffs 掛接根文件系統:root=/dev/mtdblock/0;120( 3)從以太網掛接根文件系統:root=/dev/nfs。5.小結本章介紹了嵌入式 Linux的背景、移植項目、 init進程修改和文件系統移植,通過這些步驟,我們可以在嵌入式系統上啟動一個基本的 Linux?;?ARM 的嵌入式 Linux 移植真實體驗(4)――設備驅動宋寶華 21cnbao@21cn.com出處:dev.yesky.com設備驅動程序是操作系統內核和機器硬件之間的接口,它為應用程序屏蔽硬件的細節,一般來說,Linux 的設備驅動程序需要完成如下功能:? 設備初始化、釋放;? 提供各類設備服務;? 負責內核和設備之間的數據交換;? 檢測和處理設備工作過程中出現的錯誤。Linux 下的設備驅動程序被組織為一組完成不同任務的函數的集合,通過這些函數使得Linux 的設備操作猶如文件一般。在應用程序看來,硬件設備只是一個設備文件,應用程序可以象操作普通文件一樣對硬件設備進行操作,如 open ()、close ()、read ()、write () 等。Linux 主要將設備分為二類:字符設備和塊設備。字符設備是指設備發送和接收數據以字符的形式進行;而塊設備則以整個數據緩沖區的形式進行。在對字符設備發出讀/寫請求時,實際的硬件I/O 一般就緊接著發生了;而塊設備則不然,它利用一塊系統內存作緩沖區,當用戶進程對設備請求能滿足用戶的要求,就返回請求的數據,如果不能,就調用請求函數來進行實際的 I/O 操作。塊設備主要針對磁盤等慢速設備。1.內存分配由于Linux驅動程序在內核中運行,因此在設備驅動程序需要申請/釋放內存時,不能使用用戶級的malloc/free函數,而需由內核級的函數 kmalloc/kfree ()來實現, kmalloc()函數的原型為:void kmalloc (size_t size ,int priority);參數 size為申請分配內存的字節數, kmalloc最多只能開辟 128k的內存;參數 priority說明若 kmalloc()不能馬上分配內存時用戶進程要采用的動作: GFP_KERNEL 表示等待,即等kmalloc()函數將一些內存安排到交換區來滿足你的內存需要, GFP_ATOMIC 表示不等待,如不能立即分配到內存則返回0 值;函數的返回值指向已分配內存的起始地址,出錯時,返回 0。kmalloc ()分配的內存需用kfree()函數來釋放,kfree ()被定義為:# define kfree (n) kfree_s( (n) ,0)其中 kfree_s ()函數原型為:void kfree_s (void * ptr ,int size);參數 ptr為 kmalloc()返回的已分配內存的指針,size 是要釋放內存的字節數,若為0 時,由內核自動確定內存的大小。2.中斷許多設備涉及到中斷操作,因此,在這樣的設備的驅動程序中需要對硬件產生的中斷請求提供中斷服務程序。與注冊基本入口點一樣,驅動程序也要請求內核將特定的中斷請求和中斷服務程序聯系在一起。在 Linux中,用 request_irq()函數來實現請求:121int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);參數 irq為要中斷請求號,參數 handler為指向中斷服務程序的指針,參數 type用來確定是正常中斷還是快速中斷(正常中斷指中斷服務子程序返回后,內核可以執行調度程序來確定將運行哪一個進程;而快速中斷是指中斷服務子程序返回后,立即執行被中斷程序,正常中斷 type 取值為0 ,快速中斷type 取值為SA_INTERRUPT),參數name 是設備驅動程序的名稱。3.字符設備驅動我 們 必 須 為 字 符 設 備 提 供 一 個 初 始 化 函 數 , 該 函 數 用 來 完 成 對 所 控 設 備 的 初 始 化 工 作 , 并 調 用register_chrdev() 函數注冊字符設備。假設有一字符設備“exampledev”,則其init 函數為:void exampledev_init(void){if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))TRACE_TXT("Device exampledev driver registered error");elseTRACE_TXT("Device exampledev driver registered successfully");…//設備初始化}其 中 , register_chrdev函 數 中 的 參 數 MAJOR_NUM為 主 設 備 號 ,“exampledev”為 設 備 名 ,exampledev_fops 為包含基本函數入口點的結構體, 類型為file_operations。 當執行exampledev_init 時,它將調用內核函數 register_chrdev,把驅動程序的基本入口點指針存放在內核的字符設備地址表中,在用戶進程對該設備執行系統調用時提供入口地址。隨著內核功能的加強, file_operations 結構體也變得更加龐大。但是大多數的驅動程序只是利用了其中的一部分,對于驅動程序中無需提供的功能,只需要把相應位置的值設為 NULL。對于字符設備來說,要提供的主要入口有: open ()、release ()、read ()、write ()、ioctl ()等。open()函數 對設備特殊文件進行open()系統調用時,將調用驅動程序的open () 函數:int (*open)(struct inode * inode,struct file *filp);其中參數 inode為設備特殊文件的 inode (索引結點)結構的指針,參數 filp是指向這一設備的文件結構的指針。 open()的主要任務是確定硬件處在就緒狀態、驗證次設備號的合法性(次設備號可以用MINOR(inode->i_rdev) 取得)、控制使用設備的進程數、根據執行情況返回狀態碼(0表示成功,負數表示存在錯誤)等;release()函數 當最后一個打開設備的用戶進程執行close ()系統調用時, 內核將調用驅動程序的release ()函數:void (*release) (struct inode * inode,struct file *filp) ;release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、用戶自定義排他標志的復位等。read()函數 當對設備特殊文件進行read() 系統調用時,將調用驅動程序read() 函數:ssize_t (*read) (struct file * filp, char * buf, size_t count, loff_t * offp);參數 buf是指向用戶空間緩沖區的指針,由用戶進程給出, count 為用戶進程要求讀取的字節數,也由用戶給出。read() 函數的功能就是從硬設備或內核內存中讀取或復制count 個字節到buf 指定的緩沖區中。在復制數據時要注意,驅動程序運行在內核中,而 buf 指定的緩沖區在用戶內存區中,是不能直接在內核中訪問使用的,因此,必須使用特殊的復制函數來完成復制工作,這些函數在 include/asm/uaccess.h中被聲明:unsigned long copy_to_user (void * to, void * from, unsigned long len);此外, put_user()函數用于內核空間和用戶空間的單值交互(如char、int、long)。write( )函數 當設備特殊文件進行 write ()系統調用時,將調用驅動程序的 write ()函數:ssize_t (*write) (struct file *, const char *, size_t, loff_t *);122write ()的功能是將參數buf 指定的緩沖區中的count 個字節內容復制到硬件或內核內存中,和read() 一樣,復制工作也需要由特殊函數來完成:unsigned long copy_from_user(void *to, const void *from, unsigned long n);此外, get_user()函數用于內核空間和用戶空間的單值交互(如char、int、long)。ioctl()函數 該函數是特殊的控制函數,可以通過它向設備傳遞控制信息或從設備取得狀態信息,函數原型為:int (*ioctl) (struct inode * inode,struct file * filp,unsigned int cmd,unsigned long arg);參數 cmd為設備驅動程序要執行的命令的代碼,由用戶自定義,參數arg 為相應的命令提供參數,類型可以是整型、指針等。同樣,在驅動程序中,這些函數的定義也必須符合命名規則,按照本文約定,設備“exampledev”的驅動程序的 這 些 函 數 應 分 別 命 名 為 exampledev_open 、exampledev_ release 、exampledev_read 、exampledev_write 、exampledev_ioctl , 因 此 設 備“exampledev” 的 基 本 入 口 點 結 構 變 量exampledev_fops 賦值如下(對較早版本的內核):struct file_operations exampledev_fops {NULL ,exampledev_read ,exampledev_write ,NULL ,NULL ,exampledev_ioctl ,NULL ,exampledev_open ,exampledev_release ,NULL ,NULL ,NULL ,NULL} ;就目前而言,由于 file_operations結構體已經很龐大,我們更適合用 GNU擴展的 C語法來初始化exampledev_fops:struct file_operations exampledev_fops = {read: exampledev _read,write: exampledev _write,ioctl: exampledev_ioctl ,open: exampledev_open ,release : exampledev_release ,};看看第一章電路板硬件原理圖,板上包含四個用戶可編程的發光二極管(LED),這些LED 連接在ARM 處理器的可編程 I/O 口(GPIO)上,現在來編寫這些LED 的驅動:#include <linux/config.h>#include <linux/kernel.h>#include <linux/init.h>#include <linux/miscdevice.h>#include <linux/sched.h>123#include <linux/delay.h>#include <asm/hardware.h>#define DEVICE_NAME "leds" /*定義led 設備的名字*/#define LED_MAJOR 231 /*定義led 設備的主設備號*/static unsigned long led_table[] ={/*I/O 方式 led設備對應的硬件資源*/GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6,};/*使用 ioctl控制 led*/static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd,unsigned long arg){switch (cmd){case 0:case 1:if (arg > 4){return -EINVAL;}write_gpio_bit(led_table[arg], !cmd);default:return -EINVAL;}}static struct file_operations leds_fops ={owner: THIS_MODULE, ioctl: leds_ioctl,};static devfs_handle_t devfs_handle;static int __init leds_init(void){int ret;int i;/*在內核中注冊設備*/ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);if (ret < 0){printk(DEVICE_NAME " can't register major number/n");return ret;}devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR,1240, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL);/*使用宏進行端口初始化, set_gpio_ctrl和 write_gpio_bit均為宏定義*/for (i = 0; i < 8; i++){set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);write_gpio_bit(led_table[i], 1);}printk(DEVICE_NAME " initialized/n");return 0;}static void __exit leds_exit(void){devfs_unregister(devfs_handle);unregister_chrdev(LED_MAJOR, DEVICE_NAME);}module_init(leds_init);module_exit(leds_exit);使用命令方式編譯 led驅動模塊:#arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include-DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c以上命令將生成 leds.o文件,把該文件復制到板子的/lib目錄下,使用以下命令就可以安裝 leds驅動模塊:#insmod /lib/ leds.o刪除該模塊的命令是:#rmmod leds4.塊設備驅動塊設備驅動程序的編寫是一個浩繁的工程,其難度遠超過字符設備,上千行的代碼往往只能搞定一個簡單的塊設備,而數十行代碼就可能搞定一個字符設備。因此,非得有相當的基本功才能完成此項工作。下面先給出一個實例,即 mtdblock 塊設備的驅動。我們通過分析此實例中的代碼來說明塊設備驅動程序的寫法(由于篇幅的關系,大量的代碼被省略,只保留了必要的主干):#include <linux/config.h>#include <linux/devfs_fs_kernel.h>static void mtd_notify_add(struct mtd_info* mtd);static void mtd_notify_remove(struct mtd_info* mtd);static struct mtd_notifier notifier = {mtd_notify_add,mtd_notify_remove,NULL};static devfs_handle_t devfs_dir_handle = NULL;static devfs_handle_t devfs_rw_handle[MAX_MTD_DEVICES];125static struct mtdblk_dev {struct mtd_info *mtd; /* Locked */int count;struct semaphore cache_sem;unsigned char *cache_data;unsigned long cache_offset;unsigned int cache_size;enum { STATE_EMPTY, STATE_CLEAN, STATE_DIRTY } cache_state;} *mtdblks[MAX_MTD_DEVICES];static spinlock_t mtdblks_lock;/* this lock is used just in kernels >= 2.5.x */static spinlock_t mtdblock_lock;static int mtd_sizes[MAX_MTD_DEVICES];static int mtd_blksizes[MAX_MTD_DEVICES];static void erase_callback(struct erase_info *done){wait_queue_head_t *wait_q = (wait_queue_head_t *)done->priv;wake_up(wait_q);}static int erase_write (struct mtd_info *mtd, unsigned long pos,int len, const char *buf){struct erase_info erase;DECLARE_WAITQUEUE(wait, current);wait_queue_head_t wait_q;size_t retlen;int ret;/** First, let's erase the flash block.*/init_waitqueue_head(&wait_q);erase.mtd = mtd;erase.callback = erase_callback;erase.addr = pos;erase.len = len;erase.priv = (u_long)&wait_q;set_current_state(TASK_INTERRUPTIBLE);126add_wait_queue(&wait_q, &wait);ret = MTD_ERASE(mtd, &erase);if (ret) {set_current_state(TASK_RUNNING);remove_wait_queue(&wait_q, &wait);printk (KERN_WARNING "mtdblock: erase of region [0x%lx, 0x%x] ""on /"%s/" failed/n",pos, len, mtd->name);return ret;}schedule(); /* Wait for erase to finish. */remove_wait_queue(&wait_q, &wait);/** Next, writhe data to flash.*/ret = MTD_WRITE (mtd, pos, len, &retlen, buf);if (ret)return ret;if (retlen != len)return -EIO;return 0;}static int write_cached_data (struct mtdblk_dev *mtdblk){struct mtd_info *mtd = mtdblk->mtd;int ret;if (mtdblk->cache_state != STATE_DIRTY)return 0;DEBUG(MTD_DEBUG_LEVEL2, "mtdblock: writing cached data for /"%s/" ""at 0x%lx, size 0x%x/n", mtd->name,mtdblk->cache_offset, mtdblk->cache_size);ret = erase_write (mtd, mtdblk->cache_offset,mtdblk->cache_size, mtdblk->cache_data);if (ret)return ret;127mtdblk->cache_state = STATE_EMPTY;return 0;}static int do_cached_write (struct mtdblk_dev *mtdblk, unsigned long pos,int len, const char *buf){…}static int do_cached_read (struct mtdblk_dev *mtdblk, unsigned long pos,int len, char *buf){…}static int mtdblock_open(struct inode *inode, struct file *file){…}static release_t mtdblock_release(struct inode *inode, struct file *file){int dev;struct mtdblk_dev *mtdblk;DEBUG(MTD_DEBUG_LEVEL1, "mtdblock_release/n");if (inode == NULL)release_return(-ENODEV);dev = minor(inode->i_rdev);mtdblk = mtdblks[dev];down(&mtdblk->cache_sem);write_cached_data(mtdblk);up(&mtdblk->cache_sem);spin_lock(&mtdblks_lock);if (!--mtdblk->count) {/* It was the last usage. Free the device */mtdblks[dev] = NULL;spin_unlock(&mtdblks_lock);if (mtdblk->mtd->sync)128mtdblk->mtd->sync(mtdblk->mtd);put_mtd_device(mtdblk->mtd);vfree(mtdblk->cache_data);kfree(mtdblk);} else {spin_unlock(&mtdblks_lock);}DEBUG(MTD_DEBUG_LEVEL1, "ok/n");BLK_DEC_USE_COUNT;release_return(0);}/** This is a special request_fn because it is executed in a process context* to be able to sleep independently of the caller. The* io_request_lock (for <2.5) or queue_lock (for >=2.5) is held upon entry* and exit. The head of our request queue is considered active so there is* no need to dequeue requests before we are done.*/static void handle_mtdblock_request(void){struct request *req;struct mtdblk_dev *mtdblk;unsigned int res;for (;;) {INIT_REQUEST;req = CURRENT;spin_unlock_irq(QUEUE_LOCK(QUEUE));mtdblk = mtdblks[minor(req->rq_dev)];res = 0;if (minor(req->rq_dev) >= MAX_MTD_DEVICES)panic("%s : minor out of bound", __FUNCTION__);if (!IS_REQ_CMD(req))goto end_req;if ((req->sector + req->current_nr_sectors) > (mtdblk->mtd->size >> 9))goto end_req;129// Handle the requestswitch (rq_data_dir(req)){int err;case READ:down(&mtdblk->cache_sem);err = do_cached_read (mtdblk, req->sector << 9,req->current_nr_sectors << 9,req->buffer);up(&mtdblk->cache_sem);if (!err)res = 1;break;case WRITE:// Read only deviceif ( !(mtdblk->mtd->flags & MTD_WRITEABLE) )break;// Do the writedown(&mtdblk->cache_sem);err = do_cached_write (mtdblk, req->sector << 9,req->current_nr_sectors << 9,req->buffer);up(&mtdblk->cache_sem);if (!err)res = 1;break;}end_req:spin_lock_irq(QUEUE_LOCK(QUEUE));end_request(res);}}static volatile int leaving = 0;static DECLARE_MUTEX_LOCKED(thread_sem);static DECLARE_WAIT_QUEUE_HEAD(thr_wq);int mtdblock_thread(void *dummy){…130}#define RQFUNC_ARG request_queue_t *qstatic void mtdblock_request(RQFUNC_ARG){/* Don't do anything, except wake the thread if necessary */wake_up(&thr_wq);}static int mtdblock_ioctl(struct inode * inode, struct file * file,unsigned int cmd, unsigned long arg){struct mtdblk_dev *mtdblk;mtdblk = mtdblks[minor(inode->i_rdev)];switch (cmd) {case BLKGETSIZE: /* Return device size */return put_user((mtdblk->mtd->size >> 9), (unsigned long *) arg);case BLKFLSBUF:if(!capable(CAP_SYS_ADMIN))return -EACCES;fsync_dev(inode->i_rdev);invalidate_buffers(inode->i_rdev);down(&mtdblk->cache_sem);write_cached_data(mtdblk);up(&mtdblk->cache_sem);if (mtdblk->mtd->sync)mtdblk->mtd->sync(mtdblk->mtd);return 0;default:return -EINVAL;}}static struct block_device_operations mtd_fops ={owner: THIS_MODULE,open: mtdblock_open,131release: mtdblock_release,ioctl: mtdblock_ioctl};static void mtd_notify_add(struct mtd_info* mtd){…}static void mtd_notify_remove(struct mtd_info* mtd){if (!mtd || mtd->type == MTD_ABSENT)return;devfs_unregister(devfs_rw_handle[mtd->index]);}int __init init_mtdblock(void){int i;spin_lock_init(&mtdblks_lock);/* this lock is used just in kernels >= 2.5.x */spin_lock_init(&mtdblock_lock);#ifdef CONFIG_DEVFS_FSif (devfs_register_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME, &mtd_fops)){printk(KERN_NOTICE "Can't allocate major number %d for Memory TechnologyDevices./n",MTD_BLOCK_MAJOR);return -EAGAIN;}devfs_dir_handle = devfs_mk_dir(NULL, DEVICE_NAME, NULL);register_mtd_user(?ifier);#elseif (register_blkdev(MAJOR_NR,DEVICE_NAME,&mtd_fops)) {printk(KERN_NOTICE "Can't allocate major number %d for Memory TechnologyDevices./n",MTD_BLOCK_MAJOR);return -EAGAIN;}#endif132/* We fill it in at open() time. */for (i=0; i< MAX_MTD_DEVICES; i++) {mtd_sizes[i] = 0;mtd_blksizes[i] = BLOCK_SIZE;}init_waitqueue_head(&thr_wq);/* Allow the block size to default to BLOCK_SIZE. */blksize_size[MAJOR_NR] = mtd_blksizes;blk_size[MAJOR_NR] = mtd_sizes;BLK_INIT_QUEUE(BLK_DEFAULT_QUEUE(MAJOR_NR), &mtdblock_request,&mtdblock_lock);kernel_thread (mtdblock_thread, NULL, CLONE_FS|CLONE_FILES|CLONE_SIGHAND);return 0;}static void __exit cleanup_mtdblock(void){leaving = 1;wake_up(&thr_wq);down(&thread_sem);#ifdef CONFIG_DEVFS_FSunregister_mtd_user(?ifier);devfs_unregister(devfs_dir_handle);devfs_unregister_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME);#elseunregister_blkdev(MAJOR_NR,DEVICE_NAME);#endifblk_cleanup_queue(BLK_DEFAULT_QUEUE(MAJOR_NR));blksize_size[MAJOR_NR] = NULL;blk_size[MAJOR_NR] = NULL;}module_init(init_mtdblock);module_exit(cleanup_mtdblock);從上述源代碼中我們發現,塊設備也以與字符設備 register_chrdev、unregister_ chrdev 函數類似的方法進行設備的注冊與釋放:int register_blkdev(unsigned int major, const char *name, struct block_device_operations*bdops);int unregister_blkdev(unsigned int major, const char *name);但 是 , register_chrdev使 用 一 個 向 file_operations結 構 的 指 針 , 而 register_blkdev則 使 用block_device_operations 結構的指針,其中定義的open、release 和ioctl 方法和字符設備的對應方法相133同,但未定義 read或者 write操作。這是因為,所有涉及到塊設備的 I/O通常由系統進行緩沖處理。塊驅動程序最終必須提供完成實際塊 I/O 操作的機制,在Linux 當中,用于這些I/O 操作的方法稱為“request(請求)”。在塊設備的注冊過程中,需要初始化request 隊列,這一動作通過blk_init_queue 來完成, blk_init_queue 函數建立隊列,并將該驅動程序的request 函數關聯到隊列。在模塊的清除階段,應調用 blk_cleanup_queue 函數。本例中相關的代碼為:BLK_INIT_QUEUE(BLK_DEFAULT_QUEUE(MAJOR_NR), &mtdblock_request, &mtdblock_lock);blk_cleanup_queue(BLK_DEFAULT_QUEUE(MAJOR_NR));每個設備有一個默認使用的請求隊列,必要時,可使用 BLK_DEFAULT_QUEUE(major) 宏得到該默認隊列。這個宏在 blk_dev_struct 結構形成的全局數組(該數組名為blk_dev)中搜索得到對應的默認隊列。blk_dev 數組由內核維護,并可通過主設備號索引。blk_dev_struct 接口定義如下:struct blk_dev_struct {/** queue_proc has to be atomic*/request_queue_t request_queue;queue_proc *queue;void *data;};request_queue 成員包含了初始化之后的 I/O 請求隊列,data 成員可由驅動程序使用,以便保存一些私有數據。request_queue 定義為:struct request_queue{/** the queue request freelist, one for reads and one for writes*/struct request_list rq[2];/** Together with queue_head for cacheline sharing*/struct list_head queue_head;elevator_t elevator;request_fn_proc * request_fn;merge_request_fn * back_merge_fn;merge_request_fn * front_merge_fn;merge_requests_fn * merge_requests_fn;make_request_fn * make_request_fn;plug_device_fn * plug_device_fn;/** The queue owner gets to use this for whatever they like.* ll_rw_blk doesn't touch it.134*/void * queuedata;/** This is used to remove the plug when tq_disk runs.*/struct tq_struct plug_tq;/** Boolean that indicates whether this queue is plugged or not.*/char plugged;/** Boolean that indicates whether current_request is active or* not.*/char head_active;/** Is meant to protect the queue in the future instead of* io_request_lock*/spinlock_t queue_lock;/** Tasks wait here for free request*/wait_queue_head_t wait_for_request;};下圖表征了 blk_dev、blk_dev_struct 和request_queue 的關系:下圖則表征了塊設備的注冊和釋放過程:1355.小結本章講述了 Linux設備驅動程序的入口函數及驅動程序中的內存申請、中斷等,并分別以實例講述了字符設備及塊設備的驅動開發方法。Trackback: http://tb.donews.net/TrackBack.aspx?PostId=1000099[點擊此處收藏本文]發表于 2006年 08月 14日 8:07 PM基于 ARM 的嵌入式 Linux 移植真實體驗(5)――應用實例宋寶華 21cnbao@21cn.com出處:dev.yesky.com應用實例的編寫實際上已經不屬于 Linux操作系統移植的范疇,但是為了保證本系列文章的完整性,這里提供一系列針對嵌入式 Linux 開發應用程序的實例。編寫 Linux 應用程序要用到如下工具:( 1)編譯器:GCCGCC 是 Linux平臺下最重要的開發工具,它是 GNU的 C和 C++編譯器,其基本用法為:gcc [options]136[filenames]。我們應該使用 arm-linux-gcc。( 2)調試器:GDBgdb 是一個用來調試 C和 C++程序的強力調試器,我們能通過它進行一系列調試工作,包括設置斷點、觀查變量、單步等。我們應該使用 arm-linux-gdb。( 3)MakeGNU Make 的主要工作是讀進一個文本文件,稱為makefile。這個文件記錄了哪些文件由哪些文件產生,用什么命令來產生。 Make 依靠此makefile 中的信息檢查磁盤上的文件,如果目的文件的創建或修改時間比它的一個依靠文件舊的話, make 就執行相應的命令,以便更新目的文件。Makefile 中的編譯規則要相應地使用arm-linux-版本。( 4)代碼編輯可以使用傳統的 vi 編輯器,但最好采用emacs 軟件,它具備語法高亮、版本控制等附帶功能。在宿主機上用上述工具完成應用程序的開發后,可以通過如下途徑將程序下載到目標板上運行:( 1)通過串口通信協議rz 將程序下載到目標板的文件系統中(感謝Linux 提供了rz 這樣的一個命令);( 2)通過ftp 通信協議從宿主機上的ftp 目錄里將程序下載到目標板的文件系統中;( 3)將程序拷入U 盤,在目標機上mount U 盤,運行U 盤中的程序;( 4)如果目標機Linux 使用NFS 文件系統,則可以直接將程序拷入到宿主機相應的目錄內,在目標機Linux中可以直接使用。1. 文件編程Linux 的文件操作API 涉及到創建、打開、讀寫和關閉文件。創建int creat(const char *filename, mode_t mode);參數 mode指定新建文件的存取權限, 它同 umask一起決定文件的最終權限 ( mode&umask), 其中umask代表了文件在創建時需要去掉的一些存取權限。 umask 可通過系統調用umask()來改變:int umask(int newmask);該調用將 umask設置為 newmask,然后返回舊的umask,它只影響讀、寫和執行權限。打開int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);讀寫在文件打開以后,我們才可對文件進行讀寫了, Linux 中提供文件讀寫的系統調用是read、write 函數:int read(int fd, const void *buf, size_t length);int write(int fd, const void *buf, size_t length);其中參數 buf為指向緩沖區的指針, length為緩沖區的大?。ㄒ宰止潪閱挝唬:瘮?read()實現從文件描述符 fd 所指定的文件中讀取length 個字節到buf 所指向的緩沖區中,返回值為實際讀取的字節數。函數write實現將把 length個字節從 buf指向的緩沖區中寫到文件描述符 fd所指向的文件中,返回值為實際寫入的字節數。以 O_CREAT 為標志的open 實際上實現了文件創建的功能,因此,下面的函數等同creat()函數:int open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);定位對于隨機文件,我們可以隨機的指定位置讀寫,使用如下函數進行定位:int lseek(int fd, offset_t offset, int whence);137lseek()將文件讀寫指針相對whence 移動offset 個字節。操作成功時,返回文件指針相對于文件頭的位置。參數 whence 可使用下述值:SEEK_SET:相對文件開頭SEEK_CUR:相對文件讀寫指針的當前位置SEEK_END:相對文件末尾offset 可取負值,例如下述調用可將文件指針相對當前位置向前移動5 個字節:lseek(fd, -5, SEEK_CUR);由于 lseek函數的返回值為文件指針相對于文件頭的位置,因此下列調用的返回值就是文件的長度:lseek(fd, 0, SEEK_END);關閉只要調用 close 就可以了,其中fd 是我們要關閉的文件描述符:int close(int fd);下面我們來編寫一個應用程序, 在當前目錄下創建用戶可讀寫文件“example.txt”, 在其中寫入“Hello World”,關閉文件,再次打開它,讀取其中的內容并輸出在屏幕上:#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdio.h>#define LENGTH 100main(){int fd, len;char str[LENGTH];fd = open("hello.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); /* 創建并打開文件*/if (fd){write(fd, "Hello, Software Weekly", strlen("Hello, software weekly")); /* 寫入 Hello, softwareweekly 字符串 */close(fd);}fd = open("hello.txt", O_RDWR);len = read(fd, str, LENGTH); /* 讀取文件內容*/str[len] = '/0';printf("%s/n", str);close(fd);}2. 進程控制/通信編程進程控制中主要涉及到進程的創建、睡眠和退出等,在 Linux 中主要提供了fork、exec、clone 的進程創建方法, sleep 的進程睡眠和exit 的進程退出調用,另外Linux 還提供了父進程等待子進程結束的系統調用wait。fork對于沒有接觸過 Unix/Linux操作系統的人來說, fork是最難理解的概念之一,因為它執行一次卻返回兩個值,138以前“聞所未聞”。先看下面的程序:int main(){int i;if (fork() == 0){for (i = 1; i < 3; i++)printf("This is child process/n");}else{for (i = 1; i < 3; i++)printf("This is parent process/n");}}執行結果為:This is child processThis is child processThis is parent processThis is parent processfork 在英文中是“分叉”的意思,一個進程在運行中,如果使用了fork,就產生了另一個進程,于是進程就“分叉”了。當前進程為父進程,通過fork()會產生一個子進程。對于父進程,fork 函數返回子程序的進程號而對于子程序, fork 函數則返回零,這就是一個函數返回兩次的本質。exec在 Linux中可使用 exec函數族,包含多個函數( execl、execlp、execle、execv、execve 和execvp),被用于啟動一個指定路徑和文件名的進程。 exec 函數族的特點體現在:某進程一旦調用了exec 類函數,正在執行的程序就被干掉了,系統把代碼段替換成新的程序(由 exec 類函數執行)的代碼,并且原有的數據段和堆棧段也被廢棄,新的數據段與堆棧段被分配,但是進程號卻被保留。也就是說, exec 執行的結果為:系統認為正在執行的還是原先的進程,但是進程對應的程序被替換了。fork 函數可以創建一個子進程而當前進程不死,如果我們在fork 的子進程中調用exec 函數族就可以實現既讓父進程的代碼執行又啟動一個新的指定進程,這很好。 fork 和exec 的搭配巧妙地解決了程序啟動另一程序的執行但自己仍繼續運行的問題,請看下面的例子:char command[MAX_CMD_LEN];void main(){int rtn; /* 子進程的返回數值 */while (1){/* 從終端讀取要執行的命令 */printf(">");fgets(command, MAX_CMD_LEN, stdin);command[strlen(command) - 1] = 0;if (fork() == 0){139/* 子進程執行此命令*/execlp(command, command);/* 如果 exec函數返回,表明沒有正常執行命令,打印錯誤信息*/perror(command);exit(errorno);}else{/* 父進程,等待子進程結束,并打印子進程的返回值 */wait(&rtn);printf(" child process return %d/n", rtn);}}}這個函數實現了一個 shell的功能,它讀取用戶輸入的進程名和參數,并啟動對應的進程。cloneclone 是Linux2.0 以后才具備的新功能,它較fork 更強(可認為fork 是clone 要實現的一部分),可以使得創建的子進程共享父進程的資源,并且要使用此函數必須在編譯內核時設置 clone_actually_works_ok選項。clone 函數的原型為:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);此函數返回創建進程的 PID,函數中的flags 標志用于設置創建子進程時的相關選項。來看下面的例子:int variable, fd;int do_something() {variable = 42;close(fd);_exit(0);}int main(int argc, char *argv[]) {void **child_stack;char tempch;variable = 9;fd = open("test.file", O_RDONLY);child_stack = (void **) malloc(16384);printf("The variable was %d/n", variable);clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);sleep(1); /* 延時以便子進程完成關閉文件操作、修改變量*/printf("The variable is now %d/n", variable);140if (read(fd, &tempch, 1) < 1) {perror("File Read Error");exit(1);}printf("We could read from the file/n");return 0;}運行輸出:The variable is now 42File Read Error程序的輸出結果告訴我們,子進程將文件關閉并將變量修改 (調用clone 時用到的CLONE_VM、CLONE_FILES標志將使得變量和文件描述符表被共享),父進程隨即就感覺到了,這就是clone 的特點。sleep函數調用 sleep可以用來使進程掛起指定的秒數,該函數的原型為:unsigned int sleep(unsigned int seconds);該函數調用使得進程掛起一個指定的時間,如果指定掛起的時間到了,該調用返回0;如果該函數調用被信號所打斷,則返回剩余掛起的時間數(指定的時間減去已經掛起的時間)。exit系統調用 exit的功能是終止本進程,其函數原型為:void _exit(int status);_exit 會立即終止發出調用的進程,所有屬于該進程的文件描述符都關閉。參數status 作為退出的狀態值返回父進程,在父進程中通過系統調用 wait 可獲得此值。waitwait 系統調用包括:pid_t wait(int *status);pid_t waitpid(pid_t pid, int *status, int options);wait 的作用為發出調用的進程只要有子進程,就睡眠到它們中的一個終止為止;waitpid 等待由參數pid 指定的子進程退出。Linux 的進程間通信(IPC,InterProcess Communication)通信方法有管道、消息隊列、共享內存、信號量、套接口等。套接字通信并不為 Linux 所專有,在所有提供了TCP/IP 協議棧的操作系統中幾乎都提供了socket,而所有這樣操作系統,對套接字的編程方法幾乎是完全一樣的。管道分為有名管道和無名管道,無名管道只能用于親屬進程之間的通信,而有名管道則可用于無親屬關系的進程之間;消息隊列用于運行于同一臺機器上的進程間通信,與管道相似;共享內存通常由一個進程創建,其余進程對這塊內存區進行讀寫;信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。下面是一個使用信號量的例子,該程序創建一個特定的 IPC 結構的關鍵字和一個信號量,建立此信號量的索引,修改索引指向的信號量的值,最后清除信號量:#include <stdio.h>#include <sys/types.h>#include <sys/sem.h>#include <sys/ipc.h>void main(){key_t unique_key; /* 定義一個 IPC 關鍵字*/int id;141struct sembuf lock_it;union semun options;int i;unique_key = ftok(".", 'a'); /* 生成關鍵字,字符'a'是一個隨機種子*//* 創建一個新的信號量集合*/id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);printf("semaphore id=%d/n", id);options.val = 1; /*設置變量值*/semctl(id, 0, SETVAL, options); /*設置索引0 的信號量*//*打印出信號量的值*/i = semctl(id, 0, GETVAL, 0);printf("value of semaphore at index 0 is %d/n", i);/*下面重新設置信號量*/lock_it.sem_num = 0; /*設置哪個信號量*/lock_it.sem_op = - 1; /*定義操作*/lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/if (semop(id, &lock_it, 1) == - 1){printf("can not lock semaphore./n");exit(1);}i = semctl(id, 0, GETVAL, 0);printf("value of semaphore at index 0 is %d/n", i);/*清除信號量*/semctl(id, 0, IPC_RMID, 0);}3. 線程控制/通信編程Linux 本身只有進程的概念,而其所謂的“線程”本質上在內核里仍然是進程。大家知道,進程是資源分配的單位,同一進程中的多個線程共享該進程的資源(如作為共享內存的全局變量)。 Linux 中所謂的“線程”只是在被創建的時候“克隆”(clone)了父進程的資源,因此,clone 出來的進程表現為“線程”。Linux 中最流行的線程機制為 LinuxThreads,它實現了一種Posix 1003.1c “pthread”標準接口。線程之間的通信涉及同步和互斥,互斥體的用法為:pthread_mutex_t mutex;pthread_mutex_init(&mutex, NULL); //按缺省的屬性初始化互斥體變量mutexpthread_mutex_lock(&mutex); // 給互斥體變量加鎖… //臨界資源phtread_mutex_unlock(&mutex); // 給互斥體變量解鎖同步就是線程等待某個事件的發生。只有當等待的事件發生線程才繼續執行,否則線程掛起并放棄處理器。當142多個線程協作時,相互作用的任務必須在一定的條件下同步。Linux 下的C 語言編程有多種線程同步機制,最典型的是條件變量(condition variable)。而在頭文件semaphore.h 中定義的信號量則完成了互斥體和條件變量的封裝,按照多線程程序設計中訪問控制機制,控制對資源的同步訪問,提供程序設計人員更方便的調用接口。下面的生產者/消費者問題說明了Linux 線程的控制和通信:#include <stdio.h>#include <pthread.h>#define BUFFER_SIZE 16struct prodcons{int buffer[BUFFER_SIZE];pthread_mutex_t lock;int readpos, writepos;pthread_cond_t notempty;pthread_cond_t notfull;};/* 初始化緩沖區結構 */void init(struct prodcons *b){pthread_mutex_init(&b->lock, NULL);pthread_cond_init(&b->notempty, NULL);pthread_cond_init(&b->notfull, NULL);b->readpos = 0;b->writepos = 0;}/* 將產品放入緩沖區,這里是存入一個整數*/void put(struct prodcons *b, int data){pthread_mutex_lock(&b->lock);/* 等待緩沖區未滿*/if ((b->writepos + 1) % BUFFER_SIZE == b->readpos){pthread_cond_wait(&b->notfull, &b->lock);}/* 寫數據,并移動指針*/b->buffer[b->writepos] = data;b->writepos++;if (b->writepos > = BUFFER_SIZE)b->writepos = 0;/* 設置緩沖區非空的條件變量*/pthread_cond_signal(&b->notempty);pthread_mutex_unlock(&b->lock);}/* 從緩沖區中取出整數*/143int get(struct prodcons *b){int data;pthread_mutex_lock(&b->lock);/* 等待緩沖區非空*/if (b->writepos == b->readpos){pthread_cond_wait(&b->notempty, &b->lock);}/* 讀數據,移動讀指針*/data = b->buffer[b->readpos];b->readpos++;if (b->readpos > = BUFFER_SIZE)b->readpos = 0;/* 設置緩沖區未滿的條件變量*/pthread_cond_signal(&b->notfull);pthread_mutex_unlock(&b->lock);return data;}/* 測試:生產者線程將1 到10000 的整數送入緩沖區,消費者線程從緩沖區中獲取整數,兩者都打印信息*/#define OVER ( - 1)struct prodcons buffer;void *producer(void *data){int n;for (n = 0; n < 10000; n++){printf("%d --->/n", n);put(&buffer, n);} put(&buffer, OVER);return NULL;}void *consumer(void *data){int d;while (1){d = get(&buffer);if (d == OVER)break;printf("--->%d /n", d);144}return NULL;}int main(void){pthread_t th_a, th_b;void *retval;init(&buffer);/* 創建生產者和消費者線程*/pthread_create(&th_a, NULL, producer, 0);pthread_create(&th_b, NULL, consumer, 0);/* 等待兩個線程結束*/pthread_join(th_a, &retval);pthread_join(th_b, &retval);return 0;}4.小結本章主要給出了 Linux平臺下文件、進程控制與通信、線程控制與通信的編程實例。至此,一個完整的,涉及硬件原理、 Bootloader、操作系統及文件系統移植、驅動程序開發及應用程序編寫的嵌入式Linux 系列講解就全部結束了。深入淺出 Linux 設備驅動編程宋寶華 21cnbao@21cn.comyesky1.Linux 內核模塊Linux 設備驅動屬于內核的一部分,Linux 內核的一個模塊可以以兩種方式被編譯和加載:( 1)直接編譯進Linux 內核,隨同Linux 啟動時加載;( 2)編譯成一個可加載和刪除的模塊,使用insmod 加載(modprobe 和insmod 命令類似,但依賴于相關的配置文件), rmmod 刪除。這種方式控制了內核的大小,而模塊一旦被插入內核,它就和內核其他部分一樣。下面我們給出一個內核模塊的例子:#include <linux/module.h> //所有模塊都需要的頭文件#include <linux/init.h> // init&exit 相關宏MODULE_LICENSE("GPL");static int __init hello_init (void)145{printk("Hello module init/n");return 0;}static void __exit hello_exit (void){printk("Hello module exit/n");}module_init(hello_init);module_exit(hello_exit);分析上述程序,發現一個 Linux內核模塊需包含模塊初始化和模塊卸載函數,前者在 insmod 的時候運行,后者在 rmmod 的時候運行。初始化與卸載函數必須在宏module_init 和module_exit 使用前定義,否則會出現編譯錯誤。程序中的 MODULE_LICENSE("GPL")用于聲明模塊的許可證。如果要把上述程序編譯為一個運行時加載和刪除的模塊,則編譯命令為:gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o hello.o hello.c由此可見, Linux內核模塊的編譯需要給 gcc指示–D__KERNEL__ -DMODULE –DLINUX參數。 -I選項跟著 Linux 內核源代碼中Include 目錄的路徑。下列命令將可加載 hello 模塊:insmod ./hello.o下列命令完成相反過程:rmmod hello如果要將其直接編譯入 Linux內核,則需要將源代碼文件拷貝入 Linux內核源代碼的相應路徑里,并修改Makefile。我們有必要補充一下 Linux 內核編程的一些基本知識:內存在 Linux 內核模式下,我們不能使用用戶態的malloc()和free()函數申請和釋放內存。進行內核編程時,最常用的內存申請和釋放函數為在 include/linux/kernel.h 文件中聲明的kmalloc()和kfree(),其原型為:void *kmalloc(unsigned int len, int priority);void kfree(void *__ptr);kmalloc 的 priority參數通常設置為 GFP_KERNEL,如果在中斷服務程序里申請內存則要用GFP_ATOMIC參數,因為使用 GFP_KERNEL參數可能會引起睡眠,不能用于非進程上下文中(在中斷中是不允許睡眠的)。由于內核態和用戶態使用不同的內存定義,所以二者之間不能直接訪問對方的內存。而應該使用 Linux 中的用戶和內核態內存交互函數(這些函數在 include/asm/uaccess.h 中被聲明):unsigned long copy_from_user(void *to, const void *from, unsigned long n);unsigned long copy_to_user (void * to, void * from, unsigned long len);copy_from_user、 copy_to_user函數返回不能被復制的字節數,因此,如果完全復制成功,返回值為0。include/asm/uaccess.h 中定義的put_user 和get_user 用于內核空間和用戶空間的單值交互(如char、int、long)。這里給出的僅僅是關于內核中內存管理的皮毛, 關于 Linux 內存管理的更多細節知識, 我們會在本文第9 節 《內存與 I/O 操作》進行更加深入地介紹。輸出146在內核編程中,我們不能使用用戶態 C庫函數中的 printf()函數輸出信息,而只能使用printk()。但是,內核中 printk()函數的設計目的并不是為了和用戶交流,它實際上是內核的一種日志機制,用來記錄下日志信息或者給出警告提示。每個 printk 都會有個優先級,內核一共有8 個優先級,它們都有對應的宏定義。如果未指定優先級,內核會選擇默認的優先級 DEFAULT_MESSAGE_LOGLEVEL。如果優先級數字比int console_loglevel 變量小的話,消息就會打印到控制臺上。如果 syslogd 和klogd 守護進程在運行的話,則不管是否向控制臺輸出,消息都會被追加進/var/log/messages 文件。klogd 只處理內核消息,syslogd 處理其他系統消息,比如應用程序。模塊參數2.4 內核下,include/linux/module.h中定義的宏 MODULE_PARM(var,type)用于向模塊傳遞命令行參數。var 為接受參數值的變量名,type 為采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min 及max 用于表示當參數為數組類型時,允許輸入的數組元素的個數范圍; b:byte;h:short;i:int;l:long;s:string。在裝載內核模塊時,用戶可以向模塊傳遞一些參數:insmod modname var=value如果用戶未指定參數, var將使用模塊內定義的缺省值。2.字符設備驅動程序Linux 下的設備驅動程序被組織為一組完成不同任務的函數的集合,通過這些函數使得Windows 的設備操作猶如文件一般。在應用程序看來,硬件設備只是一個設備文件,應用程序可以象操作普通文件一樣對硬件設備進行操作,如 open ()、close ()、read ()、write () 等。Linux 主要將設備分為二類:字符設備和塊設備。字符設備是指設備發送和接收數據以字符的形式進行;而塊設備則以整個數據緩沖區的形式進行。字符設備的驅動相對比較簡單。下面我們來假設一個非常簡單的虛擬字符設備:這個設備中只有一個 4 個字節的全局變量int global_var,而這個設備的名字叫做“gobalvar”。對“gobalvar”設備的讀寫等操作即是對其中全局變量global_var 的操作。驅動程序是內核的一部分,因此我們需要給其添加模塊初始化函數,該函數用來完成對所控設備的初始化工作,并調用 register_chrdev() 函數注冊字符設備:static int __init gobalvar_init(void){if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops)){//…注冊失敗}else{//…注冊成功}}其中, register_chrdev函數中的參數 MAJOR_NUM為主設備號,“gobalvar”為設備名,gobalvar_fops 為包含基本函數入口點的結構體,類型為 file_operations。當gobalvar 模塊被加載時,gobalvar_init 被執行,它將調用內核函數 register_chrdev,把驅動程序的基本入口點指針存放在內核的字符設備地址表中,在用戶進程對該設備執行系統調用時提供入口地址。與模塊初始化函數對應的就是模塊卸載函數,需要調用 register_chrdev()的“反函數” unregister_chrdev():static void __exit gobalvar_exit(void)147{if (unregister_chrdev(MAJOR_NUM, " gobalvar ")){//…卸載失敗}else{//…卸載成功}}隨著內核不斷增加新的功能, file_operations結構體已逐漸變得越來越大,但是大多數的驅動程序只是利用了其中的一部分。對于字符設備來說,要提供的主要入口有: open ()、release ()、read ()、write ()、ioctl ()、llseek()、poll()等。open()函數 對設備特殊文件進行open()系統調用時,將調用驅動程序的open () 函數:int (*open)(struct inode * ,struct file *);其中參數 inode為設備特殊文件的 inode (索引結點)結構的指針,參數 file是指向這一設備的文件結構的指針。 open()的主要任務是確定硬件處在就緒狀態、驗證次設備號的合法性(次設備號可以用MINOR(inode-> i- rdev) 取得)、控制使用設備的進程數、根據執行情況返回狀態碼(0表示成功,負數表示存在錯誤)等;release()函數 當最后一個打開設備的用戶進程執行close ()系統調用時, 內核將調用驅動程序的release ()函數:void (*release) (struct inode * ,struct file *) ;release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、用戶自定義排他標志的復位等。read()函數 當對設備特殊文件進行read() 系統調用時,將調用驅動程序read() 函數:ssize_t (*read) (struct file *, char *, size_t, loff_t *);用來從設備中讀取數據。當該函數指針被賦為 NULL 值時,將導致read 系統調用出錯并返回-EINVAL( “Invalid argument,非法參數”)。函數返回非負值表示成功讀取的字節數(返回值為“signed size”數據類型,通常就是目標平臺上的固有整數類型)。globalvar_read 函數中內核空間與用戶空間的內存交互需要借助第2 節所介紹的函數:static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){…copy_to_user(buf, &global_var, sizeof(int));…}write( )函數 當設備特殊文件進行 write ()系統調用時,將調用驅動程序的 write ()函數:ssize_t (*write) (struct file *, const char *, size_t, loff_t *);向設備發送數據。如果沒有這個函數, write系統調用會向調用程序返回一個-EINVAL。如果返回值非負,則表示成功寫入的字節數。globalvar_write 函數中內核空間與用戶空間的內存交互需要借助第2 節所介紹的函數:static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off){…copy_from_user(&global_var, buf, sizeof(int));148…}ioctl()函數 該函數是特殊的控制函數,可以通過它向設備傳遞控制信息或從設備取得狀態信息,函數原型為:int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);unsigned int 參數為設備驅動程序要執行的命令的代碼,由用戶自定義,unsigned long 參數為相應的命令提供參數,類型可以是整型、指針等。如果設備不提供 ioctl 入口點,則對于任何內核未預先定義的請求,ioctl 系統調用將返回錯誤( -ENOTTY,“No such ioctl fordevice,該設備無此ioctl 命令”)。如果該設備方法返回一個非負值,那么該值會被返回給調用程序以表示調用成功。llseek()函數該函數用來修改文件的當前讀寫位置,并將新位置作為(正的)返回值返回,原型為:loff_t (*llseek) (struct file *, loff_t, int);poll()函數poll 方法是poll 和select 這兩個系統調用的后端實現,用來查詢設備是否可讀或可寫,或是否處于某種特殊狀態,原型為:unsigned int (*poll) (struct file *, struct poll_table_struct *);我們將在“設備的阻塞與非阻塞操作”一節對該函數進行更深入的介紹。設 備 “gobalvar” 的 驅 動 程 序 的 這 些 函 數 應 分 別 命 名 為gobalvar_open 、gobalvar_ release 、gobalvar_read 、gobalvar_write 、gobalvar_ioctl , 因 此 設 備“gobalvar” 的 基 本 入 口 點 結 構 變 量gobalvar_fops 賦值如下:struct file_operations gobalvar_fops = {read: gobalvar_read,write: gobalvar_write,};上述代碼中對 gobalvar_fops的初始化方法并不是標準 C所支持的,屬于 GNU擴展語法。完整的 globalvar.c 文件源代碼如下:#include <linux/module.h>#include <linux/init.h>#include <linux/fs.h>#include <asm/uaccess.h>MODULE_LICENSE("GPL");#define MAJOR_NUM 254 //主設備號static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);//初始化字符設備驅動的 file_operations結構體struct file_operations globalvar_fops ={read: globalvar_read, write: globalvar_write,};static int global_var = 0; //“globalvar”設備的全局變量static int __init globalvar_init(void)149{int ret;//注冊設備驅動ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);if (ret){printk("globalvar register failure");}else{printk("globalvar register success");}return ret;}static void __exit globalvar_exit(void){int ret;//注銷設備驅動ret = unregister_chrdev(MAJOR_NUM, "globalvar");if (ret){printk("globalvar unregister failure");}else{printk("globalvar unregister success");}}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){//將 global_var從內核空間復制到用戶空間if (copy_to_user(buf, &global_var, sizeof(int))){return - EFAULT;}return sizeof(int);}static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off)150{//將用戶空間的數據復制到內核空間的 global_varif (copy_from_user(&global_var, buf, sizeof(int))){return - EFAULT;}return sizeof(int);}module_init(globalvar_init);module_exit(globalvar_exit);運行gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o globalvar.oglobalvar.c編譯代碼,運行inmod globalvar.o加載 globalvar模塊,再運行cat /proc/devices發現其中多出了“254 globalvar”一行,如下圖:接著我們可以運行:mknod /dev/globalvar c 254 0創建設備節點,用戶進程通過/dev/globalvar這個路徑就可以訪問到這個全局變量虛擬設備了。我們寫一個用戶態的程序 globalvartest.c 來驗證上述設備:#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>main(){int fd, num;//打開“/dev/globalvar”fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);if (fd != -1 ){//初次讀 globalvarread(fd, &num, sizeof(int));printf("The globalvar is %d/n", num);//寫 globalvarprintf("Please input the num written to globalvar/n");scanf("%d", &num);write(fd, &num, sizeof(int));//再次讀 globalvar151read(fd, &num, sizeof(int));printf("The globalvar is %d/n", num);//關閉“/dev/globalvar”close(fd);}else{printf("Device open failure/n");}}編譯上述文件:gcc –o globalvartest.o globalvartest.c運行./globalvartest.o可以發現“globalvar”設備可以正確的讀寫。3.設備驅動中的并發控制在驅動程序中,當多個線程同時訪問相同的資源時(驅動程序中的全局變量是一種典型的共享資源),可能會引發“競態”,因此我們必須對共享資源進行并發控制。Linux 內核中解決并發控制的最常用方法是自旋鎖與信號量(絕大多數時候作為互斥鎖使用)。自旋鎖與信號量“類似而不類”,類似說的是它們功能上的相似性,“不類”指代它們在本質和實現機理上完全不一樣,不屬于一類。自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環查看是否該自旋鎖的保持者已經釋放了鎖, “自旋”就是“在原地打轉”。而信號量則引起調用者睡眠,它把進程從運行隊列上拖出去,除非獲得鎖。這就是它們的“不類”。但是,無論是信號量,還是自旋鎖,在任何時刻,最多只能有一個保持者,即在任何時刻最多只能有一個執行單元獲得鎖。這就是它們的“類似”。鑒于自旋鎖與信號量的上述特點,一般而言,自旋鎖適合于保持時間非常短的情況,它可以在任何上下文使用;信號量適合于保持時間較長的情況,會只能在進程上下文使用。如果被保護的共享資源只在進程上下文訪問,則可以以信號量來保護該共享資源,如果對共享資源的訪問時間非常短,自旋鎖也是好的選擇。但是,如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。與信號量相關的 API 主要有:定義信號量struct semaphore sem;初始化信號量void sema_init (struct semaphore *sem, int val);該函數初始化信號量,并設置信號量 sem的值為 valvoid init_MUTEX (struct semaphore *sem);該函數用于初始化一個互斥鎖,即它把信號量 sem 的值設置為1,等同于sema_init (struct semaphore*sem, 1);void init_MUTEX_LOCKED (struct semaphore *sem);152該函數也用于初始化一個互斥鎖,但它把信號量 sem 的值設置為0,等同于sema_init (struct semaphore*sem, 0);獲得信號量void down(struct semaphore * sem);該函數用于獲得信號量 sem,它會導致睡眠,因此不能在中斷上下文使用;int down_interruptible(struct semaphore * sem);該函數功能與 down類似,不同之處為, down不能被信號打斷,但 down_interruptible能被信號打斷;int down_trylock(struct semaphore * sem);該函數嘗試獲得信號量 sem,如果能夠立刻獲得,它就獲得該信號量并返回0,否則,返回非0 值。它不會導致調用者睡眠,可以在中斷上下文使用。釋放信號量void up(struct semaphore * sem);該函數釋放信號量 sem,喚醒等待者。與自旋鎖相關的 API 主要有:定義自旋鎖spinlock_t spin;初始化自旋鎖spin_lock_init(lock)該宏用于動態初始化自旋鎖 lock獲得自旋鎖spin_lock(lock)該宏用于獲得自旋鎖 lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那里,直到該自旋鎖的保持者釋放;spin_trylock(lock)該宏嘗試獲得自旋鎖 lock, 如果能立即獲得鎖, 它獲得鎖并返回真, 否則立即返回假, 實際上不再“在原地打轉”;釋放自旋鎖spin_unlock(lock)該宏釋放自旋鎖 lock,它與spin_trylock 或spin_lock 配對使用;除此之外,還有一組自旋鎖使用于中斷情況下的 API。下面進入對并發控制的實戰。 首先, 在 globalvar 的驅動程序中, 我們可以通過信號量來控制對int global_var的并發訪問,下面給出源代碼:#include <linux/module.h>#include <linux/init.h>#include <linux/fs.h>#include <asm/uaccess.h>#include <asm/semaphore.h>MODULE_LICENSE("GPL");#define MAJOR_NUM 254static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);153struct file_operations globalvar_fops ={read: globalvar_read, write: globalvar_write,};static int global_var = 0;static struct semaphore sem;static int __init globalvar_init(void){int ret;ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);if (ret){printk("globalvar register failure");}else{printk("globalvar register success");init_MUTEX(&sem);}return ret;}static void __exit globalvar_exit(void){int ret;ret = unregister_chrdev(MAJOR_NUM, "globalvar");if (ret){printk("globalvar unregister failure");}else{printk("globalvar unregister success");}}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){//獲得信號量if (down_interruptible(&sem)){return - ERESTARTSYS;}154//將global_var 從內核空間復制到用戶空間if (copy_to_user(buf, &global_var, sizeof(int))){up(&sem);return - EFAULT;}//釋放信號量up(&sem);return sizeof(int);}ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off){//獲得信號量if (down_interruptible(&sem)){return - ERESTARTSYS;}//將用戶空間的數據復制到內核空間的 global_varif (copy_from_user(&global_var, buf, sizeof(int))){up(&sem);return - EFAULT;}//釋放信號量up(&sem);return sizeof(int);}module_init(globalvar_init);module_exit(globalvar_exit);接下來,我們給 globalvar的驅動程序增加 open()和release()函數,并在其中借助自旋鎖來保護對全局變量int globalvar_count ( 記 錄 打 開 設 備 的 進 程 數 ) 的 訪 問 來 實 現 設 備 只 能 被 一 個 進 程 打 開 ( 必 須 確 保globalvar_count 最多只能為1):#include <linux/module.h>#include <linux/init.h>155#include <linux/fs.h>#include <asm/uaccess.h>#include <asm/semaphore.h>MODULE_LICENSE("GPL");#define MAJOR_NUM 254static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);static int globalvar_open(struct inode *inode, struct file *filp);static int globalvar_release(struct inode *inode, struct file *filp);struct file_operations globalvar_fops ={read: globalvar_read, write: globalvar_write, open: globalvar_open, release:globalvar_release,};static int global_var = 0;static int globalvar_count = 0;static struct semaphore sem;static spinlock_t spin = SPIN_LOCK_UNLOCKED;static int __init globalvar_init(void){int ret;ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);if (ret){printk("globalvar register failure");}else{printk("globalvar register success");init_MUTEX(&sem);}return ret;}static void __exit globalvar_exit(void){int ret;156ret = unregister_chrdev(MAJOR_NUM, "globalvar");if (ret){printk("globalvar unregister failure");}else{printk("globalvar unregister success");}}static int globalvar_open(struct inode *inode, struct file *filp){//獲得自選鎖spin_lock(&spin);//臨界資源訪問if (globalvar_count){spin_unlock(&spin);return - EBUSY;}globalvar_count++;//釋放自選鎖spin_unlock(&spin);return 0;}static int globalvar_release(struct inode *inode, struct file *filp){globalvar_count--;return 0;}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t*off){if (down_interruptible(&sem)){return - ERESTARTSYS;}157if (copy_to_user(buf, &global_var, sizeof(int))){up(&sem);return - EFAULT;}up(&sem);return sizeof(int);}static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,loff_t *off){if (down_interruptible(&sem)){return - ERESTARTSYS;}if (copy_from_user(&global_var, buf, sizeof(int))){up(&sem);return - EFAULT;}up(&sem);return sizeof(int);}module_init(globalvar_init);module_exit(globalvar_exit);為 了 上 述 驅 動 程 序 的 效 果 , 我 們 啟 動 兩 個 進 程 分 別 打 開/dev/globalvar 。 在 兩 個 終 端 中 調用./globalvartest.o 測試程序,當一個進程打開/dev/globalvar后,另外一個進程將打開失敗,輸出“deviceopen failure”,如下圖:4.設備的阻塞與非阻塞操作阻塞操作是指,在執行設備操作時,若不能獲得資源,則進程掛起直到滿足可操作的條件再進行操作。非阻塞操作的進程在不能進行設備操作時,并不掛起。被掛起的進程進入 sleep 狀態,被從調度器的運行隊列移走,直到等待的條件被滿足。在 Linux 驅動程序中,我們可以使用等待隊列(wait queue)來實現阻塞操作。wait queue 很早就作為一個基本的功能單位出現在 Linux 內核里了,它以隊列為基礎數據結構,與進程調度機制緊密結合,能夠用于實現核心的異步事件通知機制。等待隊列可以用來同步對系統資源的訪問,上節中所講述 Linux 信號量在內核中也是由等待隊列來實現的。158下面我們重新定義設備“globalvar”,它可以被多個進程打開,但是每次只有當一個進程寫入了一個數據之后本進程或其它進程才可以讀取該數據,否則一直阻塞。#include <linux/module.h>#include <linux/init.h>#include <linux/fs.h>#include <asm/uaccess.h>#include <linux/wait.h>#include <asm/semaphore.h>MODULE_LICENSE("GPL");#define MAJOR_NUM 254static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);struct file_operations globalvar_fops ={read: globalvar_read, write: globalvar_write,};static int global_var = 0;static struct semaphore sem;static wait_queue_head_t outq;static int flag = 0;static int __init globalvar_init(void){int ret;ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);if (ret){printk("globalvar register failure");}else{printk("globalvar register success");init_MUTEX(&sem);init_waitqueue_head(&outq);}return ret;}static void __exit globalvar_exit(void)159{int ret;ret = unregister_chrdev(MAJOR_NUM, "globalvar");if (ret){printk("globalvar unregister failure");}else{printk("globalvar unregister success");}}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t*off){//等待數據可獲得if (wait_event_interruptible(outq, flag != 0)){return - ERESTARTSYS;}if (down_interruptible(&sem)){return - ERESTARTSYS;}flag = 0;if (copy_to_user(buf, &global_var, sizeof(int))){up(&sem);return - EFAULT;}up(&sem);return sizeof(int);}static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,loff_t *off){if (down_interruptible(&sem))160{return - ERESTARTSYS;}if (copy_from_user(&global_var, buf, sizeof(int))){up(&sem);return - EFAULT;}up(&sem);flag = 1;//通知數據可獲得wake_up_interruptible(&outq);return sizeof(int);}module_init(globalvar_init);module_exit(globalvar_exit);編寫兩個用戶態的程序來測試,第一個用于阻塞地讀/dev/globalvar,另一個用于寫/dev/globalvar。只有當后一個對/dev/globalvar 進行了輸入之后,前者的read 才能返回。讀的程序為:#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>main(){int fd, num;fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);if (fd != - 1){while (1){read(fd, &num, sizeof(int)); //程序將阻塞在此語句,除非有針對globalvar 的輸入printf("The globalvar is %d/n", num);//如果輸入是 0,則退出if (num == 0){close(fd);break;}}161}else{printf("device open failure/n");}}寫的程序為:#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>main(){int fd, num;fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);if (fd != - 1){while (1){printf("Please input the globalvar:/n");scanf("%d", &num);write(fd, &num, sizeof(int));//如果輸入 0,退出if (num == 0){close(fd);break;}}}else{printf("device open failure/n");}}打開兩個終端,分別運行上述兩個應用程序,發現當在第二個終端中沒有輸入數據時,第一個終端沒有輸出(阻塞),每當我們在第二個終端中給 globalvar 輸入一個值,第一個終端就會輸出這個值,如下圖:關于上述例程,我們補充說一點,如果將驅動程序中的 read 函數改為:static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t*off){//獲取信號量:可能阻塞162if (down_interruptible(&sem)){return - ERESTARTSYS;}//等待數據可獲得:可能阻塞if (wait_event_interruptible(outq, flag != 0)){return - ERESTARTSYS;}flag = 0;//臨界資源訪問if (copy_to_user(buf, &global_var, sizeof(int))){up(&sem);return - EFAULT;}//釋放信號量up(&sem);return sizeof(int);}即交換 wait_event_interruptible(outq, flag != 0)和down_interruptible(&sem)的順序,這個驅動程序將變得不可運行。實際上,當兩個可能要阻塞的事件同時出現時,即兩個 wait_event 或 down擺在一起的時候,將變得非常危險,死鎖的可能性很大,這個時候我們要特別留意它們的出現順序。當然,我們應該盡可能地避免這種情況的發生!+還有一個與設備阻塞與非阻塞訪問息息相關的論題,即select 和poll,select 和poll 的本質一樣,前者在BSD Unix 中引入,后者在System V 中引入。poll 和select 用于查詢設備的狀態,以便用戶程序獲知是否能對設備進行非阻塞的訪問,它們都需要設備驅動程序中的 poll 函數支持。驅動程序中 poll 函數中最主要用到的一個API 是poll_wait,其原型如下:void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);poll_wait 函數所做的工作是把當前進程添加到wait 參數指定的等待列表(poll_table)中。下面我們給globalvar 的驅動添加一個poll 函數:static unsigned int globalvar_poll(struct file *filp, poll_table *wait){unsigned int mask = 0;poll_wait(filp, &outq, wait);//數據是否可獲得?if (flag != 0){163mask |= POLLIN | POLLRDNORM; //標示數據可獲得}return mask;}需要說明的是, poll_wait函數并不阻塞,程序中 poll_wait(filp, &outq, wait)這句話的意思并不是說一直等待 outq 信號量可獲得,真正的阻塞動作是上層的select/poll 函數中完成的。select/poll 會在一個循環中對每個需要監聽的設備調用它們自己的 poll 支持函數以使得當前進程被加入各個設備的等待列表。若當前沒有任何被監聽的設備就緒,則內核進行調度(調用 schedule)讓出cpu 進入阻塞狀態,schedule 返回時將再次循環檢測是否有操作可以進行,如此反復;否則,若有任意一個設備就緒, select/poll 都立即返回。我們編寫一個用戶態應用程序來測試改寫后的驅動。 程序中要用到 BSD Unix 中引入的select 函數, 其原型為:int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval*timeout);其中 readfds、writefds、exceptfds 分別是被select()監視的讀、寫和異常處理的文件描述符集合,numfds的值是需要檢查的號碼最高的文件描述符加 1。timeout 參數是一個指向struct timeval 類型的指針,它可以使 select()在等待timeout 時間后若沒有文件描述符準備好則返回。struct timeval 數據結構為:struct timeval{int tv_sec; /* seconds */int tv_usec; /* microseconds */};除此之外,我們還將使用下列 API:FD_ZERO(fd_set *set)――清除一個文件描述符集;FD_SET(int fd,fd_set *set)――將一個文件描述符加入文件描述符集中;FD_CLR(int fd,fd_set *set)――將一個文件描述符從文件描述符集中清除;FD_ISSET(int fd,fd_set *set)――判斷文件描述符是否被置位。下面的用戶態測試程序等待/dev/globalvar 可讀,但是設置了5 秒的等待超時,若超過5 秒仍然沒有數據可讀,則輸出“No data within 5 seconds”:#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>main(){int fd, num;fd_set rfds;struct timeval tv;fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);if (fd != - 1)164{while (1){//查看 globalvar是否有輸入FD_ZERO(&rfds);FD_SET(fd, &rfds);//設置超時時間為 5stv.tv_sec = 5;tv.tv_usec = 0;select(fd + 1, &rfds, NULL, NULL, &tv);//數據是否可獲得?if (FD_ISSET(fd, &rfds)){read(fd, &num, sizeof(int));printf("The globalvar is %d/n", num);//輸入為 0,退出if (num == 0){close(fd);break;}}elseprintf("No data within 5 seconds./n");}}else{printf("device open failure/n");}}開兩個終端,分別運行程序:一個對 globalvar進行寫,一個用上述程序對 globalvar進行讀。當我們在寫終端給 globalvar 輸入一個值后,讀終端立即就能輸出該值,當我們連續5 秒沒有輸入時,“No data within 5seconds”在讀終端被輸出.
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
成人网页在线免费观看| 久久伊人精品天天| 懂色av影视一区二区三区| 亚洲免费av电影| 日韩精品视频免费专区在线播放| 青青草99啪国产免费| 九九久久久久99精品| 成人在线视频福利| 日韩av免费在线| 亚洲欧美国产日韩天堂区| 日韩欧美在线视频免费观看| 成年无码av片在线| 97久久精品人人澡人人爽缅北| 九九久久国产精品| 国产精品第8页| 日韩精品中文字幕有码专区| 免费97视频在线精品国自产拍| 亚洲精品网站在线播放gif| 日韩电影中文 亚洲精品乱码| 97久久精品人人澡人人爽缅北| 日韩黄在线观看| 中文字幕精品久久久久| 亚洲在线视频福利| 欧美一级视频一区二区| 欧美网站在线观看| 国产精品白嫩初高中害羞小美女| 一区二区欧美日韩视频| 国内精品久久久| 国产精品久久一区主播| 欧美午夜性色大片在线观看| 欧美一区三区三区高中清蜜桃| wwwwwwww亚洲| 高清日韩电视剧大全免费播放在线观看| 亚洲成人亚洲激情| 亚洲国产高清自拍| 亚洲国产精品久久久久秋霞不卡| 91在线无精精品一区二区| 亚洲男人天堂2023| 夜夜嗨av一区二区三区四区| 91在线免费网站| 日韩中文av在线| 韩国欧美亚洲国产| 亚洲精品电影网在线观看| 欧美国产乱视频| 亚洲国产精品久久91精品| 久久视频在线播放| 欧美成人午夜影院| 国产主播喷水一区二区| 中文字幕日韩欧美在线| 久久久久久网站| 亚洲影院色无极综合| 91最新国产视频| 欧美超级免费视 在线| 色香阁99久久精品久久久| 日韩精品亚洲精品| 欧美日韩福利电影| 亚洲国产天堂久久综合| 亚洲女人初尝黑人巨大| 成人午夜高潮视频| 亚洲女人天堂av| 国产精品午夜国产小视频| 亚洲女同精品视频| 国产精品三级久久久久久电影| 这里只有精品丝袜| 久久精品一区中文字幕| 久久精品视频一| 欧美日韩成人在线播放| 色哟哟入口国产精品| 国产精品久久久久国产a级| 国产欧美精品久久久| 国产男女猛烈无遮挡91| 在线成人激情视频| 中文字幕自拍vr一区二区三区| 久久综合88中文色鬼| 亚洲欧美在线一区二区| 成人免费看吃奶视频网站| 欧美高清第一页| 亚洲天堂免费视频| 亚洲天堂免费观看| 亚洲欧洲成视频免费观看| 欧美理论电影在线播放| 久久久精品在线| 亚洲一区中文字幕在线观看| 91av视频在线| 日本高清不卡在线| 亚洲精品久久久久国产| 国产精品第1页| 国产视频亚洲精品| 91免费高清视频| 日韩欧美在线免费| 国产91精品视频在线观看| 久久99青青精品免费观看| 国产亚洲xxx| 日韩电影在线观看永久视频免费网站| 欧美黑人xxxⅹ高潮交| 国产精品福利小视频| 亚洲男人天堂网| 久热精品视频在线免费观看| 日韩中文字幕在线视频播放| 96国产粉嫩美女| 久久精品视频播放| 最好看的2019年中文视频| 国产精品成人一区二区| 欧美最猛黑人xxxx黑人猛叫黄| 欧美一级视频免费在线观看| 国产精品久久婷婷六月丁香| 欧美黑人性视频| 欧洲美女免费图片一区| 三级精品视频久久久久| 国产精品嫩草影院一区二区| 国产91久久婷婷一区二区| 日韩网站免费观看高清| 插插插亚洲综合网| 久久久www成人免费精品| 欧美美女15p| 国产成人精品免费久久久久| 一区二区三区四区精品| 一区二区亚洲欧洲国产日韩| 亚洲欧美在线免费| 亚洲xxxxx| 九九热这里只有精品免费看| 亚洲一级免费视频| 97久久精品人人澡人人爽缅北| 2019中文在线观看| 日韩视频在线观看免费| 中文字幕亚洲一区在线观看| 日韩一区二区精品视频| 亚洲国产欧美在线成人app| 法国裸体一区二区| 国产精品久久99久久| 国语自产偷拍精品视频偷| 日韩国产在线看| 91青草视频久久| 日韩精品中文字幕有码专区| 成人免费xxxxx在线观看| 97在线观看免费| 92国产精品久久久久首页| 亚洲激情在线观看视频免费| 韩国一区二区电影| 久久精品国产91精品亚洲| 国产精品一久久香蕉国产线看观看| 国产欧美日韩91| 成人精品一区二区三区电影免费| 国产欧美一区二区三区在线看| 亚洲精品乱码久久久久久金桔影视| 51ⅴ精品国产91久久久久久| 国产69精品99久久久久久宅男| 亚洲视频在线观看免费| 国产精品狼人色视频一区| 国语对白做受69| 国产精品久久久久久久久久久久| 色小说视频一区| 高清亚洲成在人网站天堂| 国产精品视频网| 亚洲黄色在线看| 亚洲女人被黑人巨大进入| 国产日韩亚洲欧美| 久久久爽爽爽美女图片| 亚洲精品久久久久久久久| 97视频在线看| 亚洲精品一区二区三区婷婷月| 久久久久女教师免费一区| 亚洲精品日韩欧美| 久久久伊人欧美|