2017年8月30日 星期三

Arduino 之間的 I2C 通訊 (8) 浮點的傳送

之前有點忙, 在準備 "Arduino 之間的 I2C 通訊 (7) 單片機有效傳送數據的選擇" 中, 突然停了一下, 沒想到一停就停了一年多.

當中留下了一個問題給看官思考的, 就是有關 浮點的傳送.

網上經常會看到, 有人問用 串口通訊時, 如何把 浮點 的數據送出去?
一般的答案, 都會是轉成字串發送吧, 例如 "123.45".
如果需要的浮點, 有固定的大小, 精確度有限, 這個有可能失真的方法還不錯吧.
為什麼說"失真'呢?  因為原本的數值, 小點後可能是連續很長的, 要轉成字串, 就要把長度限制了, 自然會有失真.

但大家有否考慮過, 可以簡單又完全不失真的傳送呢?
注意, 這裡的"不失真", 只是跟原來儲存的數值比較, 並非所有數值都可以.
如果原來儲存的數值已經是有"失真"的話, 傳送時也只會原原本本的發過去.
這是什麼意思呢?
簡單說, 要記錄 pi, 用 float 本身就有精確度的限制, 發送出去的 float, 絕不可能突然變成 double 的精度吧.  可以做到的, 就是你給我一個 float, 我就把這個 float 原整的發出去.  詳細情況, 可以在最後的例子中看到.

如果要轉成字串, 要用多少個字符才足夠呢? 100? 1000? 就是一萬也不一定是完全一樣.

但如果有考慮過電腦內的記憶體的運作,  無論發送什麼數值的 float, 也只需 4 個 byte 就可以了.
為什麼? 發送 123.45 有 5 個數字, 還要記下小數位, 也只需 4 bytes?

沒錯, 就是 4 bytes, 這沒有什特別, 因為原本用來儲存 float 的記憶體, 就只需要 4 bytes.
只要我們把這 4 個 bytes 發出去, 就可以原原本本的把那個數值傳過去了.

但怎樣可以把這 4 個 bytes 發出去, 在 接收端得到一個 float 呢?

看看 wire 或 serial 有關的 function, 都沒有以 float 作為參數或結果的, 那可以怎樣做到.

學習 c 的人, 應該不會有問題吧.  c 的 pointer 可是超好用的東西.

大家可以看看 wire 的庫, write 的方法之中, 其中一個就是:
virtual size_t write(const uint8_t *, size_t);
只要把 const uint8_t * 指到要發出的 float 去, size_t 設定為 4, 不就可以把 float 的 4 個 byte 發出去了嗎?

比如 你的資料儲存在   float data = 123.45678, 當中 data 是一個 float.
只要用  wire.write((uint8_t *) &data, 4)  就可以把它的值, 原原本本的發出去.

注意:  由於 data 是 float, &data 是 (float *), c 是不會主動把 (float *) 轉成 (uint8_t *) 的, 所以需要自己指定轉換.


之後, 在接收端收到 4 個 byte, 用相反的方法, 把 4 個 byte 用 pointer 放進 float 對應的記憶體, 就可以還原了.

文字很難清楚說明, 還是直接看程式碼吧.


slave 回傳 float

以下是一個收到任何請求, 都回傳一個 float 數值的例子,

#include 
 
#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 115200 
 
#define I2C_BUFFER_SIZE 32  
uint8_t i2cBuffer[I2C_BUFFER_SIZE];
uint8_t i2cBufferCnt = 0;

float data = 123.45678;
 
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.08 started\n");
}
 
void loop() {
}
 
void requestEvent()
{
  Wire.write((uint8_t *) &data,4);
}



master 接收 float

下面是對應的 master 程式例子:

#include 
 
#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 115200 
#define DATA_SIZE 4
 
void setup()
{
  Wire.begin();
  
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Master.08 started");
  Serial.println();
}

float data = 0;
  
void loop()
{
  if (Serial.available()) {
 
    Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
    Wire.beginTransmission(SLAVE_ADDRESS);
    data = 0;
    uint8_t *ptr = (uint8_t *) &data;
    if (Wire.available()) {
      Serial.print("Data returned: ");
      while (Wire.available()) {
        uint8_t b = Wire.read();
        *ptr++ = b;
        Serial.print(b, HEX);
        Serial.print(" ");
      }
      Serial.println();
      Serial.print("Data value: ");
      Serial.println(data, 6);
    }
    Wire.endTransmission();
    while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
  }
 
}


以上範例, 只在於說明如何發送參數給 slave 使用, 當中並沒有加入錯誤的檢測.

執行之後, 你會得到以下結果:

Data returned: DF E9 F6 42
Data value: 123.456779


DF E9 F6 42 就是記憶體中, 用來儲存 123.45678 的 4 個 bytes 了.

但...為什麼 Data value 是 123.456779, 而不是 123.456780 呢?  不是說不會"失真"嗎?

這裡那 0.000001 的差別, 並非傳送時的失真, 而是 float 本身的失真的.
在 arduino 的 float, 是不能準確記下 123.456780 的, 其精確度所限, 會記錄成 123.456779.
正如上面說過, 傳送過程是會原原本本的發過去, 結果就是 123.456779 了.

不信的話, 你可以試試在 slave 的程式中, 把 data 以 6 位小數印出來看看吧.




相關程式下載:


3 則留言:

  1. http://gammon.com.au/Arduino/I2C_Anything.zip

    回覆刪除
  2. 請問如果有2組數據以上(如為上述123.45678 與 另一個 987.65432)
    要送至Master分別顯示要如何寫?謝謝

    回覆刪除