要完成一個通訊, 最基本會有兩個主角, 發送者 及 接收者.
兩者之間, 只要有著共同的通訊協定, 就可以簡單達成通訊了.
以下的例子中, 在表達傳送數據的時候, 為了分辨一個 byte, 以及文字的數據, 將會用 [xx] 表示一個數值為 xx 的十六進數據 (有需要時, 我會加上 {...} 去顯示二進值), 例如 [31] 即數值為 0x31 的一個 byte. 而要表達一些文字時, 就會用 "..." 去表達, 例如 "A" 就是一個 A 字, 其 ACSII code 為 65, 實際送出 0x41. 所以 [41] 跟 "A" 將會是相同的東西.
通訊協定的選擇
(1) 單一字節
對於發送單一字節的訊息, 大家可能會覺得沒有什麼協定吧, 就是一個 write, 另一個 read 就完成.
其實, 當中對通訊的資料, 也有一個協定了, 就是發出的資料, 長度是一個 byte.
資料的長度, 也是協定的一種.
例: 如要送出 x=30, 就送出以下一個 byte
[1E]
(2) 簡單合成
比如我要發送 一個物體的 坐標 (x,y,z 均為 0-1023 的整數), 每個坐標可以用 2 byte 去發出.
最簡單的方法, 就是定了資料的次序, 每個數據以 H L 的次序送出 2 byte, 連續發出 6 個 byte, 那這個協定就可以看成是:
[X][X][Y][Y][Z][Z]
例: 當 x=30, y=123, y= 567 時, 就會送出
[00][1E][00][7B][02][37]
(3) 轉化合成
另一個方式, 考慮到 0-1023 的數值, 只用了 10bits, 3 個數字加起來, 也只有 30 bit. 如果認為通訊時間比較重要, 不介意多做前後期工作, 甚至可以把數據先合成 4個 bytes 再送出去.
最後餘下的 2個 byte, 還可以用作 checksum.
這個協定, 就變成是:
{xxxxxxxx}{xxyyyyyy}{yyyyzzzz}{zzzzzzcc}
例: 當 x=30, y=123, y= 567 時, 就會送出
30 = 0x001E = 0000011110b
123 = 0x007B = 0001111011b
567 = 0x0237 = 1000110111b
cc = 00 (暫定不作任何計算)
{00000111}{10000111}{10111000}{11011100}
即 發送以下 4 個 bytes 就可以了.
[07][87][B8][DC]
(4) 在數據中插入開始/結束碼
有時, 數據的變化比較大, 比如要發送一段文字, 總不可能每次都固定為最大長度去發送吧. 當中加入結束碼, 可以更有效處理數據. 開始/結束碼的選擇, 也有一定的技巧, 一定要避開數據中會出現的所以可能性, 如果沒法避免, 就要加入特別處理方法.
例: 當 x=30, y=123, z= 567, 結束碼為 [FF] 時, 就會送出
[1E][FF][7B][FF][02][37][FF]
萬一數據中有可能出現 [FF], 就要對數據加以處理, 例如數據中每一個 [FF] 就變成 [FF][FF].
接收的時候, 就做一次還原, 每當碰上連續兩個 [FF] 就變成 [FF], 最後單獨的就是結束碼了.
例如 x=255, y=123, z=567
就會發送 [FF][FF][FF][7B][FF][02][37][FF]
最前的兩個 [FF] 合成數據 [FF}, 之後單獨的就是 結束碼, 如此下去就可以了.
以上都是站在電腦的角度去溝通, 但有時為了讓用家方便看到資料, 會把數據轉成人類可以看到的形式...就是完轉化成文字送出去. 上面的例子中, 數據是0-1023, 最簡單的方法, 每個數據用 4個 字符, 前面補 零, 合共 12 個字符.
[X][X][X][X][Y][Y][Y][Y][Z][Z][Z][Z]
例: 當 x=30, y=123, z= 567 時, 就會送出
"003001230567"
即送出以下 12 個 byte:
[30][30][33][30][30][31][32][33][30][35][36][37]
(6) 在文字中加入開始/結束碼
跟 4 有點相似, 上例中 x=30 時, 前面浪費了 2 個 byte, 有些時間, 如果數據的變化比較大, 用固定長度會出現更大的浪費. 比如數值由 0-10000000, 因為要滿足所有數值, 就要預留 8 個 byte, 如果正常數據只是 100 以內, 那大部份數據都要浪費 6 個 byte 了. 所以, 有些協定, 就會加入 開始/結束 碼, 把幾個數提分開. 注意, 為免誤認 開始/結束碼, 一般會使用數據中不會出現的字符. 例如上面要傳送數字, 就絕不會用數字作開始/結束 碼了. 但當要發送二進數據時, 就要像 (4) 的對數據進行處理.
如果只用結束碼 [#] 的話, 上面的協定就變成:
[X]*[#][Y]*[#][Z]*[#]
例: 當 x=30, y=123, z= 567 時, 就會送出
"30#123#567#"
如果要發送二進的數據, 因為開始/結束 碼有機會在數據中出現, 就要進行特別處理.
例如, 當數據中出現 "#" 時, 把每一個 "#" 就變成 "##". 接收程式只會在收到 基數的連續 "#" 才會把最後一個看成是結束碼,
例如: "12#3", 就會送出 "12##3#", 接收時, 因為第一次的 "#" 是連續兩個 "#", 就會變回數據中的一個 "#", 而最後一個是單獨的, 就是結束碼了.
慢慢發展下去, 有些人會覺得, 這種固定次序的方式, 對程式更新有限制.
比如 將來 加減資料, 新舊程式難以共存. 對於一些公用的服務, 會有很大影響.
經過不斷改良, 就出現了像 XML 的格式, 每一個資料都加上一個名稱. 讀取資料就更準確, 不易出錯了. 將來改變參數也不會有大問題.
比如上面的例子上, 要發送一個位置 x=30, y=123, z= 567 , 可以送出以下字串:
<location><x>30</x><y>123</y><z>567</z></location>
非常清楚的表達了要發送的資料, 以及每個資料的意義, 很好吧?
但請細心想想, 對通訊而言, (2) 的簡單合成固定長度位置發出去, 好在那裡?
以 MCU 為中心去作出選擇
機械不是人類, 只要有清楚的協定, 就會明白數據的意義.
對桌面機或大型電腦而言, 丁點禿的數據處理, 沒什麼大不了.
但在 MCU 上應用, 只會是加重負擔, 可以說是畫蛇添足的門面功夫. 既浪費資源, 亦沒有好處.
(3) 的方法可以省點通訊時間, 但合成數據除非本身有簡單硬件去做, 又或數據的來源就是合成的, 不需額外處理, 否則, 對一般通訊而言, 未必有大幫助. 但對程式的要求會相對提高了, 所以不贊成初學者使用. 而且, 執行上也要浪費更多資源去處理數據的合成及還原.
(5),(6),(7) 可能是比較多人會喜歡用的, 特別是 (6), 例如用 "30,123,567" 去發送資料, "看"起來不錯吧.
但這個"看"法, 只在於用人眼去"看", 對MCU而言, 一點也不好"看" 呢.
而 MCU 一般用途比較專一, 將來出現大變化的機會不會太大, (7) 的方法實在是太浪費了, MCU 的程式, 應該要更重視資源的運用. 那些多餘的 tag header, 實在要不得.
一般而言, 個人會選擇 (2) 或 (4), 又或是混合使用. 如果數據是固定的, (2) 可以說是最快捷簡單的方式. 有時可以加入開始/ 結束碼, 去防止傳送時出現問題, 而誤認數據, 就更加安全了.
日常生活中, 網絡發送的數據包, 它的 header 也是用固定長度的數據送出的.
以下是 IP Header 的資料格式的例子:
如果你把數據拿來看, 只會是一堆難以直接理解的二進數值.
但對電腦系統而言, 要看這樣的數據, 比看文字容易得多呢. 接收端可以直接讀取 第 3,4 個byte 去獲取資料的總長度, 第 10 個 byte 去辨別類型, .....
如果轉成了文字發送, 你自己可能會容易看到資料的意思, 例如 "TotalLength:128; Protocol:TCP", 但系統要了解當中的意思, 可要花費不少資源呢.
通訊用的資料是給系統看的, 應該先考慮那個方法對系統更好, 可不要 喧賓奪主.
注意:
我並不是說 XML 不適合用作通訊, 在大型的系統上, XML 也有很大的好處的.
只是因應 MCU 的限制, 以 XML 格式進行通訊, 絕非一個好的選擇.
發送資料的方式
但發送數據, 並非只有一個個 byte 的, 不同的數據, 又如何轉成 byte[] 去發送呢?
網上經常會看到, 有人問用 串口通訊時, 如何把 浮點 的數據送出去?
很多人都會選用 文字的方式, 例如 "123.45" 送出去.
如果精確度是固定的話, 這個方法還勉強可以, 如果精確度不定, 那要轉成多少個位數才合理呢?
如果大家可以把自己是人類的前提取下, 把自己當成是一台電腦, 問題就會簡單得多了.
為什麼? 先問問自己, 你要發送的資料, 發送之前是放在那裡的? 是用什麼形式去放?
或許你會答我, 是放在 某個 變數中吧. 例如 float a = 123.45; 就是放在 a 那裡.
那 a 是什麼? 那個 123.45 是怎樣放? 放在那裡?
a 可以說是放在電腦內的記憶體的某些資料吧, 至於怎樣放置 小數, 這是大學課程中有教的吧.
未完.....待續....
沒有留言:
張貼留言