M5StickC(ESP32)による「ELEGOO Arduino用UNO R3スターターキット」を利用したArduino入門 その9 DHT11 温度湿度センサー

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

概要

今回は「Lesson 11 DHT11 温度湿度センサー」です。

DHT11とは?

秋月さんだと、上記のセンサーのみになります。他にも必要な回路を追加したセンサーモジュールもあります。

信号線をプルアップする必要があるのですが、センサーモジュールではプルアップ回路が最初からついています。ESP32で使うのであればボード側でプルアップしてあげれば、センサーのみでも直接接続することはできると思います。

また、センサーは4PINありますが、使っていないPINがあるので、センサーモジュールでは3PINになっています。

温湿度のセンサーはかなりの種類がありますが、DHT11は比較的安くて、通信に1PINしか使わないので使いやすいセンサーになっています。

データシートを確認したところ3.3Vから5.5Vまで動作しますので、ESP32でも安心して使うことができます。

接続方法

Sと書いてある端子がシグナルの信号端子で、今回GPIO26に接続しました。あとは-と書いてあるところをGNDに、真ん中を3.3Vに接続しました。

ライブラリの選定

ライブラリを使わないで自分で組むことも可能ですが、ちょっと特殊なプロトコルで、他に使いそうにないので、今回はライブラリを使ってみたいと思います。

まずはライブラリマネージャーにてDHT11で検索してみます。複数出てきますね。一番上のがArduinoのワークショップ用のライブラリセットですね。次がAdafruitの定番ライブラリです。3つ目がESPと書いてあるので、ESP32に特化しているライブラリだと思います。

自作の検索画面もあります。ほぼ同じ表示ですが、ブラウザの検索が使えます。

もっと下にもありますが、このライブラリの順番は上に行くほどいいってものではないので注意してください。大抵の場合にはAdafruitさんのライブラリが標準的です。Adafruitさんはいろいろなセンサーなどのモジュールをたくさん出していて、ライブラリや使い方まで提供してくれています。

同じようなものでGrove純正センサーなどを使っている場合には、Seeedさんがライブラリを提供してくれています。

次に検討するのはESP32用に特化したものがあるかを探すことです。今回はESP32に特化しているものがあったので、検討してみます。

んー、マルチタスクを利用して定期的にデータ取得するライブラリみたいです。結構複雑ですね。。。

上記がサンプルソースですが、思ったより長いです。タイマークラスで定期実行して、別タスクでデータを取得しています。2年前のソースなので書き方は結構古いですね。

今回はAdafruitさんの定番ライブラリを使おうと思います。「DHT sensor library」をインストールしてください。

上記の場合には、依存しているライブラリがあるので「Install all」を選択して一緒にいれてください。

スケッチ例実行

DHT_Unified_Sensor

#define DHTPIN 26     // Digital pin connected to the DHT sensor 
#define DHTTYPE    DHT11     // DHT 11

スケッチ例から上記の2行を探して書き換えます。今回はGPIO26に接続しているのでPINを26にして、接続しているセンサーはDHT11なのでDHT22の行をコメントアウトして、DHT11を有効にします。

DHTxx Unified Sensor Example
------------------------------------
Temperature Sensor
Sensor Type: DHT11
Driver Ver:  1
Unique ID:   -1
Max Value:   50.00°C
Min Value:   0.00°C
Resolution:  2.00°C
------------------------------------
Humidity Sensor
Sensor Type: DHT11
Driver Ver:  1
Unique ID:   -1
Max Value:   80.00%
Min Value:   20.00%
Resolution:  5.00%
------------------------------------
Temperature: 27.50°C
Humidity: 64.00%
Temperature: 27.50°C
Humidity: 64.00%
Temperature: 27.50°C
Humidity: 58.00%

実行結果が上記になります。最初にセンサーの情報を取得しています。

温度の範囲が0度から50度になっています。実はこのセンサーは2017年に仕様が変わって-20度から60度までになっているはずです。ソースファイルを確認したところ、以下のコードがありました。

  case DHT11:
    sensor->max_value = 50.0F;
    sensor->min_value = 0.0F;
    sensor->resolution = 2.0F;

ライブラリ側で固定値が入っているので、本当のセンサーの値ではないようです。この値を見る必要はなさそうですね。

Temperature: 27.50°C
Humidity: 64.00%

温度と湿度は上記の表記でした。出力をみたところ温度は0.1度単位、湿度は1%単位で動いていました。

DHTtester

こちらもPIN番号とセンサーを書き換えて実行してみます。

DHTxx test!
Humidity: 64.00%  Temperature: 27.50°C 81.50°F  Heat index: 29.12°C 84.41°F
Humidity: 57.00%  Temperature: 27.50°C 81.50°F  Heat index: 28.47°C 83.25°F
Humidity: 64.00%  Temperature: 27.50°C 81.50°F  Heat index: 29.12°C 84.41°F
Humidity: 64.00%  Temperature: 27.50°C 81.50°F  Heat index: 29.12°C 84.41°F
Humidity: 64.00%  Temperature: 27.50°C 81.50°F  Heat index: 29.12°C 84.41°F

こっちの方がシンプルですね。最後にHeat indexがありますが、これはアメリカなどで使われている指数で体感値を計算しているようです。

上記に計算方法がありました。温度と湿度から計算しているだけですので、センサーからの値で自分で計算することも可能そうです。

こちらのサンプルの方が単純でしたので、こちらをベースにシンプルなスケッチを書いてみたいと思います。

シンプルスケッチ

#include "DHT.h"

DHT dht(26, DHT11);

void setup() {
  Serial.begin(115200);
  delay(100);

  // DHT11初期化
  dht.begin();
}

void loop() {
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature();

  Serial.printf("湿度:%2.0f%%, 温度:%2.1f°C\n", humidity, temperature);

  // Wait
  delay(2000);
}

最低限必要なコードはこんな感じでしょうか?

高精度タイマーリピート

#include "DHT.h"

DHT dht(26, DHT11);

float humidity;
float temperature;

// タイマー
hw_timer_t * timer = NULL;

// キュー
#define QUEUE_LENGTH 1
QueueHandle_t xQueue;

// タイマー処理用タスク
TaskHandle_t taskHandle;

// タイマー割り込み
void IRAM_ATTR onTimer() {
  int8_t data;

  // キューを送信
  xQueueSendFromISR(xQueue, &data, 0);
}

// 実際のタイマー処理用タスク
void task(void *pvParameters) {
  while (1) {
    int8_t data;

    // タイマー割り込みがあるまで待機する
    xQueueReceive(xQueue, &data, portMAX_DELAY);

    // 実際の処理
    float humidity = dht.readHumidity();
    float temperature = dht.readTemperature();

    Serial.printf("湿度:%2.0f%%, 温度:%2.1f°C\n", humidity, temperature);

    // シリアルのキャッシュをフラッシュ
    Serial.flush();
  }
}

void setup() {
  Serial.begin(115200);
  delay(50);

  // DHT11初期化
  dht.begin();

  // キュー作成
  xQueue = xQueueCreate(QUEUE_LENGTH, sizeof(int8_t));

  // Core1の優先度3でタスク起動
  xTaskCreateUniversal(
    task,           // タスク関数
    "task",         // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    3,              // 優先度(大きい方が高い)
    &taskHandle,    // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );

  // 4つあるタイマーの1つめを利用
  // 1マイクロ秒ごとにカウント(どの周波数でも)
  // true:カウントアップ
  timer = timerBegin(0, getApbFrequency() / 1000000, true);

  // タイマー割り込み設定
  timerAttachInterrupt(timer, &onTimer, true);

  // 2000000カウントにセット(=2000000マイクロ秒=2000ミリ秒=2秒)
  timerAlarmWrite(timer, 2000000, true);

  // タイマー開始
  timerAlarmEnable(timer);
}

void loop() {
  delay(1);
}

同じ処理をタイマー割り込みを使った例になります。センサーなどの値を取得する場合には、若干時間がかかります。そのため、センサー取得中は他の処理ができず、ボタン入力などを取りこぼす可能性があります。

マルチタスクを利用すれば、同時に複数の処理を並行実行することができます。

単純マルチタスク例

#include "DHT.h"

DHT dht(26, DHT11);

float humidity;
float temperature;

// キュー
#define QUEUE_LENGTH 1
QueueHandle_t xQueue;

// センサー処理用タスク
TaskHandle_t taskHandle;

// センサー処理用タスク
void task(void *pvParameters) {
  while (1) {
    int8_t data;

    // センサー処理要求があるまで待機する
    xQueueReceive(xQueue, &data, portMAX_DELAY);

    // 実際の処理
    float humidity = dht.readHumidity();
    float temperature = dht.readTemperature();

    Serial.printf("湿度:%2.0f%%, 温度:%2.1f°C\n", humidity, temperature);

    // シリアルのキャッシュをフラッシュ
    Serial.flush();
  }
}

void setup() {
  Serial.begin(115200);
  delay(50);

  // 入力ボタン
  pinMode(37, INPUT);

  // DHT11初期化
  dht.begin();

  // キュー作成
  xQueue = xQueueCreate(QUEUE_LENGTH, sizeof(int8_t));

  // Core1の優先度3でタスク起動
  xTaskCreateUniversal(
    task,           // タスク関数
    "task",         // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    3,              // 優先度(大きい方が高い)
    &taskHandle,    // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );
}

// ボタン処理フラグ
bool pushFlag = false;

void loop() {
  // ボタン押したのを判定
  if (pushFlag == false && digitalRead(37) == LOW) {
    // センサー読み込み要求
    int8_t data;
    xQueueSend(xQueue, &data, 0);
    pushFlag = true;
  }

  // ボタンが離されたのでボタンフラグをOFFにして再度押せるようにする
  if (digitalRead(37) == HIGH) {
    pushFlag = false;
  }

  delay(1);
}

短い時間軸でのタイマーよりは、上記のようにボタンなどを押した際に処理を止めないように別タスクで動かすことのほうが多いかもしれません。

定期取得タイマー例

#include "DHT.h"

DHT dht(26, DHT11);

// タイマー
hw_timer_t * timer = NULL;

// 受け渡しデータ
struct QueueData {
  float humidity;
  float temperature;
  uint32_t time;
};

// キュー
#define QUEUE_LENGTH 1
QueueHandle_t xQueue;
QueueHandle_t xQueueLast;

// センサー処理用タスク
TaskHandle_t taskHandle;

// タイマー割り込み
void IRAM_ATTR onTimer() {
  int8_t data;

  // キューを送信
  xQueueSendFromISR(xQueue, &data, 0);
}

// センサー処理用タスク
void task(void *pvParameters) {
  while (1) {
    int8_t data;

    // センサー処理要求があるまで待機する
    xQueueReceive(xQueue, &data, portMAX_DELAY);

    // 実際の処理
    QueueData lastData;
    lastData.humidity = dht.readHumidity();
    lastData.temperature = dht.readTemperature();
    lastData.time = millis();

    // 上書き送信
    xQueueOverwrite(xQueueLast, &lastData);
  }
}

void setup() {
  Serial.begin(115200);
  delay(50);

  // 入力ボタン
  pinMode(37, INPUT);

  // DHT11初期化
  dht.begin();

  // キュー作成
  xQueue = xQueueCreate(QUEUE_LENGTH, sizeof(int8_t));
  xQueueLast = xQueueCreate(QUEUE_LENGTH, sizeof(QueueData));

  // Core1の優先度3でタスク起動
  xTaskCreateUniversal(
    task,           // タスク関数
    "task",         // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    3,              // 優先度(大きい方が高い)
    &taskHandle,    // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );

  // 4つあるタイマーの1つめを利用
  // 1マイクロ秒ごとにカウント(どの周波数でも)
  // true:カウントアップ
  timer = timerBegin(0, getApbFrequency() / 1000000, true);

  // タイマー割り込み設定
  timerAttachInterrupt(timer, &onTimer, true);

  // 1000000カウントにセット(=1000000マイクロ秒=1000ミリ秒=1秒)
  timerAlarmWrite(timer, 1000000, true);

  // タイマー開始
  timerAlarmEnable(timer);
}

// ボタン処理フラグ
bool pushFlag = false;

void loop() {
  // ボタン押したのを判定
  if (pushFlag == false && digitalRead(37) == LOW) {
    // センサー読み込み(キューは消さない)
    QueueData data;
    xQueuePeek(xQueueLast, &data, 0);

    Serial.printf("湿度:%2.0f%%, 温度:%2.1f°C 時間:%u\n", data.humidity, data.temperature, data.time);

    pushFlag = true;
  }

  // ボタンが離されたのでボタンフラグをOFFにして再度押せるようにする
  if (digitalRead(37) == HIGH) {
    pushFlag = false;
  }

  delay(1);
}

長さ1のキューはxQueueOverwrite()関数の上書き送信という機能があります。取得時にxQueuePeek()関数でキューの中身を消さないと、定期的に取得したデータが常に入っているキューが完成します。

キューを利用しない場合には排他制御が必要ですが、キュー自体が排他制御になっているので更新や取得するタイミングに気をつける必要はありません。本来はミューテックスを利用してデータ更新時と、取得時の両方でデータをロックする必要があります。

排他制御が入っていない場合には、温度のデータを取得し終わったタイミングで、タイマー割り込みが入ると、湿度と時間は新しいデータになってしまいます。温度と湿度のペアが重要な場合には困ったことになってしまいます。

他の温度湿度センサーについて

実際のところ、たくさんありすぎてよくわかりません。

秋月さんの一覧になります。ちょっと多すぎますのでかんたんに説明をすると、接続方式によって何種類かにわかれています。

独自方式(1線が多い)

DHT11がそうですが、電源とGNDの他に信号線が1つの3ピンのものや、信号線とGNDの2ピンしかないものもあります。1-wireと呼ばれるプロトコルが有名ですが、各社からいろいろなプロトコルで通信を実装しています。

I2C(2線)

一番一般的な方式です。I2Cは複数のセンサーなどを並行に接続することが可能です。そのため信号線を2本使いますが、使い勝手がよい方式です。通信プロトコルも固定されているため、センサーなどではよく使われます。

上記にI2Cのセンサーの一覧をまとめてみました。ただし大量にありすぎますので選ぶのが大変です。

秋月で検索をして、出てきた順番が人気順ですので上位のものから選ぶのが無難です。

個人的には高精度で計測したい場合には上記がおすすめです。ただし気圧は計測できません。SHT30という、氷点下で若干精度が落ちるセンサーもあります。通常の環境であればこちらの方が安いので使いやすいと思います。

こちらも定番のセンサーです。ただし、こちらの温度計は気圧測定用の補正センサーですので、温度の精度はあまり高くありません。気圧計として使うのであれば、おすすめです。似たセンサーでBMP280もあり、こちらは湿度が計測できないので注意してください。

SHT30(温度・湿度)+BMP280(気圧)という複数を組み合わせで使う場合もあります。

M5Stack社のENV IIユニットはこの組み合わせでした。

SPI接続(3線以上)

I2Cはそれほど高速通信ができないので、より高速通信が必要な場合にはSPI接続をする必要があります。しかしながら温度などであれば、それほど高速で通信をする必要はないので、あまり使われていません。

I2CとSPI両対応のセンサーもありますが、使いやすいモジュールの場合にはI2Cにしか対応していない場合などがあるので注意してください。

ざっくりした一覧

上記のPDFがよくまとまっています。ストロベリーリナックスさんは秋月さんよりは、精度の高い、高価なセンサーを多く取り扱っています。

まとめ

夏が近づくと電子工作を趣味にする人は、BME280を使って台風で気圧が下がるのをみんなで測定する習慣があります。

気圧計は一個ぐらいもっていても便利だと思います。M5StickCの場合にはHATとUNITがありますが、おすすめはUNITです。Groveケーブルで接続するのでちょっと長くなりますがHATで直結すると本体の熱が伝わって、あまり温度の精度が高くないみたいです。

続編

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。