ArduinoのライブラリでDHT20の情報を取得

組み込みエンジニア

こんにちは、ENGかぴです。

DHT20はASAIR製の湿度と温度のセンサーを備えたモジュールです。通信方式はI2CなのでWireライブラリを使用することでデータを取得することができます。DHT20からデータを取得してLCDに表示して動作確認を行いました。

UNOの拡張基板であるSD CARD SHIELDのGrove端子にDHT20モジュールを接続して動作確認を行います。SD CARD SHIELDはアイキャッチ画像のようにArduino UNOに差し込むだけで使用することができます。

DHT20モジュールはGrove Temperature&Humidity Sensor(DHT20)(Seeed Studio製:秋月電子で購入)を使用しています。LCDはQAPASS1602(スターターキットに付属)を使用しています。LCDのライブラリの使い方については下記記事にまとめています。

Arduinoの標準ライブラリでLCDに文字を表示する方法

Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Wireライブラリの使い方

Wireライブラリの使い方をDHT20からデータを取得する方法を例にして説明します。

Wireライブラリの初期化

#include <Wire.h>

void setup() {
  Wire.begin();
  //Wire.setClock(400000);
}

最初に「Wire.h」をインクルードする必要があります。begin()関数でI2Cの条件を初期化してスタートします。ArduinoのWireはSDAがA4ピン、SCLがA5ピンになります。通信速度のデフォルトは100kHzですが、変更する場合はsetClock()関数で変更するボーレートを指定します。

ボーレートを高速にするとプルアップ抵抗やボード上の配線長によっては通信データが鈍ってしまい通信不良の要因となるため注意が必要です。

データ送信の例

/* DHT20へコマンド送出 */
uint8_t Dht20SetData(uint8_t* cmd, uint8_t len){
  uint8_t ret;

  Wire.beginTransmission(SLAVE_ADRS);
  Wire.write(cmd,len); //lenサイズ分だけデータをセット
  ret = Wire.endTransmission(); //データの送信
}

Wireでデータ送信する例を示します。beginTransmisson()関数でスレーブアドレスを指定し初期化を行います。

write()関数で送信するデータをセットします。引数に送信するデータを指定します。第1引数に送信するデータを格納しているアドレスを指定します。第2引数に送信するデータ数を指定します。

endTransmission()関数でスレーブアドレスからセットしたデータを送信します。endTransmisson()関数の戻り値を確認することで送信の状況を確認することができます。送信が成功した場合は戻り値が0になります。

データ受信の例

/* DHT20からデータを取得 */
bool Dht20GetData(uint8_t reg_adr, uint8_t* reg_data, uint8_t len){
  bool  ret = false;

  if( Wire.requestFrom(SLAVE_ADRS, len) == len ){
    Wire.readBytes(reg_data,len);
    ret = true;
  }
  return ret;
}

Wireでデータ受信する例を示します。requestFrom()関数でデータを取得します。第1引数にスレーブのアドレスを指定し、第2引数に読み込むデータのサイズを指定します。

requestFrom()関数はスレーブアドレスの指定からデータの受信までを処理するのでendTransmission()関数は必要ありません。

requestFrom()関数は読み込んだデータ数を戻り値で返すため指定したサイズと一致したことを確認し、一時保存したデータをreadBytes()関数で読み込んでデータを格納します。

readBytes()関数の第1引数に読み込んだデータを格納するアドレスを指定します。第2引数に読み込むデータ数を指定します。

スポンサーリンク

DHT20モジュールからデータを取得する

DHT20モジュールは電圧範囲がDC2.2V~5.5Vと幅広いことや湿度と温度のデータが20ビットデータで高精度で測定できることが特徴です。データシートに記載されている手順に従ってDHT20モジュールから湿度と温度のデータを取得する手順について説明します。

DHT20モジュールのデータ取得の手順
  • 手順1
    センサーの状態確認(初期化時)

    電源ONから100ms経過後、0x71を送信してセンサーの状態を確認する。
    0x18であれば手順3に進む0x18以外であれば0x1B、0x1C、0x1Eレジスタを初期化する。

  • 手順2
    レジスタの初期化(初期化時)

    Seeed Studioに公開されているソースコードで確認した0xbaコマンドでレジスタを初期化して手順3に進む。

  • 手順3
    測定開始コマンドの送信

    10ms経過後0xACコマンドを送信する。0xACコマンドに続けて0x33、0x00を送信する。

  • 手順4
    測定待ちと測定状態の確認

    測定に80ms必要なので待機しますが、測定完了はスレーブアドレスから1バイト目のstateの8ビット(bit7)で確認する。bit7が0であれば測定完了なので手順5に進む。

  • 手順5
    CRCの計算

    stateを含めた湿度と温度データまでの6ビットに対してCRC計算を行い最後尾に付与されたCRCと一致するかをチェックする。一致したら手順6に進む。

  • 手順6
    湿度と温度のデータを換算する

    取得したデータを20ビットのデータで整理して換算式を使って湿度と温度のデータに換算する。再度測定する場合は手順3に戻る

頻繁なデータ測定は計算に伴う消費電流の増加など内部で発生した熱の影響を受けるため最低でも2秒おきに測定データを取得することが推奨されています。

手順2ですが、データシートにはウェブサイトで初期化ルーチンを確認となっていますが見当たりませんでした。

電源ONして測定を行った後で電源をOFFせずにリスタートすると0x71レジスタの値が0x18以外になります。電源をOFFして数秒経過してから0x71レジスタを確認すると0x18になるため、確認せずにデータ取得の手順に進んでも問題ないと考えています。

以下ではコマンドの送信例と手順に沿ったデータの取得方法の例を説明します。また取得したデータの健全性を確認するCRC8の計算方法について説明します。

すき間時間で資格をゲット【STUDYing(スタディング)】

測定をモードで管理する

  switch(mode){
    case DHT20_MEASURE: //手順3
      //測定開始トリガの送信
      mode = DHT20_WAIT;
      break;
    case DHT20_WAIT: //手順4
      //stateのbit[7]の状態を確認する
   //bit[7]が0であればデータを読み込む
      mode = DHT20_CHK;
      break;
    case DHT20_CHK: //手順5,6
      //CRCをチェックして一致するとデータを換算する
      Datachk(); 
      mode = DHT20_MEASURE;
      break;
  }

DHT20モジュールのデータ取得の手順の手順3から手順6をモードで管理します。

  1. DHT20_MEASURE(手順3)
  2. DHT20_WAIT(手順4)
  3. DHT20_CHK(手順5、手順6)

1.DHT20_MEASUREは測定開始トリガを送信し、次のモードに進みます。

2.DHT_WAITはstateのbit[7]ビットが0であれば測定が完了しているためデータを取得して次のモードに進みます。

3.DHT20_CHKは取得したデータのCRCを計算して受信したCRCと一致するか確認します。CRCが健全であればデータを受け入れて湿度と温度に換算します。

測定開始トリガを送信する

uint8_t trig[] = { 0xAC, 0x33, 0x00};

Dht20SetData( &trig[0], sizeof(trig));

測定開始コマンドは0xACを送信しますが、続けてコマンドパラメータとして0x33と0x00を送信する必要があります。測定開始コマンド用の配列を準備してデータ送信の例で説明したDht20SetData()関数に配列のアドレスとサイズを引数に指定しています。測定が完了するまで80ms待つ必要があります。

CRCチェックを行う

データシートに記載されているCRC8のチェックの多項式は以下の通りです。

CRC[7:0] = 1+X4+X5+X8

この多項式から標準値を求めます。X=2として計算するとCRCは0x131になります。CRC8はLSBから8ビットが対象となるためPolynomial(多項式)標準値は0x31になります。またデータシートにCRCの初期値に0xFFが指定されています。

#define POLYNOMIAL 0x31

uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
  uint8_t crc = 0xFF;
  uint8_t i,j;
    
  for( i = 0; i < sz; i++){
    crc ^= *data;
        
    for( j = 0; j < 8; j++ ){
      if( crc & 0x80 ){
        crc = ( crc << 1 ) ^ POLYNOMIAL;
      }
      else{
        crc = crc << 1;
      }
    }
    ++data;
  }
  return crc;
}

CRC-8の計算方法は初期値と1バイト目のバイトデータのXORを計算します。この値を8ビット分左にシフトしていきますが、最上位ビットが立った時に1ビット分シフトしてからPolynomialとXORをとります。

2バイト目以降は前のバイトの結果を初期値として8ビット分シフトとXORを繰り返していき対象のデータのバイト数だけ繰り返します。

測定データの最後尾にstateデータから温度データまでの6バイトデータに対するCRCが付加されています。付加されたCRCと受信したデータからCRC計算した結果が一致した場合に有効なデータとして採用します。

測定データの換算

引用:Data Sheet DHT20 Read temperature and humidity data
引用:Data Sheet DHT20 Read temperature and humidity data

DHT20のデータシートを確認するとstateデータに続けて湿度データが20bit、温度データが20bit配置されています。湿度と温度データを換算するために20bitのデータを4byteデータに構成し直します。

読み込みデータを4byteデータに構成に変換
読み込みデータを4byteデータに構成に変換

湿度データについて説明します。Humidity-1は20bitデータのMSBを含むデータなのでbit0から左に12回シフトして加算します。Humidty-2はbit4からbit11を構成するデータなのでbit0から左に4回シフトして加算します。Humidty-3はreadData[3]の上位4bitなので右に4回シフトして加算します。

温度データについて説明します。温度を構成するTemperature-1は20bitデータのMSBを含むデータですがreadData[3]の下位4bitを使用するため0x0Fで論理積をとったデータを使用します。Temperature-1はbit16からbit19までを構成するデータなのでbit0から左に16回シフトして加算します。Temperature-2はbit8からbit15を構成するデータなのでbit0から左に8回シフトして加算します。Temperature-3はそのまま加算します。

uint32_t humid;
uint32_t temp;

humid = ((uint32_t)readData[1] << 12)
      + ((uint32_t)readData[2] << 4)
      + ((uint32_t)readData[3] >> 4);

 temp = (((uint32_t)readData[3] & 0x0F) << 16)
      + ((uint32_t)readData[4] << 8)
      + ((uint32_t)readData[5]);

4byteデータへの置き換えの例を示しています。readData[]を(uint32_t)でキャストしてシフト演算しています。コンパイラーによってはキャストしなくても演算できますがArduinoの場合は必要です。humidとtempは次の換算式におけるSRHとSTになります。

引用:Data Sheet DHT20 Signal Conversion
引用:Data Sheet DHT20 Signal Conversion

データシートによると上記の式によって換算すると湿度と温度の値になります。

float tempdata;
float humiddata;

humiddata = ((float)humid/1048576 ) * 100;
tempdata = ((float)temp/1048576) * 200 - 50;

4byteデータに構成した湿度と温度のデータを換算します。小数点以下を表示するためfloatの型にキャストしてから220(1,048,576)で割っています。

スポンサーリンク

LCD表示用のデータを作成

LCDのライブラリの使い方については下記記事にまとめています。

Arduinoの標準ライブラリでLCDに文字を表示する方法

以下ではLCDの表示用のデータの作成を中心に説明しています。

int humi100;
int temp100;
uint8_t disp[COL_SZ]; //NULL文字を含めたサイズ

humi100 = humiddata * 100;
temp100 = tempdata * 100;

lcd.clear();
memset(&disp[0],' ',sizeof(disp));
sprintf((char*)&disp[0],"Humi:  %d.%02d %%",humi100/100,humi100%100);

for(uint8_t i=0; i < sizeof(disp);i++){
  if(disp[i] != NULL){
    lcd.write(disp[i]);
  }
}

sprintf()は文字列に対して%d,%fなどを指定して数値を文字変換して文字列を生成する関数です。第1引数に変換後の文字列を格納するアドレスを指定します。第2引数に文字列を指定します。第3引数以降は文字列の%dなどに対応する値を指定します。

spfintf()で%fで文字列変換すると小数点を含んだ文字列になりますが、Arduino UNOでは%fの変換に対応していません。そのため湿度と温度のデータを100倍して小数点以下を整数で表示(%d)してLCDのデータを作成します。

例のdesp[]は最大でも16文字(変換後のNULL文字を入れると17文字)になるので17バイトの配列にしています。文字列は配列のサイズに関わらず生成されるため配列のサイズを超えた場合は予期しないエラーになることがあるので注意が必要です。

disp[]の配列をmemset()関数でスペースコード(‘ ‘)で初期化して不要な文字が表示されることを防いでいます。

sprintf((char*)&disp[0],"Humi:  %d.%02d %%",humi100/100,humi100%100);

文字列として”Humi: %d.%02d %%”を指定しています。最初の%dはhumi100/100の値(整数部)を文字変換した値になります。%02dはhumi100%100の値(小数点以下2桁)を文字変換した値になります。%02dとしているため0を左に詰めて2桁の表示となります。%%は%の文字を表示するための表記です。

例)湿度が74.32の場合
74.32の100倍値を取ると7432になります。この値を/100で除算すると74になります。また7432を100で割った余り(%100)は32になります。文字列に直すと「Humi: 74.32 %」になります。

sprintf()で文字列を生成すると文字の終わりにNULL文字が挿入されます。

NULL文字を表示するとLCDの表示がバグることがあるため文字列の生成後にNULL文字をスペースコード(‘ ‘)で置き換えています。

動作確認

動作確認用の回路図
動作確認用の回路図

SD CARD SHILDをArduino UNOに挿入して動作確認を行います。DHT20モジュールをA4ピンとA5ピンに配線していますがSD CARD SHILDはUNOのピンを延長しておりGrove端子に接続されているため配線上同じになります。

SD CARD SHILD上のSCL、SDAのシルク印刷されているGrove端子にDHT20モジュール(Grove)を接続します。Wireを使用するためプルアップ抵抗が必要ですがDHT20モジュールに実装されているため不要です。

電源を入れると初期画面が2秒表示した後、DHT20が測定したデータが表示されましました。またデータの測定が5秒毎に行われてLCDの表示が更新されていることが確認できました。

温度(Temperature)は部屋に置いている温湿度計によると24.1℃、湿度(Humidity)は69.8%でした。結果がほぼ一致しているため湿度と温度が取得できていることが確認できました。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <Wire.h>
#include <LiquidCrystal.h>

#define SLAVE_ADRS 0x38
#define STATUS_CHK 0x71
#define POLYNOMIAL 0x31
#define RESET_REG_ADDR 0xba

#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_DHT20_WAIT 10 //計測ウェイト
#define TIME_DHT20_MAX 500 //計測ウェイト
#define TIME_OUT_MAX 1100  //通信タイムアウト

#define LCD_RS 2
#define LCD_EN 3
#define LCD_D4 4
#define LCD_D5 5
#define LCD_D6 6
#define LCD_D7 7

#define COL_SZ (16 + 1) //文字の最後にNUll(0)が入るので+1

typedef enum{
  DHT20_MEASURE = 0,
  DHT20_WAIT,
  DHT20_CHK,  
  DHT20_MAX  
}DHT20_MODE;

typedef enum{
  INIT_CHK = 0,
  INIT_RESET,
  INIT_END,
  INIT_MAX
}INIT_MODE;

const char disptbl[2][COL_SZ]={
  "DHT20 humi&temp ",
  "         Ver1.00"
};

uint8_t trig[] = { 0xAC, 0x33, 0x00};
uint8_t status;
uint8_t readData[7];

uint32_t beforetimCnt = millis();
float temp;
float humi;
int16_t timDht20start = TIME_DHT20_WAIT;
int16_t timDht20Out = TIME_OFF;
int16_t timDht20init;
DHT20_MODE mode = DHT20_MEASURE;
INIT_MODE initmode;
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4,LCD_D5, LCD_D6, LCD_D7);

void InitDht20(void);
void mainApp(void);
void mainTimer(void);
uint8_t Dht20SetData(uint8_t* cmd, uint8_t len);
bool Dht20GetData(uint8_t reg_adr, uint8_t* reg_data, uint8_t len);
void Datachk(void);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );

void setup() {

  Serial.begin(115200);
  Wire.begin();

  while( initmode != INIT_MAX ){
    InitDht20();
    mainTimer();
  }

  lcd.begin(16,2); //16×2を表示領域
  lcd.write(&disptbl[0][0],sizeof(disptbl[0]));
  lcd.setCursor(0, 1); //2段目の左端にカーソル
  lcd.write(&disptbl[1][0],sizeof(disptbl[1]));
  delay(2000); //初期表示を2秒間行う

}

void loop() {

  mainTimer();
  mainApp();
}

/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();

    if( timDht20start > TIME_UP ){
      --timDht20start;
    }
    if( timDht20Out > TIME_UP ){
      --timDht20Out;
    }
    if( timDht20init > TIME_UP ){
      --timDht20init;
    }
  }
}
/* 初期化 */
void InitDht20(void){
  uint8_t buf;

  switch (initmode){
    case INIT_CHK:
      if( timDht20init == TIME_UP ){
        timDht20init = TIME_DHT20_WAIT;
          if( Dht20GetData(STATUS_CHK, &status, 1) ){
            Serial.print("status:");
            Serial.println(status);
            if( status != 0x18 ){
              //initmode = INIT_RESET;
              initmode = INIT_END; 
              //電源OFFしても以前の計測状態が残っていれば0x18以外になるため不要
              timDht20init = TIME_DHT20_WAIT;
            }
            else{
              initmode = INIT_MAX;
              Serial.println("OK");
            }
          }
          else{
            Serial.println("slave-err");
            while(1);
          }
      }
      break;
    case INIT_RESET:
      if( timDht20init == TIME_UP ){
        timDht20init = TIME_DHT20_WAIT;
        buf = RESET_REG_ADDR;
        if( Dht20SetData( &buf, 1) == 0 ){
          initmode = INIT_END;
        }
      }
      break;
    case INIT_END:
      if( timDht20init == TIME_UP ){
        timDht20init = TIME_DHT20_WAIT;
        initmode = INIT_MAX;
      }
      break;
  default:
    break;
  }
}
/* メイン処理関数 */
void mainApp(void){

  switch(mode){
    case DHT20_MEASURE:
      if( timDht20start == TIME_UP ){
        timDht20start = TIME_DHT20_WAIT;
        timDht20Out = TIME_OUT_MAX;
        Dht20SetData( &trig[0], sizeof(trig));
        mode = DHT20_WAIT;
      }
      break;
    case DHT20_WAIT:
      if( timDht20start == TIME_UP ){
        timDht20start = TIME_DHT20_WAIT;
        if( Dht20GetData(NULL, &readData[0], sizeof(readData)) ){
          //Serial.print("status:");
          //Serial.println(readData[0]);
          if( (readData[0] & 0x80 ) == 0){
            mode = DHT20_CHK;
          }
          else{
            mode = DHT20_MEASURE;
          }
        }
      }
      break;
    case DHT20_CHK:
      if( timDht20start == TIME_UP ){
        timDht20start = TIME_DHT20_MAX;
        timDht20Out = TIME_OFF;
        Datachk();
        mode = DHT20_MEASURE;
      }
      break;
  }

  if( timDht20Out == TIME_UP ){
    timDht20Out = TIME_OFF;
    mode = DHT20_MEASURE;
    timDht20start = DHT20_MEASURE;
  }
}

/* DHT20へコマンド送出 */
uint8_t Dht20SetData(uint8_t* cmd, uint8_t len){
  uint8_t ret;

  Wire.beginTransmission(SLAVE_ADRS);
  Wire.write(cmd,len);
  //for( uint8_t i=0; i < len; i++ ){
  //  Wire.write(*cmd); //lenサイズ分だけデータをセット
  //  ++cmd;
  //}
  
  ret = Wire.endTransmission(); //ストップ・コンディションの発行
}
/* DHT20からデータを取得 */
bool Dht20GetData(uint8_t reg_adr, uint8_t* reg_data, uint8_t len){
  bool  ret = false;

  if( reg_adr != NULL ){
    Wire.beginTransmission(SLAVE_ADRS); //スタート・コンディションの発行
    Wire.write(reg_adr); //書き込む対象のアドレスをセット(ライトで指定)
    Wire.endTransmission(); //ストップ・コンディションの発行
  }

  if( Wire.requestFrom(SLAVE_ADRS, len) == len ){
    Wire.readBytes(reg_data,len);
    //for( uint8_t i=0; i < len; i++ ){
    //  *reg_data = Wire.read(); //len分だけデータをリードする
    //  ++reg_data;
    //}
    ret = true;
  }
  return ret;
}
/* CRCチェックと湿度と温度データに換算 */
void Datachk(void){
  uint32_t temp;
  uint32_t humid;
  float tempdata;
  float humiddata;
  uint8_t crc;
  uint8_t disp[COL_SZ];
  int humi100;
  int temp100;

  crc = Crc8Calc( &readData[0],6);

  //for(uint8_t i=1; i < 6;i++){
  //  Serial.println(readData[i]);
  //}

  if( crc == readData[6]){
    humid = ( (uint32_t)readData[1] << 12 )
          + ( (uint32_t)readData[2] << 4 )
          + ( (uint32_t)readData[3] >> 4 );

    temp = ( ((uint32_t)readData[3] & 0x0F) << 16 )
         + ( (uint32_t)readData[4] << 8 )
         + ( (uint32_t)readData[5] );
    
    humiddata = ((float)humid/1048576 ) * 100;
    tempdata = ((float)temp/1048576) * 200 - 50;

    Serial.print("Tempareture: ");
    Serial.print(tempdata);
    Serial.print("℃ ");
    Serial.print("Humidity: ");
    Serial.print(humiddata);
    Serial.println("%");

    humi100 = humiddata * 100;
    temp100 = tempdata * 100;

    lcd.clear();
    memset(&disp[0],' ',sizeof(disp));
    sprintf((char*)&disp[0],"Humi:  %d.%02d %%",humi100/100,humi100%100);

    for(uint8_t i=0; i < sizeof(disp);i++){
      if(disp[i] != NULL){
        lcd.write(disp[i]);
      }
    }
    memset(&disp[0],' ',sizeof(disp));
    sprintf((char*)&disp[0],"Temp:  %d.%02d C",temp100/100,temp100%100);
    lcd.setCursor(0, 1); //2段目の左端にカーソル
    for(uint8_t i=0; i < sizeof(disp);i++){
      if(disp[i] != NULL){
        lcd.write(disp[i]);
      }
    }
  }
}

/* CRC8計算関数 */
uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
  uint8_t crc = 0xFF;
  uint8_t i,j;
    
  for( i = 0; i < sz; i++){
    crc ^= *data;
        
    for( j = 0; j < 8; j++ ){
      if( crc & 0x80 ){
        crc = ( crc << 1 ) ^ POLYNOMIAL;
      }
      else{
        crc = crc << 1;
      }
    }
    ++data;
  }
  return crc;
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

あなたの市場価値を見いだす転職サイト【ミイダス】無料会員登録

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました