こんにちは、ENGかぴです。
Grove Beginner kit for Arduinoは初心者のためのスターターキットです。湿度・温度センサーなど複数のセンサーが実装されておりArduinoライブラリの使い方やC言語(C++)の学習ができます。
本記事ではGrove Beginner Kit for Arduino(Seeed Studio製)を使用してDHT11の湿度・温度の情報を取得してOLEDに表示します。
Grove Beginner kit for Arduino(以下スターターキットとします。)はArduino UNOと同じシリーズのマイコンを使用しているためArduino UNOと同じように開発することができます。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
開発環境の作り方
スターターキットの詳細はSeeed Studioのページにまとめられています。Arduinoライブラリの説明やセンサーの使用例やPWMの考え方などが解説されています。
Grove Beginner Kit for Arduino – Seeed Wiki (seeedstudio.com)
日本語対応のページが作成されていませんが、Google Chromeのブラウザーによる変換機能を使用することで日本語でページを表示することができます。
スターターキットにはソフトがプリインストールされているためUSBを接続すると各センサーの動作を確認することができます。プリインストールされているソフトは公開されているのでいつでも初期状態に戻すことができます。
タイマ管理で使用するMsTimer2ライブラリとOLEDの表示を行うU8g2ライブラリは標準ライブラリとして搭載されていないためライブラリマネージャーで追加します。ライブラリの追加の仕方や使用例については下記記事にまとめています。
Grove Beginner Kit for Arduinoの使い方
Grove Beginner Kit for Arduinoの照度センサーの使い方
MsTimer2はDHT11でデータを測定するタイミングを管理し、U8g2は取得したデータをOLEDに表示する目的で使用します。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
DHT11からデータを取得する
DHT11は温度・湿度のデータをシングルバス方式で取得します。シングルバス方式は一つのバスラインでデータの要求と受信を行うものです。ArduinoのDI/DOのポートを1本使ってデータの要求と受信を行います。
DO/DIポート一つでデータが取得でいるメリットがありますが、DHT11の上位互換であるDHT20等のセンサーモジュールの多くがI2C通信を使用するものが多く使用頻度は多くない印象です。
シングルバス方式の例をデータシートを引用して説明します。Host signal(黒線)はArduinoから出力するDOによって行います。DOでstart signalを出力するとDHT11(Signal from the machine)が応答してArduinoにデータを返信します。
- 手順1DOに切り替えてHighを出力
start signal出力の為DOポートに切り替えてHighを出力して待機する。
- 手順2start signalによるデータの要求
DOをLowにして18ms以上待機してDOをHighに戻す。Highに戻した後、数us程度のウェイトする。
- 手順3DOをDIに切り替えて受信待機
DOをDI切り替えて待機する。DHT11からデータの応答信号(The response signal)が送信されるので読み飛ばして、以降の湿度・温度データ取得のため待機する
- 手順4湿度・温度のデータとチェックデジットを取得する
1bit分のデータをHighの間隔の長さで1か0を判断しながら40bit分取得する。
- 手順5チェックデジットの確認
湿度・温度のデータを加算した結果とチェックデジット(Check digit)を比較して一致した場合、有効なデータとして受け入れてデータを換算する。
手順1はstart singnalはHighからスタートするのでDOでHighを出力して待機します。測定開始のトリガーはDOをLowにしたタイミングです。
手順2はデータシートによると18ms以上Lowを維持した後にHighに戻します。after releasing the bus master pullingの期間をHighに維持するため数usウェイトを置いてからDOをDIに切り替えます。
手順3はDOをDIに切り替えてDHT11からのデータの返信を待機します。応答信号(The response signal)として80usのパルス信号が送信されるので読み飛ばします。以降がBegins to transmit dataとなり取得するデータになります。
手順4は湿度・温度のデータとチェックデジットを含むデータを1bitずつ合計40bit分送信されるので各ビットの0か1を判断しながらデータを格納します。0か1の判断の方法についてはDHT11のデータのHighとLowを判定して格納の項で説明しています。
手順5はDHT11から取得した湿度・温度のデータからチェックデジットを計算して、DHT11から取得したチェックデジットと比較します。比較結果が一致する場合はデータが健全であると判断してデータを受け入れます。
DHT11のデータのHighとLowを判定して格納
DHT11が送信するデータフォーマットは最初にLowを50us出力します。その後のHighを出力します。Highの区間の長さによって各ビットの0か1を判定します。
Highの区間が26us~28usであれば0となり、70usであれば1と判定します。これを40bit分繰り返します。HighとLowの判定の例を示します。
#define H_JUDGE 6 //25us以上であればHとみなす
dat = &data.h_humi[0];
for(i=0; i < 41; i++){
if( i == 0 ){
while( digitalRead(PIN_DIO) == LOW){}//50us程度
while( digitalRead(PIN_DIO) == HIGH){} //50us程度
}//ヘッダー部分なので読み飛ばし
else{
basecnt = 0; //Lowの部分のカウントをクリア
while( digitalRead(PIN_DIO) == LOW){
++basecnt;
}
hcnt = 0; //Highの部分のカウントをクリア
while( digitalRead(PIN_DIO) == HIGH){
++hcnt;
}
if(hcnt >= H_JUDGE){ //カウントが5以上
*dat = 1; //1と判定して格納
}
else{
*dat = 0; //0と判定して格納
}
++dat;
}
}
digitalRead()関数でDIのポートの状態を確認します。digitalRead()関数は汎用的に作られているため直接マイコンのDI関係のレジスタを読み込むよりも処理が多く、1コールで約5usの処理時間になります。
湿度・温度とチェックデジットの取得のためfor()文で繰り返しますが、応答信号を含むため41回繰り返します。ループの最初は応答信号なのでデータを判定せず読み飛ばします。2周目以降から湿度・温度・チェックデジットの順で送信されるのでデータを格納します。
Low部分のサイクル(basecnt)をwhile()で繰り返しながらカウントします。50us程度Lowになるため正常に通信できている場合はbasecntは10±1になります。
High部分のサイクル(hcnt)をwhile()で繰り返しながらカウントします。Highのサイクルでデータの1bitが0か1かを判定します。0の場合は26us~28usになるので5または6になります。1の場合は70usになるので12±1になります。
hcnt値が6以上であれば該当のビットが1であると判断して1を格納します。6未満であればビットが0であると判断して0を格納します。
PR:エンジニア転職なら100%自社内開発求人に強い【クラウドリンク】
データの換算とチェック
for(i=0; i < 8; i++){
humi_h |= ( data.h_humi[i] << (7 - i));
humi_l |= ( data.l_humi[i] << (7 - i));
temp_h |= ( data.h_temp[i] << (7 - i));
temp_l |= ( data.l_temp[i] << (7 - i));
chk |= ( data.chk[i] << (7 - i));
}
sum = humi_h + humi_l + temp_h + temp_l; //チェックデジットの計算
測定データは湿度データH(8バイト)、湿度データL(8バイト)、温度データH(8バイト)、温度データ(8バイト)、チェックデジット(8バイト)の順に合計40バイトのデータで格納しています。
8バイトのデータで構成している0または1のデータを8ビットデータに換算します。MSB(最上位ビット)をセットするためデータを左に7回シフトして論理和を取っています。続けて6ビット目はデータを左に6回シフトして論理和を取ります。同様にして5ビット目~LSBまで左にシフトしながら論理和を取ると8ビットのデータが生成できます。
チェックデジットは8ビットデータの換算した湿度データ(HとL)と温度データ(HとL)を加算して計算します。受信したチェックデジットを8ビットデータに換算した結果と加算した結果が一致した場合、データが健全であると判断して湿度・温度のデータとして採用します。
OLEDに測定値を表示する
U8g2ライブラリでイベント発生時の測定値の様子を表示します。ライブラリの追加の仕方や使用例については下記記事を参考にしてください。
Grove Beginner Kit for Arduinoの使い方
Grove Beginner Kit for Arduinoの使い方 その2
以下ではOLEDに測定値を表示する方法を中心に説明します。
void anaShow(void){
uint16_t bar;
u8g2.firstPage(); //バッファをクリア
do{
u8g2.setFont(u8g2_font_t0_16_mr);
u8g2.setCursor(10, 16);
u8g2.print("Temp:");
u8g2.setCursor(20, 32);
u8g2.print(data.temp);
u8g2.print("C");
u8g2.setCursor(10, 48);
u8g2.print("Humid:");
u8g2.setCursor(20, 64);
u8g2.print(data.humid);
u8g2.print("%");
}while(u8g2.nextPage());
}
最初にfirstPage()関数でOLEDに使用している内部のバッファをクリアします。
setFont()関数でOLEDに表示する文字のフォントを指定します。フォントの定義はライブラリのフォルダ(XXX\Arduino\libraries\U8g2\src\clib)にu8g2.hにあります。XXX\はArduinoのライブラリの保存先に指定しているアドレスです。
firstPage()関数で内部のバッファをクリアします。setCursor()関数でOLEDの表示位置にカーソルを合わせます。第1引数にX座標を指定します。X座標は画面左から右に向かって進みます。第2引数にY座標を指定します。Y座標は画面上から下に向かって進みます。
print()関数は引数に指定した文字列を表示します。変数を引数に指定した場合は引数の値を文字列に変換して表示します。print()関数で文字列と取得したデータを表示しています。
nextPage()関数で行データごとのデータを更新してOLEDの表示を作りますが、ウェイトが必要となるためdo while()で表示が終えるまでループさせています。本記事では約120ms必要でした。OLEDの表示は遅延が発生するため頻繁な更新はしないように調整が必要です。
DHTライブラリを追加して使用する(参考)
DHT11からのデータの取得をDO/DIの操作で取得する方法を説明しましたが、Arduino IDEでDHTライブラリを追加する方法もあります。Seeed Studio が公開しているGrove Temperature And Humidity Sensorライブラリを使用する方法を参考として説明します。
Arduino IDEのライブラリマネージャの検索欄にgrove dht11を入力するとライブラリの候補が表示されます。候補の中からGrove Temperature And Humidity Sensorをインストールします。使用例は以下の通りです。
#include <DHT.h>
#include <MsTimer2.h>
#include <U8g2lib.h>
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_MEAS 500
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
int16_t timmeas =TIM_MEAS;
DHT dht(3,DHT11);
uint8_t humid;
uint8_t temp;
/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void Dht11Show(void);
void setup() {
Serial.begin(115200);
u8g2.begin();
MsTimer2::set(10, mainTimer); // 10ms period
MsTimer2::start();
}
void loop() {
if(timmeas == TIME_UP){
timmeas = TIM_MEAS;
mainApp();
}
}
/* タイマ管理 */
void mainTimer(void){
if( timmeas > TIME_UP ){
--timmeas;
}
}
/* メイン処理 */
void mainApp(void){
humid = dht.readHumidity();
temp = dht.readTemperature();
Serial.print("Humid:");
Serial.print(humid);
Serial.print("Temp:");
Serial.print(temp);
Serial.println();
Dht11Show();
}
/* OLED表示*/
void Dht11Show(void){
u8g2.firstPage(); //バッファをクリア
do{
u8g2.setFont(u8g2_font_t0_16_mr);
u8g2.setCursor(10, 16);
u8g2.print("Temp:");
u8g2.setCursor(20, 32);
u8g2.print(temp);
u8g2.print("C");
u8g2.setCursor(10, 48);
u8g2.print("Humid:");
u8g2.setCursor(20, 64);
u8g2.print(humid);
u8g2.print("%");
}while(u8g2.nextPage());
}
DHT.hをインクルードします。DHTクラスの変数を宣言してインスタンス化します。例ではdhtでインスタンス化しています。引数に使用するDO/DIピンを指定します。第2引数にDHT11などの種別を指定します。
湿度の取得はReadHumidity()関数、温度の取得はreadTemperature()関数を使用します。
PR:エンジニア転職なら100%自社内開発求人に強い【クラウドリンク】
動作確認
電源を入れると5秒後にDHT11が測定した湿度と温度をOLEDに表示します。DHT11は測定間隔を2秒以上空けることが推奨されているので余裕をもって5秒間ごとに測定を行いOLEDの表示を更新します。
部屋に設置している温度計は23.5℃でしたが、DHT11は小数点以下までは表示できないことを考慮すると誤差は少なく測定できています。湿度は判断基準はありませんが息を吹きかけると値が上がることや加湿器を使用していると徐々に値が上がっていることから測定はできていると言えます。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <MsTimer2.h>
#include <U8g2lib.h>
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_MEAS 500
#define PIN_DIO 3
#define FIRST_JUDGE 8
#define H_JUDGE 6 //25us以上であればHとみなす
struct SIG_TYP{
uint8_t h_humi[8];
uint8_t l_humi[8];
uint8_t h_temp[8];
uint8_t l_temp[8];
uint8_t chk[8];
uint8_t temp;
uint8_t humid;
};
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
int16_t timmeas =TIM_MEAS;
SIG_TYP data;
/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void chkData(void);
void Dht11Show(void);
void setup() {
Serial.begin(115200);
pinMode(PIN_DIO, OUTPUT);
digitalWrite(PIN_DIO,HIGH);
u8g2.begin();
MsTimer2::set(10, mainTimer); // 10ms period
MsTimer2::start();
}
void loop() {
if(timmeas == TIME_UP){
timmeas = TIM_MEAS;
mainApp();
}
}
/* タイマ管理 */
void mainTimer(void){
if( timmeas > TIME_UP ){
--timmeas;
}
}
void mainApp(void){
uint8_t i;
uint8_t basecnt;
uint8_t hcnt;
uint8_t *dat;
//DIOはHighでスタート
memset(&data,0, sizeof(data));
digitalWrite(PIN_DIO, LOW);
delay(20);
digitalWrite(PIN_DIO, HIGH);
delayMicroseconds(20);
pinMode(PIN_DIO, INPUT);
dat = &data.h_humi[0];
for(i=0; i < 41; i++){
if( i == 0 ){
while( digitalRead(PIN_DIO) == LOW){}//5us程度
while( digitalRead(PIN_DIO) == HIGH){}
}
else{
basecnt = 0;
while( digitalRead(PIN_DIO) == LOW){
++basecnt;
}
hcnt = 0;
while( digitalRead(PIN_DIO) == HIGH){
++hcnt;
}
//if( basecnt >= FIRST_JUDGE ){ //50us程度を判定
if(hcnt >= H_JUDGE){
*dat = 1;
}
else{
*dat = 0;
}
//}
++dat;
}
}
pinMode(PIN_DIO, OUTPUT);
digitalWrite(PIN_DIO, HIGH);
chkData();
}
/*データのチェックと変換*/
void chkData(void){
uint8_t i;
uint8_t humi_h=0;
uint8_t temp_h=0;
uint8_t humi_l=0;
uint8_t temp_l=0;
uint8_t chk=0;
uint8_t sum;
for(i=0; i < 8; i++){
humi_h |= ( data.h_humi[i] << (7 - i));
humi_l |= ( data.l_humi[i] << (7 - i));
temp_h |= ( data.h_temp[i] << (7 - i));
temp_l |= ( data.l_temp[i] << (7 - i));
chk |= ( data.chk[i] << (7 - i));
}
sum = humi_h + humi_l + temp_h + temp_l;
if( sum == chk){
data.humid = humi_h;
data.temp = temp_h;
Serial.print("Humid:");
Serial.print(data.humid);
Serial.print("Temp:");
Serial.print(data.temp);
Serial.println();
Dht11Show();
}
}
/* OLED表示*/
void Dht11Show(void){
u8g2.firstPage(); //バッファをクリア
do{
u8g2.setFont(u8g2_font_t0_16_mr);
u8g2.setCursor(10, 16);
u8g2.print("Temp:");
u8g2.setCursor(20, 32);
u8g2.print(data.temp);
u8g2.print("C");
u8g2.setCursor(10, 48);
u8g2.print(F("Humid:"));
u8g2.setCursor(20, 64);
u8g2.print(data.humid);
u8g2.print("%");
}while(u8g2.nextPage());
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))
最後まで、読んでいただきありがとうございました。