C++編寫串列埠通訊程式
宣告:本部落格的內容主要是本人學習其他串列埠通訊部落格之後的總結,主要參考的部落格地址如下:
串列埠通訊一般分為同步和非同步兩種方式,本部落格主要講述非同步通訊程式的編寫,其程式設計步驟主要分為四步驟:
一、開啟串列埠
Win32系統把檔案的概念進行了擴充套件。無論是檔案、通訊裝置、命名管道、郵件槽、磁碟、還是控制檯,都是用API函式CreateFile來開啟或建立的。本程式串列埠類中開啟串列埠的函式定義如下:
bool My_Com::Open_Com(LPCTSTR Port) { hCom = CreateFile( Port, //將要開啟的串列埠邏輯名 GENERIC_READ | GENERIC_WRITE, //允許讀和寫 0, //指定共享屬性,由於串列埠不能共享,該引數必須置為0,獨佔方式 NULL,//引用安全性屬性結構,預設值為NULL OPEN_EXISTING, /建立標誌,對串列埠操作該引數必須置為OPEN_EXISTING FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //屬性描述,此處指定該串列埠非同步 NULL //對串列埠而言該引數必須置為NULL); if (hCom == INVALID_HANDLE_VALUE) { printf ("開啟串列埠失敗!\n"); return FALSE; } else { printf("開啟串列埠成功!\n"); } return TRUE; }
注意:FILE_ATTRIBUTE_NORMAL 和 FILE_FLAG_OVERLAPPED 均代表非同步通訊。也可以借用CreateFile函式加上for迴圈實現對外設驅動的掃描以自動找出插入的串列埠號,其程式碼實現如下:
GetCom() { MyComm.hCom = INVALID_HANDLE_VALUE; BOOL ret = false; int j = 0; //逐一進行COM 埠檢測 printf("正在搜尋當前可用埠...\n"); for (int i = 1; i <= 10; i++) { string comname = "COM" + to_string(i); MyComm.hCom = CreateFile(stringToLPCWSTR(comname), // 開啟串列埠 GENERIC_READ | GENERIC_WRITE, //讀寫方式 0, //不能共享 NULL, //安全屬性,一般不用設為NULL OPEN_EXISTING, //開啟已存在的裝置 FILE_ATTRIBUTE_NORMAL, //普通檔案屬性 NULL); //無模板 if (MyComm.hCom != INVALID_HANDLE_VALUE) { j++; printf("埠 COM %d 可用\n", i); ret = CloseHandle(MyComm.hCom); if (!ret) printf("關閉串列埠失敗!!"); } } if (!j) { printf("無可用埠!!\n"); return FALSE; } else return TRUE; }
二、配置串列埠
本部分主要是一些初始化配置:
1、DCB結構相關引數的配置(波特率、資料位數、奇偶校驗和停止位數等資訊),呼叫GetCommState函式獲取串列埠的初始配置,然後先修改DCB結構,再呼叫SetCommState函式設定串列埠。
2、COMMTIMEOUTS結構串列埠讀寫超時引數設定(單位:毫秒;若設定為0,表示該引數不起作用)
ReadIntervalTimeout:兩字元之間最大的延時,當讀取串列埠資料時,一旦兩個字元傳輸的時間差超過該時間,讀取函式將返回現有的資料。在ReadFile操作期間,時間週期從第一個字元接收到算起。如果收到的兩個字元之間的間隔超過該值,ReadFile操作完畢並返回所有緩衝資料。如果值為MAXDWORD,並且ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier兩個值都為0, 則指定讀操作攜帶已經收到的字元立即返回,即使沒有收到任何字元。如果ReadIntervalTimeout為0,則該值不起作用。
ReadTotalTimeoutMultiplier:讀取每字元間的超時。 指定累積值,用於計算讀操作時的超時總數。對於每次讀操作,該值與所要讀的位元組數相乘。
ReadTotalTimeoutConstant:一次讀取串列埠資料的固定超時。在一次讀取串列埠的操作中,其超時為 ReadTotalTimeoutMultiplier乘以讀取的位元組數再加上 ReadTotalTimeoutConstant。將ReadIntervalTimeout設定為MAXDWORD,並將 ReadTotalTimeoutMultiplier 和ReadTotalTimeoutConstant設定為0,表示讀取操作將立即返回存放在輸入緩衝區的字元。
WriteTotalTimeoutMultiplier:寫入每字元間的超時。
WriteTotalTimeoutConstant:一次寫入串列埠資料的固定超時。在一次寫入串列埠的操作中,其超時為WriteTotalTimeoutMultiplier乘以寫入的位元組數再加上 WriteTotalTimeoutConstant。
間隔超時=ReadIntervalTimeout
總超時 = ReadTotalTimeoutMultiplier * 位元組數 + ReadTotalTimeoutConstant
這裡一串列埠讀取事件為例進行詳細的分析,其過程分兩個階段:
第一個階段是:串列埠執行到ReadFile函式時,串列埠還沒有開始傳輸資料,所以串列埠緩衝區的第一個位元組是沒有裝資料的,這時候總超時起作用,如果在總超時時間內沒有進行串列埠資料的傳輸,ReadFile函式就返回,當然 沒有讀取到任何資料。而且,間隔超時並沒有起作用。
第二階段:假設總超時為20秒,程式執行到ReadFile,總超時開始從0 計時,如果在計時到達10秒時,串列埠開始了資料的傳輸,那麼從接收的第一個位元組開始,間隔超時就開始計時,假如間隔超時為1ms,那麼在讀取完第一個位元組後,串列埠開始等待1ms,如果1ms之內接收到了第二個位元組,就讀取第二個位元組,間隔超時重置為0並計時,等待第三個位元組的到來,如果第三個位元組到來的時間超過了1ms,那麼ReadFile函式立即返回,這時候總超時計時是沒到20秒的。如果在20秒總計時時間結束之前,所有的資料都遵守資料間隔為1ms的約定並陸陸續續的到達串列埠緩衝區,那麼就成功進行了一次串列埠傳輸和讀取;如果20秒總計時時間到,串列埠還陸陸續續的有資料到達,即使遵守位元組間隔為1ms的約定,ReadFile函式也會立即返回,這時候總超時就起作用了。
總結起來,總超時在兩種情況下起作用:一是串列埠沒進行資料傳輸,等待總超時時間那麼長ReadFile才返回(即非正常資料傳輸);二是資料太長,總超時設定太短,資料還沒讀取完就返回了(讀取的資料是不全的)。間隔超時觸發的條件:在總超時時間內且串列埠進行了資料的傳輸。
3、利用SetUpComm、PurgeComm兩個函式分別設定輸出輸出緩衝區的大小並進行清空處理
SetupComm引數解釋:dwInQueue指定輸入緩衝區的大小(位元組);dwOutQueue指定輸出緩衝區的大小(位元組)。
PurgeComm引數解釋:PURGE_TXABORT 終止所有正在進行的字元輸出操作,PURGE_RXABORT 終止所有正在進行的字元。輸入操作PURGE_TXCLEAR 裝置驅動程式清除輸出緩衝,PURGE_RXCLEAR 裝置驅動程式清除輸入緩衝區。
4、利用CreateEvent函式建立讀寫及等待的操作事件,用於讀寫函式中做判斷使用。
5、利用SetCommMask函式設定要監控的事件,而WaitCommEvent函式是等待串列埠通訊事件的發生放在讀函式中。
6、建立讀取執行緒。
bool My_Com::Config_Com()
{
SetupComm(hCom, 1024, 1024); //輸入緩衝區和輸出緩衝區的大小都是1024
DCB dcb;
GetCommState(hCom, &dcb);
dcb.BaudRate = 9600; //波特率為9600
dcb.ByteSize = 8; //每個位元組有8位
dcb.Parity = NOPARITY; //無奇偶校驗位
dcb.StopBits = TWOSTOPBITS; //兩個停止位
SetCommState(hCom, &dcb);
COMMTIMEOUTS TimeOuts; //設定讀超時
TimeOuts.ReadIntervalTimeout = MAXDWORD;
TimeOuts.ReadTotalTimeoutMultiplier = 0;
TimeOuts.ReadTotalTimeoutConstant = 0; //設定寫超時
TimeOuts.WriteTotalTimeoutMultiplier = 500;
TimeOuts.WriteTotalTimeoutConstant = 2000;
SetCommTimeouts(hCom, &TimeOuts); //設定超時
PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);
m_ovRead.hEvent = CreateEvent(NULL, false, false, NULL);
m_ovWrite.hEvent = CreateEvent(NULL, false, false, NULL);
m_ovWait.hEvent = CreateEvent(NULL, false, false, NULL);
//SetCommMask設定要監控的事件
//EV_RXCHAR:輸入緩衝區中已收到資料,即接收到一個位元組並放入輸入緩衝區。
//EV_ERR:線路狀態錯誤,包括了CE_FRAME / CE_OVERRUN / CE_RXPARITY 3種錯誤。
SetCommMask(hCom, EV_ERR | EV_RXCHAR);
//_beginThreadex建立讀取執行緒
m_Thread = (HANDLE)_beginthreadex(NULL, 0, &My_Com::ComRecv, this, 0, NULL);
m_IsOpen = true;
return TRUE;
}
三、串列埠讀寫
寫函式ComWrite(傳送資料)中主要呼叫了WriteFile函式引數:HANDLE
hFile檔案控制代碼,LPCVOID
lpBuffer資料快取區指標DWORD
nNumberOfBytesToWrite你要寫的位元組數,LPDWORD
lpNumberOfBytesWritten用於儲存實際寫入位元組數的儲存區域的指標LPOVERLAPPED lpOverlappedOVERLAPPED結構體指標
bool My_Com::ComWrite(LPBYTE buf, int &len)
{
BOOL rtn = FALSE;
DWORD WriteSize = 0; //DWORD 代表 unsigned long
PurgeComm(hCom, PURGE_TXCLEAR | PURGE_TXABORT);
m_ovWait.Offset = 0;
rtn = WriteFile(hCom, buf, len, &WriteSize, &m_ovWrite);
if (FALSE == rtn && GetLastError() == ERROR_IO_PENDING)//後臺讀取
{
//等待資料寫入完成
printf("已傳送 :");
for (int i = 0; i < len; i++)
printf("%d ", buf[i]);
printf("\n");
}
return rtn != FALSE;
}
讀函式ComRecv(接收資料)的解釋已在程式碼區詳細備註,其函式定義如下:
unsigned int __stdcall My_Com::ComRecv(void* LPParam)
{
My_Com *obj = static_cast<My_Com*>(LPParam);
DWORD WaitEvent = 0, Bytes = 0;
BOOL Status = FALSE;
BYTE ReadBuf[4096];
DWORD Error;
COMSTAT cs = { 0 };
int i;
while (obj->m_IsOpen)
{
WaitEvent = 0;
obj->m_ovWait.Offset = 0;
Status = WaitCommEvent(obj->hCom, &WaitEvent, &obj->m_ovWait);
/*
WaitCommEvent等待串列埠通訊事件的發生
用途:用來判斷用SetCommMask()函式設定的串列埠通訊事件是否已發生。
原型:BOOL WaitCommEvent(HANDLE hFile,LPDWORD lpEvtMask,LPOVERLAPPED lpOverlapped);
引數說明:
-hFile:串列埠控制代碼
-lpEvtMask:函式執行完後如果檢測到串列埠通訊事件的話就將其寫入該引數中。
-lpOverlapped:非同步結構,用來儲存非同步操作結果。
*/
//GetLastError()函式返回ERROR_IO_PENDING,表明串列埠正在進行讀操作
if (FALSE == Status && GetLastError() == ERROR_IO_PENDING)
{
// GetOverlappedResult函式的最後一個引數設為TRUE,函式會一直等待,直到讀操作完成或由於錯誤而返回。
Status = GetOverlappedResult(obj->hCom, &obj->m_ovWait, &Bytes, TRUE);
}
//在使用ReadFile 函式進行讀操作前,應先使用ClearCommError函式清除錯誤。
ClearCommError(obj->hCom, &Error, &cs);
if (TRUE == Status //等待事件成功
&& WaitEvent&EV_RXCHAR//快取中有資料到達
&& cs.cbInQue > 0)//有資料
{
Bytes = 0;
obj->m_ovRead.Offset = 0;
memset(ReadBuf, 0, sizeof(ReadBuf));
/*
BOOL ReadFile(
HANDLE hFile, //串列埠的控制代碼
LPVOID lpBuffer,// 讀入的資料儲存的地址,即讀入的資料將儲存在以該指標的值為首地址的一片記憶體區
DWORD nNumberOfBytesToRead,// 要讀入的資料的位元組數
LPDWORD lpNumberOfBytesRead,// 指向一個DWORD數值,該數值返回讀操作實際讀入的位元組數
LPOVERLAPPED lpOverlapped // 重疊操作時,該引數指向一個OVERLAPPED結構,同步操作時,該引數為NULL
);
*/
Status = ReadFile(obj->hCom, &ReadBuf, 4096, &Bytes, &obj->m_ovRead);
if (Status != FALSE)
{
printf("收到 :");
for (i = 0; i < Bytes; i++)
{
printf("%d ", ReadBuf[i]);
}
printf("\n");
}
//PurgeComm函式清空串列埠的輸入輸出緩衝區
PurgeComm(obj->hCom, PURGE_RXCLEAR | PURGE_RXABORT);
}
}
return 0;
}
四、關閉串列埠
本部分主要編寫了關閉串列埠的函式體,其被串列埠類的解構函式呼叫,實現各開啟功能的關閉。若只是關閉開啟的串列埠號,可直接呼叫底層程式碼提供的CloseHandle函式。
void My_Com::Close_Com()
{
m_IsOpen = false;
if (INVALID_HANDLE_VALUE != hCom)
{
CloseHandle(hCom);
hCom = INVALID_HANDLE_VALUE;
}
if (NULL != m_ovRead.hEvent)
{
CloseHandle(m_ovRead.hEvent);
m_ovRead.hEvent = NULL;
}
if (NULL != m_ovWrite.hEvent)
{
CloseHandle(m_ovWrite.hEvent);
m_ovWrite.hEvent = NULL;
}
if (NULL != m_ovWait.hEvent)
{
CloseHandle(m_ovWait.hEvent);
m_ovWait.hEvent = NULL;
}
if (NULL != m_Thread)
{
WaitForSingleObject(m_Thread, 5000);//等待執行緒結束
CloseHandle(m_Thread);
m_Thread = NULL;
}
}