用Windows API设计多线程的串行通信ActiveX控件
2009-05-05
作者:雷 鹏 陈 星
摘 要: 介绍了利用VC++6.0实现基于Window API的多线程串行通信ActiveX控件的设计方法,并给出主要的通信程序代码。
关键词: ActiveX Windows API 串行通信 多线程
串行通信是计算机之间及计算机与数字化仪器和设备的一种重要通信手段,是实现工业监控的一种主要方式。Windows下的串行通信主要有两种方法:利用VB的MSCOMM控件和利用Windows API。MSCOMM控件简单易用,但由于其对串口设备的封装及调用方式的局限性,不能灵活方便地对串口设备进行控制。而通过Windows API则可以实现对串口设备的完全控制,并且可以提供多线程的通信机制。
在复杂应用中,通信通常在后台完成,需要采用多线程技术。一个多线程的应用程序实际上是在其内部实现了多任务扩展,为代码赋予了并行执行的特性,适于执行一些实时性或随机性很强的操作,也有利于提高CPU的利用率,加快通信程序的信息处理速度。
本文以一台工业控制PC机与多台基于单片机的智能控制单元进行串行通信为实例。PC机和各智能控制单元通过RS485总线互联。由于RS485的通信方式是半双工的,只能由作为主节点的PC机依次轮询网络上的各智能控制单元子节点。每次通信都是由PC机通过串口向智能控制单元发布命令,智能控制单元在接收到正确的命令后做出应答。
系统的主节点应用程序是用VB6.0编写的,为了既能提供多线程的串行通信机制,又可使应用程序易于实现串行通信功能,利用VC++6.0开发基于Window API的多线程串行通信ActiveX控件。主节点的应用程序通过对串行通信ActiveX控件的调用完成与各子节点的通信。
1 创建ActiveX控件JinRiComm.OCX
VC++6.0和MFC是创建ActiveX控件的强大而又灵活的工具。JinRiComm控件创建步骤简单概述如下:
(1)用MFC ActiveX ControlWizard生成ActiveX控件工程,命名为JinRiComm。
(2)打开ClassWizard窗口,选择Automation标签,单击“Add Property”按钮,命名新的属性。单击“Add Method”按钮,命名新的方法。选择ActiveX Event标签,单击“Add Event”按钮,命名新的事件。
(3) 向控件工程中添加类CSerialPort,为该类添加成员变量和成员函数,该类将完成串行通信工作。
2 串口通信的基本编程
用Windows API函数实现串行通信,其特点是对串口的操作如对文件操作一样,打开和关闭串行设备与打开和关闭文件使用相同的函数。
(1) 打开串口
m_hComm = CreateFile(szPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
(2) 获取当前通信信息,设置通信设备
GetCommState(m_hComm, &m_dcb);
SetCommState(m_hComm, &m_dcb);
(3) 读、写串口
bResult=ReadFile(port->m_hComm, &RXBuff, 1, &BytesRead, port->m_ov);
bResult=WriteFile(port->m_hComm,&(port->m_Byte)[i],1, &BytesSent, &port->m_ov);
(4) 关闭串口
CloseHandle(m_hComm);
3 设计程序中的线程
MFC执行两种类型的线程:用户界面线程和工作线程。前者用来处理用户输入,响应由用户产生的事件和消息。后者不处理窗口消息,用于完成后台计算、打印和其它一些没有必要强迫用户来等待的任务。在本程序中,用户界面线程就是程序的主线程,另外再添加两个工作线程:通信线程和延时线程。它们的功能介绍如表1所示。
应用AfxBeginThread函数来启动一个工作线程,用法如下:
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,LPVOID pParam,int nPriority=THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL )
在启动一个工作线程之前,必须为线程编写一个全局的线程函数。这个线程函数接受一个32位的LPVOID作为参数,返回一个UINT,线程函数的结构为:
UINT ThreadUFunction(LPVOID pParam )
{
//线程处理代码
return 0;
}
终止线程有两种途径:当线程函数返回时,线程终止;线程函数也可以在内部调用AfxEndThread函数来终止自己。
程序流程图如图1所示。
4 线程间的通信
(1) 通过全局变量
主线程可以采用多种方式与工作线程进行通信,最简单的办法是通过全局变量,因为进程中的所有线程都可以访问所有的全局变量。如:定义全局变量bReceiveSuccess,它表示是否收到了正确的响应。在主线程向串口写数据之后它被置为FALSE,然后延时线程启动。当系统收到正确的响应后,bReceiveSuccess被主线程改为TRUE。延时线程根据bReceiveSuccess的值来决定是结束该线程还是给主线程发消息。
(2) 通过参数
主线程可以向工作线程传递一个4字节的参数,一种使用该参数的常见方式是传递一个指针,它指向这个线程的父类。如:
UINT CSerialPort::CommThread(LPVOID pParam)
{
CSerialPort *port=(CSerialPort*)pParam; //取得串口类指针
//线程处理代码
}
(3) 通过消息
工作线程获得主线程的窗口句柄,则可以给主线程发送消息。如:
通信线程通知主线程,串口接收到了数据
::PostMessage((port->m_pOwner)->m_hWnd, WM_COMM_RXCHAR,(WPARAM) RXBuff, (LPARAM) port->m_nPortNr);
5 线程的同步
多线程的优点之一是所有线程都可以访问相同的全局对象和共享资源,它提供了程序设计的简捷性和便利性,提高了对信息处理的并发度。但如果不妥善处理好线程的并发问题,也会带来数据的错误或是资源的死锁。为了避免这些问题发生,线程在使用共享资源或对象前必须获得一个约束访问同步对象的权力,也就是通过同步的机制来控制这种权力的使用。线程间的同步有多种方法。
(1) 临界区
临界区是通过对多个线程的串行化来访问公共资源或一段代码。如:
InitializeCriticalSection(&(port->m_csCommunicationSync)); //初始化临界区对象
EnterCriticalSection(&port->m_csCommunicationSync);
//使调用线程等待获得临界区对象并在获得拥有权时返回
Do
{
if(!bReceiveSuccess) //访问全局变量
{
LeaveCriticalSection(&port->m_csCommunicationSync);
//释放对临界区对象的拥有权
//其它处理代码
}
}
(2) 事件
事件用来通知线程有一些事件已经发生,比较适合于信号控制。事件有手动复位和自动复位两种。手动复位事件是在应用程序或系统后台控制下改变它的信号状态。当手动复位事件处于有信号状态时,所有等待该事件的线程都被激活,事件保留有信号状态直到被一个应用程序复位为止。当一个自动复位事件处于有信号状态时,只有一个等待线程会被激活,并且事件将复位成无信号,其它所有等待着的线程仍将保持挂起状态。
定义3个事件:
m_hEventArray[0] = m_hShutdownEvent; //结束通信线程事件
m_hEventArray[1] = m_ov.hEvent; //读事件
m_hEventArray[2] = m_hWriteEvent; //写事件
在通信线程的线程函数CommThread中等待3个事件的发生
Event=WaitForMultipleObjects(3,port->m_hEventArray, FALSE, INFINITE);
switch (Event)
{
case 0: //结束通信线程事件
{
port->m_bThreadAlive = FALSE;
AfxEndThread(100);//结束通信线程
break;
}
case 1: //读事件
{
GetCommMask(port->m_hComm, &CommEvent);
if (CommEvent & EV_RXCHAR)
ReceiveChar(port, comstat);//从串口读数
break;
}
case 2: //写事件
{
WriteChar(port); //向窗口写数
break;
}
} // end switch
参考文献
1 Visual C++6.0 Online Help [M]
2 (美)本内特(Bennett,D.)著,徐 军译.Visual C++5开发人员指南.北京:机械工业出版社,1998.6