串口通信协议

一、串口API

1.  打开串口

  使用CreateFile函数可以打开串口。通常有两种方式可以打开,一种是同步方式(NonOverlapped),另外一种异步方式(Overlapped)。

        HANDLE hComm;

        hComm = CreateFile(   gszPort,                                                    //串口名

                                           GENERIC_READ|GENERIC_WRITE          //读写

                                           0,                        //注意:串口为不可共享设备,本参数须为0

                                           0,

                                           OPEN_EXISTING,

                                           FILE_FLAG_OVERLAPPED,                      //异步方式

                                           0);

        if(hComm == INVALID_HANDLE_VALUE)     //打开串口失败处理

               ······

2.   配置串口

  DCB(Device Control Block)结构定义了串口通信设备的控制设置,有3种方式可以初始化DCB。


通过GetCommState函数得到DCB的初始值:

      DCB dcb;

               memset(&dcb, 0, sizeof(dcb));

               if(!GetCommState(hComm, &dcb))     ……        //错误处理

               else ……                                                         //已准备就绪


用BuildCommDCB函数初始化DCB结构:

      DCB dcb;

      memset(&dcb, 0, sizeof(dcb));

      dcb.DCBlength = sizeof(dcb);

      if(!BuildCommDCB(“9600,n,8,1”,  &dcb))       ……     //参数配置错误

      else ……                                                                //已准备就绪


用SetCommState函数手动设置DCB初值:

      DCB dcb;

      memset(&dcb, 0, sizeof(dcb));

      if(!GetCommState(hComm, &dcb))     return FALSE;

      dcb.BaudRate = CBR_9600;

3.  流控设备

  流控制有如下两种设置:


硬件流控制:硬件流控有两种,DTE/DSR方式和RTS/CTS方式。这与DCB结构的初始化有关系,建议采用标准流行的流控方式,采用硬件流控时,DTE、DSR、RTS、CTS的逻辑位直接影响到数据的读写及收发数据的缓冲区控制。
软件流控制:串口通信中采用特殊字符XON和XOFF作为控制串口数据的收发。

注意:在不设置流控制方式或软件流控的情况下,基本上不会出现什么问题,但在硬件流控下,规范的RTS_CONTROL_HANDSHAKE流控方式的含义本来是当缓冲区快满的时候RTS会自动OFF通知对方暂停发送,当缓冲区重新空出来的时候,RTS会自动ON,但很多时候当RTS变OFF以后即使已经清空了缓冲区,RTS也不会自动的ON,造成对方停在那里不发送了。所以,如果要用硬件流控制的话,还要在接收后最好加上检测缓冲区大小的判断,具体做法是使用ClearCommError后返回COMSTAT.cbInQue,当缓冲区已经空出来的时候,要使用invoke(EscapeCommFunction,hComm,SETRTS)重新将RTS设置为ON。

4.  串口读写操作

  串口读写有两种方式:同步方式(NonOverlapped)和异步方式(Overlapped)。同步方式指必须完成了读写操作,函数才返回,这可能会使程序无响应,因为如果在读写时发生了错误,永远不返回就会出错,可能线程将停在原地。而异步方式则灵活的多,一旦读写不成功,就将读写挂起,函数直接返回,可以通过GetLastError函数得知读写未成功的原因,所以串口读写常常采用异步方式操作。

ReadFile()函数用于完成读操作,异步方式的读操作为:

  DWORD dwRead;

  BOOL fWaitingOnRead = FALSE;

  OVERLAPPED osReader;

  memset(&osReader, 0, sizeof(osReader));

  osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  if(osReader.hEvent == NULL)      ……       //错误处理

  if(!fWaitingOnRead)

  {

          if(!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader))              //读串口

          {

                 if(GetLastError() != ERROR_IO_PENDING)     ……       //报告错误

                 else fWaitingOnRead = TRUE;

     }

  }

  else

  {

    //读取完成,不必在调用GetOverlappedResults函数

    HandleASuccessfulRead(lpBuf, dwRead);

        }

 

  //如果读操作被挂起,可以调用WaitForSingleObject()函数或

  //WaitForMuntilpleObjects()等待读操作完成或者超时发生,

  //再调用GetOverlappedResult()得到想要的信息。

  if(fWaitingOnRead)

  {

    dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);

    switch(dwRes)

    {

    case WAIT_OBJECT_0:        //完成读操作

           if(!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))   …… //错误

           else ……        //全部读取成功

           HandleASuccessfulRead(lpBuf, dwRead);

             fWaitintOnRead = FALSE;

             break;

    case WAIT_TIMEOUT:         //操作尚未完成

           …….                           //处理其他任务

           break;

    default:

           ……              //出现错误

           break;

    }

  }

  注意上述代码在处理多线程串口在windows系列下存在一些问题,修改完成后代码参考1.4节。

5.  关闭串口

  程序结束或需要释放串口资源时,必须正确关闭串口。调用CloseHandle函数关闭串口的句柄即可,

         CloseHandle(hComm);

  值得注意的是,在关闭串口前必须保证读写串口线程已经退出,否则会引起误操作,一般采用的办法是使用事件驱动机制,启动一事件,通知串口读写线程强制退出。

6. 其他问题

  串口通信中其他必须处理的问题主要有如下几个:


检测通信事件:用SetCommMask()设置想要得到的通信事件的掩码,再调用WaitCommEvent()检测通信事件的发生。可设置事件标志有EV_BREAK \ EV_VTS \ EV_DSR \ EV_ERR \ EV_RING \ EV_RLSD \ EV_RXCHAR \ EV_RXFLAG \ EV_TXEMPTY。
处理通信超时:在通信中,超时是一个很重要的考虑因素,因为数据接收过程中由于某种原因突然中断或停止,如果不采取超时控制机制,将会使得I/O线程被挂起或无限阻塞。超时设置分两步,首先设置COMMTIMEOUTS结构的5个变量,然后调用SetcommTimeouts()设置超时值,对于使用异步方式读写的操作,如果操作挂起后,异步成功完成了读写,WaitForSingleObject()或WaitForMultipleObjects()将返回WAIT_OBJECT_0,GetOverlappedResult()返回TRUE。其实还可以用GetCommTimeouts()得到系统初始值。
错误处理和通信状态:在串口通信中,可以会产生很多的错误,使用ClearCommError()可以检测错误并且清除错误条件。
WaitCommEvent()返回时,只是指出了如CTS等等状态有变化。但要了解具体变化情况必须使用GetCommModemStatus()获得串口线路状态更详细的信息。

 

二、串口操作方式

1.  同步方式

  同步(NonOverlapped)方式是比较简单的一种方式,编写代码长度明显少于异步(Overlapped)方式。同步方式中,读串口的函数试图在串口的接收缓冲区中读取规定数据的数据,直到规定数据的数据全部被读出或设定超时时间已到时才返回。例如:

       COMMTIMEOUTS timeOver;

       memset(&timeOver, 0, sizeof(timeOver));

       DWORD timeMultiplier, timeConstant;

       ……

       timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;

       timeOver.ReadTotalTimeoutConstant = timeConstant;

       SetCommTimeouts(hComm, &timeOver);

       ……

       ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL); //NULL指采用同步文件读写

  如果所规定的待读取数据的数目nWantRead较大且设定的超时时间较长,而接收缓冲区中数据较少,则可能引起线程阻塞。解决这一问题的方法是检查COSTAT结构的cbInQue成员,该成员的大小即为接收缓冲区中处于等待状态的实际个数。如果令nWantRead的值等于COMSTAT.cbInQue,就能很好的防止线程阻塞。                       

2. 异步方式

  在异步方式中,利用Windows的多线程结构,可以让串口的读写操作在后台进行,而应用程序的其他部分在前台执行。例如:

       OVERLAPPED wrOverlapped;

       COMMTIMEOUTS timeOVer;

       memset(&timeOver, 0, sizeof(timeOver));

       DWORD timeMultiplier, timeConstant;

       ……       //给timeMultiplier, timeConstant赋值

       timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;

       timeOver.ReadTotalTimeoutConstant = timeConstant;

       SetCommTimeouts(hComm, &timeOver);

       wrOverlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

       ……

       ReadFile(hComm, inBuffer, nWantRead, &nRealRead,  &wrOverlapped);

       GetOverlappedResult(hComm, &wrOverlapped, &nRealRead,TRUE);

       ……

       ResetEvent(wrOverlapped.hEvent);

  上面代码中的ReadFile由于采用了异步方式,所以只返回数据是否已经开始读入的状态,并不返回实际的读入数据,即ReadFile中的nRealRead无效。实际读入的数据由GetOverlappedResult返回的,该函数的最后一个参数值为TRUE,表示它等待异步操作结束后才返回到应用程序,此时,GetOverlappedResult与WaitForSingleObject函数无效。

3. 查询方式

  即一个进程中的某一线程定时地查询串口的接收缓冲区,如果缓冲区中有数据,就读取数据;若缓冲区没有数据,该线程将继续执行,因此会占用大量的CPU时间,它实际上是同步方式的一种派生。例如:

              COMMTIMEOUTS timeOver,

              Memset(&timeOver, 0, sizeof(timeOver));

              timeOver.ReadIntervalTimeout = MAXWORD;

              SetCommTimeouts(hComm, &timeOver);

              ……ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL);

  除了COMMTIMEOUTS结构的变量timeOver设置不同外,查询方式与同步方式在程序代码方面很类似,但二者的工作方式却差别很大。尽管ReadFile采用的也是同步文件读写方式,但由于timeOver的区间超过时间设置为MAXWORD,所以ReadFile每次将读出接收队列中的所有处于等待状态的数据,一次最多可读出nWantRead个字节的数据。

4.  事件驱动方式

  若对端口数据的响应时间要求较严格,可采用事件驱动方式。事件驱动方式通过设置事件通知,当所希望的事件发生时,Windows发出该事件已经发生的通知。Windows定义了9中串口通信事件,常用的有以下3中:


EV_RXCHAR:接收到一个字节,并放入输入缓冲区。
EV_TXEMPTY:输出缓冲区中的最后一个字符,发送出去。
EV_RXFLAG:接收到事件字符(DCB结构中的EvtChar成员),放入输入缓冲区。

  在用SetCommMask()制定了有用的事件后,应用程序可调用WaitCommEvent()来等待事件的发生。SetCommMask可使WaitCommEvent()中止。例如:

              COMSTAT comStat;

              DWORD dwEvent;

              SetCommMask(hComm, EV_RXCHAR);

              ……

              if(WaitCommEvent(hComm, &dwEvent, NULL))

                     if((dwEvent & EV_RXCHAR) && comstat.cbInQue)

                            ReadFile(hComm, inBuffer, comstat.cbInQue, &nRealRead, NULL);

5. 总结

  一般要求情况下,查询方式是一种最直接的读串口的方式。但定时查询存在一个致命的弱点,即查询是定时发生的,可能发生的过早或过晚。在数据变化较快的情况下,特别是主控计算机的串口通过扩展板扩展多个时,需定时对所有串口轮流查询,容易发生数据的丢失。虽然定时间隔越小,数据的实时性越高,但系统的资源也被占用越多。

  Windows中提出文件读写的异步方式,主要是针对文件IO相对较慢的速度而进行的改进,它利用了系统的多线程结构,虽然在Windows中没有实现任何对文件IO的异步操作,但它却能对串口进行异步操作。采用异步方式,可以提高系统整体性能,在对系统强壮性要求高的场合,建议采用这种方式。

  事件驱动方式是一种高效的串口读方式。这种方式实时性较高,特别对扩展了多个串口的情况,并不要求像查询方式那样定时地对所有串口轮询,而像中断方式那样,只有当设定的事件发生时,应用程序得到windows操作系统发出的消息后,才进行相应处理,以免数据丢失。

posted on 2017-05-16 15:31  怀想天空2013  阅读(2370)  评论(0编辑  收藏  举报

导航