Windows API 实现串口通信程序

Windows API 实现串口通信程序

注:最近需要利用串口通信调试一个项目,需要先熟悉一下串口通信方式,由于利用Windows API 实现这种方法可以清楚地掌握串口通信的机制,并且自由灵活,因此选用这种方式实现最佳。但是查了好多资料,几乎大部分都是介绍API ,很少有在代码实现上做出全面的介绍的。因此,本人基于vs2015利用Windows API 实现了串口通信的程序。仅此而已,与大家分享,,,,,
下面介绍一下API串口编程的一般步骤及相关API函数
无论哪种工作方式,串口操作都分四个步骤来完成:

(1)打开串口
(2)配置串口
(3)读写串口
(4) 关闭串口

Windows API函数介绍:

1、//打开串口设备
在32位的Windows系统中串口和其他的通信设备均作为文件处理的,串口的打开、配置、关闭、读取和写入所用的函数与操作文件的函数完全一致。使用CreateFile()函数可以打开串口,以便进行串口的读写访问操作。CreateFile()返回串口句柄,可以在以后的串口相应操作中使用。

//CreateFile()函数声明:
HANDLE WINAPI CreateFile
(
LPCTSTR lpFileName,       //将要打开的串口逻辑名,如“COMx”;

DWORD dwDesiredAccess,   //指定串口访问的类型,可以是读取、写入或二者并列;

DWORD dwShareMode,    //指定共享属性,由于串口不能共享,该参数必须置为0;

LPSECURITY_ATTRIBUTES lpSecurityAttributes, //引用安全性属性结构,缺省值为NULL;

DWORD dwCreationDisposition,        //创建标志,对串口操作该参数必须置为OPEN_EXISTING;

DWORD dwFlagsAndAttributes,        //属性描述,用于指定串口同步、异步操作,该值为0,表示同步I/O操作; 该值为FILE_FLAG_OVERLAPPED,表示使用异步I/O操作;

HANDLE hTemplateFile            //对串口而言该参数必须置为NULL;
);

其中的参数说明如下:
_lpFileName:将要打开的串口逻辑名,字符串表示,如“COMx”表示串口x。

_dwDesiredAccess:指定串口的操作类型。与文件类似,串口可以被打开以供读取、写入或者两者兼顾。
GENERIC_READ为读操作串口设备,GENERIC_READ为写操作端口设备。对应的常数定义为:
const GENERIC_READ = 0x80000000h;
const GENERIC_WRITE = 0x40000000h;
可以通过位操作实现串口设备的读/写操作。因为大部分串口通信都是双向的,如:
dwDesiredAccess = GENERIC_READ | GENERIC_WRITE;

_dwShareMode:指定串口设备的共享属性。该参数主要是为那些可以由许多应用程序共享的文件提供使用。对于不能共享的串口设备,其值必须为0。这是文件与串口等通信设备之间的主要差异之一。如果在当前的应用程序调用CreateFile()时,另一个应用程序已经打开了串口,该函数就会返回错误代码,原因是两个应用程序不能共享一个端口。然而,同一个应用程序的多个线程可以共享由CreateFile()返回的端口句柄,并且根据安全性属性设置,该句柄可以被打开端口的应用程序的子程序所继承。

_lpSecurityAttributes:引用安全性属性结构,该结构定义了一些属性,例如句柄如何被打开端口的应用程序的子程序所继承。将该参数设置为NULL将为该端口分配缺省的安全性属性。子应用程序所继承的缺省属性是该端口不能被继承的。
安全属性结构SECURITY_ARRTIBUTES结构声明:
typedef struct_SECURITY_ARRTIBUTE
{
DWORD   nLength;

LPVOID   IpSecurityDescriptor;

BOOLHandlt  bInheritHandle;

} SECURITY_ARRTIBUTE;
结构成员nLength指明该结构体的长度,lpSecurityDescriptor指向一个安全描述字符,bInheritHandle表明句柄是否能被继承。

_dwCreationDisposition:因为串口总是存在的,该值必须设置为OPEN_EXISTING。该标志表示Windows不用企图创建新端口,而是打开已经存在的端口。OPEN_EXISTING常数定义为:
const OPEN_EXISTING = 3;

_dwFlagsAndAttributes:描述了串口设备的各种属性。对于文件来说,可能有很多属性,但对于串口,唯一有意义的设置是FILE_FLAG_OVERLAPPED。当创建时指定该值,端口I/O可以在后台进行(也称异步I/O)。
FILE_FLAG_OVERLAPPED常数定义:
const FILE_FLAG_OVERLAPPED = 0x40000000h

_hTemplateFile:一个指向模板文件的句柄,且该模板必须是以GENERIC_READ访问方式打开的。如果此参数不是NULL,则会使用hTemplateFile关联的文件的属性和标志来创建文件。另外,如果是打开一个现有文件,则该参数被忽略。
重叠I/O打开串口的示例代码:

//打开串口
    hcom_ = CreateFile(port,            //串口号
        GENERIC_READ | GENERIC_WRITE,   //允许读和写
        0,                              //独占方式
        NULL, 
        OPEN_EXISTING,                  //打开而不是创建
        FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,//设置异步标识
        NULL);
    if (hcom_==INVALID_HANDLE_VALUE )
    {
        return false;
    }

2、//串口配置及属性
(1)、串口配置_DCB
CreateFile函数打开串口后,系统会根据上次打开串口时设置的值来初始化串口,可以集成上次打开操作后的数值,包括设备控制块(DCB)和超时控制结构(COMMTIMEOUTS)。如果是首次打开串口,Windows操作系统就会使用缺省的配置。在打开通讯设备句柄后,需要根据需求对串口进行一些初始化配置工作。这需要通过一个DCB结构来进行。DCB结构包含了诸如波特率、数据位数、奇偶校验和停止位数等信息。在查询或配置串口的属性时,需要用DCB结构来作为缓冲区。
一般用CreateFile打开串口后,可以调用GetCommState()函数获取串口的当前的DCB配置。要修改串口的配置,应先修改DCB结构,使用SetCommState()重新分配串口资源的各个参数。

GetCommState()函数声明:
BOOL GetCommState
(
HANDLE hFile, // 串口设备句柄

LPDCB lpDCB // 指向DCB结构体的指针
);
其中的参数说明如下:
_hFile:由CreateFile()函数返回的指向已打开串口的句柄。

_lpDCB:一个非常重要的结构体—设备控制块DCB。

DCB结构的主要参数说明如下:
DCBLength: 以字节为单位的DCB结构的大小。

Baudrate: 用于指定串口设备通信的数据传输速率,它可以是实际的数据传输速率数值,也可以是下列数据之一:CBR_110, CBR_19200, CBR_300, CBR_38400, CBR_600, CBR_56000, CBR_1200, CBR_57600, CBR_2400, CBR_115200, CBR_4800, CBR_12800, CBR_9600, CBR_25600, CBR_14400。

fBinary: 指定是否允许二进制。Win32API不支持非二进制传输,因此这个参数必须设置为TRUE,如果设置为FALSE则不能正常工作。

fParity: 指定是否允许奇偶校验,如果这个参数设置为TRUE,则执行奇偶校验并报告错误信息。

fOutxCtsFlow: 指定CTS是否用于检测发送流控制。当该成员为TRUE,而CTS为OFF时,发送将被挂起,直到CTS置ON。

fOutxDsrFlow: 指定DSR是否用于检测发送流控制,当该成员为TRUE,而DSR为OFF时,发送将被挂起,直到DSR置ON。

fDtrControl: 指定DTR流量控制,可以是表中任一值。

表一 DTR流量控制

功能描述
DTR_CONTROL_DISABLE 禁止DTR线,并保持禁止状态
DTR_CONTROL_ENABLE 允许DTR线,并保持允许状态
DTR_CONTROL_HANDSHAKE 允许DTR握手,如果允许握手,则不允许应用程序使用EscapeCommFunction函数调整线路

fDsrSensitivity: 指定通信驱动程序对DTR信号线是否敏感,如果该位置设为TRUE时,DSR信号为OFF,接收的任何字节将被忽略。

fTXContinueOnXoff: 指定当接收缓冲区已满,并且驱动程序已经发送出XoffChar字符时发送是否停止。当该成员为TRUE时,在接收缓冲区内接收到了缓冲区已满的字节XoffLim,并且驱动程序已经发送出XoffChar字符终止接收字节之后,发送继续进行。该成员为FALSE时,接收缓冲区接收到代表缓冲区已空的字节XonLim,并且驱动程序已经发送出恢复发送的XonChar字符后,发送可以继续进行。

fOutX: 该成员为TRUE时,接收到XoffChar之后停止发送,接收到XonChar之后发送将重新开始。

fInX: 该成员为TRUE时,接收缓冲区内接收到代表缓冲区满的字节XoffLim之后,XoffChar发送出去,接收缓冲区接收到代表缓冲区已空的字节XonLim之后,XonChar发送出去。

fErrorChar: 当该成员为TRUE,并且fParity为TRUE时,就会用ErrorChar成员指定的字符来代替奇偶校验错误的接收字符。

fNull: 指明是否丢弃接收到的NULL( ASCII 0 )字符,该成员为TRUE时,接收时去掉空(零值)字节;反之则不丢弃。

fRtsControl: 指定 RTS 流量控制,可以取表2中的值。0值和DTR_CONTROL_HANDSHAKE等价。

表二 RTS 流量控制

功能描述
RTS_CONTROL_DISABLE 打开设备时禁止RTS线,并保持禁止状态
RTS_CONTROL_ENABLE 打开设备时允许RTS线,并保持允许状态
DTR_CONTROL_HANDSHAKE 允许握手。在接收缓冲区小于半满时将RTS 置为ON,在接收缓冲区超过3/4时将RTS置为OFF。如果允许握手,则不允许应用程序使用EscapeCommFunction函数调整线路
DTR_CONTROL_TOGGLE 当发送的字节有效,将RTS置为 ON,发送完缓冲区的所有字节后, RTS置为OFF

fAbortOnError: 如果发送错误,指定是否可以终止读、写操作。如果该位为TRUE,当发生错误时,驱动程序以出错状态终止所有的读写操作。只有当应用程序调用ClearCommError()函数处理后,串口才能接收随后的通信操作。

fDummy2: 保留的位,没有使用。

wReserved:没有使用,必须为零。

XonLim: 指定在XOFF字符发送之前接收到缓冲区中可允许的最小字节数。

XoffLim: 指定在XOFF字符发送之前缓冲区中可允许的最小可用字节数

ByteSize: 指定端口当前使用的数据位数。

Parity: 指定端口当前使用的奇偶校验方法。它的可能值如表3所示。

表三 奇偶校验方法

功能描述
EVENPARITY 偶校验
MARKPARITY 标号校验
NOPARITY 无校验
ODDPARITY 奇校验
SPACEPARITY 空格效益

StopBits: 指定串口当前使用的停止位数,可能值如表4所示。

表四  停止位数描述

功能描述
ONESTOPBIT 1位停止位
ONE5STOPBITS 1.5位停止位
TWOSTOPBITS 2位停止位

·XonChar: 指明发送和接收的XON字符值,它表明允许继续传输。

·XoffChar: 指明发送和接收的XOFF字符值,它表示暂停数据传输。

·ErrorChar: 本字符用来代替接收到的奇偶校验发生错误的字符。

·EofChar: 用来表示数据的结束。

·EvtChar: 事件字符。当接收到此字符的时候,会产生一个事件。

·wReserved1: 保留的位,没有使用。


虽然DCB结构体包含了串口的各项参数设置,但串口通信中该结构中常用以下变量其他可保持默认设置
DWORD BaudRate; //波特率,指定通信设备的传输速率。这个成员可以是实际波特率值或者下面的常量值之一:
CBR_110,CBR_300,CBR_600,CBR_1200,CBR_2400,CBR_4800,CBR_9600,CBR_19200, CBR_38400,
CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000, CBR_14400

DWORD fParity; //指定奇偶校验使能。若此成员为1,允许奇偶校验检查


BYTE ByteSize; //通信字节位数,4—8


BYTE Parity; //指定奇偶校验方法。此成员可以有下列值:


EVENPARITY偶校验 NOPARITY 无校验


MARKPARITY标记校验 ODDPARITY 奇校验


BYTE StopBits; //指定停止位的位数。此成员可以有下列值:
ONESTOPBIT    :1位停止位
TWOSTOPBITS   :2位停止位
ONE5STOPBITS  :1.5位停止位

winbase.h文件中定义了以上用到的常量。如下:

define NOPARITY 0

define ODDPARITY 1

define EVENPARITY 2

define ONESTOPBIT 0

define ONE5STOPBITS 1

define TWOSTOPBITS 2

define CBR_110 110

define CBR_300 300

define CBR_600 600

define CBR_1200 1200

define CBR_2400 2400

define CBR_4800 4800

define CBR_9600 9600

define CBR_14400 14400

define CBR_19200 19200

define CBR_38400 38400

define CBR_56000 56000

define CBR_57600 57600

define CBR_115200 115200

define CBR_128000 128000

define CBR_256000 256000

SetCommState()函数声明如下:
BOOL SetCommState
(
HANDLE hFile, // 已打开的串口的句柄

LPDCB lpDCB // 指向DCB结构的指针
);
其中的参数说明如下:
第一参数hFile是由CreateFile()函数返回的已打开的串口的句柄,第二个参数也是指向DCB结构的。如果函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。出错时可以调用GetLastError()函数获得进一步的出错信息。

DCB dcb;                    //DCB(设备控制块)结构
    GetCommState(hcom_, &dcb);  //获得COM口的设备控制块,从而获得相关参数
    //配置串口相关参数
    dcb.DCBlength = sizeof(dcb);
    dcb.Parity = NOPARITY;      //无奇偶校验位
    dcb.BaudRate = CBR_9600;    //波特率9600
    dcb.StopBits = ONESTOPBIT;  //一个停止位
    SetCommState(hcom_, &dcb);  //设置COM口的设备控制块

(2)、串口配置_设置超时
在用ReadFile和WriteFile读写串行口时引入了超时结构。超时结构直接影响读和写的操作行为。当事先设定的超时间隔结束时,ReadFile() 、 WriteFile()操作仍未结束,那么超时设置将无条件结束读写操作,而不管是否已读出或已写入指定数量的字符。
要查询当前的超时设置应调用GetCommTimeouts函数,该函数会填充一个COMMTIMEOUTS结构。调用SetCommTimeouts可以用某一个COMMTIMEOUTS结构的内容来设置超时。读写串口的超时有两种:间隔超时和总超时。间隔超时是指在接收时两个字符之间的最大时延。总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读操作两种超时均支持。用COMMTIMEOUTS结构可以规定读写操作的超时。
超时结构COMMTIMEOUTS定义为:

typedef struct _COMMTIMEOUTS
{

DWORD ReadIntervalTimeout;     //读间隔超时

DWORD ReadTotalTimeoutMultiplier;  //读时间系数

DWORD ReadTotalTimeoutConstant;  //读时间常量

DWORD WriteTotalTimeoutMultiplier;  // 写时间系数

DWORD WriteTotalTimeoutConstant;  //写时间常量

} COMMTIMEOUTS,*LPCOMMTIMEOUTS

其中主要参数如下:
_ReadIntervalTimeout:以ms为单位指定通信线路上两个字符到达之间的最大时间间隔。在ReadFile()操作期间,从接收到第一个字符时开始计时。如果任意两个字符到达之间的时间间隔超过这个最大值,则ReadFile()操作完成,并返回缓冲数据。如果被置为0,则表示不使用间隔超时。

_ReadTotalTimeoutMultiplier:以ms为单位指定一个系数,该系数用来计算读操作的总超时。

_ReadTotalTimeoutConstant:以ms为单位指定一个常数,该常数也用来计算读操作的总超时。

_WriteTotalTimeoutMultiplier:以ms为单位指定一个系数,该系数用来计算写操作的总超时。

_WriteTotalTimeoutConstant:以ms为单位指定一个常数,该常数也用来计算写操作的总超时。

总超时=时间系数×要求读/写的字符数+时间常量
例如,要读入10个字符,那么读操作的总超时的计算公式为:
读总超时=ReadTotalTimeoutMultiplier×10+ReadTotalTimeoutConstant
该方程使总超时时间成为灵活的工具。总超时时间不是固定值,而是根据读或写的字节数而“漂浮不定”。应用程序通过设置系数为0而只是用常数,和通过设置常数为0而只使用于系数。如果系数和常数都为0,则没有总超时时间。
可以看出:间隔超时和总超时的设置是不相关的,这可以方便通信程序灵活地设置各种超时。
因此每个读操作的总超时时间等于ReadTotalTimeoutMultiplier参数值乘以读操作要读取的字节数再加上ReadTotalTimeoutConstant参数值的和。如果将ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都设置为0,则表示读操作不使用总超时时间。每个读间隔超时参数ReadIntervalTimeout被设置为MAXDWORK,而且两个读总超时参数都被设置为0,那么标识只要一读完接收缓冲区而不管得到什么字符就完成读操作,即使它是空的。当接收中有间隔时,间隔超时将迫使读操作返回。因此使用间隔超时的进程可以设置一个非常短的间隔超时参数,这样它可以实现对一个或一些字符的小的、孤立的数据作出反应。
在用重叠方式读写串口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。在这种情况下,超时规定的是操作的完成时间,而不是ReadFile和WriteFile的返回时间。

调用GetCommTimeouts()函数可以获得当前超时参数。该函数声明如下:
BOOL GetCommTimeouts
(
HANDLE hFile,

LPCOMMTIMEOUTS lpCommTimeouts
);
其中主要参数如下:
_hFile:串口设备句柄,CreateFile()函数返回该句柄;

_lpCommTimeouts:指向一个CommTIMEOUTS结构,返回超时信息。
如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。

如果要设置原来的超时参数,应用程序可以调用SetCommTimeouts()函数。该函数声明如下:
BOOL SetCommTimeouts
(
HANDLE hFile,

LPCOMMTIMEOUTS lpCommTimeouts
);

其中主要参数如下:
_hFile:串口设备句柄,CreateFile()函数返回该句柄;

_lpCommTimeouts:指向一个CommTIMEOUTS结构,设置超时信息。
如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。

COMMTIMEOUTS ct;
    ct.ReadIntervalTimeout = MAXDWORD;//读取无延时,因为有WaitCommEvent等待数据
    ct.ReadTotalTimeoutConstant = 0;  //
    ct.ReadTotalTimeoutMultiplier = 0;//
    ct.WriteTotalTimeoutMultiplier = 500;
    ct.WriteTotalTimeoutConstant = 5000;
    //设定读超时
    SetCommTimeouts(hcom_, &ct);

(3)、串口配置_缓冲区
除了BCD的设置外,一般还需要设置I/O缓冲区的大小和超时。Windows用I/O缓冲区来暂存串口输入和输出的数据。如果通信的速率较高,则应该设置较大的缓冲区。调用SetupComm函数可以设置串行口的输入和输出缓冲区的大小。
SetupComm()函数的声明如下:
BOOL SetupComm
(

HANDLE hFile, // 串口设备的句柄

DWORD dwInQueue, // 输入缓冲区的大小(字节数)

DWORD dwOutQueue // 输出缓冲区的大小(字节数)

);

Windows API 除了提供SetupComm()函数实现初始化的缓冲区控制外,还提供了PurgeComm()函数,在读写串口之前,用PurgeComm()函数清空缓冲区,该函数原型:
PurgeComm()函数的声明如下:
(

HANDLE hFile, // 返回的串口设备句柄

DWORD dwFlags // 执行的动作
);
其中的参数说明如下:
hFile指向由CreateFile函数返回的句柄,dwFlags表示执行的动作,这个参数可以是表5中的任一个。可以调用

表五 dwFlags取值

功能描述
PURGE_TXABORT 即使发送操作没有完成,也终止所有的重叠发送操作,立即返回
PURGE_RXABORT 即使接收操作没有完成,也终止所有的重叠接收操作,立即返回
PURGE_TXCLEAR 清除发送缓冲区
PURGE_RXCLEAR 清除接收缓冲区

(4)、串口配置_通信错误和通信状态
如果在串口通信中发生错误,如中断,奇偶错误等,I/O操作将会终止。程序若要进一步执行I/O操作,必须调用ClearCommError()函数。ClearCommError()函数有两个作用:第一个作用是清除错误;第二个作用是确定串口通信状态。ClearCommError()函数的声明如下:
BOOL ClearCommError
(
HANDLE hFile,

LPDWORD lpErrors,

LPCOMSTAT lpStat
);

其中主要参数介绍如下:
_hFile :标识通信设备,CreateFile()函数返回该句柄。

_lpErrors:指向用一个指明错误类型的掩码填充的32位变量。该参数可以是表6中各值的组合。

_lpStat:指向一个COMSTAT结构,该结构接收设备的状态信息。如果lpStat参数不设置,则没有设备状态信息被返回。

表六 ClearCommError()函数_通信错误列表

描述
CE_BREAK 硬件检测到一个中断条件
CE_FRAME 硬件检测到一个帧出错
CE_IOE 发生I/O错误
CE_MODE 模式出错,或者是句柄无效
CE_OVERRUN 超速错误
CE_RXOVER 接收缓冲区超限,或者是输入缓冲区中没有空间,或者实在文件结束符(EOF)接收后接收到一个字符
CE_RXPARITY 奇偶校验错误
CE_TXFULL 发送缓冲区满
CE_DNS 没有检测到并行设备
CE_OOP 并行设备缺纸
CE_PTO 并行设备发生超时错误

如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。在同步操作时,可以调用ClearCommError()函数来确定串口的接收缓冲区处于等待状态的字节数,而后可以使用ReadFile()或者WriteFile()函数一次读写完。
COMSTAT结构存放有关通信设备的当前信息。该结构内容由ClearCommError()函数填写。COMSTAT结构声明如下:
typedef struct_COMSTAT
(
DWORD fCtsHold: 1;

DWORD fDsrHold: 1;

DWORD fRlsdHold: 1;

DWORD fXoffSent: 1;

DWORD fEof: 1;

DWORD fTxim: 1;

DWORD fReserved: 25;

DWORD cbInQue;

DWORD cbOutQue;

} COMSTAT,*LPCOMSTAT;

其中主要参数介绍如下:

_fCtsHold:指明是否等待CRS信号,如果为1,则发送等待。

_fDsrHold:指明是否等到DRS信号,如果为1,则发送等待。

_fRlsdHold:指明是否等待RLSD信号,如果为1,则发送等待。

_fXoffSent:指明收到XOFF字符后发送是否等待。如果为1,则发送等待。如果把XOFF字符发送给一系统时,该系统就把下一个字符当成XON,而不管实际字符是什么,此时发送将停止。

_fEof:EOF字符送出。

_fTxim:指明字符是否正等待被发送,如果为1,则字符正等待被发送。

_fReserved:系统保留。

_cbInQue:指明串行设备接收到的字节数。并不是指ReadFile操作要求读的字节数。

_cbOutQue:指明发送缓冲区尚未发送的字节数。如果进行不重叠写操作时值为0。

本文只用到了cbInQue成员变量,该成员变量的值代表输入缓冲区的字节数。


(5)、串口配置_通信事件
Windows进程中监视发生在通信资源中的一组事件,这样应用程序可以不检查端口状态就可以知道某些条件何时发生,这将是非常有用的。通过使用事件,应用程序不需要为接收字节而连续不断地检测端口,从而节省CPU时间。
通信事件
Windows可以利用GetCommMask()函数和 SetCommMask函数来控制表七所示的通信事件。

表七  Windows通信事件列表

描述
EV_BREAK 检测到输入的终止
EV_CTS CTS(清除发送)信号改变状态
EV_DSR DSR(数据设置就绪)信号改变状态
EV_ERR 发生了线路状态错误,线路状态错误时CE_FRAME(帧错误)、CE_OVERRUN (接收缓冲区超限)和CE_RXPARITY(奇偶校验错误)
EV_RING 检测到振铃
EV_RLSD RLSD(接收到线路信号检测)信号改变状态
EV_RXCHAR 接收到一个字符,并放入输入缓冲区
EV_RXFLAG 接收到事件字符(DCB结构地EvtChar成员),并放入输入缓冲区
EV_TXEMPTY 输出缓冲区中最后一个字符发送出去

操作通信事件

应用程序可以利用SetCommMask()函数设置事件掩模来监视指定通信资源上的事件。SetCommMask函数的声明如下:
BOOL SetCommMask
(
HANDLE hFile,

DWORD dwEvtMask
);

其中主要参数介绍如下:
_hFile :串口设备句柄,CreateFile()函数返回该句柄。

_dwEvtMask:事件掩模,标识将被监视的通信事件。如果该参数设置为0,则表示禁止所有事件。如果不为0,则可以是表7中各种事件的组合。

如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。
如果想获取特定通信资源的当前事件掩模,可以使用GetCommMask()函数。GetCommMask()函数声明如下:
BOOL GetCommMask
(
HANDLE hFile,

LPDWORD lpEvtMask
);

其中主要参数介绍如下:
_hFile :串口设备句柄,CreateFile()函数返回该句柄。

_dwEvtMask:事件掩模,标识将被监视的通信事件,一个32位变量,可以是表7中各种事件的组合。
如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。

监视通信事件

在用SetCommMask()指定了有用的事件后,应用程序就调用WaitCommEvent()函数来等待其中一个事件发生。WaitCommEvent()函数既可以同步使用,也可以异步使用。WaitCommEvent()函数声明如下:
BOOL WaitCommEvent
(
HANDLE hFile,

LPDWORD lpEvtMask,

LPOVERLAPPED lpOverlapped,
);

其中主要参数介绍如下:
_hFile :串口设备句柄,CreateFile()函数返回该句柄。

_dwEvtMask:指向一个32位变量,接收事件掩模,标识所发生的通信事件属于何种类型。可以是表7中各种事件的组合。

_lpOverlapped:指向重叠结构,如果串口打开时指定了FILE_FLAG_OVERLAPPED标志 ,则该参数不能为NULL,且重叠结构中 应该包含一个手工重置对象句柄(通过CreateEvent()创建)。

如果该函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。
如果lpOverlapped参数不设置或打开hFile标识的通信设备是未指定FILE_FLAG_OVERLAPPED标志,则直到发生了指定事件或出错时,WaitCommEvent()函数才返回。如果lpOverlapped参数指向一个OVERLAPPED结构,并且打开hFile标识的通信设备时指定了FILE_FLAG_OVERLAPPED标志,则WaitCommEvent()函数以异步操作实现。这种情况下,OVERLAPPED结构中必须含有一个人工复位事件的句柄。和所有异步函数一样,如果异步操作不能立即实现,则该函数返回0,并且GetLastError()函数返回ERROR_IO_PENDING,以指示该操作正在后台进行。此时WaitCommEvent()函数返回之前,系统将OVERLAPPED结构中的hEvent参数设置为无信号状态;当发生了指定事件或出错时,系统将其设置为有信号状态。调用程序可使用等待函数确定事件对象的状态,然后使用GetOverlappedResult()函数确定WaitCommEvent()函数的操作结束。GetOverlappedResult()函数报告该操作成功或者失败,并且lpEvtMask()函数所指向的变量被设置以指示所发生的事件。
WaitCommEvent()只检测发生在等待开始后的事件。例如,如果指定EV_RXCHAR事件,则只有当收到函数字符并将字符放进接收缓冲区后才能满足等待条件。WaitCommEvent()调用时已在接收缓冲区中的字符不符合等待条件。
重叠I/O非常灵活,它也可以实现阻塞(例如我们可以设置一定要读取到一个数据才能进行到下一步操作)。有两种方法可以等待操作完成:一种方法是用WaitForSingleObject这样的等待函数来等待OVERLAPPED结构的hEvent成员;另一种方法是调用GetOverlappedResult函数等待,本文用后者实现。

GetOverlappedResult()可以判断一个重叠操作当前的状态,用来判断异步操作是否完成。
GetOverlappedResult()函数声明如下
BOOL WINAPI GetOverlappedResult
(
HANDLE   hFile,              //文件句柄
LPOVERLAPPED   lpOverlapped,      //指向欲检查的重叠结构
LPDWORD   lpNumberOfBytesTransferred,  //读或写操作的字节数
BOOL     bWait
);
如果参数bWait为TRUE则函数会一直等待直到重叠机构中的hEvent变成有信号;FALSE为如果检测到pending状态则立即返回,此时函数返回FALSE,GetLastError()返回值为ERROR_IO_INCOMPLETE。

3、//串口的读取
读串口操作
我们使用ReadFile和WriteFile读写串口,下面是两个函数的声明:
ReadFile()函数声明如下:
BOOL ReadFile
(
HANDLE hFile, //串口的句柄

LPVOID lpBuffer, // 读入的数据存储的地址,即读入的数据将存储在以该指针的值为首地址的一片内存区

DWORD nNumberOfBytesToRead, // 要读入的数据的字节数

LPDWORD lpNumberOfBytesRead, // 指向一个DWORD数值,该数值返回读操作实际读入的字节数

LPOVERLAPPED lpOverlapped // 重叠操作时,该参数指向一个OVERLAPPED结构,同步操作时,该参数为NULL。

);

其中主要参数介绍如下:
_hFile:指向标识的句柄。对串口来说,就是由CreateFile函数返回的句柄。

_lpBuffer:指向一个缓冲区,该缓冲区主要用来存放从串口设备中读取的数据。

_nNumberOfBytesToRead:指定要从串口设备读取的字节数。

_pNumberOfBytesRead:指向调用该函数读出的字节数。

_lpOverlapped:是一个OVERLAPPED的结构,该结构将在后面介绍。如果hFile以FILE_FLAG_OVERLAPPED方式常见,则需要此结构;否则,不需要此结构。

需要注意的是如果该函数因为超时而返回,那么返回值是TRUE。参数lpOverlapped 在操作时应该指向一个OVERLAPPED的结构,如果该参数为NULL ,那么函数将进行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED 标志建立的。当ReadFile 返回FALSE时,不一定就是操作失败,线程应该调用GetLastError函数分析返回的结果。例如,在重叠操作时如果操作还未完成函数返回,那么函数就返回FALSE,而且GetLastError函数返回ERROR_IO_PENDING。

写串口操作
WriteFile()函数声明如下:
BOOL WriteFile
(
HANDLE hFile, //串口的句柄

LPCVOID lpBuffer, // 写入的数据存储的地址,即以该指针的值为首地址的一片内存区

DWORD nNumberOfBytesToWrite, //要写入的数据的字节数

LPDWORD lpNumberOfBytesWritten, // 指向指向一个DWORD数值,该数值返回实际写入的字节数

LPOVERLAPPED lpOverlapped // 重叠操作时,该参数指向一个OVERLAPPED结构, 同步操作时,该参数为NULL。

其中主要参数介绍如下:
_hFile:指向标识的句柄。对串口来说,就是由CreateFile函数返回的句柄。

_lpBuffer:指向一个缓冲区,该缓冲区主要用来存放待写入串口设备的数据。

_nNumberOfBytesToWrite:指定要向串口设备写入的字节数。

_lpNumberOfBytesWritten:指向调用该函数已写入的字节数。

_lpOverlapped:是一个OVERLAPPED的结构,该结构将在后面介绍。如果hFile以FILE_FLAG_OVERLAPPED方式常见,则需要此结构;否则,不需要此结构。
如果函数调用成功,则返回值不为零;若函数调用失败,则返回值为零。调用GetLastError()函数可以获得进一步的出错信息。

在使用ReadFile和WriteFile重叠操作时,线程需要创建OVERLAPPED结构以供这两个函数使用。线程通过OVERLAPPED结构获得当前的操作状态,该结构最重要的成员是hEvent。hEvent是读写事件。当串口使用异步通讯时,函数返回时操作可能还没有完成,程序可以通过检查该事件得知是否读写完毕。
要使用OVERLAPPED的结构,CreateFile()函数的dwFlagsAndAttributes参数必须设为FILE_FLAG_OVERLAPPED标识,读写串口函数必须指定OVERLAPPED结构。异步I/O操作在Windows中使用广泛。
OVERLAPPED结构类型声明如下:
typedef struct_OVERLAPPED { // 0
DWORD Internal;

DWORD InteralHigh;

DWORD Offset;

DWORD OffsetHigh;

HANDLE hEvent;

} OVERLAPPED;

其中主要参数如下:
_Internal:操作系统保留,指出一个和系统相关的状态。当GetOverlappedResult()函数返回时,如果将扩展信息设置为ERROR_IO_PENDING,该参数有效。

_InteralHigh:操作系统保留,指出发送或接收的数据长度,当GetOverlappedResult()函数返回值不为0时,该参数有效。

_Offset和OffsetHigh:指明文件传送的开始位置和字节偏移量的高位字。当进行端口操作时,这两个参数被忽略。

_hEvent:指定一个I/O操作完成后触发的事件(信号)。在调用读写函数进行I/O操作之前,必须设置该参数。

在设置了异步I/O操作后,I/O操作和函数返回有以下两种情况:
1、函数返回时I/O操作已经完成:此时结果好像是同步执行的,但实际上这是异步执行的结果。

2、函数返回时I/O操作还没完成:此时一方面,函数返回值为零,并且GetLastError()函数返回ERROR_IO_PENDING;另一方面,系统把OVERLAPPED中的信号事件设为无信号状态。当I/O操作完成时,系统要把它设置为信号状态。

4、//关闭串口设备
关闭串口使用CloseHandle()函数来完成,利用API函数关闭相应串口非常简单,只要以CreateFile函数返回的句柄作为参数调用CloseHandle即可:

//CloseHandle()函数声明:
BOOL CloseHandle
(
HANDLE hObject; //handle to object to close
);
使用串口时一般要关闭它,如果忘记关闭串口,串口就会始终处于打开状态,其它应用程序就不能打开并使用串口了。

5、自己的程序
COM口异步读取类声明

#pragma once
#include <windows.h>
#include <iostream>

class SerialPortCommunication
{
    //类成员变量声明
private:
    HANDLE hcom_;
    OVERLAPPED write_overlapped_struct_;//用于写入数据
    OVERLAPPED read_overlapped_struct_;//用于读取数据
    OVERLAPPED wait_overlapped_struct_;//用于等待数据
    volatile bool is_open_;//串口是否打开
    HANDLE thread_;//读取线程句柄
    //类函数声明
public:
    //SerialPortCommunication类的无参数构造函数
     SerialPortCommunication();
     //SerialPortCommunication类的析构函数
    ~SerialPortCommunication();
    //打开串口并配置串口
    bool InitCom(LPCTSTR port); 
    //关闭串口并清理
    void UnInitCom();          
    //异步写串口
    bool ComWrite(LPBYTE buf, int &len);
    //读取线程
    static unsigned int __stdcall ComReceive(void*);
};
SerialPortCommunication::SerialPortCommunication() 
{   
    hcom_=INVALID_HANDLE_VALUE;
    is_open_=false;
    thread_=NULL;
    memset(&write_overlapped_struct_, 0, sizeof(write_overlapped_struct_));
    memset(&read_overlapped_struct_, 0, sizeof(read_overlapped_struct_));
    memset(&wait_overlapped_struct_, 0, sizeof(wait_overlapped_struct_));
}

初始化并配置串口

bool SerialPortCommunication::InitCom(LPCTSTR port)
{
    //打开串口
    hcom_ = CreateFile(port,            //串口号
        GENERIC_READ | GENERIC_WRITE,   //允许读和写
        0,                              //独占方式
        NULL, 
        OPEN_EXISTING,                  //打开而不是创建
        FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,//设置异步标识
        NULL);
    if (hcom_==INVALID_HANDLE_VALUE )
    {
        return false;
    }
    SetupComm(hcom_, 1024, 1024);////输入缓冲区和输出缓冲区的大小都是1024
    DCB dcb;                    //DCB(设备控制块)结构
    GetCommState(hcom_, &dcb);  //获得COM口的设备控制块,从而获得相关参数
    //配置串口相关参数
    dcb.DCBlength = sizeof(dcb);
    dcb.Parity = NOPARITY;      //无奇偶校验位
    dcb.BaudRate = CBR_9600;    //波特率9600
    dcb.StopBits = ONESTOPBIT;  //一个停止位
    SetCommState(hcom_, &dcb);  //设置COM口的设备控制块
    //在读写串口之前,清空缓冲区
    /*PURGE_TXABORT  中断所有写操作并立即返回,即使写操作还没有完成
      PURGE_RXABORT  中断所有读操作并立即返回,即使读操作还没有完成
      PURGE_TXCLEAR  清除输出缓冲区
      PURGE_RXCLEAR  清除输入缓冲区*/
    PurgeComm(hcom_, PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR | PURGE_TXABORT);
    //设置COMMTIMEOUTS结构
    COMMTIMEOUTS ct;
    ct.ReadIntervalTimeout = MAXDWORD;//读取无延时,因为有WaitCommEvent等待数据
    ct.ReadTotalTimeoutConstant = 0;  //
    ct.ReadTotalTimeoutMultiplier = 0;//
    ct.WriteTotalTimeoutMultiplier = 500;
    ct.WriteTotalTimeoutConstant = 5000;
    //设定读超时
    SetCommTimeouts(hcom_, &ct);
    //创建事件对象
    read_overlapped_struct_.hEvent = CreateEvent(NULL, 
                                                false, // Event被设置为有信号当有一个wait到它的Thread时,该Event就会自动复位,变成无信号. 
                                                false, // 初始状态,true,有信号,false无信号 
                                                NULL); // 事件对象的名称
    write_overlapped_struct_.hEvent = CreateEvent(NULL, false, false, NULL);
    wait_overlapped_struct_.hEvent = CreateEvent(NULL, false, false, NULL);
    //设置串口通信事件
    SetCommMask(hcom_, EV_ERR | EV_RXCHAR);//EV_ERR:线路状态错误,EV_RXCHAR:输入缓冲区中已收到数据
    //创建读取线程
    thread_ = (HANDLE)_beginthreadex(NULL, //安全属性,NULL为默认安全属性
                                        0, //指定线程堆栈的大小
                                        &SerialPortCommunication::ComReceive, //指定线程函数的地址
                                        this, //传递给线程的参数的指针
                                        0,   //线程初始状态,立即执行
                                        NULL);//用于记录线程ID的地址
    is_open_ = true;
    return true;
}

写入数据

bool SerialPortCommunication::ComWrite(LPBYTE buf, int &len)
{
    BOOL writefile_return = FALSE;
    DWORD write_size = 0;
    //在写串口之前,清空缓冲区
    PurgeComm(hcom_, PURGE_TXCLEAR | PURGE_TXABORT);
    //wait_overlapped_struct_.Offset = 0;
    //异步串口读
    writefile_return = WriteFile(hcom_,//串口句柄
                    buf,  //指向一个缓冲区,包含要写入的数据
                    len,  ////要写入数据的字节数
                    &write_size, //实际写入的字节数
                    &write_overlapped_struct_);//指向一个OVERLAPPEN结构体

    len = 0;
    if ( writefile_return==FALSE && GetLastError() == ERROR_IO_PENDING)//后台写入
    {
        //等待数据写入完成
        //GetOverlappedResult函数判断重叠操作当前的状态,用来判断异步操作是否完成
        //操作完成则返回ture
        if (FALSE == GetOverlappedResult(hcom_, //串口句柄
                                        &write_overlapped_struct_, //指向欲检查的重叠结构
                                        &write_size, //读或写操作的字节数
                                        TRUE))//函数一直等待直到重叠机构中的hEvent变成有信号
        {
            return false;
        }
    }

    len = write_size;
    return writefile_return != FALSE;
}

读取数据

unsigned int __stdcall SerialPortCommunication::ComReceive(void* lpparam)
{
    SerialPortCommunication *object = static_cast<SerialPortCommunication*>(lpparam);
    DWORD wait_event = 0;
    DWORD bytes = 0;
    BOOL status = FALSE;
    BYTE read_buf[4096];
    DWORD error;
    COMSTAT cs = { 0 };
    while (object->is_open_)
    {
        wait_event = 0;
        object->wait_overlapped_struct_.Offset = 0;
        //WaitCommEvent判断SetCommMask()函数设置的串口通信事件是否已发生
        status = WaitCommEvent(object->hcom_, //串口句柄
                                &wait_event, //函数执行完后如果检测到串口通信事件的话就将其写入该参数中
                                &object->wait_overlapped_struct_);//异步结构,用来保存异步操作结果
        //WaitCommEvent也是一个异步命令,所以需要等待
        if (FALSE == status && GetLastError() == ERROR_IO_PENDING)//
        {
            //如果缓存中无数据线程会停在此,如果hCom关闭会立即返回False
            status = GetOverlappedResult(object->hcom_, &object->wait_overlapped_struct_, &bytes, TRUE);
        }
        //清除(获得)通信错误和获得串口的当前通信状态
        ClearCommError(object->hcom_, &error, &cs);
        if (TRUE == status //等待事件成功
            && wait_event&EV_RXCHAR//缓存中有数据到达
            && cs.cbInQue > 0)//有数据
        {
            bytes = 0;
            object->read_overlapped_struct_.Offset = 0;
            memset(read_buf, 0, sizeof(read_buf));
            //数据已经到达缓存区,ReadFile不会当成异步命令,而是立即读取并返回True
            status = ReadFile(object->hcom_,//串口句柄
                                read_buf, //指向一个缓冲区,保存读取的数据
                                sizeof(read_buf), //要读取数据的字节数
                                &bytes, //实际读取的字节数
                                &object->read_overlapped_struct_);//指向一个OVERLAPPED结构
            if (status != FALSE)
            {
                cout << "Read:" << (LPCSTR)read_buf << "   Len:" << bytes << endl;
            }
            PurgeComm(object->hcom_, PURGE_RXCLEAR | PURGE_RXABORT);
        }

    }
    return 0;
}

关闭串口并清理

void SerialPortCommunication::UnInitCom()
{
    is_open_ = false;
    if (INVALID_HANDLE_VALUE !=hcom_)
    {
        CloseHandle(hcom_);
        hcom_ = INVALID_HANDLE_VALUE;
    }
    if (NULL != read_overlapped_struct_.hEvent)
    {
        CloseHandle(read_overlapped_struct_.hEvent);
        read_overlapped_struct_.hEvent = NULL;
    }
    if (NULL != write_overlapped_struct_.hEvent)
    {
        CloseHandle(write_overlapped_struct_.hEvent);
        write_overlapped_struct_.hEvent = NULL;
    }
    if (NULL != wait_overlapped_struct_.hEvent)
    {
        CloseHandle(wait_overlapped_struct_.hEvent);
        wait_overlapped_struct_.hEvent = NULL;
    }
    if (NULL != thread_)
    {
        WaitForSingleObject(thread_, 5000);//等待线程结束
        CloseHandle(thread_);
        thread_ = NULL;
    }
}

main函数

#include "serial_port_communication.h"
#include <tchar.h>
using namespace std;

//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(int argc, _TCHAR* argv[])
{
     SerialPortCommunication com;
     com.InitCom(_T("COM2"));
     BYTE buf[1024];
     int len;
     while (1)
        {
            cin >> buf;
            len = strlen((char*)buf);
            com.ComWrite(buf, len);
        }
     com.UnInitCom();
     return 0;
}

好了,以上就是全部内容,自己搭建个环境就可以跑了。。。。。。
[参考博文:] (https://blog.csdn.net/lingtianyulong/article/details/8477130)
[参考博文:] (https://blog.csdn.net/nocodelife/article/details/8594073)
[参考博文:] (https://blog.csdn.net/wlk1229/article/details/52566701)

猜你喜欢

转载自blog.csdn.net/weixin_42546496/article/details/81542536