C语言嵌入式系统编程修炼
?
?
C語言嵌入式系統編程修煉之內存操作篇
數據指針
???? 在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的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)。
??? 記住:CPU以字節為單位編址,而C語言指針以指向的數據類型長度作自增和自減。理解這一點對于以指針直接操作內存是相當重要的。
函數指針
???? 首先要理解以下三個問題:
???? (1)C語言中函數名直接對應于函數生成的指令代碼在內存中的地址,因此函數名可以直接賦給指向函數的指針;
???? (2)調用函數實際上等同于"調轉指令+參數傳遞處理+回歸位置入棧",本質上最核心的操作是將函數生成的目標代碼的首地址賦給CPU的PC寄存器;
???? (3)因為函數調用的本質是跳轉到某一個地址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:
???? 請拿出你可以獲得的任何一本大學《微型計算機原理》教材,書中講到,186 CPU啟動后跳轉至絕對地址0xFFFF0(對應C語言指針是0xF000FFF0,0xF000為段地址,0xFFF0為段內偏移)執行,請看下面的代碼:
??? typedef void (*lpFunction) ( ); /* 定義一個無參數、無返回類型的 */
??? /* 函數指針類型 */
??? lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定義一個函數指針,指向*/
??? /* CPU啟動后所執行第一條指令的位置 */
??? lpReset(); /* 調用函數 */
???? 在以上的程序中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟重啟"的作用,跳轉到CPU啟動后第一條要執行的指令的位置。
???? 記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!
數組vs.動態申請
???? 在嵌入式系統中動態內存申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的內存空間往往是十分有限的,不經意的內存泄露會很快導致系統的崩潰。
???? 所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程序:
??? char * function(void)
??? {
???? char *p;
???? p = (char *)malloc(…);
???? if(p==NULL)
???? …;
???? … /* 一系列針對p的操作 */
???? return p;
??? }
???? 在某處調用function(),用完function中動態申請的內存后將其free,如下:
??? char *q = function();
??? …
??? free(q);
???? 上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即"誰申請,就由誰釋放"原則。不滿足這個原則,會導致代碼的耦合度增大,因為用戶在調用function函數時需要知道其內部細節!
???? 正確的做法是在調用處申請內存,并傳入function函數,如下:
??? char *p="malloc"(…);
??? if(p==NULL)
??? …;
??? function(p);
??? …
??? free(p);
??? p="NULL";
???? 而函數function則接收參數p,如下:
??? void function(char *p)
??? {
???? … /* 一系列針對p的操作 */
??? }
???? 基本上,動態申請內存方式可以用較大的數組替換。對于編程新手,筆者推薦你盡量采用數組!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻范政治錯誤走反革命道路的楊康。
???? 給出原則:
???? (1)盡可能的選用數組,數組不能越界訪問(真理越過一步就是謬誤,數組越過界限就光榮地成全了一個混亂的嵌入式系統);
???? (2)如果使用動態申請,則申請后一定要判斷是否申請成功了,并且malloc和free應成對出現!
??? const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:"只能讀的普通變量",可以稱其為"不能改變的變量"(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在C語言中如下程序是非法的:
關鍵字const
???? const意味著"只讀"。區別如下代碼的功能非常重要,也是老生長嘆,如果你還不知道它們的區別,而且已經在程序界摸爬滾打多年,那只能說這是一個悲哀:
??? const int a;
??? int const a;
??? const int *a;
??? int * const a;
??? int const * a const;
??? const int SIZE = 10;
??? char a[SIZE]; /* 非法:編譯階段不能用到變量 */
關鍵字volatile
??? volatile變量可能用于如下幾種情況:
???? (1) 并行設備的硬件寄存器(如:狀態寄存器,例中的代碼屬于此類);
???? (2) 一個中斷服務子程序中會訪問到的非自動變量(也就是全局變量);
???? (3) 多線程應用中被幾個任務共享的變量。
C語言嵌入式系統編程修煉之屏幕操作篇
漢字處理
???? 現在要解決的問題是,嵌入式系統中經常要使用的并非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的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)
??? {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(…)
??? {
???? static 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) 沒有定時器,一個網絡協議將無法運轉,因為其無法獲知何時包傳輸超時并重傳之,無法在特定的時間完成特定的任務。
???? 因此,沒有定時器將意味著沒有操作系統、沒有網絡、沒有多媒體,這將是怎樣的黑暗?所以,合理并靈活地使用各種定時器,是對一個軟件人的最基本需求!
菜單操作
???? 無數人為之絞盡腦汁的問題終于出現了,在這一節里,我們將看到,在C語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!
???? 筆者曾經是個笨蛋,被菜單搞暈了,給出這樣的一個系統:
???? 要求以鍵盤上的"← →"鍵切換菜單焦點,當用戶在焦點處于某菜單時,若敲擊鍵盤上的OK、CANCEL鍵則調用該焦點菜單對應之處理函數。我曾經傻傻地這樣做著:
??? /* 按下OK鍵 */
??? void onOkKey()
??? {
???? /* 判斷在什么焦點菜單上按下Ok鍵,調用相應處理函數 */
???? Switch(currentFocus)
???? {
???? case MENU1:
???? menu1OnOk();
???? break;
???? case MENU2:
???? menu2OnOk();
???? break;
???? …
???? }
??? }
??? /* 按下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
???? }
???? ,
???? {
???? " menu4", 7, 48, menu4OnOk, menu4OnCancel
???? }
???? …
??? };
???? OK鍵和CANCEL鍵的處理變成:
??? /* 按下OK鍵 */
??? void onOkKey()
??? {
???? menu[currentFocusMenu].onOkFun();
??? }
??? /* 按下Cancel鍵 */
??? void onCancelKey()
??? {
???? menu[currentFocusMenu].onCancelFun();
??? }
???? 程序被大大簡化了,也開始具有很好的可擴展性!我們僅僅利用了面向對象中的封裝思想,就讓程序結構清晰,其結果是幾乎可以在無需修改程序的情況下在系統中添加更多的菜單,而系統的按鍵處理函數保持不變。
???? 面向對象,真神了!
C語言嵌入式系統編程修煉之鍵盤操作篇
??? 功能鍵的問題在于,用戶界面并非固定的,用戶功能鍵的選擇將使屏幕畫面處于不同的顯示狀態下。
處理功能鍵
???? 功能鍵的問題在于,用戶界面并非固定的,用戶功能鍵的選擇將使屏幕畫面處于不同的顯示狀態下。例如,主畫面如圖1:
??? 圖1 主畫面
???? 當用戶在設置XX上按下Enter鍵之后,畫面就切換到了設置XX的界面,如圖2:
??? 圖2 切換到設置XX畫面
???? 程序如何判斷用戶處于哪一畫面,并在該畫面的程序狀態下調用對應的功能鍵處理函數,而且保證良好的結構,是一個值得思考的問題。
???? 讓我們來看看WIN32編程中用到的"窗口"概念,當消息(message)被發送給不同窗口的時候,該窗口的消息處理函數(是一個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 .ID!= currentFocus)&& (i >"移位操作效率更高,我們僅是為了說明問題的方便。試想,如果用戶輸入是十進制的,power函數或許是唯一的選擇了。
總結
???? 本篇給出了鍵盤操作所涉及的各個方面:功能鍵處理、數字鍵處理及用戶輸入整理,基本上提供了一個全套的按鍵處理方案。對于功能鍵處理方法,將LCD屏幕與Windows窗口進行類比,提出了較新穎地解決屏幕、鍵盤繁雜交互問題的方案。
???? 計算機學的許多知識都具有相通性,因而,不斷追趕時髦技術而忽略基本功的做法是徒勞無意的。我們最多需要"精通"三種語言(精通,一個在如今的求職簡歷里泛濫成災的詞語),最佳拍檔是匯編、C、C++(或JAVA),很顯然,如果你"精通"了這三種語言,其它語言你應該是可以很快"熟悉"的,否則你就沒有"精通"它們。
C語言嵌入式系統編程修煉之性能優化篇
使用宏定義
???? 在C語言中,宏是產生內嵌代碼的唯一方法。對于嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法。
???? 寫一個"標準"宏MIN ,這個宏輸入兩個參數并返回較小的一個:
???? 錯誤做法:
??? #define MIN(A,B) ( A 外部同步RAM > 外部異步RAM > FLASH/ROM
???? 對于程序代碼,已經被燒錄在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 >"通??梢蕴岣咚惴ㄐ?。因為乘除運算指令周期通常比移位運算大。
???? C語言位運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關系。我們通常要對硬件寄存器進行位設置,譬如,我們通過將AM186ER型80186處理器的中斷屏蔽控制寄存器的第低6位設置為0(開中斷2),最通用的做法是:
??? #define INT_I2_MASK 0x0040
??? wTemp = inword(INT_MASK);
??? outword(INT_MASK, wTemp &~INT_I2_MASK);
???? 而將該位設置為1的做法是:
??? #define INT_I2_MASK 0x0040
??? wTemp = inword(INT_MASK);
??? outword(INT_MASK, wTemp | INT_I2_MASK);
???? 判斷該位是否為1的做法是:
??? #define INT_I2_MASK 0x0040
??? wTemp = inword(INT_MASK);
??? if(wTemp & INT_I2_MASK)
??? {
??? … /* 該位為1 */
??? }
???? 上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握。
總結
???? 在性能優化方面永遠注意80-20準備,不要優化程序中開銷不大的那80%,這是勞而無功的。
???? 宏定義是C語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但宏在本質上不是函數,因而要防止宏展開后出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準C至今沒有包括C++中inline函數的功能,inline函數兼具無調用開銷和安全的優點。
???? 使用寄存器變量、內嵌匯編和活用位操作也是提高程序效率的有效方法。
???? 除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬件設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用DMA傳輸方式等。
?
轉載于:https://blog.51cto.com/1572091hyl10/479143
總結
以上是生活随笔為你收集整理的C语言嵌入式系统编程修炼的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用PHP的Popen实现RRDTOOL
- 下一篇: PowerShell2.0之Window