通信協議
串行通信接口(如RS232、RS485等)作為計算機與單片機交互數據的主要接口,廣泛用於各類儀器儀表、工業監測及自動控制領域中。
通信協議是需要通信的雙方所達成的一種約定,它對包括數據格式、同步方式、傳送速度、傳送步驟、檢糾錯方式以及控制字符定義等問題作出統一規定,在雙方的通信中必須共同遵守。在實際應用系統中,如果缺少一個嚴格、合理、規范的串口通信協議,將無法保證數據傳輸的正確性及通信的可靠性。
因此,需要提出一種基於狀態機串口通信協議的設計方法:通過合理地設置數據包格式來保證了數據傳輸的正確性:引入了狀態機方法,簡化了協議的實現難度,提高了通信的可靠性,同時使通信過程具有較高的容錯能力。
定義數據包格式
串口通信中最小的的信息單元是數據幀。一個數據幀通常包括起始位、數據位、結束位,另外還可以包含用於檢測傳輸錯誤的“奇偶校驗位”,每個數據幀中傳輸的數據位可以有5、6、7、8或9個。
實際通信過程中,數據的發送是一幀一幀地進行,當被傳輸的數據超過一幀時(例如浮點型數據),如果沒有對數據幀進行必要的打包,發送出去的數據將會很難被數據接收方解釋與分析,進而造成數據傳輸混亂與錯誤。因此,在一般應用中有必要將數據幀組裝成數據包再發送。
- 起始標志表示開始接收一個新的數據包。
- 數據長度命令和附加數據共占的字節數。設置此字段,可方便接收方識別數據包的長度並能夠准確地接收數據包。
- 命令用來說明數據包的用途。
- 附加數據 當命令不同時,含義不同。
- 校驗是對命令字段與附加數據字段的所有字節數據的異或校驗。
- 結束標志表示該數據包結束。
另外,在多機通信中,數據包中還應增加源地址與設備地址等字段。
通信狀態機
狀態機簡介
狀態機由事物所處的狀態及引發狀態變化的外部事件兩部分組成。
在軟件編程中,事物所處的狀態可以描述為某個程序片斷或函數,而引發狀態變化的處部條件可以理解為條件判斷語句,當條件為真時,事物的狀態發生變化。事物發生變化前的狀態稱為現態,變化后的狀態稱為次態,程序中可以通過不同的數字對不同的狀態進行編號。現態到次態的變化可以通過狀態變量值的改變來描述。
在協議中需要傳輸的基本信息單元是數據包,數據包一般包含多個數據幀。實際傳輸過程中,數據的傳輸通常是一幀一幀地進行,數據包是被拆分成若干幀數據后再進行傳輸,數據接受方也是分幀接受一個數據包。
數據接受方在解釋與分析數據包時可能存在兩個問題:
1.識別並接收完整的數據包
- 對於數據接收方,一個數據包是分若干批到來,在識別包頭與包尾時,也就是幀同步問題;
- 具體編程時存在難度,特別對於已接收部分與未接收部分以及數據接收的進度及狀態的處理。
2.數據傳輸時的容錯能力
- 數據傳輸過程中已經出現錯誤時,系統應該具有擺脫錯誤狀態,恢復到正常狀態的能力。
- 例如,當一個數據包只傳輸完一部分時,因為未知故障,下一個數據包就開始傳輸,系統應該能識別出傳輸錯誤,拋棄前一個出錯的數據包,並且能正確接收下一個數據包。
- 實際編程時處理這種問題難度較大,結果很可能會出現將第一個數據包的前一部分與第二個數據包的前一部分拼裝成一個新的數據包的情況,這就損失了兩個數據包,最嚴重的結果可能是系統無法從錯誤中恢復,這就嚴重降低了系統的安全性與可靠性。
為解決上面的兩個問題,在協議中引入了狀態機。
在狀態機中,狀態的變化依賴於外部觸發條件,當條件滿足時,狀態將發生變化。
在協議中將數據包接收的各個階段定義為不同的狀態,將接收一幀新的數據或數據處理的結果作為外部觸發條件,從而達到狀態改變的目的,最終完成一個數據包的接收與校驗。
串口通信狀態圖
串口通信協議中,發送數據包時一般不需引入狀態機,這主要是為提高發送速率和簡化編程模型而考慮。
在協議中主要針對數據接收過程建立狀態機。
串口通信數據接收過程
- 當未開始接收數據包或發現數據傳輸出錯時,系統進入空閑狀態;
- 當數接收到數據包起始標志時,變為收到起始標志狀態,如果收到的數據不為起始標志,系統繼續保持空閑狀態;
- 進入收到起始標志狀態后,新接收到的任何數據將被當作數據包中命令與附加數據的總字節數(記為LEN),系統進入收到數據長度狀態;
- 繼續接收新的數據,直至接到新收到的數據總字節數達到LEN +2,進入檢驗結束標志狀態;
- 這時可以檢驗結束標志是否為協議定義的標志值,如果是,說明傳輸正確,否則傳輸出錯,出錯后應查找接收緩沖區中本數據包的起始標志后有無其它起始標志,如果沒有發現起始標志,系統應進入空閑狀態,否則應直接進入接收到起始標志狀態,這樣可提高系統容錯能力,方便系統從錯誤中恢復。
- 檢驗結柬標志正確后,進入數據校驗狀態;
- 校驗結果如果正確,數據包接收完成,否則說明傳輸出錯,系統進入空閑狀態。
上位機軟件編程邏輯
上位機軟件中,當接收到數據時,串口控件會觸發一個事件,在事件處理代碼中應及時將收到的數據存入接收沖區,同時不應該把串口通信協議接收部分的代碼放置在此事件中,否則后面到來的數據可能因為前面先到的數據沒有及時處理完畢而被沖掉,導致數據丟失。
- 在上位機軟件運行時,應該啟動一個Windows線程,用於不斷檢測接收緩沖區是否為空,不為空時則對緩沖中的數據進行處理;
- 線程類創建好后,應具體編寫線程類執行函數的處理過程,在其中通過狀態指示變量sp實現狀態機機制;
- 數據包的接收進度依據於狀態指示變量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)); } } }