win32串口編程


 

翻譯自:ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.WIN32COM.v10.en/dnfiles/html/msdn_serial.htm

老外寫的文章,雖比較全面,但很啰嗦,不如看各個函數的文檔來得快。為方便以后查閱,列出本文涉及的主要函數如下:

CreateFile、ReadFile、WriteFile、GetOverlappedResult、WaitForSingleObject

SetCommMask、WaitCommEvent

ClearCommError、GetCommModemStatus、EscapeCommFunction

GetCommState、BuildCommDCB、SetCommState、SetCommTimeouts

0 簡介

本文僅關注在Windows NT和95間兼容的API。Windows 95支持Telephony API(TAPI),但Windows NT 3.x不支持TAPI,所以本文不討論它。

本文的示例程序MTTTY(Multithreaded TTY)使用了三個線程:一個進行內存管理的界面線程;控制所有寫入操作的寫入者線程;讀取數據和處理端口狀態改變的讀取/狀態線程。示例采用了一些不同的堆來進行內存管理;還大量使用了同步方法來進行線程間通信。

1 打開端口

使用CreateFile函數打開端口。打開端口時有兩種方法:重疊的和非重疊的。下列代碼片段以重疊方式打開端口:

 

HANDLE hComm;
hComm = CreateFile( gszPort,  
                    GENERIC_READ | GENERIC_WRITE, 
                    0, 
                    0, 
                    OPEN_EXISTING,
                    FILE_FLAG_OVERLAPPED,
                    0);
if (hComm == INVALID_HANDLE_VALUE)
   // error opening port; abort

 

 

 

 

 

 







去掉參數中的FILE_FLAG_OVERLAPPED就是非重疊操作方式了。
用CreateFile打開通信端口時,有下列限制:

  • fdwShareMode 必須是0。通信端口不能像文件那樣被共享。要共享通信端口,需要使用句柄繼承或者復制操作。
  • fdwCreate 必須指定 OPEN_EXISTING 標志。
  • hTemplateFile 參數必須是 NULL

端口名通常是COM1、COM2、COM3和COM4。Win32 API不提供確定系統中有哪些端口可用的機制。Windows NT和Windows 95跟蹤系統已安裝端口的方法是不同的,所以不太可能提供兼容的確定可用端口的方法。某些系統可能有多於4個端口,而傳統的通信端口最大個數是4。硬件廠商和串口驅動編寫者可以自由地為端口命名。所以,程序最好可以讓用戶指定要使用的端口名字。如果端口不存在,則試圖打開端口時會返回ERROR_FILE_NOT_FOUND錯誤,這時應該提示用戶端口不可用。

2 讀寫操作

通信端口的讀寫操作與文件I/O操作非常相似,它們使用同樣的函數。Win32的I/O操作可分為兩種:重疊(overlapped)的和非重疊的(nonoverlapped)。平台SDK文檔分別使用異步(asynchronous)和同步(synchronous)來表示這兩種I/O方式。

很多開發者都熟悉非重疊I/O,因為它就是傳統的I/O方式:函數返回時,所請求的操作已經完成。然而在重疊I/O的情況下,系統則可能在操作還沒有完成的情形下立即返回,隨后才通知調用者操作完成。程序可以在發起I/O請求和請求被完成之間進行一些后台工作。

2.1 非重疊I/O

非重疊I/O的工作方式很簡單:I/O操作進行時,調用線程被阻塞;操作完成后,函數返回,調用線程可以繼續執行。在多線程應用中,這種I/O方式很有用:一個線程阻塞在某I/O操作上時,其他線程可以繼續工作。應用程序應該保證對端口的串行訪問。某個線程阻塞在等待某I/O操作上時,其他線程后續的通信API調用也都將阻塞。比如說,一個線程在等待ReadFile調用返回時,另一個線程的WriteFile函數調用將阻塞。

在選擇使用非重疊還是重疊方式時,可移植性是要考慮的因素之一。有時候重疊操作並不是好的選擇,因為很多操作系統不支持它;然而很多操作系統都支持某種形式的多線程。所以從兼容性方面考慮,多線程非重疊I/O可能是最好的選擇。

2.2 重疊I/O

重疊I/O不像非重疊I/O那樣簡單易懂,但卻靈活高效。使用重疊方式打開的端口允許多個線程同時進行I/O操作,並且在操作進行期間可以進行其他的工作。此外,重疊操作的行為方式還允許單個線程提交多個不同的請求,然后在操作進行期間進行其他后台工作。

在單線程和多線程應用中,都必須在提交I/O請求和處理操作結果間進行一些同步操作。線程可能需要在操作結果可用前阻塞;當然也可以進行其他工作。如果沒有其他需要進行的工作,則重疊I/O的優點是更好的用戶響應性能。

MTTTY使用了重疊I/O。它創建用於讀取數據和監測端口狀態的線程,並且還定時進行一些后台工作;此外它還另外創建一個線程用於寫入數據。

重疊I/O操作分為兩個部分:創建I/O操作和檢測操作完成。創建I/O操作涉及到建立OVERLAPPED結構體、創建用於同步的手動復位事件、調用恰當的函數(ReadFile或者WriteFile)。I/O操作可能立即完成,也可能不能立即完成,不能認為一個重疊I/O操作請求總是生成一個重疊操作。如果操作立即完成,程序應該可以繼續進行通常的處理。檢測操作完成涉及到等待事件句柄、檢查操作完成結果、處理數據。與重疊I/O相關的工作更多的原因是有更多的失敗點。非重疊操作中,簡單地通過函數返回值判斷操作是否失敗;而重疊操作中,則可能在創建操作請求時失敗,或者操作阻塞期間失敗,也可能是操作超時,或者是等待操作完成信號超時。

2.2.1 讀操作

下面的代碼片段展示了提交重疊的讀操作請求的方法。注意,如果ReadFile返回TRUE,調用了一個函數處理數據。代碼定義了fWaitingOnRead標志,它表示是否有重疊的讀取操作存在,用於阻止在一個操作進行中時提交另一個讀取操作請求。

 

DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};
// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (osReader.hEvent == NULL)
   // Error creating overlapped event; abort.
if (!fWaitingOnRead) {
   // Issue read operation.
   if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
      if (GetLastError() != ERROR_IO_PENDING)     // read not delayed?
         // Error in communications; report it.
      else
         fWaitingOnRead = TRUE;
   }
   else {    
      // read completed immediately
      HandleASuccessfulRead(lpBuf, dwRead);
    }
}

 

OVERLAPPED結構體的事件句柄被傳遞給WaitForSingleObject以等待事件授信,操作完成。注意,事件受信表示操作完成,而不是操作成功完成。應該用GetOverlappedResult來取得操作結果,它返回TRUE表示操作成功完成;FALSE表示有錯誤發生,用GetLastError可以取得具體的錯誤碼。也可以用GetOverlappedResult來檢測操作完成:GetOverlappedResult返回FALSE,GetLastError返回ERROR_IO_INCOMPLETE表示操作進行中。如果對bWait參數傳入TRUE,則效果就是重疊操作變成了非重疊的,直到操作完成,函數才返回。下面的代碼片段展示了一種檢測重疊讀取操作完成的方法。注意fWaitingOnRead標志的使用,它是檢測代碼的控制入口,只有在某操作進行中時,才應該調用檢測代碼。

#define READ_TIMEOUT      500      // milliseconds
DWORD dwRes;
if (fWaitingOnRead) {
   dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
   switch(dwRes)
   {
      // Read completed.
      case WAIT_OBJECT_0:
          if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
             // Error in communications; report it.
          else
             // Read completed successfully.
             HandleASuccessfulRead(lpBuf, dwRead);
          //  Reset flag so that another opertion can be issued.
          fWaitingOnRead = FALSE;
          break;
      case WAIT_TIMEOUT:
          // Operation isn't complete yet. fWaitingOnRead flag isn't
          // changed since I'll loop back around, and I don't want
          // to issue another read until the first one finishes.
          //
          // This is a good time to do some background work.
          break;                       
      default:
          // Error in the WaitForSingleObject; abort.
          // This indicates a problem with the OVERLAPPED structure's
          // event handle.
          break;
   }
}

 

 

2.2.2 寫入操作

寫入操作跟讀取操作非常相似。下面的代碼片段展示了如何提交寫入操作,並等待操作完成。

 

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL fRes;

   // Create this write operation's OVERLAPPED structure's hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // error creating overlapped event handle
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) { 
         // WriteFile failed, but isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // OVERLAPPED structure's event has been signaled. 
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else
                  // Write operation completed successfully.
                  fRes = TRUE;
                 break;
            
            default:
                 // An error has occurred in WaitForSingleObject.
                 // This usually indicates a problem with the
                // OVERLAPPED structure's event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

 

注意,上面的代碼使用WaitForSingleObject時,超時值是INFINITE,這使得函數無限等待直到操作完成。這可能讓調用線程似乎是被掛起了;而實際上只是寫入操作需要較長的時間,或者流控制阻塞了傳輸操作。下文將討論的狀態檢查可以檢測到這種情況,但它也不會讓WaitForSingleObject返回。有三種方法可以克服此問題:

  • 把代碼放在單獨的線程中。這樣寫入線程在等待寫操作完成時,其他線程可以進行任何所需的操作。MTTTY就是這么做的。
  • 使用COMMTIMEOUTS使得寫操作在經過一個超時值指定的時間后完成。本文后面的“通信超時”節將詳細討論它。MTTTY也可以使用這種方法。
  • 修改WaitForSingleObject調用,使用超時值。這樣會更麻煩:如果原來的操作仍在進行中,程序提交另一個操作請求,則需要分配新的OVERLAPPED結構和重疊事件。這種記錄跟蹤保持是很困難的,尤其是與“工作隊列”相比較時。MTTTY使用了工作隊列。

上面代碼中的WaitForSingleObject使用了INFINITE作為超時值,其效果等同於使用TRUE作為GetOverlappedResult的fWait參數。下面是等效的更簡潔的代碼:

 

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   BOOL fRes;

   // Create this writes OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) { 
         // WriteFile failed, but it isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else {
         // Write is pending.
         if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE))
            fRes = FALSE;
         else
            // Write operation completed successfully.
            fRes = TRUE;
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

 

GetOverlappedResult並不總是等待重疊操作完成的最好方法。比如說,如果應用需要同時等待另一個事件句柄,則第一個代碼片段模型比第二個更好,因為可以很容易地用WaitForMultipleObjects替換WaitForSingleObject,來等待更多的句柄。MTTTY就是這么做的。

在前一個重疊操作完成前重用OVERLAPPED結構是重疊I/O編程中常現的一個錯誤。如果要在前一個重疊操作完成前提交新的重疊操作請求,則需要分配新的OVERLAPPED結構,其hEvent字段也應該包含新的手動復位事件句柄。只有在重疊操作完成后,OVERLAPPED結構和其事件句柄才可以被重用。

串口通信中使用OVERLAPPED結構時只需要修改hEvent字段,其他字段只需要初始化為零。

3 串口狀態

有兩種獲取通信端口狀態的方法。第一種方法是設置事件掩碼,當指定事件發生時應用程序會收到通知。SetCommMask函數用於設置事件掩碼,WaitCommEvent用於等待指定的事件發生。它們與16位Windows中的SetCommEventMaskEnableCommNotification類似,只是它們不發送WM_COMMNOTIFY消息。第二種方法是不時地調用另一些狀態函數來獲取通信端口的狀態。當然,輪詢是低效的,不建議使用。

3.1 通信事件

通信事件在使用通信端口時可能隨時發生。接收通信事件需要兩個步驟:

  • SetCommMask設定需要接收通知的事件
  • WaitCommEvent提交狀態檢查請求,請求可以是重疊的或者非重疊的,與讀寫操作一樣。

下面是使用SetCommMask的示例:

DWORD dwStoredFlags;
dwStoredFlags = EV_BREAK | EV_CTS   | EV_DSR | EV_ERR | EV_RING |               
                EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(hComm, dwStoredFlags))   
   // error setting communications mask
 
         

 

 


下表描述了每種事件類型。

 

事件標志

描述

EV_BREAK

檢測到輸入中的break

EV_CTS

CTS(Clear To Send)信號狀態改變。要取得CTS線路狀態,應使用GetCommModemStatus函數。

EV_DSR

DSR(Data Set Ready)信號狀態改變。要取得DSR線路狀態,應使用GetCommModemStatus函數。

EV_ERR

某線路狀態錯誤發生。線路狀態錯誤包括CE_FRAME、CE_OVERRUN和CE_RXPARITY。要取得具體錯誤種類,需調用ClearCommError函數。

EV_RING

檢測到振鈴指示

EV_RLSD

RLSD(Receive Line Signal Detect)信號狀態改變。要取得RLSD線路狀態,需調用GetCommModemStatus函數。注意,RLSD通常被稱作CD(carrier detect)。

EV_RXCHAR

接收到一個字符並且已放入輸入緩沖區。請參考下面的“警告”節對此標志的詳細討論。

EV_RXFLAG

接收到一個事件字符並且已放入輸入緩沖區。事件字符由下文討論的DCB結構EvtChar字段指定。下面的“警告”節也討論了這個標志。

EV_TXEMPTY

輸出緩沖區中最后一個字符被發送到串口設備了。如果使用硬件緩沖區,此標志僅表示所有數據已經發送到硬件了。如果不與設備驅動交互,是無法確定硬件緩沖區空的。

 

指定事件掩碼后,使用WaitCommEvent函數檢測事件發生。如果以非重疊方式打開端口,則WaitCommEvent不需要OVERLAPPED結構體,函數阻塞調用線程直到某事件發生。如果沒有事件發生,調用線程將無限阻塞。

下面的代碼片段展示了如何在以非重疊方式打開的端口上等待EV_RING事件。

   DWORD dwCommEvent;
   if (!SetCommMask(hComm, EV_RING))      
       // Error setting communications mask      
       return FALSE;
   if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
       // An error occurred waiting for the event.
       return FALSE;
   else
       // Event has occurred.
       return TRUE;


 

 

 

 


如果沒有事件發生,上面的代碼將無限阻塞調用線程。解決方法是以重疊方式打開端口,用下面的方式等待狀態事件:

 

   #define STATUS_CHECK_TIMEOUT      500   // Milliseconds


   DWORD      dwRes;
   DWORD      dwCommEvent;
   DWORD      dwStoredFlags;
   BOOL      fWaitingOnStat = FALSE;
   OVERLAPPED osStatus = {0};
   dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\
                  EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
   if (!SetCommMask(comHandle, dwStoredFlags))
      // error setting communications mask; abort
      return 0;
   osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osStatus.hEvent == NULL)
      // error creating event; abort
      return 0;
   for ( ; ; ) {
      // Issue a status event check if one hasn't been issued already.
      if (!fWaitingOnStat) {
         if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
            if (GetLastError() == ERROR_IO_PENDING)
               bWaitingOnStatusHandle = TRUE;
            else
               // error in WaitCommEvent; abort
               break;
         }
         else
            // WaitCommEvent returned immediately.
            // Deal with status event as appropriate.
            ReportStatusEvent(dwCommEvent); 
      }
      // Check on overlapped operation.
      if (fWaitingOnStat) {
         // Wait a little while for an event to occur.
         dwRes = WaitForSingleObject(osStatus.hEvent, STATUS_CHECK_TIMEOUT);
         switch(dwRes)
         {
             // Event occurred.
             case WAIT_OBJECT_0: 
                 if (!GetOverlappedResult(hComm, &osStatus, &dwOvRes, FALSE))
                    // An error occurred in the overlapped operation;
                    // call GetLastError to find out what it was
                    // and abort if it is fatal.
                 else
                    // Status event is stored in the event flag
                    // specified in the original WaitCommEvent call.
                    // Deal with the status event as appropriate.
                    ReportStatusEvent(dwCommEvent);
                 // Set fWaitingOnStat flag to indicate that a new
                 // WaitCommEvent is to be issued.
                 fWaitingOnStat = FALSE;
                 break;
             case WAIT_TIMEOUT:
                 // Operation isn't complete yet. fWaitingOnStatusHandle flag 
                 // isn't changed since I'll loop back around and I don't want
                 // to issue another WaitCommEvent until the first one finishes.
                 //
                 // This is a good time to do some background work.
                DoBackgroundWork();
                 break;                       
             default:
                 // Error in the WaitForSingleObject; abort
                 // This indicates a problem with the OVERLAPPED structure's
                 // event handle.
                CloseHandle(osStatus.hEvent);
                return 0;
         }
      }
   }
   CloseHandle(osStatus.hEvent);


上面的代碼片段與重疊讀取操作的代碼非常相似。實際上,MTTTY使用WaitForMultipleObjects在同一個線程中等待讀取完成或者狀態改變事件發生。 SetCommMask和WaitCommEvent有兩種很有意思的邊際效應。第一,如果以非重疊方式打開通信端口,WaitCommEvent將阻塞直到某事件發生。如果其他線程調用SetCommMask設置新的事件掩碼,則線程將阻塞在SetCommMask調用上,原因是第一個線程的WaitCommEvent調用仍在執行中。SetCommMask將一直阻塞調用線程,直到第一個線程的WaitCommEvent調用返回。這種邊際效應對於以非重疊方式打開的端口是通用的。如果某線程阻塞在任何通信函數上,則第二個線程對任何通信函數的調用都將阻塞,直到第一個線程的函數調用返回。第二種邊際效應是關於以重疊方式打開的端口的。如果使用SetCommMask設置新的事件掩碼,則未決的WaitCommEvent調用將成功完成,導致調用完成的事件掩碼將是NULL

3.2 警告

使用EV_RXCHAR標志可以在每個字節到達端口時通知線程。與ReadFile配合使用,可以讓程序在數據到達接收緩沖區后立即被讀取;這與提交讀取操作請求,然后等待數據到達是不同的。這對於以非重疊方式打開的端口特別有用,因為程序在數據到達時被EV_RXCHAR事件通知,而不需要輪詢操作。這樣可以得到下列偽代碼:

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask.

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
         // A byte has been read; process it.
      else
         // An error occurred in the ReadFile call.
         break;
   }
   else
      // Error in WaitCommEvent.
      break;
}

 

 

 

 

 

 

 

 

 





上面的代碼等待EV_RXCHAR事件發生,然后讀取接收到的一個字節,隨后繼續循環,等待下一個EV_RXCHAR事件。當一個或者兩個字節快速連續到達時,這段代碼工作得很好。收到一個字節導致EV_RXCHAR發生,代碼讀取該字節。如果在下一次調用WaitCommEvent之前沒有其他字節到達,一切都好:下一個字節的到達將使WaitCommEvent收到EV_RXCHAR事件。如果在下一次調用WaitCommEvent之前,另一個字節到達,一切也都很好:第一個字節仍然正常讀取;第二個字節的到達導致內部設定EV_RXCHAR標志;當代碼再次調用WaitCommEvent時,仍然可以收到EV_RXCHAR事件,然后通過ReadFile讀取第二個字節。

當三個或者更多個字節連續快速到達時,上面的代碼就有問題了。第一個字節使得EV_RXCHAR事件發生;第二個字節使得內部設定EV_RXCHAR標志,下一次調用WaitCommEvent時,這個標志指示EV_RXCHAR事件發生。現在,第三個字節到達通信端口,系統試圖在內部設置EV_RXCHAR標志;但是第二個字節到達時EV_RXCHAR標志已經被設置,所以第三個字節的到達是不被注意的。代碼最終正常讀取第一個字節;此后調用WaitCommEvnet時,EV_RXCHAR事件(由第二個字節到達引發)使得第二個字節被讀取,第三個字節保持在接收緩沖區中。此時,系統和代碼就失去同步了。當第四個字節到達導致EV_RXCHAR發生時,代碼讀取的是第三個字節。這種情況將一直持續下去。

似乎只要增加讀取操作請求的字節數就可以解決這個問題:不是請求一個字節,而是請求兩個、十個或者其他數目的字節。此方法的問題是,超過請求的字節數兩個或者更多個字節快速連續到達時,代碼還是會失敗。就是說,如果請求兩個字節,那么,四個字節快速連續到達會導致問題;如果請求10個字節,那么12個字節快速連續到達會導致問題。

真正的解決方法是從端口讀取數據,直到沒有數據可讀取,下面的代碼展示了這一方法。另一種可能的方法是調用ClearCommError確定緩沖區中有多少個字節,然后一次讀取所有數據。這種方法要求更復雜的緩沖區管理,但是在大量數據一次到達時可以減少讀取操作次數。

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      do {
         if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
            // A byte has been read; process it.
         else
            // An error occurred in the ReadFile call.
            break;
      } while (dwRead);
   }
   else
      // Error in WaitCommEvent
      break;
}

如果沒有正確的通信超時值,上面的代碼也不能正常工作。后文的“通信超時”節將討論通信超時,它會影響到ReadFile的行為,使得ReadFile不等待字節到達就返回。上述關於EV_RXCHAR的警告也適用於EV_RXFLAG。如果標志字符連續快速到達,可能每個字符都不會觸發EV_RXFLAG。同樣,最好的解決方案是讀取數據直到沒有可讀數據了。上述警告同樣也適用於與字符接收無關的其他事件。如果其他事件連續快速發生,則某些通知可能丟失。比如說,如果CTS信號線電平開始是高,然后變為低,變為高,再變為低,則會發生EV_CTS事件;如果CTS線電平改變發生得太快,則無法保證WaitCommEvent最終可以檢測到多少個EV_CTS事件。因此,不能用WaitCommEvent來保持信號線的狀態。線路狀態將在本文隨后的“Modem狀態”節講述。

4 錯誤處理和通信狀態

調用SetCommMask時可以指定EV_ERR這個事件標。EV_ERR事件表示通信端口存在錯誤條件,然而端口發生的某些錯誤不會導致EV_ERR的發生。通信端口相關的錯誤將導致所有I/O操作被掛起,直到移除了錯誤條件為止。ClearCommError用於檢測錯誤和清除錯誤條件。ClearCommError也可以提供通信狀態,以指示傳輸為何終止;它還可以指示收發緩沖區中各有多少個字節。傳輸終止的原因可能是存在錯誤,或者因為流控制。本文隨后將討論流控制。下列代碼展示了ClearCommError的使用:

 
         
    COMSTAT comStat;
    DWORD   dwErrors;
    BOOL    fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
    BOOL    fBREAK, fDNS, fFRAME, fIOE, fMODE;

    // Get and clear current errors on the port.
    if (!ClearCommError(hComm, &dwErrors, &comStat))
        // Report error in ClearCommError.
        return;

    // Get error flags.
    fDNS = dwErrors & CE_DNS;
    fIOE = dwErrors & CE_IOE;
    fOOP = dwErrors & CE_OOP;
    fPTO = dwErrors & CE_PTO;
    fMODE = dwErrors & CE_MODE;
    fBREAK = dwErrors & CE_BREAK;
    fFRAME = dwErrors & CE_FRAME;
    fRXOVER = dwErrors & CE_RXOVER;
    fTXFULL = dwErrors & CE_TXFULL;
    fOVERRUN = dwErrors & CE_OVERRUN;
    fRXPARITY = dwErrors & CE_RXPARITY;

    // COMSTAT structure contains information regarding
    // communications status.
    if (comStat.fCtsHold)
        // Tx waiting for CTS signal

    if (comStat.fDsrHold)
        // Tx waiting for DSR signal

    if (comStat.fRlsdHold)
        // Tx waiting for RLSD signal

    if (comStat.fXoffHold)
        // Tx waiting, XOFF char rec'd

    if (comStat.fXoffSent)
        // Tx waiting, XOFF char sent
    
    if (comStat.fEof)
        // EOF character received
    
    if (comStat.fTxim)
        // Character waiting for Tx; char queued with TransmitCommChar

    if (comStat.cbInQue)
        // comStat.cbInQue bytes have been received, but not read

    if (comStat.cbOutQue)
        // comStat.cbOutQue bytes are awaiting transfer

4.1 Modem狀態(線路狀態)

SetCommMask的調用可能包含EV_CTSEV_DSREV_RINGEV_RLSD等標志,這些標志指示串口信號線電平的改變,但僅僅指示發生了改變,不能指示信號線的實際狀態。GetCommModemStatus函數可以獲取這些狀態線的實際狀態,它返回一個比特掩碼用以表示每個信號線的狀態。下面的代碼展示了GetCommModemStatus的使用:

 
         
   DWORD dwModemStatus;
   BOOL  fCTS, fDSR, fRING, fRLSD;

   if (!GetCommModemStatus(hComm, &dwModemStatus))
      // Error in GetCommModemStatus;
      return;

   fCTS = MS_CTS_ON & dwModemStatus;
   fDSR = MS_DSR_ON & dwModemStatus;
   fRING = MS_RING_ON & dwModemStatus;
   fRLSD = MS_RLSD_ON & dwModemStatus;

   // Do something with the flags.

4.2 擴展函數

某些時候可能要用應用程序來代替串口通信驅動程序對控制線進行控制,比如說,當應用要實現自己的流控制時。此時應用必須負責RTS和DTR信號線的狀態改變。EscapeCommFunction可以讓通信驅動程序進行這些擴展操作。它還可以讓驅動程序執行一些其他功能,如設置和清除BREAK條件。關於此函數的更多信息,請參考平台SDK文檔,Win32 SDK知識庫和MSDN。

5 串口設置

5.1 DCB設置

設備控制塊(Device Control Block,DCB)的設置是串口編程中最重要的部分,很多通常的錯誤都跟沒有正確設置DCB結構有關。GetCommState()函數可以獲取當前正在使用的DCB結構;BuildCommDCB()函數可以填充DCB結構的波特率、校驗類型、停止位數、數據位數字段;SetCommState()用於設置新的DCB結構。DCB設置的一般方法如下所示:

 
         

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   if (!GetCommState(hComm, &dcb))     // get current DCB
      // Error in GetCommState
      return FALSE;

   // Update DCB rate.
   dcb.BaudRate = CBR_9600 ;

   // Set new state.
   if (!SetCommState(hComm, &dcb))
      // Error in SetCommState. Possibly a problem with the communications
      // port handle or a problem with the DCB structure itself.

 

6 流控制

流控制可以在通信的某一方忙或者由於其他原因不能進行通信時暫停通信。通常有兩種流控制:硬件流控制和軟件流控制。串行通信中一個通常的問題是寫操作實際上並沒有把數據寫入到設備中。通常,這是流控制的效果。此時,DCB結構的下列字段可能是TRUE:fOutxCtsFlowfOutxDsrFlow或者fOutX。另一個確定流控制啟用的方法是調用ClearCommError()並檢查COMSTAT結構體,它可以反映傳輸因為流控制而暫停。

在詳細討論流控制前,最好了解下相關術語。串行通信發生在兩個設備間,通常是PC和調制解調器或者打印機。PC稱作數據終端設備(Data Terminal Equipment,DTE),有時也稱為主機(host);調制解調器,打印機,或者其他外設稱作數據通信設備(Data Communications Equipment,DCE),有時也稱為設備(device)。

6.1 硬件流控制

硬件流控制使用串行線路中控制線的電平來控制收發。DTE和DCE必須就通信會話中使用的流控制類型進行協商。設置DCB結構體以啟用流控制只是配置了DTE。此外還需要配置DCE以保證DTE和DCE使用相同類型的流控制,然而Win32沒有提供設置DCE流控制的機制。通常要使用設備上的DIP開關,或者向設備發生命令來進行流控制配置。下表描述了控制線、流控制方向和線路對DTE與DCE的影響。

 

Line and Direction Effect on DTE/DCE
CTS
(Clear To Send)
Output flow control
DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxCtsFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxCtsFlow member of the DCB is FALSE, then the state of the line does not affect transmission.

DSR
(Data Set Ready)
Output flow control
DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxDsrFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxDsrFlow member of the DCB is FALSE, then the state of the line does not affect transmission.

DSR
(Data Set Ready)
Input flow control
If the DSR line is low, then data that arrives at the port is ignored. If the DSR line is high, data that arrives at the port is received.

This behavior occurs if the fDsrSensitivity member of the DCB is set to TRUE. If it is FALSE, then the state of the line does not affect reception.

RTS
(Ready To Send)
Input flow control
The RTS line is controlled by the DTE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the RTS line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the RTS line low.

If the fRtsControl member of the DCB is set to RTS_CONTROL_TOGGLE, the driver sets the RTS line high when data is available for sending. The driver sets the line low when no data is available for sending. Windows 95 ignores this value and treats it the same as RTS_CONTROL_ENABLE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_ENABLE or RTS_CONTROL_DISABLE, the application is free to change the state of the line as it needs. Note that in this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

DTR
(Data Terminal Ready)
Input flow control
The DTR line is controlled by the DTE.

If the fDtrControl member of the DCB is set to DTR_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the DTR line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the DTR line low.

If the fDtrControl member of the DCB is set to DTR_CONTROL_ENABLE or DTR_CONTROL_DISABLE, the application is free to change the state of the line as it needs. In this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

可以簡單地認為CE_RXOVER錯誤發生時,就需要流控制了。CE_RXOVER錯誤表示接收緩沖區溢出,數據丟失。如果數據到達端口的速度比它被讀取的數據快,則可能發生CE_RXOVER錯誤。增加輸入緩沖區大小可能減小錯誤發生的頻率,但不能完全解決問題。這時需要輸入流控制。驅動程序檢測到輸入緩沖區快滿的時候,會拉低輸入流控制線電平,使得DCE停止傳輸,讓DTE有足夠的時間從輸入緩沖區讀取數據。當輸入緩沖區有足夠的空閑區域時,流控制線電平被設置為高,DCE繼發送數據。

CE_OVERRUN是一種類似的錯誤,它表示數據在通信硬件和驅動程序完全接收原數據前,新的數據到達。當傳輸速度對於通信硬件或者CPU而言太快時可能發生CE_OVERRUN錯誤;操作系統沒有時間為通信硬件服務時也可能發生這個錯誤。解決此問題的方法是降低傳輸速度,替換硬件,或者提升CPU速度。有時候第三方硬件驅動程序不能有效使用CPU資源也可能導致此錯誤。流控制可以減低CE_OVERRUN發生的頻度,但不能完全解決問題。

6.2 軟件流控制

軟件流控制使用通信流中的數據來控制收發操作。因為軟件流控制使用XOFF和XON這兩個特殊字符,所以不能應用於二進制傳輸。軟件流控制對基於文本的通信,或者不使用XOFF和XON的傳輸有效。要啟用軟件流控制,需設置DCB結構的fOutX和fInX字段為TRUE:fOutX控制輸出流控制;fInX控制輸入流控制。程序可以動態指定流控制字符,DCB結構的XoffChar字段指示輸入和輸出流控制使用的XOFF字符,XonChar則指定XON字符。對於輸入流控制,XoffLim字段指示發送XOFF字符前輸入緩沖區允許的最小可用空間大小;如果輸入緩沖區可用空間大小小於這個值,則會發送XOFF字符。XonLim字段指示在發送XON字符前輸入緩沖區中最小的數據字節數;如果輸入緩沖區中的數據量小於此值,則會發送XON字符。下面描述了使用XOFF/XON流控制時DTE的行為

Table 4. Software flow-control behavior

Flow-control character Behavior
XOFF received by DTE DTE transmission is suspended until XON is received. DTE reception continues. The fOutX member of the DCB controls this behavior.
XON received by DTE If DTE transmission is suspended because of a previous XOFF character being received, DTE transmission is resumed. The fOutX member of the DCB controls this behavior.
XOFF sent from DTE XOFF is automatically sent by the DTE when the receive buffer approaches full. The actual limit is dictated by the XoffLim member of the DCB. The fInXmember of the DCB controls this behavior. DTE transmission is controlled by the fTXContinueOnXoff member of the DCB as described below.
XON sent from the DTE XON is automatically sent by the DTE when the receive buffer approaches empty. The actual limit is dictated by the XonLim member of the DCB. The fInXmember of the DCB controls this behavior.

 

如果輸入控制啟用了軟件流控制,則DCB的fTXContinueOnXoff字段有效,它控制是否在系統自動發送XOFF字符后暫停傳輸。如果fTXContinueOnXoff為TRUE,則在接收緩沖區滿,發送了XOFF字符后繼續傳輸;否則暫停傳輸直到系統自動發送XON字符。使用軟件流控制的DCE設備會在接收到XOFF字符后暫停發送。某些設備會在DTE發送XON字符后恢復發送,然而,有些DCE設備會在接收到任何字符后恢復發送。如果DTE在自動發送XOFF后繼續傳輸,DCE會繼續發送,使得XOFF失效。Win32 API沒有提供讓DTE與這些設備行為相同的機制。DCB結構沒有提供字段以指示在接收到任何字符后恢復被暫停的傳輸。只有XON字符可以恢復傳輸。接收到XON和XOFF字符會讓未決的讀取操作返回零字節而完成,但應用程序不會讀取到XON和XOFF字符,因為它們不在輸入緩沖區中。很多程序,包括Windows中的超級終端,都可以讓用戶選擇流控制類型:硬件流控制,軟件流控制,或者不使用流控制。實際上,可以自由設置DCB結構中影響流控制的各個字段,來進行各種流控制配置,需要遵循的限制只是便於最終用戶使用,當然也要考慮設備是否支持所有類型的流控制。

 本文摘錄積小流,成江海的博客。


免責聲明!

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



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