DHT11をC++ & pigpiod_if2で使ってみる

## 今回の目的 ##
温湿度センサーで安価に買えるものとしてDHT11というセンサーがある。このDHT１１について調査していると、Python用のライブラリは出てくるのだが、CやC++はライブラリや情報がほとんど見つからない。筆者はC++でコードをよく書くため、他のコードでも利用できるようにC++でデータ取得できるようにしたい。

<img width="300" alt="dht11" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079355/66efd7c8-e823-caad-3211-000139a470b5.jpeg">

https://akizukidenshi.com/catalog/g/g107003/

## DHT11の制御方法 ##
DHT11はVccとGND、データ送受信用にGPIOピンを一つ使用する。まず、ラズパイ側からスタート信号を送信し、GPIOを読み取りモードに切り替え、送られてくる40bitのデータから温湿度を取得するという仕組みである。
DHT11の信号について[データシート](https://akizukidenshi.com/goodsaffix/DHT11_20180119.pdf)より解読する。

### センサーに送信するスタート信号 ###
まず、ラズパイ側からデータを要求するためにスタート信号を送信するのだが、LOWの状態を18ms以上続ければ良いようだ。
<img width="400" alt="dht11" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079355/dbebbfb0-64b5-8b5b-a4ed-0062157abd60.png">

### センサーから送られてくるデータのフォーマット ###
0bitを示す信号はHIGHが26~28μs、1bitを示す信号はHIGHが70μs、それぞれのビットの区切りにはLOWの状態が50μs続くということである。
<img width="700" alt="dht11" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079355/354cf584-145a-40a5-86ce-0a404d4db6f8.png">

GPIO制御のライブラリとして現在おそらく主流なのが`pigpio`というライブラリであるが、pigpioはC言語で書かれているようで、C用にはデーモン版(`pigpiod_if2.h`)とスタンドアロン版(`pigpio.h`)が存在する。Pythonはデーモン版のみしか存在しない。

DHT11の操作ライブラリはPythonの[DHT11_Python](https://github.com/szazo/DHT11_Python)を利用している記事が多くある。このライブラリではGPIOの値を細かい周期で読み取り、HIGHが連続して入力された回数から、送信されたbitが0か1かの判定を行っている。

また、GPIOの操作には`RPi.GPIO`を使用している。このコードを`pigpiod_if2`の関数に置き換えて同じコードを書いてみたのだが、なぜかデータがうまく取れないという問題が発覚した。

## GPIOライブラリごとにデータ取得の実験 ##
そこで、Pythonで`RPi.GPIO`、`pigpio`、C++でデーモン版、スタンドアロン版の`pigpio`でそれぞれ以下に示すコードを書いて実験を行った。スタート信号を送信し、データを受信したら表示し100回同じデータが続いたら終了するという単純なプログラムである。
### 1. RPi.GPIO(Python) ###
```python:RPi.GPIO(Python)
import time
import RPi
import RPi.GPIO as GPIO

PIN=4

#GPIO初期化
GPIO.setwarnings(True)
GPIO.setmode(GPIO.BCM)

#スタート信号送信
RPi.GPIO.setup(PIN,RPi.GPIO.OUT)
RPi.GPIO.output(PIN,1)
time.sleep(0.05)
RPi.GPIO.output(PIN,0)
time.sleep(0.02)

#読み取りモードに変更
RPi.GPIO.setup(PIN,RPi.GPIO.IN,RPi.GPIO.PUD_UP)

last = -1  
unchanged_count = 0

try:
    while True:
        current=RPi.GPIO.input(PIN)
        print(current,end=",")

        #同じ値が100回続いたら終了
        if last != current:
            unchanged_count=0
            last = current
        else:
            unchanged_count += 1
            if unchanged_count > 100:
                    break
                    
except KeyboardInterrupt:
    GPIO.cleanup()
```
### 2. pigpio(Python) ###
```python:pigpio(Python)
import time
import pigpio

PIN=4

#GPIO初期化
pi=pigpio.pi()

#スタート信号送信
pi.set_mode(PIN,pigpio.OUTPUT)
pi.write(PIN,1)
time.sleep(0.05)
pi.write(PIN,0)
time.sleep(0.02)

#読み取りモードに変更
pi.set_mode(PIN,pigpio.INPUT)
pi.set_pull_up_down(PIN, pigpio.PUD_UP)

last = -1  
unchanged_count = 0

try:
    while True:
        current=pi.read(PIN)
        print(current,end=",")

        #同じ値が100回続いたら終了
        if last != current:
            unchanged_count=0
            last = current
        else:
            unchanged_count += 1
            if unchanged_count > 100:
                    break
                    
except KeyboardInterrupt:
    pi.stop()
```
### 3.pigpiod_if2(C++) デーモン版 ###
```cpp:pigpiod_if2(C++)
#include <iostream>
#include <pigpiod_if2.h>
#include <vector>
#include <unistd.h>

#define PIN 4

int main() {
    int pi = pigpio_start(NULL, NULL); // pigpioデーモンへの接続
    if (pi < 0) {
        std::cerr << "Failed to connect to pigpio daemon" << std::endl;
        return 1;
    }

    set_mode(pi, PIN, PI_OUTPUT);
    gpio_write(pi, PIN, 0);
    usleep(20000); // 20 ms
    gpio_write(pi, PIN,1);
    usleep(40); // 40 µs

    //GPIOを読み取りモードに変更
    set_mode(pi, PIN, PI_INPUT);

    int last = -1;
    int current = -1;
    int unchanged_count = 0;
    
    // データ受信
    while (1)
    {
        std::cout << gpio_read(pi,PIN);
        
        //同じ値が100回続いたら終了
        if(last != current){
            unchanged_count=0;
            last = current;
        }else{
            unchanged_count ++;
            if(unchanged_count > 100){
                    break;
            }
        }
    }
    
    pigpio_stop(pi); // pigpioデーモンから切断
    return 0;
}
```
### 4.pigpio(C++) スタンドアロン版 ###
```cpp:pigpio(C++)
#include <iostream>
#include <pigpio.h>
#include <unistd.h>

#define PIN 4

int main()
{
    //GPIOの初期化
    if (gpioInitialise() < 0)
    {
        std::cerr << "Failed to initialize pigpio" << std::endl;
        return 1;
    }

    // スタート信号
    gpioSetMode(PIN, PI_OUTPUT);
    gpioWrite(PIN, PI_LOW);
    usleep(20000); // 20 ms
    gpioWrite(PIN,1);
    usleep(40); // 40 µs
    
    //読み取りモードに変更
    gpioSetMode(PIN, PI_INPUT);
    
    int last = -1;
    int current = -1;
    int unchanged_count = 0;
    
    // データ受信
    while (1)
    {
        std::cout << gpioRead(PIN);
        
        //同じ値が100回続いたら終了
        if(last != current){
            unchanged_count=0;
            last = current;
        }else{
            unchanged_count ++;
            if(unchanged_count > 100){
                    break;
            }
        }
    }
    
    gpioTerminate();
    return 0;
}
```
<b>※bit数の計算は省略している</b>
これらを実行すると(1)RPi.GPIOと(4)C++のスタンドアロン版pigpioを使用したものはデータが40bit受信できたのに対し、(2)Pythonのpigpioと(3)pigpiod_if2では20bitも受信できないという結果になった。画面に出力されるデータの数が明らかに違うので見ればわかると思われる。


つまり、<b>デーモン版pigpioが相性が悪い</b>と推測される、なのでGPIOの状態取得の方法を少し変えてみることにした。
[こちらの記事](https://qiita.com/dev_greentea/items/cd665700641515345e25)で使用しているPython版pigpioを使用したライブラリのコードを見てみると、コールバック関数を使用してGPIOの数値を取得していることがわかる。
これを参考にコールバックを呼び出して、ピンの状態が1になったタイミングで現在時刻を取得、0に変わったタイミングの時刻との差をとることでHIGHが出力されている時間を取得することにした。
```cpp:pigpiod_if2(C++) -成功-
#include <iostream>
#include <pigpiod_if2.h>
#include <unistd.h>
#include <vector>
#include <cstdint>
#include <climits>
#include <chrono>
#include <thread>

#define PIN 4 // データ受信PIN番号

uint32_t last_rising_tick = 0;
std::vector<int> pulse_lengths;

// コールバック関数
void cb(int pi, unsigned gpio, unsigned level, uint32_t tick)
{
    std::cout << level;
    if (level == 1)
    {
        last_rising_tick = tick; // 立ち上がり時刻を記録
    }
    else if (level == 0)
    {
        if (last_rising_tick != 0)
        {
            int pulse_length = tick - last_rising_tick; // 立ち下がり時刻から立ち上がり時刻を引いてパルス幅を算出
            pulse_lengths.push_back(pulse_length);      // パルス幅を配列に格納
            last_rising_tick = 0;                       // リセット
        }
    }
}

int main()
{
    int pi = pigpio_start(NULL, NULL); // pigpioデーモンに接続
    if (pi < 0)
    {
        std::cerr << "Failed to connect to pigpio daemon" << std::endl;
        return 1;
    }

    // スタート信号
    set_mode(pi, PIN, PI_OUTPUT);
    gpio_write(pi, PIN, PI_LOW);
    usleep(20000); // 18 ms
    gpio_write(pi, PIN, PI_HIGH);
    usleep(40); // 40 µs

    // GPIOを読み取りモードに変更
    set_mode(pi, PIN, PI_INPUT);

    // コールバック設定
    callback(pi, PIN, EITHER_EDGE, cb);

    // メインループ
    while (true)
    {
        //40回1の連続を取得できたら（40bit取得できたら）
        if (pulse_lengths.size() == 40)
        {
            break;
        }
    }

    pigpio_stop(pi); // pigpioの終了

    return 0;
}

```
この結果、データを正しく受信できた。
pigpioのデーモン版はデフォルトで5μsecでGPIOピンのサンプリングをしているようである、DHT11のパルス幅は上記に示したデータシートより20~50μsec程度なのでGPIOを読み取れると思われるのだが、、、
デーモン版はすこしリアルタイム性が低いようだが、そんなに差があるのだろうか？

もし原因がわかる方がいればご教授いただけたら幸いである。

## 結論 ##
スタンドアロン版のpigpioを使用しても良いのだが、その場合は管理者権限でのプログラムの実行が必要であるのとデーモン版のpigpioは併用して使用することができないという、微妙に嫌な感じである。

とりあえずC++で動かしたい人は筆者の作成したライブラリを参考にしていただけたらと思う。(まだ開発中...)

https://github.com/takuteh/DHT11_Cpp.git

そのうち各ライブラリのGPIO読み取りの周期を調査してみてもいいかもしれない。


### 参考サイト ###
以下のサイトを参考にさせていただきました

https://negligible.hatenablog.com/entry/2021/03/26/035001#%E8%A6%81%E6%B1%82%E3%81%95%E3%82%8C%E3%82%8B%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E6%80%A7

https://qiita.com/girlfellfromsky/items/b577b03fe52d273b328f

https://qiita.com/Ko_Kiduchi/items/9eed09e4e24ecb6e5393

https://www.souichi.club/technology/dht11-datasheet/