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とモジュールの結線
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の編集)して、グラフィック型の液晶表示とタッチパネル制御を行います。
センサモジュールは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です。
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など)で利用する際は信号レベル変換モジュールを入れてます。
https://sensirion.com/media/documents/4EAF6AF8/61652C3C/Sensirion_CO2_Sensors_SCD30_Datasheet.pdf
このモジュールのI2Cアドレスは0x61です。
電源を入れている間は約2秒毎に計測が行われ、計測時には窓部分が橙色に発光します。
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とILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールのDS3231、CO2濃度センサSCD30と気圧・温湿度センサBME280をI2C接続する結線図です。
ESP32-DevKitCの開発ツール arduino-esp32のインストール
ESP32-DevKitCの開発ツール arduino-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ファイルを追加します。
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番号を変更した箇所です。左端の // を削除して上書き保存します。
SCD30センサのライブラリ
SparkFun_SCD30_Arduino_Library.h を利用させていただきました。
ライブラリのzipファイル( SparkFun_SCD30_Arduino_Library-main.zip)をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
以下、他のライブラリも同様の手順で追加しました。
https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
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
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形式で書き込みます。
ESP32-DevKitCの電源をON(スケッチ起動)すると初期化ステータス(setup)を表示した後、測定データの表示画面(loop)に遷移します。
進行状況表示のメッセージ1行あたりdelay(1000);が入っています。
SDカードの挿入忘れやセンサ異常の際には赤文字でエラー表示します。
PCのUSBポート(5V)とESP32-DevKitCのmicro USB type-bコネクタ間にUSBデジタルテスタを挿入して、今回の回路構成時の電流値を測ると定常値が100mA、10秒毎の測定時に150mA程のピーク値でした。
1秒毎に日時・時刻を、10秒毎に気圧、CO2濃度、温度、湿度を2.8インチ240×320液晶に表示してSDカードに記録するスケッチです。
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)
https://sensirion.com/media/documents/D7CEEF4A/6165372F/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
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.
ユニバーサル基板ではんだ付けして、二合枡ケースに実装(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」に変更しています。