Orange Pi OneでDHT11のセンサ情報を読み出す

 Orange Pi OneのGPIOを試用してみたいと思いつつ、GPIOの40ピンヘッダに指すコネクタが手持ちに無いため足踏み状態でした。が、手持ちに温湿度センサDHT11のピンソケットが付いたジャンパ線ならあることに気付きました。
 早速DHT11をOrange Pi OneのGPIOに接続して、DHT11のセンサ情報を読み出してみたいと思います。
f:id:kachine:20160908182049j:plain
 

前提

  • Armbianを導入済
  • WiringOpを導入済

 

配線

  • DHT11のGND端子を、GPIO端子の6番ピン(GND)に接続
  • DHT11のVCC端子を、GPIO端子の1番ピン(3.3V)に接続
  • DHT11のプルアップしたDATA端子を、GPIO端子の7番ピン(GPIO.7)に接続
 +-----+-----+----------+------+---+--OrangePiOne-+------+----------+-----+-----+
 | BCM | wPi |   Name   | Mode | V | Physical | V | Mode | Name     | wPi | BCM |
 +-----+-----+----------+------+---+----++----+---+------+----------+-----+-----+
 |     |     |     3.3v |      |   |  1 || 2  |   |      | 5v       |     |     |
 |   2 |   8 |    SDA.0 | ALT5 | 0 |  3 || 4  |   |      | 5V       |     |     |
 |   3 |   9 |    SCL.0 | ALT5 | 0 |  5 || 6  |   |      | 0v       |     |     |
 |   4 |   7 |   GPIO.7 |   IN | 1 |  7 || 8  | 0 | OUT  | TxD3     | 15  | 14  |
 |     |     |       0v |      |   |  9 || 10 | 0 | OUT  | RxD3     | 16  | 15  |
 |  17 |   0 |     RxD2 |  OUT | 0 | 11 || 12 | 0 | OUT  | GPIO.1   | 1   | 18  |
 |  27 |   2 |     TxD2 |  OUT | 0 | 13 || 14 |   |      | 0v       |     |     |
 |  22 |   3 |     CTS2 |  OUT | 0 | 15 || 16 | 0 | OUT  | GPIO.4   | 4   | 23  |
 |     |     |     3.3v |      |   | 17 || 18 | 0 | OUT  | GPIO.5   | 5   | 24  |
 |  10 |  12 |     MOSI | ALT4 | 0 | 19 || 20 |   |      | 0v       |     |     |
 |   9 |  13 |     MISO | ALT4 | 0 | 21 || 22 | 0 | OUT  | RTS2     | 6   | 25  |
 |  11 |  14 |     SCLK | ALT4 | 0 | 23 || 24 | 0 | ALT4 | CE0      | 10  | 8   |
 |     |     |       0v |      |   | 25 || 26 | 0 | OUT  | GPIO.11  | 11  | 7   |
 |   0 |  30 |    SDA.1 | ALT4 | 0 | 27 || 28 | 0 | ALT4 | SCL.1    | 31  | 1   |
 |   5 |  21 |  GPIO.21 |  OUT | 0 | 29 || 30 |   |      | 0v       |     |     |
 |   6 |  22 |  GPIO.22 |  OUT | 0 | 31 || 32 | 0 | OUT  | RTS1     | 26  | 12  |
 |  13 |  23 |  GPIO.23 |  OUT | 0 | 33 || 34 |   |      | 0v       |     |     |
 |  19 |  24 |  GPIO.24 |  OUT | 0 | 35 || 36 | 0 | OUT  | CTS1     | 27  | 16  |
 |  26 |  25 |  GPIO.25 | ALT3 | 0 | 37 || 38 | 0 | OUT  | TxD1     | 28  | 20  |
 |     |     |       0v |      |   | 39 || 40 | 0 | OUT  | RxD1     | 29  | 21  |
 +-----+-----+----------+------+---+----++----+---+------+----------+-----+-----+
 | BCM | wPi |   Name   | Mode | V | Physical | V | Mode | Name     | wPi | BCM |
 +-----+-----+----------+------+---+--OrangePiOne-+------+----------+-----+-----+

f:id:kachine:20160908181820j:plain
 DHT11は単品でも売られていますが、プルアップ抵抗と共に小型基板に実装されたものも各社から流通しています。価格差はあまりないため、上記画像からも判りますが後者のタイプを利用しています。

 

ソフトウェア

 私は元々Arduinoで使用するためにDHT11が手元にあるのですが、Arduinoとは違いOrange Pi用DHT11ライブラリ的なものはもちろん無さそうです。Raspberry Pi用のDHT11ライブラリ的なものを探してみたところ、Python用が見つかりましたが、私はPython使ったことが無いのでパス。
 で、C言語で扱えるライブラリを探したところ無さそう。ですが、サンプルコードは見付けることができました。
RPiBlog: Interfacing Temperature and Humidity Sensor (DHT11) With Raspberry Pi

 Raspberry Pi用のコードなので、Orange Piではそのまま動かないんだろうなと思っていたら、WiringOPが存在するおかげで特に何も修正せずに、あっけなく動作しました。

 動いてうれしい反面、ただコピペしただけでこのコードが何しているのかも解らず、コード中の不思議なマジックナンバーが何なのかも解らず、結果的に何のスキルも身についた気がしません。
 また、上記のサンプルコードはオープンソースともパブリックドメインとも明示されておらず、コメント欄に記載されている以下の問合せも回答されておらず、ちょっとした実験用ならともかく、流用して何かを作るのは怖い気もします。

Hello is your code open source an can i use it in my own project?

 ということで、DHT11のデータシートを読みつつ、スクラッチでコード書いてみることにしました。
 データシートは秋月が公開している、以下のものを参照しました(ネット上には異なる仕様書も存在しており、DHT11の製造元って実は複数あるんでしょうかね?)。
DHT11 Product Manual(PDF)
 

DHT11から温度と湿度を取得するコード(C言語)

// -----------------------------------------------------------------------------
// Title:
//   DHT11 Temparature & Humidity Sensor Reader for OrangePi using WiringOP
// -----------------------------------------------------------------------------
// Date       ModifiedPerson Note
// =============================================================================
// 2016/09/07 kachine        initial
// =============================================================================
// How to compile:
//   gcc -o readDHT11 readDHT11.c -L/usr/local/lib -lwiringPi -lpthread
// How to run: (need root privilege to use WiringPi)
//   sudo ./readDHT11
// -----------------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <wiringPi.h>

#define DHT11_DATAPIN             7       // PIN# OF DHT11 DATA CONNECTED, MODIFY IF REQUIRED
#define DHT11_START_SIG_WIDTH    20       // Pulse width of DHT11 start signal is more than 18ms
#define DHT11_NUM_OF_PULSE        1+40    // # OF DHT11 PULSE (first response signal + 40bit data)
#define DHT11_MAX_PULSE_WIDTH    80       // Max pulse width of DHT11 response signal is 80us
#define DHT11_NUM_OF_DATA        5        // # OF DHT11 DATA (High humidity, Low humidity, High temp, Low temp, Parity)
#define RET_OK                    0       // Return code OK
#define RET_ERR                    -1     // Return code Error

int readDHT11(float *Humidity, float *Temparature)
{
    // variable to keep Return code
    int RET_CODE = RET_OK;
    // variable to keep previous pulse state
    int stts = HIGH;
    // array to keep pulse width from DHT11, each pulse has lower & higher state (DHT11_NUM_OF_PULSE * 2) + start of pulse and end of pulse (+2)
    int pulse[DHT11_NUM_OF_PULSE * 2 + 2] = {1};
    // array to store final data acquired from DHT11
    unsigned int DHT11_DATA[DHT11_NUM_OF_DATA] = {0};
//    // acquired data from DHT11 in char format(for debug)
//    char data[DHT11_NUM_OF_PULSE+1]="";
    
    // Host sends out Start signal >18ms
    pinMode(DHT11_DATAPIN, OUTPUT);
    digitalWrite(DHT11_DATAPIN, LOW);
    delay(DHT11_START_SIG_WIDTH);
    // Pull up
    digitalWrite(DHT11_DATAPIN, HIGH);
    // Read from DHT11
    pinMode(DHT11_DATAPIN, INPUT);
    
    // Measure each pulse state's duration
    for(int i = 0; i < DHT11_NUM_OF_PULSE * 2 + 2; i++)
    {
        // Get pulse state
        stts = digitalRead(DHT11_DATAPIN);
        delayMicroseconds(1);
        
        // Wait until pulse state changes or timeout
        while(digitalRead(DHT11_DATAPIN) == stts){
            delayMicroseconds(1);
            if(++pulse[i] == DHT11_MAX_PULSE_WIDTH){
                break;
            }
        }
        // extract data bit into byte
        if(i >= 3 && (i % 2) == 0){
            // If the first state is longer than next, it means 0 in DHT11 data
            if(pulse[i-1] > pulse[i]){
//                sprintf(data, "%s0", data);
            }
            else{
                DHT11_DATA[( i / 2 - 2) / 8] += (unsigned int) 1 << 8 - ((i / 2 - 2) % 8 + 1);
//                sprintf(data, "%s1", data);
            }
        }
    }
//     printf("Received data(bin): %s\n",data);
    
    // Check checksum
    if (DHT11_DATA[4] == (DHT11_DATA[0] + DHT11_DATA[1] + DHT11_DATA[2] + DHT11_DATA[3]) & 0xFF ){
//        printf("Humidity= %d.%d %%, Temperature= %d.%d (in Celsius)\n", DHT11_DATA[0], DHT11_DATA[1], DHT11_DATA[2], DHT11_DATA[3]);
        // merge decimal and fractional data and integer into float (each DHT11_DATA is 8bit(3digit) data)
        *Humidity = (float) DHT11_DATA[1] / 1000 + DHT11_DATA[0];
        *Temparature = (float) DHT11_DATA[3] / 1000 + DHT11_DATA[2];
        RET_CODE = RET_OK;
    }
    else
    {
//        printf("Checksum error\n");
        RET_CODE = RET_ERR;
    }
    
    return RET_CODE;
}


int main(void)
{
    time_t timeNow;
    struct tm *tmLocal;
    float fHumidity, fTemparature;
    
    // Initialize WiringPi
    if(wiringPiSetup()){
        printf("Failed to initialize WiringPi(WiringOp)\n");
        exit(RET_ERR);
    }

    // To get realtime data, call readDHT11 twice, 1st time data will be ignored
    while(readDHT11(&fHumidity, &fTemparature))
    {
            delay(100);
    }
    // call readDHT11 2nd time
    while(readDHT11(&fHumidity, &fTemparature))
    {
            delay(100);
    }
    
    // Get current datetime
    timeNow = time(NULL);
    tmLocal = localtime(&timeNow);
    
    // Output
    printf("%04d/%02d/%02d %02d:%02d:%02d Temperature: %3.3f (in Celsius), Humidity: %3.3f %%\n", tmLocal->tm_year + 1900, tmLocal->tm_mon + 1, tmLocal->tm_mday, tmLocal->tm_hour, tmLocal->tm_min, tmLocal->tm_sec, fTemparature, fHumidity);
    
    return RET_OK;
}

 これを実行すると、以下のような結果が得られます。

$ sudo ./readDHT11
2016/09/07 19:17:26 Temperature: 30.000 (in Celsius), Humidity: 19.000 %

 ということで、DHT11を繋いでOrange Pi OneのGPIOを始めて使って見たわけですが、GPIOが正しく制御でき、プログラム中からまともに扱えることが解りました。

 このコード自体は単にセンサー情報読み出しているだけで、実用性は低いですが、cronにでも登録して適当にリダイレクトしてファイルに書き出すようにでもしたら温湿度ロガーとして使えます。

 これに赤外線LEDを組み合わせ、「家の温度が高かったら帰宅前にスマートフォンからエアコンの電源を入れられるようにする」ようなWebアプリもOrange Piで実現できそうですね(事前にエアコンの赤外線リモコン信号の解析が必要ですが)。

 といった構想はいくらでも広がりますが、上記コードで行っていることの要点を、自分が忘れないためにも簡単に記載しておきます。

DHT11読み出しコードの要点
  • タイミングチャート(DHT11 Product ManualのP.4に掲載のData Timing Diagram)に従って、OrangePi⇔DHT11間の信号(パルス)のやり取りを行う
  • OrangePi側から開始信号として18ms以上のパルス幅の信号を送出することから始まる
  • DHT11の応答信号を受け取り、続いて流れてくる40bit相当の信号がデータ本体
  • 応答信号のパルス幅も、40bit相当の信号のパルス幅も決まっている
  • 最初に来るのは応答信号なので、特に何もせずにスルーする
  • データ部のパルスのLOWとHIGHの時間がLOWの方が長ければ0と判断できる
    (DHT11 Product ManualのP.6に掲載のBit data "0" data format/Bit data "1" data formatの図には具体的な時間が記載されているが、単純化するとLOW状態とHIGH状態どちらが長いかで0/1を識別できる)
  • データ部の40bit相当の信号は、DHT11 Product ManualのP.4に掲載のData formatの通り、整数部湿度(8bit)・小数部湿度(8bit)・整数部温度(8bit)・少数部温度(8bit)・チェックサム(8bit)の順に流れてくる
  • 1パルスの終了は2回状態が変わった(LOW⇒HIGH/HIGH⇒LOW)ことで識別可能
  • データ部のパルスが終了する都度、0を意味するパルスだったか、1を意味するパルスだったか判定する
  • 1だったら当該パルスの送られた順序(8bit中の何ビット目か)に従って、ビットシフトして加算して2進数から変換する
  • チェックサムはDHT11 Product ManualのP.4に掲載のParity bit data definitionの通り、整数部湿度(8bit)・小数部湿度(8bit)・整数部温度(8bit)・少数部温度(8bit)を加算した値の下位8bit
  • チェックサムが一致しないことが割と普通に発生する
  • チェックサムが一致しない場合はreadDHT11()関数をエラーとして、main()関数からリトライさせる
  • DHT11から2回読み出して1回目の値は使わない(DHT11 Product ManualのP.4に1回目の値は前回の測定値で、リアルタイムな測定値が欲しければ2回目の値を使えと記載がある)

 
 自分で書いてみると、先のサンプルソースマジックナンバーが何だったのかも見えてきますね。
 タイミングチャート見るのも久しぶりだし、タイミングチャートほどではないもののピュアなC言語も久しぶりで、たまには頭使わないとどんどん忘れるということを痛感しました。
 



以上。