ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールDS3231、CO2濃度センサSCD41と気圧・温湿度センサBME280をI2C接続しました。
Sensirion社のSCD4xファミリーにはSCD40、SCD41などがあるようです。今回は手持ちのSCD41を使って動作検証しています。SCD4xは、5秒毎にデータの準備が完了となります。
参考:Sensirion SCD4xミニチュアCO2センサー
RTCモジュールの時刻表示は1秒刻み、測定データの2.8インチTFT液晶への表示とSDカードへの記録は10秒間隔で行うスケッチをプログラムした際のメモです。
下記は、CO2濃度センサとしてSensirion社のSCD30を用いた記事です。
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モジュール(4ピン基板)は、基板裏面に電源電圧Vccのレギュレータ(LDO:Low Dropout)と I2Cの電圧レベル変換回路を実装しているので、5V系(Arduinoなど)と3.3V 系(ESP32-DevkitCなど)のどちらとも直接つないで使えます。
このモジュールのI2Cアドレスは0x76です。
下記の6ピン基板はSPI接続とI2C接続の両方で使えますが、信号レベルは3.3Vです。4ピン基板、6ピン基板ともにI2C接続であればESP32-DevkitCで使えました。
CO2濃度センサSCD4x
Sensirion(センシリオン)社のSCD4xファミリーにはSCD40、SCD41などがあるようです。今回は手持ちのSCD41を使って動作検証しています。SCD4xはPASens技術により、SCD30のNDIR方式にある光学ビーム経路を必要としないため、構造もシンプルで小型です。測定範囲はSCD30が400~10000ppm、SCD41が400~5000ppmです。このモジュールのI2Cアドレスは0x62です。
参考:Sensirion SCD4xミニチュアCO2センサー(特徴、仕様)
ピンヘッダが付属しないときは、ピンヘッダ 1列タイプ 40ピンから4ピンをニッパ(1×4)でカットしてSCD4xセンサモジュールにはんだ付けします。
このモジュールはI2C(SCL、SDA)のプルアップ抵抗(4.7KΩ)を内蔵していました。購入時は有効化されていないので、I2C線路内の他モジュールでもプルアップされていない場合は基板裏面のランドパターンを半田でショートします。
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濃度センサSCD41と気圧・温湿度センサBME280をI2C接続する結線図です。
ESP32-DevKitCの開発ツール arduino-esp32のインストール
ESP32-DevKitCの開発ツール arduino-esp32を準備してスケッチを作っていきます。インストール手順を纏めました。
本記事の作成時(2024年7月20日)のArduino IDEのバージョンは、2.3.2、esp32 by Espressif Systemsのバージョンは3.0.3でした。
ライブラリのインクルード
先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。
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番号を変更した箇所です。左端の // を削除して上書き保存します。
SCD41センサのライブラリ
SparkFun_SCD4x_Arduino_Library.h を利用させていただきました。
ライブラリのzipファイル( SparkFun_SCD4x_Arduino_Library-main.zip)をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
以下、他のライブラリも同様の手順で追加しました。
https://github.com/sparkfun/SparkFun_SCD4x_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
スケッチの作成、実行
時刻、気圧、CO2濃度、温湿度を2.8インチ240×320液晶に表示、SDカードに記録するスケッチ「esp32-devkitc_ili9341_scd4x_bme280_ds3231_SD.ino」を作りました。
このスケッチで使った漢字と記号は「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カードの挿入忘れやセンサ異常の際には赤文字でエラー表示します。
1秒毎に日時・時刻を、10秒毎に気圧、CO2濃度、温度、湿度を2.8インチ240×320液晶に表示してSDカードに記録するスケッチです(SCD4xは5秒毎にデータの準備が完了)。
esp32-devkitc_ili9341_scd4x_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_SCD4x_Arduino_Library.h" // https://github.com/sparkfun/SparkFun_SCD4x_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);
SCD4x mySensor;
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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
const unsigned char ooo_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, 0xF0, 0x1F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x80, 0x07, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x01, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x01, 0x00, 0x03, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x03, 0x80, 0x01, 0x80, 0x03, 0xC0, 0x01, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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);
// SCD4x初期化
Wire.begin();
if (mySensor.begin() == false) {
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawString("SCD4x not detected", 0, 170, 4);
while (1);
}
//The SCD4x has data ready every five seconds
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("SCD4x detected", 0, 170, 4);
delay(5000);
// 画面クリア
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(5, 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);
// ----- TFT画面への初期表示用の測定 -----
//The SCD4x has data ready every five seconds
// ----- SCD4xセンサからデータ取得、測定値をTFT表示 -----
co2_tmp=mySensor.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")){
// ----- SCD4xセンサが稼働している時の処理 -----
// readMeasurement will return true when fresh data is available
if (mySensor.readMeasurement()) {
// ----- 液晶画面に測定値を表示 -----
tft.setTextColor(TFT_WHITE, TFT_BLACK);
// ----- SCD4xセンサからデータ取得、測定値を表示 -----
co2_tmp=mySensor.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);
}
ユニバーサル基板ではんだ付けして、二合枡ケースに実装(2024/08/02 リンク追加)
ブレッドボードで動作テストした後で悩むのが、ユニバーサル基板へのモジュールの配置とそれらを収めるケースです。
今回使ったパーツの中で最も大きな2.8インチTFT液晶(ILI9341)モジュールの横幅が二合枡の内部寸法にジャストフィットで収まりました。センサ類は外気に晒しておきたいので前面下部に設置しています。
室内専用となりますがケースとして二合枡を使い、木工ドリルでACアダプター接続用のDCジャック、スケッチ書き込み用のUSBケーブルの差し込み穴、ユニバーサル基板とゴム足固定用の小穴をあける作業でした。
ユニバーサル基板ではんだ付けして実装することでワイヤの接触不良がなくなり安定動作しています。