串口通信工程筆記一


一、串口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操作系統發出的消息后,才進行相應處理,以免數據丟失。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM