基于多线程技术的多串口通信王苓1,苏维均2(1.北京工商大学信息工程学院,北京100037;2.北京工商大学信息工程学院,北京100037)摘要:介绍了一个多串口通信模块,该模块采用VC++6.0并结合多线程技术编写,用来处理从远程终端站上传来数据。同时良好的线程同步解决方法也保证了模块程序能够运行更可靠,数据的实时性能更好。关键词:串行通信;多线程;线程同步;API接口MultiportCommunicationBasedonMultithreadTechnologyWANGLing,SUWei-jun(BeijingTechnology&BusinessUniversity,Beijing,100037,China)Abstract:Presentsamultiportcommunicationmodule,whichcandealwithdatafromRTUbyMultiportSerialBoards,combinewithmultithreadtechnology.ThemoduledevelopedwithVC++6.0.Inordertoresolvethereal-timeproblemindataacquisitionandreliabilityinprogram,thesynchronizationofthesethreadsamongtheserialportcommunicationhavebeendescribedindetail.Keywords:serialcommunication;Multithread;synchronizationofthreads;APIinterface1引言在目前的工控系统中,工控仪器如何把可靠数据成功传送给计算机终端已经是一个至关重要的环节,而串行通信以其方便易行,信道成本比较低廉的优势,常常作为计算机与外部串行设备之间的首选数据传输通道,又因为许多设备和计算机都可以通过串口对外设进行控制、检测,串口通讯日益成为计算机和外设进行通讯、获取由外设采集到的监测数据的一个非常重要的手段。本文是在搭建多段长输石油管道泄漏监测系统时总结的有关多串口通信技术方面的讨论。这是作者简介:王苓(1979-10),女,天津人,在校硕士研究生,主要研究方向控制理论与控制工程;苏维均(1962),男,北京人,副教授,主要从事检测技术与智能控制方向的研究一套实时数据采集监控系统,其中的实时数据与各个站点的现场情况以及管道石油的运行状况密切相关。该系统把远程终端站(RTU)采集到的数据按照自定义的串口通讯协议通过RS-232串口线传到中心控制站(MTU)。当管线的压力数据产生变化时,中心控制站的监测系统就会根据这些原始数据作出分析和判断。由于本程序要对串行端口进行实时监控,这就要求它是一个后台程序,在监控的同时可以在前台进行其他一些与之无关的操作。所以在这里创建几个辅助线程就可以完成此任务。它是一个没有消息循环,执行后台任务的好方式。当然,使用ActiveX控件也能完成此通信任务,而且程序实现也非常简单,结构十分清晰。但是此法有一个很大的缺点就是欠灵活。比如,ActiveX控件对二进制数据的支持不是很好,遇到0X00时会停止发送数据。而使用API通信函数时这个问题得到较好的解决,该函数用起来非常灵活,但是处理过程也相对较繁琐。综合以上特点本文采用了多线程技术来完成这一功能。Windows是抢先式多任务的操作系统,启动了一个应用程序就等于启动了一个进程。一个进程通常拥有一个线程,在系统资源管理中,每一个线程被分配一定的时间片[3]。采用多线程的设计方法可以使程序拥有多个线程,这样程序就能同时处理更多的任务。当使用多个串行口进行通讯时,每个线程处理一个串行口的通信任务,实时监视串行口的事件并做数据的预处理。然后利用Windows的消息分发机制,将串行口事件发送到主窗口,由主窗口的消息响应函数进行数据的综合处理。下面介绍的程序主要完成了含多串口的上位机监控程序所涉及到的无阻塞通信后台运行、数据的实时接收、处理和显示等功能。2设置站点属性2.1设定各站点处理数据的参数虽然之前准备了很多串口,但未必都是作为数据通信之用,所以要求对每个站点占用的串口,采用的通信方式,以及下位机采集数据需要用到的参数进行设置。如图1。2.2配置串口并确定其可用CommConfigDialog是弹出系统内置串口设置对话框的API,我们利用其在设备管理器中设置串口参数对话框。使用此API时不用先打开端口,它并不针对一个已打开的端口,而仅仅是把DCB的内容填写到对话框中,当按确定后把输入的结果存回到DCB数据结构中。本文用到该函数,除了要完成以上的功能外,还增加了检验该串口是图1站点通信属性设置否存在,是否被占用的功能。如图2。具体实现代码如下:voidCSerialComm::CommConfig(CStringstrPort){…………………HANDLEh_CommDev;//2005-5-18h_CommDev=CreateFile(sCommName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);if(h_CommDev!=(HANDLE)-1){DWORDdwSize;COMMCONFIGsCommConfig;dwSize=sizeof(COMMCONFIG);GetCommConfig(h_CommDev,&sCommConfig,&dwSize);sCommConfig.dcb.BaudRate=lpApp-GetProfileInt(sCommName,BaudRate,9600);if(CommConfigDialog(sCommName,m_hWnd,&sCommConfig)==1){SetCommConfig(h_CommDev,&sCommConfig,dwSize);lpApp-WriteProfileInt(sCommName,BaudRate,sCommConfig.dcb.BaudRate);}CloseHandle(h_CommDev);}elseMessageBox(串口被占用或不存在!!,提示信息,MB_OK);}3通信连接在程序启动之初,虽然作了六个备用的辅助线程,每个线程监测一个站点(串口),但是为了节省系统资源,并没有把所有线程全部打开,而是由现场操作人员选择要监测的站点。下面仔细介绍从建立通信链路到对各种数据拆包、处理的全过程。3.1握手(1)在系统运行之初开启通信类的定时器,这样方便在线程建立之前监测下位机回复的握手信号是否正确。以创建文件的形式打开文件,采用异步通信的方式,串口可读写,填充DCB数据控制块结构,设置COMMTIMEOUTS超时。(2)向下位机发送握手信号,定时查询是否有回复正确的信号出现。当查询超过一定时间后,仍未收到设定的信号,则认为握手信号发送失败。重新发送握手信号,直至收到设定的信号为止。3.2下载采集参数,开启数据处理线程下载下位机进行数据采集所需参数,一旦查询到下位机回复了设定的信号则开启对应站号的线图2串口属性程。实现函数为:if(m_nStationNum==1)//m_nStationNum为站点号{……………m_hStation1Thread=CreateThread((LPSECURITY_ATTRIBUTES)NULL,0,(LPTHREAD_START_ROUTINE)WaitStation1Event,//WaitStation1Event为1号站辅助线程的处理函数(LPVOID)this,0,&wStation1ThreadId);if(m_hStation1Thread==NULL){AfxMessageBox(创建线程失败!);return;}}3.3数据处理3.3.1线程同步因为同一进程的所有线程共享进程的虚拟地址空间,进程中的线程是并行执行的,每个线程占用CPU的时间由系统来划分。系统为每一个线程分配一个CPU时间片,某个线程只有在分配的时间片内才有对CPU的控制权。实际上,在PC机中,同一时间只有一个线程在运行,系统不停地在各个线程之间切换,而且线程是汇编级中断,所以有可能实现多个线程同时访问同一个对象的情况。这些对象可以是全局变量,MFC的对象,也可以是MFC的API等等。而串口通信对每个串口对象只提供一个缓冲区,即发送接收都要用到这一个缓冲区,如果一个线程在未完成对某一大尺寸全局变量的读操作时,另一个线程又对该变量进行了写操作,那么第一个线程读入的变量值可能是一种修改过程中的不稳定值。所以必须建立同步线程,使得一个时刻只能进行一种线程操作,一个线程必须等待另一线程结束才能开始。在处理一个线程的同时必须把其余待处理的线程挂起等待,以减少待处理线程对CPU的资源占用。正处理的线程一旦处理结束,则通过线程间的通信发出信号来击活被挂起的线程中的一个线程进入处理。这样才能达到协调运行多个线程的目的。VC++提供了多种同步对象来协调实现多线程的并行,Csemaphore信号灯对象,Cmutex互斥量对象,Cevent事件对象,以及CcriticalSection临界区对象都可以做到这一点。以临界区对象为例,进程负责为临界区对象分配内存空间,临界区对象实际上是一个CRITICAL_SECTION型的变量,它一次只能被一个线程拥有[1]。在线程使用临界区对象之前,必须调用InitializeCriticalSection函数将其初始化。如果线程中有一段关键的代码不希望被别的线程中断,那么可以调用EnterCriticalSection函数来申请关键节的所有权,在运行完关键代码后再用LeaveCriticalSection函数来释放所有权。如果在调用EnterCriticalSection时关键节对象已被另一个线程拥有,那么该函数将无限期等待所有权。具体实现代码为:UINTWaitStation1Event(LPVOIDlpParameter){……………while(1){if(lpSerialComm1-m_bStation1Exit)AfxEndThread(TRUE);//接到1号站停止标志,停止线程ReadMask=0;ReadNum=0;WaitCommEvent(lpSerialComm1-m_hCOM,&ReadMask,&os);//等待串口通信事件的发生,检测返回的dwEvtMask,知道发生了什么串口事件Event=WaitForMultipleObjects(2,lpSerialComm1-m_hEventArray,FALSE,INFINITE);//m_hEventArray[0]为读事件,m_hEventArray[1]为写事件switch(Event){case0:{//readeventif(ReadMask&EV_RXCHAR==EV_RXCHAR){Sleep(600);COMSTATComStat;DWORDdwLength;DWORDdwErrorFlags;EnterCriticalSection(&lpSerialComm1-m_csCommunicationSync);ClearCommError(lpSerialComm1-m_hCOM,&dwErrorFlags,&ComStat);LeaveCriticalSection(&lpSerialComm1-m_csCommunicationSync);dwLength=ComStat.cbInQue;//输入缓冲区有多少数据?if(dwLength0){EnterCriticalSection(&lpSerialComm1-m_csCommunicationSync);BOOLfReadStat;fReadStat=ReadFile(lpSerialComm1-m_hCOM,lpSerialComm1-m_COMInput,dwLe