2015年8月1日 星期六

Arduino 之間的 I2C 通訊 (5) master 向 slave 要求不同資料

相關指令:

指令發出耆作用
Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
Wire.beginTransmission(<address>);master開始對 <address> 的連線
Wire.endTransmission();master 關閉之前的連線
Wire.requestFrom(<address>,<len>);master在線上向拍定地址發出回傳 <len> bytes 資料的請求
Wire.available();master檢查連線上是否有可接收的資料
Wire.read();master / slave讀取連線上的一個 byte 的資料
Wire.write(<array>,<size>);slave / slave在連線上送出 <size>個 byte 的資料
Wire.onReceive(<function>)slave 設定用來接收資料的函數
Wire.onRequest(<function>)slave 設定函數用來回應線上的請求

這個可以說是之前的一個初步小總結了.

從 (4) 已經說明了 slave 如果把資料回傳, 但大家可能會發覺, 某些 i2c device, 是可以選擇不同的資料的, 在處理請求的函數中, 如何得知 master 想要什麼?

對, 在 request 之中, 是沒有任何有關請求的資料提供的, 就只是一個空白的請求.

但就像 MPU6050 之類的模塊, 不是可以選擇不同的數據嗎?
如果大家有用過 I2CDev 的庫, 或許會發覺 readByte 的函數, 不是有一個 regAddr, 向 slave 要求不同的數據嗎?

    static int8_t readByte(uint8_t devAddr, uint8_t regAddr, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);

大家有看過之前 (1) - (4) 的話, 難道想不出來嗎?  多思考一下, 把所知道的結合起來.

對了, 說穿了就只是把兩個工序結合起來.

  • master 先向 slave 發送所需資料的指令
  • slave 收到後, 先記下來, 不作處理
  • master 再向 slave 發出傳送資料的請求
  • slave 收到請求後, 先看看 master 剛發過來有關所需資料的指令, 再把相關資料回傳

是否很簡單呢?

master 向 slave 選擇所需資料

明白了原理, 步驟也很簡單, 就是把 (2) + (4) 結合:

  1. 執行 beginTransmission 並指定接收指令的地址
  2. 以 Wire.write 把所需資料的指令發送出去
  3. 執行 endTransmission 完成指令發送
  4. 執行 beginTransmission 並指定連線地址
  5. 以 Wire.requestFrom 發出請求
  6. 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出
  7. 執行 endTransmission 作結束

當中 3. 4. 是否有必要, 我也不肯定.  但為了把 發送 / 接收 可獨立一點, 先把發送完結或者會好一點.  而且, 3. 4. 之後, 有需要或許要加入一點 delay, 好讓 slave 處理資料.  當然, 看實際情況決定, 並不是必須的.

以下是簡單的例子, master 發出請求時, 會先指定所需資料.


#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 
#define DATA_SIZE 8
 
void setup()
{
  Wire.begin();
 
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Master.05 started");
  Serial.println();
}
 
 
void loop()
{
  if (Serial.available()) {
    
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(Serial.read());
    delay(1);
    Wire.endTransmission();

    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
    if (Wire.available()) {
      Serial.print("Data returned: ");
      while (Wire.available()) Serial.print((char) Wire.read());
      Serial.println();
    }
    Wire.endTransmission();
    while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave

  }
}


salve 向 master 回傳所需資料

跟 master 一樣, 所需步驟, 同樣是把 (3) + (4) 結合起來就可以了.


  • 執行 Wire.onReceive 去設定接收資料的函數
  • 執行 Wire.onRequest 去設定處理請求的函數
  • 在 onRecevie 函數中, 以 Wire.available 檢查是否有資料需要處理.  
  • 要有複雜的程序, 先記下來, 留待主程式去做
  • 在主程序中檢查是否有 接收資料待處理
  • 在 onRequest 函數中, 跟據之前 onReceive 收到的指令, 以 Wire.write 把 所需資料回傳


但這裡有一點要留意, 之前沒提及的.  就是當 master 發出 requestFrom 時, slave 是會觸發一次 onReceive 而當中是沒有資料的.  所以在 onReceive 當中, 必須要先檢查 Wire.available.

以下是一個簡單例子, master 可以選擇 2 種資料, 而回傳結果如下:
0 - 回傳 "Super169"
1 - 回傳 "Apple II"
其他 - 回傳 "WhoAmI"


#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 

uint8_t dataMode = '0';
boolean modeChanged = false;

void setup() {
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onReceive(receiveEvent); // register event
  Wire.onRequest(requestEvent); // register event
 
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Slave.05 started\n");
}

void loop() {
  if (modeChanged) {
    Serial.print("Change data mode to ");
    Serial.println((char) dataMode);
    modeChanged = false;
  }
}

void receiveEvent(int count) {
  if (Wire.available()) {
    dataMode = Wire.read();
    modeChanged = true;
    while (Wire.available()) Wire.read();
  }
}

void requestEvent()
{
  switch (dataMode) {
    case '0':
      Wire.write("Super169",8);
      break;
    case '1':
      Wire.write("Apple II",8);
      break;
    default:
      Wire.write("Who am I",8);
  }
}


額外測試項目

大家記得之前 (2) 時提及, 不要把 Serial.print 放在 receiveEvent 當中嗎?
在之前的例子, 由於只要 receiveEvent 在工作, 不會有任何影響.
但在今次的例子中, 大家看到為了把 data mode 的改變顯示出來, Serial.print 的部份放到主程式內.
有興趣的朋友可以試試把它修改一下, 放回 receiveEvent 內, 看看會有什麼結果.


相關程式下載:


Arduino 之間的 I2C 通訊 (4) 由 master 向 slave 要求資料回傳

相關指令:

指令發出耆作用
Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
Wire.beginTransmission(<address>);master開始對 <address> 的連線
Wire.endTransmission();master 關閉之前的連線
Wire.requestFrom(<address>,<len>);master在線上向拍定地址發出回傳 <len> bytes 資料的請求
Wire.available();master檢查連線上是否有可接收的資料
Wire.read();master讀取連線上的一個 byte 的資料
Wire.onRequest(<function>)slave 設定函數用來回應線上的請求
Wire.write(<array>,<size>);slave在連線上送出 <size>個 byte 的資料

之前已經試過由 master 向 slave 發送資料, 但對於一些像 傳感器的設備, 反而是需要由 slave 把資料回傳的.  有些傳感器可能需要一段時間去讀取資料 (例如 PM2.5 收集 30 秒數據), 如果在主控中執行, 可能會影響其他工序.  如果加一片 arduino 板子, 以 slave 形式去讀取資料, 當 master 發出請求時, 就可以直接把最後的結果回傳, master 就不用花太多時間了.  再者, 把程序分開後, 每個程序可獨立除錯, 而主程式亦變得簡單了.  就如 PM2.5 的例子, 把當中讀取及計算的部份後 master 中抽走, 就不用考慮因為要讀取連續數據而影響其他程序的問題了.

由於 slave 是不能主動發動連線, 所以只可以等待 master 的請求, 然後把資料送出去.

slave 回應請求

像之前接收資料一樣, slave 會設定一個 函數去回應 master 的請求的.


  • 執行 Wire.onRequest 去設定處理請求的函數
  • 在該函數中, 以 Wire.write 把 資料回傳

以下是一個簡單例子, 當接到請求後, 就回傳一句 "Super169"

#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 

#define I2C_BUFFER_SIZE 32  
uint8_t i2cBuffer[I2C_BUFFER_SIZE];
uint8_t i2cBufferCnt = 0;

void setup() {
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onRequest(requestEvent); // register event
 
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Slave.04 started\n");
}

void loop() {
}

void requestEvent()
{
  Wire.write("Super169",8);
}


master 發出請求 並 接收資料


在 master 中發出請求及接收資料, 也只是幾個簡單步驟就可以了:

  1. 執行 beginTransmission 並指定地址
  2. 以 Wire.requestFrom 向指定地址發出請求, 並指定回傳資料的數目
  3. 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出
  4. 執行 endTransmission 作結束

這裡大家可能會對 requestForm 有點疑問:

1. 為什麼 requestForm 要提供地址?
在 beginTransmission 中, 已指定了連線的地址, 其他地址是不能使用的, 為何還要指定地址?
這個本人亦有疑問, 但在網上找不到確實的答案.  以下是本人的推測, 有錯的希望大家指出.
首先, requestForm 是不需要放在 beginTransmission 之後的, 是可以獨立執行的, 所以必須要有地址.
但為什麼又要放入 beginTransmission 之後呢?  可能是為了確保 slave 在回傳資料時, 可以合法使用連線.  
2. 為什麼 requestForm 要提供回傳資料的大小
在之前的通訊中, 大家也看到, slave 發送完畢, 是不需要執行任何指令的.  對於 Wire 而言, 是沒有明確的訊息得知 slave 已發送完畢.  所以只好在發出請求時, 設定回傳資料的數目.  在正常情況下, 一般有特別的通訊協定, 發出請求的一方, 都會知道回傳資料的數目, 所以不會有問題的.  如果真的是沒有限定的話, 例如通訊協定中, 以回傳的第一個 byte 回報資料長度.  這樣只好用盡 Wire 的 buffer 把資料都接回來, 之後再進行分析處理.

以下就是一個前 slave 發出請求回傳資料的例子:

#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 
#define DATA_SIZE 8

void setup()
{
  Wire.begin();
 
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Master.04 started");
  Serial.println();
}
 
 
void loop()
{
  if (Serial.available()) {

    Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
    Wire.beginTransmission(SLAVE_ADDRESS);
    if (Wire.available()) {
      Serial.print("Data returned: ");
      while (Wire.available()) Serial.print((char) Wire.read());
      Serial.println();
    }
    Wire.endTransmission();
    while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
  }

}



執行後, 在 master 一方的 serial monitor 不論輸入什麼, 都會向 slave 發出回傳資料的請求.  而 slave 回傳的字句, 會在 serial monitor 中顯示出來.


相關程式下載:




Arduino 之間的 I2C 通訊 (3) 由 master 向 slave 發送資料/發出指令 [slave 延遲處理]

相關指令:

指令發出耆作用
Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
Wire.beginTransmission(<address>);master開始對 <address> 的連線
Wire.endTransmission();master 關閉之前的連線
Wire.write(<data>);master 在連線上送出 一個 byte 的資料
Wire.onReceive(<function>)slave 設定用來接收資料的函數
Wire.available();slave檢查連線上是否有可接收的資料
Wire.read();slave讀取連線上的一個 byte 的資料


slave 延遲處理

之前的一篇, 不是已經可以由 master 向 slave 發送資料, 而 slave 亦成功收到了, 為什麼又攪個 延遲處理出來?

之前一篇, 處理 master 送來的資料, 都是在接收的函數之內.  但大家不要忘記, arduino 的主線, 是在 loop 之內執行的.  接收函數太大, 或會影響主程式進行, 而且, 有些時候, 主程式或許需要用到接收回來的資料.

延後處理就是把資料放進緩存一樣, 讓主程式去處理.
方法很簡單, 可以直接把資料放有有關變數, 又或用最通用的方式, 先放入一個 buffer 中.

以下例子, 就是用最通用的方式處理,

#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 

#define I2C_BUFFER_SIZE 32  
uint8_t i2cBuffer[I2C_BUFFER_SIZE];
uint8_t i2cBufferCnt = 0;
boolean dataPending = false;

void setup() {
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onReceive(receiveEvent); // register event

  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Slave.03 started\n");
}

void loop() {
  if (dataPending) {
    Serial.println("Receive Data:");
    for (int idx = 0; idx < i2cBufferCnt; idx++) Serial.print((char) i2cBuffer[idx]);
    Serial.println("\n");   
    dataPending = false;
  }
}

void receiveEvent(int count) {
  i2cBufferCnt = 0;
  while(Wire.available()) {
    i2cBuffer[i2cBufferCnt++] = Wire.read();
  }
  dataPending = true;
}


執行後, 跟之前的是沒分別的.
當然, 你也可以嘗試把 i2c 的 buffer 加大或收細, 看看有什麼影響.


相關程式下載 (master_03 跟 master_02 是一樣的)

Arduino 之間的 I2C 通訊 (2) 由 master 向 slave 發送資料/發出指令 [slave 直接處理]

相關指令:

指令發出耆作用
Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
Wire.beginTransmission(<address>);master開始對 <address> 的連線
Wire.endTransmission();master 關閉之前的連線
Wire.write(<data>);master 在連線上送出 一個 byte 的資料
Wire.onReceive(<function>)slave 設定用來接收資料的函數
Wire.available();slave檢查連線上是否有可接收的資料
Wire.read();slave讀取連線上的一個 byte 的資料


通訊也有不同程度的, 先做一個最簡單的單向通訊.
由於只有 master 可以主動發出通訊要求, 最簡單的就是由 master 向 slave 發送資料了.
這個例子是針對一些操控裝置, 例如你做了一個 i2C 的舵機, 由 arduino 發出指令, 要它轉到指定的角度, 而舵機是不會回傳任何資料的

程式同樣分開 mater 及 slave 的部份, master 發送, slave 接收.

master 發送資料

在 master 發送資料, 只有幾個簡單步驟就可以了:

  1. 執行 beginTransmission 並指定接收資料地址
  2. 以 Wire.write 把資料送出去
  3. 執行 endTransmission 作結束

以下是一個簡單的例子, master 把 串口收到的資料, 發送給 slave.

#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 

 
void setup()
{
  Wire.begin();
 
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Master.02 started");
  Serial.println();
}
 
 
void loop()
{
  if (Serial.available()) {
    Wire.beginTransmission(SLAVE_ADDRESS);
    while(Serial.available()) {
      Wire.write(Serial.read());
      delay(1);
    }
    Wire.endTransmission();
  }
}


slave 接收資料

而 slave 接收, 是有點像 interrupt driven 的形式去做,  簡單的做法如下.


  1. 執行 Wire.onReceive 去設定接收資料的函數
  2. 在接收資料的函數中, 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出, 直到所有資料提出後, Wire.available 為 false

注意, 因為 Wire 庫的 buffer 只有 32 個 byte, 每次發送的資料應限制在 32 個 byte 之內.
由於 master 進行 write 是沒有限制的, 如果發送超過 32 bytes, 之後的資料就會流失.

以下例子, 是在 接收的函數中, 直接處理接收回來的資料.



#include <Wire.h>

#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 57600 

 
void setup() {
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onReceive(receiveEvent); // register event

  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Slave.02 started\n");
}

void loop() {
}

void receiveEvent(int count) {
  Serial.println("Receive Data:");
  while(Wire.available()) {
    Serial.print((char) Wire.read());
  }
  Serial.println("\n");
}


注意:  
這個例子中在 receiveEvent 中用到 Serial.print, 只是為了簡單展示結果, 雖然這裡沒有問題, 但其實是絕不應該的.
在正常的程式中, 應該盡量避免在 receiveEvent 中放入複雜的程序, 在下一章中會提及正確的做法.


執行程式後, 在 master 板子連線的 serial monitor 輸入資料, 就會發送到 slave 板子, 再在相關的 serial monitor 顯示出來.


相關程式下載: