BLEモジュールnRF52による温湿度センサDHT11の制御

# 記事の概要
NordicのBLEモジュールを使用して、シリアル単線(1-wireライク)通信方式の温湿度センサDHT11を制御します

他のマイコンでもGPIO端子設定を各マイコン向けの関数に置き換えれば、データ取得できるはずです。

# 準備

## nRF52840
スイッチサイエンスさんで開発ボードを購入しました。

[nRF52840 MDBT50Q 評価ボード](https://www.switch-science.com/catalog/5532/)

今回はGPIO端子のP1.09とDHT11を接続しました。

## DHT11

秋月電子でDHT11を購入しました。

[ＤＨＴ１１使用温湿度センサモジュール](https://akizukidenshi.com/catalog/g/gM-15491/)
[ＤＨＴ１１温湿度モジュール（Ｔｅｍｐｅｒａｔｕｒｅ　ａｎｄ　Ｈｕｍｉｄｉｔｙ　Ｓｅｎｓｏｒ）](https://akizukidenshi.com/catalog/g/gM-07040/)


# シリアル単線(1-wireライク)通信の仕様
通信仕様の詳細は以下のマニュアルをご覧ください。
https://akizukidenshi.com/download/ds/aosong/DHT11_20180119.pdf

## IO端子の設定

シリアル単線(1-wireライク)通信は1つのIO端子だけで、送信と受信を行わないといけません。
よってGPIO端子の機能を小まめに入力と出力に切り替えます。

nRF52とDHT11が通信していない時はPull Upした入力端子か、High出力端子のどちらかにしておきます。
GPIO端子のHighからLowへの立ち下がりを合図にセンサとの通信が開始するので、IO端子がLOWだと通信開始ができません。

サンプルプログラム中では初期化時にHigh出力設定にしています。

```C:cpu_set.c  
static void gpio_init(void)
{
    ret_code_t err_code;

    err_code = nrf_drv_gpiote_init();
    APP_ERROR_CHECK(err_code);
    
    //-----------------------------------------------
    // DHT11 BUS
    nrf_gpio_cfg_output(DHT11_BUS_PIN);
    nrf_gpio_pin_set(DHT11_BUS_PIN); // default:High

    //nrf_gpio_cfg_input(DHT11_BUS_PIN, NRF_GPIO_PIN_PULLUP);
}
```

## 通信開始
通信を開始するには、最初に以下の図のような信号を送受信します。
赤が出力で、緑が入力を意味します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/0300185d-dd59-dbe6-9139-9304e6d08c68.png)

通信開始信号は、最低で10ms、最大で50ms、標準では20msのLow出力を送信します。

```C
static void DHT11_start(void)
{
    // setting OUTPUT
    nrf_gpio_cfg_output(DHT11_BUS_PIN);

    nrf_gpio_pin_set(DHT11_BUS_PIN);
    nrf_delay_ms(200);
    nrf_gpio_pin_clear(DHT11_BUS_PIN);
    nrf_delay_ms(30); // min:10ms, typ:20ms, max:50ms
    nrf_gpio_pin_set(DHT11_BUS_PIN);
    nrf_delay_us(40);
}
```



出力が終わったら、端子機能を入力端子に切り替えます。
準備完了信号はLow入力が83usec、High入力が87usecの信号です。
特に時間を測る必要がないので、Low入力→High入力→Low入力の順番で信号を受信できたかを監視します。

```C
static bool DHT11_ready_for(void)
{
    uint8_t count = 0;

    // setting INPUT
    nrf_gpio_cfg_input(DHT11_BUS_PIN, NRF_GPIO_PIN_PULLUP);

    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    // High入力待機
    count = 0;
    while(0 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    return true;
}
```



もし200usecを超えても入力信号に変化がなければエラーと判定して、通信を中止します。

## データ受信
準備完了信号の受信が終わると、続いてデータ受信が始まります。
データは1bitずつ受信されます。
Low入力とHigh入力の長さでbitが0か1かを判定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/748c3b39-c833-fc71-6db2-8c3a39049b6a.png)

```C
// 端子設定が入力で、端子状態が0で開始することを前提とする
static uint8_t DHT11_get_bit(void)
{
    uint8_t count = 0;

    // High入力待機
    count = 0;
    while(0 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return 0xFF;
    }
    
    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return 0xFF;         
    }

    // bit 0 or 1 判定
//    if(count > 65 && count < 75)
//        return 1;
//    else if(count > 20 && count < 28)
//        return 0;
//    else
//        return 0xFF;
    if(count > 28)
        return 1;
    else if(count > 10)
        return 0;
    else
        return 0xFF;
}
```

データは5byte分あるので、合計で40回bitの0と1の判定を行います。
1バイト目は湿度の整数値、2バイト目は湿度の小数点以下の値です。
3バイト目は温度の整数値、4バイト目は温度の小数点以下の値です。

5バイト目は1バイト目から4バイト目までの和の下位8bitの値で、パリティと呼びます。
データを正常に受信できたかを確認するのに使用します。
1バイト目から4バイト目までの和を計算して、パリティと一致していれば、全データが正常に受信できていたと判定できます。
今回は使用しませんでした。

ここで注意しないといけないのは、各バイトは8bit目から1bit目の順番でデータ受信することです。
逆の順番で計算すると誤った計測値になってしまいます。
8回データを受信するごとに1バイトにまとめます。

```C
static uint8_t DHT11_get_byte(void)
{
    uint8_t byte_data = 0;
    uint8_t bit_data = 0;

    for(int i=0; i<8; i++)
    {
        bit_data = DHT11_get_bit();
        if(0xFF == bit_data)
            return 0xFF;
        else
            byte_data |= (bit_data) << (7-i);
    }

    return byte_data;
}
```

## 通信終了
通信が完了したら、IO端子を出力端子に設定して、High出力にします。

```C
static void DHT11_end(void)
{
    // setting OUTPUT
    nrf_gpio_cfg_output(DHT11_BUS_PIN);

    nrf_gpio_pin_set(DHT11_BUS_PIN);
}
```

エラーで通信途中で終了してしまった場合も、出力端子に設定してHigh出力にするのを忘れないようにします。

## まとめ
以上を実行すると、DHT11からのデータを取得できます。

```C
static bool DHT11_get_data(void)
{
    uint8_t dht11_data_buf[5] = {0};

    DHT11_start();
    if(false == DHT11_ready_for())
    {
        DHT11_end();
        return false;
    }

    for(int i=0; i<5; i++)
    {
        dht11_data_buf[i] = DHT11_get_byte();
        if(0xFF == dht11_data_buf[i])
        {
            nrf_delay_ms(10); //センサからの送信完了を待機
            DHT11_end();
            return false;
        }
    }

    // データ取得成功した場合
    humidity_high    = dht11_data_buf[0];
    humidity_low     = dht11_data_buf[1];
    temperature_high = dht11_data_buf[2];
    temperature_low  = dht11_data_buf[3];
    parity           = dht11_data_buf[4];

    DHT11_end();
    return true;
}
```

# ハマった点

## 取得できる計測値が異常
データ受信時、各バイトは8bit目から1bit目の順番で処理しないといけないのに、誤って以下のように1bit目から8bit目の順番で処理していたのが原因でした。

```C
    for(int i=0; i<8; i++)
    {
        byte_data |= (bit_data) << i;
    }
```

## 入力信号の長さを正しくカウントできない

データ受信においてbit判定する際、High期間が15usec前後と45usec前後のいずれかの値になってしまいました。
仕様書では0信号は最低23usec以上、1信号は最低68usec以上、High期間を保持するはずです。
`nrf_delay_us(1)`によりusecの長さをカウントしているのですが、うまくいきません。

何度やってもうまくいかないので、やむなく仕様書とは異なる判定をすることにしました。
28usecを超えるならbit 1で、28usec以下かつ10usecを超えるならばbit 0と判定します。

```C
    if(count > 28)
        return 1;
    else if(count > 10)
        return 0;
    else
        return 0xFF;
```


## 頻繁にエラーになる

準備完了信号受信のカウント200usecを超えてしまうことが頻繫に発生しました。
他にもデータ受信時にも200usecを超えることもよく起きます。
色々と工夫しましたが、測定が安定することはありませんでした。

よってこれらのエラーが起きた場合は、成功するまで何度もやり直すようにしました。

```C
static uint8_t dht11_error_count;
void DHT11_certainty_get_data(void)
{
    uint8_t retry_count = 0;

    while(1)
    {        
        //　エラーの場合、100回までやり直す
        if(false == DHT11_get_data())
        {
            retry_count += 1;
            if(100 == retry_count)
            {
                dht11_error_count += 1;
                return;
            }
        }
        else
        {
            printf("temperature is %d.%d C\n",temperature_high, temperature_low);
            printf("humidity is %d.%d %RH\n",humidity_high, humidity_low);
            return;
        }
    }
}
```


# サンプルプログラム

```C:
static uint8_t humidity_high;
static uint8_t humidity_low;
static uint8_t temperature_high;
static uint8_t temperature_low;
static uint8_t parity;

// DHT11
#define DHT11_BUS_PIN (41) //P1.09 //32+09=41

static void gpio_init(void)
{
    ret_code_t err_code;

    err_code = nrf_drv_gpiote_init();
    APP_ERROR_CHECK(err_code);
    
    //-----------------------------------------------
    // DHT11 BUS
    nrf_gpio_cfg_output(DHT11_BUS_PIN);
    nrf_gpio_pin_set(DHT11_BUS_PIN); // default:High

    //nrf_gpio_cfg_input(DHT11_BUS_PIN, NRF_GPIO_PIN_PULLUP);
}



static void DHT11_start(void)
{
    // setting OUTPUT
    nrf_gpio_cfg_output(DHT11_BUS_PIN);

    nrf_gpio_pin_set(DHT11_BUS_PIN);
    nrf_delay_ms(200);
    nrf_gpio_pin_clear(DHT11_BUS_PIN);
    nrf_delay_ms(30); // min:10ms, typ:20ms, max:50ms
    nrf_gpio_pin_set(DHT11_BUS_PIN);
    nrf_delay_us(40);
}

static bool DHT11_ready_for(void)
{
    uint8_t count = 0;

    // setting INPUT
    nrf_gpio_cfg_input(DHT11_BUS_PIN, NRF_GPIO_PIN_PULLUP);

    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    // High入力待機
    count = 0;
    while(0 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return false;
    }

    return true;
}

// 端子設定が入力で、端子状態が0で開始することを前提とする
static uint8_t DHT11_get_bit(void)
{
    uint8_t count = 0;

    // High入力待機
    count = 0;
    while(0 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return 0xFF;
    }
    
    // Low入力待機
    count = 0;
    while(1 == nrf_gpio_pin_read(DHT11_BUS_PIN))
    {
        nrf_delay_us(1);
        count += 1;
        if(count > 200)
            return 0xFF;         
    }

    // bit 0 or 1 判定
//    if(count > 65 && count < 75)
//        return 1;
//    else if(count > 20 && count < 28)
//        return 0;
//    else
//        return 0xFF;
    if(count > 28)
        return 1;
    else if(count > 10)
        return 0;
    else
        return 0xFF;
}

static uint8_t DHT11_get_byte(void)
{
    uint8_t byte_data = 0;
    uint8_t bit_data = 0;

    for(int i=0; i<8; i++)
    {
        bit_data = DHT11_get_bit();
        if(0xFF == bit_data)
            return 0xFF;
        else
            byte_data |= (bit_data) << (7-i);
    }

    return byte_data;
}

static void DHT11_end(void)
{
    // setting OUTPUT
    nrf_gpio_cfg_output(DHT11_BUS_PIN);

    nrf_gpio_pin_set(DHT11_BUS_PIN);
}

static bool DHT11_get_data(void)
{
    uint8_t dht11_data_buf[5] = {0};

    DHT11_start();
    if(false == DHT11_ready_for())
    {
        DHT11_end();
        return false;
    }

    for(int i=0; i<5; i++)
    {
        dht11_data_buf[i] = DHT11_get_byte();
        if(0xFF == dht11_data_buf[i])
        {
            nrf_delay_ms(10); //センサからの送信完了を待機
            DHT11_end();
            return false;
        }
    }

    // データ取得成功した場合
    humidity_high    = dht11_data_buf[0];
    humidity_low     = dht11_data_buf[1];
    temperature_high = dht11_data_buf[2];
    temperature_low  = dht11_data_buf[3];
    parity           = dht11_data_buf[4];

    DHT11_end();
    return true;
}

static uint8_t dht11_error_count;
static void DHT11_certainty_get_data(void)
{
    uint8_t retry_count = 0;

    while(1)
    {        
        //　エラーの場合、100回までやり直す
        if(false == DHT11_get_data())
        {
            retry_count += 1;
            if(100 == retry_count)
            {
                dht11_error_count += 1;
                return;
            }
        }
        else
        {
            printf("temperature is %d.%d C\n",temperature_high, temperature_low);
            printf("humidity is %d.%d %RH\n",humidity_high, humidity_low);
            return;
        }
    }
}


int main(void)
{
    gpio_init();

    // Enter main loop.
    for (;;)
    {
        DHT11_certainty_get_data();
        nrf_delay_ms(1000);

        idle_state_handle();
    }
}
```

# 感想
使用した感想は、通信制御のタイミングの取り方が難しいというものでした。
今回は、正確に1usecを測れずに四苦八苦したり、計測が頻繫に失敗しました。
それに、もし計測途中に割り込みが発生したら、タイミングがずれてしまいます。

GPIO端子を1個だけしか使わず、I2Cモジュールが使えない場合も使用できるのは魅力ですが、逆に言えば、マイコンにI2Cモジュール機能があり、端子の数が足りているならば、普通のI2Cセンサの方を使いたいと思いました

# 参考
今回の記事を作成するにあたり、以下を参照しました。

[Nordic nRF52840 で温度湿度センサ DHT11 を使う](https://qiita.com/nanbuwks/items/6440657c055b1c8b457e)