基于API函数的串口通信编程
用到的串口通信编程方法有:使用通信控件、在高级语言中嵌入汇编以及使用API函数。在这几种方法中,使用API函数编写的串口通信程序最为高效、灵活。串口通信编程将用到三种API函数——串口通信相关API函数、多线程API函数和实现消息机制的API函数,下面将分别介绍这几种API函数。
1 与串口通信有关的API函数
Windows系统通信一般都以WOSA(Windows Open Services Architecture,即Windows开放式服务体系)模型为基础,在此模型中位于上层的应用程序通过调用各种通信API与位于下层的设备驱动程序进行数据交换。下面将按一般串口通信程序的流程顺序介绍这些API函数。
1.1 打开串口
Win32操作系统把串口看作一个文件,因此打开串口我们将要用到CreateFile函数。该函数第一个参数指明要打开的文件名称,对串口操作来说,就是COM1、COM2等。第二个参数为读写模式设置,因为要对串口进行读写,所以该参数应设为GENERIC_READ|GENERIC_WRITE。第三个参数值必须为0,表示不将串口与其他应用程序共享。第四个参数指向一个Security Attribute结构,通常设为NULL。第五个参数指定如何打开文件,在打开设备(串口是一种设备)时,此参数必须指定为OPEN_EXISTING。第六个参数指定文件属性及相关标 志,但是对于串行口,唯一有意义的设置是FILE_FLAG_OVERLAPPED或0;最后一个参数必须为NULL。若该函数打开串口成功,则返回创建的句柄,该句柄供随后对串行口的设置、读写等操作用;否则返回INVALID_HANDLE_VALUE。
1.2 设置串口
串口打开后,即可进行一系列初始化设置。最基本的初始化设置将通过GetCommState和SetCommState函数来实现。先调用GetCommState函数获取当前串口配置填充设备控制块(DCB),然后将DCB结构中几个重要参数如波特率、数据位、停止位、校验位改成符合实际设计要求的值,最后用SetCommState将刚刚所做的改动重新设置串口。串口I/O缓冲区的大小用SetupComm函数设置。通信速率越高,缓冲区应设置得越大,但不能超出设备驱动程序所能 处理的范围。另一个很重要的设置是串口超时设置。通信中因未知原因将出现不可预测的事件,譬如:接收数据过程中突然被中断,或者发送数据突然停止等。如果不认真对待,这些情况可能会引起I/O线程挂起或者线程被无限阻塞。Windows对于这类问题提供了安全措施,它可通过超时设置来决定通信是否异常并作相应处理,因此超时设置在串行通信中显得尤为重要。超时设置过程分为两步,首先设置Commtimeouts结构中的五个成员,然后调用SetCommTimeouts函数设置超时值。Commtimeouts结构的五个成员分别是:读串口间隔超时、读串口总超时乘数、读串口总超时常数(ms)、写串口总超时乘数、写串口总超时常数(ms)。
1.3 读写串口
设置工作完成后,即可用ReadFile和WriteFile对串口进行读写操作。在调用读写操作函数之前,应先用ClearCommError函数清除错误标志和获取当前串口状态。读写操作分为同步和重叠I/O(异步)。同步执行时,函数直到操作完成后才返回;重叠I/O操作时,即使操作尚未完成,调用的函数也会立即返回,费时的I/O操作在后台进行。可见,同步操作线程被阻塞,效率低,只能用在对通信要求比较低的场合,我们一般用到的都是效率较高的重叠I/O操作。前面提到的CreateFile函数第六个参数设置为FILE_FLAG_OVERLAPPED即可指定Read File和WriteFile函数为重叠I/O执行。使用重叠I/O还需为读写函数指定一个Overlapped结构,该结构有五个数据成员,对串口通信来说,其中的Offset和hEvent成员是很重要的。Offset指示文件指针偏移量,在重叠I/O操作时系统不能自动维护文件指针,所以要靠Offset在程序中手动调整文件指针。而hEvent标志读写操作是否完成。若操作完成,
则将hEvent置为信号态;否则即置为非信号态。最后要说明的是,在重叠I/O操作时,读写函数返回值是FALSE并不能说明操作失败,应该调用GetLastError函数分析返回结果。如果 此时GetLastError函数返回值是ERROR_IO_PENDING,则说明操作未完成(并不是操作失败)。我们将用等待函数来等待操作的完成。典型的两个等待函数有WaitForSingleObject和GetOverlappedResult。函数的相同之处为都是等待读写操作指定的Overlapped结构hEvent成员置为信号态(即代表操作完成);不同之处是WaitForSingleObject是可设置超时,但无法得到重叠I/O操作的结果,GetOverlappedResult用来得到重叠I/O操作的结果,但无法设置超时。因此,我们经常两者结合起来使用,在用WaitForSingleObject等待操作结束后,用GetOverlap-pedResult得到操作结果。
1.4 关闭串口
串行口是非共享资源,某应用程序打开串行口后,即独占该资源,使其它应用程序无法再访问,直到该应用程序释放串口。所以对串口操作完成后,一定要关闭串口。关闭串口使用CloseHandle函数,该函数唯一参数即为用CreateFile打开串口时所创建的句柄。
2 多线程API函数
Windows是多线程(multi-threaded)、抢先多任务的(preemptible )。Windows中,一个可执行程序的运行时刻实例称为进程(process)。一个进程可以有多个线程(thread),Windows是按照线程分配CPU时间片的,而分配的机制就是抢先多任务方式。
对于读写串口这种耗时的工作,使用多线程技术,创建辅助线程来管理串口是一个常用的方案。这样在进行串口读写的同时,能对读入的数据进行处理。如果使用单线程,就需要等待串口读写操作完成,整个进程都被阻塞。而使用多线程就可以避免这种情况。
多线程也会带来一些新的问题,其中的一个问题就是线程的同步,如果同步问题解决不好,程序的稳定性会受到很大的影响。通常用到的几种线程同步的方法有互斥体对象(Mutex)、利用信号(Semaphore)、利用事件对象(Event)和设置临界区(Critical Section)。笔者在实际应用中使用的是事件对象结合Windows消息机制使线程同步,收到了很好的效果。
创建线程函数为CreateThread,用SuspendThread和ResumeThread函数来挂起和唤醒线程。创建事件函数为CreateEvent,用SetEvent和ResetEvent函数来将事件置为信号态和非信号态,以此来同步线程。串口通信的辅助线程管理经常还要用到SetCommMask和WaitCommEvent函数。SetCommMask用来指定一系
列事件监视串口,比如监视串口是否有数据收到;WaitCommEvent则用来等待指定的事件发生。笔者在实际应用中,就是在辅助线程中用SetCommMask指定串口监视接收数据事件,然后用WaitCommEvent等到串口真的接收到数据时,用PostMessage发出消息通知主线程,由主线程处理接收到的数据。
3 实现消息机制的API函数
Windows是一个消息驱动操作系统,简单的说消息就是指通过输入设备向程序发出的指令以要求
其执行某个操作。具体的操作由消息处理函数实现。用户可以自定义消息在线程之间传递。把WM_USER(它的值等于0×0400)当作基数,然后顺序地去加序号,譬如:
WM_COMMNOTIFY equ WM_USER+100h(小于WM_USER的值是Windows系统的保留值,大于该值留给用户来使用)。
前一节已经提到在串口通信编程中对消息机制的利用,这里将继续说明怎样实现消息机制。由于笔者使用的编程工具是Borland公司的C++ Builder(BCB),因此对于消息机制的实现有其特殊之处。在BCB中实现消息的方法有三种:使用消息映射Message Map重载TObject的Dispatch虚成员函数;重载TControl的WndProc方法;重载Appli
cation的OnMessage方法。其中以第三种方法最快,因为一般情况下,BCB会为每个程序自动生成一个TApplication类的实例,消息到达BCB程序时,最先得到它们的就是TApplication对象。经由TApplication之后,才传递给Form的。前两种方法都是重载TForm的方法,显然比直接重载Application的OnMessage方法要晚一些收到消息。我们要做的只是定义好自己的消息处理函数:
void __fastcall TForm1MyOnMessagetagMSG &Msg
bool &Handled
TMessage Message
switch(Msg.message)
case WM_COMMNOTIFY
Message.Msg=Msg.message;
Message.WParam=Msg.wParam;
Message.LParam=Msg.lParam;
//此处添加处理该消息的代码
Handled=true;
break;
然后在窗口创建时用自定义的消息处理函数重载Application的OnMessage方法:
void __fastcall TForm1FormCreate(TObject Sender)
Application->OnMessage = MyOnMessage;
这样就可以在程序中收到自定义的消息并作出相应处理。
值得注意的是,使用Application->OnMessage并不能捕获非队列消息,它无法捕获使用SendMessage直接发送给窗口的消息,这是因为其不通过消息队列。但可以使用另一个发送消息的API函数——PostMessage,该函数发出的消息是队列消息。
4 结束语
利用多线程、消息机制和重叠I/O的API函数进行串口编程的方法,可实现串口通信的实时高效,为开发Windows系统下串口驱动程序提供了有益参考。&
参考文献
1 范逸之.江文贤.陈立元.C++ Builder与RS-232串行通信控制[M].清华大学出版社,2002
2 黄军等.Delphi串口通信编程[M].人民邮电出版社,2001
3 张志明.李蓉艳.王磊.WIN32环境下串行通信编程技术研究[J].《计算机应用研究》 2002;9