摘要:
本文介紹了在Microsoft Visual C++ 6.0環境下對RS-232-C串行端口進行編程,以及對後台監控程序所普遍涉及到的無阻塞後台運行、數據的實時接收和處理等問題的解決方法。
一、 引言
在實驗室和工業應用中,受信道成本限制,串口常常作為計算機與外部串行設備之間的首選數據傳輸通道,而且由於串行通信方便易行,許多設備和計算機都可以通過串口對外設進行控制、檢測,串口通訊日益成為計算機和外設進行通訊、獲取由外設采集到的監測數據的一個非常重要的手段。本文所描述的程序實例運行於Windows 9x操作系統下,可後台運行、實時接收、處理從端口傳來的數據,並能通過向串口發送命令來控制外設的動作。為了避免在實時監控數據時引發程序阻塞,在本程序中引入了線程和端口中斷響應等技術。
二、 程序設計思路
由於本程序要對串行端口進行實時監控,這就要求它是一個後台程序,在監控的同時可以在前台進行其他一些於之無關的操作。因而在實現時即要避免無時無刻都在反復讀端口的效率低下的輪詢方式,又不能因為來不及處理而將突然到達的監測數據丟失。只有采取端口中斷的異步方式才能實現高效、安全的監控過程,只要一有數據到達端口,馬上拋出中斷請求,中斷處理函數便會及時啟動以處理到來的數據,從而避免了輪詢間隙丟時數據的可能。而在大部分無數據到達的時間內不會有中斷拋出,中斷處理函數也不會執行,即僅僅在有數據到達的一瞬間進行工作,此效率不可謂不高。
綜上所述,要實現上述要求,就要用到下列技術來解決所遇到的關鍵性問題:一是采用多線程來避免在進行文件操作等耗時操作時會引發阻塞現象的發生。同時為了防止多個線程同時對同一個變量進行操作引起時序上的差錯,為了保持線程的同步,還采取了臨界區加解鎖的技術;二是對端口的數據讀取方式采取中斷響應模式,具體原因前面以講的很清楚,在此不再贅述;三是使用了定時器,以滿足實時監控類程序的實時顯示功能,以便及時的將所接收到的動態數據及時的反映到屏
幕上。
三、 RS-232-C串行端口監控軟件的程序實現
(一) 界面風格
由於是實時監控軟件,那就既要監測從外設傳來的實時數據,又要通過串口向外設發送一些具體的指令以控制外設完成預先設定的動作。為了方便向串口發送命令可以在工具條上再加一個類似於"Internet Explorer 浏覽器"風格的對話條,可以在初建工程時指定"Internet Explorer ReBars"風格,也可以通過添加Microsoft Visual C++ 6.0自帶的"Dialog Bar"組件來實現。而要及時將從外部讀取的數據顯示給管理人員,並且留有相當記錄以備查閱,可以選擇列表視圖來實現。
(二) 串口的參數設置及打開
對RS-232-C串行端口進行參數配置是使用串口進行通訊的必要條件。而且由於場合不同、用途、功能的不同對串口也采取不同的配置方式,為了使本程序更靈活,適應面更廣,采取將所有的可能參數都預先設置在幾個組合框中,可以在程序運行後隨時更改設置。自定義一個設置串口參數的數據結構:
typedef struct tagCOM_CONFIG
{
int nPort; file://端口號,從COM1到COM4
int nBaud; file://波特率,從1200bps到57600bps(對應的宏為CBR_1200到CBR_57600)
int nData; file://數據位個數,7位或是8位
int nStop; file://停止位個數,可以是1位、1.5位、2位。
int nParity;//采取的校驗方式,有無校驗(NOPARITY)、
file://奇校驗(ODDPARITY)和偶校驗(EVENPARITY)等。
}COM_CONFIG;
當選擇好適當的參數後就可以根據設置好的端口配置情況打開通訊端口了。與以往DOS下串行通信程序不同的是,Windows操作平台下不提倡應用程序直接控制硬件(包括端口),也不讓使用中斷(除非打入到Ring0系統級),而是通過Windows操作系統提供的設備驅動程序來進行數據傳遞。在Windows操作系統下串行口和其他通訊端口一樣是作為文件來進行處理的,而不是直接對端口進行操作,對於串行通信,Win 32 提供了相應的文件I/O函數與通信函數,通過了解這些函數的使用,可以編制出符合不同需要的通信程序。與通信設備相關的結構有COMMCONFIG ,COMMPROP,COMMTIMEOUTS,COMSTAT,DCB,MODEMDEVCAPS,MODEMSETTINGS共7個,與通信有關的Windows API函數共有26個,具體說明可參考MSDN幫助文件。下面是打開串口的部分關鍵代碼:
//以創建文件的形式打開文件,並將返回的端口句柄保存於句柄idComDev之中。
idComDev =CreateFile( g_szCom_Port[g_com_config.nPort],
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL );
……
file://cfg為COMMCONFIG結構的實例對象,獲取當前通訊口的狀態。
cfg.dcb.DCBlength = sizeof( DCB ) ;
GetCommState( idComDev, &(cfg.dcb) ) ;
file://設置發送、接收緩存大小
SetupComm( idComDev, 4096, 4096 ) ;
// PurgeComm()是一個清除函數,它可以用來中止任何未決的後台讀或寫,並且可以沖掉I/O
file://緩沖區.其中:PURGE_TXABORT 用於中止後台寫操作;PRUGE_RXABORT用於中止後台
file://讀操作 ;PRUGE_TXCLEAR用於清除發送緩沖區;PRUGE_RXCLEAR用於清除接收緩沖區
PurgeComm(idComDev,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
file://根據設置的參數填充DCB結構對象dcb的各個數據成員變量
dcb.DCBlength = sizeof( DCB ) ;
GetCommState( idComDev, &dcb ) ;
file://設置端口通訊參數
dcb.BaudRate =g_Com_Baud[g_com_config.nBaud];
dcb.ByteSize =g_Com_ByteSize[g_com_config.nData];
dcb.Parity =g_Com_Parity[g_com_config.nParity] ;
dcb.StopBits =g_Com_StopBits[g_com_config.nStop];
file://硬件流控制
dcb.fDtrControl = DTR_CONTROL_DISABLE ;
dcb.fOutxCtsFlow = FALSE ;
dcb.fRtsControl = RTS_CONTROL_DISABLE ;
file://軟件流控制
dcb.fInX = dcb.fOutX = FALSE ;
dcb.XonChar = (char)0xFF ;
dcb.XoffChar = (char)0XFF ;
dcb.XonLim = 100 ;
dcb.XoffLim = 100 ;
dcb.EvtChar=0x0d;
dcb.fBinary = TRUE ;
dcb.fParity = TRUE ;
file://超時控制的設置。超時有兩種:區間超時:(僅對從端口中讀取數據有用)它指定在讀取兩個字符之間要經歷的時間;總超時: 當讀或寫特定的字節數需要的總時間超過某一阈值時,超時觸發。計算超時可以根據公式:
file://ReadTotalTimeout = (ReadTotalTimeoutMultiplIEr * bytes_to_read)+
// ReadToTaltimeoutConstant
file://WriteTotalTimeout = (WriteTotalTimeoutMuliplIEr * bytes_to_write)+
// WritetoTotalTimeoutConstant
file://如果在設置超時時參數為0則為無限等待,即無超時。
CommTimeOuts.ReadIntervalTimeout =MAXDWord;
CommTimeOuts.ReadTotalTimeoutMultiplIEr =0;
CommTimeOuts.ReadTotalTimeoutConstant = 0 ;
CommTimeOuts.WriteTotalTimeoutMultiplIEr =2*9600/dcb.BaudRate ;
CommTimeOuts.WriteTotalTimeoutConstant = 25 ;
SetCommTimeouts(idComDev , &CommTimeOuts ) ;
file://根據設置好的dcb結構設置好通訊口的狀態,並開啟用於偵聽端口,監視從外設傳來的數
file://據的線程COMReadThreadProc。
if (SetCommState( idComDev, &dcb ))
{
m_bComPortOpen=TRUE;
g_hCom=idComDev;
AfxBeginThread(COMReadThreadProc,NULL,THREAD_PRIORITY_NORMAL);
return;
}
BOOL CCOMReadBuf::GetOneByte(BYTE *cb)
{
m_Lock.Lock();
if(m_nHead==m_nTail)
{
m_Lock.Unlock();
return FALSE;//空
}
*cb=m_readbuf[m_nTail];
if(m_nTail < m_nBufSize-1)
m_nTail++;
else
m_nTail=0;
m_Lock.Unlock();
return TRUE;//空
}
void CCOMReadBuf::Add(BYTE buf[],int nBytes)
{
int nt,i;
m_Lock.Lock();
for(i=0;iBR> {
nt=(m_nHead-m_nTail);
if(nt<0)
nt+=m_nBufSize;
if(nt+1==m_nBufSize)
break;//緩沖區滿
m_readbuf[m_nHead]=buf[i];
if(m_nHead < m_nBufSize-1)
m_nHead++;
else
m_nHead=0;
}
m_Lock.Unlock();
}
(四) 控制命令的發送
控制命令可以從對話條上的編輯框獲取,然後就可以通過寫文件形式從端口發送出去,這部分實現起來較簡單,也牽扯不到線程等技術。主要的代碼主要有:
……
file://從對話條獲取命令行
nRead=m_wndDlgBar.GetDlgItemText(IDC_EDIT_SEND,buf,500);
file://向端口發送命令
if(nRead>0)
{
buf[nRead]=0x0d;
buf[nRead+1]=0x00;
::WriteFile(g_hCom,buf,nRead+1,&dwActWrite,NULL);
}
……
(五) 監測信息的顯示
本程序選擇了列表視圖作為數據的顯示途徑。為了能及時的將接收到的數據反饋給監控者,在視類中通過定時器完成定時刷新的功能,可以在視類的OnCreate() 函數裡用SetTimer(……)函數在程序開始執行時打開定時器,在OnDestroy()裡用KillTimer(……)函數在程序退出前先關閉定時器。在定時器消息 WM_TIMER的響應函數裡完成向列表控件添加最新接收到的信息。主要語句有:
……
file://獲取列表視相關的列表空間的句柄
CListCtrl &ListCtrl=GetListCtrl();
file://列表有兩列:收到字符的時間和對應的信息
CTime t = CTime::GetCurrentTime();
CString szTemp;
szTemp.Format("%02d:%02d:%02d",t.GetHour(),t.GetMinute(),t.GetSecond());
file://向列表添加信息
int nIndex=ListCtrl.InsertItem(0, szTemp);
if(-1!=nIndex)
{
m_Buf[m_nCurPoint]=0;
ListCtrl.SetItemText(nIndex,1,LPTSTR(m_Buf));
}
……
四、 調試與檢測
現在程序已經寫完,可以編譯運行。我們最好先檢驗一下機器串口是否能正常工作,可用DOS下的Comdebug程序檢查。在確認串口工作正常後,如果條件允許最好同另一台計算機或外設相連,進行檢測,如筆者用的是一台高頻段數傳電台。如果只有一台計算機也可以進行簡單的測試:將計算機串口的第2腳和第3腳短接,即自己發送、接收數據。如果接有外設,當有采集到的數據送到端口時就會在列表中將時間和信息內容記錄下來,也可以在對話條中輸入命令來控制外設的工作狀態,完全具備實時監控軟件所需的功能。
小結:
串行通訊在通訊領域被廣泛應用,標准的RS-232-C接口已成為計算機、外設、交換機和許多通訊設備的標准接口。計算機與計算機、計算機與外設等都可以通過RS-232-C接口進行方便的連接,以實現監視、控制外設和傳輸數據等目的。對於其他類型的串口通訊程序本文所介紹的方法也是值得借鑒的。