串行通信与重叠I/O

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命令则关闭串行口。

T12_1.tif (174388 bytes)

图12.1 Terminal终端仿真程序

  当用户选择File->Settings...命令时,会弹出一个Communication settings对话框,如图12.2所示。该对话框主要用来设置串行口,包括端口、波特率、每字节位数、校验、停止位数和流控制。

T12_2.tif (92960 bytes)

图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<nLength;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操作和多线程特性,程序员可以编写出高效的通信程序。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Visual C++/Turbo C串口通信编程实践及源代码 第1章 轻松体验串口通信编程与调试 1 1.1 使用串口调试助手来体验串口通信 1 1.2 体验windows环境下的visual c++串口通信编程 4 1.3 体验dos环境下turbo c串口通信编程 12 第2章 多线程串口编程工具cserialport类 16 2.1 cserialport类的功能及成员函数介绍 16 2.2 应用cserialport类编制基于对话框的应用程序 30 2.3 应用cserialport类编制基于单文档的应用程序 35 2.4 对cserialport类的改进 40 2.4.1 改进一:ascii文本和二进制数据发送方式兼容 40 2.4.2 改进二:也许能解决内存泄漏 43 2.4.3 改进三:彻底关闭串口,释放串口资源 44 第3章 控件mscomm串口编程 46 3.1 mscomm控件介绍 46 3.1.1 vc中应用mscomm控件编程步骤 46 3.1.2 mscomm控件串行通信处理方式 47 3.1.3 mscomm 控件的属性说明 48 3.1.4 mscomm控件错误信息 55 3.2 使用mscomm控件的几个疑难问题 56 3.2.1 使用variant 和safearray 数据类型从串口读写数据 56 .3.2.2 mscomm控件能离开对话框独立存在吗 59 3.2.3 如何发送接收ascii值为0和大于128的字符 60 3.2.4 在同一程序中用mscomm控件控制多个串口的具体操作方法 62 3.2.5 解决使用控件编程时程序占用的内存会不断增大的问题 62 3.2.6 在mscomm控件串口编程时遇到的其他问题 63 3.3 在基于单文档(sdi)程序中应用mscomm控件 63 3.4 应用mscomm控件控制多个串口实例 69 3.5 串口与modem拨号应用简例 76 3.5.1 创建工程 76 3.5.2 代码分析 78 3.5.3 应用 85 第4章 windows api串口编程 87 4.1 windows api串口编程概述 87 4.2 api串口编程中用到的结构及相关概念说明 89 4.2.1 dcb(device control block)结构 89 4.2.2 超时设置commtimeouts结构 92 4.2.3 overlapped异步i/o重叠结构 94 4.2.4 通信错误与通信设备状态 95 4.2.5 串行通信事件 96 4.3 windows api串行通信函数 97 4.4 win32 api串口通信编程的一般流程和特殊实例 116 4.4.1 win32 api串口通信编程的一般流程 116 4.4.2 用查询方式串口 116 4.4.3 同步i/o读写数据 117 4.4.4 关于流控制的设置问题 118 4.5 cserialport类中的api函数编程应用剖析 119 4.6 win32 api串口编程tty(虚拟终端)实例 128 4.6.1 建立程序工程 128 4.6.2 建立串口设置对话框 129 4.6.3 编写ctermdoc类的相关代码 132 4.6.4 小结 141 4.6.5 在ctermview类中字添加符键入处理代码与串口接收处理代码 142 第5章 串口调试助手v2.2编程 147 5.1 建立scomm程序工程实现界面功能 147 5.2 串口的初始化及关闭 150 5.3 串口数据的发送与接收及十六进制数据的处理 151 5.3.1 十六进数据发送处理 152 5.3.2 手动发送处理 152 5.3.3 自动发送处理 153 5.3.4 接收处理及十六进制显示 154 5.4 其他辅助功能的实现 156 5.4.1 接收数据的文件保存 156 5.4.2 实现小文件发送 158 5.4.3 图钉按钮功能使程序能浮在最上层 161 5.4.4 对话框动画图标的实现 162 5.4.5 超链接功能的实现 164 5.4.6 如何打开帮助网页文件 164 第6章 dos环境下的turbo c串口编程及通用实例gserial类 168 6.1 pc机异步通信适配器8250及其编程操作 169 6.1.1 ins8250内部寄存器及其选择方式 169 6.1.2 波特率设置 169 6.1.3 数据位、奇偶校验、停止位等数据格式设置 170 6.1.4 查询i/o方式相关设置 171 6.1.5 中断i/o通信方式相关设置 171 6.1.6 modem寄存器 172 6.2 comrxtx程序实例 173 6.3 通用实例程序gserial类 175 6.4 用gserial类控制多串口 186 6.5 多串口编程pc机高号中断8259a可编程中断控制器的控制 195 第7章 串口通信用户层协议的编制与数据处理方法 197 7.1 通信协议的编制 197 7.1.1 为什么要编制用户通信协议 197 7.1.2 串口通信中用户层协议编制原则 199 7.1.3 在串口通信中几种常用的用户层协议 200 7.2 串口通信数据包处理方法编程实例 202 7.2.1 编程任务 203 7.2.2 编程步骤 203 7.2.3 程序测试 216 第8章 单片机串口通信 218 8.1 单片机串口硬件系统及c51程序开发 218 8.1.1 较典型的单片机硬件系统实例 218 8.1.2 c51语言及程序简介 220 8.1.3 开发c51程序的利器keil c51 uvision2及串口程序仿真 221 8.2 c51单片机串口通信程序实例 226 8.2.1 实例一 226 8.2.2 实例二 227 第9章 串口与网络结合的解决方案及编程 230 9.1 串口与网络结合的硬件解决方案 230 9.2典型串口与联网的设备 231 9.2.1 nport5400系列产品的特点 231 9.2.2 nport 5400系列产品的典型应用介绍 233 9.2.3 nport5400系列产品的设置与编程测试 235 9.3 与access数据库结合的串口通信实例 237 9.3.1 微机网络检测系统说明 237 9.3.2 创建odbc数据源 238 9.3.3 创建工程 239 9.3.4 程序简介 244 9.4 与winsock结合的串口通信实例 246 9.4.1 客户端应用程序 247 9.4.2 服务器应用程序 252 9.5 在已经编好的串口通信程序中加入网络通信功能 260 9.5.1参照mfc appwizard创建winsockets程序 261 9.5.2 利用windows sockets api和第三方提供的类进行编程 262 9.6 串口通信用于遥控操作简例 262 第10章 计算机串口与其他设备通信编程实例 266 10.1通过串口收发短消息 266 10.1.1 sms编码规范及编码与解码例程 266 10.1.2 at命令收发短消息实例 273 10.1.3 "实时"接收短消息的方法 281 10.1.4 用串口收发sms短信编程的一些讨论 283 10.2 计算机与rabbit 2000嵌入式系统通信编程实例 286 10.2.1 rabbit 2000微处理器介绍 286 10.2.2 动态c(dynamic c)语言介绍 287 10.2.3 某车载无线调度系统实例介绍 288 10.3 计算机与plc通信程序实例 294 10.4 matlab环境串口编程通信实例 295 10.4.1 matlab串口类serial应用 295 10.4.2 通过串口使matlab simulink与下位机通讯进行控制 299 10.4.3 xpc目标环境下串口通信实现 299 第11章 串口通信基本概念及标准 302 11.1 串口通信基本概念 302 11.1.1 串行通信概述 302 11.1.2 单工、半双工和全双工的定义 305 11.1.3 同步传送与异步传送 306 11.1.4 串行通信协议 306 11.2 rs-232-c串口标准 309 11.2.1 rs-232-c标准 309 11.2.2 rs-232-c串行通信接线实例 312 11.3 rs-422/485串口标准 314 11.3.1 概述 314 11.3.2 rs-422与rs-485串行接口标准 315 11.3.3 rs-422与rs-485的网络安装注意要点 317 11.3.4 rs-232、rs422、rs485电气参数对比 318 11.4 串口调试注意事项 318 11.5 常用数据校验法 318 11.5.1 奇偶校验 318 11.5.2 循环冗余码校验 319 11.6 串口连接和tcp/ip连接对比 320 11.7 现场总线与rs-232、rs-485的本质区别 320 11.8 modem通信技术 320 11.8.1 modem的基本工作原理 320 11.8.2 modem的功能 322 11.8.3 modem的分类 322 11.8.4 modem的安装 324 11.8.5 modem v.92标准介绍 326 11.8.6 modem的速度 327 11.8.7 modem优化方法 328 11.8.8 modem命令/at命令 329 第12章 不占用串口串口数据捕捉 338 12.1 驱动程序的基本概念:vxd与wdm 338 12.1.1 虚拟设备驱动程序vxd 338 12.1.2 win32驱动程序模型wdm 340 12.1.3 在不同操作系统下选用哪种驱动程序模式 341 12.2 vxd示例程序介绍--vtoolsd中的commhook 341 12.3 串口数据捕捉实例程序 351 12.3.1 编程任务 351 12.3.2 编程步骤 351 12.4 虚拟串口简介 364 附录a turbo c说明 366 附录b ascii码表 376

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值