分类:
2006-11-23 17:21:47
check back:
12. 4 一个通信演示程序 为了使读者更好地掌握本章的概念,这里举一个具体实例来说明问题。如图12.1所示,例子程序名为Terminal,是一个简单的TTY终端仿真程序。读者可以用该程序打开一个串行口,该程序会把用户的键盘输入发送给串行口,并把从串口接收到的字符显示在视图中。用户通过选择File->Connect命令来打开串行口,选择File->Disconnect命令则关闭串行口。 图12.1 Terminal终端仿真程序 当用户选择File->Settings...命令时,会弹出一个Communication settings对话框,如图12.2所示。该对话框主要用来设置串行口,包括端口、波特率、每字节位数、校验、停止位数和流控制。 图12.2 Communication settings对话框
通过该对话框也可以设置TTY终端仿真的属性,如果选择New Line(自动换行),那么每当从串口读到回车符(‘\r’)时,视图中的正文就会换行,否则,只有在读到换行符(‘\n’)时才会换行。如果选择Local echo(本地回显),那么发送的字符会在视图中显示出来。 终端仿真程序的特点是数据的传输没有规律。因为键盘输入速度有限,所以发送的数据量较小,但接收的数据源是不确定的,所以有可能会有大量数据高速涌入的情况发生。根据Terminal的这些特性,我们在程序中创建了一个辅助工作者线程专门来监视串行口的输入。由于写入串行口的数据量不大,不会太费时,所以在主线程中完成写端口的任务是可以的,不必另外创建线程。 现在就让我们开始工作。请读者按下面几步进行:
表12.5 新菜单项
表12.6 通信设置对话框中的主要控件
表12.7 CSetupDlg类的数据成员
清单12.6 CTerminalDoc类的部分代码 // TerminalDoc.h : interface of the CTerminalDoc class // /////////////////////////////////////////////////////////////////////////////
#define MAXBLOCK 2048 #define XON 0x11 #define XOFF 0x13
UINT CommProc(LPVOID pParam);
class CTerminalDoc : public CDocument { protected: // create from serialization only CTerminalDoc(); DECLARE_DYNCREATE(CTerminalDoc)
// Attributes public:
CWinThread* m_pThread; // 代表辅助线程 volatile BOOL m_bConnected; volatile HWND m_hTermWnd; volatile HANDLE m_hPostMsgEvent; // 用于WM_COMMNOTIFY消息的事件对象 OVERLAPPED m_osRead, m_osWrite; // 用于重叠读/写
volatile HANDLE m_hCom; // 串行口句柄 int m_nBaud; int m_nDataBits; BOOL m_bEcho; int m_nFlowCtrl; BOOL m_bNewLine; int m_nParity; CString m_sPort; int m_nStopBits;
// Operations public:
BOOL ConfigConnection(); BOOL OpenConnection(); void CloseConnection(); DWORD ReadComm(char *buf,DWORD dwLength); DWORD WriteComm(char *buf,DWORD dwLength); // Overrides . . . };
///////////////////////////////////////////////////////////////////////////// // TerminalDoc.cpp : implementation of the CTerminalDoc class //
#include "SetupDlg.h"
CTerminalDoc::CTerminalDoc() { // TODO: add one-time construction code here
m_bConnected=FALSE; m_pThread=NULL;
m_nBaud = 9600; m_nDataBits = 8; m_bEcho = FALSE; m_nFlowCtrl = 0; m_bNewLine = FALSE; m_nParity = 0; m_sPort = "COM2"; m_nStopBits = 0; }
CTerminalDoc::~CTerminalDoc() {
if(m_bConnected) CloseConnection(); // 删除事件句柄 if(m_hPostMsgEvent) CloseHandle(m_hPostMsgEvent); if(m_osRead.hEvent) CloseHandle(m_osRead.hEvent); if(m_osWrite.hEvent) CloseHandle(m_osWrite.hEvent); }
BOOL CTerminalDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; ((CEditView*)m_viewList.GetHead())->SetWindowText(NULL);
// TODO: add reinitialization code here // (SDI documents will reuse this document)
// 为WM_COMMNOTIFY消息创建事件对象,手工重置,初始化为有信号的 if((m_hPostMsgEvent=CreateEvent(NULL, TRUE, TRUE, NULL))==NULL) return FALSE; memset(&m_osRead, 0, sizeof(OVERLAPPED)); memset(&m_osWrite, 0, sizeof(OVERLAPPED)); // 为重叠读创建事件对象,手工重置,初始化为无信号的 if((m_osRead.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL) return FALSE; // 为重叠写创建事件对象,手工重置,初始化为无信号的 if((m_osWrite.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL) return FALSE; return TRUE; }
void CTerminalDoc::OnFileConnect() { // TODO: Add your command handler code here
if(!OpenConnection()) AfxMessageBox("Can't open connection"); }
void CTerminalDoc::OnFileDisconnect() { // TODO: Add your command handler code here
CloseConnection(); }
void CTerminalDoc::OnUpdateFileConnect(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here
pCmdUI->Enable(!m_bConnected); }
void CTerminalDoc::OnUpdateFileDisconnect(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here
pCmdUI->Enable(m_bConnected); }
// 打开并配置串行口,建立工作者线程 BOOL CTerminalDoc::OpenConnection() { COMMTIMEOUTS TimeOuts; POSITION firstViewPos; CView *pView;
firstViewPos=GetFirstViewPosition(); pView=GetNextView(firstViewPos); m_hTermWnd=pView->GetSafeHwnd();
if(m_bConnected) return FALSE; m_hCom=CreateFile(m_sPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); // 重叠方式 if(m_hCom==INVALID_HANDLE_VALUE) return FALSE; SetupComm(m_hCom,MAXBLOCK,MAXBLOCK); SetCommMask(m_hCom, EV_RXCHAR);
// 把间隔超时设为最大,把总超时设为0将导致ReadFile立即返回并完成操作 TimeOuts.ReadIntervalTimeout=MAXDWORD; TimeOuts.ReadTotalTimeoutMultiplier=0; TimeOuts.ReadTotalTimeoutConstant=0; /* 设置写超时以指定WriteComm成员函数中的 GetOverlappedResult函数的等待时间*/ TimeOuts.WriteTotalTimeoutMultiplier=50; TimeOuts.WriteTotalTimeoutConstant=2000; SetCommTimeouts(m_hCom, &TimeOuts); if(ConfigConnection()) { m_pThread=AfxBeginThread(CommProc, this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL); // 创建并挂起线程 if(m_pThread==NULL) { CloseHandle(m_hCom); return FALSE; } else { m_bConnected=TRUE; m_pThread->ResumeThread(); // 恢复线程运行 } } else { CloseHandle(m_hCom); return FALSE; } return TRUE; }
// 结束工作者线程,关闭串行口 void CTerminalDoc::CloseConnection() { if(!m_bConnected) return; m_bConnected=FALSE;
//结束CommProc线程中WaitSingleObject函数的等待 SetEvent(m_hPostMsgEvent);
//结束CommProc线程中WaitCommEvent的等待 SetCommMask(m_hCom, 0);
//等待辅助线程终止 WaitForSingleObject(m_pThread->m_hThread, INFINITE); m_pThread=NULL; CloseHandle(m_hCom); }
// 让用户设置串行口 void CTerminalDoc::OnFileSettings() { // TODO: Add your command handler code here
CSetupDlg dlg; CString str;
dlg.m_bConnected=m_bConnected; dlg.m_sPort=m_sPort; str.Format("%d",m_nBaud); dlg.m_sBaud=str; str.Format("%d",m_nDataBits); dlg.m_sDataBits=str; dlg.m_nParity=m_nParity; dlg.m_nStopBits=m_nStopBits; dlg.m_nFlowCtrl=m_nFlowCtrl; dlg.m_bEcho=m_bEcho; dlg.m_bNewLine=m_bNewLine; if(dlg.DoModal()==IDOK) { m_sPort=dlg.m_sPort; m_nBaud=atoi(dlg.m_sBaud); m_nDataBits=atoi(dlg.m_sDataBits); m_nParity=dlg.m_nParity; m_nStopBits=dlg.m_nStopBits; m_nFlowCtrl=dlg.m_nFlowCtrl; m_bEcho=dlg.m_bEcho; m_bNewLine=dlg.m_bNewLine; if(m_bConnected) if(!ConfigConnection()) AfxMessageBox("Can't realize the settings!"); } }
// 配置串行口 BOOL CTerminalDoc::ConfigConnection() { DCB dcb;
if(!GetCommState(m_hCom, &dcb)) return FALSE; dcb.fBinary=TRUE; dcb.BaudRate=m_nBaud; // 波特率 dcb.ByteSize=m_nDataBits; // 每字节位数 dcb.fParity=TRUE; switch(m_nParity) // 校验设置 { case 0: dcb.Parity=NOPARITY; break; case 1: dcb.Parity=EVENPARITY; break; case 2: dcb.Parity=ODDPARITY; break; default:; } switch(m_nStopBits) // 停止位 { case 0: dcb.StopBits=ONESTOPBIT; break; case 1: dcb.StopBits=ONE5STOPBITS; break; case 2: dcb.StopBits=TWOSTOPBITS; break; default:; } // 硬件流控制设置 dcb.fOutxCtsFlow=m_nFlowCtrl==1; dcb.fRtsControl=m_nFlowCtrl==1? RTS_CONTROL_HANDSHAKE:RTS_CONTROL_ENABLE; // XON/XOFF流控制设置 dcb.fInX=dcb.fOutX=m_nFlowCtrl==2; dcb.XonChar=XON; dcb.XoffChar=XOFF; dcb.XonLim=50; dcb.XoffLim=50; return SetCommState(m_hCom, &dcb); }
// 从串行口输入缓冲区中读入指定数量的字符 DWORD CTerminalDoc::ReadComm(char *buf,DWORD dwLength) { DWORD length=0; COMSTAT ComStat; DWORD dwErrorFlags; ClearCommError(m_hCom,&dwErrorFlags,&ComStat); length=min(dwLength, ComStat.cbInQue); ReadFile(m_hCom,buf,length,&length,&m_osRead); return length;
}
// 将指定数量的字符从串行口输出 DWORD CTerminalDoc::WriteComm(char *buf,DWORD dwLength) { BOOL fState; DWORD length=dwLength; COMSTAT ComStat; DWORD dwErrorFlags; ClearCommError(m_hCom,&dwErrorFlags,&ComStat); fState=WriteFile(m_hCom,buf,length,&length,&m_osWrite); if(!fState){ if(GetLastError()==ERROR_IO_PENDING) { GetOverlappedResult(m_hCom,&m_osWrite,&length,TRUE);// 等待 } else length=0; } return length; }
// 工作者线程,负责监视串行口 UINT CommProc(LPVOID pParam) { OVERLAPPED os; DWORD dwMask, dwTrans; COMSTAT ComStat; DWORD dwErrorFlags; CTerminalDoc *pDoc=(CTerminalDoc*)pParam;
memset(&os, 0, sizeof(OVERLAPPED)); os.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL); if(os.hEvent==NULL) { AfxMessageBox("Can't create event object!"); return (UINT)-1; } while(pDoc->m_bConnected) { ClearCommError(pDoc->m_hCom,&dwErrorFlags,&ComStat); if(ComStat.cbInQue) { // 无限等待WM_COMMNOTIFY消息被处理完 WaitForSingleObject(pDoc->m_hPostMsgEvent, INFINITE); ResetEvent(pDoc->m_hPostMsgEvent); // 通知视图 PostMessage(pDoc->m_hTermWnd, WM_COMMNOTIFY, EV_RXCHAR, 0); continue; } dwMask=0; if(!WaitCommEvent(pDoc->m_hCom, &dwMask, &os)) // 重叠操作 { if(GetLastError()==ERROR_IO_PENDING) // 无限等待重叠操作结果 GetOverlappedResult(pDoc->m_hCom, &os, &dwTrans, TRUE); else { CloseHandle(os.hEvent); return (UINT)-1; } } } CloseHandle(os.hEvent); return 0; }
BOOL CTerminalDoc::CanCloseFrame(CFrameWnd* pFrame) { // TODO: Add your specialized code here and/or call the base class
SetModifiedFlag(FALSE); // 将文档的修改标志设置成未修改 return CDocument::CanCloseFrame(pFrame); } 毫无疑问,CTerminalDoc类是研究重点。该类负责Terminal的通信任务,主要包括设置通信参数、打开和关闭串行口、建立和终止辅助工作线程、用辅助线程监视串行口等等。 在CTerminalDoc类的头文件中,有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。 成员m_bConnected用来表明当前是否存在一个通信连接。m_hTermWnd用来保存是视图的窗口句柄。m_hPostMsgEvent事件对象用于WM_COMMNOTIFY消息的允许和禁止。m_pThread用来指向AfxBeginThread创建的CWinThread对象,以便对线程进行控制。OVERLAPPED结构m_osRead和m_osWrite用于串行口的重叠读/写,程序应该为它们的hEvent成员创建事件句柄。 CTerminalDoc类的构造函数主要完成一些通信参数的初始化工作。OnNewDocument成员函数创建了三个事件对象,CTerminalDoc的析构函数关闭串行口并删除事件对象句柄。 OnFileSettings是File->Settings...的命令处理函数,该函数弹出一个CSetupDlg对话框来设置通信参数。实际的设置工作由ConfigConnection函数完成,在OpenConnection和OnFileSettings中都会调用该函数。 OpenConnection负责打开串行口并建立辅助工作线程,当用户选择了File->Connect命令时,消息处理函数OnFileConnect将调用该函数。该函数调用CreateFile以重叠方式打开指定的串行口并把返回的句柄保存在m_hCom成员中。接着,函数对m_hCom通信设备进行各种设置。需要注意的是对超时的设定,将读间隔超时设置为MAXDWORD并使其它读超时参数为0会导致ReadFile函数立即完成操作并返回,而不管读入了多少字符。设置超时就规定了GetOverlappedResult函数的等待时间,因此有必要将写超时设置成适当的值,这样如果不能完成写串口的任务,GetOverlappedResult函数会在超过规定超时后结束等待并报告实际传输的字符数。 如果对m_hCom设置成功,则函数会建立一个辅助线程并暂时将其挂起。在最后,调用CWinThread:: ResumeThread使线程开始运行。 OpenConnection调用成功后,线程函数CommProc就开始工作。该函数的主体是一个while循环,在该循环内,混合了两种方法监视串行口输入的方法。先是调用ClearCommError函数查询输入缓冲区中是否有字符,如果有,就向视图发送WM_COMMNOTIFY消息通知其接收字符。如果没有,则调用WaitCommEvent函数监视EV_RXCHAR通信事件,该函数执行重叠操作,紧接着调用的GetOverlappedResult函数无限等待通信事件,如果EV_RXCHAR事件发生(串口收到字符并放入输入缓冲区中),那么函数就结束等待。 上述两种方法的混合使用兼顾了线程的效率和可靠性。如果只用ClearCommError函数,则辅助线程将不断耗费CPU时间来查询,效率较低。如果只用WaitCommEvent来监视,那么由于该函数对输入缓冲区中已有的字符不会产生EV_RXCHAR事件,因此在通信速率较高时,会造成数据的延误和丢失。 注意到辅助线程用m_PostMsgEvent事件对象来同步WM_COMMNOTIFY消息的发送。在发送消息之前,WaitForSingleObject函数无限等待m_PostMsgEvent对象,WM_COMMNOTIFY的消息处理函数CTerminalView::OnCommNotify在返回时会把该对象置为有信号,因此,如果WaitForSingleObject函数返回,则说明上一个WM_COMMNOTIFY消息已被处理完,这时才能发下一个消息,在发消息前还要调用ResetEvent把m_PostMsgEvent对象置为无信号的,以供下次使用。 由于PostMessage函数在消息队列中放入消息后会立即返回,所以如果不采取上述措施,那么辅助线程可能在主线程未处理之前重复发出WM_COMMNOTIFY消息,这会降低系统的效率。 可能有读者会问,为什么不用SendMessage?该函数在发送的消息被处理完毕后才返回,这样不就不用考虑同步问题了吗?是的,本例中也可以使用SendMessage,但该函数会阻塞辅助线程的执行直到消息处理完毕,这会降低效率。如果用PostMessage,那么在函数立即返回后线程还可以干别的事情,因此,考虑到效率问题,这里使用了PostMessage而不是SendMessage。 函数ReadComm和WriteComm分别用来从m_hCom通信设备中读/写指定数量的字符。ReadComm函数很简单,由于对读超时的特殊设定,ReadFile函数会立即返回并完成操作,并在length变量中报告实际读入的字符数。此时,没有必要调用等待函数或GetOverlappedResult。在WriteComm中,调用GerOverlappedResult来等待操作结果,直到超时发生。不管是否超时,该函数在结束等待后都会报告实际的传输字符数。 CloseConnection函数的主要任务是终止辅助线程并关闭m_hCom通信设备。为了终止线程,该函数设置了一系列信号,以结束辅助线程中的等待和循环,然后调用WaitForSingleObject等待线程结束。 清单12.7 CTerminalView类的部分代码 // TerminalView.h : interface of the CTerminalView class /////////////////////////////////////////////////////////////////////////////
class CTerminalView : public CEditView { . . . afx_msg LRESULT OnCommNotify(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() };
// TerminalView.cpp : implementation of the CTerminalView class // BEGIN_MESSAGE_MAP(CTerminalView, CEditView) . . . ON_MESSAGE(WM_COMMNOTIFY, OnCommNotify) END_MESSAGE_MAP()
LRESULT CTerminalView::OnCommNotify(WPARAM wParam, LPARAM lParam) { char buf[MAXBLOCK/4]; CString str; int nLength, nTextLength; CTerminalDoc* pDoc=GetDocument(); CEdit& edit=GetEditCtrl(); if(!pDoc->m_bConnected || (wParam & EV_RXCHAR)!=EV_RXCHAR) // 是否是EV_RXCHAR事件? { SetEvent(pDoc->m_hPostMsgEvent); // 允许发送下一个WM_COMMNOTIFY消息 return 0L; } nLength=pDoc->ReadComm(buf,100); if(nLength) { nTextLength=edit.GetWindowTextLength(); edit.SetSel(nTextLength,nTextLength); //移动插入光标到正文末尾 for(int i=0;i { switch(buf[i]) { case '\r': // 回车 if(!pDoc->m_bNewLine) break; case '\n': // 换行 str+="\r\n"; break; case '\b': // 退格 edit.SetSel(-1, 0); edit.ReplaceSel(str); nTextLength=edit.GetWindowTextLength(); edit.SetSel(nTextLength-1,nTextLength); edit.ReplaceSel(""); //回退一个字符 str=""; break; case '\a': // 振铃 MessageBeep((UINT)-1); break; default : str+=buf[i]; } } edit.SetSel(-1, 0); edit.ReplaceSel(str); // 向编辑视图中插入收到的字符 } SetEvent(pDoc->m_hPostMsgEvent); // 允许发送下一个WM_COMMNOTIFY消息 return 0L; } void CTerminalView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default CTerminalDoc* pDoc=GetDocument(); char c=(char)nChar; if(!pDoc->m_bConnected)return; pDoc->WriteComm(&c, 1); if(pDoc->m_bEcho) CEditView::OnChar(nChar, nRepCnt, nFlags); // 本地回显 } CTerminalView是CEditView的派生类,利用CEditView的编辑功能,可以大大简化程序的设计。 OnChar函数对WM_CHAR消息进行处理,它调用CTerminalDoc::WriteComm把用户键入的字符从串行口输出。如果设置了Local echo,那么就调用CEditView::OnChar把字符输出到视图中。 OnCommNotify是WM_COMMNOTIFY消息的处理函数。该函数调用CTerminalDoc::ReadComm从串行口输入缓冲区中读入字符并把它们输出到编辑视图中。在输出前,函数会对一些特殊字符进行处理。如果读者对控制编辑视图的代码不太明白,那么请参见6.1.4。在函数返回时,要调用SetEvent把m_hPostMsgEvent置为有信号。 清单12.8 CSetupDlg类的部分代码 // SetupDlg.h : header file // class CSetupDlg : public CDialog { . . . public: BOOL m_bConnected; . . . }; // SetupDlg.cpp : implementation file // BOOL CSetupDlg::OnInitDialog() { CDialog::OnInitDialog(); // TODO: Add extra initialization here GetDlgItem(IDC_PORT)->EnableWindow(!m_bConnected); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } CSetupDlg的主要任务是配置通信参数。在OnInitDialog函数中,要根据当前是否连接来允许/禁止Port组合框。因为在打开一个连接后,显然不能随便改变端口。 |