記事の概要
NordicのBLEモジュールを使用して、シリアル単線(1-wireライク)通信方式の温湿度センサDHT11を制御します
他のマイコンでもGPIO端子設定を各マイコン向けの関数に置き換えれば、データ取得できるはずです。
準備
nRF52840
スイッチサイエンスさんで開発ボードを購入しました。
今回はGPIO端子のP1.09とDHT11を接続しました。
DHT11
秋月電子でDHT11を購入しました。
DHT11使用温湿度センサモジュール
DHT11温湿度モジュール(Temperature and Humidity Sensor)
シリアル単線(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出力設定にしています。
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);
}
通信開始
通信を開始するには、最初に以下の図のような信号を送受信します。
赤が出力で、緑が入力を意味します。
通信開始信号は、最低で10ms、最大で50ms、標準では20msのLow出力を送信します。
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入力の順番で信号を受信できたかを監視します。
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かを判定します。
// 端子設定が入力で、端子状態が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バイトにまとめます。
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出力にします。
static void DHT11_end(void)
{
// setting OUTPUT
nrf_gpio_cfg_output(DHT11_BUS_PIN);
nrf_gpio_pin_set(DHT11_BUS_PIN);
}
エラーで通信途中で終了してしまった場合も、出力端子に設定してHigh出力にするのを忘れないようにします。
まとめ
以上を実行すると、DHT11からのデータを取得できます。
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目の順番で処理していたのが原因でした。
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と判定します。
if(count > 28)
return 1;
else if(count > 10)
return 0;
else
return 0xFF;
頻繁にエラーになる
準備完了信号受信のカウント200usecを超えてしまうことが頻繫に発生しました。
他にもデータ受信時にも200usecを超えることもよく起きます。
色々と工夫しましたが、測定が安定することはありませんでした。
よってこれらのエラーが起きた場合は、成功するまで何度もやり直すようにしました。
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;
}
}
}
サンプルプログラム
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センサの方を使いたいと思いました
参考
今回の記事を作成するにあたり、以下を参照しました。