Seeed Studio XIAO ESP32C3と2.8インチ液晶モジュール(240×320)を使ったCO2濃度、気圧・温湿度&WiFi時計(3):内蔵SDカードスロットをSPI接続

ILI9341を搭載した240×320の2.8インチTFT液晶モジュールと内蔵するSDカードスロットをSeeed Studio XIAO ESP32C3(以下、XIAO ESP32C3)にSPI接続して動作確認した際の作業メモです。
年月日、時刻、CO2濃度、気圧、気温、湿度をSDカードに記録します。モニタ用として2.8インチTFT液晶にも表示しています。
RTC(DS3231など)は使っていないので、起動時と1日1回NTPサーバに接続して時刻合わせを実行します。

本記事の動作不具合について
esp32 by Espressif Systemsのバージョン3.0.3 では、本サイトの手順とプログラムでは「Seeed Studio XIAO ESP32C3」と「ILI9341を搭載した240×320の2.8インチTFT液晶モジュール」の場合、Leaving… Hard resetting via RTS pin…の後も白画面のままでした。
本記事の執筆当時のesp32 by Espressif Systemsバージョン2.0.5 にすると正常動作を確認できました(Arduino IDEのバージョンは2.3.2)。
当面本記事については esp32 by Espressif Systemsバージョン2.0.5 でお試しください。

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カードに記録
目次

準備したパーツ

ネット通販(秋月電子通商マルツオンライン など)でパーツを集めてブレッドボード上で結線しました。

   パーツ  個数   備考
1Seeed Studio XIAO ESP32C31【M-17454】
2ILI9431搭載2.8インチ
SPI制御タッチパネル付TFT液晶MSP2807
※SDカードスロットはピンヘッダのはんだ付けが必要
1【M-16265】
3BME280 気圧、温湿度センサ モジュール
※今回使ったモジュールはI2C接続、電圧レギュレータとI2C電圧レベル変換回路を内蔵
1手持ち
4CO2センサモジュール
(二酸化炭素+温度/湿度センサ)【SCD30】
1【SCD30】
5プルアップ抵抗適量※必要に応じて
6ブレッドボード 6穴版 EIC-39011【P-12366】
7ミニブレッドボード BB-601(白)
※SDカードスロット結線用
1【M-15178】
8細ピンヘッダ 1×40(黒)1【C-06631】
9ジャンパーワイヤ
オス-オス 10cmセット
オス-オス 15cmセット(SDカードスロット結線用)
適量【C-05371】
10SDカード
※FAT32でフォーマット
※64GB以上のSDカードは32GB以下でパーテションを切るか、32GB以上のFAT32フォーマットができるRaspberry Pi Imager等のツールを利用
1手持ち

・モジュールにピンヘッダが付属しているか、ピンヘッダのピンの太さがブレッドボードに適しているか、自分がはんだ付けする必要があるか等を確認します。
・モジュールがプルアップ抵抗を内蔵しているか、その抵抗値と有効化方法を確認します。無い場合は必要に応じで外部でプルアップ抵抗を実装します。
・基板内に電圧レギュレータやI2C電圧レベル変換回路を内蔵して5V系(Arduinoなど)と3.3V系(XIAO ESP32C3など)の両方で使えるモジュールもあります。一方で電圧レギュレータを内蔵して電源電圧は5V、3.3V両方で使えますがI2CやSPI等のロジックレベルは3.3V系といったモジュールもあるので仕様書で確認します。

結線図と組立て

SCD30センサとBME280センサはI2C接続、2.8インチTFT液晶モジュールとSDカードスロットはSPI接続です。

XIAO ESP32C3のI2CやSPIのピンレイアウトは下記サイトを参照して結線しました。

Getting Started with Seeed Studio XIAO ESP32C3
Pinout diagram

https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/

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カードスロット
2.8インチTFT液晶モジュールと内蔵SDカードスロット

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)
1VCC 
※共通、3.3V
3V3
2GND 
※共通
GND
3CSGPIO2 (CS)
4RESETGPIO3
5DCGPIO4
6SDI(MOSI)GPIO10 (MOSI)
7SCKGPIO8 (SCK)
8LED
※バックライト
3V3
9SDO(MISO)GPIO9 (MISO)

SDカードスロットのCSピンはGPIO5に設定しました。スケッチでは if (!SD.begin(5)) { }でGPIO5をCSピンとして指定します。

  # SDカードスロット(J4コネクタ)のシルク印刷 XIAO ESP32C3のGPIO番号
1SD_CSGPIO5 (CS)
2SD_MOSIGPIO10 (MOSI)
3SD_MISOGPIO9 (MISO)
4SD_SCKGPIO8 (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)」を参照ください。

動作確認できた現在の結線図
結線図

参考:
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
+ Software setup

https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/#software-setup

当サイトでもESP32-DevKitCを例として、arduino-esp32 をArduino IDE 2.0.x版にインストールする手順メモを纏めました。

ボード(XIAO_ESP32C3を選択)とポート(当サイトのPCではCOM6)を切り替えることでXIAO ESP32C3でもそのまま使えます。

Arduino IDEでボードとポートの選択
ボード:XIAO_ESP32C3を選択

本記事の動作不具合について
esp32 by Espressif Systemsのバージョン3.0.3 では、本サイトの手順とプログラムでは「Seeed Studio XIAO ESP32C3」と「ILI9341を搭載した240×320の2.8インチTFT液晶モジュール」の場合、Leaving… Hard resetting via RTS pin…の後も白画面のままでした。
本記事の執筆当時のesp32 by Espressif Systemsバージョン2.0.5 にすると正常動作を確認できました(Arduino IDEのバージョンは2.3.2)。
当面本記事については esp32 by Espressif Systemsバージョン2.0.5 でお試しください。

バージョン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…の後も白画面のままになった場合、電源を入れ直すことで正常に液晶表示されることもありました。

あわせて読みたい
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の編集

2.8インチTFT液晶モジュールを利用するにあたりTFT_eSPIライブラリのインクルードと初期設定を行います。

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

GitHubサイトからTFT_eSPIライブラリのzipファイル( TFT_eSPI-master.zip )をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。

Bodmer/TFT_eSPI

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 の編集
左側は初期のUser_Setup.h 右側は今回結線したXIAO_ESP32C3環境向けのUser_Setup.h(200行目付近)

・ESP32-DevKitC と XIAO_ESP32C3 の2つの環境でArduino IDE使う場合は、User_Setup.hファイルを別々に分けて保存しておき、利用時に入れ替えます。
・ライブラリのアップデートを実施した際にUser_Setup.hファイルが初期化される事象があったのでUser_Setup.hファイルは別フォルダに保存しています。

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 を利用させていただきました。

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

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カードに記録
SDカードに記録した測定データ(datalog.txt)
※ストーブを焚いている部屋の窓を全開にしたのでCO2濃度が1488ppmから400ppm台に下がっています。
2.8インチTFT液晶モジュールと内蔵SDカードスロットをSPI接続して環境センサの測定データをSDカードに記録
XIAO ESP32C3とSDカードソケットをSPI接続

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)
SDカードに記録した測定データ(datalog.txt)
2.8インチTFT液晶モジュールと内蔵SDカードスロットをSPI接続して環境センサの測定データをSDカードに記録
SCD41センサに入れ替え
※SCD41センサと2.8インチTFT液晶は供給電流が大きいのでXIAO ESP32C3の3V3端子から個別にジャンパーワイヤで3.3Vを給電
あわせて読みたい
Seeed Studio XIAO ESP32C3と2.8インチ液晶モジュール(240×320)を使ったCO2濃度、気圧・温湿度&WiFi時... Sensirion社のSCD41を搭載したCO2・温湿度センサモジュールとXIAO ESP32C3とをI2C接続して動作確認した際の作業メモです。 本記事の動作不具合についてesp32 by Espress...
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);
}
よかったらシェアしてね!
  • URLをコピーしました!
目次