ILI9341を搭載した240×320の2.8インチTFT液晶モジュールと内蔵するSDカードスロットをSeeed Studio XIAO ESP32C3(以下、XIAO ESP32C3)にSPI接続して動作確認した際の作業メモです。
年月日、時刻、CO2濃度、気圧、気温、湿度をSDカードに記録します。モニタ用として2.8インチTFT液晶にも表示しています。
RTC(DS3231など)は使っていないので、起動時と1日1回NTPサーバに接続して時刻合わせを実行します。
Seeed StudioのWiki よくあるご質問(FAQ):
What to do when upload fails/the program runs abnormally/the device port is not found? | Seeed Studio Wiki
デバイスが認識されない問題やアップロードに失敗する問題の解決
![2.8インチTFT液晶モジュールと内蔵SDカードスロットをSPI接続して環境センサの測定データをSDカードに記録](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-04_DSC02409_c_b_r_t_r2-1024x576.png)
準備したパーツ
ネット通販(秋月電子通商、マルツオンライン など)でパーツを集めてブレッドボード上で結線しました。
# | パーツ | 個数 | 備考 |
1 | Seeed Studio XIAO ESP32C3 | 1 | 【M-17454】 |
2 | ILI9431搭載2.8インチ SPI制御タッチパネル付TFT液晶MSP2807 ※SDカードスロットはピンヘッダのはんだ付けが必要 | 1 | 【M-16265】 |
3 | BME280 気圧、温湿度センサ モジュール ※今回使ったモジュールはI2C接続、電圧レギュレータとI2C電圧レベル変換回路を内蔵 | 1 | 手持ち |
4 | CO2センサモジュール (二酸化炭素+温度/湿度センサ)【SCD30】 | 1 | 【SCD30】 |
5 | プルアップ抵抗 | 適量 | ※必要に応じて |
6 | ブレッドボード 6穴版 EIC-3901 | 1 | 【P-12366】 |
7 | ミニブレッドボード BB-601(白) ※SDカードスロット結線用 | 1 | 【M-15178】 |
8 | 細ピンヘッダ 1×40(黒) | 1 | 【C-06631】 |
9 | ジャンパーワイヤ オス-オス 10cmセット オス-オス 15cmセット(SDカードスロット結線用) | 適量 | 【C-05371】 |
10 | SDカード ※FAT32でフォーマット ※64GB以上のSDカードは32GB以下でパーテションを切るか、32GB以上のFAT32フォーマットができるRaspberry Pi Imager等のツールを利用 | 1 | 手持ち |
結線図と組立て
SCD30センサとBME280センサはI2C接続、2.8インチTFT液晶モジュールとSDカードスロットはSPI接続です。
XIAO ESP32C3のI2CやSPIのピンレイアウトは下記サイトを参照して結線しました。
Getting Started with Seeed Studio XIAO ESP32C3
https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/
Pinout diagram
2.8インチTFT液晶モジュールと内蔵SDカードスロットの2つをSPI接続
2.8インチTFT液晶モジュールにはILI9341を使った240×320の液晶モジュールに加えて、XPT2046を使ったタッチパネル、SDカードスロットが実装されています。この液晶モジュールのロジックレベルは3.3Vなので、同じ3.3V系のXIAO ESP32C3と直結できます。
今回、2.8インチTFT液晶モジュールとSDカードスロットの2つをSPI接続します。
SPI信号(MOSI、MISO、SCK)は共有し、CS信号(2.8インチTFT液晶をGPIO2、SDカードスロットをGPIO5に設定)で切り替えます。
電源電圧は3.3Vで使用するので、秋月電子通商サイトを参考に、J1のランドパターンをはんだ付けして三端子レギュレータ(U1)をバイパスしています。
![2.8インチTFT液晶モジュールと内蔵SDカードスロット](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-04_2.8-TFT_LCD_SD-card-1024x576.png)
J2コネクタ(2.8インチTFT液晶への接続箇所)、J4コネクタ(SDカードスロット)とXIAO ESP32C3のGPIOとのピン配列を纏めました。
2.8インチTFT液晶のSPIピン、CSピンは、TFT_eSPIライブラリのUser_Setup.hファイルを編集して設定します。
# | 2.8インチTFT液晶(J2コネクタ)のシルク印刷 | XIAO ESP32CのGPIO番号(SPI) |
1 | VCC ※共通、3.3V | 3V3 |
2 | GND ※共通 | GND |
3 | CS | GPIO2 (CS) |
4 | RESET | GPIO3 |
5 | DC | GPIO4 |
6 | SDI(MOSI) | GPIO10 (MOSI) |
7 | SCK | GPIO8 (SCK) |
8 | LED ※バックライト | 3V3 |
9 | SDO(MISO) | GPIO9 (MISO) |
SDカードスロットのCSピンはGPIO5に設定しました。スケッチでは if (!SD.begin(5)) { }でGPIO5をCSピンとして指定します。
# | SDカードスロット(J4コネクタ)のシルク印刷 | XIAO ESP32C3のGPIO番号 |
1 | SD_CS | GPIO5 (CS) |
2 | SD_MOSI | GPIO10 (MOSI) |
3 | SD_MISO | GPIO9 (MISO) |
4 | SD_SCK | GPIO8 (SCK) |
結線図
動作確認できた現在の結線図です。
SDカードスロットのSD_MISOを1kΩでプルアップすることで動作が安定しました。他の信号線はモジュール内部で1kΩのチップ抵抗(終端抵抗?)につながっています。
SCD41(最大:205mA、平均:3mA)、SCD30(最大:75mA、平均:19mA)と2.8インチTFT液晶(約90mA)は供給電流が比較的大きいモジュールです。ブレッドボードとジャンパーワイヤの接触が悪いと安定動作しない時がありました。
下記の回路図では、XIAO ESP32C3のピン番号はGPIO番号を記載しています。詳細なピン配置は「Getting Started with Seeed Studio XIAO ESP32C3(Pinout diagram、power-pins)」を参照ください。
![動作確認できた現在の結線図](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-04_xiao-esp32c3_2.8TFT-LCD_SCD30_BME280_SD-card_wiring_r5-1024x540.png)
参考:
・Getting Started with Seeed Studio XIAO ESP32C3(Pinout diagram、power-pins)
・ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶(akizukidenshi.com)
・2.8inch SPI Module ILI9341 SKU-MSP2807 – LCD wiki
+ Product Documentation
+ 2.8 inch SPI Module Schematic(回路図)
開発ツール arduino-esp32のインストール
ESP32の開発ツール arduino-esp32を準備してスケッチを作っていきます。XIAO ESP32C3サイトにarduino-esp32のインストール手順の記載があります。
Getting Started with Seeed Studio XIAO ESP32C3
https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/#software-setup
+ Software setup
当サイトでもESP32-DevKitCを例として、arduino-esp32 をArduino IDE 2.0.x版にインストールする手順メモを纏めました。
ボード(XIAO_ESP32C3を選択)とポート(当サイトのPCではCOM6)を切り替えることでXIAO ESP32C3でもそのまま使えます。
![Arduino IDEでボードとポートの選択](https://yokahiyori.com/wp-content/uploads/2022/12/2022-12-13_Xiao_Boad-1024x576.png)
バージョン2.0.5へのダウングレードはバージョン3.0.3を削除(アンインストール)し、プルダウンから2.0.5を選んでインストール下さい。その後、Arduino IDEを再起動して下さい。
書き込み中にシリアルエラーが出た際は、Seeed Studio XIAO ESP32C3基板上のBボタンを押しながら電源を入れることで回避できることもあります。
Seeed StudioのWiki よくあるご質問(FAQ):
What to do when upload fails/the program runs abnormally/the device port is not found? | Seeed Studio Wiki
デバイスが認識されない問題やアップロードに失敗する問題の解決
また、書き込み後にLeaving… Hard resetting via RTS pin…の後も白画面のままになった場合、電源を入れ直すことで正常に液晶表示されることもありました。
![](https://yokahiyori.com/wp-content/uploads/2023/01/esp32-by-Espressif-Systems_2.0.5_downgrade.png)
![](https://yokahiyori.com/wp-content/uploads/2021/11/2022-10-16_arduino-ide_esp32-2-300x169.png)
ライブラリのインクルード
先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。
TFT_eSPIライブラリのインクルード、User_Setup.hの編集
2.8インチTFT液晶モジュールを利用するにあたりTFT_eSPIライブラリのインクルードと初期設定を行います。
TFT_eSPIライブラリのインクルード
GitHubサイトからTFT_eSPIライブラリのzipファイル( TFT_eSPI-master.zip )をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
https://github.com/Bodmer/TFT_eSPI
User_Setup.hのGPIO番号を結線した環境に合わせて設定変更
Arduino IDEのメニューからTFT_eSPIライブラリのzipファイルをインクルードすると「User_Setup.h」はWinsowsでは下記フォルダ配下にありました。
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 の編集](https://yokahiyori.com/wp-content/uploads/2022/12/2022-12-25_2.8TFT-LCD_User_Setup_r5-1024x489.png)
SCD30センサ
sparkfun/SparkFun_SCD30_Arduino_Library サイトのライブラリを利用させていただきました。 サイトの「Code」プルダウンから「SparkFun_SCD30_Arduino_Library-main .zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」でダウンロードしたzipファイルを指定します。
BME280センサ
BME280センサから測定データを取得するライブラリとして adafruit/Adafruit_BME280_Library と adafruit/Adafruit_Sensor を利用させていただきました。
TimeLib.h
Time.h、TimeLib.hライブラリは PaulStoffregen/Time を利用させていただきました。サイトの「Code」プルダウンから「Download ZIP」でPCに「Time-master.zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」で「Time-master.zip」を指定します。
esp_sntp.h
「esp_sntp.h」は arduino-esp32同梱モジュールです。詳細は Official develooment framework for ESP32 (ESP-IDF) に記述があります。mulong.meサイトを参考に内蔵時計の時刻が NTPサーバから取得した時刻に一致しているかの判定に使っています。
スケッチ:SDカードにSCD30とBME280の測定データを記録
SPI接続したSDカードに年月日、時刻、CO2濃度、気圧、気温、湿度をCSVデータ(ファイル名はdatalog.txt)として記録します。モニタ用として2.8インチTFT液晶にも表示しています。
SDカードへの書き込み間隔は10秒、時計表示は1秒です。
下記スケッチの void loop(){ } は1秒間に数回ループさせ、必要に応じてif文で処理タイミングの分岐処理を行っています(delay(1000);だと処理によっては秒飛びが起きることがあったため)。
SDカードには10秒サイクルで測定データを書込みますが、同じ1秒間に多重書き込みが起こらないように書き込んだ時点の秒(d_sec_b)と現在の秒(d_sec)をif文で判別しています。
![測定データをマイクロSDカードに記録](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-04_datalog_r2-1024x575.png)
※ストーブを焚いている部屋の窓を全開にしたのでCO2濃度が1488ppmから400ppm台に下がっています。
![2.8インチTFT液晶モジュールと内蔵SDカードスロットをSPI接続して環境センサの測定データをSDカードに記録](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-04_DSC02409_c_b_r_t_r2-1024x576.png)
RTC(リアルタイムクロック)モジュールは使っていないので、電源起動(スケッチ実行)時にWiFi経由でNTPサーバに接続してXIAO ESP32C3の内蔵時計をJSTに時刻合わせします。加えて、1日1回、NTPサーバに接続(下記スケッチでは12時30分0秒)して時刻合わせすることで精度を維持します。
最大1310720バイトのフラッシュメモリのうち、スケッチが776254バイト(59%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が39860バイト(12%)を使っていて、ローカル変数で287820バイト使うことができます。
xiao-esp32c3_2.8TFT-LCD_SCD30_BME280_SD-card.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <esp_sntp.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 <SPI.h>
#include <SD.h>
File dataFile;
TFT_eSPI tft = TFT_eSPI();
SCD30 airSensor;
float co2_tmp;
Adafruit_BME280 bme;
bool status;
float pressure;
float temp;
float humid;
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 char* weekStr[7] = {"(Sun)","(Mon)","(Tue)","(Wed)","(Thu)","(Fri)","(Sat)"};
char d_mes[12] ;
char t_mes[12] ;
struct tm *tm;
int d_year ;
int d_mon ;
int d_mday ;
int d_hour ;
int d_min ;
int d_sec ;
int d_sec_b ;
int d_wday ;
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(3);
tft.setTextSize(1);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("Initializing TFT library", 0, 20, 4);
delay(1000);
//---------SDカード初期化
tft.drawString("Initializing SD library", 0, 80, 4);
delay(1000);
//---------SDカードマウント確認
if (!SD.begin(5)) { // SS_pin = 5
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);
//---------内蔵時計のJST同期(起動時)--------
wifisyncjst();
// BME280初期化
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, 7, 4);
// CO2濃度(ppm)
tft.drawXBitmap(5, 47, ccc_bmp, 32, 32, 0xFFFF);
tft.drawXBitmap(30, 47, ooo_bmp, 32, 32, 0xFFFF);
tft.drawString("2", 62, 60, 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);
}
void loop() {
//---------内蔵時計の表示--------
time_t t = time(NULL);
tm = localtime(&t);
d_year = tm->tm_year+1900;
d_mon = tm->tm_mon+1;
d_mday = tm->tm_mday;
d_hour = tm->tm_hour;
d_min = tm->tm_min;
d_sec = tm->tm_sec;
d_wday = tm->tm_wday;
sprintf(d_mes, "%04d/%02d/%02d", d_year, d_mon, d_mday);
sprintf(t_mes, "%02d:%02d:%02d", d_hour, d_min, d_sec);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(70, 190, 4);
tft.println(d_mes);
tft.setCursor(200, 190, 4);
tft.println(weekStr[d_wday]);
tft.setCursor(110, 215, 4);
tft.println(t_mes);
// -------- 10秒毎に測定してTFT表示 --------
if((String(d_sec) == "0") || (String(d_sec) == "10") ||
(String(d_sec) == "20") || (String(d_sec) == "30") ||
(String(d_sec) == "40") || (String(d_sec) == "50")) {
// ----- SCD30センサが稼働している時の処理 -----
if (airSensor.dataAvailable()) {
// ----- SCD30センサからデータ取得、測定値を表示 -----
tft.setTextColor(TFT_WHITE, TFT_BLACK);
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);
} //dataAvailable_SCD30ここまで
// -----SDカード書き込み
if( d_sec != d_sec_b ) { // 2重書き込み防止判定
sdcardwrite();
d_sec_b = d_sec; // 書き込んだ時点の秒を記憶
}
} //10秒毎ここまで
//---------内蔵時計のJST同期(1日1回、12時30分0秒に実行)--------
if ((String(d_hour) == "12") && (String(d_min) == "30") && (String(d_sec) == "0")) {
wifisyncjst();
tft.fillRect(105, 210, 205, 25, TFT_BLACK); // 残像消去
}
delay(200);
}
void sdcardwrite() {
// ----- SDカードへの書き込み用データファイルの生成 -----
// データ格納ファイル生成
String dataString = "";
// 内蔵時計の年月日と時分秒を記録
dataString += String(d_mes);
dataString += ","; // カンマセパレータ
dataString += String(t_mes);
// 測定データ1:CO2濃度
dataString += ","; // カンマセパレータ
if(!isnan(co2_tmp)){
dataString += String(co2_tmp,0);
}else{
dataString += " ";
}
// 測定データ2:BME280の気圧
dataString += ","; // カンマセパレータ
if(!isnan(pressure)){
dataString += String(pressure,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();
delay(100);
}
void wifisyncjst() {
//---------内蔵時計のJST同期--------
// WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
tft.drawString("WiFi bigin", 110, 215, 4);
}
delay(1000);
// WiFi接続の表示
tft.drawString("WiFi connected", 110, 215, 4);
delay(1000);
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
tft.drawString("JST synchronized", 110, 215, 4);
delay(1000);
// 内蔵時計の時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
delay(1000);
}
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
補足:SCD41センサを使ったスケッチ(2023/1/23追加)
CO2濃度センサをSCD41センサモジュール(FSNS-SCD41-X00)に替えたときのスケッチです。他のコードは同じです。
sparkfun/SparkFun_SCD4x_Arduino_Library サイトのSCD4xライブラリを利用させていただきました。 サイトの「Code」プルダウンから「SparkFun_SCD4x_Arduino_Library-main.zip」をダウンロードしてインクルードします。
![SDカードに記録した測定データ(datalog.txt)](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-25_SCD41_datalog-1024x577.png)
![2.8インチTFT液晶モジュールと内蔵SDカードスロットをSPI接続して環境センサの測定データをSDカードに記録](https://yokahiyori.com/wp-content/uploads/2023/01/2023-01-23_DSC00244_b-1024x576.png)
※SCD41センサと2.8インチTFT液晶は供給電流が大きいのでXIAO ESP32C3の3V3端子から個別にジャンパーワイヤで3.3Vを給電
![](https://yokahiyori.com/wp-content/uploads/2022/12/2022-12-30_SCD41-300x169.png)
xiao-esp32c3_2.8TFT-LCD_SCD41_BME280_SD-card.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <esp_sntp.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 <SPI.h>
#include <SD.h>
File dataFile;
TFT_eSPI tft = TFT_eSPI();
SCD4x mySensor;
float co2_41x;
Adafruit_BME280 bme;
bool status;
float pressure;
float temp;
float humid;
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 char* weekStr[7] = {"(Sun)","(Mon)","(Tue)","(Wed)","(Thu)","(Fri)","(Sat)"};
char d_mes[12] ;
char t_mes[12] ;
struct tm *tm;
int d_year ;
int d_mon ;
int d_mday ;
int d_hour ;
int d_min ;
int d_sec ;
int d_sec_b ;
int d_wday ;
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(3);
tft.setTextSize(1);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("Initializing TFT library", 0, 20, 4);
delay(1000);
//---------SDカード初期化
tft.drawString("Initializing SD library", 0, 80, 4);
delay(1000);
//---------SDカードマウント確認
if (!SD.begin(5)) { // SS_pin = 5
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);
//---------内蔵時計のJST同期(起動時)--------
wifisyncjst();
// BME280初期化
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);
//-------SCD41初期化
if (mySensor.begin() == false) {
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawString("SCD41 not detected", 0, 170, 4);
while (1);
}
//The SCD41 has data ready every five seconds
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("SCD41 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, 7, 4);
// CO2濃度(ppm)
tft.drawXBitmap(5, 47, ccc_bmp, 32, 32, 0xFFFF);
tft.drawXBitmap(30, 47, ooo_bmp, 32, 32, 0xFFFF);
tft.drawString("2", 62, 60, 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);
}
void loop() {
//---------内蔵時計の表示--------
time_t t = time(NULL);
tm = localtime(&t);
d_year = tm->tm_year+1900;
d_mon = tm->tm_mon+1;
d_mday = tm->tm_mday;
d_hour = tm->tm_hour;
d_min = tm->tm_min;
d_sec = tm->tm_sec;
d_wday = tm->tm_wday;
sprintf(d_mes, "%04d/%02d/%02d", d_year, d_mon, d_mday);
sprintf(t_mes, "%02d:%02d:%02d", d_hour, d_min, d_sec);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(70, 190, 4);
tft.println(d_mes);
tft.setCursor(200, 190, 4);
tft.println(weekStr[d_wday]);
tft.setCursor(110, 215, 4);
tft.println(t_mes);
// -------- 10秒毎に測定してTFT表示 --------
if((String(d_sec) == "0") || (String(d_sec) == "10") ||
(String(d_sec) == "20") || (String(d_sec) == "30") ||
(String(d_sec) == "40") || (String(d_sec) == "50")) {
// ----- SCD41センサが稼働している時の処理 -----
if (mySensor.readMeasurement()) {
// ----- SCD41センサからデータ取得、測定値をTFT表示 -----
tft.setTextColor(TFT_GREEN, TFT_BLACK);
co2_41x=mySensor.getCO2();
tft.fillRect(105, 47, 140, 45, TFT_BLACK); // 残像消去
tft.drawFloat(co2_41x, 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);
} //dataAvailable_SCD41ここまで
// -----SDカード書き込み
if( d_sec != d_sec_b ) { // 2重書き込み防止判定
sdcardwrite();
d_sec_b = d_sec; // 書き込んだ時点の秒を記憶
}
} //10秒毎ここまで
//---------内蔵時計のJST同期(1日1回、12時30分0秒に実行)--------
if ((String(d_hour) == "12") && (String(d_min) == "30") && (String(d_sec) == "0")) {
wifisyncjst();
tft.fillRect(105, 210, 205, 25, TFT_BLACK); // 残像消去
}
delay(200);
}
void sdcardwrite() {
// ----- SDカードへの書き込み用データファイルの生成 -----
// データ格納ファイル生成
String dataString = "";
// 内蔵時計の年月日と時分秒を記録
dataString += String(d_mes);
dataString += ","; // カンマセパレータ
dataString += String(t_mes);
// 測定データ1:CO2濃度
dataString += ","; // カンマセパレータ
if(!isnan(co2_41x)){
dataString += String(co2_41x,0);
}else{
dataString += " ";
}
// 測定データ2:BME280の気圧
dataString += ","; // カンマセパレータ
if(!isnan(pressure)){
dataString += String(pressure,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();
delay(100);
}
void wifisyncjst() {
//---------内蔵時計のJST同期--------
// WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
tft.drawString("WiFi bigin", 110, 215, 4);
}
delay(1000);
// WiFi接続の表示
tft.drawString("WiFi connected", 110, 215, 4);
delay(1000);
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
tft.drawString("JST synchronized", 110, 215, 4);
delay(1000);
// 内蔵時計の時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
delay(1000);
}
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}