VS2013 Windows API 串列埠通訊程式設計--多執行緒非同步方式
轉載自:blog.csdn.net/mingojiang
1.串列埠通訊基礎
提到串列埠讓人想起並口,它們是計算機中兩個比較重要的通訊方式. 串列埠:也叫COM口,把位元組的二進位制位按位列隊進行傳輸,每個位元組佔一個固定的時間長,速度慢,但是傳輸距離遠, 有9針和25針兩種,是陽插座(插座中有針凸起),目前25針較少使用;Modem\滑鼠\USB口\老式攝像頭等都是用串列埠. 並口:把位元組的二進位制位用多條線同時傳輸,速度快串列埠8倍左右,傳輸距離有限,一般計算機內部資料傳輸用此方式, 平常使用的有印表機,掃描器等;為25針,陰插座(插座有25個針孔).1.1串列埠通訊原理與特點
串列埠是
序列通訊有成本低的特點,而且可以在現有的電話網路上進行傳輸,家庭通過電話線上網即是這種方式.只要配置一個相應的通訊介面,如:Modem.
1.2串列埠通訊的傳輸方式
單工:只能從一頭傳輸到另一頭,如只能從A向B傳或者B向A傳,如看電視,只允許電視臺向電視發資料,不允許電視向電視臺發資料.在單工傳輸方式上一般採用兩個通訊,一個通道傳輸資料,一個通道傳輸控制訊號.
半雙工:允許互傳資訊,但是不能同時進行,如對講機,A說話時,B不能說話,B
全雙工:允許雙同時通訊,如講電話.
1.3串列埠通訊的同步技術
物理連線建立後,需要使用一種機制使對方正確解釋傳送的資料,傳送方安位發出資料後,接收方如何識別這些資料,並如何正確組裝成正確的位元組.這就需要同步技術.資料同步技術一般解決如下問題:
?確定傳送資料起始時間
?傳送資料的傳輸速率
?傳送資料所需的時間
?傳送時間間隔
3.1非同步傳輸
按位元組為單位傳輸,非同步傳輸方式也叫起止方式,在被傳輸的位元組前後加起止位,起止位無訊號時處於高電平,接收方檢測到低電平訊號表示開始接收,收到停止訊號表示傳輸完成.
3.2同步傳輸
以資料塊為單位傳輸,在塊的前後加一個特殊位元組表示起止
1.4序列介面標準
常用標準有RS-232C,RS-485,RS-422等,其中RS-232C被廣泛用於計算機串列埠通訊.RS-232C標準要求一般線路不要超過15米.
2.API函式實現串列埠通訊
API函式串列埠程式設計,可採用簡單的查詢方式或定時方式,也可採用複雜的事件驅動方式,所謂事件驅動方式是當輸入緩衝區中有資料時,將自動呼叫某個方法執行相應的操作.定時方式是在一定的時間間隔內判斷緩衝區內有資料被寫入,此方法效率不高,查詢方式就更落後的一種方式.所以設計的好的串列埠通訊程式一般用事件驅動,有實時,高效,靈活等特點.
一般編制序列通訊程式分以下幾個部分:
?開啟串列埠:開啟通訊資源,設定通訊引數、設定通訊事件、建立讀、寫事件、進入等待串列埠訊息迴圈。
?讀取串列埠資訊:當串列埠發生EV_RXCHAR(接收到字元並放入了輸入緩衝區)訊息後讀取串列埠、資料傳輸錯誤處理、字串處理如回車符、空格並相應轉化成資料,如果模擬量還要進行資料檢驗等功能。
?寫串列埠資訊:將要傳送的資訊寫入串列埠,相應進行錯誤處理。
?斷開串列埠連線:關閉事件,清除通訊事件,丟棄通訊資源並關閉。
2.1開啟串列埠
2.1.1串列埠是否有驅動
如何判斷PC機中串列埠是否正常,驅動是否安裝,串列埠名(邏輯埠名)是多少.如果PC機有串列埠同時驅動正常,那麼在登錄檔的HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP目錄下,包含字元"Serial"或"VCom"項下面的值就是,可以有多項,如下圖:
項SERIALCOMM下有一個值----COM11,表明有一個可用串列埠,如果目錄下包含字元Serial或VCom的項下沒有任何值,表明沒有串列埠或者驅動不正常.以下是獲取串列埠邏輯名的程式碼:
#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383
HKEY hTestKey;
if (ERROR_SUCCESS == RegOpenKeyEx(HKEY_LOCAL_MACHINE,
TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"), 0, KEY_READ, &hTestKey))
{
TCHAR achClass[MAX_PATH] = TEXT(""); // buffer for class name
DWORD cchClassName = MAX_PATH; // size of class string
DWORD cSubKeys = 0; // number of subkeys
DWORD cbMaxSubKey; // longest subkey size
DWORD cchMaxClass; // longest class string
DWORD cValues; // number of values for key
DWORD cchMaxValue; // longest value name
DWORD cbMaxValueData; // longest value data
DWORD cbSecurityDescriptor; // size of security descriptor
FILETIME ftLastWriteTime; // last write time
DWORD i, retCode;
TCHAR achValue[MAX_VALUE_NAME];
DWORD cchValue = MAX_VALUE_NAME;
// Get the class name and the value count.
retCode = RegQueryInfoKey(
hKey, // key handle
achClass, // buffer for class name
&cchClassName, // size of class string
NULL, // reserved
&cSubKeys, // number of subkeys
&cbMaxSubKey, // longest subkey size
&cchMaxClass, // longest class string
&cValues, // number of values for this key
&cchMaxValue, // longest value name
&cbMaxValueData, // longest value data
&cbSecurityDescriptor, // security descriptor
&ftLastWriteTime); // last write time
if (cValues > 0) {
for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++)
{
cchValue = MAX_VALUE_NAME; achValue[0] = '\0';
if (ERROR_SUCCESS == RegEnumValue(hKey, i, achValue, &cchValue, NULL, NULL, NULL, NULL)) {
CString szName(achValue);
if (-1 != szName.Find(_T("Serial")) || -1 != szName.Find(_T("VCom"))){
BYTE strDSName[10]; memset(strDSName, 0, 10);
DWORD nValueType = 0, nBuffLen = 10;
if (ERROR_SUCCESS == RegQueryValueEx(hKey, (LPCTSTR)achValue, NULL,
&nValueType, strDSName, &nBuffLen)){
int nIndex = -1;
while (++nIndex < 20){
if (-1 == m_nComArray[nIndex]) {
m_nComArray[nIndex] = atoi((char*)(strDSName + 3));
break;
}
}
}
}
}
}
}
else{
AfxMessageBox(_T("機PC機沒有COM口....."));
}
}
RegCloseKey(hTestKey);
2.1.2連線串列埠
串列埠是系統資源,也當作檔案一樣操作,所以也用CreateFile函式,如果呼叫成功返回串列埠控制代碼,如果失敗返回INVALID_HANDLE_VALUE值.函式引數說明如下:
HANDLE WINAPI CreateFile
(
__in LPCTSTR lpFileName,//串列埠名(邏輯埠名),如:”COM1”,”COM2”
__in DWORD dwDesiredAccess,//訪問模式,對串列埠有讀/寫許可權
__in DWORD dwShareMode,//共享模式,有讀/寫/刪除共享,對串列埠通訊只能獨佔模式
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,//檔案安全屬性,對串列埠設定為NULL
__in DWORD dwCreationDisposition,//建立方式,串列埠通訊設定為OPEN_EXISTING
__in DWORD dwFlagsAndAttributes,//檔案屬性標記,是否非同步方式,在些設定
__in_opt HANDLE hTemplateFile//檔案控制代碼,如果不為NULL,新檔案從該夠本複製或擴充套件,如果函式執行成功,返回新的串列埠控制代碼.
);
?第一個引數:邏輯串列埠號,用字串”COMX”表示,”X”是串列埠序號,關於電腦中的邏輯串列埠號,在登錄檔HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP目錄下,包含字元"Serial"或"VCom"項下面的值就是,可以有多個
?第二個引數:是對對串列埠的訪問許可權,串列埠只有讀/寫(GENERIC_READ/GENERIC_WRITE)許可權,可以同時設定成讀/寫許可權,也可以單獨只設置讀或寫許可權.
?第三個引數:是共享模式,有讀/寫/刪除共享,對串列埠通訊只能獨佔模式,即是0.
?第四個引數:檔案安全屬性,對串列埠設定成NULL.
?第五個引數:建立方式,有CREATE_ALWAYS/ CREATE_NEW/ OPEN_EXISTING等方式,但是對串列埠只能是OPEN_EXISTING,只能開啟存在的串列埠,不像檔案一樣可建立之類
?第六個引數:檔案屬性與標誌,詳細資訊檢視MSDN,如果想把串列埠設定成非同步方式,那麼要設定成FILE_FLAG_OVERLAPPED.
?第七個引數:檔案控制代碼,新檔案從該控制代碼複製或擴充套件,如果函式執行成功,返回新的控制代碼,對串列埠通訊,設定成NULL
以下為程式碼示例:
HANDLE hCom = CreateFile
(
"COM1", //開啟串列埠1
GENERIC_READ | GENERIC_WRITE, //允許讀和寫操作
0, //獨佔方式
NULL,
OPEN_EXISTING,//開啟存在的串列埠,必須是OPEN_EXISTING,檔案還可以CREATE_NEW,串列埠不能建立
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //非同步方式開啟
NULL
);
if (INVALID_HANDLE_VALUE == m_hCom)
{
int nError = GetLastError();
}
如果執行成功返回串列埠控制代碼,如果失敗通過GetLastError獲取錯誤碼.
2.1.3串列埠邏輯埠號大於10無法開啟問題
通過函式CreateFile開啟串列埠,其第一個引數就是串列埠的邏輯埠名,是用”COMX”表示的,其中X是1~N的整數,當X大於10時,有時會出現無法開啟的問題,把邏輯埠名改成"\\\\.\\COMX"即可解決.
CString szCom;
szCom.Format(_T("\\\\.\\COM%d"), nPort);
COMFile = CreateFile
(
szCom.GetBuffer(50),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
2.2串列埠配置
對串列埠進行設定,如緩衝區大小,是否允許二進位制,引數配置等.
2.2.1設定緩衝區大小
設定IO的緩衝區大小根據自己需求,太小容易丟失資料,所以根據自己的需求設定.
BOOL SetupComm(
HANDLE hFile, //串列埠控制代碼,CreateFile的有效返回值
DWORD dwInQueue, //輸入緩衝區位元組數
DWORD dwOutQueue//輸出緩衝區位元組數
);
2.2.2設定串列埠狀態
設定串列埠的各種狀態,波特率/資料位/停止位/校驗位/流控制/二進位制等.設定視窗狀態是通過函式SetCommState設定的,第一個引數是串列埠控制代碼,第二個是DCB指標.DCB結構比較複雜,引數多,所以一般是通過GetCommState來填充一個DCB物件,然後再修改這物件,再把修改好的對像給SetCommState做引數.這是常用的方法.下面是對DCB引數介紹:
typedef struct _DCB { // dcb
DWORD DCBlength; // sizeof(DCB)
DWORD BaudRate; // current baud rate 指定當前的波特率
DWORD fBinary : 1; // binary mode, no EOF check 指定是否允許二進位制模式,WINDOWS 95中必須為TRUE
DWORD fParity : 1; // enable parity checking 指定奇偶校驗是否允許
DWORD fOutxCtsFlow : 1;// CTS output flow control 指定CTS是否用於檢測傳送控制.當為TRUE是CTS為OFF,傳送將被掛起
DWORD fOutxDsrFlow : 1;// DSR output flow control 指定DSR是否用於檢測傳送控制.當為TRUE是DSR為OFF,傳送將被掛起
DWORD fDtrControl : 2; // DTR flow control type DTR_CONTROL_DISABLE值將DTR置為OFF, DTR_CONTROL_ENABLE值將
//DTR置為ON, DTR_CONTROL_HANDSHAKE允許DTR"握手",
DWORD fDsrSensitivity : 1; // DSR sensitivity 當該值為TRUE時DSR為OFF時接收的位元組被忽略
DWORD fTXContinueOnXoff : 1; // XOFF continues Tx 指定當接收緩衝區已滿,並且驅動程式已經發送出XoffChar字元時傳送
//是否停止.TRUE時,在接收緩衝區接收到緩衝區已滿的位元組XoffLim且驅動程式已經發送
//出XoffChar字元中止接收位元組之後,傳送繼續進行。FALSE時,在接收緩衝區接收到代表緩衝
//區已空的位元組XonChar且驅動程式已經發送出恢復傳送的XonChar之後,傳送繼續進行。
DWORD fOutX : 1; // XON/XOFF out flow control TRUE時,接收到XoffChar之後便停止傳送.接收到XonChar之後將重新開始
DWORD fInX : 1; // XON/XOFF in flow control TRUE時,接收緩衝區接收到代表緩衝區滿的XoffLim之後,XoffChar傳送出
//去.接收緩衝區接收到代表緩衝區空的XonLim之後,XonChar傳送出去
DWORD fErrorChar : 1; // enable error replacement 該值為TRUE且fParity為TRUE時,用ErrorChar 成員指定的字元代替奇
//偶校驗錯誤的接收字元
DWORD fNull : 1; // enable null stripping TRUE時,接收時去掉空(0值)位元組
DWORD fRtsControl : 2; // RTS flow control RTS_CONTROL_DISABLE時,RTS置為OFF RTS_CONTROL_ENABLE時, RTS置為ON RTS_CONTROL_HANDSHAKE時,當接收緩衝區小於半滿時RTS為ON 當接收緩衝區超過四分之三滿時RTS為OFF RTS_CONTROL_TOGGLE時,當接收緩衝區仍有剩餘位元組時RTS為ON ,否則預設為OFF
DWORD fAbortOnError : 1; // abort reads/writes on error TRUE時,有錯誤發生時中止讀和寫操作
DWORD fDummy2 : 17; // reserved 未使用
WORD wReserved; // not currently used 未使用,必須為0
WORD XonLim; // transmit XON threshold 指定在XON字元傳送這前接收緩衝區中可允許的最小位元組數
WORD XoffLim; // transmit XOFF threshold 指定在XOFF字元傳送這前接收緩衝區中可允許的最小位元組數
BYTE ByteSize; // number of bits/byte, 4-8 指定埠當前使用的資料位
BYTE Parity; // 0-4=no,odd,even,mark,space 指定埠當前使用的奇偶校驗方法,可能為:
//EVENPARITY,MARKPARITY,NOPARITY,ODDPARITY
BYTE StopBits; // 0,1,2 = 1, 1.5, 2 指定埠當前使用的停止位數,可能為:ONESTOPBIT,ONE5STOPBITS,TWOSTOPBITS
char XonChar; // Tx and Rx XON character 指定用於傳送和接收字元XON的值
char XoffChar; // Tx and Rx XOFF character 指定用於傳送和接收字元XOFF值
char ErrorChar; // error replacement character 本字元用來代替接收到的奇偶校驗發生錯誤時的值
char EofChar; // end of input character 當沒有使用二進位制模式時,本字元可用來指示資料的結束
char EvtChar; // received event character 當接收到此字元時,會產生一個事件
WORD wReserved1; // reserved; do not use 未使用
} DCB;
以下為示例程式碼:
DCB dcb;
DCB dcb;
GetCommState(hCom, &dcb);
dcb.BaudRate = 9600;//波特率
dcb.fBinary = TRUE;//是否允許傳二進位制
dcb.fParity = TRUE;//是否奇偶校驗
dcb.ByteSize = 8;//資料位
dcb.Parity = ODDPARITY;//奇偶校驗方式
dcb.StopBits = ONESTOPBIT;//停止位
SetCommState(hCom, &dcb);
2.2.3設定需通知的事件
設定你關心的事件,當此事件發生時,將得到事件通知,通過SetCommMask函式設定,SetCommMask函式兩個引數,第一個為串列埠控制代碼,第二個為事件,可通過位或的方式指定多個事件,如下:
OOL WINAPI SetCommMask(
__in HANDLE hFile,
__in DWORD dwEvtMask
);
//示例程式碼:
SetCommMask(m_hCom, EV_RXCHAR);
EV_RXCHAR事件指當輸入緩衝區內有資料時,通過WaitCommEvent函式可獲得通知,其他事件同理,其他事件還有EV_BREAK/EV_CTS/EV_RING等,詳情參看MSDN.2.2.4清空緩衝區
在接收/傳送資料前緩衝區中可能有垃圾資料,或者中途想清空緩衝區資料,可以用以下呼叫以下函式:
BOOL WINAPI PurgeComm(
__in HANDLE hFile,//串列埠控制代碼
__in DWORD dwFlags//清空方式PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR
);
//示例程式碼
PurgeComm(m_hCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
引數說明:
- URGE_TXABORT:立即中斷寫操作並清空輸出緩衝區
- PURGE_TXCLEAR :清空輸出緩衝區
- PURGE_RXABORT:立即中斷讀操作並清空輸入緩衝區
- PURGE_RXCLEAR:清空輸入緩衝區
2.3非同步接收資料
在2.2.3中通過SetCommMask函式,設定了非同步通知事件EV_RXCHAR,所以當輸入緩衝區中有資料時,將會有事件觸發,那怎麼獲得事件呢?通過WaitCommEvent函式擷取,與WaitForSingleObject原理類似.函式說明如下:
BOOL WINAPI WaitCommEvent(
__in HANDLE hFile,//串列埠控制代碼
__out LPDWORD lpEvtMask,//Out型指標,接收事件標誌
__in LPOVERLAPPED lpOverlapped//接收事件資訊事件狀態
)
以下為示例程式碼:
HANDLE hEventArr[2];
hEventArr[0] = osRead.hEvent;
hEventArr[1] = *g_OutPutList.GetEvent();
while (1){
DWORD nResutl = WaitForMultipleObjectsEx(2, hEventArr, FALSE, 200, TRUE/*INFINITE*/);
if (0 == nResutl){
if (g_OutPutList.GetCount() > 1000)
g_OutPutList.RemoveAll();
WORD nLen = (WORD)m_nBuffLen + 2;
PBYTE pIn = new BYTE[nLen];
pIn[0] = HIBYTE(nLen);
pIn[1] = LOBYTE(nLen);
memcpy(pIn + 2, m_InPutBuff, m_nBuffLen);
g_InPutList.AddTail(pIn);
m_nBuffLen = 0;
}
else if (1 == nResutl){
PBYTE pOut = (PBYTE)g_OutPutList.RemoveHead();
int nLen = pOut[0] * 0x100 + pOut[1] - 2;
WriteCommBlock(pOut + 2, nLen);
delete[] pOut;
}
DWORD dwEvtMask = 0;
WaitCommEvent(COMFile, &dwEvtMask, &ShareEvent);//等待串列埠事件
if ((dwEvtMask & EV_RXCHAR) == EV_RXCHAR) {
ReadCommBlock();
}
}
3. 示例程式碼
3.1 連線串列埠並設定引數
DCB dcb;
BOOL fRetVal;
COMMTIMEOUTS CommTimeOuts;
CString szCom;
szCom.Format(_T("\\\\.\\COM%d"), nPort);
COMFile = CreateFile(szCom.GetBuffer(50),
GENERIC_READ | GENERIC_WRITE,//可讀可寫
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (INVALID_HANDLE_VALUE == COMFile)
{
return (FALSE);
}
SetupComm(COMFile, 6000, 6000);
SetCommMask(/*COMFileTemp*/COMFile, EV_RXCHAR);
CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF;
CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
CommTimeOuts.ReadTotalTimeoutConstant = 1000;
CommTimeOuts.WriteTotalTimeoutMultiplier = 2 * CBR_9600 / 9600;
CommTimeOuts.WriteTotalTimeoutConstant = 0;
SetCommTimeouts(/*COMFileTemp*/COMFile, &CommTimeOuts);
dcb.DCBlength = sizeof(DCB);
GetCommState(COMFile, &dcb);
dcb.BaudRate = CBR_9600;
dcb.StopBits = ONESTOPBIT;
dcb.Parity = NOPARITY;
dcb.ByteSize = 8;
dcb.fBinary = TRUE;
dcb.fOutxDsrFlow = 0;
dcb.fDtrControl = DTR_CONTROL_ENABLE;
dcb.fOutxCtsFlow = 0;
dcb.fRtsControl = RTS_CONTROL_ENABLE;
dcb.fInX = dcb.fOutX = 1;
dcb.XonChar = 0X11;
dcb.XoffChar = 0X13;
dcb.XonLim = 100;
dcb.XoffLim = 100;
dcb.fParity = TRUE;
fRetVal = SetCommState(/*COMFileTemp*/COMFile, &dcb);
if (!fRetVal) return FALSE;
PurgeComm( /*COMFileTemp*/COMFile, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
EscapeCommFunction( /*COMFileTemp*/COMFile, SETDTR);
3.2 傳送與接收資料
HANDLE hEventArr[2];
hEventArr[0] = osRead.hEvent;
hEventArr[1] = *g_OutPutList.GetEvent();
while (1)
{
DWORD nResutl = WaitForMultipleObjectsEx(2, hEventArr, FALSE, 200, TRUE/*INFINITE*/);
if (0 == nResutl){
if (g_OutPutList.GetCount() > 1000)
g_OutPutList.RemoveAll();
WORD nLen = (WORD)m_nBuffLen + 2;
PBYTE pIn = new BYTE[nLen];
pIn[0] = HIBYTE(nLen);
pIn[1] = LOBYTE(nLen);
memcpy(pIn + 2, m_InPutBuff, m_nBuffLen);
g_InPutList.AddTail(pIn);
m_nBuffLen = 0;
}
else if (1 == nResutl){
PBYTE pOut = (PBYTE)g_OutPutList.RemoveHead();
int nLen = pOut[0] * 0x100 + pOut[1] - 2;
WriteCommBlock(pOut + 2, nLen);
delete[] pOut;
}
DWORD dwEvtMask = 0;
WaitCommEvent(COMFile, &dwEvtMask, &ShareEvent);//等待串列埠事件
if ((dwEvtMask & EV_RXCHAR) == EV_RXCHAR) {
ReadCommBlock();
}
}
3.3 關閉串列埠
//禁止串列埠所有事件
SetCommMask(COMFile, 0);
//清除資料終端就緒訊號
EscapeCommFunction(COMFile, CLRDTR);
//丟棄通訊資源的輸出或輸入緩衝區字元並終止在通訊資源上掛起的讀、寫操操作
PurgeComm(COMFile, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
CloseHandle(COMFile);
COMFile = NULL;