本文描述了基于串口進行數據幀通信的協議設計和實現方法。
數據幀格式
| 前導碼 | 頭部 | 數據 | 校驗 |
- 前導碼: 用于幀同步,通知接收端數據幀開始
- 頭部: 幀描述信息,包括長度,幀ID,ACK,版本號等
- 數據: 有效載荷
- 校驗: 對頭部和數據部分的CRC校驗
發送端
數據結構:
- 一個固定長度的發送隊列,例如10個數據幀的鏈表
- 重傳定時器,取值為一個數據幀的傳輸時延
執行循環:
- 如果隊列有空閑空間,取上層應用緩存中的數據幀發送,否則
- 如果重傳定時器超時,取發送隊列中最老的數據幀發送
- 如果沒有數據幀可以發送,取消重傳定時器,否則,
- 重置重傳定時器
- 發送前導碼
- 發送數據幀
- 發送校驗碼
- 如果接收到成功答復,從隊列中去除該數據幀。如果該幀不是隊列中最老的幀,說明發生幀丟失,立刻觸發重傳機制
- 如果接收到失敗答復,從隊列中重傳該數據幀,并重置重傳定時器
設計說明:
- 發送隊列的目的是為了提高發送速度,不需要嚴格順序化發送數據包。
- 可以將每個隊列看作一條數據流,頭部加入流ID后可以支持多條數據流,這時需要一個調度器進行流量調整
接收端
數據結構:
- 一個固定長度的發送隊列,例如10個數據幀的鏈表
- 最后接收的幀ID
執行循環:
- 接收前導碼,并丟棄無效數據,直到發現有效前導碼。如果一直沒有發現前導碼,返回失敗答復。
- 接收頭部,解析長度信息,并做有效性驗證,如果驗證無效,返回失敗答復。如果在頭部中發現前導碼,則認為是新的數據幀開始,重新開始接收頭部。
- 接收數據,接收CRC,并作有效性驗證,如果驗證無效,返回失敗答復。如果在數據和CRC中發現前導碼,則認為是新的數據幀開始,重新開始接收頭部。
- 數據幀接收完畢,返回成功答復。
- 嘗試將數據幀加入接收隊列,如果發現重復幀,直接丟棄,否則按幀ID進行順序插入。如果該幀是最后一次成功接收的下一幀,則通知應用層獲取數據,數據取走后,更新最后接收幀ID。
- 如果接收隊列已滿,則通知發送端停止發送,知道有足夠空閑空間再通知發送端重傳。
設計說明:
- 接收隊列的目的跟發送隊列類似,另外提供基于幀ID的排序功能
- 在整個接收過程中持續檢測前導碼,避免丟棄有效數據幀
- 接收端對發送端進行抑制,防止上層應用接收數據不及時導致發送端無效重傳
ACK幀格式
單向數據流簡化設計
接收端發送返回幀時,通信角色發生調換,原來的接收端變為發送端,原來的發送端變為接收端。為了簡化這個階段的協議設計,避免重復執行上述流程,可以將ACK幀按照最簡單形式設計,比如僅用1個字節來表示:
|一字節返回碼|
返回碼取值:
- 0:表示接收端緩存已滿,停止發送
- -128:表示接收端緩存有空閑,發送端可以繼續發送
- +N: 表示成功接收到幀ID為N的數據幀,取值范圍 [1, 127]
- -N: 表示幀ID為N的數據幀接收失敗,取值范圍 [-1, -127]
進一步說明:
- 沒有前導碼,單個字節表述所有信息
- 有效幀ID為 [1, 127],超過127后幀ID會回繞為1。因此發送和接收端在127邊界處要保證前面的數據幀完全傳輸成功后再進行回繞處理。
雙向數據流設計
在雙工模式下,無法進行上述簡化處理,通信兩端互為發送端和接收端,此時ACK可以作為獨立數據幀發送,也可以夾在有效數據幀頭部發送。
前導碼
如果前導碼在有效數據幀中出現,那么會被誤認為是新的數據幀開始,從而導致新的數據幀以外校驗失敗而丟棄,這在一定程度上浪費帶寬資源。因此要求前導碼具有唯一性,不允許出現在數據幀中。一種做法是在發送端對數據幀進行替換處理,將數值與前導碼相同的數據進行格式變換。比如前導碼為01010101,那么如果可以將該數值擴展為01010101 01010101,在接收端進行反向操作,將01010101 010101替換為01010101。這會增加協議處理的消耗,但是提高了帶寬利用率,減少了無效重傳的消耗。可以在真實的環境中進行測試這種機制的效果如何。