1. 程式人生 > >VC++串列埠通訊程式設計詳解

VC++串列埠通訊程式設計詳解

總結來看串列埠通訊原理,(也可以說大多數通訊原理也是如此)。

通訊首先要有個通訊,可以簡單的把通訊看成一個小桶,傳送方住水桶裡裝水,接收方從水桶中取水。如果你要和對方通訊首先需要將桶蓋開啟,再將水裝入到桶中,這時接收方才能夠從桶中取到水。這裡就存在著一定的問題,

1,如果桶蓋還沒有開啟,傳送方已經發送了。這時接收方再從桶中取水,肯定取的水不對,會不一部分缺失了。解決方式就是讓桶蓋開啟再往其中加水。

2,但是桶蓋何時開啟,傳送方何時傳送,這個不好把握。解決方法:接收方接到資料時,要返回一個應答標誌,告訴傳送方我已經取到資料了,而且是取得到正確資料才應答,否則不理會,繼續取資料。或者一直查詢,直到與傳送方發來的資料一致再停止取資料。
一般的,進行串列埠通訊總有一個是主動方一個是被動方,而且二者傳輸資料時,會有一定的協商好的資料格式,二者傳送接收都按照此資料格式進行。

在工業控制中,工控機(一般都基於Windows平臺)經常需要與智慧儀表通過串列埠進行通訊。串列埠通訊方便易行,應用廣泛。
一般情況下,工控機和各智慧儀表通過RS485匯流排進行通訊。RS485的通訊方式是半雙工的,只能由作為主節點的工控PC機依次輪詢網路上的各智慧控制單元子節點。每次通訊都是由PC機通過串列埠向智慧控制單元釋出命令,智慧控制單元在接收到正確的命令後作出應答。

  在Win32下,可以使用兩種程式設計方式實現串列埠通訊,其一是使用ActiveX控制元件,這種方法程式簡單,但欠靈活。其二是呼叫Windows的API函式,這種方法可以清楚地掌握串列埠通訊的機制,並且自由靈活。本文我們只介紹API串列埠通訊部分。

  串列埠的操作可以有兩種操作方式:同步操作方式和重疊操作方式(又稱為非同步操作方式)。同步操作時,API函式會阻塞直到操作完成以後才能返回(在多執行緒方式中,雖然不會阻塞主執行緒,但是仍然會阻塞監聽執行緒);而重疊操作方式,API函式會立即返回,操作在後臺進行,避免執行緒的阻塞。

       無論那種操作方式,一般都通過四個步驟來完成:

       (1) 開啟串列埠
       (2) 配置串列埠
       (3) 讀寫串列埠
       (4) 關閉串列埠

       1、開啟串列埠


Win32系統把檔案的概念進行了擴充套件。無論是檔案、通訊裝置、命名管道、郵件槽、磁碟、還是控制檯,都是用API函式CreateFile來開啟或建立的。該函式的原型為:

C++程式碼
  1. HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);    

       lpFileName:將要開啟的串列埠邏輯名,如“COM1”;
       dwDesiredAccess:指定串列埠訪問的型別,可以是讀取、寫入或二者並列; 
       dwShareMode:指定共享屬性,由於串列埠不能共享,該引數必須置為0; 
       lpSecurityAttributes:引用安全性屬性結構,預設值為NULL; 
       dwCreationDistribution:建立標誌,對串列埠操作該引數必須置為OPEN_EXISTING; 
       dwFlagsAndAttributes:屬性描述,用於指定該串列埠是否進行非同步操作,該值為FILE_FLAG_OVERLAPPED,表示使用非同步的I/O;該值為0,表示同步I/O操作;
       hTemplateFile:對串列埠而言該引數必須置為NULL。

       同步I/O方式開啟串列埠的示例程式碼:

C++程式碼
  1. HANDLE hCom; //全域性變數,串列埠控制代碼   
  2. hCom=CreateFile("COM1",//COM1口  
  3.  GENERIC_READ|GENERIC_WRITE, //允許讀和寫  
  4.  0, //獨佔方式  
  5.  NULL,  
  6.  OPEN_EXISTING, //開啟而不是建立  
  7.  0, //同步方式  
  8.  NULL);   
  9. if(hCom==(HANDLE)-1)   
  10. {  
  11.    AfxMessageBox("開啟COM失敗!");  
  12.    return FALSE;   
  13. }  
  14. return TRUE;    

       重疊I/O開啟串列埠的示例程式碼:

C++程式碼
  1. HANDLE hCom; //全域性變數,串列埠控制代碼     
  2. hCom =CreateFile("COM1"//COM1口    
  3.  GENERIC_READ|GENERIC_WRITE, //允許讀和寫    
  4.  0, //獨佔方式    
  5.  NULL,    
  6.  OPEN_EXISTING, //開啟而不是建立     
  7.  FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, //重疊方式    
  8.  NULL);     
  9. if(hCom ==INVALID_HANDLE_VALUE)     
  10. {     
  11.    AfxMessageBox("開啟COM失敗!");     
  12.    return FALSE;     
  13. }     
  14. return TRUE;  

       2、配置串列埠

       在開啟通訊裝置控制代碼後,常常需要對串列埠進行一些初始化配置工作。這需要通過一個DCB結構來進行。DCB結構包含了諸如波特率、資料位數、奇偶校驗和停止位數等資訊。在查詢或配置串列埠的屬性時,都要用DCB結構來作為緩衝區。

       一般用CreateFile開啟串列埠後,可以呼叫GetCommState函式來獲取串列埠的初始配置。要修改串列埠的配置,應該先修改DCB結構,然後再呼叫SetCommState函式設定串列埠。
DCB結構包含了串列埠的各項引數設定,下面僅介紹幾個該結構常用的變數:

typedef struct _DCB{ ……… 

DWORD BaudRate;//波特率,指定通訊裝置的傳輸速率。這個成員可以是實際波特率值或者下面的常量值之一:  CBR_110,CBR_300,CBR_600,CBR_1200,CBR_2400,CBR_4800,CBR_9600,CBR_19200, CBR_38400, CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000, CBR_14400

DWORD fParity; // 指定奇偶校驗使能。若此成員為1,允許奇偶校驗檢查 …

BYTE ByteSize; // 通訊位元組位數,4—8

BYTE Parity; //指定奇偶校驗方法。此成員可以有下列值: EVENPARITY 偶校驗 NOPARITY 無校驗 MARKPARITY 標記校驗 ODDPARITY 奇校驗

BYTE StopBits; //指定停止位的位數。此成員可以有下列值: ONESTOPBIT 1位停止位 TWOSTOPBITS 2位停止位
ON 5STOPBITS   1.5位停止位
                                                                                                                                                                      

GetCommState函式可以獲得COM口的裝置控制塊,從而獲得相關引數: 
    BOOL GetCommState( 

            HANDLE hFile, //標識通訊埠的控制代碼 

            LPDCB lpDCB //指向一個裝置控制塊(DCB結構)的指標 ); 

SetCommState函式設定COM口的裝置控制塊: 

    BOOL SetCommState( HANDLE hFile, LPDCB lpDCB ); 
除了在BCD中的設定外,程式一般還需要設定I/O緩衝區的大小和超時。Windows用I/O緩衝區來暫存串列埠輸入和輸出的資料。如果通訊的速率較高,則應該設定較大的緩衝區。呼叫SetupComm函式可以設定序列口的輸入和輸出緩衝區的大小。
BOOL SetupComm( HANDLE hFile, // 通訊裝置的控制代碼 

            DWORD dwInQueue, // 輸入緩衝區的大小(位元組數) 

            DWORD dwOutQueue // 輸出緩衝區的大小(位元組數) ); 
在用ReadFile和WriteFile讀寫序列口時,需要考慮超時問題。超時的作用是在指定的時間內沒有讀入或傳送指定數量的字元,ReadFile或WriteFile的操作仍然會結束。
要查詢當前的超時設定應呼叫GetCommTimeouts函式,該函式會填充一個COMMTIMEOUTS結構。呼叫SetCommTimeouts可以用某一個COMMTIMEOUTS結構的內容來設定超時。
讀寫串列埠的超時有兩種:間隔超時和總超時。間隔超時是指在接收時兩個字元之間的最大時延。總超時是指讀寫操作總共花費的最大時間。寫操作只支援總超時,而讀操作兩種超時均支援。用COMMTIMEOUTS結構可以規定讀寫操作的超時。
COMMTIMEOUTS結構的定義為: 
    typedef struct _COMMTIMEOUTS { 

         DWORD ReadIntervalTimeout; //讀間隔超時

         DWORD ReadTotalTimeoutMultiplier; //讀時間係數

         DWORD ReadTotalTimeoutConstant; //讀時間常量

         DWORD WriteTotalTimeoutMultiplier; // 寫時間係數

         DWORD WriteTotalTimeoutConstant; //寫時間常量

} COMMTIMEOUTS,*LPCOMMTIMEOUTS; 
COMMTIMEOUTS結構的成員都以毫秒為單位。

總超時的計算公式是:總超時=時間係數×要求讀/寫的字元數+時間常量
例如,要讀入10個字元,那麼讀操作的總超時的計算公式為:
讀總超時=ReadTotalTimeoutMultiplier×10+ReadTotalTimeoutConstant
可以看出:間隔超時和總超時的設定是不相關的,這可以方便通訊程式靈活地設定各種超時。

    如果所有寫超時引數均為0,那麼就不使用寫超時。如果ReadIntervalTimeout為0,那麼就不使用讀間隔超時。如果ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 都為0,則不使用讀總超時。如果讀間隔超時被設定成MAXDWORD並且讀時間係數和讀時間常量都為0,那麼在讀一次輸入緩衝區的內容後讀操作就立即返回,而不管是否讀入了要求的字元。
    在用重疊方式讀寫串列埠時,雖然ReadFile和WriteFile在完成操作以前就可能返回,但超時仍然是起作用的。在這種情況下,超時規定的是操作的完成時間,而不是ReadFile和WriteFile的返回時間。
配置串列埠的示例程式碼: 
    SetupComm(hCom,1024,1024); //輸入緩衝區和輸出緩衝區的大小都是1024 

    COMMTIMEOUTS TimeOuts; //設定讀超時

    TimeOuts.ReadIntervalTimeout=1000;

    TimeOuts.ReadTotalTimeoutMultiplier=500;

    TimeOuts.ReadTotalTimeoutConstant=5000; //設定寫超時

    TimeOuts.WriteTotalTimeoutMultiplier=500;

    TimeOuts.WriteTotalTimeoutConstant=2000;

    SetCommTimeouts(hCom,&TimeOuts); //設定超時

    DCB dcb; 

    GetCommState(hCom,&dcb);

    dcb.BaudRate=9600; //波特率為9600

    dcb.ByteSize=8; //每個位元組有8位

    dcb.Parity=NOPARITY; //無奇偶校驗位

    dcb.StopBits=TWOSTOPBITS; //兩個停止位

    SetCommState(hCom,&dcb);

    PurgeComm(hCom,PURGE_TXCLEAR|PURGE_RXCLEAR); 
在讀寫串列埠之前,還要用PurgeComm()函式清空緩衝區,該函式原型: 
    BOOL PurgeComm( HANDLE hFile, //串列埠控制代碼 

                DWORD dwFlags // 需要完成的操作 ); 
引數dwFlags指定要完成的操作,可以是下列值的組合: 
    PURGE_TXABORT 中斷所有寫操作並立即返回,即使寫操作還沒有完成。 

    PURGE_RXABORT 中斷所有讀操作並立即返回,即使讀操作還沒有完成。 

    PURGE_TXCLEAR 清除輸出緩衝區   

    PURGE_RXCLEAR 清除輸入緩衝區 

       3、讀寫串列埠

       我們使用ReadFile和WriteFile讀寫串列埠,下面是兩個函式的宣告:

BOOL ReadFile( HANDLE hFile, //串列埠的控制代碼 

// 讀入的資料儲存的地址, 

// 即讀入的資料將儲存在以該指標的值為首地址的一片記憶體區

LPVOID lpBuffer, 

// 要讀入的資料的位元組數 

DWORD nNumberOfBytesToRead, 

// 指向一個DWORD數值,該數值返回讀操作實際讀入的位元組數

LPDWORD lpNumberOfBytesRead, 

// 重疊操作時,該引數指向一個OVERLAPPED結構,同步操作時,該引數為NULL。

LPOVERLAPPED lpOverlapped );

BOOL WriteFile( HANDLE hFile, //串列埠的控制代碼 

// 寫入的資料儲存的地址, 

// 即以該指標的值為首地址的

LPCVOID lpBuffer,
//要寫入的資料的位元組數
DWORD nNumberOfBytesToWrite,

// 指向指向一個DWORD數值,該數值返回實際寫入的位元組數

LPDWORD lpNumberOfBytesWritten,  

// 重疊操作時,該引數指向一個OVERLAPPED結構,

// 同步操作時,該引數為NULL。

LPOVERLAPPED lpOverlapped ); 

在用ReadFile和WriteFile讀寫串列埠時,既可以同步執行,也可以重疊執行。在同步執行時,函式直到操作完成後才返回。這意味著同步執行時執行緒會被阻塞,從而導致效率下降。在重疊執行時,即使操作還未完成,這兩個函式也會立即返回,費時的I/O操作在後臺進行。
ReadFile和WriteFile函式是同步還是非同步由CreateFile函式決定,如果在呼叫CreateFile建立控制代碼時指定了FILE_FLAG_OVERLAPPED標誌,那麼呼叫ReadFile和WriteFile對該控制代碼進行的操作就應該是重疊的;如果未指定重疊標誌,則讀寫操作應該是同步的。ReadFile和WriteFile函式的同步或者非同步應該和CreateFile函式相一致。
ReadFile函式只要在串列埠輸入緩衝區中讀入指定數量的字元,就算完成操作。而WriteFile函式不但要把指定數量的字元拷入到輸出緩衝區,而且要等這些字元從序列口送出去後才算完成操作。
如果操作成功,這兩個函式都返回TRUE。需要注意的是,當ReadFile和WriteFile返回FALSE時,不一定就是操作失敗,執行緒應該呼叫GetLastError函式分析返回的結果。例如,在重疊操作時如果操作還未完成函式就返回,那麼函式就返回FALSE,而且GetLastError函式返回ERROR_IO_PENDING。這說明重疊操作還未完成。

同步方式讀寫串列埠比較簡單,下面先例舉同步方式讀寫串列埠的程式碼: 
//同步讀串列埠 

char str[100]; 

DWORD wCount;//讀取的位元組數

BOOL bReadStat; 

bReadStat=ReadFile(hCom,str,100,&wCount,NULL);

if(!bReadStat) { AfxMessageBox("讀串列埠失敗!"); return FALSE; } return TRUE; //同步寫串列埠

char lpOutBuffer[100];

DWORD dwBytesWrite=100;

COMSTAT ComStat; 

DWORD dwErrorFlags; 

BOOL bWriteStat; 

ClearCommError(hCom,&dwErrorFlags,&ComStat);

bWriteStat=WriteFile(hCom,lpOutBuffer,dwBytesWrite,& dwBytesWrite,NULL);

if(!bWriteStat) { AfxMessageBox("寫串列埠失敗!"); }

PurgeComm(hCom, PURGE_TXABORT| PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR); 

在重疊操作時,操作還未完成函式就返回。

重疊I/O非常靈活,它也可以實現阻塞(例如我們可以設定一定要讀取到一個數據才能進行到下一步操作)。有兩種方法可以等待操作完成:一種方法是用象WaitForSingleObject這樣的等待函式來等待OVERLAPPED結構的hEvent成員;另一種方法是呼叫GetOverlappedResult函式等待,後面將演示說明。
下面我們先簡單說一下OVERLAPPED結構和GetOverlappedResult函式:
OVERLAPPED結構
OVERLAPPED結構包含了重疊I/O的一些資訊,定義如下: 
typedef struct _OVERLAPPED { // o 

DWORD Internal; 

DWORD InternalHigh; 

DWORD Offset; 

DWORD OffsetHigh; 

HANDLE hEvent; 

} OVERLAPPED; 
在使用ReadFile和WriteFile重疊操作時,執行緒需要建立OVERLAPPED結構以供這兩個函式使用。執行緒通過OVERLAPPED結構獲得當前的操作狀態,該結構最重要的成員是hEvent。hEvent是讀寫事件。當串列埠使用非同步通訊時,函式返回時操作可能還沒有完成,程式可以通過檢查該事件得知是否讀寫完畢。
當呼叫ReadFile, WriteFile 函式的時候,該成員會自動被置為無訊號狀態;當重疊操作完成後,該成員變數會自動被置為有訊號狀態。 
GetOverlappedResult函式 BOOL GetOverlappedResult( HANDLE hFile, // 串列埠的控制代碼 // 指向重疊操作開始時指定的OVERLAPPED結構 LPOVERLAPPED lpOverlapped, // 指向一個32位變數,該變數的值返回實際讀寫操作傳輸的位元組數。 LPDWORD lpNumberOfBytesTransferred, // 該引數用於指定函式是否一直等到重疊操作結束。 // 如果該引數為TRUE,函式直到操作結束才返回。 // 如果該引數為FALSE,函式直接返回,這時如果操作沒有完成, // 通過呼叫GetLastError()函式會返回ERROR_IO_INCOMPLETE。 BOOL bWait );  
該函式返回重疊操作的結果,用來判斷非同步操作是否完成,它是通過判斷OVERLAPPED結構中的hEvent是否被置位來實現的。

非同步讀串列埠的示例程式碼:

char lpInBuffer[1024]; 

DWORD dwBytesRead=1024; 

COMSTAT ComStat; 

DWORD dwErrorFlags; 

OVERLAPPED m_osRead; 

memset(&m_osRead,0,sizeof(OVERLAPPED)); 

m_osRead.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL); 

ClearCommError(hCom,&dwErrorFlags,&ComStat); 

dwBytesRead=min(dwBytesRead,(DWORD)ComStat.cbInQue); 

if(!dwBytesRead) return FALSE; 

BOOL bReadStatus; 

bReadStatus=ReadFile(hCom,lpInBuffer, dwBytesRead,&dwBytesRead,&m_osRead);

if(!bReadStatus) 

//如果ReadFile函式返回FALSE 


    if(GetLastError()==ERROR_IO_PENDING) 

    //GetLastError()函式返回ERROR_IO_PENDING,表明串列埠正在進行讀操作 

    { 

        WaitForSingleObject(m_osRead.hEvent,2000); 

        //使用WaitForSingleObject函式等待,直到讀操作完成或延時已達到2秒鐘 

        //當串列埠讀操作進行完畢後,m_osRead的hEvent事件會變為有訊號 

        PurgeComm(hCom, PURGE_TXABORT| PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR); 

        return dwBytesRead; 

    }

    return 0; 

}

PurgeComm(hCom, PURGE_TXABORT| PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR); 

return dwBytesRead; 
對以上程式碼再作簡要說明:

在使用ReadFile 函式進行讀操作前,應先使用ClearCommError函式清除錯誤。

ClearCommError函式的原型如下: 
BOOL ClearCommError( HANDLE hFile, // 串列埠控制代碼 

LPDWORD lpErrors, // 指向接收錯誤碼的變數 

LPCOMSTAT lpStat // 指向通訊狀態緩衝區 ); 
該函式獲得通訊錯誤並報告串列埠的當前狀態,同時,該函式清除串列埠的錯誤標誌以便繼續輸入、輸出操作。
引數lpStat指向一個COMSTAT結構,該結構返回串列埠狀態資訊。 

COMSTAT結構 COMSTAT結構包含串列埠的資訊,結構定義如下: 
typedef struct _COMSTAT { // cst DWORD fCtsHold : 1; // Tx waiting for CTS signal DWORD fDsrHold : 1; // Tx waiting for DSR signal DWORD fRlsdHold : 1; // Tx waiting for RLSD signal DWORD fXoffHold : 1; // Tx waiting, XOFF char rec''d DWORD fXoffSent : 1; // Tx waiting, XOFF char sent DWORD fEof : 1; // EOF character sent DWORD fTxim : 1; // character waiting for Tx DWORD fReserved : 25; // reserved DWORD cbInQue; // bytes in input buffer DWORD cbOutQue; // bytes in output buffer } COMSTAT, *LPCOMSTAT; 
本文只用到了cbInQue成員變數,該成員變數的值代表輸入緩衝區的位元組數。

最後用PurgeComm函式清空串列埠的輸入輸出緩衝區。 
這段程式碼用WaitForSingleObject函式來等待OVERLAPPED結構的hEvent成員,下面我們再演示一段呼叫GetOverlappedResult函式等待的非同步讀串列埠示例程式碼:

char lpInBuffer[1024]; 

DWORD dwBytesRead=1024; 

BOOL bReadStatus; 

DWORD dwErrorFlags;

COMSTAT ComStat; 

OVERLAPPED m_osRead; 

ClearCommError(hCom,&dwErrorFlags,&ComStat); 

if(!ComStat.cbInQue) return 0; 

dwBytesRead=min(dwBytesRead,(DWORD)ComStat.cbInQue); 

bReadStatus=ReadFile(hCom, lpInBuffer,dwBytesRead, &dwBytesRead,&m_osRead); 

if(!bReadStatus) //如果ReadFile函式返回FALSE 

{ if(GetLastError()==ERROR_IO_PENDING) 

{ GetOverlappedResult(hCom, &m_osRead,&dwBytesRead,TRUE); 

// GetOverlappedResult函式的最後一個引數設為TRUE, 

//函式會一直等待,直到讀操作完成或由於錯誤而返回。 

return dwBytesRead; } 

return 0; } 

return dwBytesRead;  
非同步寫串列埠的示例程式碼: 
char buffer[1024]; 

DWORD dwBytesWritten=1024; 

DWORD dwErrorFlags; 

COMSTAT ComStat; 

OVERLAPPED m_osWrite; 

BOOL bWriteStat; 

bWriteStat=WriteFile(hCom,buffer,dwBytesWritten, &dwBytesWritten,&m_OsWrite); 

if(!bWriteStat)

{ if(GetLastError()==ERROR_IO_PENDING) 

{ WaitForSingleObject(m_osWrite.hEvent,1000); 

return dwBytesWritten; } 

return 0; } 

return dwBytesWritten; 

       4、關閉串列埠

       利用API函式關閉串列埠非常簡單,只需使用CreateFile函式返回的控制代碼作為引數呼叫CloseHandle即可:

BOOL CloseHandle(
    HANDLE hObject; //handle to object to close
);