GroveBeginnerKit を、C++(Arduino)とRuby(ラズパイ,Rboard) で使う　I2C - 温湿度センサー(DHT20) 編

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

[Grove Beginner Kit for Arduino を使ってみる記事](https://qiita.com/HirohitoHigashi/items/dc43ea72d572b21a6823)の第6回、今回は I<sup>2</sup>C 接続の温湿度センサーを題材にします。

このレポートでは、

<b>「メーカーのデータシートを見て、センサICを直接コントロールをすること」</b>

を、方針とします。
もちろん、プログラムを書く上で既存のライブラリ等の実装を参考にすることは良い事です。しかしながらこのレポートは、I2C バスやセンサ IC の扱い方、データシートの読み方を示す事も目的としているため、できるだけ低レイヤーでの説明を行います。

ターゲットは、以下の通り。
* Arduino　- 付属の Arduino Uno 互換機
* Ruby - Raspberry Pi + [Grove Base HAT for Raspberry Pi](https://www.seeedstudio.com/Grove-Base-Hat-for-Raspberry-Pi.html)
* mruby/c - [RBoard](https://www.sjc-inc.co.jp/service/rboard)


# 温湿度センサー (Temperature&Humidity Sensor(DHT20))

| 外観 | 回路図 |
|:-:|:-:|
| ![grove_TempHum.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/030b7326-12fe-bb3c-fae1-1d3e1a34b225.jpeg) | （センサーがGrove端子に接続されてるだけなので省略） |

（[メーカーWiki: Grove - Temperature&Humidity Sensor(DHT20)](https://wiki.seeedstudio.com/Grove-Temperature-Humidity-Sensor-DH20/)　より引用）

搭載されているセンサーICは、ASAIR製 DHT20（ケース色が黒色） です。
2022年頃、Grove Beginner Kit に仕様変更があったようで、従来は DHT11（ケース色が水色）が使用されていました。Grove Beginner Kit 公式ドキュメントもまだ DHT11 のままです。この両者には、制御方法に互換性がないので、注意が必要です。
このレポートでは、DHT20 の方を説明しています。


# データシートの確認

[ICメーカー製品ページ](http://www.aosong.com/en/products-67.html) で、データシートを確認します。
このデータシートは、情報が断片的で、なかなかに理解が難しいものでしたので、一緒に提供されているサンプルプログラムと併せて読み解きます。
また、このICは、よくある内部レジスタを読み書きする方法ではなく、まるでストリーム通信のようにI2Cバスを使っています。もちろんこういった使い方は規格違反ではありませんが、たまに見かける「内部レジスタを1バイトで指定してデータ送受信を行うことしか想定していないI2Cライブラリ」では扱うことができないと思われます。


## ICの初期化

ICの初期化は以下の通りです。

* 電源ON後、500ms 待つ。（データシートでは、最低100msと記載されているが、サンプルプログラムでは 500msを推奨と書いてあった）
* 0x71 を送信し 1バイトをステータスワードとして読み取る
* (ステータスワード & 0x18) == 0x18 でなければ、IC自体の初期化が必要。だが、初期化方法はデータシートには無くウェブページを参照するように記載してある

## データの読み込み

プログラムからの指示で測定を開始し、測定完了を待って測定データを取得する方式です。またデータ化けなどが起こっていないかの確認のため、1バイトのCRCが付与されています。読み取りシーケンスは以下の通りです。

* トリガーコマンド `0xAC,0x33,0x00` を送信する
* 80ms 待つ
* 0x71 を送信し 1バイトをステータスワードとして読み取る
* ステータスワードの bit7 が 1 ならば、もう少し待つ
* ステータスワードの bit7 が 0 ならば、続いて 6 バイト読み取る
* データは、温度、湿度とも、20bit長

![grove_Humi_ReadData.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/973cbedc-f27e-9f0d-fc91-da664fd95a43.png)

データを読み込んだ後、必要に応じて CRC を確認後、データシート記載の計算式に従ってデータを換算します。
```math
RH = (\frac{S_{RH}}{2^{20}}) * 100
```
```math
T = (\frac{S_T}{2^{20}}) * 200 - 50
```

# サンプルプログラム

プログラムを簡単にするため、ステータスワード確認から引き続き6バイトの読み込みは、合わせて7バイトの読み込みをしてから確認することにします。また、起動時に確認している `(ステータスワード & 0x18) != 0x18` 時の IC 自体の初期化は、こちらで試す限りでは一度も必要になりませんでしたので、コードには反映していません。


## Arduino

```c++:DHT20_Humidity.ino
#include <Wire.h>

const int I2C_ADRS = 0x38;

void setup() {
  Wire.begin();
  Serial.begin(9600);

  // IC初期化
  delay( 500 );

  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0x71 );
  Wire.endTransmission( false );
  Wire.requestFrom( I2C_ADRS, 1 );
  int sts = Wire.read();
  if( (sts & 0x18) != 0x18 ) {
    Serial.println("Sensor needs initialize.");
    while(1)
      ;
  }
}

void loop() {
  byte data[7];

  // Trigger
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xac );
  Wire.write( 0x33 );
  Wire.write( 0x00 );
  Wire.endTransmission();

  // Read status & data
  int i;
  for(i = 0; i < 10; i++) {
    delay(80);

    Wire.requestFrom( I2C_ADRS, 7 );
    for( int i = 0; i < 7; i++ ) {
      data[i] = Wire.read();
    }
    if( (data[0] & 0x80) == 0 ) break;
  }
  if( i == 10 ) {
    Serial.println("Sensor read error.");
    return;
  }

  // check CRC
  int crc = 0xff;
  for( int i = 0; i < 6; i++ ) {
    crc ^= data[i];
    for( int i = 0; i < 8; i++ ) {
      crc <<= 1;
      if( crc > 0xff ) {
        crc ^= 0x31;
      }
      crc &= 0xff;
    }
  }
  if( crc != data[6] ) {
    Serial.println("CRC error.");
    return;
  }

  // convert
  double humidity = (double)((uint32_t)data[1] << 12 | (uint32_t)data[2 << 4] | data[3] >> 4) / 1048576 * 100;
  double temperature = (double)((uint32_t)(data[3] & 0x0f) << 16 | (uint32_t)data[4] << 8 | data[5]) / 1048576 * 200 - 50;

  // display
  Serial.print("Temp: ");
  Serial.print(temperature);
  Serial.print("  Humi: ");
  Serial.println(humidity);

  delay( 1000 );
}
```


## Raspberry Pi (CRuby)

```ruby:DHT20_Humidity.rb
require "mruby/i2c"

class DHT20
  I2C_ADRS = 0x38

  #
  # init instance
  #
  def initialize( i2c_bus, address = I2C_ADRS )
    @bus = i2c_bus
    @address = address
  end

  #
  # init sensor
  #
  def init()
    sleep 0.5
    data = @bus.read( @address, 1, 0x71 ).bytes
    if (data[0] & 0x18) != 0x18
      raise "Sensor needs initialize."
    end
  end

  #
  # meas
  #
  def meas()
    @bus.write( @address, 0xac, 0x33, 0x00 )    # Trigger

    retry_count = 0
    while true
      sleep 0.08
      d = @bus.read( @address, 7 ).bytes
      break if d.size > 1 && d[0][7] == 0
      retry_count += 1
      raise "Retry error." if retry_count > 10
    end

    # check CRC
    crc = 0xff
    6.times {|i|
      crc ^= d[i]
      8.times {
        crc <<= 1
        crc ^= 0x31  if crc > 0xff
        crc &= 0xff
      }
    }
    raise "CRC error."  if crc != d[6]

    return {
      :humidity => (d[1] << 12 | d[2] << 4 | d[3] >> 4).to_f / 2**20 * 100,
      :temperature => ((d[3] & 0x0f) << 16 | d[4] << 8 | d[5]).to_f / 2**20 * 200 - 50,
    }
  end
end

sensor = DHT20.new( I2C.new )
sensor.init

while true
  data = sensor.meas
  printf("Temp:%5.2f Humi:%3.0f\n", data[:temperature], data[:humidity] )
  sleep 1
end
```

## RBoard (mruby/c)

RaspberryPiと同じプログラムから、require をコメントアウトしたプログラムがそのまま動きます。


# おわりに

今回は、温湿度センサーを使ってみました。
このセンサーの公称性能「確度、温度 ±0.5℃、湿度 ±3%」が正確に測れるとしたら驚異的な性能ですね。
一般的に、温度や湿度は測定誤差が大きくなりがちです。特にセンサ周辺に熱源があったり輻射熱をうけたり、また、値が逐次変化する場合はセンサー自体の時定数の影響もあります。身近なのに、意外と正しく測定するのは難しいのが、温度、湿度です。

次回は最終回、OLED ビットマップディスプレイを題材にしようと思います。