今天,我們將討論arduino通訊協定的有關內容。設備往往需要相互通訊以中繼所處環境相關資訊,顯示其狀態變化,或請求執行輔助操作。在進行任何電子設備愛好相關研究時,您將需要使用多個不同的感測器或多個不同的模組(如ESP8266),屆時必然會遇到一個或多個主流的通訊協議。在本教學中,我們將會介紹電子設備使用的標準通訊協定,並使用Arduino Uno對其進行詳細說明。
設備間的通訊透過數位信號進行。在討論通訊協議之前,我們將首先討論如何傳輸這些信號。
在數位信號中,資料由一系列的高電平到低電平 或 低電平到高電平的脈衝進行傳輸,並且切換非常迅速。數位信號中的高電平和低電平分別代表1和0,當按照順序連在一起時,就攜帶了可由微控制器編譯的資訊。聽說過位元和位元組嗎?這些1和0是位,當它們以8個為一組時,就稱之為位位元組!
一個位元組看起來大概是這樣:10111001
事實證明,這個由8位組成的序列代表了一個數字,就像597這個數字代表了597一樣。每個數字佔據了一個位置值,該位置值中的1或者0表示該位置值被計數了多少次。
在597的範例中,5表示有5個100,9表示有9個10,7表示有7個1。加在一起,就表示5個100 + 9個10 + 7個1 (500 + 90 + 7)…或597。因為1 是 100,10 是 101,100 是 102等等,因此這叫做基於10的系統!在基於10的系統中,每個數位的取值範圍為0到9(0到10-1)。
現在我們再來看10111001。我們可以看出這是基於2的系統,因為每個數位的取值範圍為0到1。我們可以說每個數字都是2的冪,這意味著10111001實際為1 * 27 + 0 * 26 + 1 * 25 + 1 * 24 + 1 * 23 + 0 * 22 + 0 * 21 + 1 * 20,或者說為基於10的系統中的185。
可以想像,您可以擁有基於任何數位的數位系統!常見的一些數字為2、8、10和16。為了簡單起見,數學家為這些常見的數位系統命了名——基於2的為二進位,基於8的為八進制,基於10的為十進位,基於16的為十六進位。每個數位系統都遵循相同的原理:每個數字代表該基數的冪被計數的次數,並且每個數字的值都只能在0到(基數-1)之間。
瞭解不同的基數系統很有用,因為位元組和資料通常以不同的方式進行表達。可以看出,寫B9(十六進位)比寫10111001(二進位)容易。在軟體中,二進位數字以0b作為首碼,八進位數以0作為首碼,十六進位數以0x作為首碼。十進位數字沒有首碼。
知道如何在基數之間進行轉換也很有用,因為一些很酷的數學技巧通常用二進位數字進行表達。但是,出於本教學的目的,我們僅講述到這裡。請查看以下本教學的附錄來獲取有關這些技巧的文章!
電氣工程行業使用三種通訊協定對電子設備進行標準化,以確保設備之間的相容性。將設備以幾種協定為中心進行標準化,意味著設計者可以透過掌握每個通訊協議的一些基本概念實現任何設備之間的交互。UART,SPI和I2C這三種協定的實現方式不同,但都會達到相同的目的:將資料高速傳輸到任何相容的設備上。
1. UART
我們將要介紹的第一個通訊協定是通用非同步收發器(UART)。UART是一種串列通訊,因為資料是一位元一位元依次進行傳輸的(我們將在稍後介紹)。設置UART通訊的接線非常簡單:一根用於傳輸資料(TX)的線,另一根用於接收資料(RX)的線。如您所料,TX線用於發送資料,RX線用於接收資料。使用串列通訊的設備的TX線和RX線將一起形成一個序列埠,透過該埠可以進行通訊。
UART一詞實際上是指管理串列資料打包和轉換的板載硬體。如果希望設備能夠透過UART協定進行通訊,就必須具有該硬體!在 Arduino Uno上,有一個專用於與Arduino所連接電腦之間進行通訊的序列埠。對!通用序列匯流排USB正是一個序列埠!在Arduino Uno上,USB的連接透過板載硬體設定為成兩個數位引腳GPIO 0 和 GPIO 1,可用於涉及與電腦以外的電子設備進行串列通訊的專案。
您還可以使用SoftwareSerial Arduino 庫(SoftwareSerial.h)將其他GPIO引腳用作串口RX和TX線。
UART之所以成為非同步,是因為不使用試圖相互通訊的兩個設備之間的同步時鐘信號進行通訊。由於通訊速率不是透過這種穩定信號定義的,“發送方”設備無法確定“接收方”設備是否獲取了正確的資料。因此,設備將資料分成了固定大小的塊,以確保接收到的資料與發送的資料相同。
UART資料包如下所示:
透過UART進行通訊的設備會發送預定義大小的資料包,其中包含有關消息的開始與結束,以及確認消息是否接收的附加資訊。例如,為了開始通訊,發送設備將發送線拉低,指示資料包開始發送。然而,目前遇到的問題是,與同步通訊方式相比,UART速度較慢,因為傳輸的資料只有一部分用於設備的應用程式(其餘部分用於通訊本身!)。
在大多數嵌入式平臺(例如Arduino)上實現UART串列通訊時,使用者不用在位的資料級別上進行通訊,平臺通常提供更高級別的軟體庫,用戶只需在這些軟體庫中處理通訊內容即可。在Arduino平臺上,用戶可以使用Serial 和 SoftwareSerial庫為自己的項目實現UART通訊。
以下是有關Arduino Serial和SoftwareSerial的初始化和使用的C++簡要參考。
Serial 和 SoftwareSerial 方法(method) | 目的 | 代碼 | 釋義 |
Constructor (僅SoftwareSerial) | 定義GPIO引腳為UART RX線和TX線。 | SoftwareSerial comms (2 , 3); | 定義GPIO 2 上的RX線與GPIO 3上的TX線為串列連接 |
begin | 定義串列連接的串列傳輸速率(傳送速率)在範圍4800~115200之間 | comms.begin(9600); | “comms”序列埠上的通訊將以9600串列傳輸速率的速度進行 |
透過串列連接將位元組資料轉換為可閱讀的字元 | comms.println(“Hello World”); | 寫入等效於Hello World (可讀字元)的位元組 | |
write | 透過串列連接寫入原始位元組資料 | comms.write(45); | 寫入值為45的位元組 |
available | 當資料可透過串列連接獲取時評估為真(true) | if (comms.available()) | 如果可獲取透過串列連接讀取的資料,執行if語句 |
read | 讀取從串列連接獲得的資料 | comms.read(); | 從串列連接讀取資料 |
有關UART通訊,有一個重要的點需要注意,該通訊協定下一次只能實現兩個設備之間的通訊。因為該協定僅發送指示消息的開始、消息內容和消息的結束的位元,所以沒有辦法區分同一條線路上的多個發送和接收設備。如果有多個設備嘗試在同一條線路上傳輸資料,則會發生匯流排爭用,並且接收設備很可能會接收無法使用的垃圾資料!
此外,UART是半雙工,這意味著即使可以在兩個方向上進行通訊,兩個設備也無法同一時間相互傳輸資料。例如,在一個項目中,兩個Arduino透過串列連接相互通訊,這意味著在給定的時刻,只有其中一個Arduino可以與另一個“交流”。對於大多數應用來說,這一特點相對來說並不重要,並且不會產生不利影響。
2. SPI
我們將介紹的下一個通訊協定是串列週邊設備介面(SPI)。SPI與UART主要有以下不同點:
SPI的硬體連接圖稍微複雜一些,看起來像這樣:
SPI的第一個特點是遵循主從模型。這意味著通訊中將會有一個設備為主設備,而其他設備為從設備。在該模式下,會在設備之間創建層次結構,從而顯示出哪個設備有效地“控制”了其他設備。我們會在闡述一個主從設備之間的通訊範例時簡單地討論一下此主從模型。
前面我們提到,多個從設備可以連接到一個主設備。這種系統的硬體圖如下所示:
SPI不需要為連接到主設備的每個從設備提供單獨的發送線和接收線。在所有從設備和主設備之間連接了一條公共接收線(MISO)和一條公共發送線(MOSI),以及一條公共時鐘線(SCK)。主設備透過每個從設備分別配置的SS線來決定將與哪個從設備進行通訊。這意味著每增加一個與主設備通訊的從設備,都需要在主設備一側再使用一個GPIO引腳。
SPI是同步的,也就是說主設備和從設備之間的通訊與主設備定義的時鐘信號(固定頻率的方波)緊密相關。從這裡我們可以看出主從模型的直接影響之一,即主設備透過時鐘信號指定通訊速率來驅動通訊,而從設備在該速率下進行通訊來回應主設備。所定義的速率適用於主設備所主導的任何通訊過程(在從設備可以承受的最大速率範圍內)。
在SPI中,時鐘信號的兩個特徵決定了資料傳輸的開始和結束:時鐘極性(CPOL)和時鐘相位(CPHA)。CPOL是指時鐘信號的空閒狀態(低電平或高電平)。為了節省功耗,設備在不與任何從設備通訊時會將時鐘線置於空閒狀態,並且在該空閒狀態下可用的兩個選項為低電平或高電平。CPHA是指時鐘信號的跳變沿,決定何時對資料進行採樣。方波有兩種跳變沿(上升沿和下降沿),並且根據CPHA設置,可以對上升沿或下降沿進行採樣。
CPOL和CPHA有四種不同的組合方式,如下表所示:
CPHA = 0
(時鐘信號的“第一種跳變沿”) |
CPHA = 1
(時鐘信號的“第二種跳變沿”) |
|
CPOL = 0
(空閒狀態為0) |
|
|
CPOL = 1
(空閒狀態為 1) |
|
|
維琪百科以圖形方式很好地表示出了CPOL和CPHA的設置與其所傳輸資料的關係!
現在,我們將重點介紹使用Arduino作為主設備(SPI.h)在Arduino上實現SPI的方法。SCK、MOSI和MISO的SPI數位引腳連接要在Arduino開發板上進行預定義。對於Arduino Uno,連接如下:
任何數位引腳都可以作為SS引腳。為了選擇設備,該數位引腳必須被驅動為低電平。
以下是有關Arduino SPI初始化和使用的C++簡要參考。
SPI 方法 | 目的 | 代碼 | 釋義 |
Constructor | 定義時脈速率、資料位元順序(最高有效位元在前或最低有效位在前)以及SPI模式 | SPI.beginTransaction (SPISettings(14000000, MSBFIRST, SPI_MODE0)); |
在SPI模式0定義14MHz下的SPI連接,並以最高有效位元在前的方式進行資料傳輸 |
digitalWrite | 選擇連接到該GPIO引腳的從設備 | digitalWrite(10, LOW); | 將GPIO引腳驅動為低電平以選擇從設備。然後將引腳驅動為高電平以取消選擇從設備。 |
transfer | 將位元組傳送到所選擇的從設備 | SPI.transfer(0x00); | 發送值為0的位元組 |
endTransaction | 結束SPI程式(應在SS線上調用digitalWrite(high)之後調用) | SPI.endTransaction(); | 結束SPI程式 |
SPI是全雙工的,這意味著即使在應用中只要求在一個方向上進行資料傳輸,通訊也始種是雙向進行的。SPI全雙工資料傳輸通常透過移位暫存器來實現。(請參閱附錄中有關移位暫存器的教學!)。這表明在讀取一個位時,前面的位將被移動一位,隨後,最前邊的位將會被觸發,並透過SPI連接發送到另一台設備上!
3. I2C
內部積體電路匯流排(I2C)的發音為“I方C”,是我們在本教學中介紹的最後一個通訊協議。雖然該協定的實現是三種協議中最複雜的,但是I2C解決了其他通訊協議中存在的一些問題,使其在某些應用程式中比其他通訊協定更具有優勢。這些優勢包括:
從硬體層面來說,I2C是一種兩線介面—I2C連接中僅需要的兩根線是資料線(成為SDA)和時鐘線(稱為SCL)。資料線和時鐘線在空閒狀態下被拉高,而在需要透過連接發送資料時,這些線會透過一些MOSFET電路被拉低。在本教學中,我們將不討論I2C電路內部的MOSFET操作,這裡需要說明的一個重要點是該系統是漏極開路的,即線路只能由設備驅動為低電平。因此,在項目中使用I2C時,上拉電阻(通常為4.7kΩ)這一步至關重要,以確保在空閒狀態下線路確實被拉高。
I2C具有其獨特性,因為它透過定址解決了與多個從設備之間的介面問題。與SPI通訊一樣, I2C利用主從模型建立了通訊的“層次結構”。但是,主設備不是透過單獨的數位線路選擇從設備,而是透過主設備中具有唯一性的位元組位址來選擇從設備,這種位元組位址大概類似於這樣:0x1B。這意味著將從設備連接到主設備不再需要添加數位線路。只要每個從設備都有唯一性的位址,應用程式就可以區分這些位址所對應的從設備。您可以將這些位址視為名稱。要調用從設備的功能,主設備僅調用其名稱即可,而只有具有該名稱的從設備會發生回應。
I2C通訊線路中的位址和對應資料看起來像下圖這樣。
請注意通訊線上的ACK 和 NACK位。這些位元表示被定址的從設備是否回應通訊—這是一種定期檢查通訊是否按照預期進行的方法。這些位當然與發送的位址位或資料位元無關,但是在複雜的通訊體系中,與包含許多開始和結束位並會發生停頓的類似UART這樣的協議相比,它們只增加了非常少的額外時間而已。
I2C 使從設備可以自由決定通訊請求的方式。向不同的從設備寫入或發出請求需要在SDA線以不同的順序寫入不同的位元組。例如,在某些加速計模組中,在讀取請求發送之前,需要寫入指示主設備所要讀取的硬體寄存器的位元組。對於這些規格,使用者需要參考從設備資料手冊中的設備位址、寄存器位址和設備設置。
在Arduino上,I2C 透過Wire庫(Wire.h)實現應用。Arduino可以配置為一個I2C主設備或從設備。在Arduino Uno上的連接如下所示:
以下是有關 Arduino I2C 初始化和使用的C++簡要參考。
I2C (線) Method | 目的 | 代碼 | 釋義 |
begin | 啟動庫,並以主設備或從設備的身份加入I2C匯流排。 | Wire.begin(); | 以主設備的身份加入I2C匯流排。如果將位址指定為方法的參數,則Arduino將以該位址作為從設備加入匯流排。 |
beginTransmission | 對於配置為I2C主設備的Arduino:啟動與具有給定位址的從設備之間的傳輸。 | Wire.beginTransmission(0x68); | 開始對具有十六進位位址Ox68的從設備進行傳輸。 |
write | 透過I2C匯流排寫入位元組資料。 | Wire.write(0x6B); | 透過I2C匯流排寫入值Ox68的位元組資料。 |
requestFrom | 向具有給定位址的從設備請求指定值的位元組;可以選擇釋放I2C線或將其保留以進行進一步的通訊。
所請求的位元組被放入緩衝區,隨後透過Wire.read()調用讀取。 |
Wire.requestFrom(0x68, 6, true); | 向位址為Ox68的從設備請求6個位元組。請求完成後釋放I2C線。
如果最後一個參數為false,則Arduino將保留I2C線以進行進一步的通訊,而不允許其他設備透過該線進行通訊。 |
read | 從設備發送資料後,讀取放入緩衝區的位元組。
該方法將在調用 Wire.requestFrom之後調用。 |
Wire.read(); | 從緩衝區讀取一個位元組(在調用requestFrom之後)。要從緩衝區讀取兩個位元組,必須兩次調用該方法。
例: Wire.requestFrom(0x68, 2, true); Wire.read(); Wire.read(); |
endTransmission | 結束當前傳輸;可以選擇釋放I2C線或將其保留以進行進一步的通訊。 | Wire.endTransmission(true); | 結束傳輸並釋放I2C線。 |
只需將SDA和SCL線連接到匯流排上,就可以透過I2C匯流排連接多個主設備。但是,一次只能有一個主設備與從設備通訊,因為讓多個設備進行相互通訊會導致匯流排爭用。同樣,不能同時進行主設備到從設備 和 從設備到主設備之間的雙向通訊,因為這也會導致匯流排爭用。這使得I2C 變成了半雙工,就像UART那樣!
總之,多個主設備無法透過同一個I2C 匯流排實現相互通訊。在將多個主設備連接到從設備的應用中,主設備可以透過單獨的匯流排或單獨的通訊協定實現相互之間的通訊。
如果您學完了本教學,那麼應該已經擁有所有使用UART、SPI和I2C 通訊協定所需的工具了!查看我們的一些Arduino專案,可以獲取有關使用這些協定的更多範例!
附錄
C語言中的按位元操作:http://www.cprogramming.com/tutorial/bitwise_operators.html
移位暫存器:https://learn.sparkfun.com/tutorials/shift-registers