要包含一個單片機硬件的資源頭文件。
各模塊使用了定義在Common.h中的一些數據類型。如:BIT(bit) BYTE(unsigned char)等,具體請參見源程序。
時鐘模塊
在單片機軟件設計中, 時鐘是重要資源, 為了充分利用時鐘資源, 故設計本時鐘模塊。 本模塊使用定時器0,在完成用戶指定功能的同時, 還能夠自動處理一些其它模塊中與時鐘相關的信息。
時鐘模塊由聲明文件Timer.h以及實現文件Timer.c組成。
用戶應該在Config.h中定義宏TIMER_RELOAD來設定定時器0的重裝載初值。推薦的定時器0的中斷時間大于1毫秒。
在程序的初始化階段調用時鐘模塊的初始化函數InitTimerModule()之后,就可以使用時鐘模塊所以支持的各種功能。具體描述如下:
延時:當用戶需要進行一定時間的延時時,可以通過調用Delay()來進行,參數為時鐘中斷的次數。如時鐘中斷周期為1ms, 想進行100ms的延時, 則可以調用Delay(100)。
注意:
如果延時的絕對時間小于時鐘中斷的周期,則不能夠用本方法做到延時。
定時:當程序中需要使用定時功能時,如等待某外部事件,如果在一定時間內發生則繼續執行,如果在這段時間內發生,則認為出現錯誤,轉向錯誤處理機制。
在此推薦一種編程模式,但用戶可以用自己認為更合理的方式處理此類問題。
這里簡單說明一下關于阻塞式函數及非阻塞式函數。簡單說,阻塞式函數就是當檢測完成條件,如果不能夠完成則等待,如:
void CheckSomething()
{
// gbitSuccessFlag is a global variable
while(gbitSuccessFlag == FALSE)
{
// do nothing but waiting
}
}
可以看到,當bitSuccessFlag沒有被設置為TRUE時,函數保持等待狀態不返回,這樣就是阻塞式的函數。
另外一種情況:
BIT CheckSomething()
{
if(gbitSuccessFlag == TRUE)
{
// …
return TRUE;
}
return FALSE;
}
在這里,如果所檢測的事件有沒有完成,函數進行檢測之后,立刻返回,通過返回值報告完成情況,如果沒有完成,則等待調用者分配再次執行的機會。這樣的函數就是非阻塞函數。
在應用定時功能時,首先要將檢測函數定義成非阻塞函數。如上面的第二個版本的CheckSomething。
然后下面模式:
BIT bitDone = FALSE;
ResetClock(); // clear timer interrupt times counter
while(GetClock() < MAX_WAITINGTIME)
{
if(CheckSomething() == TRUE)
{
bitDone = TRUE;
break;
}
}
if(bitDone == FALSE)
{
// process time out
}
或者簡單寫成:
BIT bitDone = FALSE;
ResetClock();
while(GetClock() < MAX_WAITINGTIME && (bitDone = CheckSomething));
if(bitDone == FLASE)
{
// …
}
軟件看門狗:實現具有局限性的看門狗功能。在程序中合適的地方加入對軟件看門狗的復位函數ResetWatchDog(),在Config.h中加入宏 TIMER_WATCHDOGTIMEOUT。當程序運行時,如果在發生TIMER_WATCHDOGTIMEOUT次時鐘中斷之內沒有復位軟件看門狗,則系統復位。
注意:
如果沒有加入TIMER_WATCHDOGTIMEOUT宏,程序中的ResetWatchDog沒有任何用處,不用刪除。
如果系統不能實現時鐘中斷,則軟件看門狗也同時失去功能。
目前版本的的時鐘模塊的復位功能并不是完全復位,主要表現在當復位之后,系統將不再響應任何中斷。所以軟件看門狗只是一個程序的調試功能,不應該將它用于正式工作的程序,此時應該使用硬件看門狗。
用戶自定義任務:如果想在時鐘中斷內執行一些耗時較短的任務,可以定義回調函數OnTimerInterrupt。函數原形為:void OnTimerInterrupt();
如果想在發生時鐘中斷時執行一些功能,而這些功能又耗時相對較長,不合適放在中斷響應函數內部,則可以在程序中的主循環中的任意地方添加: ImpTimerService(),同時提供原形為void OnTimerEvent()的回調函數。具體的程序如下所示:
void main()
{
Initialize();
while(TRUE)
{
// … working
ImpTimerService();
// … working
}
}
void OnTimerEvent()
{
// do some task
}
對通訊模塊提供支持:如通訊中的各種超時等,見通訊模塊中的詳細說明。
對鍵盤掃描模塊提供支持:可以自動調用鍵盤掃描模塊,見鍵盤掃描模塊中的詳細說明。
對程序調試提供支持:在程序開發過程中,有時為了判斷程序是不是在工作,常用利用單片機系統的某一空閑引腳通過一個限流電阻接一個發光二極管,在程序中間隔固定時間交替控制發光管的明暗。實現這個功能只要在Config.h文件中定義TIMER_FLASHLED宏,如:
#define TIMER_FLASHLED P1_0
則當時鐘中斷發生256次之后,改變發光管的狀態。
通訊模塊
串口資源做為單片機與外界通信的常用手段,通訊模塊提供了完全緩沖的串口通訊底層機制,適用于長度不大的數據包的發送及接收。如果處理關鍵數據,需要用戶自己提供糾錯協議。
通訊模塊由聲明文件SComm.h及實現文件SComm.c組成。
初始化:調用函數InitSCommModule()來初始化通訊模塊:
void InitSCommModule(BYTE byTimerReload, BIT bitTurbo)
參數說明:
byTimerReload: 定時器1的重裝載初始值。
bitTurob: 當此參數為TRUE時,串行通訊在定時器1的溢出速率基礎上加倍。為FALSE時,串行通訊速率為定時器1的溢出速率。
緩沖區:模塊使用了由宏SCOMM_SENDBUFSIZE、SCOMM_RECEBUFSIZE及SCOMM_PKGBUFSIZE所指定長度的三個緩沖區,分別為發送、接收及數據包(用于處理接收到的數據)緩沖區(如果沒有使用異步接收功能,則不需要使用數據包緩沖區)。
在缺省時,這三個宏都被定義為10,但用戶可以自已按照系統的RAM資源占用情況在Config.h中重定義緩沖區的大小。需要注意的是,如果緩沖的長度不夠,當發送或接收長數據包的時候可能會發生問題,關于數據緩沖區的最小值的設置可以參考下面的說明。
注意:需要盡快取出接收緩沖區中的數據,否則當緩沖區滿之后,新的數據將被簡單的丟掉。
字節級服務函數: 在Config.h文件中定義了宏SCOMM_DriverInterface(如:#define SCOMM_DriverInterface),則可以使用字節級服務函數,即通訊模塊的底層函數。
共有兩個函數可以使用:
void SendByte(BYTE byData);
發送一個字節,如果當前緩沖區滿,則等待。參數byData為要發送的數據。
BYTE ReceByte();
接收一個字節,如果當前緩沖區中沒有數據,則此函數阻塞,直到接收到數據為止。接收到數據通過返回值返回。
可以通過調用IsSendBufEmpty() IsSendBufFull() IsReceBufEmpty() IsReceBufFull() 宏來判斷緩沖區的空或滿,以防系統阻塞。
不推薦直接使用這一級的服務函數,應該使用高層次上的服務函數或者在這一級服務函數的基礎上構造自己的通訊函數。
數據包級服務函數:在Config.h文件中定義宏SCOMM_PackageInterface(如: #define SCOMM_PackageInterface)則可以使用數據包級服務函數。
共有兩個函數可以使用:
void SendPackage(BYTE* pbyData, BYTE byLen);
發送數據包,參數pbyData為將要發送的數據包緩沖區(數組)的指針,byLen為將要發送的數據包的長度。
當沒有定義SCOMM_DriverInterface時,數據被完全緩沖。即不能夠發送長度超過發送緩沖區長度的數據包。當定義了SCOMM_DriverInterface時,采用單字節發送,這時不限制需要發送的數據的長度。
BYTE RecePackage(BYTE* pbyData, BYTE byLen);
接收數據包,參數pbyData為存放將要接收的數據的緩沖區,byLen為緩沖區長度。返回值為接收到的字節數,當模塊的接收緩沖區為空時,函數非阻塞,立即返回,返回值為零。
同步發送接收服務函數:
比如在一個串行總線多機通訊系統中,主機需要定時循檢各從機的狀態,往往是發一個包含從機地址及指令的數據包給從機,之后等待一定的時間,從機需要在這段時間之內給主機一個應答,如果沒有這個應答,則認為從機工作狀態出錯,轉去進行相應的處理。在這個模型里,主機不能夠不進行等待而給另一臺從機發送指令,也不能夠不管從機在很久沒有應答的情況下繼續等待。還有一種情況,比如當使用485總線進行通信時,如果是兩條通訊線則系統只能工作在半雙工模式下,總線在同一時間內只能工作在發送或接收,為了防止發送和接收相互干擾,這時的通訊常常需要使用同步發送和接收。
當在Config.h文件中定義宏SCOMM_SyncInterface后,則可以使用通訊模塊提供同步發送接收函數:
void SendPackage(BYTE* pbyData, BYTE byLen);
發送數據包,參數pbyData為將要改善的數據包的緩沖區指針,byLen為將要發送的數據包的長度。
這個函數可以保證等待一個完整的數據包完全發送出去之后,它才返回,在這段時間內,它會阻塞運行。
BYTE SyncRecePackage(BYTE* pbyBuf, BYTE byBufLen, WORD wTimeout, BYTE byParam);
接收數據包。返回值為接收到的數據包長度。參數pbyBuf為將要接收數據包的緩沖區的指針,byBufLen為提供的緩沖區的長度,wTimeout為通信超時值,如果在發生了由wTimeout所指定次數的時鐘中斷而還沒有接收到或沒有接收到完整的數據包時,函數返回零,最后一個參數byParam的含義見后面的解釋。
異步發送接收服務函數:
在一個簡單的系統或多機通訊系統中的從機上,一般情況下不需要復雜的停等的工作模式,而且往往單片機需要對硬件進行控制和檢測,不允許長時間的停下來檢測通訊,但又要求當需要通訊時需要盡快的反應速度,這時就需要使用異步發送和接收服務函數。
使用異步發送和接收服務函數需要在Config.h文件中定義SCOMM_AsyncInterface宏。
同樣提供兩個服務函數:
void SendPackage(BYTE* pbyData, BYTE byLen);
發送數據包,參數pbyData為將要改善的數據包的緩沖區指針,byLen為將要發送的數據包的長度。
這里的函數的接口與同步發送和接收的服務函數相同。關于這里的細節,見后面對同步和異步服務函數的說明。
void AsyncRecePackage(BYTE byParam);
接收數據包,參數byParam的意義見后面的描述。
使用異步通訊需要用戶定義一個回調函數,原型如下:
void OnRecePackage(BYTE* pbyData, BYTE byBufLen);
當異步接收服務函數接收到數據包之后,調用OnRecePackage回調函數,在pbyData指定的緩沖區中存放數據包,byBufLen為數據包的長度。
在Config.h文件中定義宏SCOMM_TIMEOUT可以設定異步接收的超時值,當開始接收數據包,但沒有收完數據而發生了SCOMM_TIMEOUT次時鐘中斷后,認為接收超時,將已接收到的數據刪除。
同步和異步通訊服務函數:
有些情況下,比如一個通訊系統中,由一臺計算機通過串口控制主機,主機通過串口連接很多從機,主機的串口采用分時復用,在這樣的模型中,主機和控制計算機之間的通訊可以使用,異步通訊方式,而主機與從機可以使用同步通訊方式。而同步和異步的發送函數接口是相同的,在這樣的情況下,發送都是同步的。在這樣的模型中,當使用不同的接收函數之前,需要注意清除接收緩沖區中的內容,通訊模塊提供函數:ClearReceBuffer來做到這一點,此函數原型如下:
void ClearReceBuffer();
通訊過程中,數據包往往是有固定的格式的,這種格式需要根據用戶所使用的協議的不同而不同。同步和異步接收服務函數支持從接收到的數據中識別出一定格式的數據包。
舉例說明:目前使用的協議決定數據包的格式為固定的包頭0xff,固定的長度4個字節。其它的細節在這里不重要,所以忽略掉。
為了能夠使用用SyncRecePackage或AsyncRecePackage函數從接收到的數據中識別出如上格式的數據包,有兩種方法:
第一種辦法是在Config.h文件中定義宏SCOMM_SimplePackageFormat,說明數據包為一種簡單格式,比如上面的協議。
之后還要定義兩個宏分別用來識別數據包頭和數據包尾,兩個宏分別是:
IsPackageHeader(x)和IsPackageTailer(x, y, z)
接收函數(SyncRecePackage和AsyncRecePackage)在沒有開始接收數據包(準確的說是還沒有從接收到的數據包中找到包頭的時候),會對接收到的每一個字節的數據調用IsPackageHeader宏,將相應的數據作為參數,如果IsPackageHeader宏的結果為 TRUE,則認為找到了數據包頭,否則繼續對下一個字節進行判斷。
上面的協議對應的IsPackageHeader宏可以寫為:
#define IsPackageHeader(x) ((x) == 0xff)
當接收到包頭之后,接收函數會對接下來的每一個字節數據調用IsPackagTailer宏來判斷是不是已經接收完數據包,三個參數分別為:
x: 當前判斷的數據。
y: 從包頭開始到當前被判斷的數據止的計數值,即當前已經接收到的字節數。
z:用戶在調用SyncRecePackage或AsyncRecePackage時指定的byParam參數。
與IsPackageHeader相似,如果宏IsPackageTailer的運算結果為TRUE,則認為接收到完整的數據包,則調用相應的回調函數(對于異步接收函數)或返回(對于同步接收函數)。如果運算結果為FALSE則繼續判斷下一個字節的數據。
上面的協議對應的IsPackageTailer宏可以寫為:
#define IsPackageTailer(x, y, z) ((y) >= (z))
當然,用戶也可以將IsPackageHeader和IsPackageTailer定義成為函數,通過BIT類型的返回值來向調用者提供與相應宏相同的信息。
另一種辦法需要在Config.h文件中定義宏SCOMM_ComplexPackageFormat。(需要注意的是,不能夠同時定義 SCOMM_SimplePackageFormat和SCOMM_ComplexPackageFormat宏,否則會造成嚴重的不可預見性錯誤。
這時需要提供回調函數QueryPackageFormat,原形如下:
BYTE QueryPackageFormat(BYTE byData, BYTE byCount, BYTE byParam);
函數中三個參數的含義與使用簡單數據包格式時判斷數據包尾的宏的參數相同。
函數通過返回值來通知作為調用者的接收函數對接收到的數據如何處理,但目前這種方法僅為需要處理復雜數據包格式時的一種可選方法,但不推薦。用戶如果想使用這種方法可以自己更改接收函數中相應的
#ifdef SCOM_ComplexPackageFormat
#endif // SCOMM_ComplexPackageFormat
預編譯指令之間的內容。
例如指定QueryPackageFormat的返回值的含義:
0:繼續找數據包頭或繼續找數據包尾。
1:找到數據包頭。
2:找到數據包尾。
3:數據包出錯,需要拋棄。
然后更改源代碼來實現上面的協議。
注意:當用戶需要使用字符串的時候,可以利用簡單的包裝函數將字符串轉換為字節數組。所以沒有必要提供專用的字符串處理函數。
鍵盤掃描模塊
鍵盤掃描模塊有兩種工作方式, 一種為自動的由時鐘模塊調用, 另一種是由程序員自行調用。
1) 由時鐘模塊自動調用的方式
將時鐘模塊實現文件(Timer.h)及鍵盤掃描模塊的實現文件(KBScan。c)包含進工程, 在Config.h 文件中添加TIMER_KBSCANDELAY宏。時鐘模塊自動對時鐘中斷進行計數, 當達到TIMER_KBSCANDELAY宏所定義的值后, 自動調用鍵盤掃描模塊中的函數KBScanProcess()進行鍵盤掃描,也就是說,這個宏的值可以決定按鍵消抖動的時間。
用戶應該提供兩個回調函數OnKBScan()及onKeysPressed()。 在函數OnKBScan中進行鍵盤掃描, 并返回掃描碼。掃描碼的類型缺省為BYTE, 當鍵盤規模較大時, BYTE不能夠完全包含鍵盤信息時, 可在Config.h文件中重定義宏KBVALUE, 如下:
#define KBVALUE WORD
這樣, 就可以使用16位的鍵盤掃描碼, 如果此時還達不到要求, 可以將鍵盤掃描碼定義成一個結構, 但這樣做將會增加代碼量及消耗更多的RAM資源, 故不推薦。
掃描模塊調用OnKBScan取得掃描碼, 并調用用戶可以重定義的宏IsNoKeyPressed來判斷是否有鍵按下, 缺省的IsNoKeyPressed實現如下:
#define IsNoKeyPressed(x) ((x) == 0x00)
即認為OnKBScan返回0掃描碼時為沒有鍵按下, 如果掃描函數返回其它非零掃描碼做為無鍵按下的掃描碼時, 可以在Config.h文件中重定義IsNoKeyPressed宏的實現。
8位鍵盤掃描碼(缺省值)時, 相應的掃描函數為:
BYTE OnKBScan()
當掃描模塊經過軟件消抖動之后, 發現有鍵按下, 就會調用另一個回調函數onKeysPressed。 函數的聲明應該如下:
void onKeyPressed(BYTE byKBValue, BYTE byState)
其中中的參數byKBValue的類型為BYTE, 此為缺省值, 如果使用其它類型的掃描碼, 就將此參數變為相應類型。這個值由OnKBScan返回。另一個參數byState在通常情況下為零。但當用戶在Config.h中定義宏KBSCAN_BRUSTCOUNT,同時鍵盤上的某鍵被按住不放時, 掃描模塊對它自己的調用(注意這里和TIMER_KBSCANDELAY宏不同, TIMER_KBSCANDELAY是時鐘中斷足夠的次數后調用掃描模塊, 而KBSCAN_BRUSHCOUNT為掃描模塊自身的被調用次數)進行計數,當達到KBSCAN_BRUSTCOUNT時,掃描模塊調用 onKeysPressed,此時第一個參數的含義不變,而byState變成1, 同時計數器復位,又經過一段時間后,用值為3的byState 調用onKeysPressed。 這樣就可以很方便的實現多功能鍵或者檢測某鍵的長時間被按下。
2)由用戶自行調用
由用戶自行在程序中調用掃描模塊,而不是由時鐘中斷自行調用。其它與方式1相同。
注意:
1) 函數KBScanProcess為非阻塞函數,它將在很快的時間內返回,等待再次分配給它執行的機會。
2) 函數KBScanProcess是在時鐘中斷外部運行的,它的過程可以被任何中斷打斷,但不影響系統運行。
3) byState的最大值為250,之后被復位為零。
應用舉例
現在來舉例說明上述幾個模塊的使用方法。
硬件環境描述:
為了控制一盞燈,需要單片機提供一個做控制功能的開關量,這里不描述外部接口電路,只說明當單片機的P10腳為高電平時,燈滅,當P10腳為低電平時,燈亮。
可以通過計算機由串口發送命令來控制,或通過一個按鍵(push button不是自鎖式的按鍵)來手動控制(按鍵接在P11腳上,當鍵沒有按下時,P11電平為高,鍵按下時,引腳電平被接低),當使用按鍵手動控制的時候,需要給計算機發送通知。
設定串口通訊指令如下:
數據包由0xff做包頭,4個字節長,第二個字節為命令代碼,第三個字節為數據,最后一個字節為校驗位。
命令和數據代碼有如下組合:
(計算機發給單片機)
0x10 0x01: 計算機控制燈亮。(數據位是非零值即可)
0x10 0x00: 計算機控制燈滅。
(單片機發給計算機)
0x11 0x01:單片機正常執行控制指令,返回。(數據位是非零值即可)
0x11 0x00: 單片機不能夠正常執行控制指令,或控制指令錯(不明含義的數據包或校驗錯等)。
0x12 0x01:手動控制燈亮。(數據位是非零值即可)
0x12 0x00: 手動控制燈滅。
建立工程:
在硬盤上建立文件夾Projects,在Projects下建立Common文件夾及Example文件夾。將各模塊的頭文件及實現文件拷貝到 Common文件夾下(推薦使用這樣的文件組織結構,其它工程也可以建立在Projects下,各工程共享Common文件夾中的代碼)。
啟動KeilC的IDE,在Example下建立新工程,將各模塊的實現文件包含進工程。
在Example文件夾下建立Output文件夾,更改工程設置,將Output作為輸出文件和List文件的輸出文件夾(推薦使用這樣的結構,當保存工程文件時,可以簡單的刪除Output文件夾中的內容而不會誤刪有用的工程文件)。
建立工程配置頭文件Config.h及工程主文件Example.c,并將Exmaple.c文件加入工程。
輸入代碼:
代碼的具體編寫過程略。下面是最后的Config.h文件及Example.c文件。
//
// file: Config.h
//
#ifndef _CONFIG_H_
#define _CONFIG_H_
#i nclude <Atmel/At89x52.h> // 使用AT89C52做控制
#i nclude “../Common/Common.h” // 使用自定義的數據類型
#define TIMER_RELOAD 922 // 11.0592MHz晶振,1ms中斷周期
#define TIMER_KBSCANDELAY 40 // 40ms重檢測按鍵狀態,即40ms消抖
#define SCOMM_AsyncInterface // 使用異步通訊服務
#define IsPackageHeader(x) ((x) == 0xff) // 判斷包頭是不是0xff
#define IsPackageTailer(x, y, z) ((y) <= (z)) // 判斷包的長度是不是足夠
#endif // _CONFIG_H_
//
// file: Example.c
//
#i nclude <Atmail/At89x52.h>
#i nclude “../Common/Common.h”
#i nclude “../Common/Timer.h”
#i nclude “../Common/Scomm.h”
#i nclude “../Common/KBScan.h”
BIT gbitLampState = 1; // 燈的狀態,缺省為off
static void Initialize()
{
InitTimerModule(); // 初始化時鐘模塊
InitSCommModule(0xfd, TRUE); // 初始化通訊模塊,11.0592MHz晶振,
// 波特率為19200
EA = 1; // 開中斷
}
void main()
{
Initialize(); // 初始化
while(TRUE) // 主循環
{
ImpTimerService(); // 實現時鐘中斷服務,如鍵盤掃描
AsyncRecePackage(4); // 接收4個字節長的數據包
}
}
// 在中斷外部響應時鐘中斷事件
void OnTimerEvent()
{
// do nothing
}
// 控制外部燈
static void TriggerLamp(BIT bEnable)
{
P10 = ~bEnable; // 需要反相控制
}
// 鍵掃描回調函數
BYTE KBScan()
{
BIT b;
P11 = 1; // 讀之前拉高引腳電平
b = P11; // 讀入引腳狀態
return ~b; // 數據反相做掃描碼
}
// 計算校驗和
static BYTE CalcCheckSum(BYTE* pbyBuf, BYTE byLen)
{
BYTE by, bySum = 0;
for(by = 0; by < byLen; by++)
bySum += pbyBuf[by];
return 0 – bySum;
}
// 接收到鍵盤消息回調函數
void onKeyPressed(BYTE byValue, BYTE byState)
{
BYTE by[4];
if(byState == 0)
{
switch(byValue)
{
case 0x01:
gbitLampState = ~g bitLampState; // 燈狀態取反
TriggerLamp(gbitLampState); // 執行控制
by[0] = 0xff; // 構造數據包
by[1] = 0x12;
by[2] = (BYTE)gbitLampState;
by[3] = CalcCheckSum(by, 3); // 求校驗和
SendPackage(by, 4); // 發送數據包
break;
// 處理其它掃描碼
default:
break;
}
}
// 接收到數據包回調函數
void OnRecePackage(BYTE* pbyBuf, BYTE byBufLen)
{
BYTE by[4];
by[0] = 0xff;
by[1] = 0x11;
if(byBufLen != 4 || pbyBuf[3] != CalcCheckSum(pbyBuf, 3))
{
by[2] = 0;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 處理長度或校驗和不正確
}
switch(pbyBuf[1])
{
case 0x10:
gbitLampState = (BIT)pbyBuf[2];
TriggerLamp(gbitLampState);
by[2] = 1;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 發送成功執行通知
break;
default: // 不知道的命令
by[2] = 0;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 發送沒有成功執行通知
break;