串口類
從本系列文章可以看出,與通過WIN32 API進行串口訪問相比,通過MScomm這個Activex控 件進行串口訪問要來的方便許多,它基本上可以向用戶屏蔽多線程的細節,以事件(發出 OnComm消息)方式實現串口的異步訪問。
盡管如此,MScomm控件的使用仍有諸多不便,譬如其發送和接收數據都要進行VARIANT類 型對象與字符串的轉化等。因此,國內外許多優秀的程序員自己編寫了一些串口類,使用這 些類,我們將可以更方便的操作串口。在筆者的《深入淺出Win32多線程程序設計之綜合實例 》(網址:http://dev.yesky.com)一文中,曾向讀者展示了由Remon Spekreijse編寫的 CSerialPort串口類,而本文將向您展示由程序員llbird編寫的cnComm(中國串口?)串口類。
llbird是一位優秀的程序員,他的代碼風格簡潔而緊湊,類的聲明和實現都被定義在一個 頭文件中,使用這個類的朋友只需要在工程中包含這一頭文件即可:
/* Comm Base Library(WIN98/NT/2000) ver 1.1 Compile by: BC++ 5; C++ BUILDER 4, 5, 6, X; VC++ 5, 6; VC.NET; GCC; copyright(c) 2004.5 - 2005.8 llbird [email protected] */ #ifndef _CN_COMM_H_ #define _CN_COMM_H_ #pragma warning(disable: 4530) #pragma warning(disable: 4786) #pragma warning(disable: 4800) #include <assert.h> #include <stdio.h> #include <windows.h> //送到窗口的消息 WPARAM 端口號 #define ON_COM_RECEIVE WM_USER + 618 #define ON_COM_CTS WM_USER + 619 //LPARAM 1 valid #define ON_COM_DSR WM_USER + 621 //LPARAM 1 valid #define ON_COM_RING WM_USER + 623 #define ON_COM_RLSD WM_USER + 624 #define ON_COM_BREAK WM_USER + 625 #define ON_COM_TXEMPTY WM_USER + 626 #define ON_COM_ERROR WM_USER + 627 //LPARAM save Error ID #define DEFAULT_COM_MASK_EVENT EV_RXCHAR | EV_ERR | EV_CTS | EV_DSR | EV_BREAK | EV_TXEMPTY | EV_RING | EV_RLSD class cnComm { public: //------------------------------Construction----------------------------------- //第1個參數為是否在打開串口時啟動監視線程, 第2個參數為IO方式 阻塞方式(0)/ 異步重疊方式(默認) cnComm(bool fAutoBeginThread = true, DWORD dwIOMode = FILE_FLAG_OVERLAPPED): _dwIOMode(dwIOMode), _fAutoBeginThread(fAutoBeginThread) { Init(); } virtual ~cnComm() { Close(); UnInit(); } //----------------------------------Attributes---------------------------------- //判斷串口是否打開 inline bool IsOpen() { return _hCommHandle != INVALID_HANDLE_VALUE; } //判斷串口是否打開 operator bool() { return _hCommHandle != INVALID_HANDLE_VALUE; } //獲得串口句炳 inline HANDLE GetHandle() { return _hCommHandle; } //獲得串口句炳 operator HANDLE() { return _hCommHandle; } //獲得串口參數 DCB DCB *GetState() { return IsOpen() && ::GetCommState(_hCommHandle, &_DCB) == TRUE ? &_DCB: NULL; } //設置串口參數 DCB bool SetState(DCB *pdcb = NULL) { return IsOpen() ? ::SetCommState(_hCommHandle, pdcb == NULL ? &_DCB:pdcb) == TRUE: false; } //設置串口參數:波特率,停止位,等 支持設置字符串 "9600, 8, n, 1" bool SetState(char *szSetStr) { if (IsOpen()) { if (::GetCommState(_hCommHandle, &_DCB) != TRUE) return false; if (::BuildCommDCB(szSetStr, &_DCB) != TRUE) return false; return ::SetCommState(_hCommHandle, &_DCB) == TRUE; } return false; } //設置串口參數:波特率,停止位,等 bool SetState(DWORD dwBaudRate, DWORD dwByteSize = 8, DWORD dwParity = NOPARITY, DWORD dwStopBits = ONESTOPBIT) { if (IsOpen()) { if (::GetCommState(_hCommHandle, &_DCB) != TRUE) return false; _DCB.BaudRate = dwBaudRate; _DCB.ByteSize = (unsigned char)dwByteSize; _DCB.Parity = (unsigned char)dwParity; _DCB.StopBits = (unsigned char)dwStopBits; return ::SetCommState(_hCommHandle, &_DCB) == TRUE; } return false; } //獲得超時結構 LPCOMMTIMEOUTS GetTimeouts(void) { return IsOpen() && ::GetCommTimeouts(_hCommHandle, &_CO) == TRUE ? &_CO: NULL; } //設置超時 bool SetTimeouts(LPCOMMTIMEOUTS lpCO) { return IsOpen() ? ::SetCommTimeouts(_hCommHandle, lpCO) == TRUE:false; } //設置串口的I/O緩沖區大小 bool SetBufferSize(DWORD dwInputSize, DWORD dwOutputSize) { return IsOpen() ? ::SetupComm(_hCommHandle, dwInputSize, dwOutputSize)== TRUE: false; } //關聯消息的窗口句柄 inline void SetWnd(HWND hWnd) { assert(::IsWindow(hWnd)); _hNotifyWnd = hWnd; } //設定發送通知, 接受字符最小值 inline void SetNotifyNum(DWORD dwNum) { _dwNotifyNum = dwNum; } //線程是否運行 inline bool IsThreadRunning() { return _hThreadHandle != NULL; } //獲得線程句柄 inline HANDLE GetThread() { return _hThreadHandle; } //設置要監視的事件, 打開前設置有效 void SetMaskEvent(DWORD dwEvent = DEFAULT_COM_MASK_EVENT) { _dwMaskEvent = dwEvent; } //獲得讀緩沖區的字符數 int GetInputSize() { COMSTAT Stat; DWORD dwError; return ::ClearCommError(_hCommHandle, &dwError, &Stat) == TRUE ? Stat.cbInQue : (DWORD) - 1L; } //----------------------------------Operations---------------------------------- //打開串口 缺省 9600, 8, n, 1 bool Open(DWORD dwPort) { return Open(dwPort, 9600); } //打開串口 缺省 baud_rate, 8, n, 1 bool Open(DWORD dwPort, DWORD dwBaudRate) { if (dwPort < 1 || dwPort > 1024) return false; BindCommPort(dwPort); if (!OpenCommPort()) return false; if (!SetupPort()) return false; return SetState(dwBaudRate); } //打開串口, 使用類似"9600, 8, n, 1"的設置字符串設置串口 bool Open(DWORD dwPort, char *szSetStr) { if (dwPort < 1 || dwPort > 1024) return false; BindCommPort(dwPort); if (!OpenCommPort()) return false; if (!SetupPort()) return false; return SetState(szSetStr); } //讀取串口 dwBufferLength個字符到 Buffer 返回實際讀到的字符數 可讀任意數據 DWORD Read(LPVOID Buffer, DWORD dwBufferLength, DWORD dwWaitTime = 10) { if (!IsOpen()) return 0; COMSTAT Stat; DWORD dwError; if (::ClearCommError(_hCommHandle, &dwError, &Stat) && dwError > 0) { ::PurgeComm(_hCommHandle,PURGE_RXABORT | PURGE_RXCLEAR); return 0; } if (!Stat.cbInQue) // 緩沖區無數據 return 0; unsigned long uReadLength = 0; dwBufferLength = dwBufferLength > Stat.cbInQue ? Stat.cbInQue :dwBufferLength; if (!::ReadFile(_hCommHandle, Buffer, dwBufferLength, &uReadLength,&_ReadOverlapped)) { if (::GetLastError() == ERROR_IO_PENDING) { WaitForSingleObject(_ReadOverlapped.hEvent, dwWaitTime); // 結束異步I/O if (!::GetOverlappedResult(_hCommHandle, &_ReadOverlapped,&uReadLength, false)) { if (::GetLastError() != ERROR_IO_INCOMPLETE) uReadLength = 0; } } else uReadLength = 0; } return uReadLength; } //讀取串口 dwBufferLength - 1 個字符到 szBuffer 返回ANSI C 模式字符串指針 適合一般字符通訊 char *ReadString(char *szBuffer, DWORD dwBufferLength, DWORD dwWaitTime =20) { unsigned long uReadLength = Read(szBuffer, dwBufferLength - 1,dwWaitTime); szBuffer[uReadLength] = '\0'; return szBuffer; } //寫串口 可寫任意數據 "abcd" or "\x0\x1\x2" DWORD Write(LPVOID Buffer, DWORD dwBufferLength) { if (!IsOpen()) return 0; DWORD dwError; if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0) ::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR); unsigned long uWriteLength = 0; if (!::WriteFile(_hCommHandle, Buffer, dwBufferLength, &uWriteLength,&_WriteOverlapped)) if (::GetLastError() != ERROR_IO_PENDING) uWriteLength = 0; return uWriteLength; } //寫串口 寫ANSI C 模式字符串指針 DWORD Write(const char *szBuffer) { assert(szBuffer); return Write((void*)szBuffer, strlen(szBuffer)); } //讀串口 同步應用 DWORD ReadSync(LPVOID Buffer, DWORD dwBufferLength) { if (!IsOpen()) return 0; DWORD dwError; if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0) { ::PurgeComm(_hCommHandle,PURGE_RXABORT | PURGE_RXCLEAR); return 0; } DWORD uReadLength = 0; ::ReadFile(_hCommHandle, Buffer, dwBufferLength, &uReadLength, NULL); return uReadLength; } //寫串口 同步應用 DWORD WriteSync(LPVOID Buffer, DWORD dwBufferLength) { if (!IsOpen()) return 0; DWORD dwError; if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0) ::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR); unsigned long uWriteLength = 0; ::WriteFile(_hCommHandle, Buffer, dwBufferLength, &uWriteLength, NULL); return uWriteLength; } //寫串口 szBuffer 可以輸出格式字符串 包含緩沖區長度 DWORD Write(char *szBuffer, DWORD dwBufferLength, char *szFormat, ...) { if (!IsOpen()) return 0; va_list va; va_start(va, szFormat); _vsnprintf(szBuffer, dwBufferLength, szFormat, va); va_end(va); return Write(szBuffer); } //寫串口 szBuffer 可以輸出格式字符串 不檢查緩沖區長度 小心溢出 DWORD Write(char *szBuffer, char *szFormat, ...) { if (!IsOpen()) return 0; va_list va; va_start(va, szFormat); vsprintf(szBuffer, szFormat, va); va_end(va); return Write(szBuffer); } //關閉串口 同時也關閉關聯線程 virtual void Close() { if (IsOpen()) { PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR); EndThread(); ::CloseHandle(_hCommHandle); _hCommHandle = INVALID_HANDLE_VALUE; } } //DTR 電平控制 bool SetDTR(bool OnOrOff) { return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETDTR :CLRDTR): false; } //RTS 電平控制 bool SetRTS(bool OnOrOff) { return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETRTS :CLRRTS): false; } // bool SetBreak(bool OnOrOff) { return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETBREAK: CLRBREAK): false; } //輔助線程控制 建監視線程 bool BeginThread() { if (!IsThreadRunning()) { _fRunFlag = true; _hThreadHandle = NULL; DWORD id; _hThreadHandle = ::CreateThread(NULL, 0, CommThreadProc, this, 0,&id); return (_hThreadHandle != NULL); } return false; } //暫停監視線程 inline bool SuspendThread() { return IsThreadRunning() ? ::SuspendThread(_hThreadHandle) !=0xFFFFFFFF: false; } //恢復監視線程 inline bool ResumeThread() { return IsThreadRunning() ? ::ResumeThread(_hThreadHandle) !=0xFFFFFFFF: false; } //終止線程 bool EndThread(DWORD dwWaitTime = 100) { if (IsThreadRunning()) { _fRunFlag = false; ::SetCommMask(_hCommHandle, 0); ::SetEvent(_WaitOverlapped.hEvent); if (::WaitForSingleObject(_hThreadHandle, dwWaitTime) !=WAIT_OBJECT_0) if (!::TerminateThread(_hThreadHandle, 0)) return false; ::CloseHandle(_hThreadHandle); ::ResetEvent(_WaitOverlapped.hEvent); _hThreadHandle = NULL; return true; } return false; } protected: volatile DWORD _dwPort; //串口號 volatile HANDLE _hCommHandle; //串口句柄 char _szCommStr[20]; //保存COM1類似的字符串 DCB _DCB; //波特率,停止位,等 COMMTIMEOUTS _CO; //超時結構 DWORD _dwIOMode; // 0 同步 默認 FILE_FLAG_OVERLAPPED重疊I/O異步 OVERLAPPED _ReadOverlapped, _WriteOverlapped; // 重疊I/O volatile HANDLE _hThreadHandle; //輔助線程 volatile HWND _hNotifyWnd; // 通知窗口 volatile DWORD _dwNotifyNum; //接受多少字節(>=_dwNotifyNum)發送通知消息 volatile DWORD _dwMaskEvent; //監視的事件 volatile bool _fRunFlag; //線程運行循環標志 bool _fAutoBeginThread; //Open() 自動 BeginThread(); OVERLAPPED _WaitOverlapped; //WaitCommEvent use //初始化 void Init() { memset(_szCommStr, 0, 20); memset(&_DCB, 0, sizeof(_DCB)); _DCB.DCBlength = sizeof(_DCB); _hCommHandle = INVALID_HANDLE_VALUE; memset(&_ReadOverlapped, 0, sizeof(_ReadOverlapped)); memset(&_WriteOverlapped, 0, sizeof(_WriteOverlapped)); _ReadOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL); assert(_ReadOverlapped.hEvent != INVALID_HANDLE_VALUE); _WriteOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL); assert(_WriteOverlapped.hEvent != INVALID_HANDLE_VALUE); _hNotifyWnd = NULL; _dwNotifyNum = 0; _dwMaskEvent = DEFAULT_COM_MASK_EVENT; _hThreadHandle = NULL; memset(&_WaitOverlapped, 0, sizeof(_WaitOverlapped)); _WaitOverlapped.hEvent = ::CreateEvent(NULL, true, false, NULL); assert(_WaitOverlapped.hEvent != INVALID_HANDLE_VALUE); } //析構 void UnInit() { if (_ReadOverlapped.hEvent != INVALID_HANDLE_VALUE) CloseHandle(_ReadOverlapped.hEvent); if (_WriteOverlapped.hEvent != INVALID_HANDLE_VALUE) CloseHandle(_WriteOverlapped.hEvent); if (_WaitOverlapped.hEvent != INVALID_HANDLE_VALUE) CloseHandle(_WaitOverlapped.hEvent); } //綁定串口 void BindCommPort(DWORD dwPort) { assert(dwPort >= 1 && dwPort <= 1024); char p[5]; _dwPort = dwPort; strcpy(_szCommStr, "\\\\.\\COM"); ltoa(_dwPort, p, 10); strcat(_szCommStr, p); } //打開串口 virtual bool OpenCommPort() { if (IsOpen()) Close(); _hCommHandle = ::CreateFile(_szCommStr, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | _dwIOMode,NULL); if (_fAutoBeginThread) { if (IsOpen() && BeginThread()) return true; else { Close(); //創建線程失敗 return false; } } return IsOpen(); } //設置串口 virtual bool SetupPort() { if (!IsOpen()) return false; if (!::SetupComm(_hCommHandle, 4096, 4096)) return false; if (!::GetCommTimeouts(_hCommHandle, &_CO)) return false; _CO.ReadIntervalTimeout = 0; _CO.ReadTotalTimeoutMultiplier = 1; _CO.ReadTotalTimeoutConstant = 1000; _CO.WriteTotalTimeoutMultiplier = 1; _CO.WriteTotalTimeoutConstant = 1000; if (!::SetCommTimeouts(_hCommHandle, &_CO)) return false; if (!::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_RXABORT |PURGE_TXCLEAR | PURGE_RXCLEAR)) return false; return true; } //---------------------------------------threads callback----------------------------------- //線程收到消息自動調用, 如窗口句柄有效, 送出消息, 包含串口編號, 均為虛函數可以在基層類中擴展 virtual void OnReceive() //EV_RXCHAR { if (::IsWindow(_hNotifyWnd)) ::PostMessage(_hNotifyWnd, ON_COM_RECEIVE, WPARAM(_dwPort), LPARAM (0)); } virtual void OnDSR() { if (::IsWindow(_hNotifyWnd)) { DWORD Status; if (GetCommModemStatus(_hCommHandle, &Status)) ::PostMessage(_hNotifyWnd, ON_COM_DSR, WPARAM(_dwPort),LPARAM((Status &MS_DSR_ON) ? 1 : 0)); } } virtual void OnCTS() { if (::IsWindow(_hNotifyWnd)) { DWORD Status; if (GetCommModemStatus(_hCommHandle, &Status)) ::PostMessage(_hNotifyWnd, ON_COM_CTS, WPARAM(_dwPort), LPARAM( (Status &MS_CTS_ON) ? 1 : 0)); } } virtual void OnBreak() { if (::IsWindow(_hNotifyWnd)) { ::PostMessage(_hNotifyWnd, ON_COM_BREAK, WPARAM(_dwPort), LPARAM(0)); } } virtual void OnTXEmpty() { if (::IsWindow(_hNotifyWnd)) ::PostMessage(_hNotifyWnd, ON_COM_TXEMPTY, WPARAM(_dwPort), LPARAM (0)); } virtual void OnError() { DWORD dwError; ::ClearCommError(_hCommHandle, &dwError, NULL); if (::IsWindow(_hNotifyWnd)) ::PostMessage(_hNotifyWnd, ON_COM_ERROR, WPARAM(_dwPort), LPARAM (dwError)); } virtual void OnRing() { if (::IsWindow(_hNotifyWnd)) ::PostMessage(_hNotifyWnd, ON_COM_RING, WPARAM(_dwPort), LPARAM(0)); } virtual void OnRLSD() { if (::IsWindow(_hNotifyWnd)) ::PostMessage(_hNotifyWnd, ON_COM_RLSD, WPARAM(_dwPort), LPARAM(0)); } virtual DWORD ThreadFunc() { if (!::SetCommMask(_hCommHandle, _dwMaskEvent)) { char szBuffer[256]; _snprintf(szBuffer, 255, "%s(%d) : COM%d Call WINAPI SetCommMask(%x, %x) Fail, thread work invalid! GetLastError() = %d;", __FILE__, __LINE__, _dwPort, _hCommHandle, _dwMaskEvent, GetLastError()); MessageBox(NULL, szBuffer, "Class cnComm", MB_OK); return 1; } COMSTAT Stat; DWORD dwError; for (DWORD dwLength, dwMask = 0; _fRunFlag && IsOpen(); dwMask = 0) { if (!::WaitCommEvent(_hCommHandle, &dwMask, &_WaitOverlapped)) { if (::GetLastError() == ERROR_IO_PENDING) // asynchronous ::GetOverlappedResult(_hCommHandle, &_WaitOverlapped,&dwLength, TRUE); else continue; } if (dwMask == 0) continue; switch (dwMask) { case EV_RXCHAR: ::ClearCommError(_hCommHandle, &dwError, &Stat); if (Stat.cbInQue >= _dwNotifyNum) OnReceive(); break; case EV_TXEMPTY: OnTXEmpty(); break; case EV_CTS: OnCTS(); break; case EV_DSR: OnDSR(); break; case EV_RING: OnRing(); break; case EV_RLSD: OnRLSD(); break; case EV_BREAK: OnBreak(); break; case EV_ERR: OnError(); break; } //case } //for return 0; } private: //the function protected cnComm(const cnComm &); cnComm &operator = (const cnComm &); //base function for thread static DWORD WINAPI CommThreadProc(LPVOID lpPara) { return ((cnComm*)lpPara)->ThreadFunc(); } }; #endif //_CN_COMM_H_
2.實例
程序的功能和界面(如下圖)都與本文連載三、四中《基於WIN32 API的串口編程》和《基於控件的串口編程》相同,不同的只是本節的串口通信要以llbird定義的cnComm類來實現。
我們需要為串口的接收事件定義一個用戶消息ON_COM_RECEIVE,因此對話框的消息映射為:
BEGIN_MESSAGE_MAP(CSerialPortClassDlg, CDialog)
//{{AFX_MSG_MAP(CSerialPortClassDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_CLEAR_BUTTON, OnClearButton)
ON_BN_CLICKED(IDC_SEND_BUTTON, OnSendButton)
ON_MESSAGE(ON_COM_RECEIVE,OnCommRecv)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
同時,我們需要在對話框類的頭文件中定義cnComm類的成員變量com和接收數據消息處理函數OnCommRecv:
cnComm com;
afx_msg void OnCommRecv(WPARAM wParam, LPARAM lParam);
在對話框初始化時調用打開串口:
BOOL CSerialPortClassDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX &0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu *pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
com.Open(1); //打開串口1並使用默認設置
com.SetWnd(AfxGetMainWnd()->m_hWnd); //設置消息處理窗口
return TRUE; // return TRUE unless you set the focus to a control
}
發送字符串的過程很簡單,只需要調用cnComm類的Write函數:
//"發送"按鈕函數(完成數據的發送功能)
void CSerialPortClassDlg::OnSendButton()
{
// TODO: Add your control notification handler code here
UpdateData(true);
com.Write(m_send); //發送字符串
}
接收字符串的過程也很簡單,只需要調用cnComm類的ReadString函數:
void CSerialPortClassDlg::OnCommRecv(WPARAM wParam, LPARAM lParam)
{
UpdateData(true);
//讀取串口上的字符
char str[100];
com.ReadString(str, 100);
m_recv += str;
UpdateData(false);
}
讀者朋友們這時一定會發出感慨:使用cnComm類後,進行串口數據收發的程序是多麼簡單啊!的確,串口的初始化、讀寫幾乎都是用1~2條語句搞定的!
這就是我們要特別用一次連載來講述使用第三方類來進行串口通信的原因。實際上,筆者在進行網絡通信程序編程時,也不認為MS提供的CSocket類是最方便的選擇,照樣習慣使用第三方的網絡通信類。它們的確有非常簡潔明快的接口,這一點也是值得MS哥哥們學習的。