Win 32系统为串行通信提供了全新的服务。传统的OpenComm、ReadComm、WriteComm、CloseComm等函数已经过时,WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函数提供的打开和关闭通信资源句柄及读写操作的基本接口。
新的文件I/O函数(CreateFile、ReadFile、WriteFile等)支持重叠式输入输出,这使得线程可以从费时的I/O操作中解放出来,从而极大地提高了程序的运行效率。
12.3.1 串行口的打开和关闭
Win 32系统把文件的概念进行了扩展。无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用API函数CreateFile来打开或创建的。该函数的声明为:
HANDLE CreateFile(
LPCTSTR lpFileName, // 文件名
DWORD dwDesiredAccess, // 访问模式
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常为NULL
DWORD dwCreationDistribution, // 创建方式
DWORD dwFlagsAndAttributes, // 文件属性和标志
HANDLE hTemplateFile // 临时文件的句柄,通常为NULL
);
如果调用成功,那么该函数返回文件的句柄,如果调用失败,则函数返回INVALID_HANDLE_VALUE。
如果想要用重叠I/O方式(参见12.3.3)打开COM2口,则一般应象清单12.4那样调用CreateFile函数。注意在打开一个通信端口时,应该以独占方式打开,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和FILE_ATTRIBUTE_NORMAL等属性。如果要打开重叠I/O,则应该指定 FILE_FLAG_OVERLAPPED属性。
清单12.4
HANDLE hCom;
DWORD dwError;
hCom=CreateFile(“COM2”, // 文件名
GENERIC_READ | GENERIC_WRITE, // 允许读和写
0, // 独占方式
NULL,
OPEN_EXISTING, //打开而不是创建
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重叠方式
NULL
);
if(hCom = = INVALID_HANDLE_VALUE)
{
dwError=GetLastError( );
. . . // 处理错误
}
当不再使用文件句柄时,应该调用CloseHandle函数关闭之。
12.3.2 串行口的初始化
在打开通信设备句柄后,常常需要对串行口进行一些初始化工作。这需要通过一个DCB结构来进行。DCB结构包含了诸如波特率、每个字符的数据位数、奇偶校验和停止位数等信息。在查询或配置置串行口的属性时,都要用DCB结构来作为缓冲区。
调用GetCommState函数可以获得串口的配置,该函数把当前配置填充到一个DCB结构中。一般在用CreateFile打开串行口后,可以调用GetCommState函数来获取串行口的初始配置。要修改串行口的配置,应该先修改DCB结构,然后再调用SetCommState函数用指定的DCB结构来设置串行口。
除了在DCB中的设置外,程序一般还需要设置I/O缓冲区的大小和超时。Windows用I/O缓冲区来暂存串行口输入和输出的数据,如果通信的速率较高,则应该设置较大的缓冲区。调用SetupComm函数可以设置串行口的输入和输出缓冲区的大小。
在用ReadFile和WriteFile读写串行口时,需要考虑超时问题。如果在指定的时间内没有读出或写入指定数量的字符,那么ReadFile或WriteFile的操作就会结束。要查询当前的超时设置应调用GetCommTimeouts函数,该函数会填充一个COMMTIMEOUTS结构。调用SetCommTimeouts可以用某一个COMMTIMEOUTS结构的内容来设置超时。
有两种超时:间隔超时和总超时。间隔超时是指在接收时两个字符之间的最大时延,总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读操作两种超时均支持。用COMMTIMEOUTS结构可以规定读/写操作的超时,该结构的定义为:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; // 读间隔超时
DWORD ReadTotalTimeoutMultiplier; // 读时间系数
DWORD ReadTotalTimeoutConstant; // 读时间常量
DWORD WriteTotalTimeoutMultiplier; // 写时间系数
DWORD WriteTotalTimeoutConstant; // 写时间常量
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
COMMTIMEOUTS结构的成员都以毫秒为单位。总超时的计算公式是:
总超时=时间系数×要求读/写的字符数 + 时间常量
例如,如果要读入10个字符,那么读操作的总超时的计算公式为:
读总超时=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant
可以看出,间隔超时和总超时的设置是不相关的,这可以方便通信程序灵活地设置各种超时。
如果所有写超时参数均为0,那么就不使用写超时。如果ReadIntervalTimeout为0,那么就不使用读间隔超时,如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都为0,则不使用读总超时。如果读间隔超时被设置成MAXDWORD并且两个读总超时为0,那么在读一次输入缓冲区中的内容后读操作就立即完成,而不管是否读入了要求的字符。
在用重叠方式读写串行口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。在这种情况下,超时规定的是操作的完成时间,而不是ReadFile和WriteFile的返回时间。
清单12.5列出了一段简单的串行口初始化代码。
清单12.5 打开并初始化串行口
HANDLE hCom;
DWORD dwError;
DCB dcb;
COMMTIMEOUTS TimeOuts;
hCom=CreateFile(“COM2”, // 文件名
GENERIC_READ | GENERIC_WRITE, // 允许读和写
0, // 独占方式
NULL,
OPEN_EXISTING, //打开而不是创建
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重叠方式
NULL
);
if(hCom = = INVALID_HANDLE_VALUE)
{
dwError=GetLastError( );
. . . // 处理错误
}
SetupComm( hCom, 1024, 1024 ) //缓冲区的大小为1024
TimeOuts. ReadIntervalTimeout=1000;
TimeOuts.ReadTotalTimeoutMultiplier=500;
TimeOuts.ReadTotalTimeoutConstant=5000;
TimeOuts.WriteTotalTimeoutMultiplier=500;
TimeOuts.WriteTotalTimeoutConstant=5000;
SetCommTimeouts(hCom, &TimeOuts); // 设置超时
GetCommState(hCom, &dcb);
dcb.BaudRate=2400; // 波特率为2400
dcb.ByteSize=8; // 每个字符有8位
dcb.Parity=NOPARITY; //无校验
dcb.StopBits=ONESTOPBIT; //一个停止位
SetCommState(hCom, &dcb);
12.3.3 重叠I/O
在用ReadFile和WriteFile读写串行口时,既可以同步执行,也可以重叠(异步)执行。在同步执行时,函数直到操作完成后才返回。这意味着在同步执行时线程会被阻塞,从而导致效率下降。在重叠执行时,即使操作还未完成,调用的函数也会立即返回。费时的I/O操作在后台进行,这样线程就可以干别的事情。例如,线程可以在不同的句柄上同时执行I/O操作,甚至可以在同一句柄上同时进行读写操作。“重叠”一词的含义就在于此。
ReadFile函数只要在串行口输入缓冲区中读入指定数量的字符,就算完成操作。而WriteFile函数不但要把指定数量的字符拷入到输出缓冲中,而且要等这些字符从串行口送出去后才算完成操作。
ReadFile和WriteFile函数是否为执行重叠操作是由CreateFile函数决定的。如果在调用CreateFile创建句柄时指定了FILE_FLAG_OVERLAPPED标志,那么调用ReadFile和WriteFile对该句柄进行的读写操作就是重叠的,如果未指定重叠标志,则读写操作是同步的。
函数ReadFile和WriteFile的参数和返回值很相似。这里仅列出ReadFile函数的声明:
BOOL ReadFile(
HANDLE hFile, // 文件句柄
LPVOID lpBuffer, // 读缓冲区
DWORD nNumberOfBytesToRead, // 要求读入的字节数
LPDWORD lpNumberOfBytesRead, // 实际读入的字节数
LPOVERLAPPED lpOverlapped // 指向一个OVERLAPPED结构
); //若返回TRUE则表明操作成功
需要注意的是如果该函数因为超时而返回,那么返回值是TRUE。参数lpOverlapped在重叠操作时应该指向一个OVERLAPPED结构,如果该参数为NULL,那么函数将进行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED标志建立的。
当ReadFile和WriteFile返回FALSE时,不一定就是操作失败,线程应该调用GetLastError函数分析返回的结果。例如,在重叠操作时如果操作还未完成函数就返回,那么函数就返回FALSE,而且GetLastError函数返回ERROR_IO_PENDING。
在使用重叠I/O时,线程需要创建OVERLAPPED结构以供读写函数使用。OVERLAPPED结构最重要的成员是hEvent,hEvent是一个事件对象句柄,线程应该用CreateEvent函数为hEvent成员创建一个手工重置事件,hEvent成员将作为线程的同步对象使用。如果读写函数未完成操作就返回,就那么把hEvent成员设置成无信号的。操作完成后(包括超时),hEvent会变成有信号的。
如果GetLastError函数返回ERROR_IO_PENDING,则说明重叠操作还为完成,线程可以等待操作完成。有两种等待办法:一种办法是用象WaitForSingleObject这样的等待函数来等待OVERLAPPED结构的hEvent成员,可以规定等待的时间,在等待函数返回后,调用GetOverlappedResult。另一种办法是调用GetOverlappedResult函数等待,如果指定该函数的bWait参数为TRUE,那么该函数将等待OVERLAPPED结构的hEvent 事件。GetOverlappedResult可以返回一个OVERLAPPED结构来报告包括实际传输字节在内的重叠操作结果。
如果规定了读/写操作的超时,那么当超过规定时间后,hEvent成员会变成有信号的。因此,在超时发生后,WaitForSingleObject和GetOverlappedResult都会结束等待。WaitForSingleObject的dwMilliseconds参数会规定一个等待超时,该函数实际等待的时间是两个超时的最小值。注意GetOverlappedResult不能设置等待的时限,因此如果hEvent成员无信号,则该函数将一直等待下去。
在调用ReadFile和WriteFile之前,线程应该调用ClearCommError函数清除错误标志。该函数负责报告指定的错误和设备的当前状态。
调用PurgeComm函数可以终止正在进行的读写操作,该函数还会清除输入或输出缓冲区中的内容。
12.3.4 通信事件
在Windows 95/NT中,WM_COMMNOTIFY消息已经取消,在串行口产生一个通信事件时,程序并不会收到通知消息。线程需要调用WaitCommEvent函数来监视发生在串行口中的各种事件,该函数的第二个参数返回一个事件屏蔽变量,用来指示事件的类型。线程可以用SetCommMask建立事件屏蔽以指定要监视的事件,表12.4列出了可以监视的事件。调用GetCommMask可以查询串行口当前的事件屏蔽。
表12.4 通信事件
事件屏蔽 |
含义 |
EV_BREAK |
检测到一个输入中断 |
EV_CTS |
CTS信号发生变化 |
EV_DSR |
DSR信号发生变化 |
EV_ERR |
发生行状态错误 |
EV_RING |
检测到振铃信号 |
EV_RLSD |
RLSD(CD)信号发生变化 |
EV_RXCHAR |
输入缓冲区接收到新字符 |
EV_RXFLAG |
输入缓冲区收到事件字符 |
EV_TXEMPTY |
发送缓冲区为空 |
WaitCommEvent即可以同步使用,也可以重叠使用。如果串口是用FILE_FLAG_OVERLAPPED标志打开的,那么WaitCommEvent就进行重叠操作,此时该函数需要一个OVERLAPPED结构。线程可以调用等待函数或GetOverlappedResult函数来等待重叠操作的完成。
当指定范围内的某一事件发生后,线程就结束等待并把该事件的屏蔽码设置到事件屏蔽变量中。需要注意的是,WaitCommEvent只检测调用该函数后发生的事件。例如,如果在调用WaitCommEvent前在输入缓冲区中就有字符,则不会因为这些字符而产生EV_RXCHAR事件。
如果检测到输入的硬件信号(如CTS、RTS和CD信号等)发生了变化,线程可以调用GetCommMaskStatus函数来查询它们的状态。而用EscapeCommFunction函数可以控制输出的硬件信号(如DTR和RTS信号)。
为了使读者更好地掌握本章的概念,这里举一个具体实例来说明问题。如图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的这些特性,我们在程序中创建了一个辅助工作者线程专门来监视串行口的输入。由于写入串行口的数据量不大,不会太费时,所以在主线程中完成写端口的任务是可以的,不必另外创建线程。
现在就让我们开始工作。请读者按下面几步进行:
用AppWizard建立一个名为Terminal的MFC应用程序。在MFC AppWizard对话框的第1步选择Single document,在第4步去掉Docking toolbar的选择,在第6步把CTerminalView的基类改为CEditView。
在Terminal工程的资源视图中打开IDR_MAINFRAME菜单资源。去掉Edit菜单和View菜单,并去掉File菜单中除Exit以外的所有菜单项。然后在File菜单中加入三个菜单项,如表12.5所示。
表12.5 新菜单项
标题 |
ID |
Settings... |
ID_FILE_SETTINGS |
Connect |
ID_FILE_CONNECT |
Disconnect |
ID_FILE_DISCONNECT |
用ClassWizard为CTerminalDoc类创建三个与上表菜单消息对应的命令处理函数,使用缺省的函数名。为ID_FILE_CONNECT和ID_FILE_DISCONNECT命令创建命令更新处理函数。另外,用ClassWizard为该类加入CanCloseFrame成员函数。
用ClassWizard为CTerminalView类创建OnChar函数,该函数用来把用户键入的字符向串行口输出。
新建一个对话框模板资源,令其ID为IDD_COMSETTINGS。请按图12.2和表12.6设计对话框模板。
表12.6 通信设置对话框中的主要控件
控件 |
ID |
属性设置 |
Base options组框 |
缺省 |
标题为Base options |
Port组合框 |
IDC_PORT |
Drop List,不选Sort,初始列表为COM1、COM2、COM3、COM4 |
Baud rate组合框 |
IDC_BAUD |
Drop List,不选Sort,初始列表为300、600、1200、2400、9600、14400、19200、38400、57600 |
Data bits组合框 |
IDC_DATABITS |
Drop List,不选Sort,初列表为5、6、7、8 |
Parity组合框 |
IDC_PARITY |
Drop List,不选Sort,初列表为None、Even、Odd |
Stop bits组合框 |
IDC_STOPBITS |
Drop List,不选Sort,初列表为1、1.5、2 |
Flow control组框 |
缺省 |
标题为Flow control |
None单选按钮 |
IDC_FLOWCTRL |
标题为None,选择Group属性 |
RTS/CTS单选按钮 |
缺省 |
标题为RTS/CTS |
XON/XOFF单选按钮 |
缺省 |
标题为XON/XOFF |
TTY options组框 |
缺省 |
标题为TTY options |
New line检查框 |
IDC_NEWLINE |
标题为New line |
Local echo检查框 |
IDC_ECHO |
标题为Local echo |
打开ClassWizard,为IDD_COMSETTINGS模板创建一个名为CSetupDlg的对话框类。为该类加入OnInitDialog成员函数,并按表12.7加入数据成员。
表12.7 CSetupDlg类的数据成员
控件ID |
变量名 |
数据类型 |
IDC_BAND |
m_sBaud |
CString |
IDC_DATABITS |
m_sDataBits |
CString |
IDC_ECHO |
m_bEcho |
BOOL |
IDC_FLOWCTRL |
m_nFlowCtrl |
int |
IDC_NEWLINE |
m_bNewLine |
BOOL |
IDC_PARITY |
m_nParity |
int |
IDC_PORT |
m_sPort |
CString |
IDC_STOPBITS |
m_nStopBits |
int |
按清单12.6、12.7和12.8修改程序。清单12.6列出了CTerminalDoc类的部分代码,清单12.7是CTerminalView的部分代码,清单12.8是CSetupDlg类的部分代码。在本例中使用了WM_COMMNOTIFY消息。虽然在Win32中,WM_COMMNOTIFY消息已经取消,系统自己不会产生该消息,但Visual C++对该消息的定义依然保留。考虑到使用习惯,Terminal程序辅助线程通过发送该消息来通知视图有通信事件发生。
清单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组合框。因为在打开一个连接后,显然不能随便改变端口。
本章重点介绍了Win 32环境下的多线程和串行通信编程。本章的要点如下:
Windows 3.x实行的是协同式多任务,应用程序必须“自觉”地放弃CPU控制权,否则系统会被挂起。
Windows 95/NT实现了抢先式多任务,应用程序对CPU的控制时间由系统分配,系统可以在任何时候中断应用程序,并把控制权转交给别的程序。
在Win 32环境下,每个进程可以同时执行多个线程。线程是系统分配CPU时间片的基本实体,系统在所有线程之间快速切换以实现多任务。
由于同一进程的所有线程共享进程的虚拟地址空间、Windows 95的重入问题、MFC在对象级的线程不安全性以及线程之间的协调等原因,多个线程必须同步执行。同步机制是由同步对象和等待函数共同实现的。同步对象主要包括事件、mutex和信号灯,进程和线程句柄、文件和通信设备也可以用作同步对象。
在Win 32中,传统的OpenComm、ReadComm、WriteComm、CloseComm等串行通信函数已经过时,WM_COMMNOTIFY消息也消失了。程序应该调用CreateFile打开一个串行通信设备,用ReadFile和WriteFile来进行I/O操作,用WaitCommEvent来监视通信事件。ReadFile、WriteFile和WaitCommEvent既可以同步操作,也可以重叠操作。
利用Win 32的重叠I/O操作和多线程特性,程序员可以编写出高效的通信程序。