内容 |
1. 引言
随着科技的发展,软硬件资源的成熟和完善,嵌入式系统在现代工业控制领域中得到了越来越广泛的应用,其应用领域涉及通信、自动化、信息家电、军事等各个方面。而嵌入式操作系统的引入大大提高了嵌入式系统的功能,方便了嵌入式应用软件的设计。 Windows CE是微软公司开发的一种嵌入式实时操作系统,它是一种模块化的、实时的、有强大的通信功能的、抢先式、多任务具有强大通信功能的32位嵌入式操作系统。 在嵌入式系统的实现中一般都会涉及数据的采集和处理,因此数据的通信成了系统稳定可靠运行的关键。串行通信是计算机与外部设备交换信息的重要途径,由于其实现简单,节省I/O口和线路,传输时序明晰等特点,应用的非常普遍,同样在嵌入式系统中它也是一种主要的通信方式。在本文中研究的是基于Windows CE操作系统的掌上电脑和单片机之间的串行通信问题。
2. 系统结构和Windows CE简介
本文介绍的是一种基于掌上电脑的便携式动态心电信号采集及处理系统,主要讨论系统串行通信的设计和实现。整个系统由检测模块和掌上电脑两部分组成。其中,检测模块是由AT89C52单片机控制的智能模块,负责心电信号的检测、放大、滤波与采集;掌上电脑负责参数的设置,心电波形数据存储、处理、分析以及波形显示等;掌上电脑基于Windows CE操作系统。图1为整个系统的功能框图,检测模块与掌上电脑之间通过RS232接口实现通信,而掌上电脑通过RS232或USB接口和PC机进行数据通信,由PC机对数据进行深入的分析和处理。整个系统的实现中,数据的串行通信是最基本也是最重要的部分。由于掌上电脑和PC机之间的通信由商家提供专门的接口线以及驱程,因此我们在这不作具体的研究。 Windows CE作为一种嵌入式操作系统,它的很多特性都是为了适应嵌入式系统的特殊要求,它与一般的Windows程序有很多区别,如API函数,存储器的限制,电源管理方式,硬件特性等等。但是在通信方面Windows CE基本拥有和Windows同样的Win32 API,因为运行Windows CE的系统或者是移动的,或者需要与远程服务器进行连接,因此必须具有强大的通信功能。Windows CE下的应用程序是通过文件I/O函数CreateFile,ReadFile,WriteFile,CloseHandle访问设备驱动程序的,对文件进行操作时,在Windows CE下的设备不支持重叠I/O。

图1 系统整体结构概略图
3. Windows CE下基于多线程的串行通信实现
什么是使用多线程的好时机呢?如果你的程序有许多事要忙,但是你还要随时保持注意某些外部事件(可能来自硬件或来自使用者),这时就适合使用多线程来帮忙。以通信程序为例,你可以让主线程负责使用者界面,并保持中枢的地位,而以―个分离的线程处理通信端口,这样就可以在串口读写数据的同时保持使用者界面依然灵活,不受影响。本文就是采用这种多线程的方法来实现串行通信的,创建了单独的读和写线程来处理串口读写数据。 Windows CE下的串行设备被视为用于打开、关闭、读和写串行端口的常规、可安装的流设备。这里我们构造一个串口类CSerial来对Win32 API串口操作函数CreateFile,ReadFile,WriteFile,CloseHandle等进行封装,并在其中完成对串口的各项设置。在本系统中主要时在Windows CE环境中接收单片机上传的大量数据,因此我们将对数据的接收作比较详细的分析。
1) 串口的打开和配置 在类CSerial中用BOOL Open( int nPort, int nBaud)来完成串口的打开和初始化工作。先调用CreateFile打开指定的串口,然后通过GetCommState和SetCommState函数来配置串口,最后设置串口读写数据的超时值。 配置串口时一般先调用GetCommState得到默认的DCB结构,然后根据自己的需要来对它作必要的修改,再用SetCommState来重新配置串口。DCB结构包括波特率、流控制、传输模式、起始位、停止位、校验等设置。需要注意的是Win32操作系统一般只支持二进制的传输模式,因此fBinary字段应设为TRUE,另外接收缓冲器应该尽量设的大一些。 下面具体研究一下读写数据的超时值,通过GetCommTimeouts和SetCommTimeouts对COMMTIMEOUTS结构的5个字段进行设置。通常在实现串口通信时往往不重视甚至忽略对读写数据超时值的设置,这样可能就会造成串口数据读写的不可靠性,特别是在接收大量数据时,如果超时值的设置不合适将会使数据不能完全接收过来而导致通信出错。在本系统中如下设置串口超时值。 COMMTIMEOUTS CommTimeOuts; CommTimeOuts.ReadIntervalTimeout =10; CommTimeOuts.ReadTotalTimeoutMultiplier =10; CommTimeOuts.ReadTotalTimeoutConstant = 10; CommTimeOuts.WriteTotalTimeoutMultiplier = 5; CommTimeOuts.WriteTotalTimeoutConstant = 5; 其中ReadIntervalTimeout设置串口相邻字节接收间隔时间的最大值,单位为毫秒。如果前后两个字节之间的间隔时间超过该设定值,ReadFile就返回,终止接收。ReadTotalTimeoutMultiplier用来计算 ReadFile函数的总超时,单位为毫秒。每次读取串口操作,将其与要接收字节数相乘再与ReadTotalTimeoutConstant相加来计算 ReadFile函数的总超时时间。写操作两个字段的设置与读操作类似。 当波特率较高时,ReadIntervalTimeout不能设的太大,否则两次接收将会当作一次处理,通信将出现错误。而对于后两者,由于 ReadFile当总超时时间到时要立刻返回,因此要综合考虑波特率、应接收字节数等因素,以期串口的正确运行。很多人在实现串行通信时简单的将ReadIntervalTimeout设置为MAXDWORD, ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant设为0,这种做法在进行大量数据传输中并不适用,可能会导致数据的丢失。也不能在设置了适当的字节间超时后就简单的把总超时设为0以期待直到所有数据读完后ReadFile才返回,这样可能会使ReadFile一直处于等待状态,不能正常返回。
2) 数据的接收
数据的接收我们用DWORD ReadData(char *data,CString FileName)函数来完成,如下所示。 DWORD CSerial::ReadData( char *data,CString FileName) { char Byte[1000]; DWORD dwComStatus,dwBytesTransferred; DWORD len=0; CFile ECGFile; ECGFile.Open(FileName,CFile::modeCreate|CFile::modeWrite); SetCommMask (m_hComID, EV_RXCHAR | EV_CTS | EV_DSR); if (m_hComID!= INVALID_HANDLE_VALUE) { WaitCommEvent (m_hComID, &dwComStatus, 0); if (dwComStatus & EV_RXCHAR) { do { ReadFile (m_hComID, &Byte, 1000, &dwBytesTransferred, 0 ); if (dwBytesTransferred) { // strncat(data,Byte,dwBytesTransferred); //接收数据较少时 strncpy(data,Byte,dwBytesTransferred); //接收数据较多时 len+=dwBytesTransferred; g_nCount=len; ECGFile.Write(Byte,dwBytesTransferred); ECGFile.Flush(); } } while (dwBytesTransferred); } } ECGFile.Close(); return len; } 该函数的调用是在一个单独的线程函数ReadThread中,我们创建一个单独的线程来读串口数据,用如下的语句来创建该读线程。 hReadThread = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)ReadThread, this, 0, &dwThreadID)) 在ReadData函数中先使用SetCommMask设置事件掩码,然后WaitCommEvent就阻塞线程,直到“串口接收到一个字符”的预定事件发生线程才继续执行。
在用ReadFile函数读数据是要注意以下3点: a) 接收缓冲区Byte的大小最好和ReadFile中第3个参数(即要读取的字节数)一致。 b) 缓冲区Byte的大小要根据实际情况来设置,当要接收的数据比较多,波特率又设的较高时应尽量将缓冲区设的大些,否则可能会使数据丢失。 c) ReadFile中的第四个参数是实际接收到的字节数,由于通信中常常不可预料的会发生各种异常情况,每次实际接收到的字节数未必和你希望接收的数量一致,所以当每次从接收缓冲区中取数据时应以dwBytesTransferred的值为准,这样可以避免将不是串口得到的数据也错误的取进来。 当接收的数据量大时我们不得不考虑到Windows CE系统的内存限制问题,那么有限的内存根本无法将那么多的数据同时放在内存中。实际情况确实也是这样的,在实验中每次当串口接收的数据多达几十K时,往往会发生堆栈溢出等异常。于是我们考虑将每次ReadFile接收到的数据读进内存后就将它永久存储到对象存储器中,当然也可以是自备的存储卡,就像上面给出的程序,我们用MFC中的CFile类来完成文件的存储功能。ReadData函数的第2个参数传入的就是存储文件的路径和名字。这样每次只要消耗固定量的内存,解决了内存的问题。当然,如果在实际中需要从串口接收的数据不是很多时,为了方便数据的处理,我们通常还是把它们都放在内存中。
3) 数据的发送 主要是在函数SendData中调用了API函数WriteFile,本系统中只需向单片机发送一些参数的设置和简单的控制指令,应用相对比较简单。我们也创建一个单独的线程来写数据到串口,对SendData函数的调用在线程函数SendThread中,创建写线程的方法和读线程类似。
4) 串口的关闭 串口的关闭是最简单的,只需使用CloseHandle函数就可以了。
4. AT89C52单片机的串行通信
智能采集部分我们采用的是AT89C52单片机,采用中断的方式来与掌上电脑进行数据通信。我们设定单片机的串口控制寄存器SCON=0x50 ,使串口工作在方式1(即10位异步收发方式),在这种方式下,串行口的波特率是可编程的,由所使用的定时器的溢出率决定。AT89C52除了有定时器0和1外,还增加了定时器2,定时器2是一个16位定时/计数器,其控制和状态位位于T2CON和T2MOD,寄存器对RCAP2H,RCAP2L是定时器2在16位自动重装载方式下的自动重装载寄存器。 在单片机的串行通信中波特率的设定是最关键的工作,它决定了通信的速度和成败。波特率最终是由单片机的主机频率和定时器的工作方式决定的。通常情况下,单片机的晶振频率一般选用12M或24M等整数,采用定时器1来作为波特率发生器,因为51系列的单片机没有定时器2。这样就会出现问题,大家经常会发现当设置波特率较高时串口接收的数据就会发生错误。经过了一段时间的研究,我们找到了原因,当采用T1作为自动重装初值的8位计数器来产生波特率时,由于单片机晶振是12M或24M,T1的计数频率是1/12的单片机主频,根据T1的溢出率计算得出的定时器初值不够精确,会产生一定的误差,而且误差随着所设波特率的提高而增加。这时的波特率计算公式如下:
波特率= 
其中fosc是单片机主频,当SMOD=1时,波特率加倍。 有如下2个方法可以解决这个问题: 1) 调整单片机的主频,可以选用11.0592M,22.1184M等来消除波特率设置的误差。 2) 采用具有16位定时/计数器T2的单片机,如AT89C52。这时使用T2的16位自动重装初 值的工作方式来产生波特率,在串口工作在工作方式1时,波特率的计算公式如下: 波特率=  由于T2的初值是16位的,且这种工作方式下T2的计数频率是1/2的单片机主频,按照上述公式计算得到的定时器初值的精度足以实现我们所需的波特率。 根据上面的分析,我们采用第2个方案,设置T2CON=0x34,使T2工作于波特率发生器方式,通过TH2,TL2设置定时器初值,在该方式下寄存器RCAP2H和RCAP2L中的值应与TH2和TL2中相同,以便在T2溢出时,将RCAP2H和RCAP2L中的初值自动重装到TH2和TL2中。 具体的单片机串口设置如下: SCON=0x50; //串口工作在方式1 TH2=0xff; TL2=0xd9; //设置波特率为19200 RCAP2H=0xff; RCAP2L=0xd9; T2MOD=0x00; T2CON=0x34; //T2工作工作于波特率发生器方式 IE=0x90; //开串口中断
5 总结
本文介绍了在Windows CE环境下与单片机的基于多线程的串行通信的实现问题。深入研究了Windows CE中对基于多线程的串口通信的各项设置和数据接收中应注意的地方,并提出了AT89C52单片机的串口通信中波特率正确设定的方法,特别适用于传输的数据量较大且波特率较高的情况。在实际工作中,我们利用基于Windows CE的系统,通过RS-232C标准接口,与使用单片机的采集模块进行大量数据通信,采用文中介绍的方法,实现了准确、可靠的数据传输。
|