第一篇:嵌入式C語言編程心得
一、.H文件與.C文件的關系:
迄今為止,寫過的程序都是一些很簡單的程序,從來沒有想到要自己寫.H文件,也不知道.H文件到底什么用,與.C文件什么關系。只是最近寫鍵盤程序,參考別人的程序時,發現別人寫的嚴格的程序都帶有一個“KEY.H”,里面定義了.C文件里用到的自己寫的函數,如Keyhit()、Keyscan()等。
經過查找資料得知,.H文件就是頭文件,估計就是Head的意思吧,這是規范程序結構化設計的需要,既可以實現大型程序的模塊化,又可以實現根各模塊的連接調試。
1、.H文件介紹:
在單片機C程序設計中,項目一般按功能模塊化進行結構化設計。將一個項目劃分為多個功能,每個功能的相關程序放在一個C程序文檔中,稱之為一個模塊,對應的文件名即為模塊名。一個模塊通常由兩個文檔組成,一個為頭文件*.h,對模塊中的數據結構和函數原型進行描述;另一個則為C文件*.c,對數據實例或對象定義,以及函數算法具體實現。
2、.H文件的作用
作為項目設計,除了對項目總體功能進行詳細描述外,就是對每個模塊進行詳細定義,也就是給出所有模塊的頭文件。通常H頭文件要定義模塊中各函數的功能,以及輸入和輸出參數的要求。模塊的具體實現,由項目組成根據H文件進行設計、編程、調試完成。為了保密和安全,模塊實現后以可連接文件OBJ、或庫文件LIB的方式提供給項目其他成員使用。由于不用提供源程序文檔,一方面可以公開發行,保證開發人員的所有權;另一方面可以防止別人有意或無意修改產生非一致性,造成版本混亂。所以H頭文件是項目的詳細設計和團隊工作劃分的依據,也是對模塊進行測試的功能說明。要引用模塊內的數據或算法,只要用包含include指定模塊H頭文件即可。
3、.H文件的基本組成 /*如下為鍵盤驅動的頭文檔*/ #ifndef _KEY_H_ //防重復引用,如果沒有定義過_KEY_H_,則編譯下句 #define _KEY_H_ //此符號唯一,表示只要引用過一次,即#i nclude,則定義符號_KEY_H_ ///////////////////////////////////////////////////////////////// char keyhit(void);//擊鍵否
unsigned char Keyscan(void);//取鍵值
///////////////////////////////////////////////////////////////// #endif.c文件是整個程序中的一個或幾個函數組成,在別的.C文件里可以調用它,不只是在主函數中。這樣做可以增強程序的模塊化,提高程序的可讀性。當編制好一個模塊時你可以保存在一個工程下,文件名改為**.C。這樣在另一個.C文件中的文件頭處只要你對它進行說明就可以調用它。比如#include string.c 那么你就可以在你說明了的.C文件中調用它,這樣一個模塊可以在很多處調用,使得編程復雜度降低。程序編譯時把你所說明的文件復制到你調用處程序就可以運行了。
.H文件是對單片機一些端口及一些常用的程序的庫說明,比如我們在程序中用到端口一即P1,我們能夠用它是因為在庫中已對它進行了說明,對P1賦予了一個端口地址,這樣程序編譯是才不會出錯,不然會提示你UNDIFINED。
分開多個文件寫就有很多的.c和.h文件了,當然要加上#include語句把其它的包含進來,分開多個文件寫有利于管理,其實我覺得.c和.h區別不是很大,只是.h一般都是寫定義、聲明的東西,.c文件一般都寫函數的具體實現
h文件就是頭文件,一般進行聲明、宏定義等。比如要編寫流水燈的程序,h文件這樣寫: #ifndef _LED_H #define _LED_H define LED_ON P1.1=1;define LED_OFF P1.1=0;//還可以進行函數的聲明 //void LED_twikle(void);#endif
在你的c文件中要加入#include“LED.h”
二、盡量使用宏定義#define
開始看別人的程序時,發現程序開頭,在文件包含后面有很多#define語句,當時就想,搞這么多標示符替換來替換去的,麻不麻煩啊,完全沒有理解這種寫法的好處。原來,用一個標示符表示常數,有利于以后的修改和維護,修改時只要在程序開頭改一下,程序中所有用到的地方就全部修改,節省時間。
#define KEYNUM 65//按鍵數量,用于Keycode[KEYNUM] #define LINENUM 8//鍵盤行數 #define ROWNUM 8//鍵盤列數 注意的地方:
1、宏名一般用大寫
2、宏定義不是C語句,結尾不加分號
三、不要亂定義變量類型
以前寫程序,當需要一個新的變量時,不管函數內還是函數外的,直接在程序開頭定義,雖然不是原則上的錯誤,但是很不可取的作法。
下面說一下,C語言中變量類型的有關概念: 從變量的作用范圍來分,分為局部變量和全局變量:
1、全局變量:是在函數外定義的變量,像我以前定義在程序開頭的變量都是全局變量,這里我就犯了一個大忌,使用了過多的全局變量。
帶來的問題有兩個:一是,全局變量在程序全部執行過程中都占用資源;二是,全局變量過多使程序的通用性變差,因為全局變量是模塊間耦合的原因之一。
2、局部變量:在函數內部定義的變量,只在函數內部有效。從變量的變量值存在的時間分為兩種:
1、靜態存儲變量:程序運行期間分配固定的存儲空間。
2、動態存儲變量:程序運行期間根據需要動態地分配存儲空間。具體又包括四種存儲方式:auto, static ,register, extern
1、局部變量,不加說明默認為auto型,即動態存儲,如果不賦初值,將是一個不確定的值。而將局部變量定義為static型的話,則它的值在函數內是不變的,且初值默認為0。
static unsigned char sts;//按鍵狀態變量 static unsigned char Nowkeycode;//此時的鍵碼 static unsigned char Prekeycode;//上一次的鍵碼
static unsigned char Keydowntime;//矩形鍵盤按下去抖時間變量 static unsigned char Keyuptime;//矩形鍵盤釋放去抖時間變量 static unsigned char Onoffdowntime;//關機鍵按下去抖時間變量 static unsigned char Onoffuptime;//關機鍵釋放去抖時間變量
static unsigned char onoff_10ms;//判斷關機鍵中斷次數變量,累計150次大約為3S,因為前后進了兩個10ms中斷
2、全局變量,編譯時分配為靜態存儲區,可以被本文件中的各個函數引用。如果是多個文件的話,如果在一個文件中引用另外文件中的變量,在此文件中要用extern聲明。不過如果一個全局變量定義為static的話,就只能在此一個文件中使用。
static 函數內部函數和外部函數
當一個源程序由多個源文件組成時,C語言根據函數能否被其它源文件中的函數調用,將函數分為內部函數和外部函數。
內部函數(又稱靜態函數)
如果在一個源文件中定義的函數,只能被本文件中的函數調用,而不能被同一程序其它文件中的函數調用,這種函數稱為內部函數。
定義一個內部函數,只需在函數類型前再加一個“static”關鍵字即可,如下所示:
static 函數類型 函數名(函數參數表){??}
關鍵字“static”,譯成中文就是“靜態的”,所以內部函數又稱靜態函數。但此處“static”的含義不是指存儲方式,而是指對函數的作用域僅局限于本文件。
使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數,是否會與其它文件中的函數同名,因為同名也沒有關系。外部函數
外部函數的定義:在定義函數時,如果沒有加關鍵字“static”,或冠以關鍵字“extern”,表示此函數是外部函數:
[extern] 函數類型 函數名(函數參數表){??} 調用外部函數時,需要對其進行說明:
[extern] 函數類型 函數名(參數類型表)[,函數名2(參數類型表2)??];
四、特殊關鍵字const volatile的使用
1、const const用于聲明一個只讀的變量
const unsigned char a=1;//定義a=1,編譯器不允許修改a的值 作用:保護不希望被修改的參數 const unsigned char Key_code[KEYNUM]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, 0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10, 0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, 0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20, 0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, 0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30, 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38, 0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,0x40, 0x41 };//鍵碼
const unsigned char Line_out[LINENUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//行輸出編碼 const unsigned char Row_in[ROWNUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//列輸入編碼
2、volatile 一個定義為volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。
static int i=0;int main(void){...while(1){ if(i)dosomething();} } /* Interrupt service routine.*/ void ISR_2(void){ i=1;} 程序的本意是希望ISR_2中斷產生時,在main當中調用dosomething函數,但是,由于編譯器判斷在main函數里面沒有修改過i,因此可能只執行一次對從i到某寄存器的讀操作,然后每次if判斷都只使用這個寄存器里面的“i副本”,導致dosomething永遠也不會被調用。如果將將變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
五、C語言中extern的用法
extern可以置于變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。
另外,extern也可用來進行鏈接指定。
extern 變量
在一個源文件里定義了一個數組:char a[6];在另外一個文件里用下列語句進行了聲明:extern char *a; 請問,這樣可以嗎? 答案與分析: 1)、不可以,程序運行時會告訴你非法訪問。原因在于,指向類型T的指針并不等價于類型T的數組。extern char *a聲明的是一個指針變量而不是字符數組,因此與實際的定義不同,從而造成運行時非法訪問。應該將聲明改為extern char a[ ]。2)、例子分析如下,如果a[] = “abcd”,則外部變量a=0x12345678(數組的起始地址),而*a是重新定義了一個指針變量a的地址可能是0x87654321,直接使用*a是錯誤的.3)、這提示我們,在使用extern時候要嚴格對應聲明時的格式,在實際編程中,這樣的錯誤屢見不鮮。4)、extern用在變量聲明中常常有這樣一個作用,你在*.c文件中聲明了一個全局的變量,這個全局的變量如果要被引用,就放在*.h中并用extern來聲明。
函數
extern 函數1 常常見extern放在函數的前面成為函數聲明的一部分,那么,C語言的關鍵字extern在函數的聲明中起什么作用? 答案與分析: 如果函數的聲明中帶有關鍵字extern,僅僅是暗示這個函數可能在別的源文件里定義,沒有其它作用。即下述兩個函數聲明沒有明顯的區別: extern int f();和int f();當然,這樣的用處還是有的,就是在程序中取代include “*.h”來聲明函數,在一些復雜的項目中,我比較習慣在所有的函數聲明前添加extern修飾。extern 函數2 當函數提供方單方面修改函數原型時,如果使用方不知情繼續沿用原來的extern申明,這樣編譯時編譯器不會報錯。但是在運行過程中,因為少了或者多了輸入參數,往往會照成系統錯誤,這種情況應該如何解決? 答案與分析: 目前業界針對這種情況的處理沒有一個很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供對外部接口的聲明,然后調用方include該頭文件,從而省去extern這一步。以避免這種錯誤。寶劍有雙鋒,對extern的應用,不同的場合應該選擇不同的做法。extern “C” 在C++環境下使用C函數的時候,常常會出現編譯器無法找到obj模塊中的C函數定義,從而導致鏈接失敗的情況,應該如何解決這種情況呢? 答案與分析: C++語言在編譯的時候為了解決函數的多態問題,會將函數名和參數聯合起來生成一個中間的函數名稱,而C語言則不會,因此會造成鏈接時找不到對應函數的情況,此時C函數就需要用extern “C”進行鏈接指定,這告訴編譯器,請保持我的名稱,不要給我生成用于鏈接的中間函數名。下面是一個標準的寫法: //在.h文件的頭上 #ifdef __cplusplus #if __cplusplus extern “C”{ #endif #endif /* __cplusplus */ ? ? //.h文件結束的地方 #ifdef __cplusplus #if __cplusplus } #endif #endif /* __cplusplus */
六、c++中-> 是什么意思,如何使用
->是指針的指向運算符,通常與結構體一起使用。
具體使用方法可以參考如下程序: #include
七、warning: last line of file ends without a newline 怎么解決?
原因:編譯器要求有空行作為程序的結束
解決辦法:將光標移到提示告警的代碼最后一行(有代碼的那一行)然后按住del鍵,直到確定下面沒有回車行,最后回車一下或多下即可。
八、__I、__O、__IO volatile是什么?怎么用?
__I、__O、__IO是什么意思?
這是ST庫里面的宏定義,定義如下:
#define __I volatile const /*!< defines 'read only' permissions */ #define __O volatile /*!< defines 'write only' permissions */ #define __IO volatile /*!< defines 'read / write' permissions */
顯然,這三個宏定義都是用來替換成 volatile 和 const 的,所以我們先要了解 這兩個關鍵字的作用:
volatile
簡單的說,就是不讓編譯器進行優化,即每次讀取或者修改值的時候,都必須重新從內存或者寄存器中讀取或者修改。
一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
我認為這是區分C程序員和嵌入式系統程序員的最基本的問題。搞嵌入式的家伙們經常同硬件、中斷、RTOS等等打交道,所有這些都要求用到 volatile變量。不懂得volatile的內容將會帶來災難。假設被面試者正確地回答了這是問題(嗯,懷疑是否會是這樣),我將稍微深究一下,看一下這家伙是不是直正懂得volatile完全的重要性。
1)一個參數既可以是const還可以是volatile嗎?解釋為什么。
2);一個指針可以是volatile 嗎?解釋為什么。
3);下面的函數有什么錯誤:
int square(volatile int *ptr){
return *ptr * *ptr;}
1)是的。一個例子是只讀的狀態寄存器。它是volatile因為它可能被意想不到地改變。它是const因為程序不應該試圖去修改它。
2);是的。盡管這并不很常見。一個例子是當一個中服務子程序修該一個指向一個buffer的指針時。
3)這段代碼有點變態。這段代碼的目的是用來返指針*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;}
const 只讀變量,即變量保存在只讀靜態存儲區。編譯時,如何嘗試修改只讀變量,則編譯器提示出錯,就能防止誤修改。
const與define
兩者都可以用來定義常量,但是const定義時,定義了常量的類型,所以更精確一些(其實const定義的是只讀變量,而不是常量)。#define只是簡單的文本替換,除了可以定義常量外,還可以用來定義一些簡單的函數,有點類似內置函數。const和define定義的常量可以放在頭文件里面。(小注:可以多次聲明,但只能定義一次)
const與指針
int me;
const int * p1=&me;//p1可變,*p1不可變 const 修飾的是 *p1,即*p1不可變
int * const p2=&me;//p2不可變,*p2可變 const 修飾的是 p2,即p2不可變
const int *const p3=&me;//p3不可變,*p3也不可變 前一個const 修飾的是 *p3,后一個const 修飾的是p3,兩者都不可變
前面介紹了 volatile 和 const 的用法,不知道大家了解了沒?了解了后,下面的講解就更加容易了:
__I :輸入口。既然是輸入,那么寄存器的值就隨時會外部修改,那就不能進行優化,每次都要重新從寄存器中讀取。也不能寫,即只讀,不然就不是輸入而是輸出了。
__O :輸出口,也不能進行優化,不然你連續兩次輸出相同值,編譯器認為沒改變,就忽略了后面那一次輸出,假如外部在兩次輸出中間修改了值,那就影響輸出
__IO:輸入輸出口,同上
為什么加下劃線?
原因是:避免命名沖突
一般宏定義都是大寫,但因為這里的字母比較少,所以再添加下劃線來區分。這樣一般都可以避免命名沖突問題,因為很少人這樣命名,這樣命名的人肯定知道這些是有什么用的。
經常寫大工程時,都會發現老是命名沖突,要不是全局變量沖突,要不就是宏定義沖突,所以我們要盡量避免這些問題,不然出問題了都不知道問題在哪里。
九、C語言中關于枚舉類型
1.enum 枚舉的定義
枚舉類型定義的一般形式為: enum 枚舉名{ 枚舉值表 };在枚舉值表中應羅列出所有可用值。這些值也稱為枚舉元素。例如:
該枚舉名為weekday,枚舉值共有7個,即一周中的七天。凡被說明為weekday類型變量的取值只能是七天中的某一天。
2.枚舉變量的說明
如同結構和聯合一樣,枚舉變量也可用不同的方式說明,即先定義后說明,同時定義說明或直接說明。
設有變量a,b,c被說明為上述的weekday,可采用下述任一種方式: enum weekday{ sun,mou,tue,wed,thu,fri,sat };enum weekday a,b,c;或者為:
enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c;或者為:
enum { sun,mou,tue,wed,thu,fri,sat }a,b,c;
3、枚舉類型變量的賦值和使用 枚舉類型在使用中有以下規定:
a.枚舉值是常量,不是變量。不能在程序中用賦值語句再對它賦值。例如對枚舉weekday的元素再作以下賦值: sun=5;mon=2;sun=mon;都是錯誤的。
b.枚舉元素本身由系統定義了一個表示序號的數值,從0開始順序定義為0,1,2…。如在weekday中,sun值為0,mon值為1,…,sat值為6。int main(){ enum weekday { sun,mon,tue,wed,thu,fri,sat } a,b,c;a=sun;b=mon;c=tue;printf(“%d,%d,%d”,a,b,c);return 0;} 說明:
只能把枚舉值賦予枚舉變量,不能把元素的數值直接賦予枚舉變量。如: a=sum;b=mon;是正確的。而: a=0;b=1;是錯誤的。如一定要把數值賦予枚舉變量,則必須用強制類型轉換。如:a=(enum weekday)2;其意義是將順序號為2的枚舉元素賦予枚舉變量a,相當于: a=tue;還應該說明的是枚舉元素不是字符常量也不是字符串常量,使用時不要加單、雙引號。int main(){ enum body { a,b,c,d } month[31],j;int i;j=a;for(i=1;i<=30;i++){ month[i]=j;j++;if(j>d)j=a;} for(i=1;i<=30;i++){ switch(month[i]){ case a:printf(“ %2d %ct”,i,'a');break;case b:printf(“ %2d %ct”,i,'b');break;case c:printf(“ %2d %ct”,i,'c');break;case d:printf(“ %2d %ct”,i,'d');break;default:break;} } printf(“n”);return 0;}
第二篇:嵌入式系統c語言編程該怎么學?
雪中悍刀行 http://bmdqw.com/
嵌入式系統c語言編程該怎么學?
C語言博大精深,玩了很長時間了,一直徘徊在入門處。看了很多別人的編程經驗,加上項目程序越做越大,直到這半年來突然有很多體會,明天就要回家了,下午閑來無事也試著總結一些心得體會,喜歡對師弟妹們的學習有所幫助。
首先要說說編程的幾個重要原則,看了很多別人的編程經驗,更多的是說技巧。技巧能顯著提高程序的效率,固然重要但是技巧的掌握靠了還是大量的工程實踐,只有在有一定功底后才可以去追求這些編程技巧。但是編程的原則卻是要在學習一開始就要認真貫徹,才能養成良好的編程習慣,苦練內功后練上層功夫才不會走火入魔。
言歸正傳,嵌入式系統C語言編程需要遵守什么樣的原則呢?隨著時代和技術的不斷發展,這個問題也許仁者見仁智者見智了,但是總結起來大家還是有很多共識。根據目前提倡的軟件工程的做法,和我們教研室的做法,列舉最重要原則:
一、模塊劃分.C語言作為一種結構化的程序設計語言,在模塊的劃分上主要依據功能:
(1)一個功能模塊即是一個.c文件和一個.h文件的結合,.h文件中是對于該模塊功能函數和使變 量的聲明
(2)該模塊提供給其它模塊調用的外部函數及數據都需要在.h中文件中以extern關鍵字聲明
(3)模塊內的函數和全局變量只能在.c文件定義
(4)不允許在.h文件中定義變量(定義變量和聲明變量的區別在于定義會產生內存分配的操作,而聲明則只是告訴包含該聲明的模塊在連接階段從其它模塊尋找外部函數和變量)。
二、一個嵌入式系統程序包括兩類模塊分三層編寫:
(1)硬件驅動模塊,一種特定硬件對應一個模塊,包括了片內集成的硬件模塊和外部擴展的(2)軟件功能模塊,軟件功能模塊是建立在硬件驅動模塊上的與硬件無關的邏輯功能。
(3)三層編寫即HAL(硬件應用層),API(應用函數包),APP(邏輯應用層)注:名稱是借用的,表大個意思而已。HAL就是硬件驅動模塊和系統硬件密切相關,API可以是建立在HAL上的硬件應用服務程序也可以是通用的函數模塊,APP則是最終構成嵌入式系統應用的功能邏輯關系。HAL和API是為了方便技術積累和提高開發效率而分開了,APP則是針對特殊應用而定制的。
三、中斷服務程序的要求:
(1)不能返回值
(2)不能向ISR傳遞參數
(3)ISR應該盡可能的短小精悍,不允許有等待信號的操作
四、編程風格問題
五、需要學會熟練應用的C語言的基本手法
(1)數據指針,不能僅僅只會使用數組
(2)宏定義,定義寄存器地址,定義宏函數等
(3)函數指針的應用
(4)條件編譯,在帶操作系統的應用時經常要用
六、不要偷懶,寫好必要的注釋
七、做到以上幾點時就可以吸收高超的編程技巧了。轉載請保留連接
本文由www.tmdps.cn整理
第三篇:C語言嵌入式系統編程修煉之道
C語言嵌入式系統編程修煉之道收藏
C語言嵌入式系統編程修煉之道——背景篇...1 C語言嵌入式系統編程修煉之道——軟件架構篇...4 1.模塊劃分...4 2.多任務還是單任務...5 3.單任務程序典型架構...6 4.中斷服務程序...7 5.硬件驅動模塊...9 6.C的面向對象化...10 總結...10 C語言嵌入式系統編程修煉之道——內存操作篇...12 1.數據指針...12 2.函數指針...13 3.數組vs.動態申請...14 4.關鍵字const 15 5.關鍵字volatile.16 6.CPU字長與存儲器位寬不一致處理...17 總結...18 C語言嵌入式系統編程修煉之道——屏幕操作篇...19 1.漢字處理...19 2.系統時間顯示...20 3.動畫顯示...21 4.菜單操作...22 5.模擬MessageBox函數...24 總結...26 C語言嵌入式系統編程修煉之道——鍵盤操作篇...27 1.處理功能鍵...27 2.處理數字鍵...28 3.整理用戶輸入...29 總結...30 C語言嵌入式系統編程修煉之道——性能優化篇...31 1.使用宏定義...31 2.使用寄存器變量...31 3.內嵌匯編...32 4.利用硬件特性...32 5.活用位操作...33 總結
C語言嵌入式系統編程修煉之道——背景篇 不同于一般形式的軟件編程,嵌入式系統編程建立在特定的硬件平臺上,勢必要求其編程語言具備較強的硬件直接操作能力。無疑,匯編語言具備這樣的特質。但是,歸因于匯編語言開發過程的復雜性,它并不是嵌入式系統開發的一般選擇。而與之相比,C語言——一種“高級的低級”語言,則成為嵌入式系統開發的最佳選擇。筆者在嵌入式系統項目的開發過程中,一次又一次感受到C語言的精妙,沉醉于C語言給嵌入式開發帶來的便利。本文的目的在于進行“C語言嵌入式系統開發的內功心法”秀,一共包括25招。
圖1給出了本文的討論所基于的硬件平臺,實際上,這也是大多數嵌入式系統的硬件平臺。它包括兩部分:
(1)
以通用處理器為中心的協議處理模塊,用于網絡控制協議的處理;(2)
以數字信號處理器(DSP)為中心的信號處理模塊,用于調制、解調和數/模信號轉換。
本文的討論主要圍繞以通用處理器為中心的協議處理模塊進行,因為它更多地牽涉到具體的C語言編程技巧。而DSP編程則重點關注具體的數字信號處理算法,主要涉及通信領域的知識,不是本文的討論重點。
著眼于討論普遍的嵌入式系統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處理器的目標代碼并通過PC機的COM口(RS-232串口)或以太網口下載到目標機上運行,如圖2。其駐留于目標機FLASH存儲器中的monitor程序可以監控宿主機Windows調試平臺上的用戶調試指令,獲取CPU寄存器的值及目標機存儲空間、I/O空間的內容。圖2 交叉開發環境
后續章節將從軟件架構、內存操作、屏幕操作、鍵盤操作、性能優化等多方面闡述C語言嵌入式系統的編程技巧。軟件架構是一個宏觀概念,與具體硬件的聯系不大;內存操作主要涉及系統中的FLASH、RAM和NVRAM芯片;屏幕操作則涉及顯示控制器和實時鐘;鍵盤操作主要涉及鍵盤控制器;性能優化則給出一些具體的減小程序時間、空間消耗的技巧。
本文即將講述的25個主題可分為兩類,一類是編程技巧,有很強的適用性;一類則介紹嵌入式系統編程的一般常識,具有一定的理論意義。So, let’s go.C語言嵌入式系統編程修煉之道——軟件架構篇 1.模塊劃分
模塊劃分的“劃”是規劃的意思,意指怎樣合理的將一個很大的軟件劃分為一系列功能獨立的部分合作完成系統的需求。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文件 */ /*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)軟件功能模塊,其模塊的劃分應滿足低偶合、高內聚的要求。2.多任務還是單任務
所謂“單任務系統”是指該系統不能支持多任務并發操作,宏觀串行地執行一個任務。而多任務系統則可以宏觀并行(微觀上可能串行)地“同時”執行多個任務。
多任務的并發執行通常依賴于一個多任務操作系統(OS),多任務OS的核心是系統調度器,它使用任務控制塊(TCB)來管理任務調度功能。TCB包括任務的當前狀態、優先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。調度器在任務被激活時,要用到這些信息。此外,TCB還被用來存放任務的“上下文”(context)。任務的上下文就是當一個執行中的任務被停止時,所要保存的所有信息。通常,上下文就是計算機當前的狀態,也即各個寄存器的內容。當發生任務切換時,當前運行的任務的上下文被存入TCB,并將要被執行的任務的上下文從它的TCB中取出,放入各個寄存器中。嵌入式多任務OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遙不可及的神壇之物,我們可以用不到1000行代碼實現一個針對80186處理器的功能最簡單的OS內核,作者正準備進行此項工作,希望能將心得貢獻給大家。
究竟選擇多任務還是單任務方式,依賴于軟件的體系是否龐大。例如,絕大多數手機程序都是多任務的,但也有一些小靈通的協議棧是單任務的,沒有操作系統,它們的主程序輪流調用各個軟件模塊的處理程序,模擬多任務環境。3.單任務程序典型架構
(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,但是你得不到鮮花和掌聲。實際上,這是一個沒有太大意義的牛角尖,因為這個世界從來不需要一個處理完幾個消息就喊著要OS殺死它的WIN32程序,不需要一個剛開始RUN就自行了斷的嵌入式系統,不需要莫名其妙啟動一個做一點事就干掉自己的線程。有時候,過于嚴謹制造的不是便利而是麻煩。君不見,五層的TCP/IP協議棧超越嚴謹的ISO/OSI七層協議棧大行其道成為事實上的標準? 經常有網友討論:
printf(“%d,%d”,++i,i++);
/* 輸出是什么?*/ c = a+++b;
/* c=? */ 等類似問題。面對這些問題,我們只能發出由衷的感慨:世界上還有很多有意義的事情等著我們去消化攝入的食物。實際上,嵌入式系統要運行到世界末日。4.中斷服務程序
中斷是嵌入式系統中重要的組成部分,但是在標準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(){
int intType;
intType = GetSystemType();QueueAddTail(lpIntQueueHead, intType);/* 在隊列尾加入新的中斷 */ } 在主程序循環中判斷是否有中斷: While(1){ If(!IsIntQueueEmpty())
{
intType = GetFirstInt();
switch(intType)
/* 是不是很象WIN32程序的消息解析函數? */
{
/* 對,我們的中斷類型解析很類似于消息驅動 */
case xxx:
/* 我們稱其為“中斷驅動”吧? */
…
break;
case xxx:
…
break;
…
} }
} 按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執行了。5.硬件驅動模塊
一個硬件驅動模塊通常應包括如下函數:(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中斷服務程序 */ /* 相對于中斷向量表首地址的偏移 */
*m_myPtr = &UART _Isr;
/* UART _Isr:UART的中斷服務程序 */(3)設置CPU針對該硬件的控制線
a.如果控制線可作PIO(可編程I/O)和控制信號用,則設置CPU內部對應寄存器使其作為控制信號;
b.設置CPU內部的針對該設備的中斷屏蔽位,設置中斷方式(電平觸發還是邊緣觸發)。
(4)提供一系列針對該設備的操作接口函數。例如,對于LCD,其驅動模塊應提供繪制像素、畫線、繪制矩陣、顯示字符點陣等函數;而對于實時鐘,其驅動模塊則需提供獲取時間、設置時間等函數。6.C的面向對象化
在面向對象的語言里面,出現了類的概念。類是對特定數據的特定操作的集合體。類包含了兩個范疇:數據和操作。而C語言中的struct僅僅是數據的集合,我們可以利用函數指針將struct模擬為一個包含數據和操作的“類”。下面的C程序模擬了一個最簡單的“類”: #ifndef C_Class
#define C_Class struct #endif C_Class A {
C_Class A *A_this;
/* this指針 */
void(*Foo)(C_Class A *A_this);/* 行為:函數指針 */
int a;
/* 數據 */
int b;};我們可以利用C語言模擬出面向對象的三個特性:封裝、繼承和多態,但是更多的時候,我們只是需要將數據與行為封裝以解決軟件結構混亂的問題。C模擬面向對象思想的目的不在于模擬行為本身,而在于解決某些情況下使用C語言編程時程序整體框架結構分散、數據和函數脫節的問題。我們在后續章節會看到這樣的例子。總結
本篇介紹了嵌入式系統編程軟件架構方面的知識,主要包括模塊劃分、多任務還是單任務選取、單任務程序典型架構、中斷服務程序、硬件驅動模塊設計等,從宏觀上給出了一個嵌入式系統軟件所包含的主要元素。
請記?。很浖Y構是軟件的靈魂!結構混亂的程序面目可憎,調試、測試、維護、升級都極度困難。
一個高尚的程序員應該是寫出如藝術作品般程序的程序員。
C語言嵌入式系統編程修煉之道——內存操作篇 1.數據指針
在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的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)。
記?。篊PU以字節為單位編址,而C語言指針以指向的數據類型長度作自增和自減。理解這一點對于以指針直接操作內存是相當重要的。2.函數指針
首先要理解以下三個問題:
(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啟動后第一條要執行的指令的位置。
記?。汉瘮禑o它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!3.數組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應成對出現!4.關鍵字const const意味著“只讀”。區別如下代碼的功能非常重要,也是老生長嘆,如果你還不知道它們的區別,而且已經在程序界摸爬滾打多年,那只能說這是一個悲哀: const int a;int const a;const int *a;int * const a;int const * a const;(1)關鍵字const的作用是為給讀你代碼的人傳達非常有用的信息。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬于“輸入參數”。在有多個形參的時候,函數的調用者可以憑借參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。
(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:“只能讀的普通變量”,可以稱其為“不能改變的變量”(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在C語言中如下程序是非法的: const int SIZE = 10;char a[SIZE];/* 非法:編譯階段不能用到變量 */ 5.關鍵字volatile C語言編譯器會對用戶書寫的代碼進行優化,譬如如下代碼: int a,b,c;a = inWord(0x100);/*讀取I/O空間0x100端口的內容存入a變量*/ b = a;a = inWord(0x100);/*再次讀取I/O空間0x100端口的內容存入a變量*/ c = a;很可能被編譯器優化為: int a,b,c;a = inWord(0x100);/*讀取I/O空間0x100端口的內容存入a變量*/ b = a;c = a;但是這樣的優化結果可能導致錯誤,如果I/O空間0x100端口的內容在執行第一次讀操作后被其它程序寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變量a的定義前加上volatile關鍵字可以防止編譯器的類似優化,正確的做法是: volatile int a;
volatile變量可能用于如下幾種情況:
(1)并行設備的硬件寄存器(如:狀態寄存器,例中的代碼屬于此類);(2)一個中斷服務子程序中會訪問到的非自動變量(也就是全局變量);(3)多線程應用中被幾個任務共享的變量。6.CPU字長與存儲器位寬不一致處理
在背景篇中提到,本文特意選擇了一個與CPU字長不一致的存儲芯片,就是為了進行本節的討論,解決CPU字長與存儲器位寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫字節、字的接口,如下: typedef unsigned char BYTE;typedef unsigned int WORD;
/* 函數功能:讀NVRAM中字節
* 參數:wOffset,讀取位置相對NVRAM基地址的偏移
* 返回:讀取到的字節值 */ extern BYTE ReadByteNVRAM(WORD wOffset){
LPBYTE lpAddr =(BYTE*)(NVRAM + wOffset * 2);/* 為什么偏移要×2? */
return *lpAddr;}
/* 函數功能:讀NVRAM中字
* 參數:wOffset,讀取位置相對NVRAM基地址的偏移
* 返回:讀取到的字 */ extern WORD ReadWordNVRAM(WORD wOffset){
WORD wTmp = 0;
LPBYTE lpAddr;
/* 讀取高位字節 */
lpAddr =(BYTE*)(NVRAM + wOffset * 2);
/* 為什么偏移要×2? */
wTmp +=(*lpAddr)*256;
/* 讀取低位字節 */
lpAddr =(BYTE*)(NVRAM +(wOffset +1)* 2);
/* 為什么偏移要×2? */
wTmp += *lpAddr;
return wTmp;}
/* 函數功能:向NVRAM中寫一個字節
*參數:wOffset,寫入位置相對NVRAM基地址的偏移 *
byData,欲寫入的字節 */ extern void WriteByteNVRAM(WORD wOffset, BYTE byData){
… }
/* 函數功能:向NVRAM中寫一個字 */ *參數:wOffset,寫入位置相對NVRAM基地址的偏移 *
wData,欲寫入的字 */ extern void WriteWordNVRAM(WORD wOffset, WORD wData){
… } 子貢問曰:Why偏移要乘以2? 子曰:請看圖1,16位80186與8位NVRAM之間互連只能以地址線A1對其A0,CPU本身的A0與NVRAM不連接。因此,NVRAM的地址只能是偶數地址,故每次以2為單位前進!
圖1 CPU與NVRAM地址線連接
子貢再問:So why 80186的地址線A0不與NVRAM的A0連接? 子曰:請看《IT論語》之《微機原理篇》,那里面講述了關于計算機組成的圣人之道。總結
本篇主要講述了嵌入式系統C編程中內存操作的相關技巧。掌握并深入理解關于數據指針、函數指針、動態申請內存、const及volatile關鍵字等的相關知識,是一個優秀的C語言程序設計師的基本要求。當我們已經牢固掌握了上述技巧后,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在內存操作中體現。
我們之所以在嵌入式系統中使用C語言進行程序設計,99%是因為其強大的內存操作能力!
如果你愛編程,請你愛C語言; 如果你愛C語言,請你愛指針; 如果你愛指針,請你愛指針的指針!
C語言嵌入式系統編程修煉之道——屏幕操作篇 1.漢字處理
現在要解決的問題是,嵌入式系統中經常要使用的并非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的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}
} } 要顯示特定漢字的時候,只需要從數組中查找內碼與要求漢字內碼相同的即可獲得字模。如果前面的漢字在數組中以內碼大小順序排列,那么可以以二分查找法更高效的查找到漢字的字模。
這是一種很有效的組織小漢字庫的方法,它可以保證程序有很好的結構。2.系統時間顯示
從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具有了更加強大的威力,它使得某些數據和函數脫離“對象”而成為“類”的一部分,正是它的這一特點,成就了軟件的無數優秀設計。3.動畫顯示
動畫是無所謂有,無所謂無的,靜止的畫面走的路多了,也就成了動畫。隨著時間的變更,在屏幕上顯示不同的靜止畫面,即是動畫之本質。所以,在一個嵌入式系統的LCD上欲顯示動畫,必須借助定時器。沒有硬件或軟件定時器的世界是無法想像的:
(1)
沒有定時器,一個操作系統將無法進行時間片的輪轉,于是無法進行多任務的調度,于是便不再成其為一個多任務操作系統;
(2)
沒有定時器,一個多媒體播放軟件將無法運作,因為它不知道何時應該切換到下一幀畫面;
(3)
沒有定時器,一個網絡協議將無法運轉,因為其無法獲知何時包傳輸超時并重傳之,無法在特定的時間完成特定的任務。
因此,沒有定時器將意味著沒有操作系統、沒有網絡、沒有多媒體,這將是怎樣的黑暗?所以,合理并靈活地使用各種定時器,是對一個軟件人的最基本需求!在80186為主芯片的嵌入式系統中,我們需要借助硬件定時器的中斷來作為軟件定時器,在中斷發生后變更畫面的顯示內容。在時間顯示“xx:xx”中讓冒號交替有無,每次秒中斷發生后,需調用ShowDot: void ShowDot(){ static BOOL bShowDot = TRUE;
/* 再一次領略static關鍵字的威力 */ if(bShowDot)
{ showChar(‘:’,xPos,yPos);} else
{ showChar(‘ ’,xPos,yPos);
} bShowDot =!bShowDot;} 4.菜單操作
無數人為之絞盡腦汁的問題終于出現了,在這一節里,我們將看到,在C語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!筆者曾經是個笨蛋,被菜單搞暈了,給出這樣的一個系統: 圖1 菜單范例
要求以鍵盤上的“←→”鍵切換菜單焦點,當用戶在焦點處于某菜單時,若敲擊鍵盤上的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();
} 程序被大大簡化了,也開始具有很好的可擴展性!我們僅僅利用了面向對象中的封裝思想,就讓程序結構清晰,其結果是幾乎可以在無需修改程序的情況下在系統中添加更多的菜單,而系統的按鍵處理函數保持不變。面向對象,真神了!5.模擬MessageBox函數
MessageBox函數,這個Windows編程中的超級猛料,不知道是多少入門者第一次用到的函數。還記得我們第一次在Windows中利用MessageBox輸出“Hello,World!”對話框時新奇的感覺嗎?無法統計,這個世界上究竟有多少程序員學習Windows編程是從MessageBox(“Hello,World!”,?)開始的。在我本科的學校,廣泛流傳著一個詞匯,叫做“‘Hello,World’級程序員”,意指入門級程序員,但似乎“‘Hello,World’級”這個說法更搞笑而形象。
圖2 經典的Hello,World!圖2給出了兩種永恒經典的Hello,World對話框,一種只具有“確定”,一種則包含“確定”、“取消”。是的,MessageBox的確有,而且也應該有兩類!這完全是由特定的應用需求決定的。
嵌入式系統中沒有給我們提供MessageBox,但是鑒于其功能強大,我們需要模擬之,一個模擬的MessageBox函數為:
/****************************************** /*
函數名稱:
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;} } 上述函數與我們平素在VC++等中使用的MessageBox是何等的神似啊?實現這個函數,你會看到它在嵌入式系統中的妙用是無窮的??偨Y
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統屏幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被LCD上凌亂不堪的顯示內容所困擾。
屏幕乃嵌入式系統生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不系統、最混亂的部分,筆者曾深受其害。
C語言嵌入式系統編程修煉之道——鍵盤操作篇 1.處理功能鍵
功能鍵的問題在于,用戶界面并非固定的,用戶功能鍵的選擇將使屏幕畫面處于不同的顯示狀態下。例如,主畫面如圖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 [i].ID!= currentFocus)&&(i < ELEMENT_NUM))
{
i++;
}
/* “消息映射” */
if(i < ELEMENT_NUM)
{
switch(keyValue)
{
case OK:
element[i].OnOk();
break;
…
}
} } 在窗口的消息處理函數中調用相應元素按鍵函數的過程類似于“消息映射”,這是我們從WIN32編程中學習到的。編程到了一個境界,很多東西都是相通的了。其它地方的思想可以拿過來為我所用,是為編程中的“拿來主義”。
在這個例子中,如果我們還想玩得更大一點,我們可以借鑒MFC中處理MESSAGE_MAP的方法,我們也可以學習MFC定義幾個精妙的宏來實現“消息映射”。2.處理數字鍵
用戶輸入數字時是一位一位輸入的,每一位的輸入都對應著屏幕上的一個顯示位置(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();
} } 將數字每一位輸入的坐標和輸入值捆綁后,在數字鍵處理函數中就可以較有結構的組織程序,使程序顯得很緊湊。3.整理用戶輸入
繼續第2節的例子,在第2節的onNumKey函數中,只是獲取了數字的每一位,因而我們需要將其轉化為有效數據,譬如要轉化為有效的XXX數據,其方法是:
/* 從2進制數據位轉化為有效數據:XXX */ void convertToXXX(){
BYTE i;
XXX = 0;
for(i = 0;i < NUM_LENGTH;i++)
{
XXX += inputElement[i].byNum*power(2, NUM_LENGTH1);
}
} 反之,我們也可能需要在屏幕上顯示那些有效的數據位,因為我們也需要能夠反向轉化:
/* 從有效數據轉化為2進制數據位:XXX */ void convertFromXXX(){
BYTE i;
XXX = 0;
for(i = 0;i < NUM_LENGTH;i++)
{
inputElement[i].byNum = XXX / power(2, NUM_LENGTH1)% 2;
}
} 當然在上面的例子中,因為數據是2進制的,用power函數不是很好的選擇,直接用“<< >>”移位操作效率更高,我們僅是為了說明問題的方便。試想,如果用戶輸入是十進制的,power函數或許是唯一的選擇了。總結
本篇給出了鍵盤操作所涉及的各個方面:功能鍵處理、數字鍵處理及用戶輸入整理,基本上提供了一個全套的按鍵處理方案。對于功能鍵處理方法,將LCD屏幕與Windows窗口進行類比,提出了較新穎地解決屏幕、鍵盤繁雜交互問題的方案。
計算機學的許多知識都具有相通性,因而,不斷追趕時髦技術而忽略基本功的做法是徒勞無意的。我們最多需要“精通”三種語言(精通,一個在如今的求職簡歷里泛濫成災的詞語),最佳拍檔是匯編、C、C++(或JAVA),很顯然,如果你“精通”了這三種語言,其它語言你應該是可以很快“熟悉”的,否則你就沒有“精通”它們。
C語言嵌入式系統編程修煉之道——性能優化篇 1.使用宏定義
在C語言中,宏是產生內嵌代碼的唯一方法。對于嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法。
寫一個“標準”宏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))發生的事情無法預料。
因而不要給宏定義傳入有副作用的“參數”。2.使用寄存器變量
當對一個變量頻繁被讀寫時,需要反復訪問內存,從而花費大量的存取時間。為此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使用時,不需要訪問內存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的說明符是register。對于循環次數較多的循環控制變量及循環體內反復使用的變量均可定義為寄存器變量,而循環計數是應用寄存器變量的最好候選者。(1)
只有局部自動變量和形參才可以定義為寄存器變量。因為寄存器變量屬于動態存儲方式,凡需要采用靜態存儲方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部static變量;
(2)
register是一個“建議”型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因為條件不滿足并未成為寄存器變量,而是被放在了存儲器中,但編譯器中并不報錯(在C++語言中有另一個“建議”型關鍵字:inline)。
下面是一個采用寄存器變量的例子: /* 求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都被頻繁使用,因此可定義為寄存器變量。3.內嵌匯編
程序中對時間要求苛刻的部分可以用內嵌匯編來重寫,以帶來速度上的顯著提高。但是,開發和測試匯編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用匯編的部分。
在程序中,存在一個80-20原則,即20%的程序消耗了80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。
嵌入式C程序中主要使用在線匯編,即在C程序中直接插入_asm{ }內嵌匯編語句:
/* 把兩個輸入參數的值相加,結果存放到另外一個全局變量中 */ int result;
void Add(long a, long *b)
{
_asm
{
MOV
AX, a
MOV
BX, b
ADD
AX, [BX]
MOV
result, AX
}
}
4.利用硬件特性
首先要明白CPU對各種存儲器的訪問速度,基本上是:
CPU內部RAM > 外部同步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與外設的并行操作程度。5.活用位操作
使用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
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傳輸方式等。
第四篇:c語言編程心得交流
“C語言編程心得交流”主題活動策劃書
一、活動背景
二、活動目的及意
通過本次活動,希望同學對C語言編程有一個重溫二深刻的認識,并且認識到是“從問題到編程”的思考和工作過程,這里既要求掌握嚴格的科學方法,又要求謹慎靈活的工程能力。要很好地完成程序設計工作,編程者需要充分發揮聰明才智,還要有細致認真、一絲不茍的工作態度。即使將來不從事程序工作,這一課程中的鍛煉也可能很重要,尤其對于理科學生,可能彌補其工程方面訓練的不足。前些年的程序設計課常用PASCAL、FORTRAN 或BASIC 等語言,目前已轉向C 或其
他類C 語言(如C++)。從作為入門課教學語言的角度看,沒有一種語具有無可比擬的天然優勢,選擇時都應考慮其有利方面,也需要克服這種選擇帶來的不利因素。
三、承辦單位
主承辦單位:湖南商學院計電學院
承辦單位:電信1102班
四.、嘉賓及評委:
計電學院電信班導師曹東波教授、胡松義教授、倪祖旭學長、王思金
學姐等
五、活動時間:2012年月日
六、活動地點
F202多媒體教室
七、活動形式
1主持人引出問題陳述同學所面臨的困惑
2班級進行自由發言,交流在編程中的體會與技巧 3曹教授和胡教授進行解答
6贈送禮物
八、Ⅰ活動前的準備
1申請使用教室
2邀請評委及嘉賓,發請帖
3為每位老師準備筆、紙和水。
Ⅱ活動進行
1發放筆、紙和水
2拍攝人員到位
3主持人進行嘉賓介紹
Ⅲ活動后期
1活動拍照留念
2贈送禮物
3打掃衛生
九、活動負責人及策劃時間
負責人:電信1102班曹教授,胡教授老師及全班同學策劃時間:2012年3月15日
第五篇:C語言編程
#include(stdio.h)
main()
{ int question[4]={-1,-1,-1,-1},i=0,j=0,k=0,A=0,B=0,answer[4]={0};
char again='y';
while(again=='y'){ srand((int)time(0));
while(i4){ k=(int)rand()%10;
for(j=0;ji;j++)if(k==question[j]){ k=-1;break;}
if(k==-1)continue;question[i]=k;i++;}/*while i*/
for(i=8;i0;i--)/*還有8次機會*/
{ A=0;B=0;printf(“n你還剩下%d次機會。”,i);
printf(“n請輸入四個0-9之間的數字,中間用空格隔開n”);for(j=0;j4;j++)scanf(“%d”,&answer[j]);
for(j=0;j4;j++)
for(k=0;k4;k++)
{ if(answer[j]==question[k]){ if(j==k)A++;else B++;} }/*for*/
if(A==4){ again='0';
printf(“n你贏了,還繼續猜嗎?(y/n)”);
while(again!='y'&&again!='n')
scanf(“...%c”,&again);break;}/*if*/
printf(“n%dA%dB”,A,B);if(i==1){ again='0';
printf(“n你輸了,正確答案是”);
for(j=0;j4;j++)
printf(“%dt”,question[j]);
printf(“n還繼續嗎?(y/n)”);
while(again!='y'&&again!='n')scanf(“%c”,&again);
printf(“%c”,again);break;}/*if*/ }/*for changce*/ }/*while again*/ printf(“感謝您玩這個游戲。”);}