极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 14008|回复: 8

串口通訊基本知識: 數值與字符 的分別

[复制链接]
发表于 2014-4-10 16:04:31 | 显示全部楼层 |阅读模式
本帖最后由 Super169 于 2014-4-10 16:12 编辑

經常看到有人問及 不能傳送什麼什麼的, 又或是傳送/接收的東西出錯, 很多時都是因為不太了解數值跟字符的關係.

發這個帖是希望可以幫助大家了解一下自己想要的是什麼, 當然亦要知道 Serial Monitor 顯不出來的, 又是什麼.

以下有幾個問題, 希望可以幫助大家思考:

(1) 大家知道以下的分別嗎?  有那些是相同的
    a) 26
    b) 0x1A
    c) "26"
    d) "1A"
    e) {0x32, 0x36}
    f) {0x31, 0x41}

    這可以說是類似問題的根本, 數值 跟 字符 的關係.

(2) Serial.write 跟 Serial.print 有何分別?  
    經常看到有朋友會說用 Serial.print 不行, 用了 Serial.write 還是不行.  兩者其實並不是可以隨意調用的.

(3) Serial Monitor 看到的是什麼? 跟其他模塊通訊是要的又是什麼?
    試想想用 (1) 當中的 6 個數, 分別用 Serial.write 及 Seril.print 發送, 猜猜 Serial Monitor 中會顯示出什麼.

先不要寫程式, 也不要再看下去, 先在心中想想結果, 再看看做出來的結果有什麼分別.

:
:

以下是簡單的解答:

(1) a) 26    - 是一個十進位 26 的數值
    b) 0x1A  - 是一個用十六進表達, 同樣是十進位 26 的數值 (i.e. 0x1A = 26)
    c) "26"  - 只是兩個字符, 分別是 "2" 和 "6"
    d) "1A"  - 只是兩個字符, 分別是 "1" 和 "A", 注意 "26" 跟 "1A" 是不等的
    e) {0x32, 0x36} - 不就是兩個數值嗎? 對, 但它同時也是 "26", 之前說 "2" 和 "6" 的兩個字符, 轉為電腦儲存的數值 (如 ASCII 格式) 就是 {0x32, 0x36}
    f) {0x31, 0x41} - 同樣, 就是 "1A" 了 (其實有少許不同, 往後會再解釋)

(2) Serial.write 是把資料的數值直接傳出去; 而 Serial.print 是把資料化成字符再傅出去

    化成字符是什麼意思?  就是把 26 變成 "26" 了.  而 "26" 本身已是字符, 會直接傳出去.
    但要注意, {0x32, 0x36} 雖然同樣有 "26" 的含意, 但傳出去時, 要看它所在的變數的設定.

    - 如果是 unit8_t a[] = {0x32, 0x36} 是不能直接 print(a) 的, 因為 print 並示支援數值 array       

    - 如果是 char a[] = {0x32, 0x36}, 可以吧?  也不太好, 可以印出去, 也會收到 "26" 但會有意外收獲.
      因為 print 接收 以 0x00 為終結的字符, 只有 {0x32, 0x36} 的話, 系統會把記憶體中, 往後的也傳出去, 直到碰上 0x00.

    - 但如果是 char a[] = "26", 雖然同樣是儲存了 "2" "6" 的字符, 但背後系統會多送一個 0x00 作結尾, 所以用 print(a) 就不會有問題了.


(3) Serial Monitor 看到的東西, 看似是簡單的問題, 但往往是發問的朋友出錯的地方.
    簡單的說, Serial Monitor 就是顯示收到的數值, 以 ASCII 表達出來的字符.

    如上例中, 假設 e) f) 用 char, 而之後的一個 byte 剛好是 0, 那 serial monior 顯示的結果應該如下 (自己可以解釋到嗎?)

    Serial.write:
        a) [SUB]        // 接收到的是數值為 26 (0x1A) 的一個 byte, 在 ASCII 表中的意思為 {substitute}, Serial Monitor 中顯示反白 SUB, 以示並非 "SUB" 的字符.
        b) [SUB]        // 同上
        c) 26
        d) 1A
        e) 26
        f) 1A

    Serial.print:
        a) 26
        b) 26
        c) 26
        d) 1A
        e) 26
        f) 1A


  明白當中原因嗎?  特別是 Serial.write 中 的 a) b), 不要因為看到亂碼, 就以為是錯誤.

  希望大家看了後會明白一點, 最後就是要想想自己需要的是什麼.
  如果是要在 Serial Monitor 上顯示一個數未, 就要傳送出字符 (或對應的值, 例如 "1" 就是 0x31).

  如果是跟其他模塊溝通, 就要看清楚規格中要求的是什麼, 可不是自己想送什麼就是什麼的.

當然...亂碼的產生, 也有可能是因為 buadrate 不對.  最好是先從低頻 (約 9600) 配對開始, 如要快速的, 先測試 成功了再慢慢調高.


(不好意思, 我是香港人, 習慣了廣東話的口語, 只會繁體輸入, 文句亦有不通順的地方, 希望大家見諒.)
回复

使用道具 举报

发表于 2014-4-10 16:32:09 | 显示全部楼层
顶,应该多普及一下基础知识。好多一上来就直接写程序不学基础的
回复 支持 反对

使用道具 举报

发表于 2014-4-10 21:30:34 | 显示全部楼层
收藏了谢谢!
回复 支持 反对

使用道具 举报

发表于 2014-4-10 21:52:04 | 显示全部楼层
在努力学习但是没学透彻,大概似乎明白了一点。不求甚解就是我
回复 支持 反对

使用道具 举报

发表于 2014-4-13 08:21:18 来自手机 | 显示全部楼层
谢谢了,终于有人系统的讲了一下这些!不过,我还有个问题!

对方以十六进制发送数据,我接收处理也用十六进制,在接收方有什么讲究?
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-4-13 11:59:24 | 显示全部楼层
本帖最后由 Super169 于 2014-4-13 13:06 编辑
maxims 发表于 2014-4-13 08:21
谢谢了,终于有人系统的讲了一下这些!不过,我还有个问题!

对方以十六进制发送数据,我接收处理也用十 ...


不好意思, 我不太肯定 "十六进制发送", 及 "十六进制处理" 的具體意思.

我猜測, 基本上說 十六进制发送" 的話, 應該是指以數值傳出去 (例如 0x1A 的一個 byte代表 26), 不會是指 十六进字符 ("1A") 吧.  或者我對兩個方法也說說吧.

如果是以 數值發送 (就是一個數值, 一個個 byte 傳出去, 沒有十六進或十進的分別, 直接說是二進的發出去, 因為最基本單位是每次去一個 bit 以 高低位訊號發送)

基本上, 最簡單就是用一個 uint8_t / byte 的變數去接收資料 (當然 int 之類亦可, 但 int 在不同機種會有不同的長度, 所以一舨用指定大小的如 uint8_t, byte, uint16_t 之類比較安全一點), 然後直接處理.
比如通訊協定如下 (有三個指令):
  - 0x01 <data> <data>
  - 0x02 <data> <data> <data>
  - 0x03 <data>

先用 uint8_t code = Serial.read(); 接收了一個 code, 之後再按照不同的 code, 接收餘下的資料進行處理就可以了.  接收方式看需要而定, 簡單的可設定一個足夠不同指令的 array, 把資料放進去.  又或有不同的地方去接收 (例如 0x01 的是一個 16bit 的數, 就合成一個 uint16_t 放到 var1, 0x02 就是三個獨立的數放入 uint8_t var2[3] 中, 而 0x03 是一個 char 就放入 char var3 之中.)  沒有絕對, 最重要是看通訊協定處理.

此外, 當中每個資料接收之間, 最好加入一點 delay.  原因是接收時, 處理的速度可能比傳送快很多.  
如果用 9600bps, 每一個 byte 最少也要 8/9600 約 1ms 的時間去傳送, 但程式處理接收的速度相對很快.  當你完成一個 read() 之後, 可能少於 0.5ms 就會進行下一次 available() 的檢測, 如果下一個 data 還未送到, 就可能會出現錯誤判定為資料發送完結.  
因此, 每次 read 之後, 在下一次 available() 檢測之前, 加入一點延遲, 如 delay(2); 之類, 只是浪費一丁點時間, 但對 available() 的檢測會較準確.  另外為免因為傳送受到干擾, 而收到錯誤資料, 一般會在資料後加一個 checksum, 最簡單的方法, 可以把整串資料的數值加起來, 取低位的一個 byte 傳出去 (你可能會發覺, 把 1,2,3 發成 3,1,2 也會被判定為正確.  但不要想得太複雜, 只是作簡單的檢測, 總不能把資料發兩次去檢測吧. 現實中有很多大同的檢測方法, 越準確對應的檢測碼也會比較長, 對單片機而言, 不可能用一串長長的檢測碼吧. 用一個 byte 的已可以了, 如有需要可以加上不同的計算, 例如加權的方式, 甚至用 2 個或以上的檢測碼, 但不要把檢測碼弄得太長..否則, 乾脆發兩次作比對算了.)


如果傳送協定是以 字符的形式進行 (例如 32路串口航舵控制板之類), 資料的長度可能不是固定的, 例如 要傳送一個 0 - 255 的值時, 用 數值的方式, 就會固定是一個 byte, 但用字符時, 一般不會補零, 就可能會出現 1 至 3 byte 長度的可能了 (例如 "3" 是一個 byte, "123" 是三個 byte 了).
一般用字符傳送的, 都會有一些分隔符號, 以便決定資料的長度.  否則, 就要用固長度的方式傳送.
例如要送出三個 0-255 的數值, 如沒有任何分隔, 亦沒有固定長度, 當發送出 "123","12","3"後, 接收的一方收到了 "123123" 的資料, 就可能出現不同的可能:
- "1", "23", "123"
- "1", "231", "23"
- "12", "3", "123"
- "12", "31", "23"
- "123", "1", "23"
- "123", "12", "3"
不同的數值, 更可能出現更大的變化.

所以, 一般會加入分隔或轉為固定長度

例如:  
- 用"#"作開始, ";"分隔每個數, "$"作結束: "#123;12;3$"
- 用固定三個字:  "123012003" 亦可轉成 16 進發出 "7B0C03"

接收的也可以準確分拆資料使用 (通常串口都會是加入分隔符號, 如用固定長度, 多會是16進的, 但亦有可能是十進, 最重要是看通訊協定)

由於以字符傳送, 資料的長度可能不能確定, 如果直接處理, 程序會比較複雜, 一般會設定一個 char[] 或 String, 先把一次的資料收下來, 再對資料進行分析.
注意, 如果用 char[] 的時候, 要確保有足夠的空間存放一次的資料.  例如用分隔符號的例子中, 傳送三個數值, 就要預留 3 x 3 + 4 = 13 個字符的空位 (或者除去開始及結束的不用再處理, 也要 11 個字符).  

當然, 有可能出現資料受干擾而收到錯亂數據, 在接收時, 資料或會長度改變了.
例如:  #123;3#123;31;234$", 第一個傳送可能出了問題, 程式中需要加入在中途對開始碼的檢測, 以為超出預定長度時的處理.  否則, 如果用 char buffer[13] 去接收, 當接收了 "#123;3#123;31" 後, 再收到 ";" 而放進 buffer[13] 中時, 就會把其他資料改變了, 後果難以估計.


最後還是一句, 接收方式不會是固定的, 不要奢望同一套方法可以完全通行, 最重要是根據通訊協定去處理.
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-4-13 13:53:54 | 显示全部楼层
再補充一點, 在數值接收時, 要把兩個 byte (uint8_t) 合成一個 word (uint16_t) 時, 多用二進操作會比較有效率.

例如:
(1)   c = a << 8 | b;
(2)   c = a * 256 + b;

兩者的結果完全沒有分別, 但 (1) 比 (2) 要快上很多很多倍的.  雖然絕對值可能只是微不足道的差別, 但作為程式員, 可能情況下多考慮一點效能上的提升, 總不會有害處的.
對複雜的系統, 一丁點一丁點的改善, 合起來就會有一定的效果了.
回复 支持 反对

使用道具 举报

发表于 2014-4-13 22:54:36 | 显示全部楼层
本帖最后由 maxims 于 2014-4-13 22:56 编辑
Super169 发表于 2014-4-13 11:59
不好意思, 我不太肯定 "十六进制发送", 及 "十六进制处理" 的具體意思.

我猜測, 基本上說 十六进制发 ...


我先回复这个十六进制发送、接收、处理的疑问。然后再慢慢的看一下你写的内容。

“龙华小区”这四个汉字的内码为“0xC1 0xFA 0xBB 0xAA 0xD0 0xA1 0xC7 0xF8”

发送端发送龙华小区给我接收,发过来的内容就是“0xC1 0xFA 0xBB 0xAA 0xD0 0xA1 0xC7 0xF8”,这算是16进制发送吗?或者叫做HEX格式发送。很多串口工具都提供HEX和ascii格式发送和显示的不是?

完整的数据“7F 00 03 10 80 01 C1 FA BB AA D0 A1 C7 F8 2F A3 B8 A3 B5 A3 B7 2F BD F0 C9 B3 B8 DB CD E5 00 93 7F”
这是我虚构的一个内容。
遵循这个“7F 00 ## 10 80 01 ## 2F ## 2F ## 00 ## 7F”格式,#号代表的内容不固定、长度不固定,最后一个#号是CRC校验。
##2F##2F##这里,或许有三个2F或许只有两个,也不固定……
有好思路解决这样的问题吗?
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-4-14 02:41:03 | 显示全部楼层
maxims 发表于 2014-4-13 22:54
我先回复这个十六进制发送、接收、处理的疑问。然后再慢慢的看一下你写的内容。

“龙华小区”这四个 ...

我不太清楚一般是否會叫 十六進, 可能協定上用十六進表達, 就叫十六進吧.  中文字 double byte 的傳送, 處理上跟一般數值沒大分別.  
基本上看 spec 時, 寫成 16進的數字, 例如 20h 之類, 會比清楚知道傳過來的是一個 byte 的長度.  也許你說得對, 用十六進的叫法或者會比較易明白.

你說的協定也不算複雜吧, 在數值傳送時利用分隔碼的分式.
只是分隔碼比較複雜, 可能是為了傳送比較多變化的資料 (例如當中某些資料有可能出現 0-255 的值, 單一碼有機會出錯).
7F 00 <- 開始碼
10 80 01 <- 為第一節分隔
2F <- 之後的分隔
00 <- 為 CRC 前的分隔
7F <- 為結束

也算後有規律吧.  在接收到 7F 00 的開始碼前, 所有資料作廢.
接收 7F 00 後, 把往後都資料放入 buffer, 直到 01 80 01 的出現.
之後同樣方法接收往後的資料.

正常的接收不會是問題, 最大問題時, 當資料有錯誤時, 如何判斷?  當中太多不肯定長度的數據, 一旦出錯了, 就會不斷接收.
應該每一節總會有個上限吧, 當超出上限已經可以判定為錯誤, 否則出了問題後, 要完全接收所有資料, 從 CRC 判定.
萬一訊號受干擾, 一直不能成功接收到分隔碼, 儲存的 buffer 總要有個上限.
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|联系我们|极客工坊

GMT+8, 2026-6-8 18:14 , Processed in 0.037639 second(s), 21 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表