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