串口編程入門

來源:互聯網
上載者:User

最近本人在做有關串口通訊的編程,可謂邊學邊用,在網上看到一篇好文,將它翻譯過來,大家一起學習,其中有翻譯錯誤的地方,還請多多指教!

這是一篇在Windows (NT 系列) 系統上進行串口通訊的入門資料。在這篇資料中提供了一個CSerialCommHelper的類,該類可以被直接放在你的應用程式中,用於串口通訊。需要提出的是,該類使用了重疊IO,而本文並不需要你過多的知道串口通訊和重疊IO,但是你需要知道一些關於象事件之類的同步對象和一些象WaitForSingleObject和WaitForMultipleObject的Windows API函數。另外,一些windows基礎的線程知識,如線程建立和線程結束,也是很必要的。

要使你的電腦具有串口通訊的功能,你的電腦必須有串口。大多數電腦上都至少有一個串口,它們通常被稱作COM1口和COM2口。有了串口,就有裝置驅動程式。只要你仔細思考一下,串口通訊所做的就是發送和接受資料,換句話說,你所作的一切就是在對串口進行IO的輸入/輸出,同時也在對磁碟檔案進行IO操作。所以,當我們看見那些用於讀寫檔案的API函數也被用於串口編程的時候,也就沒什麼人驚訝的了。當你向串口發送資料時,資料以位元組為單位發送;當它離開串口時,資料以位為單位發送。同樣,當資料到達串口時,資料以位為單位發送,當從串口擷取資料後,資料的格式又是位元組。

開啟串口

串口通訊的第一步是開啟指定的串口。你可以讓你的裝置串連上串口COM1,你可以通過以下API函數來開啟串口:

HANDLE m_hCommPort = ::CreateFile(szPortName,
                                                          GENERIC_READ|GENERIC_WRITE,
                                                          0,
                                                          0,
                                                          OPEN_EXISTING,
                                                          FILE_FLAG_OVERLAPPED,
                                                          0);
在上面的例子代碼中,第三個、第五個和第七個參數必須這麼設定。我們要以重疊的方式開啟檔案(串口),因此,第六個參數必須為FILE_FLAG_OVERLAPPED。掃後我們將詳細討論重疊IO。正如你從名字所猜測的那樣,CreateFile函數可被用於建立磁碟檔案,也可以用於開啟存在的檔案。

對於Windows系統來說,串口和磁碟檔案都是IO裝置。因此,要開啟一個已經存在的檔案(串口),我們所需要做的就是知道這個裝置的名稱(COM1),然後傳入建立標籤OPEN_EXISTING。

如果成功開啟了一個COM串口,象成功開啟了一個檔案一樣,API函數將返回指向該串口的控制代碼。當串口開啟失敗時,API函數也會傳回值INVALID_HANDLE_VALUE。你可以調用GetLastError來擷取失敗的原因。最常見的一個失敗原因是該串口已經被其它應用程式開啟,其錯誤碼為ERROR_ACCESS_DENIED(5)。同樣,如果你錯誤地開啟了一個不存在的串口,通過GetLastError,你將擷取錯誤碼ERROR_FILE_NOT_FOUND。

 注意:不要在調用GetLastError之前調用其它函數(ASSERT也不行),否則它的傳回值為0。

開啟一個串口之後,你現在所要做的就是開始使用。

 串口的讀/寫

現在你已經開啟了一個串口,你一定想向串口串連的裝置發送資料。例如,你想向串連的裝置(另外一台電腦)發送“Hello”資訊。當你向串口發送資訊時,你所做的操作就和向檔案中寫入資料一樣。使用以下API函數:

BOOL iRet = WirteFile(m_hCommPort, data, dwSize, &dwBytesWritten, &ov);

為了能夠回應“Hello”資訊,接受方發送“Hi”。因此,你需要讀取串口資訊,使用以下API函數:

BOOL iRet = ReadFile(m_hCommPort, szTmp, sizeof(szTmp), &dwBytesRead, &ovRead);

其他你需要瞭解的東西我將在以後的內容中討論。現在所瞭解的一切看起來都很簡單,對吧!

 關於串口通訊的若干問題

上面我們提到,為了回應你發送的“Hello”資訊,裝置會發送回“Hi”資訊,然後你將讀取這個資訊。但是現在的問題是你不知道裝置會在什麼時候做出回應,或者裝置更本就不會回應,你應該在何時開始對串口進行讀操作。其中一個方案是一旦你調用了WriteFile,你立即調用ReadFile。如果沒有資料可讀取,那麼你得接著進行讀操作。這會導致所謂的迴圈檢查--迴圈檢查串口,看是否有資料傳入。這種模式看上去的確不是一種好模式。如果系統能夠在資料接入時以某種方式通知你,並且你只在那時調用ReadFile,豈不是更好。這種模式以事件驅動,正符合Windows編程的一般方法論。幸好,這種模式是可能實現的。

串口通訊的另外一個問題是,既然它們是兩個裝置進行通訊,那麼這兩個裝置就必須在如何通訊上達成協議。每一邊都必須遵守一定的協議來工作。既然實現通訊的正是串口,我們要配置的就是串口。以下的API函數就是要達到這一目的:

BOOL SetCommState( HANDLE hFile, LPDCB lpDCB);

第一個參數是COM口的控制代碼,第二個參數是就是所謂的裝置控制塊。DCB是一個結構,這個結構在winbase.h中被定義,它包含有28個成員變數。例如,我們需要COM口工作的傳輸速率,我們就需要設定它的成員變數BaudRate。傳輸速率一般在9600bps。但是兩個裝置必須在同一傳輸速率下工作。同樣,如果你想進行同位,你需要設定它的成員變數Parity。並且,兩個裝置必須使用同一個同位。它的部分成員變數是保留的且必須被設定為0。以下的代碼是擷取當前的dcb,並對部分欄位進行設定:

DCB dcb = {0};
dcb.DCBlength = sizeof(dcb);
if (!GetCommState(m_hCommPort, &dcb))
{
    TRACE("Failed to Get Comm State Reason: %d", GetLastError());
     return;
}

dcb.BaudRate = dwBaudRate;
dcb.ByteSize  = byByteSize;
dcb.Parity = byParity;

if(!SetCommState(m_hCommPort, &dcb))
{
    TRACE("Failed to Set Comm State Reason:%d", GetLastError());
    ASSERT(0);
}

在大多數情況下,你不需要設定DCB結構中的其它欄位。但是如果你要對其進行設定,那你得留心,因為修改這些欄位值會影響串口的工作。所以當你修改其值時,你得確信你所做的修改。

事件驅動

回到我們開始時遇到的讀取資料的問題。如果我們不想通過迴圈檢查COM口來擷取資料,那麼我們就得擁有某種事件機制。值得慶幸的是,有一個途徑允許你要求系統在特定的事件發生時通知你。調用的API函數:

BOOL SetCommMask(HANDLE hHandle, DWORD dwEvtMask)

第一個參數是COM口的控制代碼,第二個參數是用於設定你所關心的事件掩體。需要在列表中設定的事件取決於應用程式的需要。簡而言之,比如,我們關心一個字元什麼時候到達串口,那麼我們就將掩體設定為EV_RXCHAR作為事件掩體。同樣,如果我們想知道什麼時候所有的資料發送完畢,我們要將事件掩體設定為EV_TXEMPTY。那麼,得出調用方式為:

SetCommMask(m_hCommPort, EV_RXCHAR | EV_TXEMPTY);

在這裡,有趣的事情是,雖然我們告訴系統我們關心的事件,但是我們並沒有告訴系統當這些事件發生後,系統該做什麼。就像系統是如何讓我們知道特定的事件發生了一樣,很明顯,我們需要的是回調機制。但是這個機制沒有現成的。此時,事情變得有點狡猾。為了讓系統通知我們通訊事件什麼時候發生,我們得調用WaitCommEvent。這個函數將等待SetCommMask中設定的事件。但是如果你再仔細思考一下,現在看起來我們又從事件通知機制回到開始的迴圈檢查機制。WaitCommEvent將阻塞線程知道有事件產生。那麼我們為什麼要使用他呢?答案就在於重疊IO。以下是WaitCommEvent的函數原型:

BOOL WaitCommEvent(HANDLE hCommPort, LPDWORD dwEvtMask, LPOVERLAPPED lpOverlapped);

第三個參數就是關鍵。

你可以把重疊IO看成是非同步IO。不論你什麼時候執行函數調用,設定重疊IO的結構,那都是在對當前的IO進行操作,但是如果你不能立即完成這個操作,那麼告訴我你什麼時候能夠完成它。讓系統告訴你完成操作完成的方法是設定Kenerl的事件對象,而這個對象是lpOverlapped結構的一部分。那麼你說要做的就是建立一個線程,通過WaitForSingleObject API函數,讓這個線程等待事件對象。下面是overlapped結構:

typedef struct _OVERLAPPED{
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;

其中,最後一個參數就是需要你設定的事件處理控制代碼。

你可以將重疊IO設想成非同步IO。無論什麼時候調用函數設定重疊結構,那都意味著執行當前的叫用作業,但是如果你不能立即結束調用,那請告訴我你什麼時候完成對IO的叫用作業。讓系統告訴你IO操作完成的方發是設定Kenerl的事件對象,事件對象是lpOverlapped結構的一部分。所以你所要做的就是建立一個線程,通過API函數WaitForSingleObject,讓這個線程等待事件對象的觸發。以下是重疊結構:

typedef struct _OVERLAPPED{
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    HANDLE hEvent;
}  OVERLAPPED, *LPOVERLAPPED;

最後一個參數就是你需要設定的事件控制代碼。這個事件通常是手動設定的。當你調用WaitCommEvent時,傳入一個重疊結構作為最後一個參數。而此時系統並不會完成調用,這就意味著當前沒有資料到達串口,因此WaitCommEvent會立即返回,只是傳回值為FALSE。如果你調用GetLastError,你會獲得錯誤碼ERROR_IO_PENDING,它的意思是調用被接受,但是還沒有資料到達串口。同時,這也意味著無論何時有資料到達串口,系統都會設定傳入重疊結構中的hEvent。

在當前的案例中,既然我們關心的是不止一個事件,我們需要檢查我們通過調用GetCommMask獲得了什麼事件,並且還要檢查每個事件的DWORD型別參數。我們通過以下虛擬碼來進行進一步分析(你可以從COM口讀取資料,重設事件,再次調用WaitCommEvent等等)

unsigned __stdcall CSerialCommHelper::ThreadFn(void *pvParam)
{
    OVERLAPPED ov;
    menset(&ov, sizeof(ov));
    ov.hEvent = CreateEvent(0, true, 0, 0);
    HANDLE arHandles[2];
    arHandles[0] = apThis ->m_hThreadTerm;//擷取當前的線程控制代碼

    DWORD dwWait;
    SetEvent(apThis ->m_hThreadStarted);

     while (abContinue)
    {
        BOOL abRet = WaitCommEvent(apThis -> m_hCommPort, &dwEventMask, &ov);
        if (!abRet) ASSERT(GetLastError() == ERROR_IO_PENDING);
        
        arHandles[1] = ov.hEvent;

        dwWait = WaitForMultipleObjects(2,arHandles, FALSE, INFINITE);

        switch (dwWait)
        {
        case WAIT_OBJECT_0;
            {
                _endthreadex(1);
            }
            break;
        case WAIT_OBJECT_0 + 1;
            {
                DWORD dwMask;
                if (GetCommMask(apThis -> m_hCommPort, &dwMask))
                {
                        if  (dwMask & EV_TXEMPTY) TRACE("DATA Sent");
                        ResetEvent(ov.hEvent);
                        continue;
                }
                else
                {
                    //讀取資料,並重設事件
                }
            }
        }//switch
    }//while

return 0;   
}

上面的虛擬碼是用重疊IO工作的簡單例子。

一旦我們接受到了有資料抵達的訊息後我們就得讀取資料。在這裡值得注意的是,當資料抵達串口時,它被拷貝到系統緩衝。只有當你用系統API函數,如ReadFile讀取它之後,該系統緩衝才會被清空。象其它緩衝一樣,系統緩衝的空間也是有限的。所以,如果你不儘快從系統緩衝中將資料讀出,只要有其它資料達到,系統緩衝很快就會被塞滿。 那麼對於接下來到達的資料會發生什麼了,這取決於SetCommState的參數配置。通常情況下,應用程式在應用程式層要做一些握手操作,但是你也可以通過配置,讓串口在緩衝塞滿事件發生後,不再接受任何資料。但是這已經超出了本文的討論範疇。在可能的情況下,讓應用程式在應用程式層進行握手檢查,即在收到第一資料區塊的Ok回應之前,不再發送下一資料區塊。這種握手檢查的執行,一般通過ACK/NAK 和ENQ協議完成。

為了讓我們能夠讀取資料,我們要使用API函數ReadFile,設定讀取資料的長度。比如,我們正在監視資料的到達,並且有長度為10的字串到達了串口。只要第一個字元到達了串口,系統就會設定重疊結構的事件對象(ov.hEvent),並跳出WaitForSingleObject,並且返回。那麼我們應該讀取多少資料,1位元組還是10位元組?其工作原理如下:

 當一個或多個字元到達串口時,重疊結構的事件對象只被設定一次。例如,現在你進行了一次讀操作,並且讀取了一個字元。當你讀取完成後,你最後要重設重疊結構的事件對象。現在,你可能會想起WaitCommEvent,但它返回的是false,因為沒有新的字元到達。因此,你不可能再讀取更多的字串。現在又有其它字元到達,系統會設定重疊結構的事件對象,然後你讀取一個或多個字串,但是現在所讀取的資料實際上是上次達到的資料。很明顯,這裡還存在問題。

那麼如何解決這個問題。最容易的解決方案是當你獲得了一個字元到達的訊息,你就要讀取串口的所有字串。接下來還有問題,讀取多少呢?答案是通過迴圈讀取所有的字元,以下是通過ReadFile讀取字元的虛擬碼:

WaitCommEvent(m_hCommPort, &dwEventMask, &ov);
if (WaitForSingleObject(ov.hEvent, INFINITE) == WAIT_OBJECT_0)
{
    char szBuf[100];
    memset(szBuf, sizeof(szBuf));
    do
    {
        ReadFile(hPort, szBuf, sizeof(szBuf), &dwBytesRead, &ov);        
    } while (dwBytesRead >0);
}

下面介紹一下ReadFile:

BOOL ReadFile( HANDLE hFile, //檔案控制代碼
                                LPVOID lpBuffer, //資料緩衝
                                DWORD nNumberOfBytesToRead,//讀取位元組的長度
                                LPDWORD lpNumberOfBytesRead, //讀取的位元組數
                                LPOVERLAPPED lpOverlapped)//重疊結構緩衝

第一個參數是串口控制代碼,最後一個參數是重疊結構。當然,我們要建立一個手動重設的事件,並把重疊結構作為參數傳給ReadFile函數。

正如你看的的那樣,ReadFile的參數nNumberOfBytesRead返回讀取的位元組數。如果沒有資料留下,那麼nNumberOfBytesRead返回0。比如說,到達了11位元組,而你在第一次迴圈中讀取了10位元組。在第一次迴圈中,nNumberOfBytesRead返回10,在第二次迴圈中,nNumberOfBytesRead返回1,在第三次迴圈中,返回0,然後你會讓程式挑出while迴圈。 你可以通過這種方式讀取所有資料。在這種方法中,你會發現我們並沒有用到重疊結構的優勢,但是我們仍然將它作為參數傳給ReadFile,那是因為我們的COM口是通過重疊方式開啟的。

最後,如果你要向其它裝置發送資訊,你只需要調用WriteFile。WriteFile就不必在這裡討論了。

在我們繼續討論的時候,還有一件事情值得讓我們注意,那就是通訊逾時。讓各個部分運轉起來,設定逾時很重要。設定逾時的API:

SetCommTimeouts( HANDLE hCommPort, LPCOMMTIMEOUTS lpCommTimeOuts);

typedef struct _COMMTIMEOUTS{
    DWORD ReadIntervalTimeout;
    DWORD ReadTotalTimeoutMultiplier;
    DWORD ReadTotalTimeoutConstant;
    DWORD WriteTotalTimeoutMultiplier;
    DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS, * LPCOMMTIMEOUTS;

COMMTIMEOUTS是一個結構,在MSDN中可以查詢到它的更多資訊。“...ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier的值為0,意味著讀操作立即結束,返回讀取的資料,儘管沒有接受到資料...”

這個結構的設定正是我們所需要的。因為我們不希望在調用ReadFile時,在沒有資料的情況下,程式被WaitCommEvent卡住。

然後,是下面的代碼:

COMMTIMEOUTS timeouts;
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;

if(!SetCommTimeouts(m_hCommPort, &timeouts))
{
    TRACE("Error Setting time-outs %d", GetLastError());
    ASSERT(0);
    return;
}

現在我們已經討論了本文所需要的所有內容。

 我的QQ64528619,歡迎大家網上交流!不久我也會將My Code拿出來討論,到時候還請大家批評指教。 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.