ESP32-DevKitC + DS3231 + SCD30 + BME280で日時・時刻、CO2濃度、気圧、温湿度を2.8インチ液晶表示、SDカードに記録

ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールDS3231、CO2濃度センサSCD30と気圧・温湿度センサBME280をI2C接続しました。
240×320の解像度があるので文字の描画も精細です。

RTCモジュールの時刻表示は1秒刻み、測定データの2.8インチTFT液晶への表示とSDカードへの記録は10秒間隔で行うスケッチをプログラムした際のメモです。

環境モニタ
ESP32-DevKitCとRTC、センサで構築する環境モニタ
目次

ESP32-DevKitCとモジュールの結線

2.8インチ液晶モジュールとSDカードソケットはSPI接続(VSPI、HSPI)

2.8インチ液晶モジュールには、ドライバーICとしてILI9341を使った240×320の液晶モジュールに加えて、XPT2046を使ったタッチパネル、SDカードスロットが実装されています。利用するにあたり必要となるモジュール間の結線とライブラリの初期設定を下記ページに纏めました。

ESP32-DevKitCとは4線式SPIで接続(液晶とタッチパネルはVSPI、SDカードはHSPI)。TFT_eSPIライブラリをインクルード(User_Setup.hの編集)して、グラフィック型の液晶表示とタッチパネル制御を行います。

あわせて読みたい
ESP32-DevKitCと2.8インチ液晶モジュール(240×320、タッチパネル、SDカード)をSPI接続して動作確認 ILI9341を搭載した240×320の2.8インチTFT液晶モジュールをESP32-DevKitCにSPI接続して動作確認した際の作業メモです。液晶とタッチパネルはVSPI、SDカードはHSPI接続で...

センサモジュールはI2C接続

センサモジュールとESP32-DevKitCとの結線は、I2Cの2線(SCL、SDA)と電源の2線(3.3V、GND)の4線なので容易です。

ESP32-DevKitCモジュールのI2C信号レベルは3.3Vです。

気圧・温湿度センサBME280

今回使ったBME280モジュールは基板裏面に電源電圧Vccのレギュレータ(LDO:Low Dropout)と I2Cの電圧レベル変換回路を実装しているので、5V系(Arduinoなど)と3.3V 系(ESP32-DevkitCなど)のどちらとも直接つないで使えます。

このモジュールのI2Cアドレスは0x76です。

あわせて読みたい
ESP32-DevKitCと2.8インチ240×320液晶モジュール(SDカード)をSPI接続、気圧・温湿度センサBME280をI2C接続 ESP32-DevKitCとILI9341を搭載した240×320の2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、気圧・温湿度センサBME280をI2C接続し...

CO2濃度センサSCD30

CO2センサを選ぶにあたってTVOC(Total Volatile Organic Compounds 総揮発性有機化合物の総称)測定がメインのセンサ(VOCガス濃度からCO2濃度を概算)もあるので迷いましたが、CO2濃度測定の精度が高いSensirion社の非分散型赤外線(NDIR)ベースの SCD30センサモジュールを使っています。

このモジュールには基板内に同じSensirion社のSHT31センサを補正用として内蔵しているので湿度や温度も取り出すことができますが、温湿度の表示用のデータ取り出しはBME280で行い、SCD30のデータは液晶表示には利用していません。SCD30センサモジュールとしての動作温度は0℃~+50℃の仕様です。

SCD30の電源電圧の仕様は3.3 V – 5.5 Vですが、I2CのVIH(Input high level voltage)は1.75 V – 3.0 Vです。I2C接続は3.3V 系(ESP32-DevkitCなど)では直接つないで使っていますが、5V系(Arduinoなど)で利用する際は信号レベル変換モジュールを入れてます。

SCD30_Datasheet

https://sensirion.com/media/documents/4EAF6AF8/61652C3C/Sensirion_CO2_Sensors_SCD30_Datasheet.pdf

このモジュールのI2Cアドレスは0x61です。
電源を入れている間は約2秒毎に計測が行われ、計測時には窓部分が橙色に発光します。

あわせて読みたい
ESP32-DevKitCと2.8インチ240×320液晶モジュール(SDカード)をSPI接続、CO2濃度センサSCD30をI2C接続 ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、CO2濃度センサSCD30をI2C接続しました。TFT_eSP...

RTCモジュールDS3231

ESP32-DevKitCはRTCを内蔵していますが標準実装はRC発振。精度を上げるために水晶発振子をチップ内蔵したDS3231SNを使ったRTCモジュールをI2Cで外部接続しました。DS3231は2.3V〜5.5Vの範囲の電源で動作し、I2Cピンは5Vデバイスからの信号を直接インターフェイスできる5Vトレラント機能付きなので、5V系(Arduinoなど)と3.3V 系(ESP32-DevkitCなど)のどちらとも直接つないで使えます。

このモジュールのI2Cアドレスは0x68です。

DS3231SNモジュールの背面にはバッテリーバックアップ用のコイン電池ホルダが付いているので、充電できるコイン形リチウムイオン2次電池「LIR2032」を入れておきます。

あわせて読みたい
ESP32-DevKitCでNTPサーバと時刻同期して RTCモジュール DS3231SNの時刻合わせを行うLCD時計:環境モニ... ESP32-DevKitC ESP-WROOM-32開発ボード(以下、ESP32-DevKitC)を使ってインターネット上のNTP(Network Time Protocol)サーバと時刻同期してRTC(リアルタイムクロック)...

結線図

ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールのDS3231、CO2濃度センサSCD30と気圧・温湿度センサBME280をI2C接続する結線図です。

結線図
結線図

ESP32-DevKitCの開発ツール arduino-esp32のインストール

ESP32-DevKitCの開発ツール arduino-esp32を準備してスケッチを作っていきます。インストール手順を纏めました。

あわせて読みたい
ESP32-DevKitCの開発ツール arduino-esp32 のインストール手順 Arduino IDE上で動作する ESP32-DevKitC の開発ツール arduino-esp32(Arduino core for the ESP32)をインストールした際の手順メモです。 Arduino IDE 2.0.0で ESP32-...

ライブラリのインクルード

先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。

TFT_eSPIライブラリのインクルードとUser_Setup.hの編集

ILI9341を搭載したグラフィック型の液晶モジュール表示のライブラリとして「TFT_eSPI.h」を利用させていただきました。TFT_eSPIライブラリのzipファイル( TFT_eSPI-master.zip )をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。

Bodmer/TFT_eSPI

https://github.com/Bodmer/TFT_eSPI

次に、TFT_eSPIライブラリのUser_Setup.h中に書かれているGPIO番号を結線図に合わせて設定変更します。

Arduino IDEのメニューからTFT_eSPIライブラリのzipファイルをインクルードすると「User_Setup.h」はWindowsでは下記フォルダ配下にあります。

C:\Users\[ユーザー名]\Documents\Arduino\libraries\TFT_eSPI-master
または、ライブラリのアップデートを行った際などに下記のフォルダ名になっている場合もあります。
C:\Users\[ユーザー名]\Documents\Arduino\libraries\TFT_eSPI

「User_Setup.h」の200行目付近の「// For ESP32 Dev board・・・」の下にある#defineを今回の環境用にピン番号を変更します。
左側がデフォルトのUser_Setup.h 設定、右側の黄色点線枠が環境に合わせてGPIO番号を変更した箇所です。左端の // を削除して上書き保存します。

User_Setup.h の編集
左側は初期のUser_Setup.h 右側は今回結線した環境向けのUser_Setup.h(200行目付近)

SCD30センサのライブラリ

SparkFun_SCD30_Arduino_Library.h を利用させていただきました。
ライブラリのzipファイル( SparkFun_SCD30_Arduino_Library-main.zip)をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
以下、他のライブラリも同様の手順で追加しました。

sparkfun/SparkFun_SCD30_Arduino_Library

https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library

ライブラリの追加手段として、Arduino IDEのメニューバー「ツール」—>「ライブラリを管理」—>「ライブラリマネージャ」を開いて、タイプ+トピック(センサを指定)+キーワードを入力・絞り込み検索してライブラリをインストールすることもできます。

BME280センサのライブラリ

Adafruit_Sensor.h (Adafruit_Sensor-master.zip)と Adafruit_BME280.h (Adafruit_BME280_Library-master .zip)を利用させていただきました。

adafruit/Adafruit_Sensor

https://github.com/adafruit/Adafruit_Sensor

adafruit/Adafruit_BME280_Library

https://github.com/adafruit/Adafruit_BME280_Library

Adafruit_I2CDevice.hが無いとのエラーメッセージがでた場合はadafruit/Adafruit_BusIO(Adafruit_BusIO-master.zip)をインクルードします。

DS3231のライブラリ

DS3232、DS3231用ライブラリDS3232RTC.h (DS3232RTC-master.zip)を利用させていただきました。

JChristensen/DS3232RTC

https://github.com/JChristensen/DS3232RTC

esp_sntp.h

esp_sntp.h」は arduino-esp32同梱モジュールです(別途インクルード不要)。詳細は Official develooment framework for ESP32 (ESP-IDF) に記述があります。mulong.meサイトを参考にESP32内蔵RTCの時刻が NTPで取得した時刻に一致しているかの判定に使っています。

espressif/esp-idf

https://github.com/espressif/esp-idf/blob/4a011f3/components/lwip/include/apps/esp_sntp.h

TimeLib.h

TimeライブラリのsetTime()関数を使っています。スケッチにはTimeLib.h(Time-master.zip)をincludeします。

PaulStoffregen/Time

https://github.com/PaulStoffregen/Time

スケッチの作成、実行

ESP32-DevKitC+BME280+SCD30+DS3231で時刻、気圧、CO2濃度、温湿度を2.8インチ240×320液晶に表示、SDカードに記録するスケッチを作りました。

このスケッチで使った漢字と記号は「C、O、気、圧、温、湿、度、(℃)」と少ないので、メモリ消費を抑えるために、利用する特定の文字のみをBMPファイルから変換したHEXデータで表示します。
漢字や記号のBMPファイルは、Windows標準アプリのペイント3Dで170×170ピクセル程度で作った後キャンパスを32×32ピクセル(「(℃)」のみ40×40ピクセル)に変更して圧縮します。このBMPファイルをHEXデータに変換してtft.drawXBitmap() で描画します。
HEXデータへの変換にはProgramResource.netサイトの「nfBmptoHex.exe」を利用させていただきました。

測定データの表示スペースは、気圧は4桁、CO2濃度は5桁、温度・湿度は整数部2桁と少数点以下1桁です。下段に日時、曜日、時刻を表示します。

RTCモジュールDS3231の「秒」の読み取り値を使って、2.8インチ液晶モジュールには1秒毎に日時・時刻表示、10秒毎に気圧、CO2濃度、温度、湿度を表示、SDカードにその値をファイル名「logdata.txt」でCSV形式で書き込みます。

SDカードに記録した測定データ
SDカードに10秒間隔で記録
環境モニタ
日時・時刻は1秒刻み、10秒毎に測定データの液晶表示とSDカードに記録

ESP32-DevKitCの電源をON(スケッチ起動)すると初期化ステータス(setup)を表示した後、測定データの表示画面(loop)に遷移します。

進行状況表示のメッセージ1行あたりdelay(1000);が入っています。

SDカードの挿入忘れやセンサ異常の際には赤文字でエラー表示します。

初期化状況の表示

PCのUSBポート(5V)とESP32-DevKitCのmicro USB type-bコネクタ間にUSBデジタルテスタを挿入して、今回の回路構成時の電流値を測ると定常値が100mA、10秒毎の測定時に150mA程のピーク値でした。

消費電流の測定
USBデジタルテスタで電流測定

1秒毎に日時・時刻を、10秒毎に気圧、CO2濃度、温度、湿度を2.8インチ240×320液晶に表示してSDカードに記録するスケッチです。

留意点
・再起動の都度、前回起動時に書き込まれたSDカードのデータは削除されます。
・23、24行には、WiFi接続環境のSSIDとそのパスワードを記述ください。

esp32-devkitc_ili9341_scd30_bme280_ds3231_SD.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <SD.h>
#include <TimeLib.h>    // https://github.com/PaulStoffregen/Time
#include <TFT_eSPI.h>   // https://github.com/Bodmer/TFT_eSPI
#include "SparkFun_SCD30_Arduino_Library.h"  // https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
#include <Adafruit_Sensor.h>   // https://github.com/adafruit/Adafruit_Sensor
#include <Adafruit_BME280.h>   // https://github.com/adafruit/Adafruit_BME280_Library
#include <DS3232RTC.h>         // DS3232、DS3231用ライブラリ https://github.com/JChristensen/DS3232RTC
#include <esp_sntp.h>

TFT_eSPI tft = TFT_eSPI();
SPIClass spiSD(HSPI);
SCD30 airSensor;
float co2_tmp;
Adafruit_BME280 bme;
float pressure;
float temp;
float humid;
DS3232RTC myRTC(false);
const char* weekStr[7] = {"(Sun)","(Mon)","(Tue)","(Wed)","(Thu)","(Fri)","(Sat)"};
const char* ssid      = "your ssid";
const char* password  = "your password";
const char* ntpServer = "ntp.nict.jp";
const long  gmtOffset_sec = 32400;
const int   daylightOffset_sec = 0;
const unsigned char kii_bmp[] PROGMEM = {0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0x1F, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x8E, 0xFF, 0xFF, 0x03, 0x86, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x01, 0xFC, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x80, 0x83, 0x01, 0xE0, 0xC0, 0x81, 0x01, 0xC0, 0xC3, 0x80, 0x01, 0x00, 0x67, 0x80, 0x01, 0x00, 0x7E, 0x80, 0x01, 0x00, 0x3C, 0x80, 0x01, 0x00, 0x7C, 0x80, 0x03, 0x00, 0xEF, 0x00, 0x43, 0x80, 0xC3, 0x01, 0x63, 0xE0, 0x81, 0x03, 0x63, 0xF8, 0x00, 0x07, 0x66, 0x3E, 0x00, 0x06, 0x7E, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x18, };
const unsigned char atu_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0xFF, 0x7F, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0xFE, 0xFF, 0x1F, 0x30, 0xFE, 0xFF, 0x1F, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0xCE, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
const unsigned char ccc_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0xFC, 0xFF, 0x07, 0x00, 0x7E, 0x00, 0x07, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, };
const unsigned char ooo_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0xC0, 0x0F, 0xF0, 0x03, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x01, 0x80, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x38, 0x1C, 0x00, 0x00, 0x38, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x1C, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0xE0, 0x03, 0xC0, 0x07, 0xC0, 0x0F, 0xF0, 0x03, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x03, 0x00, };
const unsigned char onn_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0xF8, 0xFF, 0x07, 0x70, 0xF8, 0xFF, 0x07, 0xE0, 0x18, 0x00, 0x06, 0xC0, 0x18, 0x00, 0x06, 0x00, 0x18, 0x00, 0x06, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xF8, 0xFF, 0x07, 0x02, 0x18, 0x00, 0x06, 0x07, 0x18, 0x00, 0x06, 0x1E, 0x18, 0x00, 0x06, 0x38, 0x38, 0x00, 0x06, 0x70, 0xF8, 0xFF, 0x07, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x3F, 0x00, 0x8C, 0xE3, 0x18, 0x60, 0x0C, 0x63, 0x18, 0x60, 0x0C, 0x63, 0x18, 0x70, 0x0C, 0x63, 0x18, 0x30, 0x0C, 0x63, 0x18, 0x30, 0x0C, 0x63, 0x18, 0x18, 0x0C, 0x63, 0x18, 0x18, 0x0C, 0x63, 0x18, 0x1C, 0x0C, 0x63, 0x18, 0x8C, 0xFF, 0xFF, 0xFF, 0xCE, 0xFF, 0xFF, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
const unsigned char doo_bmp[] PROGMEM = {0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x30, 0x60, 0xC0, 0x00, 0x30, 0x60, 0xC0, 0x00, 0x30, 0x60, 0xC0, 0x00, 0x30, 0x60, 0xC0, 0x00, 0xB0, 0xFF, 0xFF, 0x7F, 0x30, 0xE7, 0xE0, 0x70, 0x30, 0x60, 0xC0, 0x00, 0x30, 0x60, 0xC0, 0x00, 0x30, 0x60, 0xC0, 0x00, 0x30, 0xE0, 0xFF, 0x00, 0x30, 0xE0, 0xFF, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0x03, 0x38, 0xFF, 0xFF, 0x07, 0x18, 0x30, 0x00, 0x03, 0x18, 0x60, 0x80, 0x01, 0x18, 0xE0, 0xC0, 0x01, 0x18, 0xC0, 0xE1, 0x00, 0x1C, 0x80, 0x7B, 0x00, 0x0C, 0x00, 0x1F, 0x00, 0x0E, 0x00, 0x3F, 0x00, 0x0E, 0xE0, 0xFB, 0x01, 0x06, 0xFE, 0xC0, 0x3F, 0xC6, 0x1F, 0x00, 0xFE, 0xC0, 0x01, 0x00, 0x60, };
const unsigned char deg_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, 0x04, 0x10, 0x3C, 0xC0, 0x07, 0x08, 0x18, 0x24, 0xF0, 0x1F, 0x10, 0x0C, 0x42, 0x38, 0x18, 0x30, 0x04, 0x42, 0x18, 0x30, 0x20, 0x06, 0x24, 0x0C, 0x70, 0x60, 0x06, 0x18, 0x0C, 0x00, 0x40, 0x02, 0x00, 0x0C, 0x00, 0x40, 0x02, 0x00, 0x0E, 0x00, 0xC0, 0x02, 0x00, 0x06, 0x00, 0xC0, 0x02, 0x00, 0x06, 0x00, 0xC0, 0x02, 0x00, 0x0E, 0x00, 0xC0, 0x02, 0x00, 0x0E, 0x00, 0xC0, 0x02, 0x00, 0x0C, 0x00, 0x40, 0x06, 0x00, 0x0C, 0x60, 0x40, 0x04, 0x00, 0x1C, 0x30, 0x60, 0x0C, 0x00, 0x38, 0x38, 0x20, 0x08, 0x00, 0xF0, 0x1F, 0x30, 0x18, 0x00, 0xC0, 0x0F, 0x18, 0x30, 0x00, 0x00, 0x00, 0x08, 0x60, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, };
const unsigned char sit_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0xF8, 0xFF, 0x1F, 0x70, 0xF8, 0xFF, 0x1F, 0xE0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x06, 0x18, 0x00, 0x18, 0x1E, 0x18, 0x00, 0x18, 0x3C, 0x18, 0x00, 0x18, 0x70, 0x18, 0x00, 0x18, 0x20, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x0C, 0x63, 0x30, 0x60, 0x0C, 0x43, 0x30, 0x60, 0x18, 0x43, 0x18, 0x70, 0x18, 0x43, 0x18, 0x30, 0x30, 0x43, 0x0C, 0x30, 0x30, 0x43, 0x0C, 0x38, 0x30, 0x43, 0x06, 0x18, 0x00, 0x63, 0x00, 0x1C, 0x00, 0x63, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x8E, 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };


void setup(void) {
// TFT液晶初期化
  tft.init();
  tft.setRotation(1);
  tft.setTextSize(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("Initializing TFT library", 0, 20, 4);
delay(1000);
//-------------DS3231-------------
struct tm timeInfo;
myRTC.begin();
//WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
  tft.drawString("WiFi bigin", 0, 50, 4);
}
delay(1000);
// WiFi接続の表示
  tft.drawString("WiFi connected", 0, 50, 4);
delay(1000);
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  tft.drawString("JST synchronized", 0, 50, 4);
delay(1000);
// 内蔵RTCの時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
delay(1000);
}
//内蔵RTC時刻 = NTP時刻の表示
  tft.drawString("Time matched        ", 0, 50, 4);
delay(1000);
//内蔵RTCの時刻の取得
getLocalTime(&timeInfo);
//内蔵RTCの時刻をDS3231に時刻設定
// setTime(12, 40, 0, 14, 11, 2022);  // 手動設定・動作確認用(時、分、秒、日、月、年)
setTime(timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec, timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
myRTC.set(now());
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
// SDカード初期化
spiSD.begin(14, 33, 13, 15); //SCK,MISO,MOSI,CS
  tft.drawString("Initializing SD library", 0, 80, 4);
delay(1000);
// SDカードマウント確認
if (!SD.begin(15, spiSD)) {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("Card Mount Failed             ", 0, 80, 4);
return;
}
else  {
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("Card Mount Successful         ", 0, 80, 4);
}
delay(1000);
// SDカードファイル書き込み ※前回起動時に書き込んだデータは削除されます。
File dataFile = SD.open("/datalog.txt", FILE_WRITE);
dataFile.println("File written");
dataFile.close();
  tft.drawString("File written", 0, 110, 4);
delay(1000);
// BME280初期化
bool status;
status = bme.begin(0x76);
while (!status) {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("BME280 connection failed", 0, 140, 4);
}
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("BME280 connected", 0, 140, 4);
delay(1000);
// SCD30初期化
  Wire.begin();
  if (airSensor.begin() == false)  {
      tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("SCD30 not detected", 0, 170, 4);
    while (1);
  }
//The SCD30 has data ready every two seconds
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("SCD30 detected", 0, 170, 4);
delay(2000);
// 画面クリア
   tft.fillScreen(TFT_BLACK);
   tft.setTextColor(TFT_WHITE, TFT_BLACK);
// ----- 項目名を日本語で表示 ----- 
// 気圧(hPa)
   tft.drawXBitmap(10, 0, kii_bmp, 32, 32, 0xFFFF);
   tft.drawXBitmap(42, 0, atu_bmp, 32, 32, 0xFFFF);
   tft.drawString("(hPa)", 250, 5, 4);
// CO2濃度(ppm)
   tft.drawXBitmap(0, 47, ccc_bmp, 32, 32, 0xFFFF);
   tft.drawXBitmap(30, 47, ooo_bmp, 32, 32, 0xFFFF); 
   tft.drawString("2", 64, 62, 4);
   tft.drawString("(ppm)", 250, 53, 4);
// 温度(℃)
   tft.drawXBitmap(10, 95, onn_bmp, 32, 32, 0xFFFF);
   tft.drawXBitmap(42, 95, doo_bmp, 32, 32, 0xFFFF);
   tft.drawXBitmap(250, 85, deg_bmp, 40, 40, 0xFFFF);
// 湿度(%)
   tft.drawXBitmap(10, 142, sit_bmp, 32, 32, 0xFFFF);
   tft.drawXBitmap(42, 142, doo_bmp, 32, 32, 0xFFFF);
   tft.drawString("(%)", 250, 146, 4);
// ----- 測定周期を1分間にしたのでTFT画面への初期表示用 -----
// ----- SCD30センサが稼働している時の処理 -----
   if (airSensor.dataAvailable())  {
// ----- SCD30センサからデータ取得、測定値をTFT表示 ----- 
co2_tmp=airSensor.getCO2();
   tft.setTextColor(TFT_WHITE, TFT_BLACK);
   tft.fillRect(105, 47, 140, 45, TFT_BLACK);  // 残像消去
   tft.drawFloat(co2_tmp, 0, 105, 47, 6);
// ----- BME280センサからデータ取得、測定値をTFT表示 ----- 
pressure=bme.readPressure() / 100.0F;
temp=bme.readTemperature();
humid=bme.readHumidity();
   tft.setTextColor(TFT_YELLOW, TFT_BLACK);
   tft.fillRect(105, 0, 140, 45, TFT_BLACK);  // 残像消去
   tft.drawFloat(pressure, 0, 105, 0, 6); 
   tft.drawFloat(temp, 1, 105, 93, 6);
   tft.drawFloat(humid, 1, 105, 140, 6);
  }
}


void loop() {
// RTCから時刻取得
tmElements_t tm;
char d_mes[12] ;
char t_mes[12] ;
myRTC.read(tm);
sprintf(d_mes, "%04d/%02d/%02d", tm.Year + 1970, tm.Month, tm.Day);
sprintf(t_mes, "%02d:%02d:%02d", tm.Hour, tm.Minute, tm.Second);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
   tft.setCursor(70, 190, 4);
   tft.println(d_mes);
   tft.setCursor(200, 190, 4);
   tft.println(weekStr[tm.Wday - 1]);  
   tft.setCursor(110, 220, 4);
   tft.println(t_mes); 
// -------- 10秒毎に測定してTFT表示、SDカード記録 --------
   if((String(tm.Second) == "0")  || (String(tm.Second) == "10") || 
      (String(tm.Second) == "20") || (String(tm.Second) == "30") || 
      (String(tm.Second) == "40") || (String(tm.Second) == "50")){ 
// ----- SCD30センサが稼働している時の処理 -----
   if (airSensor.dataAvailable())  {
// ----- 液晶画面に測定値を表示 -----
   tft.setTextColor(TFT_WHITE, TFT_BLACK);
// ----- SCD30センサからデータ取得、測定値を表示 ----- 
co2_tmp=airSensor.getCO2();
   tft.fillRect(105, 47, 140, 45, TFT_BLACK);  // 残像消去
   tft.drawFloat(co2_tmp, 0, 105, 47, 6);
// ----- BME280センサからデータ取得、測定値を表示 ----- 
pressure=bme.readPressure() / 100.0F;
temp=bme.readTemperature();
humid=bme.readHumidity();
   tft.setTextColor(TFT_YELLOW, TFT_BLACK);
   tft.fillRect(105, 0, 140, 45, TFT_BLACK);  // 残像消去
   tft.drawFloat(pressure, 0, 105, 0, 6); 
   tft.drawFloat(temp, 1, 105, 93, 6);
   tft.drawFloat(humid, 1, 105, 140, 6);
// ----- SDカードへの書き込み用データファイルの生成 ----- 
// データ格納ファイル生成
String dataString = "";
// RTCの年月日と時分秒を記録
dataString += String(d_mes);
dataString += ",";  // カンマセパレータ
dataString += String(t_mes);
// 測定データ1:BME280の気圧
    dataString += ",";  // カンマセパレータ
    if(!isnan(pressure)){
      dataString += String(pressure,0);
    }else{
      dataString += " ";
    }
// 測定データ2:CO2濃度
    dataString += ",";  // カンマセパレータ
    if(!isnan(co2_tmp)){
      dataString += String(co2_tmp,0);
    }else{
      dataString += " ";
    }
// 測定データ3:BME280の温度
    dataString += ",";  // カンマセパレータ
    if(!isnan(temp)){
      dataString += String(temp,1);
    }else{
      dataString += " ";
    }
// 測定データ4:BME280の湿度
    dataString += ",";  // カンマセパレータ
    if(!isnan(humid)){
      dataString += String(humid,1);
    }else{
      dataString += " ";
    }
// ----- SDカードのdatalog.txtにdataStringを追加 ----- 
File dataFile = SD.open("/datalog.txt", FILE_APPEND);
dataFile.println(dataString);
dataFile.close();
   }
  }
// SDカードに2重書込みが起らないように設定
delay(1000);
}

SCD30センサのセルフキャリブレーション

SCD30センサは換気の良い場所で電源を入れたままで一定時間連続測定してセルフキャリブレーションします。400ppm台の数値に落ち着くと思います。

SparkFun SCD30 CO₂ センサー ライブラリ
注: SCD30 には、自動セルフキャリブレーション ルーチンがあります。Sensirion は、セルフキャリブレーションを完了するために、少なくとも 1 日 1 時間の「新鮮な空気」で 7 日間の連続測定を推奨しています。

Note: The SCD30 has an automatic self-calibration routine. Sensirion recommends 7 days of continuous readings with at least 1 hour a day of ‘fresh air’ for self-calibration to complete.

https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library

Interface Description Sensirion SCD30 Sensor Module p.13/21
1.4.6 自動セルフキャリブレーション (ASC) の (非) アクティブ化
継続的な自動セルフキャリブレーションは、次のコマンドで (非) アクティブにすることができます。 初めてアクティブ化する場合、アルゴリズムが ASC の初期パラメーター セットを見つけることができるように、最低 7 日間必要です。 センサーは、毎日少なくとも 1 時間は新鮮な空気にさらす必要があります。 また、その間、センサーを電源から切り離すことはできません。そうしないと、キャリブレーション パラメータを見つける手順が中止され、最初からやり直す必要があります。 正常に計算されたパラメータは SCD30 の不揮発性メモリに保存され、再起動後も以前に見つかった ASC のパラメータが引き続き存在するという効果があります。

1.4.6 (De-)Activate Automatic Self-Calibration (ASC)
Continuous automatic self-calibration can be (de-)activated with the following command. When activated for the first time a period of minimum 7 days is needed so that the algorithm can find its initial parameter set for ASC. The sensor has to be exposed to fresh air for at least 1 hour every day. Also during that period, the sensor may not be disconnected from the power supply, otherwise the procedure to find calibration parameters is aborted and has to be restarted from the beginning. The successfully calculated parameters are stored in non-volatile memory of the SCD30 having the effect that after a restart the previously found parameters for ASC are still present.

https://sensirion.com/media/documents/D7CEEF4A/6165372F/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf

ユニバーサル基板ではんだ付けして、二合枡ケースに実装(2024/08/02 リンク追加)

ブレッドボードで動作テストした後で悩むのが、ユニバーサル基板へのモジュールの配置とそれらを収めるケースです。

今回使ったパーツの中で最も大きな2.8インチTFT液晶(ILI9341)モジュールの横幅が二合枡の内部寸法にジャストフィットで収まりました。センサ類は外気に晒しておきたいので前面下部に設置しています。

室内専用となりますがケースとして二合枡を使い、木工ドリルでACアダプター接続用のDCジャック、スケッチ書き込み用のUSBケーブルの差し込み穴、ユニバーサル基板とゴム足固定用の小穴をあける作業でした。

ユニバーサル基板ではんだ付けして実装することでワイヤの接触不良がなくなり安定動作しています。

動作確認につかったスケッチは「esp32-devkitc_ili9341_scd30_bme280_ds3231_SD.ino」のCとOの文字サイズを小さめにしたHEXデータを使った「esp32-devkitc_ili9341_scd30_bme280_ds3231_SD_v2.ino」に変更しています。

あわせて読みたい
ESP32-DevKitC + 2.8インチTFT液晶(ILI9341)で作った環境モニタ用の二合枡ケース ブレッドボードで動作テストした後で悩むのが、ユニバーサル基板へのモジュールの配置とそれらを収めるケースです。今回使ったパーツの中で最も大きな2.8インチTFT液晶(...
よかったらシェアしてね!
  • URLをコピーしました!
目次