1. 程式人生 > >C++編寫串列埠通訊程式

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;
	}
}