1. 程式人生 > 實用技巧 >基於狀態機的串列埠通訊

基於狀態機的串列埠通訊

通訊協議

  序列通訊介面(如RS232、RS485等)作為計算機與微控制器互動資料的主要介面,廣泛用於各類儀器儀表、工業監測及自動控制領域中。

  通訊協議是需要通訊的雙方所達成的一種約定,它對包括資料格式、同步方式、傳送速度、傳送步驟、檢糾錯方式以及控制字元定義等問題作出統一規定,在雙方的通訊中必須共同遵守。在實際應用系統中,如果缺少一個嚴格、合理、規範的串列埠通訊協議,將無法保證資料傳輸的正確性及通訊的可靠性。

  因此,需要提出一種基於狀態機串列埠通訊協議的設計方法:通過合理地設定資料包格式來保證了資料傳輸的正確性:引入了狀態機方法,簡化了協議的實現難度,提高了通訊的可靠性,同時使通訊過程具有較高的容錯能力。


定義資料包格式

  串列埠通訊中最小的的資訊單元是資料幀。一個數據幀通常包括起始位、資料位、結束位,另外還可以包含用於檢測傳輸錯誤的“奇偶校驗位”,每個資料幀中傳輸的資料位可以有5、6、7、8或9個。
  實際通訊過程中,資料的傳送是一幀一幀地進行,當被傳輸的資料超過一幀時(例如浮點型資料),如果沒有對資料幀進行必要的打包,傳送出去的資料將會很難被資料接收方解釋與分析,進而造成資料傳輸混亂與錯誤。因此,在一般應用中有必要將資料幀組裝成資料包再發送。

  1. 起始標誌表示開始接收一個新的資料包。
  2. 資料長度命令和附加資料共佔的位元組數。設定此欄位,可方便接收方識別資料包的長度並能夠準確地接收資料包。
  3. 命令用來說明資料包的用途。
  4. 附加資料 當命令不同時,含義不同。
  5. 校驗是對命令欄位與附加資料欄位的所有位元組資料的異或校驗。
  6. 結束標誌表示該資料包結束。

  另外,在多機通訊中,資料包中還應增加源地址與裝置地址等欄位。


通訊狀態機

狀態機簡介

  狀態機由事物所處的狀態及引發狀態變化的外部事件兩部分組成。

  在軟體程式設計中,事物所處的狀態可以描述為某個程式片斷或函式,而引發狀態變化的處部條件可以理解為條件判斷語句,當條件為真時,事物的狀態發生變化。事物發生變化前的狀態稱為現態,變化後的狀態稱為次態,程式中可以通過不同的數字對不同的狀態進行編號。現態到次態的變化可以通過狀態變數值的改變來描述。


  在協議中需要傳輸的基本資訊單元是資料包,資料包一般包含多個數據幀。實際傳輸過程中,資料的傳輸通常是一幀一幀地進行,資料包是被拆分成若干幀資料後再進行傳輸,資料接受方也是分幀接受一個數據包。

  資料接受方在解釋與分析資料包時可能存在兩個問題:

1.識別並接收完整的資料包

  • 對於資料接收方,一個數據包是分若干批到來,在識別包頭與包尾時,也就是幀同步問題;
  • 具體程式設計時存在難度,特別對於已接收部分與未接收部分以及資料接收的進度及狀態的處理。

2.資料傳輸時的容錯能力

  • 資料傳輸過程中已經出現錯誤時,系統應該具有擺脫錯誤狀態,恢復到正常狀態的能力。
  • 例如,當一個數據包只傳輸完一部分時,因為未知故障,下一個資料包就開始傳輸,系統應該能識別出傳輸錯誤,拋棄前一個出錯的資料包,並且能正確接收下一個資料包。
  • 實際程式設計時處理這種問題難度較大,結果很可能會出現將第一個資料包的前一部分與第二個資料包的前一部分拼裝成一個新的資料包的情況,這就損失了兩個資料包,最嚴重的結果可能是系統無法從錯誤中恢復,這就嚴重降低了系統的安全性與可靠性。

  為解決上面的兩個問題,在協議中引入了狀態機。

  在狀態機中,狀態的變化依賴於外部觸發條件,當條件滿足時,狀態將發生變化。

  在協議中將資料包接收的各個階段定義為不同的狀態,將接收一幀新的資料或資料處理的結果作為外部觸發條件,從而達到狀態改變的目的,最終完成一個數據包的接收與校驗。


串列埠通訊狀態圖

  串列埠通訊協議中,傳送資料包時一般不需引入狀態機,這主要是為提高發送速率和簡化程式設計模型而考慮。

  在協議中主要針對資料接收過程建立狀態機。

  


串列埠通訊資料接收過程

  1. 當未開始接收資料包或發現數據傳輸出錯時,系統進入空閒狀態;
  2. 當數接收到資料包起始標誌時,變為收到起始標誌狀態,如果收到的資料不為起始標誌,系統繼續保持空閒狀態;
  3. 進入收到起始標誌狀態後,新接收到的任何資料將被當作資料包中命令與附加資料的總位元組數(記為LEN),系統進入收到資料長度狀態;
  4. 繼續接收新的資料,直至接到新收到的資料總位元組數達到LEN +2,進入檢驗結束標誌狀態;
  5. 這時可以檢驗結束標誌是否為協議定義的標誌值,如果是,說明傳輸正確,否則傳輸出錯,出錯後應查詢接收緩衝區中本資料包的起始標誌後有無其它起始標誌,如果沒有發現起始標誌,系統應進入空閒狀態,否則應直接進入接收到起始標誌狀態,這樣可提高系統容錯能力,方便系統從錯誤中恢復。
  6. 檢驗結柬標誌正確後,進入資料校驗狀態;
  7. 校驗結果如果正確,資料包接收完成,否則說明傳輸出錯,系統進入空閒狀態。


上位機軟體程式設計邏輯

  上位機軟體中,當接收到資料時,串列埠控制元件會觸發一個事件,在事件處理程式碼中應及時將收到的資料存入接收衝區,同時不應該把串列埠通訊協議接收部分的程式碼放置在此事件中,否則後面到來的資料可能因為前面先到的資料沒有及時處理完畢而被沖掉,導致資料丟失。

  1. 在上位機軟體執行時,應該啟動一個Windows執行緒,用於不斷檢測接收緩衝區是否為空,不為空時則對緩衝中的資料進行處理;
  2. 執行緒類建立好後,應具體編寫執行緒類執行函式的處理過程,在其中通過狀態指示變數sp實現狀態機機制;
  3. 資料包的接收進度依據於狀態指示變數sp。

  當資料接收順利時,sp的變化將會引導完成一個數據包的接收過程。這樣處理可以簡化程式設計的模型,使協議易於實現;資料包接收過程中,一旦發現數據傳輸出錯,立即將sp置為0(空閒狀態),也就是狀態復位,使系統進入準備接收下一個資料包的狀態,這樣可提高通訊過程的可靠性及容錯能力。


狀態機機制實現

{------------------------------
@功能:狀態機機制實現串列埠通訊
@author:成鵬致遠
@net:lcw.cnblogs.com
-------------------------------}
procedure TBufferThread.Execute;
var
  s,a :string;
  sp,mylen,oddEvenCheck,i :integer;
begin
  sp :=0; {指示讀資料狀態}
  a  :='';
  while True do
  begin {quelist為接收緩衝區}
    if quelist.Count <>0 then {緩衝區取數}
    begin
      s :=quelist.Strings[0];
      quelist.Delete(0);
      a :=a+s;
    end;
    if a='' then Continue;
    {是否空閒狀態}
    if sp =0 then
    begin
      if ord(a[1]) =0xFE then
      begin
        sp :=1; {進入到起始標誌狀態}
      end
      else {起始標誌錯誤}
      begin

      end;
      Delete(a,1,1);
    end
    {是否進入收到起始標誌狀態}
    else if sp =1 then
    begin
      mylen :=Ord(a[1]);
      sp :=2; {進入長度狀態}
      Delete(a,1,1);
    end
    {是否進入長度狀態}
    else if sp =2 then
    begin
      if Length(a) <=mylen +1 then Continue;
      {資料結束標誌:正確}
      if ord(a[mylen +2])=0xFD then
      begin
        sp :=3; {進入資料校驗狀態}
      end
      else {資料結束標誌:錯誤}
      begin
        sp :=0; {重新進入空閒狀態}
      end;
    end
    {是否進入資料校驗狀態}
    else if sp =3 then
    begin
      for i:=2 to mylen do
      begin
        oddEvenCheck :=a[1] xor a[i];
      end;
      if oddEvenCheck =Ord(a[mylen +1]) then
      begin
        sp :=4; {校驗正確,進入完成狀態}
      end
      else {檢驗錯誤,進入空閒狀態}
      begin
        sp :=0;
      end;
    end
    else if sp =4 then
      begin
        {這裡省略處理命令與附加資料程式碼}
        sp :=0; {進入空閒狀態}
        Delete(a,1,mylen+2);
      end;
  end;
end;

/*
   主要的變數宣告在這邊
/* 串列埠狀態機巨集 */
#define DATA_HEAD  3
#define DATA_LEN   4
#define DATA_COM   5
#define DATA_NUM   6
#define DATA_CRC   7
#define DATA_TAIL  8
#define DATA_ADD   9

#define COMMAND_SIZE 20

int g_count = 0;             //狀態機緩衝區下標
int g_uart_state = DATA_HEAD;//串列埠狀態機狀態標誌
unsigned char data;          //串列埠資料
                             //狀態機緩衝區
unsigned char command_buf[COMMAND_SIZE] = {0};
unsigned char *bufptr = &data;

while (1)
{
    /* 從串列埠中一次只讀取一個字元 */
    retv = read(fd, bufptr, 1);
    if (-1 == retv)
    {
        printf("read error!\n");
        exit(1);
    }

    /* 
       ---------------- 串列埠狀態機-----------------
       資料包格式
       BYTE     |  BYTE   |  BYTE  |  BYTE  |   BYTE
       包頭          長度       命令       資料       包尾
    */
    switch (g_uart_state)
    {
        /* 查詢包頭狀態 */
        case DATA_HEAD:
        {
            /* 找到包頭 */
            if (data == 包頭)
            {
                /* 將包頭存入指令陣列 */
                command_buf[g_count++] = data;
                /* 改變串列埠狀態機狀態為長度 */
                g_uart_state = DATA_LEN;
            }
            else
            {
                //包頭匹配錯誤
                g_count = 0;
            }
            break;
        }
        /* 檢查資料長度 */
        case DATA_LEN:
        {
            if (data == 長度)
            {
                //長度匹配
                command_buf[g_count++] = data;
                //改變狀態機的狀態為命令
                g_uart_state = DATA_COM;
            }
            else
            {
                //長度匹配錯誤
                g_count = 0;
                //改變狀態機的狀態為包頭
                g_uart_state = DATA_HEAD;
            }
            break;
        }
        /* 檢查命令是否合法 */
        case DATA_COM:
        {
            if (data == 命令)
            {
                //合法存入
                command_buf[g_count++] = data;
                //改變狀態機的狀態為資料
                g_uart_state = DATA_NUM;
            }
            else
            {
                //不合法改變狀態機狀態為包頭
                g_count = 0;
                g_uart_state = DATA_HEAD;
            }
            break;
        }
        /* 檢查資料是否合法 */
        case DATA_NUM:
        {
            if (data是合法的資料)
            {
                //合法存入
                command_buf[g_count++] = data;
                //改變狀態機的狀態為包尾
                g_uart_state = DATA_TAIL;
            }
            else
            {
                //不合法改變狀態機狀態為包頭
                g_count = 0;
                g_uart_state = DATA_HEAD;
            }
            break;
        }
        /* 檢查包尾*/
        case DATA_TAIL:
        {
            if (data == 包尾)
            {
                command_buf[g_count++] = data;
                //成功的解析到一個完整的符合傳輸協議的串列埠資料
            }
            /* 完成一次資料包的解析讀取 */
            g_count = 0;
            g_uart_state = DATA_HEAD;
            break;
        }
        default:
        {
            g_count = 0;
            g_uart_state = DATA_HEAD;
            memset(command_buf, '\0', sizeof(command_buf));
        }
    }
}