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。
在打开通信设备句柄后,常常需要对串行口进行一些初始化工作。这需要通过一个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的返回时间。
在用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函数可以终止正在进行的读写操作,该函数还会清除输入或输出缓冲区中的内容。
////////////////////////////////////////////////
.2 调用Win32 API函数实现串行通信编程
Windows操作系统对系统底层操作采取了屏蔽的策略,禁止应用程序直接访问计算机I/O端口,而由设备驱动程序统一管理,Windows封装了Windows的通信机制,这种方式称为通信应用程序接口API(Application Programming Interfaces)。Windows 9x/NT/2000提供的API一般都支持32位的操作,又称为Win32 API,程序员可以利用Win32 API的通信函数进行编程,不用对硬件直接进行操作,使得应用程序的编制更加方便。
2.2.1 Win32 API常用通信函数
在进行串口通信时,经常需要用到下列一些API函数:
CreateFile():用于打开一个文件访问串口;
GetCommState():获取串口的当前配置,放入设备控制块DCB中;
SetCommState():根据DCB重新配置串口参数;
SetCommTimeouts():设置串口读写操作的溢出时间;
ReadFile():从串口的输入缓冲区读取数据;
WriteFile():向串口的输出缓冲区写入数据;
SetCommMask():监视指定通信资源上的事件;
WaitCommEvent():等待通信事件发生;
CloseHandle():关闭由CreateFile函数打开的串口。
以上这些函数的原形可在参考文献[1]中找到。
1-在C++ Builder 6.0下基于api函数编写串口通信程序简介:
在dos/win95/win98的年代,操作系统对串口是不保护的,也就是说将串口的的资源完全开放给用户,用户可以用直接操作硬件的函数(比如说 TC2.0下的inport()和outport()函数) 跟串口直接打交道,这时候用户使用直接操作串口的函数怎样"折磨"串口都是没有问题的,操作系统根本就不管不问,对串口操作所造成的一切后果都是用户一个 人承担的,这时候用户对串口具有高度自由的支配权;但是,这种情况好景不长,从win2000操作系统开始,微软为了"照顾好"计算机上的硬件,开始实施了对硬件的保护策略,也就是说任何用户在他的操作系统下企图操纵串口时必须经过他的同意方可进行,其实也就是变相的将用户往必须使用他的通信api函数才 能操作串口这条"羊肠小路"上赶(当然也有别的方法操作串口,但那些并非我等普通用户能研究明白的),形象一点说就好像你想怎样操作串口的意图必须经过 win2000的翻译(其实是win2000的设备驱动程序)才能转达给串口一样,基于这一点我们说(其实是很多资料上说的)win2000下通过api 函数操作串口是具有"设备无关性的",什么意思呢?就是说你想怎样操作串口就用相应的api函数告诉操作系统你想对串口干什么,然后操作系统就把你的意思 转告给串口让其做出相应的动作,相对于dos/win95/win98下来说,据我理解也就相当于你原来写的直接操作串口的函数在win2000下他替你 完成了,但是你必须用win2000通信api函数清楚地向操作系统表达清楚你到底想干什么,所以说在这种情况下要想写好串口驱动程序你就必须至少弄明白 win2000下的通信api函数都是干什么的方可,啰里啰唆唠叨了这么多... ...sorry, 还没完呢,至少还有一件事我想说,原来在dos/win95/win98系统下有好多高手用c/c++对串口进行直接操作是非常熟练的,尤其是dos时代 的turbo 2.0操作串口的高手他们写的串口驱动程序直到win98的时候还用的非常洋洋得意,但是到了win2000的时候,他们的程序突然不好使了,而他们有的 可能还会因为知识结构上的滞后始终弄不明白怎么回事儿,兄弟们,你们该明白了吧?闲话少叙,下面介绍笔者写串口通信函数时用到的各个api函数----- ----
2-CreateFile()
用途:打开串口
原型:HANDLE CreateFile(LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
参数说明:
-lpFileName:要打开的文件名称。对串口通信来说就是COM1或COM2。
-dwDesiredAccess:读写模式设置。此处应该用GENERIC_READ及GENERIC_WRITE。
-dwShareMode:串口共享模式。此处不允许其他应用程序共享,应为0。
-lpSecurityAttributes:串口的安全属性,应为0,表示该串口不可被子程序继承。
-dwCreationDistribution:创建文件的性质,此处为OPEN_EXISTING.
-dwFlagsAndAttributes:属性及相关标志,这里使用异步方式应该用FILE_FLAG_OVERLAPPED。
-hTemplateFile:此处为0。
操作说明:若文件打开成功,串口即可使用了,该函数返回串口的句柄,以后对串口操作时
即可使用该句柄。
举例:HANDLE hComm;
hComm=CreateFile("COM1", //串口号
GENERIC_READ|GENERIC_WRITE, //允许读写
0, //通讯设备必须以独占方式打开
NULL, //无安全属性
OPEN_EXISTING, //通讯设备已存在
FILE_FLAG_OVERLAPPED, //异步I/O
0); //通讯设备不能用模板打开
hComm即为函数返回的串口1的句柄。
3-CloseHandle()
用途:关闭串口
原型:BOOL CloseHandle(HANDLE hObjedt)
参数说明:
-hObjedt:串口句柄
操作说明:成功关闭串口时返回true,否则返回false
举例:CloseHandle(hComm);
4-GetCommState()
用途:取得串口当前状态
原型:BOOL GetCommState(HANDLE hFile,
LPDCB lpDCB);
参数说明:
-hFile:串口句柄
-lpDCB:设备控制块(Device Control Block)结构地址。此结构中含有和设备相关的
参数。此处是与串口相关的参数。由于参数非常多,当需要设置串口参数
时,通常是先取得串口的参数结构,修改部分参数后再将参数结构写入。
在此仅介绍少数的几个常用的参数:
DWORD BaudRate:串口波特率
DWORD fParity:为1的话激活奇偶校验检查
DWORD Parity:校验方式,值0~4分别对应无校验、奇校验、偶校验、校验
置位、校验清零
DWORD ByteSize:一个字节的数据位个数,范围是5~8
DWORD StopBits:停止位个数,0~2分别对应1位、1.5位、2位停止位
操作举例:DCB ComDCB; //串口设备控制块
GetCommState(hComm,&ComDCB);
5-SetCommState()
用途:设置串口状态,包括常用的更改串口号、波特率、奇偶校验方式、数据位数等
原型:BOOL SetCommState(HANDLE hFile,
LPDCB lpDCB);
参数说明:
-hFile:串口句柄
-lpDCB:设备控制块(Device Control Block)结构地址。要更改的串口参数包含在此结构中。
操作举例:DCB ComDCB;
GetCommState(hComm,&ComDCB);//取得当前串口状态
ComDCB.BaudRate=9600;//更改为9600bps,该值即为你要修改后的波特率
SetCommState(hComm,&ComDCB;//将更改后的参数写入串口
6-WriteFile()
用途:向串口写数据
原型:BOOL WriteFile(HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped);
参数说明:
-hFile:串口句柄
-lpBuffer:待写入数据的首地址
-nNumberOfBytesToWrite:待写入数据的字节数长度
-lpNumberOfBytesWritten:函数返回的实际写入串口的数据个数的地址,利用此变量可判断
实际写入的字节数和准备写入的字节数是否相同。
-lpOverlapped:重叠I/O结构的指针
操作举例:DWORD BytesSent=0;
unsigned char SendBytes[5]={1,2,3,4,5};
OVERLAPPED ov_Write;
ov_Write.Offset=0;
ov_Write.OffsetHigh=0;
WriteFile(hComm, //调用成功返回非零,失败返回零
SendBytes, //输出缓冲区
5, //准备发送的字符长度
&BytesSent, //实际发出的字符数
&ov_Write); //重叠结构
如果函数执行成功的话检查BytesSent的值应该为5,此函数是WriteFile函数执行完毕后
自行填充的,利用此变量的填充值可以用来检查该函数是否将所有的数据成功写入串口
7-ReadFile()
用途:读串口数据
原型:BOOL ReadFile(HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
lpNumberOfBytesRead,
lpOverlapped);
参数说明:
-hFile:串口句柄
-lpBuffer:存储被读出数据的首地址
-nNumberOfBytesToRead:准备读出的字节个数
-NumberOfBytesRead:实际读出的字节个数
-lpOverlapped:异步I/O结构,
操作举例:unsigned char ucRxBuff[20];
COMSTAT ComStat;
DWORD dwError=0;
DWORD BytesRead=0;
OVERLAPPED ov_Read;
ov_Read.hEvent=CreateEvent(NULL, true, false, NULL);//必须创建有效事件
ClearCommError(hComm,&dwError,&ComStat);//检查串口接收缓冲区中的数据个数
bResult=ReadFile(hComm, //串口句柄
ucRxBuff, //输入缓冲区地址
ComStat.cbInQue, //想读入的字符数
&BytesRead, //实际读出的字节数的变量指针
&ov_Read); //重叠结构指针
假如当前串口中有5个字节数据的话,那么执行完ClearCommError()函数后,ComStat
结构中的ComStat.cbInQue将被填充为5,此值在ReadFile函数中可被直接利用。
8-ClearCommError()
用途:清除串口错误或者读取串口现在的状态
原型:BOOL ClearCommError(HANDLE hFile,
LPDWORD lpErrors,
LPCOMATAT lpStat
);
参数说明:
-hFile:串口句柄
-lpErrors:返回错误数值,错误常数如下:
1-CE_BREAK:检测到中断信号。意思是说检测到某个字节数据缺少合法的停止位。
2-CE_FRAME:硬件检测到帧错误。
3-CE_IOE:通信设备发生输入/输出错误。
4-CE_MODE:设置模式错误,或是hFile值错误。
5-CE_OVERRUN:溢出错误,缓冲区容量不足,数据将丢失。
6-CE_RXOVER:溢出错误。
7-CE_RXPARITY:硬件检查到校验位错误。
8-CE_TXFULL:发送缓冲区已满。
-lpStat:指向通信端口状态的结构变量,原型如下:
typedef struct _COMSTAT{
...
...
DWORD cbInQue; //输入缓冲区中的字节数
DWORD cbOutQue;//输出缓冲区中的字节数
}COMSTAT,*LPCOMSTAT;
该结构中对我们很重要的只有上面两个参数,其他的我们可以不用管。
操作举例:COMSTAT ComStat;
DWORD dwError=0;
ClearCommError(hComm,&dwError,&ComStat);
上式执行完后,ComStat.cbInQue就是串口中当前含有的数据字节个数,我们利用此
数值就可以用ReadFile()函数去读串口中的数据了。
9-PurgeComm()
用途:清除串口缓冲区
原型:BOOL PurgeComm(HANDLE hFile,
DWORD dwFlags
);
参数说明:
-hFile:串口句柄
-dwFlags:指定串口执行的动作,由以下参数组成:
-PURGE_TXABORT:停止目前所有的传输工作立即返回不管是否完成传输动作。
-PURGE_RXABORT:停止目前所有的读取工作立即返回不管是否完成读取动作。
-PURGE_TXCLEAR:清除发送缓冲区的所有数据。
-PURGE_RXCLEAR:清除接收缓冲区的所有数据。
操作举例:PurgeComm(hComm, PURGE_RXCLEAR|PURGE_TXCLEAR|PURGE_RXABORT|PURGE_TXABORT);
清除串口的所有操作。
10-SetCommMask()
用途:设置串口通信事件。
原型:BOOL SetCommMask(HANDLE hFile,
DWORD dwEvtMask
);
参数说明:
-hFile:串口句柄
-dwEvtMask:准备监视的串口事件掩码
注:在用api函数撰写串口通信函数时大体上有两种方法,一种是查寻法,另外一种是事件通知法。
这两种方法的区别在于收串口数据时,前一种方法是主动的周期性的查询串口中当前有没有
数据;后一种方法是事先设置好需要监视的串口通信事件,然后依靠单独开设的辅助线程进行
监视该事件是否已发生,如果没有发生的话该线程就一直不停的等待直到该事件发生后,将
该串口事件以消息的方式通知主窗体,然后主窗体收到该消息后依据不同的事件性质进行处理。
比如说当主窗体收到监视线程发来的RX_CHAR(串口中有数据)的消息后,就可以用ReadFile()
函数去读串口。该参数有如下信息掩码位值:
EV_BREAK:收到BREAK信号
EV_CTS:CTS(clear to send)线路发生变化
EV_DSR:DST(Data Set Ready)线路发生变化
EV_ERR:线路状态错误,包括了CE_FRAME/CE_OVERRUN/CE_RXPARITY 3钟错误。
EV_RING:检测到振铃信号。
EV_RLSD:CD(Carrier Detect)线路信号发生变化。
EV_RXCHAR:输入缓冲区中已收到数据。
EV_RXFLAG:使用SetCommState()函数设置的DCB结构中的等待字符已被传入输入缓冲区中。
EV_TXEMPTY:输出缓冲区中的数据已被完全送出。
操作举例:SetCommMask(hComm,EV_RXCHAR|EV_TXEMPTY);
上面函数执行完毕后将监视串口中有无数据和发送缓冲区中的数据是否全部发送完毕。
11-WaitCommEvent()
用途:用来判断用SetCommMask()函数设置的串口通信事件是否已发生。
原型:BOOL WaitCommEvent(HANDLE hFile,
LPDWORD lpEvtMask,
LPOVERLAPPED lpOverlapped
);
参数说明:
-hFile:串口句柄
-lpEvtMask:函数执行完后如果检测到串口通信事件的话就将其写入该参数中。
-lpOverlapped:异步结构,用来保存异步操作结果。
操作举例:OVERLAPPED os;
DWORD dwMask,dwTrans,dwError=0,err;
memset(&os,0,sizeof(OVERLAPPED));
os.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
if(!WaitCommEvent(hComm,&dwMask,&os)){
//如果异步操作不能立即完成的话,函数返回FALSE,并且调用GetLastError()函
//数分析错误原因后返回ERROR_IO_PENDING,指示异步操作正在后台进行.这种情
//况下,在函数返回之前系统设置OVERLAPPED结构中的事件为无信号状态,该函数
//等待用SetCommMask()函数设置的串口事件发生,共有9种事件可被监视:
//EV_BREAK,EV_CTS,EV_DSR,EV_ERR,EV_RING,EV_RLSD,EV_RXCHAR,
//EV_RXFLAG,EV_TXEMPTY;当其中一个事件发生或错误发生时,函数将
//OVERLAPPED结构中的事件置为有信号状态,并将事件掩码填充到dwMask参数中
if(GetLastError()==ERROR_IO_PENDING){
/**************************************************************/
/*在此等待异步操作结果,直到异步操作结束时才返回.实际上此时 */
/*WaitCommEvent()函数一直在等待串口监控的事件之一发生,当事件发*/
/*生时该函数将OVERLAPPED结构中的事件句柄置为有信号状态,此时 */
/*GetOverlappedResult()函数发现此事件有信号后马上返回,然后下面*/
/*的程序马上分析WaitCommEvent()函数等到的事件是被监视的串口事 */
/*件中的哪一个,然后执行相应的动作并发出相应消息. */
/**************************************************************/
GetOverlappedResult(hComm,&os,&dwTrans,true);
switch(dwMask){
case EV_RXCHAR:
PostMessage(Parent,WM_COMM_RXCHAR,0,0);
break;
case EV_TXEMPTY:
PostMessage(Parent,WM_COMM_TXEMPTY,0,0);
break;
case EV_ERR:
switch(dwError){
case CE_FRAME:
err=0;
break;
case CE_OVERRUN:
err=1;
break;
case CE_RXPARITY:
err=2;
break;
default:break;
}
PostMessage(Parent,WM_COMM_ERR,(WPARAM)0,(LPARAM)err);
break;
case EV_BREAK:
PostMessage(Parent,WM_COMM_BREAK,0,0);
break;
case ...://其他用SetCommMask()函数设置的被监视的串口通信事件。
... ...
break;
default:break;
}
}
//////////////////////////////////////////////////////////
Win32串口编程
金贝贝
一、基本知识
Win32下串口通信与16位串口通信有很大的区别。在Win32下,可以使用两种编程方式实现串口通信,其一是调用的Windows的API函数,其二是使用ActiveX控件。使用API 调用,可以清楚地掌握串口通信的机制,熟悉各种配置和自由灵活采用不同的流控进行串口通信。下面介绍串口操作的基本知识。
打开串口:使用CreateFile()函数,可以打开串口。有两种方法可以打开串口,一种是同步方式(NonOverlapped),另外一种异步方式(Overlapped)。使用Overlapped打开时,适当的方法是:
HANDLE hComm;
hComm = CreateFile( gszPort,
GENERIC_READ | GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
0);
if (hComm == INVALID_HANDLE_VALUE)
// error opening port; abort
配置串口:
1.DCB配置
DCB(Device Control Block)结构定义了串口通信设备的控制设置。许多重要设置都是在DCB结构中设置的,有三种方式可以初始化DCB。
(1)通过GetCommState()函数得DCB的初始值,其使用方式为:
DCB dcb = {0};
if (!GetCommState(hComm, &dcb))
// Error getting current DCB settings
else
// DCB is ready for use.
(2)用BuildCommDCB()函数初始化DCB结构,该函数填充 DCB的波特率、奇偶校验类型、数据位、停止位。对于流控成员函数设置了缺省值。其用法是:
DCB dcb;
FillMemory(&dcb, sizeof(dcb), 0);
dcb.DCBlength = sizeof(dcb);
if (!BuildCommDCB(“9600,n,8,1", &dcb)) {
// Couldn't build the DCB. Usually a problem
// with the communications specification string.
return FALSE;
}
else
// DCB is ready for use.
(3)用SetCommState()函数手动设置DCB初值。用法如下:
DCB dcb;
FillMemory(&dcb, sizeof(dcb), 0);
if (!GetCommState(hComm, &dcb)) // get current DCB
// Error in GetCommState
return FALSE;
// Update DCB rate.
dcb.BaudRate = CBR_9600 ;
// Set new state.
if (!SetCommState(hComm, &dcb))
// Error in SetCommState.
Possibly a problem with the communications
// port handle or a problem with the DCB structure itself.
手动设置DCB值时,DCB的结构的各成员的含义,可以参看MSDN帮助。
2.流控设置
硬件流控:串口通信中的硬件流控有两种,DTE/DSR方式和RTS/CTS方式,这与DCB结构的初始化有关系,DCB结构中的OutxCtsFlow、fOutxDsrFlow、fDsrSensitivity、fRtsControl、fDtrControl几个成员的初始值很关键,不同的值代表不同流控,也可以自己设置流控,但建议采用标准流行的流控方式。采用硬件流控时,DTE、DSR、RTS、CTS的逻辑位直接影响到数据的读写及收发数据的缓冲区控制。
软件流控:串口通信中采用特殊字符XON和XOFF作为控制串口数据的收发。与此相关的DCB成员是:fOut、fInX、XoffChar、XonChar、 XoffLim和XonLim。具体含义参见MSDN帮助。
串口读写操作:串口读写有两种方式:同步方式(NonOverlapped)和异步方式(Overlapped)。同步方式是指必须完成了读写操作,函数才返回,这可能造成程序死掉,因为如果在读写时发生了错误,永远不返回就会出错,可能线程将永远等待在那儿。而异步方式则灵活得多,一旦读写不成功,就将读写挂起,函数直接返回,可以通过GetLastError函数得知读写未成功的原因,所以常常采用异步方式操作。
读操作:ReadFile()函数用于完成读操作。异步方式的读操作为:
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};
// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent
(NULL, TRUE, FALSE, NULL);
if (osReader.hEvent == NULL)
// Error creating overlapped event; abort.
if (!fWaitingOnRead) {
// Issue read operation.
if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE,
&dwRead, &osReader)) {
if (GetLastError() != ERROR_IO_PENDING)
// read not delayed?
// Error in communications; report it.
else
fWaitingOnRead = TRUE;
}
else {
// read completed immediately
HandleASuccessfulRead(lpBuf, dwRead);
}
}
如果读操作被挂起,可以调用WaitForSingleObject()函数或WaitForMuntilpleObjects()函数等待读操作完成或者超时发生,再调用 GetOverlappedResult()得到想要的信息。
写操作:与读操作相似,故不详述,调用的API函数是: WriteFile函数。
串口状态:
(1)通信事件:用SetCommMask()函数设置想要得到的通信事件的掩码,再调用WaitCommEvent()函数检测通信事件的发生。可设置的通信事件标志(即SetCommMask()函数所设置的掩码)可以有EV_BREAK、EV_CTS、EV_DSR、 EV_ERR、EV_RING、EV_RLSD、EV_RXCHAR、EV_RXFLAG、EV_TXEMPTY。
注意:1对于EV_RING标志的设置,WIN95是不会返回EV_RING事件的,因为WIN95不检测该事件。2设置EV_RXCHAR,可以检测到字符到达,但是在绑定此事件和ReadFile()函数一起读取串口接收数据时,可能会出现错误,造成少读字节数,具体原因查看MSDN帮助。可以采用循环读的办法,另外一个比较好的解决办法是调用ClearCommError()函数,确定在一次读操作中在缓冲区中等待被读的字节数。
(2)错误处理和通信状态:在串口通信中,可能会产生很多的错误,使用ClearCommError()函数可以检测错误并且清除错误条件。
(3)Modem状态:用SetcommMask()可以包含很多事件标志,但是这些事件标志只指示在串口线路上的电压变化情况。而调用 GetCommModemStatus()函数可以获得线路上真正的电压状态。
扩展函数:如果应用程序想用自己的流控,可以使用 EscapeCommFunction()函数设置DTR和RTS线路的电平。
通信超时:在通信中,超时是个很重要的考虑因素,因为如果在数据接收过程中由于某种原因突然中断或停止,如果不采取超时控制机制,将会使得I/O线程被挂起或无限阻塞。串口通信中的超时设置分为两步,首先设置 COMMTIMEOUTS结构的五个变量,然后调用SetcommTimeouts()设置超时值。对于使用异步方式读写的操作,如果操作挂起后,异步成功完成了读写,WaitForSingleObject()或 WaitForMultipleObjects()函数将返回WAIT_OBJECT_0,GetOverlappedResult()返回TRUE。其实还可以用GetCommTimeouts()得到系统初始值。
关闭串口:程序结束或需要释放串口资源时,应该正确关闭串口,关闭串口比较简单,使用API调用CloseHandle()关闭串口的句柄就可以了。
调用方法为:CloseHandle(hComm);
但是值得注意的是在关闭串口之前必须保证读写串口线程已经退出,否则会引起误操作,一般采用的办法是使用事件驱动机制,启动一事件,通知串口读写线程强制退出,在线程退出之前,通知主线程可以关闭串口。
二、实现
1.程序设计思路
对于不同的应用程序,虽然界面不同,但是如果采用串口与主机之间的通信,对串口的处理方式大致相似,无非就是通过串口收发数据,对于通过串口接收到的数据,交给上层软件处理显示,对于上层要发给串口的数据,进行转发。但在实际编程中,由于采用的通信方式和流控不同,串口设置也不同,这就涉及到 DCB的初始化问题和读写串口等细节问题。串口通信应用程序设计的总体思路(即操作过程)是:首先,确定要打开的串口名、波特率、奇偶校验方式、数据位、停止位,传递给CreateFile()函数打开特定串口;其次,为了保护系统对串口的初始设置,调用 GetCommTimeouts()得到串口的原始超时设置;然后,初始化DCB对象,调用SetCommState() 设置DCB,调用SetCommTimeouts()设置串口超时控制;再次,调用SetupComm()设置串口接收发送数据的缓冲区大小,串口的设置就基本完成,之后就可以启动读写线程了。
一般来说,串口的读写由串口读写线程完成,这样可以避免读写阻塞时主程序死锁。对于全双工的串口读写,应该分别开启读线程和写线程;对于半双工和单工的,建议只需开启一个线程即可。在线程中,按照预定好的通信握手方式,正确检测串口状态,读取发送串口数据。
2.实现细节
在半双工的情况下,首先完成必要的串口配置,成功打开串口、DCB设置、超时设置;然后开启线程,如: CwinThread hSerialThread = (CWinThread*) AfxBeginThread(SerialOperation,hWnd,THREAD_PRIORITY_NORMAL); 其中开启之线程为SerialOperation,优先级为普通。
全双工情况下的串口编程,与单工差不多,区别仅仅在于启动双线程,分别为读线程和写线程,读线程根据不同的事件或消息,通过不断查询串口所收到的有效数据,完成读操作;写线程通过接收主线程的发送数据事件和要发送的数据,向串口发送。
//////////////////////////////////////////////////////////
2.3.1 利用SPComm组件实现串行通信编程
串行口VCL组件SPComm封装有丰富的与串口通信密切相关的属性和事件,是目前功能比较完善的串行通信组件,提供有完整的源代码。
(1)SPComm的主要属性
CommName:填写所要打开的串口名字,如“COM1”。
RaudRate:设定实际的串行通信波特率。
ParityCheek:奇偶校验。
ByteSize:设定字节长度。
Parity:奇偶校验位。
StopBits:停止位。
SendDataEmpty:布尔属性,为True时表示发送缓存为空,或者发送队列里没有信息;为False时表示发送缓存不为空,或者发送队列里有信息。
(2)SPComm的主要方法和事件
StartComm()过程用于打开串口,当操作失败时通常会报错,错误主要有7种:串口已经打开;打开串口错误;文件句柄不是通讯句柄;不能安装通讯缓存;不能产生事件;不能产生读进程;不能产生写进程。
StopComm()过程用于关闭串口,没有返回值。
WriteCommData()函数把要发送的字符串写入发送缓冲区,发送成功返回True,发送失败返回False。执行此函数将立即得到返回值,发送操作随后执行。此函数有两个参数:pDataToWrite是要发送的字符串;dwSixeofDataToWrite是发送数据帧的长度。
OnReceiveData(Tobject * Sender, Pointer Buffer, WORD BufferLength) 事件的功能是当输入缓存有数据时将触发该事件,可在该事件中对从串口收到的数据进行处理。参数Buffer是接收到的数据,BufferLength是接收到的数据长度。
OnReceiveError(Tobject * Sender,DWORD EventMask)事件功能是当接收数据时出现错误将触发该事件,EventMask是错误事件的代码。
OnSendDataEmpty(Tobject * Sender)事件的功能是当发交通安全数据完成时将触发该事件。
SPComm组件的应用与MSComm控件相似,基本编程时只要给组件的相关属性正确赋值就可配置串行通信参数,通过组件的方法打开/关闭串口和发送数据,在组件相应的通信事件处理函数中接收数据和处理通信事件。与MSComm ActiveX控件不同的是,SPComm组件是标准的VCL组件,提供多线程编程的支持,能够更可靠地进行串行数据通信。
Linux串口编程(待续)