この記事では、温湿度センサDHT11の値を、専用ライブラリを使わずに読み取ります。データシートのタイミング仕様を自分で読み解いてセンサ入力値を取得できるようになりましょう。

DHT11はDHT.hのようなライブラリを使えば数行で読めます。ただ、それでは「なぜセンサ値を取得できるか」が分かりません。実務でセンサやICをつなぐと、ライブラリが用意されていないことは珍しくなく、そのときはデータシートを見て通信を自分で組みます。DHT11は、その練習にちょうどよい題材だと思います。

まずライブラリで動かす形を示し、次にスタート信号・応答・40ビットのデータを、パルスの長さを測って自力で読み取ります。パルスの長さを測るには、前の記事で扱った時間の考え方を使います。

<自己紹介>
筆者は現役の車載組込みソフトエンジニアです。センサのデータシートを読み、タイミングや特性を解析する作業は、未経験のころ一番苦戦したところでもあります。だからこそ、データシートのどこをどう読むかを具体的に示しながら進めます。

DHT11で学ぶこと

DHT11は、1本の信号線で温度と湿度を送ってくる安価なセンサです。データは決まった時間の並び(タイミング)で送られるので、その仕様どおりに読み取ります。

このテーマの要点

  • DHT11は1本の信号線で、40ビット(5バイト)のデータを送る
  • マイコンがスタート信号を送ると、DHT11が応答してデータを返す
  • 各ビットはHighの長さで0か1が決まる(短い=0、長い=1)
  • パルスの長さは、時間を測る関数micros()で測る

本記事のライブラリなし版は、Arduino Uno(ATmega328P)向けにPlatformIOでコンパイル確認しました。実際の温湿度が正しく読めるかは、手元のDHT11とUno R3で確認しながら進めてください。

準備するもの

使うもの 用途
Arduino Uno R3またはR3互換ボード 本記事はUno R3(ATmega328P)を前提にする
DHT11センサ(モジュール) 温度と湿度を測る。モジュール版はプルアップ抵抗が載っていることが多い
ジャンパ線・USBケーブル 配線と、シリアルでの値の確認に使う

信号線はD2につなぐ前提で説明します。値を画面で確認するので、先にシリアルのドリルを、時間の測り方はタイマのドリルを済ませておくと理解しやすくなります。

DHT11を配線する

DHT11モジュールは、Arduinoの5VをVCC、D2をDATA、GNDをGNDへつなぎます。モジュールによって端子の並びが異なるため、位置ではなく基板上の印字を確認してください。

Arduino Uno R3とDHT11モジュールの配線図。5VをVCC、D2をDATA、GNDをGNDへ接続する
図:DHT11モジュール版の配線。端子の並びではなくVCC・DATA・GNDの印字を確認する

基板の付いていない単体4ピンのDHT11では、正面から見て左からVCC、DATA、未接続、GNDです。DATAと5Vの間には約5kΩのプルアップ抵抗を入れます。

Arduino Uno R3と単体4ピンDHT11の配線図。DATAをD2へ接続し、約5kΩで5Vへプルアップする
図:単体4ピンDHT11の配線。3番ピンは接続せず、DATAを約5kΩでプルアップする

もし上記図で分からない場合は以下サイトでも配線図がきれいに書かれているため参考にしてください。
https://docs.sunfounder.com/projects/ultimate-sensor-kit/ja/latest/components_basic/12-component_dht11.html

まずライブラリでDHT11を読む

最初に、専用ライブラリを使った読み方を見ておきます。マイコン下回りの処理が作成済みの関数なので、数行で温度と湿度が取れます。

dht_library.ino

#include <DHT.h>

DHT dht(2, DHT11);   // D2に接続、DHT11を指定

void setup() {
    Serial.begin(9600);
    dht.begin();
}

void loop() {
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    Serial.print("humidity=");
    Serial.print(h);
    Serial.print("% temp=");
    Serial.print(t);
    Serial.println("C");

    delay(2000);     // 連続読み取りは1秒以上あける
}

これで動きますが、readHumidity()の中で何が起きているかは見えません。ここからは、この関数がやっていることを自分で書いてみます。

次にライブラリなしで読む

ライブラリなしで読むには、DHT11がどんな順番・どんな長さで信号を送るかを知る必要があります。これはレジスタのビットではなく、時間の並び(タイミング)としてデータシートに書かれています。まずそこを読み解きます。

データシートからタイミングを読み解く

DHT11のデータシートには、通信の手順が時間つきで載っています。順番に整理すると次のようになります。

① マイコンからスタート信号を送る

まずマイコンが「データをください」と合図します。信号線を一定時間Lowにしてから、Highに戻します。

動作 レベル 時間の目安
マイコンがLowにする Low 18ms以上
マイコンがHighに戻して入力へ切り替える High 20〜40µs

② DHT11の応答を確認する

スタート信号を受けると、DHT11が応答を返します。この並びを確認できたら、データが続く合図です。

動作 レベル 時間の目安
DHT11が応答(Low) Low 約80µs
DHT11が応答(High) High 約80µs

③ 40ビットのデータを、Highの長さで読む

続いて40ビットが送られます。各ビットは、まず約50µsのLowがあり、そのあとのHighの長さで0か1が決まります。短ければ0、長ければ1です。

ビットの値 先頭のLow 続くHighの長さ
0 約50µs 約26〜28µs(短い)
1 約50µs 約70µs(長い)
DHT11通信のタイミング図。スタート信号、センサ応答、ビット0と1のHigh時間の違いを示す
図:DHT11のスタート信号・応答と、ビット0と1を区別するHigh時間

0と1でHighの長さがはっきり違うので、間をとって約40µsより長ければ1、短ければ0と判定できます。40ビットは、次の5バイトの意味を持ちます。

バイト 意味
1バイト目 湿度の整数部
2バイト目 湿度の小数部(DHT11は0のことが多い)
3バイト目 温度の整数部
4バイト目 温度の小数部
5バイト目 チェックサム(1〜4バイト目の和の下位8ビット)

チェックサムは、読み取りが正しかったかの確認用です。1〜4バイト目を足し合わせ、その下位8ビットが5バイト目と一致すれば、データは信用できます。

dht11_read.ino

#define DHT_PIN 2
uint8_t dht_data[5];

// 指定レベルが続く間のマイクロ秒を、タイムアウト付きで測る
long wait_level(uint8_t level, unsigned long timeout_us) {
    unsigned long start = micros();
    while (digitalRead(DHT_PIN) == level) {
        if (micros() - start > timeout_us) {
            return -1;
        }
    }
    return (long)(micros() - start);
}

bool dht11_read(void) {
    uint8_t bits[5] = {0, 0, 0, 0, 0};

    // (1) スタート信号:Lowを18ms以上→High
    pinMode(DHT_PIN, OUTPUT);
    digitalWrite(DHT_PIN, LOW);
    delay(20);
    digitalWrite(DHT_PIN, HIGH);
    delayMicroseconds(30);
    pinMode(DHT_PIN, INPUT_PULLUP);

    // (2) 応答:Low約80us→High約80us
    if (wait_level(HIGH, 100) < 0) return false;
    if (wait_level(LOW, 100) < 0) return false;
    if (wait_level(HIGH, 100) < 0) return false;

    // (3) 40bitを読む:Highの長さで0/1を判定
    for (uint8_t i = 0; i < 40; i++) {
        if (wait_level(LOW, 100) < 0) return false;
        long high_len = wait_level(HIGH, 200);
        if (high_len < 0) return false;

        bits[i / 8] <<= 1;
        if (high_len > 40) {          // 約40usより長ければ1
            bits[i / 8] |= 1;
        }
    }

    // (4) チェックサム
    uint8_t sum = bits[0] + bits + bits + bits;
    if (sum != bits) return false;

    for (uint8_t i = 0; i < 5; i++) dht_data[i] = bits[i];
    return true;
}
ライブラリ版との対応dht.readHumidity()は、このスタート信号の送信、応答の確認、40ビットの読み取り、チェックサムの照合をまとめて肩代わりしていました。中身を1つずつ書くと、ライブラリが何をしていたかが見えてきます。

練習問題(基礎5+発展2)

基礎5問は、プロトコルの読み取りとデータの意味を確認します。発展2問は、パルス幅の判定とチェックサムを扱います。まず自分で考えてから回答を開いてください。

基礎 Q1

DHT11の通信で、最初にマイコンが送る「スタート信号」は、どのくらいの時間Lowにしますか。

解答・解説を見る

解答:18ms以上。

解説:マイコンが信号線を18ms以上LowにしてからHighへ戻すと、DHT11がスタート信号だと認識して応答を返します。短すぎると認識されません。

基礎 Q2

各ビットの0と1は、何の長さで見分けますか。

解答・解説を見る

解答:Lowのあとに続くHighの長さ。短ければ0、長ければ1。

解説:0は約26〜28µs、1は約70µsです。間の約40µsをしきい値にすれば、どちらかを判定できます。

基礎 Q3

DHT11が送る40ビットは5バイトに分かれます。3バイト目は何のデータですか。

解答・解説を見る

解答:温度の整数部。

解説:1バイト目が湿度の整数部、2バイト目が湿度の小数部、3バイト目が温度の整数部、4バイト目が温度の小数部、5バイト目がチェックサムです。

基礎 Q4

読み取った値が正しいかを確認する5バイト目(チェックサム)は、どう計算しますか。

解答・解説を見る
uint8_t sum = bits[0] + bits + bits + bits;
// sum の下位8ビットが bits と一致すればOK

解説:1〜4バイト目を足し、その下位8ビットが5バイト目と同じなら、通信は正しく行えたと判断できます。uint8_tで足すと、下位8ビットだけが自然に残ります。

基礎 Q5

DHT11を続けて読むとき、読み取りの間隔はどのくらいあけますか。

解答・解説を見る

解答:1秒以上(本記事のコードでは2秒)。

解説:DHT11は測定に時間がかかるため、続けて読むと値が更新されません。データシートでも読み取り間隔をあけるよう指定されています。

発展 Q6

Highの長さhigh_lenから、そのビットの0/1をbitsに積んでいく処理を書いてください(しきい値は40µs)。

解答・解説を見る
q6_bit.ino

bits[i / 8] <<= 1;          // 1つ左へずらして場所を空ける
if (high_len > 40) {
    bits[i / 8] |= 1;      // 長ければ最下位ビットを1に
}

解説:ビットを左へシフトしてから、1のときだけ最下位を立てます。8ビットで1バイトになるので、i / 8で入れる先のバイトを選びます。シフトの考え方はC言語 ビット演算のドリルと同じです。

発展 Q7

信号が返ってこないときに固まらないよう、待ち処理にはどんな工夫が要りますか。

解答・解説を見る

解答:タイムアウトを設ける(一定時間を超えたら失敗として抜ける)。

解説:本記事のwait_level()は、micros()で経過時間を測り、指定時間を超えたら-1を返します。センサが未接続でもwhileで止まり続けないための工夫です。実務でも、外部からの応答待ちには必ずタイムアウトを入れます。

例:実務で実際に書くコード

実務でも、ライブラリのないセンサやICは、データシートのタイミング図を見て通信を組みます。DHT11で身につけた「スタート信号を送る→応答を待つ→決められた並びで読む→チェックサムで確かめる」という流れは、多くのセンサに共通します。

sensor_read.c

/* 応答待ちには必ずタイムアウトを付ける */
if (wait_response(TIMEOUT) < 0) {
    return ERR_TIMEOUT;
}

/* 受信データは、チェックサムやCRCで必ず検証する */
if (calc_checksum(buf) != buf[LAST]) {
    return ERR_CHECKSUM;
}

車載の通信でも、受け取ったデータをそのまま信じず、チェックサムやCRCで壊れていないかを確かめます。「応答にはタイムアウト、受信データには検証」は、外部と通信するときの基本の作法です。総合課題でも、DHT11の値をこの形で読み、異常なら使わないようにします。

最後につまずきポイントをおさらい

読み取り間隔をあけないと値が更新されないDHT11は測定に時間がかかります。1秒より短い間隔で読むと、前の値が返ってきたり、読み取りに失敗したりします。連続して読むときは、2秒ほど間隔をあけてください。

もう一つは、タイミングがずれると0と1を読み違える点です。delayMicroseconds()micros()の精度、割り込みによる遅れが影響することもあります。読み取り中は割り込みを一時的に止める方法もありますが、まずは間隔をあけて何度か読み、チェックサムが合う値だけを使うのが確実。DHT11は湿度の小数部が0のことが多く、より細かく測りたいときはDHT22などの上位のセンサを使います。

よくある質問(FAQ)

ライブラリを使ってはいけないのですか?

実際の開発ではライブラリを使ってかまいません。このドリルの目的は、ライブラリの中で何が起きているかを理解することです。中身が分かっていれば、ライブラリがないセンサや、うまく動かないときにも対応できます。

読み取りに失敗します

配線(信号線がD2か、電源とGNDが合っているか)、読み取り間隔(2秒ほどあけているか)をまず確認します。モジュール版でない素のDHT11では、信号線と電源の間にプルアップ抵抗が必要です。チェックサムが合わない値は使わず、次の読み取りを待つのが安全です。

DHT11とDHT22の違いは何ですか?

通信の考え方はほぼ同じですが、DHT22のほうが測定範囲が広く、小数部まで細かく測れます。まずDHT11で読み方を身につければ、DHT22へも同じ手順で応用できます。

次に読む

センサの値を読めるようになったら、次は出力側です。赤外線リモコンの信号を解析して、エアコンなどの家電を操作する練習へ進みます。DHT11で身につけた「タイミングを読む」考え方が、今度は信号を送る側で生きてきます。