1. 引言
隨著科技的發展,軟硬件資源的成熟和完善,嵌入式系統在現代工業控制領域中得到了越來越廣泛的應用,其應用領域涉及通信、自動化、信息家電、軍事等各個方面。而嵌入式操作系統的引入大大提高了嵌入式系統的功能,方便了嵌入式應用軟件的設計。
Windows CE是微軟公司開發的一種嵌入式實時操作系統,它是一種模塊化的、實時的、有強大的通信功能的、搶先式、多任務具有強大通信功能的32位嵌入式操作系統。
在嵌入式系統的實現中一般都會涉及數據的采集和處理,因此數據的通信成了系統穩定可靠運行的關鍵。串行通信是計算機與外部設備交換信息的重要途徑,由于其實現簡單,節省I/O口和線路,傳輸時序明晰等特點,應用的非常普遍,同樣在嵌入式系統中它也是一種主要的通信方式。在本文中研究的是基于Windows CE操作系統的掌上電腦和單片機之間的串行通信問題。
2. 系統結構和Windows CE簡介
本文介紹的是一種基于掌上電腦的便攜式動態心電信號采集及處理系統,主要討論系統串行通信的設計和實現。整個系統由檢測模塊和掌上電腦兩部分組成。其中,檢測模塊是由AT89C52單片機控制的智能模塊,負責心電信號的檢測、放大、濾波與采集;掌上電腦負責參數的設置,心電波形數據存儲、處理、分析以及波形顯示等;掌上電腦基于Windows CE操作系統。圖1為整個系統的功能框圖,檢測模塊與掌上電腦之間通過RS232接口實現通信,而掌上電腦通過RS232或USB接口和PC機進行數據通信,由PC機對數據進行深入的分析和處理。整個系統的實現中,數據的串行通信是最基本也是最重要的部分。由于掌上電腦和PC機之間的通信由商家提供專門的接口線以及驅程,因此我們在這不作具體的研究。
Windows CE作為一種嵌入式操作系統,它的很多特性都是為了適應嵌入式系統的特殊要求,它與一般的Windows程序有很多區別,如API函數,存儲器的限制,電源管理方式,硬件特性等等。但是在通信方面Windows CE基本擁有和Windows同樣的Win32 API,因為運行Windows CE的系統或者是移動的,或者需要與遠程服務器進行連接,因此必須具有強大的通信功能。Windows CE下的應用程序是通過文件I/O函數CreateFile,ReadFile,WriteFile,CloseHandle訪問設備驅動程序的,對文件進行操作時,在Windows CE下的設備不支持重疊I/O。
圖1 系統整體結構概略圖
3. Windows CE下基于多線程的串行通信實現
什么是使用多線程的好時機呢?如果你的程序有許多事要忙,但是你還要隨時保持注意某些外部事件(可能來自硬件或來自使用者),這時就適合使用多線程來幫忙。以通信程序為例,你可以讓主線程負責使用者界面,并保持中樞的地位,而以―個分離的線程處理通信端口,這樣就可以在串口讀寫數據的同時保持使用者界面依然靈活,不受影響。本文就是采用這種多線程的方法來實現串行通信的,創建了單獨的讀和寫線程來處理串口讀寫數據。
Windows CE下的串行設備被視為用于打開、關閉、讀和寫串行端口的常規、可安裝的流設備。這里我們構造一個串口類CSerial來對Win32 API串口操作函數CreateFile,ReadFile,WriteFile,CloseHandle等進行封裝,并在其中完成對串口的各項設置。在本系統中主要時在Windows CE環境中接收單片機上傳的大量數據,因此我們將對數據的接收作比較詳細的分析。
1) 串口的打開和配置
在類CSerial中用BOOL Open( int nPort, int nBaud)來完成串口的打開和初始化工作。先調用CreateFile打開指定的串口,然后通過GetCommState和SetCommState函數來配置串口,最后設置串口讀寫數據的超時值。
配置串口時一般先調用GetCommState得到默認的DCB結構,然后根據自己的需要來對它作必要的修改,再用SetCommState來重新配置串口。DCB結構包括波特率、流控制、傳輸模式、起始位、停止位、校驗等設置。需要注意的是Win32操作系統一般只支持二進制的傳輸模式,因此fBinary字段應設為TRUE,另外接收緩沖器應該盡量設的大一些。
下面具體研究一下讀寫數據的超時值,通過GetCommTimeouts和SetCommTimeouts對COMMTIMEOUTS結構的5個字段進行設置。通常在實現串口通信時往往不重視甚至忽略對讀寫數據超時值的設置,這樣可能就會造成串口數據讀寫的不可靠性,特別是在接收大量數據時,如果超時值的設置不合適將會使數據不能完全接收過來而導致通信出錯。在本系統中如下設置串口超時值。
COMMTIMEOUTS CommTimeOuts;
CommTimeOuts.ReadIntervalTimeout =10;
CommTimeOuts.ReadTotalTimeoutMultiplier =10;
CommTimeOuts.ReadTotalTimeoutConstant = 10;
CommTimeOuts.WriteTotalTimeoutMultiplier = 5;
CommTimeOuts.WriteTotalTimeoutConstant = 5;
其中ReadIntervalTimeout設置串口相鄰字節接收間隔時間的最大值,單位為毫秒。如果前后兩個字節之間的間隔時間超過該設定值,ReadFile就返回,終止接收。ReadTotalTimeoutMultiplier用來計算 ReadFile函數的總超時,單位為毫秒。每次讀取串口操作,將其與要接收字節數相乘再與ReadTotalTimeoutConstant相加來計算 ReadFile函數的總超時時間。寫操作兩個字段的設置與讀操作類似。
當波特率較高時,ReadIntervalTimeout不能設的太大,否則兩次接收將會當作一次處理,通信將出現錯誤。而對于后兩者,由于 ReadFile當總超時時間到時要立刻返回,因此要綜合考慮波特率、應接收字節數等因素,以期串口的正確運行。很多人在實現串行通信時簡單的將ReadIntervalTimeout設置為MAXDWORD, ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant設為0,這種做法在進行大量數據傳輸中并不適用,可能會導致數據的丟失。也不能在設置了適當的字節間超時后就簡單的把總超時設為0以期待直到所有數據讀完后ReadFile才返回,這樣可能會使ReadFile一直處于等待狀態,不能正常返回。
2) 數據的接收
數據的接收我們用DWORD ReadData(char *data,CString FileName)函數來完成,如下所示。
DWORD CSerial::ReadData( char *data,CString FileName)
{
char Byte[1000];
DWORD dwComStatus,dwBytesTransferred;
DWORD len=0;
CFile ECGFile;
ECGFile.Open(FileName,CFile::modeCreate|CFile::modeWrite);
SetCommMask (m_hComID, EV_RXCHAR | EV_CTS | EV_DSR);
if (m_hComID!= INVALID_HANDLE_VALUE)
{
WaitCommEvent (m_hComID, &dwComStatus, 0);
if (dwComStatus & EV_RXCHAR)
{
do
{
ReadFile (m_hComID,
&Byte,
1000,
&dwBytesTransferred,
0
);
if (dwBytesTransferred)
{
// strncat(data,Byte,dwBytesTransferred); //接收數據較少時
strncpy(data,Byte,dwBytesTransferred); //接收數據較多時
len+=dwBytesTransferred;
g_nCount=len;
ECGFile.Write(Byte,dwBytesTransferred);
ECGFile.Flush();
}
}
while (dwBytesTransferred);
}
}
ECGFile.Close();
return len;
}
該函數的調用是在一個單獨的線程函數ReadThread中,我們創建一個單獨的線程來讀串口數據,用如下的語句來創建該讀線程。
hReadThread = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)ReadThread, this, 0, &dwThreadID))
在ReadData函數中先使用SetCommMask設置事件掩碼,然后WaitCommEvent就阻塞線程,直到“串口接收到一個字符”的預定事件發生線程才繼續執行。
在用ReadFile函數讀數據是要注意以下3點:
a) 接收緩沖區Byte的大小最好和ReadFile中第3個參數(即要讀取的字節數)一致。
b) 緩沖區Byte的大小要根據實際情況來設置,當要接收的數據比較多,波特率又設的較高時應盡量將緩沖區設的大些,否則可能會使數據丟失。
c) ReadFile中的第四個參數是實際接收到的字節數,由于通信中常常不可預料的會發生各種異常情況,每次實際接收到的字節數未必和你希望接收的數量一致,所以當每次從接收緩沖區中取數據時應以dwBytesTransferred的值為準,這樣可以避免將不是串口得到的數據也錯誤的取進來。
當接收的數據量大時我們不得不考慮到Windows CE系統的內存限制問題,那么有限的內存根本無法將那么多的數據同時放在內存中。實際情況確實也是這樣的,在實驗中每次當串口接收的數據多達幾十K時,往往會發生堆棧溢出等異常。于是我們考慮將每次ReadFile接收到的數據讀進內存后就將它永久存儲到對象存儲器中,當然也可以是自備的存儲卡,就像上面給出的程序,我們用MFC中的CFile類來完成文件的存儲功能。ReadData函數的第2個參數傳入的就是存儲文件的路徑和名字。這樣每次只要消耗固定量的內存,解決了內存的問題。當然,如果在實際中需要從串口接收的數據不是很多時,為了方便數據的處理,我們通常還是把它們都放在內存中。
3) 數據的發送
主要是在函數SendData中調用了API函數WriteFile,本系統中只需向單片機發送一些參數的設置和簡單的控制指令,應用相對比較簡單。我們也創建一個單獨的線程來寫數據到串口,對SendData函數的調用在線程函數SendThread中,創建寫線程的方法和讀線程類似。
4) 串口的關閉
串口的關閉是最簡單的,只需使用CloseHandle函數就可以了。
4. AT89C52單片機的串行通信
智能采集部分我們采用的是AT89C52單片機,采用中斷的方式來與掌上電腦進行數據通信。我們設定單片機的串口控制寄存器SCON=0x50 ,使串口工作在方式1(即10位異步收發方式),在這種方式下,串行口的波特率是可編程的,由所使用的定時器的溢出率決定。AT89C52除了有定時器0和1外,還增加了定時器2,定時器2是一個16位定時/計數器,其控制和狀態位位于T2CON和T2MOD,寄存器對RCAP2H,RCAP2L是定時器2在16位自動重裝載方式下的自動重裝載寄存器。
在單片機的串行通信中波特率的設定是最關鍵的工作,它決定了通信的速度和成敗。波特率最終是由單片機的主機頻率和定時器的工作方式決定的。通常情況下,單片機的晶振頻率一般選用12M或24M等整數,采用定時器1來作為波特率發生器,因為51系列的單片機沒有定時器2。這樣就會出現問題,大家經常會發現當設置波特率較高時串口接收的數據就會發生錯誤。經過了一段時間的研究,我們找到了原因,當采用T1作為自動重裝初值的8位計數器來產生波特率時,由于單片機晶振是12M或24M,T1的計數頻率是1/12的單片機主頻,根據T1的溢出率計算得出的定時器初值不夠精確,會產生一定的誤差,而且誤差隨著所設波特率的提高而增加。這時的波特率計算公式如下:
波特率=
其中fosc是單片機主頻,當SMOD=1時,波特率加倍。
有如下2個方法可以解決這個問題:
1) 調整單片機的主頻,可以選用11.0592M,22.1184M等來消除波特率設置的誤差。
2) 采用具有16位定時/計數器T2的單片機,如AT89C52。這時使用T2的16位自動重裝初 值的工作方式來產生波特率,在串口工作在工作方式1時,波特率的計算公式如下:
波特率=
由于T2的初值是16位的,且這種工作方式下T2的計數頻率是1/2的單片機主頻,按照上述公式計算得到的定時器初值的精度足以實現我們所需的波特率。
根據上面的分析,我們采用第2個方案,設置T2CON=0x34,使T2工作于波特率發生器方式,通過TH2,TL2設置定時器初值,在該方式下寄存器RCAP2H和RCAP2L中的值應與TH2和TL2中相同,以便在T2溢出時,將RCAP2H和RCAP2L中的初值自動重裝到TH2和TL2中。
具體的單片機串口設置如下:
SCON=0x50; //串口工作在方式1
TH2=0xff;
TL2=0xd9; //設置波特率為19200
RCAP2H=0xff;
RCAP2L=0xd9;
T2MOD=0x00;
T2CON=0x34; //T2工作工作于波特率發生器方式
IE=0x90; //開串口中斷
5 總結
本文介紹了在Windows CE環境下與單片機的基于多線程的串行通信的實現問題。深入研究了Windows CE中對基于多線程的串口通信的各項設置和數據接收中應注意的地方,并提出了AT89C52單片機的串口通信中波特率正確設定的方法,特別適用于傳輸的數據量較大且波特率較高的情況。在實際工作中,我們利用基于Windows CE的系統,通過RS-232C標準接口,與使用單片機的采集模塊進行大量數據通信,采用文中介紹的方法,實現了準確、可靠的數據傳輸。