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;

}

}

12-以上简要介绍了大部分的串口通信api函数,笔者所写的串口通信软件用的是事件通知方式,该方式是

windows2000下效率较高的一种方式。而且只熟悉这些api函数也还是不够的,该机制下还要牵涉到多

线程和消息机制,其中读写串口的动作是由主线程来完成的,比如说操作者按下发送数据的按钮之后

,相应函数马上将某特定区域里面的数据发送出去,所以说用api函数写串口发送数据的功能是相对较

简单的。收数据的时候就要麻烦一点,在打开串口后首先主线程要设置要监视的串口通信事件,然后

将监视线程打开,用来监视主线程设置的这些串口通信事件是否已发生,当其中的某个事件发生后,

监视线程马上将该消息发送给主线程,其中监视线程在发送消息之前要确保主线程在收到消息后肯定

的知道串口究竟发生了什么样的事件,然后根据不同的事件类型进行处理。下面给出大致的主线程和

监视线程的大致工作流程:



主线程打开(其实就是主窗体打开之后)

|

|

V

打开串口(设置波特率、校验方式、数据位数、停止位数)

|

|

V

设置监视线程需要监视的串口通信事件

|

|

V

打开监视线程

|

|

V

等待各种事件的发生(比如发送数据单击事件,更改通信参数事件,监视线程发来的消息等)

--------------------------------------------------------------------------------

监视线程被打开

|

|

V

串口事件发生否(WaitCommEvent())(无论发生否均进入下面的代码)

|

|

V

异步操作是否正在后台进行?(if(GetLastError()==ERROR_IO_PENDING))

|

|

V

在此等待异步操作结果(GetOverlappedResult(hComm,&os,&dwTrans,true))

|

|

V

处理通信事件,根据事件类型的不同给主窗体发送不同的消息

---------------------------------------------------------------------------------

以上给出最最简单的工作流程,其实我感觉任何知识都是依靠查资料、问别人等等这些途径获得入门的

基本信息之后再依靠自己孜孜不倦的钻研得来的,饭来张口得到的那种东西往往不如自己嚼出来的东西

体会更深,有一句话我感觉很有味道:"书读百遍自然熟",但得是用心的读,或许一遍不懂,两遍不懂

,然后没有恒心的人便开始悄悄的准备放弃了,但有恒心的人当再读完一遍之后会越发的发现:奥,...

...,原来上次不懂的地方是这么回事...,但我个人认为,刚开始是很重要的,如果你连这个课题的最最

基本的东西都不知道的话那就很难了,所以任何人都应该有问别人的时候。

由于水平实在有限,只能介绍到这种层次了,而且直到现在我仍然在怀疑我写的串口软件是不是还

有什么不可靠的地方,但这只能靠时间来检验了,也希望大家帮我挑出毛病来,我的串口软件的位置在

http://www.chinabcb.com首页的"工具"一栏中,名字叫"串口大师(或串口监视软件)",欢迎试用。

虽然写的不好,但我感觉对于这方面的初学者还是应该有while(1){一点点}帮助的,如有对此话题

感兴趣者欢迎与我联系并讨论,欢迎转载,转载前请与本人联系!

作者:张景光

来自:沈阳

职业:电子工程师

邮箱:winloop@163.com

QQ: 43583576