C語言嵌入式系統(tǒng)編程_第1頁
C語言嵌入式系統(tǒng)編程_第2頁
C語言嵌入式系統(tǒng)編程_第3頁
C語言嵌入式系統(tǒng)編程_第4頁
C語言嵌入式系統(tǒng)編程_第5頁
已閱讀5頁,還剩16頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

C語言嵌入式系統(tǒng)編程修煉之道一一背景篇不同于一般形式的軟件編程,嵌入式系統(tǒng)編程建立在特定的硬件平臺上,勢必要求其編程語言具備較強的硬件直接操作能力。無疑,匯編語言具備這樣的特質。但是,歸因于匯編語言開發(fā)過程的復雜性,它并不是嵌入式系統(tǒng)開發(fā)的一般選擇。而與之相比,C語言一一一種“高級的低級”語言,則成為嵌入式系統(tǒng)開發(fā)的最佳選擇。筆者在嵌入式系統(tǒng)項目的開發(fā)過程中,一次又一次感受到C語言的精妙,沉醉于C語言給嵌入式開發(fā)帶來的便利。本文的目的在于進行“C語言嵌入式系統(tǒng)開發(fā)的內功心法”秀,一共包括25招。圖1給出了本文的討論所基于的硬件平臺,實際上,這也是大多數嵌入式系統(tǒng)的硬件平臺。它包括兩部分:(1) 以通用處理器為中心的協(xié)議處理模塊,用于網絡控制協(xié)議的處理;(2) 以數字信號處理器(DSP)為中心的信號處理模塊,用于調制、解調和數/模信號轉換。本文的討論主要圍繞以通用處理器為中心的協(xié)議處理模塊進行,因為它更多地牽涉到具體的C語言編程技巧。而DSP編程則重點關注具體的數字信號處理算法,主要涉及通信領域的知識,不是本文的討論重點。著眼于討論普遍的嵌入式系統(tǒng)C編程技巧,系統(tǒng)的協(xié)議處理模塊沒有選擇特別的CPU,而是選擇了眾所周知的CPU芯片——80186,每一位學習過《微機原理》的讀者都應該對此芯片有一個基本的認識,且對其指令集比較熟悉。80186的字長是16位,可以尋址到的內存空間為1MB,只有實地址模式。C語言編譯生成的指針為32位(雙字),高16位為段地址,低16位為段內編譯,一段最多64KB。圖1系統(tǒng)硬件架構協(xié)議處理模塊中的FLASH和RAM幾乎是每個嵌入式系統(tǒng)的必備設備,前者用于存儲程序,后者則是程序運行時指令及數據的存放位置。系統(tǒng)所選擇的FLASH和RAM的位寬都為16位,與CPU一致。實時鐘芯片可以為系統(tǒng)定時,給出當前的年、月、日及具體時間(小時、分、秒及毫秒),可以設定其經過一段時間即向CPU提出中斷或設定報警時間到來時向CPU提出中斷(類似鬧鐘功能)。NVRAM(非易失去性RAM)具有掉電不丟失數據的特性,可以用于保存系統(tǒng)的設置信息,譬如網絡協(xié)議參數等。在系統(tǒng)掉電或重新啟動后,仍然可以讀取先前的設置信息。其位寬為8位,比CPU字長小。文章特意選擇一個與CPU字長不一致的存儲芯片,為后文中一節(jié)的討論創(chuàng)造條件。UART則完成CPU并行數據傳輸與RS-232串行數據傳輸的轉換,它可以在接收到[1~MAX_BUFFER]字節(jié)后向CPU提出中斷,MAX_BUFFER為UART芯片存儲接收到字節(jié)的最大緩沖區(qū)。鍵盤控制器和顯示控制器則完成系統(tǒng)人機界面的控制。以上提供的是一個較完備的嵌入式系統(tǒng)硬件架構,實際的系統(tǒng)可能包含更少的外設。之所以選擇一個完備的系統(tǒng),是為了后文更全面的討論嵌入式系統(tǒng)C語言編程技巧的方方面面,所有設備都會成為后文的分析目標。嵌入式系統(tǒng)需要良好的軟件開發(fā)環(huán)境的支持,由于嵌入式系統(tǒng)的目標機資源受限,不可能在其上建立龐大、復雜的開發(fā)環(huán)境,因而其開發(fā)環(huán)境和目標運行環(huán)境相互分離。因此,嵌入式應用軟件的開發(fā)方式一般是,在宿主機(Host)上建立開發(fā)環(huán)境,進行應用程序編碼和交叉編譯,然后宿主機同目標機(Target)建立連接,將應用程序下載到目標機上進行交叉調試,經過調試和優(yōu)化,最后將應用程序固化到目標機中實際運行。

CAD-UL是適用于x86處理器的嵌入式應用軟件開發(fā)環(huán)境,它運行在Windows操作系統(tǒng)之上,可生成x86處理器的目標代碼并通過PC機的COM口(RS-232串口)或以太網口下載到目標機上運行,如圖2。其駐留于目標機FLASH存儲器中的monitor程序可以監(jiān)控宿主機Windows調試平臺上的用戶調試指令,獲取CPU寄存器的值及目標機存儲空間、I/O空間的內容。圖2交叉開發(fā)環(huán)境后續(xù)章節(jié)將從軟件架構、內存操作、屏幕操作、鍵盤操作、性能優(yōu)化等多方面闡述C語言嵌入式系統(tǒng)的編程技巧。軟件架構是一個宏觀概念,與具體硬件的聯(lián)系不大;內存操作主要涉及系統(tǒng)中的FLASH、RAM和NVRAM芯片;屏幕操作則涉及顯示控制器和實時鐘;鍵盤操作主要涉及鍵盤控制器;性能優(yōu)化則給出一些具體的減小程序時間、空間消耗的技巧。本文即將講述的25個主題可分為兩類,一類是編程技巧,有很強的適用性;一類則介紹嵌入式系統(tǒng)編程的一般常識,具有一定的理論意義。C語言嵌入式系統(tǒng)編程修煉之道一一軟件架構篇模塊劃分模塊劃分的“劃”是規(guī)劃的意思,意指怎樣合理的將一個很大的軟件劃分為一系列功能獨立的部分合作完成系統(tǒng)的需求。C語言作為一種結構化的程序設計語言,在模塊的劃分上主要依據功能(依功能進行劃分在面向對象設計中成為一個錯誤,牛頓定律遇到了相對論),C語言模塊化程序設計需理解如下概念:(1) 模塊即是一個.C文件和一個.h文件的結合,頭文件(.h)中是對于該模塊接口的聲明;(2) 某模塊提供給其它模塊調用的外部函數及數據需在.h中文件中冠以extern關鍵字聲明;(3) 模塊內的函數和全局變量需在.c文件開頭冠以static關鍵字聲明;(4)永遠不要在.h文件中定義變量!定義變量和聲明變量的區(qū)別在于定義會產生內存分配的操作,是匯編階段的概念;而聲明則只是告訴包含該聲明的模塊在連接階段從其它模塊尋找外部函數和變量。如:/*module1.h*//*module1.c*/#include“module1.h”/*module2.c*/#include“module1.h”/*module3.c*/#include“module1.h”inta=5; /*在模塊/*module1.c*/#include“module1.h”/*module2.c*/#include“module1.h”/*module3.c*/#include“module1.h”/*在模塊1中包含模塊1的.h文件*//*在模塊2中包含模塊1的.h文件*//*在模塊3中包含模塊1的.h文件*/以上程序的結果是在模塊1、2、3中都定義了整型變量a,a在不同的模塊中對應不同的地址單元,這個世界上從來不需要這樣的程序。正確的做法是:/*module1.h*/externinta; /*在模塊1的.h文件中聲明inta*//*module1.c*/#include"modulel.h”/*在模塊1中包含模塊1的.h文件*/inta=5; /*在模塊1的.c文件中定義inta*//*module2.c*/#include"modulel.h”/*在模塊2中包含模塊1的.h文件*//*module3.c*/#include"modulel.h”/*在模塊3中包含模塊1的.h文件*/這樣如果模塊1、2、3操作a的話,對應的是同一片內存單元。一個嵌入式系統(tǒng)通常包括兩類模塊:硬件驅動模塊,一種特定硬件對應一個模塊;軟件功能模塊,其模塊的劃分應滿足低偶合、高內聚的要求。多任務還是單任務所謂“單任務系統(tǒng)”是指該系統(tǒng)不能支持多任務并發(fā)操作,宏觀串行地執(zhí)行一個任務。而多任務系統(tǒng)則可以宏觀并行(微觀上可能串行)地“同時”執(zhí)行多個任務。多任務的并發(fā)執(zhí)行通常依賴于一個多任務操作系統(tǒng)(OS),多任務OS的核心是系統(tǒng)調度器,它使用任務控制塊(TCB)來管理任務調度功能。TCB包括任務的當前狀態(tài)、優(yōu)先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。調度器在任務被激活時要用到這些信息。此外,TCB還被用來存放任務的“上下文”(context)。任務的上下文就是當一個執(zhí)行中的任務被停止時,所要保存的所有信息。通常,上下文就是計算機當前的狀態(tài),也即各個寄存器的內容。當發(fā)生任務切換時,當前運行的任務的上下文被存入TCB,并將要被執(zhí)行的任務的上下文從它的TCB中取出,放入各個寄存器中。嵌入式多任務OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遙不可及的神壇之物,我們可以用不到1000行代碼實現一個針對80186處理器的功能最簡單的OS內核,作者正準備進行此項工作,希望能將心得貢獻給大家。究竟選擇多任務還是單任務方式,依賴于軟件的體系是否龐大。例如,絕大多數手機程序都是多任務的,但也有一些小靈通的協(xié)議棧是單任務的,沒有操作系統(tǒng),它們的主程序輪流調用各個軟件模塊的處理程序,模擬多任務環(huán)境。單任務程序典型架構從CPU復位時的指定地址開始執(zhí)行;跳轉至匯編代碼startup處執(zhí)行;跳轉至用戶主程序main執(zhí)行,在main中完成:初試化各硬件設備;初始化各軟件模塊;進入死循環(huán)(無限循環(huán)),調用各模塊的處理函數用戶主程序和各模塊的處理函數都以C語言完成。用戶主程序最后都進入了一個死循環(huán),其首選方案是:while(1){}有的程序員這樣寫:for(;;){}這個語法沒有確切表達代碼的含義,我們從for(;;)看不出什么,只有弄明白for(;;)在C語言中意味著無條件循環(huán)才明白其意。下面是幾個“著名”的死循環(huán):操作系統(tǒng)是死循環(huán);(2)WIN32程序是死循環(huán);嵌入式系統(tǒng)軟件是死循環(huán);多線程程序的線程處理函數是死循環(huán)。你可能會辯駁,大聲說:“凡事都不是絕對的,2、3、4都可以不是死循環(huán)”。Yes,youareright,但是你得不到鮮花和掌聲。實際上,這是一個沒有太大意義的牛角尖,因為這個世界從來不需要一個處理完幾個消息就喊著要OS殺死它的WIN32程序,不需要一個剛開始RUN就自行了斷的嵌入式系統(tǒng),不需要莫名其妙啟動一個做一點事就干掉自己的線程。有時候,過于嚴謹制造的不是便利而是麻煩。君不見,五層的TCP/IP協(xié)議棧超越嚴謹的ISO/OSI七層協(xié)議棧大行其道成為事實上的標準?經常有網友討論:printf(“%d,%d”,++i,i++);/*輸出是什么?*/c=a+++b; /*c=?*/等類似問題。面對這些問題,我們只能發(fā)出由衷的感慨:世界上還有很多有意義的事情等著我們去消化攝入的食物。實際上,嵌入式系統(tǒng)要運行到世界末日。中斷服務程序中斷是嵌入式系統(tǒng)中重要的組成部分,但是在標準C中不包含中斷。許多編譯開發(fā)商在標準C上增加了對中斷的支持,提供新的關鍵字用于標示中斷服務程序(ISR),類似于—interrupt、#programinterrupt等。當一個函數被定義為ISR的時候,編譯器會自動為該函數增加中斷服務程序所需要的中斷現場入棧和出棧代碼。中斷服務程序需要滿足如下要求:不能返回值;⑵不能向ISR傳遞參數;⑶ISR應該盡可能的短小精悍;⑷printf(char*IpFormatString,...)函數會帶來重入和性能問題,不能在ISR中采用。在某項目的開發(fā)中,我們設計了一個隊列,在中斷服務程序中,只是將中斷類型添加入該隊列中,在主程序的死循環(huán)中不斷掃描中斷隊列是否有中斷,有則取出隊列中的第一個中斷類型,進行相應處理。/*存放中斷的隊列*/typedefstructtagIntQueue{intintType; /*中斷類型*/structtagIntQueue*next;}IntQueue;IntQueuelpIntQueueHead;__interruptISRexample(){intintType;intType=GetSystemType();QueueAddTail(lpIntQueueHead,intType);/*在隊列尾加入新的中斷*/}在主程序循環(huán)中判斷是否有中斷:While(1){If(!IsIntQueueEmpty()){intType=GetFirstInt();switch(intType) /*是不是很象WIN32程序的消息解析函數?*/{/*對,我們的中斷類型解析很類似于消息驅動*/casexxx: /*我們稱其為“中斷驅動”吧?*/break;casexxx:break;}}}按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執(zhí)行了。硬件驅動模塊一個硬件驅動模塊通常應包括如下函數:(1)中斷服務程序ISR硬件初始化修改寄存器,設置硬件參數(如UART應設置其波特率,AD/DA設備應設置其采樣速率等);將中斷服務程序入口地址寫入中斷向量表:/*設置中斷向量表*/m_myPtr=make_far_pointer(OI);/*返回voidfar型指針voidfar**/m_myPtr+=ITYPE_UART;/*ITYPE_UART:uart中斷服務程序*//*相對于中斷向量表首地址的偏移*/*m_myPtr=&UART_Isr;/*UART_Isr:UART的中斷服務程序*/設置CPU針對該硬件的控制線如果控制線可作PIO(可編程I/O)和控制信號用,貝9設置CPU內部對應寄存器使其作為控制信號;設置CPU內部的針對該設備的中斷屏蔽位,設置中斷方式(電平觸發(fā)還是邊緣觸發(fā))提供一系列針對該設備的操作接口函數。例如,對于LCD,其驅動模塊應提供繪制像素、畫線、繪制矩陣、顯示字符點陣等函數;而對于實時鐘,其驅動模塊貝需提供獲取時間、設置時間等函數。6.C的面向對象化在面向對象的語言里面,出現了類的概念。類是對特定數據的特定操作的集合體。類包含了兩個范疇:數據和操作。而C語言中的struct僅僅是數據的集合,我們可以利用函數指針將struct模擬為一個包含數據和操作的“類”下面的C程序模擬了一個最簡單的“類”#ifndefC_CIass#defineC_CIassstruct#endifC_ClassA{C_ClassA*A_this; /*this指針*/void(*Foo)(C_ClassA*A_this);/*行為:函數指針*/inta;/*數據*/intb;};我們可以利用C語言模擬出面向對象的三個特性:封裝、繼承和多態(tài),但是更多的時候,我們只是需要將數據與行為封裝以解決軟件結構混亂的問題。C模擬面向對象思想的目的不在于模擬行為本身,而在于解決某些情況下使用C語言編程時程序整體框架結構分散、數據和函數脫節(jié)的問題。我們在后續(xù)章節(jié)會看到這樣的例子。總結本篇介紹了嵌入式系統(tǒng)編程軟件架構方面的知識,主要包括模塊劃分、多任務還是單任務選取、單任務程序典型架構、中斷服務程序、硬件驅動模塊設計等,從宏觀上給出了一個嵌入式系統(tǒng)軟件所包含的主要元素。請記?。很浖Y構是軟件的靈魂!結構混亂的程序面目可憎,調試、測試、維護、升級都極度困難。一個高尚的程序員應該是寫出如藝術作品般程序的程序員。C語言嵌入式系統(tǒng)編程修煉之道一一內存操作篇數據指針在嵌入式系統(tǒng)的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的MOV指令,而除C/C++以外的其它編程語言基本沒有直接訪問絕對地址的能力。在嵌入式系統(tǒng)的實際調試中,多借助C語言指針所具有的對絕對地址單元內容的讀寫能力。以指針直接操作內存多發(fā)生在如下幾種情況:(1)某I/O芯片被定位在CPU的存儲空間而非I/O空間,而且寄存器對應于某特定地址;⑵兩個CPU之間以雙端口RAM通信,CPU需要在雙端口RAM的特定單元(稱為mailbox)書寫內容以在對方CPU產生中斷;⑶讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。譬如:unsignedchar*p=(unsignedchar*)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)。同理,若執(zhí)行:longint*p=(longint*)0xF000FF00;則p++(或++p)的結果等同于:p=p+sizeof(longint),而p—(或一p)的結果是p=p-sizeof(longint)。記?。篊PU以字節(jié)為單位編址,而C語言指針以指向的數據類型長度作自增和自減。理解這一點對于以指針直接操作內存是相當重要的。函數指針首先要理解以下三個問題:(1)C語言中函數名直接對應于函數生成的指令代碼在內存中的地址,因此函數名可以直接賦給指向函數的指針;調用函數實際上等同于“調轉指令+參數傳遞處理+回歸位置入?!保举|上最核心的操作是將函數生成的目標代碼的首地址賦給CPU的PC寄存器;因為函數調用的本質是跳轉到某一個地址單元的code去執(zhí)行,所以可以“調用”一個根本就不存在的函數實體,暈?請往下看:請拿出你可以獲得的任何一本大學《微型計算機原理》教材,書中講到,186CPU啟動后跳轉至絕對地址OxFFFFO(對應C語言指針是OxFOOOFFFO,OxFOOO為段地址,OxFFFO為段內偏移)執(zhí)行,請看下面的代碼:typedefvoid(*lpFunction)(); /*定義一個無參數、無返回類型的*//*函數指針類型*/lpFunctionlpReset=(lpFunction)OxFOOOFFFO;/*定義一個函數指針,指向*//*CPU啟動后所執(zhí)行第一條指令的位置*/lpReset(); /*調用函數*/在以上的程序中,我們根本沒有看到任何一個函數實體,但是我們卻執(zhí)行了這樣的函數調用:lpReset(),它實際上起到了“軟重啟”的作用,跳轉到CPU啟動后第一條要執(zhí)行的指令的位置。記?。汉瘮禑o它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執(zhí)行指令!3?數組vs?動態(tài)申請在嵌入式系統(tǒng)中動態(tài)內存申請存在比一般系統(tǒng)編程時更嚴格的要求,這是因為嵌入式系統(tǒng)的內存空間往往是十分有限的,不經意的內存泄露會很快導致系統(tǒng)的崩潰。所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程序:char*function(void){char*p;p=(char*)malloc(...);if(p==NULL).;... /*一系列針對p的操作*/returnp;}在某處調用function。,用完function中動態(tài)申請的內存后將其free,如下:char*q=function();free(q);上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即“誰申請,就由誰釋放”原則。不滿足這個原則,會導致代碼的耦合度增大,因為用戶在調用Function函數時需要知道其內部細節(jié)!正確的做法是在調用處申請內存,并傳入function函數,如下:char*p=malloc(.);if(p==NULL)…;function(p);free(p);p=NULL;而函數function則接收參數p,如下:voidfunction(char*p){… /*一系列針對p的操作*/}基本上,動態(tài)申請內存方式可以用較大的數組替換。對于編程新手,筆者推薦你盡量采用數組!嵌入式系統(tǒng)可以以博大的胸襟接收瑕疵,而無法“海納”錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻范政治錯誤走反革命道路的楊康。給出原則:盡可能的選用數組,數組不能越界訪問(真理越過一步就是謬誤,數組越過界限就光榮地成全了一個混亂的嵌入式系統(tǒng));如果使用動態(tài)申請,則申請后一定要判斷是否申請成功了,并且malloc和free應成對出現!4.關鍵字constconst意味著“只讀”區(qū)別如下代碼的功能非常重要,也是老生長嘆,如果你還不知道它們的區(qū)別,而且已經在程序界摸爬滾打多年,那只能說這是一個悲哀:constinta;intconsta;constint*a;int*consta;intconst*aconst;關鍵字const的作用是為給讀你代碼的人傳達非常有用的信息。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬于“輸入參數”。在有多個形參的時候,函數的調用者可以憑借參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:“只能讀的普通變量”可以稱其為“不能改變的變量”(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在C語言中如下程序是非法的:constintSIZE=10;chara[SIZE];/*非法:編譯階段不能用到變量*/5.關鍵字volatileC語言編譯器會對用戶書寫的代碼進行優(yōu)化,譬如如下代碼:inta,b,c;a=inWord(0x100);/*讀取I/O空間0x100端口的內容存入a變量*/b=a;a=inWord(0x100);/*再次讀取I/O空間0x100端口的內容存入a變量*/c=a;很可能被編譯器優(yōu)化為:inta,b,c;a=inWord(0x100);/*讀取I/O空間0x100端口的內容存入a變量*/b=a;c=a;但是這樣的優(yōu)化結果可能導致錯誤,如果I/O空間0x100端口的內容在執(zhí)行第一次讀操作后被其它程序寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變量a的定義前加上volatile關鍵字可以防止編譯器的類似優(yōu)化,正確的做法是:volatileinta;volatile變量可能用于如下幾種情況:并行設備的硬件寄存器(如:狀態(tài)寄存器,例中的代碼屬于此類);一個中斷服務子程序中會訪問到的非自動變量(也就是全局變量);多線程應用中被幾個任務共享的變量。CPU字長與存儲器位寬不一致處理在背景篇中提到,本文特意選擇了一個與CPU字長不一致的存儲芯片,就是為了進行本節(jié)的討論,解決CPU字長與存儲器位寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫字節(jié)、字的接口,如下:typedefunsignedcharBYTE;typedefunsignedintWORD;/*函數功能:讀NVRAM中字節(jié)*參數:wOffset,讀取位置相對NVRAM基地址的偏移*返回:讀取到的字節(jié)值*/externBYTEReadByteNVRAM(WORDwOffset){LPBYTEIpAddr=(BYTE*)(NVRAM+wOffset*2);/*為什么偏移要X2?*/return*lpAddr;}/*函數功能:讀NVRAM中字*參數:wOffset,讀取位置相對NVRAM基地址的偏移*返回:讀取到的字*/externWORDReadWordNVRAM(WORDwOffset){WORDwTmp=0;LPBYTElpAddr;/*讀取高位字節(jié)*/IpAddr=(BYTE*)(NVRAM+wOffset*2); /*為什么偏移要X2?*/wTmp+=(*lpAddr)*256;/*讀取低位字節(jié)*/lpAddr=(BYTE*)(NVRAM+(wOffset+1)*2); /*為什么偏移要X2?*/wTmp+=*IpAddr;returnwTmp;}/*函數功能:向NVRAM中寫一個字節(jié)*參數:wOffset,寫入位置相對NVRAM基地址的偏移* byData,欲寫入的字節(jié)*/externvoidWriteByteNVRAM(WORDwOffset,BYTEbyData){}/*函數功能:向NVRAM中寫一個字*/*參數:wOffset,寫入位置相對NVRAM基地址的偏移* wData,欲寫入的字*/externvoidWriteWordNVRAM(WORDwOffset,WORDwData){}子貢問曰:Why偏移要乘以2?子曰:請看圖1,16位80186與8位NVRAM之間互連只能以地址線A】對其A°,CPU本身的A0與NVRAM不連接。因此,NVRAM的地址只能是偶數地址,故每次以2為單位前進!圖1CPU與NVRAM地址線連接子貢再問:Sowhy80186的地址線A。不與NVRAM的A。連接?子曰:請看《IT論語》之《微機原理篇》,那里面講述了關于計算機組成的圣人之道??偨Y本篇主要講述了嵌入式系統(tǒng)C編程中內存操作的相關技巧。掌握并深入理解關于數據指針、函數指針、動態(tài)申請內存、const及volatile關鍵字等的相關知識,是一個優(yōu)秀的C語言程序設計師的基本要求。當我們已經牢固掌握了上述技巧后,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在內存操作中體現。我們之所以在嵌入式系統(tǒng)中使用C語言進行程序設計,99%是因為其強大的內存操作能力!如果你愛編程,請你愛C語言;如果你愛C語言,請你愛指針;如果你愛指針,請你愛指針的指針!C語言嵌入式系統(tǒng)編程修煉之道一一屏幕操作篇漢字處理現在要解決的問題是,嵌入式系統(tǒng)中經常要使用的并非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的LCD上沒有必要提供顯示“電子郵件”的功能;一個提供漢字顯示功能的空調的LCD上不需要顯示一條“短消息”,諸如此類。但是一部手機、小靈通則通常需要包括較完整的漢字庫。如果包括的漢字庫較完整,那么,由內碼計算出漢字字模在庫中的偏移是十分簡單的:漢字庫是按照區(qū)位的順序排列的,前一個字節(jié)為該漢字的區(qū)號,后一個字節(jié)為該字的位號。每一個區(qū)記錄94個漢字,位號則為該字在該區(qū)中的位置。因此,漢字在漢字庫中的具體位置計算公式為:94*(區(qū)號-1)+位號-1。減1是因為數組是以0為開始而區(qū)號位號是以1為開始的。只需乘上一個漢字字模占用的字節(jié)數即可,即:(94*(區(qū)號-1)+位號-1)*一個漢字字模占用字節(jié)數,以16*16點陣字庫為例,計算公式則為:(94*(區(qū)號-1)+(位號-1))*32。漢字庫中從該位置起的32字節(jié)信息記錄了該字的字模信息。對于包含較完整漢字庫的系統(tǒng)而言,我們可以以上述規(guī)則計算字模的位置。但是如果僅僅是提供少量漢字呢?譬如幾十至幾百個?最好的做法是:定義宏:defineEX_FONT_CHAR(value)defineEX_FONT_UNICODE_VAL(value)(value),defineEX_FONT_ANSI_VAL(value)(value),定義結構體:typedefstruct_wide_unicode_font16x16{WORDvalue;/*內碼*/BYTEdata[32];/*字模點陣*/}Unicode;#defineCHINESE_CHAR_NUM…/*漢字數量*/字模的存儲用數組:Unicodechinese[CHINESE_CHAR_NUM]={{EX_FONT_CHAR("業(yè)")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}}}要顯示特定漢字的時候,只需要從數組中查找內碼與要求漢字內碼相同的即可獲得字模。如果前面的漢字在數組中以內碼大小順序排列,那么可以以二分查找法更高效的查找到漢字的字模。這是一種很有效的組織小漢字庫的方法,它可以保證程序有很好的結構。系統(tǒng)時間顯示從NVRAM中可以讀取系統(tǒng)的時間,系統(tǒng)一般借助NVRAM產生的秒中斷每秒讀取一次當前時間并在LCD上顯示。關于時間的顯示,有一個效率問題。因為時間有其特殊性,那就是60秒才有一次分鐘的變化,60分鐘才有一次小時變化,如果我們每次都將讀取的時間在屏幕上完全重新刷新一次,則浪費了大量的系統(tǒng)時間。一個較好的辦法是我們在時間顯示函數中以靜態(tài)變量分別存儲小時、分鐘、秒,只有在其內容發(fā)生變化的時候才更新其顯示。externvoidDisplayTime(...){staticBYTEbyHour,byMinute,bySecond;BYTEbyNewHour,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具有了更加強大的威力,它使得某些數據和函數脫離“對象”而成為“類”的一部分,正是它的這一特點,成就了軟件的無數優(yōu)秀設計。動畫顯示動畫是無所謂有,無所謂無的,靜止的畫面走的路多了,也就成了動畫。隨著時間的變更,在屏幕上顯示不同的靜止畫面,即是動畫之本質。所以,在一個嵌入式系統(tǒng)的LCD上欲顯示動畫,必須借助定時器。沒有硬件或軟件定時器的世界是無法想像的:沒有定時器,一個操作系統(tǒng)將無法進行時間片的輪轉,于是無法進行多任務的調度,于是便不再成其為一個多任務操作系統(tǒng);沒有定時器,一個多媒體播放軟件將無法運作,因為它不知道何時應該切換到下一幀畫面;沒有定時器,一個網絡協(xié)議將無法運轉,因為其無法獲知何時包傳輸超時并重傳之,無法在特定的時間完成特定的任務。因此,沒有定時器將意味著沒有操作系統(tǒng)、沒有網絡、沒有多媒體,這將是怎樣的黑暗?所以,合理并靈活地使用各種定時器,是對一個軟件人的最基本需求!在80186為主芯片的嵌入式系統(tǒng)中,我們需要借助硬件定時器的中斷來作為軟件定時器,在中斷發(fā)生后變更畫面的顯示內容。在時間顯示“xx:xx”中讓冒號交替有無,每次秒中斷發(fā)生后,需調用ShowDot:voidShowDot(){staticBOOLbShowDot=TRUE;/*再一次領略static關鍵字的威力*/if(bShowDot){showChar(‘:',xPos,yPos);}else{showChar(‘',xPos,yPos);}bShowDot=!bShowDot;}菜單操作無數人為之絞盡腦汁的問題終于出現了,在這一節(jié)里,我們將看到,在C語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!筆者曾經是個笨蛋,被菜單搞暈了,給出這樣的一個系統(tǒng):圖1菜單范例要求以鍵盤上的“一一”鍵切換菜單焦點,當用戶在焦點處于某菜單時,若敲擊鍵盤上的OK、CANCEL鍵則調用該焦點菜單對應之處理函數。我曾經傻傻地這樣做著:/*按下OK鍵*/voidonOkKey(){/*判斷在什么焦點菜單上按下Ok鍵,調用相應處理函數*/Switch(currentFocus){caseMENU1:menu1OnOk();break;caseMENU2:menu2OnOk();break;}}/*按下Cancel鍵*/voidonCancelKey(){/*判斷在什么焦點菜單上按下Cancel鍵,調用相應處理函數*/Switch(currentFocus){caseMENU1:menu1OnCancel();break;caseMENU2:menu2OnCancel();break;}}終于有一天,我這樣做了:/*將菜單的屬性和操作“封裝”在一起*/typedefstructtagSysMenu{char*text;/*菜單的文本*/BYTExPos;/*菜單在LCD上的x坐標*/BYTEyPos;/*菜單在LCD上的y坐標*/void(*onOkFun)(); /*在該菜單上按下ok鍵的處理函數指針*/void(*onCancelFun)();/*在該菜單上按下cancel鍵的處理函數指針*/}SysMenu,*LPSysMenu;當我定義菜單時,只需要這樣:staticSysMenumenu[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鍵*/voidonOkKey(){menu[currentFocusMenu].onOkFun();}/*按下Cancel鍵*/voidonCancelKey(){menu[currentFocusMenu].onCancelFun();}程序被大大簡化了,也開始具有很好的可擴展性!我們僅僅利用了面向對象中的封裝思想,就讓程序結構清晰,其結果是幾乎可以在無需修改程序的情況下在系統(tǒng)中添加更多的菜單,而系統(tǒng)的按鍵處理函數保持不變。面向對象,真神了!5.模擬MessageBox函數MessageBox函數,這個Windows編程中的超級猛料,不知道是多少入門者第一次用到的函數。還記得我們第一次在Windows中利用MessageBox輸出“Hello,World”對話框時新奇的感覺嗎?無法統(tǒng)計,這個世界上究竟有多少程序員學習Windows編程是從MessageBox(“Hello,World!”,...開始的。在我本科的學校,廣泛流傳著一個詞匯,叫做‘Hello,World‘級程序員”意指入門級程序員,但似乎‘Hello,World‘級”這個說法更搞笑而形象。圖2經典的Hello,World!圖2給出了兩種永恒經典的Hello,World對話框,一種只具有“確定”,一種則包含“確定”、“取消”是的,MessageBox的確有,而且也應該有兩類!這完全是由特定的應用需求決定的。嵌入式系統(tǒng)中沒有給我們提供MessageBox,但是鑒于其功能強大,我們需要模擬之,一個模擬的MessageBox函數為:/*函數名稱:/*功能說明:/*參數說明:/*/*返回值:MessageBox彈出式對話框,顯示提醒用戶的信息lpStr---提醒用戶的字符串輸出信息TYPE---輸出格式(ID_OK=0,ID_OKCANCEL=1)返回對話框接收的鍵值,只有兩種KEY_OK,KEY_CANCEL/*typedefenumTYPE{ID_OK,ID_OKCANCEL}MSG_TYPE;externBYTEMessageBox(LPBYTElpStr,BYTETYPE){BYTEkeyValue=-1;ClearScreen();/*清除屏幕*/DisplayString(xPos,yPos,lpStr,TRUE);/*顯示字符串*//*根據對話框類型決定是否顯示確定、取消*/switch(TYPE){caseID_OK:DisplayString(13,yPos+High+1,"確定",0);break;caseID_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){returnID_OK;}else{returnID_CANCEL;}}上述函數與我們平素在VC++等中使用的MessageBox是何等的神似啊?實現這個函數,你會看到它在嵌入式系統(tǒng)中的妙用是無窮的??偨Y本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統(tǒng)屏幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被LCD上凌亂不堪的顯示內容所困擾。屏幕乃嵌入式系統(tǒng)生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不系統(tǒng)、最混亂的部分,筆者曾深受其害。C語言嵌入式系統(tǒng)編程修煉之道一一鍵盤操作篇處理功能鍵功能鍵的問題在于,用戶界面并非固定的,用戶功能鍵的選擇將使屏幕畫面處于不同的顯示狀態(tài)下。例如,主畫面如圖1:圖1主畫面當用戶在設置XX上按下Enter鍵之后,畫面就切換到了設置XX的界面,如圖2:圖2切換到設置XX畫面程序如何判斷用戶處于哪一畫面,并在該畫面的程序狀態(tài)下調用對應的功能鍵處理函數,而且保證良好的結構,是一個值得思考的問題。讓我們來看看WIN32編程中用到的“窗口”概念,當消息(message)被發(fā)送給不同窗口的時候,該窗口的消息處理函數(是一個callback函數)最終被調用,而在該窗口的消息處理函數中,又根據消息的類型調用了該窗口中的對應處理函數。通過這種方式,WIN32有效的組織了不同的窗口,并處理不同窗口情況下的消息。我們從中學習到的就是:將不同的畫面類比為WIN32中不同的窗口,將窗口中的各種元素(菜單、按鈕等)包含在窗口之中;(2)給各個畫面提供一個功能鍵“消息”處理函數,該函數接收按鍵信息為參數;在各畫面的功能鍵“消息”處理函數中,判斷按鍵類型和當前焦點元素,并調用對應元素的按鍵處理函數。/*將窗口元素、消息處理函數封裝在窗口中*/structwindows{BYTEcurrentFocus;ELEMENTelement[ELEMENT_NUM];void(*messageFun)(BYTEkeyValue);};/*消息處理函數*/voidmessageFunction(BYTEkeyValue){BYTEi=0;/*獲得焦點元素*/while((element[i].ID!=currentFocus)&&(i<ELEMENT_NUM)){i++;}/*“消息映射”*/if(i<ELEMENT_NUM){switch(keyValue){caseOK:element[i].OnOk();break;…}}}在窗口的消息處理函數中調用相應元素按鍵函數的過程類似于“消息映射”,這是我們從WIN32編程中學習到的。編程到了一個境界,很多東西都是相通的了。其它地方的思想可以拿過來為我所用,是為編程中的“拿來主義”。在這個例子中,如果我們還想玩得更大一點,我們可以借鑒MFC中處理MESSAGE_MAP的方法,我們也可以學習MFC定義幾個精妙的宏來實現“消息映射”處理數字鍵用戶輸入數字時是一位一位輸入的,每一位的輸入都對應著屏幕上的一個顯示位置(x坐標,y坐標)。此外,程序還需要記錄該位置輸入的值,所以有效組織用戶數字輸入的最佳方式是定義一個結構體,將坐標和數值捆綁在一起:/*用戶數字輸入結構體*/typedefstructtagInputNum{BYTEbyNum;/*接收用戶輸入賦值*/BYTExPos; /*數字輸入在屏幕上的顯示位置x坐標*/BYTEyPos; /*數字輸入在屏幕上的顯示位置y坐標*/}InputNum,*LPInputNum;那么接收用戶輸入就可以定義一個結構體數組,用數組中的各位組成一個完整的數字:InputNuminputElement[NUM_LENGTH];/*接收用戶數字輸入的數組*//*數字按鍵處理函數*/externvoidonNumKey(BYTEnum){if(num==0||num==1)/*只接收二進制輸入*/{/*在屏幕上顯示用戶輸入*/DrawText(inputElement[currentElementInputPlace].xPos,inputElement[currentElementInputPlace].yPos,"%1d",num);/*將輸入賦值給數組元素*/inputElement[currentElementInputPlace].byNum=num;/*焦點及光標右移*/moveToRight();}}將數字每一位輸入的坐標和輸入值捆綁后,在數字鍵處理函數中就可以較有結構的組織程序使程序顯得很緊湊。整理用戶輸入繼續(xù)第2節(jié)的例子,在第2節(jié)的onNumKey函數中,只是獲取了數字的每一位,因而我們需要將其轉化為有效數據,譬如要轉化為有效的XXX數據,其方法是:/*從2進制數據位轉化為有效數據:XXX*/voidconvertToXXX(){BYTEi;XXX=0;for(i=0;i<NUM_LENGTH;i++){XXX+=inputElement[i].byNum*power(2,NUM_LENGTH-i-1);}}反之,我們也可能需要在屏幕上顯示那些有效的數據位,因為我們也需要能夠反向轉化:/*從有效數據轉化為2進制數據位:XXX*/voidconvertFromXXX(){BYTEi;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語言嵌入式系統(tǒng)編程修煉之道一一,性能優(yōu)化篇使用宏定義在C語言中,宏是產生內嵌代碼的唯一方法。對于嵌入式系統(tǒng)而言,為了能達到性能要求,宏是一種很好的代替函數的方法。寫一個“標準”宏MIN,這個宏輸入兩個參數并返回較小的一個:錯誤做法:#defineMIN(A,B)(A<=B?A:B)正確做法:#defineMIN(A,B)((A)<=(B)?(A):(B))對于宏,我們需要知道三點:宏定義“像”函數;宏定義不是函數,因而需要括上所有“參數”;宏定義可能產生副作用。下面的代碼:least=MIN(*p++,b);將被替換為:((*p++)<=(b)?(*p++):(b))發(fā)生的事情無法預料。因而不要給宏定義傳入有副作用的“參數”。使用寄存器變量當對一個變量頻繁被讀寫時,需要反復訪問內存,從而花費大量的存取時間。為此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使用時,不需要訪問內存,而直接從寄存器中讀寫,從而提咼效率。寄存器變量的說明符是register。對于循環(huán)次數較多的循環(huán)控制變量及循環(huán)體內反復使用的變量均可定義為寄存器變量,而循環(huán)計數是應用

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論