Arduino菜鳥通俗版解讀系列(4)串口通信

Arduino菜鳥通俗版解讀系列(4)串口通信

4 人贊了文章

v.youku.com/v_show/id_X

這一篇來講一個重要的知識點---串口通信。上面的視頻是一個用到串口通信的例子,視頻中的MPU6050慣性測量單元和Arduino之間就是通過SPI串口通信進行數據傳輸的,另外,電腦對於Arduino的監控也是通過USB的串口通信進行監控的。

由於串口通信個人感覺內容比較抽象,不知道如何講起,所以打算從以下3方面來講解:

1)串口通信是幹什麼的?為什麼很重要?串口通信的種類?

2)Arduino上USB端的串口通信是怎麼工作的?

3)如何實現遙控?

其中1)和2)部分會在這一篇講解,3)遙控由於涉及到專門的無線串口晶元NRF24L01,所以會在下一講進行。

1)串口通信是幹什麼的?為什麼很重要?串口通信的種類?

串口通信是用來在不同電子設備之間交換數據用的技術,其實就是要實現不同電子設備之間的「通訊對話」。試想在第2篇中講到的LED小燈,如果我要通過電腦,實時傳送一個亮度控制參數給Arduino,怎麼實現呢?在之前幾篇的講解中,亮度控制參數都是直接寫到程序中,然後隨著程序燒入Arduino實現的。這就意味著我沒有辦法隨時隨地地去改變LED燈的亮度參數,因為如果我想改變這個亮度參數,我就需要在程序中進行修改並重新燒錄程序進入Arduino。

那怎麼才能夠通過Arduino「實時」地去控制LED小燈的亮度呢?想實現「實時」功能當然就得建立一個和Arduino的通信通道,這樣才能隨時隨地的將我們的意圖傳達給Arduino,並讓Arduino來執行。在此,我們通過電腦作為我們的指令發送裝置,所以我們要在電腦和Arduino之間建立起這樣一個通信通道,這樣的通信通道就是串口通信。

有了上面一段所講的基本想法,那麼具體到程序裡面的形式又是怎樣的呢?基本邏輯是我們在程序里預留一個變數,例如val,這個變數val用來存儲LED燈的亮度值,在第一次燒錄程序的時候我們並不給val賦上具體的值,而是通過後續的串口通信,把這個值傳送給val,然後讓Arduino直接用val來控制LED燈,隨著我們通過電腦串口通信不斷改變 val的值,從而起到實時控制Arduino的效果。具體程序可見圖1:

圖1

圖1中的程序用於點亮一個LED小燈,其中的變數val用來控制LED燈的亮度,可以看到在程序開頭並沒有對val賦值;那麼什麼時候賦的值呢?就是步驟1和步驟2。步驟1的作用是讀取串口寄存器中的信息(這個串口寄存器就是串口通信數據的暫存地,我們通過計算機輸出的信息將會先到達串口寄存器,然後我們再從串口寄存器中轉移信息到Arduino的內存),並將這個信息存儲入A數組;然後在步驟2中,將A數組中的信息轉換成10進位的阿拉伯數字,並賦值給val。通過步驟1 和步驟2我們就完成了將信息從電腦傳送到Arduino內存這樣一個過程。好像明白了一些對嗎?別急,這裡面有兩個重要點需要說一下:第一,在前面我們直接就開始讀取串口寄存器中的數據了,但是計算機的信息到底是怎麼通過USB線傳送到串口寄存器的?數據在串口寄存器里又是怎麼分布的?這是個很重要的問題,明白了這個問題也就明白了為什麼我們需要在圖1中的程序里加上delay(100)這條命令,如果不加這條命令我們將得到完全錯誤的結果;第二,為什麼我們要加上val=strtol(A,NULL,10)這條命令?這條命令也非常重要。 OK,對於第一個問題,我們將在講解「Arduino上USB端的串口通信是怎麼工作的?」時給出答案,現在先講講為什麼要加上val=strtol(A,NULL,10)這條命令?先說一下這條命令的功能:即將A信息轉換成10進位的阿拉伯數字,然後賦值給val 。好,繼續講,還記得第一篇中我們講到Arduino的編程環境中有一個按鈕,叫做「串口通信監視窗口」嗎?,見圖2。

圖2

圖2中,點擊右上角的按鈕可打開如左側的對話框,這個對話框中可以輸入你想傳送給Arduino的信息(注意:一定要將對話框下面的波特率與右側程序中設置的波特率保持一致),在這裡我們想傳送100這個數值給到Arduino。但是!在串口對話框中輸入的任何信息都是字元型的信息,包括數字信息在內,都是以字元型存在的。所以在圖2對話框中的100其實是三個字元,即『1』,『0』,『0』。只不過它們看上去像阿拉伯數字100,但是它們絕對不能夠直接用於數學運算。所以在程序中,我們需要增加一條命令來將字元信息轉換成數字信息,然後才能傳給val用以控制LED燈亮度。所以我們需要val=strtol(A,NULL,10)這條命令,來完成字元向數字的轉變。講到這裡我們應該大致了解到串口通信是幹什麼的了,簡單說就是用來讓Arduino和電腦或者其他晶元進行溝通的,所以串口通信很重要,如果沒有串口通信,Arduino就成了啞巴和聾子,只能自己和自己玩(比如前面講的LED燈項目,沒有用到串口通信,所以其實只是一個封閉的項目),無法通過和外界溝通信息來完成更複雜的功能了。

接著說串口通信的種類,本人所知道的有兩類:I2C和SPI。這是兩種不同的串口通信協議前者是飛利浦公司發明的,後者是摩托羅拉公司發明的。從表面上來區別的話,I2C需要的傳送線數量少,SPI需要的傳送線多(SPI接線可見下文中圖5);但是I2C不能夠同時接受和發送信息,而SPI可以同時接受信息和發送信息。兩者好像也沒有誰好誰壞,也都很常見,有的晶元甚至同時支持兩種模式。不過在這一篇中,我們講SPI,因為Arduino和電腦之間的串口通信是SPI類型。

2)Arduino上USB端的串口通信是怎麼工作的?

圖3

這是本講的核心內容。首先看一下圖3。圖3中表示的場景是一台電腦正在通過USB串口通信傳送信息給Arduino。在Arduino的USB介面內部有一個串口寄存器,它是用來暫時存放電腦傳過來的信息的(之後Arduino會根據開發者的程序,從串口寄存器中提取數據,保存到Arduino內存中),在和電腦通過USB串口通信時,默認分配給Arduino UNO的串口寄存器空間可存放63幀的信息(注意是Arduino UNO,如果是Arduino其他類型的板子可能這個空間大小會不一樣)。注意圖中電腦連接線上的一個個小點,每一個小點代表一幀信息,電腦就是這樣像圖3中所示一幀一幀地傳送信息的。由於串口寄存器空間默認為63幀,所以如果數據很多,我又沒能及時讀取並清空寄存器中的信息的話,後來的信息會覆蓋之前的信息,造成信息丟失。這個該怎麼理解呢?首先講一下什麼是一幀信息?一幀你可以理解為一個數據包,這個數據包包含若干位元組的內容,如圖4所示。

圖4

一幀信息中第一個位元組代表幀頭,最後一個位元組代表幀尾。幀頭和幀尾是用來分辨一個完整幀信息的,假如沒有幀頭,Arduino將無法知道這一幀信息從何處開始,沒有幀尾的話Arduino將無法知道這一幀信息在何處結束,在幀頭和幀尾之間就是我們真正傳送的信息,每一幀信息的大小在不同的串口設備上是不同的,例如在Arduino和電腦通信的USB串口上,一幀等於3個位元組(幀頭,信息,幀尾),而在MPU6050這款慣性測量單元晶元上,一幀等於10個位元組,所以具體一幀多大要看你買的晶元的設置,這些信息在購買時賣家提供的網上資料里都會有說明。上面講了幀頭和幀尾以及一幀信息的構成,為什麼要這樣構成呢?為什麼要加上所謂的幀頭和幀尾呢?這其實也可以通過一個比喻來形象地說明:如圖4中繪製的一群小人。這一群小人排隊通過一個關卡,關卡有個管理員,這個管理員就相當於Arduino的處理器,而這一群小人就相當於從電腦端發送過來的信息。假設這些小人是從很遠的一個營地跑過來的,那麼營地就相當於一台電腦,他們在管理員面前暫時等待的區域就相當於串口通信寄存器。現在管理員要指揮這一排小人有秩序地通過關卡,這就相當於Arduino處理器要依次讀取寄存器中的信息。好,我們可以看到每一組小人有一個排頭兵和一個排尾兵,那麼這個管理員會通過識別排頭兵和排尾兵從而有秩序地讓小人一隊隊地通過,每一隊小人就相當於一幀信息,而管理員的通過規則是:每次看到排頭兵就開始放行,一直到發現排尾兵為止則停止通行;然後隔一小會兒再讓下一隊通過。那麼按照我們的常識就會知道,如果管理員放行的速率太慢,那麼由於不斷有後續的小人從營地跑過來,這個暫時等待的地方就會裝不下這麼多小人,這時候後面趕來的小人就會擠兌前面的小人,也就相當於剛才我們說的:後來的信息會覆蓋前面的信息,造成信息丟失。

那麼這種由於放行速度過慢而導致信息被覆蓋丟失的情況會出現在串口通信中嗎?一般不會出現的,因為Arduino的主頻有16M,也就是每秒運算160萬次;而串口通信的速度(也就是前面講到的波特率)最高一般用到115200,也就是每秒115200個比特,約為14400個位元組。這樣看來當後序的信息傳送過來的時候,前面的信息早就被Arduino讀取完畢了,所以不會出現擠兌的情況。在此我給出一個烏龜賽跑的概念:假如你把前面例子中的管理員換做你自己,一隊小人換作一隻烏龜,那麼串口傳輸信息的速度和Arduino運算的速度相比,相當於一排烏龜排隊衝刺,然後你拿著一個鉗子捉烏龜,每當一隻烏龜到達你面前你就抓走一隻;就算烏龜再努力地奔跑,對於你而言都是很慢的,所以不會出現烏龜來的太快導致你來不及抓而產生擁堵

OK,上面講了Arduino和電腦之間進行串口通信時,信息是以一幀一幀的形式來傳送的,下面來看看這些信息在抵達Arduino的USB埠後是怎麼進入到Arduino內部的。在圖1中的程序里我們只是直接告訴你用Serial.read()命令就可以讀取,但是其中的物理過程是怎麼樣的呢?先看一下圖5,圖5顯示了電腦和Arduino之間串口通信時的接線方式,當然這些接線都集成在USB線內部,我們看到的只是一根完整的USB線。

圖5

圖5中可見,電腦和Arduino之間的串口通信都需要一個專門的串口通信晶元(USB兩端是否分別集成一個串口通信晶元不確定,但是可以這樣理解,假定就是通過兩端的串口晶元進行的串口通信),這兩塊串口晶元各自都有4個介面:5V,TX,RX,GND。兩塊晶元的5V口和GND口要接到一起,5V和GND這兩個介面的作用是給晶元供電。TX介面是發送口,就是信息從這個口發送出去,RX介面是接受口,就是信息從這個口接收進來。所以對於電腦而言,電腦的TX口應該接Arduino的RX口,電腦的RX口應該接Arduino的TX口。另外提一句:在有的資料里不使用TX和RX這種寫法,而是採用MOSI和MISO這樣的寫法,MOSI的意思就是master out slave in,MISO的意思就是master in slave out,從字面上理解MOSI就是主機輸出從機接收,MISO就是主機接收從機輸出。所以圖5中,電腦端的TX和Arduino端的RX其實也可以分別表達為MOSI,而電腦端的RX和Arduino端的TX其實也可以分別表達為MISO。

了解完接線形式後,現在假設我們從電腦上發送了三個幀的數據給到Arduino,這三個幀的內容分別是a,b,c。那麼這三個字母是怎麼進入到Arduino內部的呢?看圖6。

圖6

圖6中左側給出的是對應的程序,右側給出的是Arduino內部的物理過程。

首先,右側中步驟1至步驟4這段時間內,信息一幀一幀地進入串口寄存器中,假如我們想要一次性讀取所有的信息,那麼在步驟1到步驟4的這段時間內我們不能夠對信息進行讀取,因為這個時候信息還沒有完全傳送完畢,也就是說所有信息沒有全部進入寄存器。假如我們在圖6中的步驟2就開始讀取寄存器中的信息,那麼我們將只能獲得a這個信息。這就是前面講到的「烏龜理論」,對於Arduino來講,串口信息的傳送是很慢的,雖然串口通信1秒能傳送上萬個位元組,對於我們人類來說是一眨眼的功夫,但是對於Arduino這個1秒鐘運算160萬次的晶元來說,這就是烏龜爬行的速度,所以我們必須在開頭給Arduino設置一個等待時間,這個等待時間一般就設定100ms即可,這就是為什麼要加上delay(100)這條命令的原因。

然後,當等待時間結束,這時候我們認為信息已經完全傳送完畢。當然,說實話這都是靠猜,我們估計100ms信息傳送完畢了,可能信息早在10ms 的時候就傳送完畢了,不過沒關係,為了保險我們寧可多等待一會兒,反正是毫秒級的時間損失,對於我們來說多等幾十毫秒根本感覺不出來,當然你覺得有把握的話也可以把等待時間縮短一些,比如50ms甚至20ms都可以,你可以嘗試著用,假如沒有出現信息丟失那你完全可以用更短的等待時間。信息完全傳送完畢後,我們才開始要把信息從串口寄存器中都讀取出來了,也即是步驟5到步驟7的過程。注意,圖6中的黑線指向的那個「大箭頭」是Arduino內部的一個指針,這個指針時刻監測著串口寄存器,我們隨時可以使用命令Serial.available()來調動這個指針,獲取傳入信息的數量,當然如我們所說,要等信息全部傳送完畢後再開始調用,假如我們在步驟2就調用Serial.available()的話,我們只能探測到一個數據。而在圖6中的步驟4我們可以看到,總共有三個信息傳入了寄存器,所以這個時候調用Serial.available()獲得的值是3,然後我們把這個值賦給變數j,於是變數j就代表了寄存器中信息的數量。

最後,還是這個指針,我們可以使用命令Serial.read()來調動這個指針來抓取數據,注意Serial.read()每次可以抓取一幀的信息,每次抓取走信息後,寄存器中原本存放這個信息的空間就空了出來,我的意思是:Serial.read()命令不是複製信息而是剪切信息。好,回過頭來說,3個信息每次抓取1個,那麼為了抓取寄存器中的所有信息,我們需要抓取3次,於是我們採用了一個 for循環,循環數設定為j,這樣就可以自動抓取所有寄存器中的信息了。每次抓取的數據我們將會存儲到數組A中,A是我們在程序開頭就定義好的一個字元型數組。關於A數組要注意兩點:1)必須定義為字元型數組,因為我們講過電腦串口輸入的所有信息都是字元形式傳輸的,就算是阿拉伯數字也只是一個字元,而不是真的阿拉伯數字;2)A數組的空間大小在定義的時候要給足,假如我們打算每次處理1幀的數據,那麼A的空間就要給到1幀或更多;假如我們每次打算處理3幀的數據,那麼A的空間就要大於等於3幀。然後我們把「幀」換算成位元組來定義A的空間。關於「幀」和位元組的關係本篇前半部分有講。

以上就是串口通信的的原理和內部傳輸過程,本篇著重講的是一台電腦和一個Arduino之間一對一的串口通信,如果涉及到多個設備間同時進行串口通信,就要涉及到一個「使能」口來選擇通信對象,這裡先不講了,基本原理和過程是一樣的。需要多說一句的是,通常我們購買一些涉及到串口通信的晶元時,買家都會提供一些資料,這些資料里往往會有適用於這款晶元的「串口通信庫」或者現成的串口通信程序,所以我們也不需要完全自己去編寫串口通信的程序,但是本篇講的通信原理是一定要了解的。


推薦閱讀:

Leetcodes Solution 28 Implement strStr()
Python 之禪 (知乎上目前最好的翻譯版本)
Python3 函數03
深入理解計算機系統(二十):數組分配和訪問
【案例】三菱FX系列PLC結構化編程實例(3)

TAG:編程 | Arduino | 嵌入式系統 |