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

DHT11の制御方法
DHT11はVccとGND、データ送受信用にGPIOピンを一つ使用する。まず、ラズパイ側からスタート信号を送信し、GPIOを読み取りモードに切り替え、送られてくる40bitのデータから温湿度を取得するという仕組みである。
DHT11の信号についてデータシートより解読する。
センサーに送信するスタート信号
まず、ラズパイ側からデータを要求するためにスタート信号を送信するのだが、LOWの状態を18ms以上続ければ良いようだ。
センサーから送られてくるデータのフォーマット
0bitを示す信号はHIGHが26~28μs、1bitを示す信号はHIGHが70μs、それぞれのビットの区切りにはLOWの状態が50μs続くということである。
GPIO制御のライブラリとして現在おそらく主流なのがpigpio
というライブラリであるが、pigpioはC言語で書かれているようで、C用にはデーモン版(pigpiod_if2.h
)とスタンドアロン版(pigpio.h
)が存在する。Pythonはデーモン版のみしか存在しない。
DHT11の操作ライブラリはPythonのDHT11_Pythonを利用している記事が多くある。このライブラリではGPIOの値を細かい周期で読み取り、HIGHが連続して入力された回数から、送信されたbitが0か1かの判定を行っている。
また、GPIOの操作にはRPi.GPIO
を使用している。このコードをpigpiod_if2
の関数に置き換えて同じコードを書いてみたのだが、なぜかデータがうまく取れないという問題が発覚した。
GPIOライブラリごとにデータ取得の実験
そこで、PythonでRPi.GPIO
、pigpio
、C++でデーモン版、スタンドアロン版のpigpio
でそれぞれ以下に示すコードを書いて実験を行った。スタート信号を送信し、データを受信したら表示し100回同じデータが続いたら終了するという単純なプログラムである。
1. 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)
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++) デーモン版
#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++) スタンドアロン版
#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;
}
※bit数の計算は省略している
これらを実行すると(1)RPi.GPIOと(4)C++のスタンドアロン版pigpioを使用したものはデータが40bit受信できたのに対し、(2)Pythonのpigpioと(3)pigpiod_if2では20bitも受信できないという結果になった。画面に出力されるデータの数が明らかに違うので見ればわかると思われる。
つまり、デーモン版pigpioが相性が悪いと推測される、なのでGPIOの状態取得の方法を少し変えてみることにした。
こちらの記事で使用しているPython版pigpioを使用したライブラリのコードを見てみると、コールバック関数を使用してGPIOの数値を取得していることがわかる。
これを参考にコールバックを呼び出して、ピンの状態が1になったタイミングで現在時刻を取得、0に変わったタイミングの時刻との差をとることでHIGHが出力されている時間を取得することにした。
#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++で動かしたい人は筆者の作成したライブラリを参考にしていただけたらと思う。(まだ開発中...)
そのうち各ライブラリのGPIO読み取りの周期を調査してみてもいいかもしれない。
参考サイト
以下のサイトを参考にさせていただきました