ESP32-DevKitC と SCD30 + 2つのBME280で作る環境モニタ:LTC4331とLANケーブルでI2Cバスを20m延長して屋外設置のBME280で温湿度を測定

ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールDS3231、CO2濃度センサSCD30、屋内と屋外の気圧・温湿度測定用に2個のBME280(アドレスは0x76と0x77)をI2C接続。絶縁型I2C延長モジュールLTC4331とLANケーブルでESP32-DevKitCのI2Cバスを20m先まで延長して屋外設置のBME280を接続、外気の温湿度の変化を遠隔モニタした際のメモです。

右下の白いブレッドボードがLTC4331とLANケーブルでI2Cバスを延長したBME280センサ(6ピン基板)
目次

BME280モジュールのI2Cアドレス変更

屋内と屋外の温湿度を測定するため、2個のBME280に異なるI2Cアドレスを割り当てます。BME280は、SDOピンをGNDにつなぐとI2Cアドレスが0x76となり、SDOピンをVCCにつなぐと0x77に設定できます。
BME280モジュールには4ピンと6ピンの基板があり、6ピンのモジュールではSDO線がピン端子として出ているのでSDOをVCCにつなぐとI2Cアドレスを0x77に変更できました。4ピンのモジュールではSDO線はプリントパターン上でGNDにつながっており(デフォルトのI2Cアドレスは0x76)、I2Cアドレスを変更するにはプリントパターンをカットするなど作業の難易度が高いです。

下記の6ピン基板はSPI接続とI2C接続の両方で使えますが、信号レベルは3.3V。4ピン基板には基板裏面に電源電圧Vccのレギュレータ(LDO:Low Dropout)と I2Cの電圧レベル変換回路を実装しているので、5V系(Arduinoなど)と3.3V 系(ESP32-DevkitCなど)のどちらとも直接つないで使えます。

参考:BME280 Datasheet(BOSCHサイト, Documents, PDF)
*上記 pdfファイルの「6.2 I2C Interface」項にアドレス変更の記載あり

6ピン基板(SPIとI2C接続の両方で使えます)と4ピン基板(I2C接続)のBME280モジュール

LTC4331モジュールによるI2C延長

I2Cは複数デバイスを2線で接続できて便利なのですが、実用的な通信距離はデバイス間や基板間程度の近距離に制限されます。LTC4331絶縁型I2C延長モジュール(マスタとスレーブ用の2個で1セット)を使うことで、I2C信号を差動通信方式に変換してLANケーブルで延長します。
LTC4331モジュール説明書 の手順でモジュール基板上のジャンパーパターンをはんだ付けしてショートすることでマスタ/スレーブ設定、プルアップ抵抗の有効/無効、Link LED点灯などを設定します。スレーブ側のLTC4331モジュールの内蔵のプルアップ抵抗(10KΩ)を有効化しています。

LTC4331モジュールのマスタ側とスレーブ側をつなぐLANケーブルは4本の信号線を使いました。通信用には4番線と5番線のみですが、スレーブ側(延長先、屋外)のLTC4331モジュールとBME280センサモジュールに給電するために1番線に3.3V、2番線にGNDを追加で結線しています。

LANケーブルを使っていますが ネットワークHUBに刺しての延長はできません。
参考:LTC4331 絶縁型I2C延長モジュール(株式会社ストロベリー・リナックス)

結線図

ESP32-DevKitCモジュールのI2C信号レベルは3.3V。センサモジュールとESP32-DevKitCとの結線は、I2Cの2線(SCL、SDA)と電源の2線(3.3V、GND)の4線なので容易です。
ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールのDS3231、CO2濃度センサSCD30と気圧・温湿度センサBME280をI2C接続する結線図です。屋外設置のBME280は絶縁型I2C延長モジュールLTC4331とLANケーブルでESP32-DevKitCのI2Cバスを20m先まで延長して接続しています。
LTC4331にはI2Cプルアップ抵抗が内蔵されており、ジャンパーパターンをはんだ付けしてショートすることで有効/無効を設定できます。

電源供給は、PC等のUSBコネクタからESP32-DevkitCのUSB micro Bコネクタに給電するか、5V出力のACアダプターをESP32-DevkitCの5Vピンにつないで動作させます(ESP32-DevkitCは内部に5Vから3.3Vの降圧レギュレータを内蔵)。ケース実装するため、2.1mm標準DCジャックに接続できるACアダプターを使っています。

手持ちのACアダプターが9V出力だったので、DC-DC降圧モジュール(MP1584EN)を使って5Vに降圧してESP32-DevkitCの5Vピンに外部給電しています。
DC-DC降圧モジュールのIN+とIN-間に9Vを印可したとき、OUT+とOUT-間が5Vになるようにモジュール上の半固定ボリュームを調整しておきます。

DC-DC降圧モジュール
結線図

参考:ESP32-DevKitC-1 pin-layout | espressif.com

開発ツール arduino-esp32の準備

ESP32-DevKitCの開発ツール arduino-esp32を準備してスケッチを作っていきます。本記事の作成時(2025年3月7日)のArduino IDEのバージョンは、2.3.4、esp32 by Espressif Systemsのバージョンは3.2.0-RC1でした。

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

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

先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。
ライブラリのzipファイルをPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
開発ツールarduino-esp32に同梱されているモジュール「esp_sntp.h」を利用しています(インクルード不要)。詳細は Official develooment framework for ESP32 (ESP-IDF) に記述があります。mulong.meサイトを参考にESP32内蔵RTCの時刻が NTPで取得した時刻に一致しているかの判定に使っています。

ライブラリzipファイル名
1LI9341液晶モジュール
Bodmer/TFT_eSPI
TFT_eSPI-master.zip
2SCD30 CO2センサ
SparkFun_SCD30_Arduino_Library
SparkFun_SCD30_Arduino_Library-main.zip
3BME280センサ
Adafruit_Sensor
Adafruit_BME280_Library
Adafruit_BusIO
Adafruit_Sensor-master.zip
Adafruit_BME280_Library-master.zip
Adafruit_BusIO-master.zip
4リアルタイムクロックDS3231
JChristensen/DS3232RTC
DS3232RTC-master.zip
※DS3231にも利用可能
5TimeライブラリのsetTime()関数
PaulStoffregen/Time
Time-master.zip

ライブラリの追加手段としては、Arduino IDEの「ライブラリマネージャ」で絞り込み検索してインストールすることもできます。

TFT_eSPIライブラリのUser_Setup.hの編集

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

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

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

C:\Users\[ユーザー名]\Documents\Arduino\libraries\TFT_eSPI-master
または、下記のフォルダ名になっている場合もあります。
C:\Users\[ユーザー名]\Documents\Arduino\libraries\TFT_eSPI

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

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

スケッチの作成、ブレッドボード上で動作確認

(1)2つのBME280測定結果をシリアルモニタに表示するスケッチ

BME280の動作確認のため、屋内用のBME280センサ(0x76)と屋外用のBME280センサ(0x77)の両センサから測定データを取得してシリアルモニタに出力します。

スケッチを書き込む前に、Arduino IDEの右上の「シリアルモニタ」アイコンをマウスでクリックしてシリアルモニタを115200 baudで開いておきます。

ESP32_BME280x2_com.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <Adafruit_Sensor.h>  // https://github.com/adafruit/Adafruit_Sensor
#include <Adafruit_BME280.h>  // https://github.com/adafruit/Adafruit_BME280_Library

Adafruit_BME280 bme;   // 屋内用センサ
Adafruit_BME280 bme2;  // 屋外用センサ

float temp;
float pressure;
float humid;
float temp2;
float pressure2;
float humid2;

void setup() {
  Serial.begin(115200);
  bool status;
  status = bme.begin(0x76);  
  while (!status) {
    Serial.println("屋内BME280 sensorが使えません");
    delay(1000);
  }
  status = bme2.begin(0x77);  
  while (!status) {
    Serial.println("屋外BME280 sensorが使えません");
    delay(1000);
  }
}

void loop() { 
  temp=bme.readTemperature();
  pressure=bme.readPressure() / 100.0F;
  humid=bme.readHumidity();
  Serial.print("屋内( ");
  Serial.print(temp);
  Serial.print(" *C  ");
  Serial.print(pressure);
  Serial.print(" hPa  ");
  Serial.print(humid);
  Serial.print(" % )");
  
  temp2=bme2.readTemperature();
  pressure2=bme2.readPressure() / 100.0F;
  humid2=bme2.readHumidity();
  Serial.print("  屋外( ");
  Serial.print(temp2);
  Serial.print(" *C  ");
  Serial.print(pressure2);
  Serial.print(" hPa  ");
  Serial.print(humid2);
  Serial.println(" % )");

  delay(1000);
}

シリアルモニタからの出力例です。

(2)測定結果を2.8インチ240×320液晶に表示するスケッチ

時刻、気圧、CO2濃度、屋内と屋外の温湿度の測定結果を2.8インチ240×320液晶に表示、SDカードに記録するスケッチ「esp32-devkitc_ili9341_scd4x_bme280x2_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桁。なお、氷点下の場合はマイナス符号表示のため、-10°C以下では少数点以下が表示領域を右側にはみ出すのでスケッチの修正が必要です。
気温・湿度は右側が室内に設置したBME280の測定結果(黄色で表示)、左側が屋外に設置したBME280の測定結果(青色で表示)を表示します。最下段に日時、曜日、時刻を表示します。
気圧やCO2濃度は表示桁数が4桁や3桁に変わる際に表示桁がずれる(残像が残る)、温度は氷点下ではマイナス符号が表示されて表示桁がずれるので、測定値を表示する前に黒背景で残像を消去しています。

日本気象協会 tenki.jpで外気温度が氷点下になった早朝の気温(マイナス表示)を確認中。
屋外のBME280センサは、20mのLANケーブルで延長して、屋根上の薪ストーブ煙突近くに設置

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

SDカードに10秒間隔でカンマセパレータパレータ形式で記録
左から年月日、時分秒、気圧(屋内、屋外)、CO2濃度、気温(屋内、屋外)、湿度(屋内、屋外)

1秒毎に日時・時刻を、10秒毎に気圧、CO2濃度、気温、湿度を2.8インチ240×320液晶に表示してSDカードに記録するスケッチ esp32-devkitc_ili9341_scd30_bme280x2_ds3231_SD.ino です。SCD30は2秒毎に計測データの準備が完了(SCD41は5秒毎)する仕様です。

メモ:
・再起動の都度、前回起動時に書き込まれたSDカードのデータは削除。
・29、30行には、WiFi接続環境のSSIDとそのパスワードを記述。
更新履歴:
・リアルタイムクロックのJST時刻合わせを起動時に加えて、1日1回 12時30分にwifi接続して実行する処理を追加(2025/03/21)
・起動時の初期化状況画面でwifi接続時のIPアドレス表示を追加(2025/03/21)

esp32-devkitc_ili9341_scd30_bme280x2_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;   // 屋内用センサ
Adafruit_BME280 bme2;  // 屋外用センサ
float temp;
float temp2;
float pressure;
float pressure2;
float humid;
float humid2;

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, };
char wifiIpdisp[14];    // ipアドレス格納

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, 10, 4);
delay(1000);
//-------------DS3231-------------
struct tm timeInfo;
myRTC.begin();
//---------内蔵時計のJST同期(起動時)@2025/03/21追加  --------
  wifisyncjst();
//内蔵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());
// SDカード初期化
spiSD.begin(14, 33, 13, 15); //SCK,MISO,MOSI,CS
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("SD:Initializing library", 0, 40, 4);
delay(1000);
// SDカードマウント確認
if (!SD.begin(15, spiSD)) {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("SD:Card Mount Failed    ", 0, 40, 4);
return;
}
else  {
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("SD:Card Mount Successful   ", 0, 40, 4);
}
delay(1000);
// SDカードファイル書き込み ※前回起動時に書き込んだデータは削除。
File dataFile = SD.open("/datalog.txt", FILE_WRITE);
dataFile.println("File written");
dataFile.close();
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("SD:File written Successful  ", 0, 40, 4);
delay(1000);
// 内部設置BME280センサの初期化
bool status;
status = bme.begin(0x76);
while (!status) {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("BME280(0x76) connection failed", 0, 70, 4);
}
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("BME280(0x76) connected    ", 0, 70, 4);
delay(1000);
// 外部設置BME280センサの初期化
bool status2;
status2 = bme2.begin(0x77);
while (!status2) {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("BME280(0x77) connection failed", 0, 100, 4);
}
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("BME280(0x77) connected        ", 0, 100, 4);
delay(1000);
// SCD30初期化
  Wire.begin();
  if (airSensor.begin() == false)  {
  tft.setTextColor(TFT_RED, TFT_BLACK);
  tft.drawString("SCD30 not detected", 0, 130, 4);
    while (1);
  }
//The SCD30 has data ready every two seconds
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString("SCD30 detected", 0, 130, 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(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);
// ----- 測定周期を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);  // 残像消去(CO2)
   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); 
//---------内蔵RTCのJST同期 @2025/03/21追加(1日1回、12時30分0秒に実行)--------
if ((String(tm.Hour) == "12") && (String(tm.Minute) == "30") && (String(tm.Second) == "0")) {
   tft.fillRect(0, 190, 320, 60, TFT_BLACK);  // 残像消去(年月日、時刻)
     wifisyncjst();
   tft.fillRect(0, 190, 320, 60, TFT_BLACK);  // 残像消去(年月日、時刻)
}
// -------- 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);  // 残像消去(CO2)
   tft.drawFloat(co2_tmp, 0, 105, 47, 6);
// ----- BME280センサからデータ取得、測定値を表示 ----- 
pressure=bme.readPressure() / 100.0F;
temp=bme.readTemperature();
humid=bme.readHumidity();
pressure2=bme2.readPressure() / 100.0F;
temp2=bme2.readTemperature();
humid2=bme2.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);
   tft.setTextColor(TFT_BLUE, TFT_BLACK);
// tft.fillRect(250, 0, 140, 45, TFT_BLACK);  // 残像消去(気圧)
   tft.fillRect(250, 93, 140, 45, TFT_BLACK); // 残像消去(温度)
// tft.drawFloat(pressure2, 250, 105, 0, 6); 
   tft.drawFloat(temp2, 1, 220, 93, 6);
   tft.drawFloat(humid2, 1, 220, 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 += " ";
    }
    dataString += ",";  // カンマセパレータ
    if(!isnan(pressure2)){
      dataString += String(pressure2,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 += " ";
    }
    dataString += ",";  // カンマセパレータ
    if(!isnan(temp2)){
      dataString += String(temp2,1);
    }else{
      dataString += " ";
    }
// 測定データ4:BME280の湿度
    dataString += ",";  // カンマセパレータ
    if(!isnan(humid)){
      dataString += String(humid,1);
    }else{
      dataString += " ";
    }
    dataString += ",";  // カンマセパレータ
    if(!isnan(humid2)){
      dataString += String(humid2,1);
    }else{
      dataString += " ";
    }
// ----- SDカードのdatalog.txtにdataStringを追加 ----- 
File dataFile = SD.open("/datalog.txt", FILE_APPEND);
dataFile.println(dataString);
dataFile.close();
   }
  }
// SDカードに2重書込みが起らないように設定
delay(1000);
}


void wifisyncjst() {
//---------内蔵時計のJST同期--------
//WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  tft.drawString("WiFi bigin", 0, 190, 4);
}
delay(1000);
// WiFi接続の表示
  tft.drawString("WiFi connected", 0, 190, 4);
//IPアドレス表示@2025/03/21追加                                                               
IPAddress wifiIp = WiFi.localIP();
sprintf(wifiIpdisp, "ip = %d.%d.%d.%d", wifiIp[0], wifiIp[1], wifiIp[2], wifiIp[3]);
  tft.drawString(wifiIpdisp, 0, 220, 4);
delay(1000);
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  tft.drawString("JST synchronized", 0, 190, 4);
delay(1000);
// 内蔵RTCの時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
delay(1000);
}
//内蔵RTC時刻 = NTP時刻の表示
  tft.drawString("Time matched        ", 0, 190, 4);
delay(1000);
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}

ユニバーサル基板ではんだ付けして、二合枡ケースに実装:BME280センサの実装位置の違いによる温度ばらつき(2025/03/18追記)

ブレッドボードで動作テストした後で悩むのが、ユニバーサル基板へのモジュールの配置とそれらを収めるケース。今回使ったパーツの中で最も大きな2.8インチTFT液晶(ILI9341)モジュールの横幅が二合枡の内部寸法にジャストフィット、パネル取付用 LANコネクタ&10cmのLANケーブルも二合枡内に格納できました。
二合枡ケースへの実装は下記の別記事にまとめました。

あわせて読みたい
ESP32-DevKitC と SCD30 + 2つのBME280で作る環境モニタ(LTC4331とLANケーブルでI2Cバスを20m延長)用... ESP32-DevKitCとILI9341を搭載した2.8インチTFT液晶モジュールをSPI接続(液晶とタッチパネルはVSPI、SDカードはHSPI)、RTCモジュールDS3231、CO2濃度センサSCD30、屋...


センサ類はケース前面下部に配置しています。BME280センサモジュールをケース表面から1cm程奥まった位置に配置したところブレッドボード実装時より温度が1~2度高めになりました。対策としてBME280センサモジュールピンをジャンバーワイヤでケースから1cmほど外出して評価中です。空気の流れ、人の吐息などで刻々と測定値が変わりますが、しばらく放置しておくと3つのBME280センサの温度の測定値は3つともほぼ同じ値になっています。

BME280センサの実装位置の違いによる温度ばらつきを評価中。
左側から今回作成したLTC4331とLANケーブルでI2Cバスを20m延長して屋外設置のBME280センサと屋内に設置した2合桝ケースに格納したESP32-DevKitC と SCD30 + BME280で作った環境モニタ本体
右端は前回作成した2合桝ケースに格納したESP32-DevKitC と SCD41 + BME280で作った環境モニタ


 

 

よかったらシェアしてね!
  • URLをコピーしました!
目次