ESP32-DevKitCとBME280で測った温度、湿度、気圧データとRTC時刻をmicroSDカード書き込み:環境モニタ(6)

ESP32-DevKitCとmicroSDカードリーダを VSPI 接続して、BME280センサで測った温度、湿度、気圧データを1分間隔でmicroSDカードに書き込んだ際の作業メモです。 PCに接続することなく、長期間の測定データを記録できて便利になりました。

手持ちのmicroSDカード
手持ちのmicroSDメモリーカード(開発3社) と マイクロSDカードスロットDIP化キット
目次

追加で集めたパーツ

microSDカードスロットはネット通販サイトに多くありますが、秋月電子通商の「マイクロSDカードスロットDIP化キット」を使いました。microSDメモリーカードは手持ち品です。

#今回、追加で準備したパーツ個数
1マイクロSDカードスロットDIP化キット1
2抵抗 10KΩ4
(手持ち)
3microSDメモリーカード
2GB、16GB、32GB、64GB、128GB
適量
(手持ち)
4ジャンパーワイヤ適量
#環境モニタ(1)~(5)で使った主なパーツ個数
1ESP32-DevKitC ESP-WROOM-32開発ボード1
2DS3231SN  I2C RTCモジュール1
32004 LCDモジュール 20×4キャラクタ 青
I2C I/F モジュール、バックライト付き、
バックライト調整用半固定ボリューム付き

2
4I2Cバス用双方向電圧レベル変換モジュール
(PCA9306)
1
5XL4015 可変DC-DCステップダウンコンバータ
(3個入り)
1
(手持ち)
6DC-POWER-JACK基板1
7USBケーブル(USB A オス to microB オス)1
(手持ち)
8コイン形リチウムイオン2次電池「LIR2032」
(4個入り)
1
9スイッチングACアダプター
9V 1.3A 100~240V
1
10BME280 温湿度・気圧センサーモジュール
Vin、I2Cレベル変換回路付き
2
11LTC4331 絶縁型I2C延長モジュール
※マスターとスレーブの2個セット
1
12LANケーブル(RJ45)10m1
13SCD30 CO2センサ モジュール
※ピンヘッダ(1×4)を準備してはんだ付け
1
14ピンヘッダ 1列タイプ 40ピン
※40ピンから4ピンをニッパでカット
1
15サンハヤト ニューブレッドボード SAD-1012
16サンハヤト ニューブレッドボード SAD-011
17ジャンパーワイヤ適量

ESP32-DevKitCとmicroSDカードスロットの結線(2022/4/21訂正)

各モジュール間をジャンパーワイヤで接続した際の結線図です。ESP32はシリアル・ペリフェラル・インタフェース(Serial Peripheral Interface, SPI)としてVSPI と HSPI の2つを内蔵していますが、VSPI 接続で結線しました。

環境モニタ(6)の結線図
環境モニタ(6)の結線図(2022/4/21訂正)

マイクロSDカードスロットDIP化キットの端子番号とESP32-DevKitCのピン番号との接続を表にしました。 通信線4本(2、3、5、7)を10KΩでプルアップしています。 端子番号1、8、9、10は未使用です。

DIP化キット
端子番号
DIP化キット
記号
ESP32-DevKitC
ピン番号
ESP32-DevKitC
VSPI
2CD/DAT3GPIO5SS (Slave Select)
3CMDGPIO23MOSI (Master Out Slave In)
4VDD3V3 
5CLKGPIO18SCK
6VSSGND 
7DAT0GPIO19MISO (Master In Slave Out)

参考:
SD規格の概要 | SD Association (sdcard.org)
マイクロSDカードスロットDIP化キット | akizukidenshi.com
microSDカードスロットモジュール取扱説明書 | 秋月電子通商
ESP32-DevKitC-1 pin-layout | espressif.com

開発ツールarduino-esp32をインストール

arduino-esp32のインストール手順を別ページに纏めました。

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

スケッチ1:サンプルスケッチ「SD_Test」で読み書きテスト

動作確認として、Arduino IDEメニューの「ファイル」–>「スケッチ例」–> ESP32 Dev Module用のスケッチ例「SD」内のサンプルスケッチ「SD_Test」を実行しました。
このスケッチの実行には時間がかかります。Used space: 1MBがでるまで待ちます

手持ちのmicroSDカードを集めてテストした結果、FAT16とFAT32でフォーマットしたmicroSDカードはサンプルスケッチで正常に書き込み(writeFile、appendFile)でき、Windows 10、11で読み込みできました。

Windowsの「ディスクの管理」では32GBを超えるmicroSDカードのフォーマット形式はNTFSかexFAT(FAT32の後継)の選択となります。64GB、128GBのmicroSDカードは、テスト時はRaspberry Pi Imagerの「Erase」を使って FAT32でカード全体を一括フォーマットしましたが、通常の利用時はWindows PCからFAT32でフォーマットできる32GB以下でパーティションを切って使っています。

#テスト時の
フォーマット容量
ファイル形式SD_Test実行結果
12GBFAT16(FAT)正常
216GBFAT32正常
332GBFAT32正常
464GBFAT32正常
564GBexFAT認識しない
6128GBFAT32正常
7128GBexFAT認識しない
サンプルスケッチ「SD_Test」を実行中のシリアルモニタ
サンプルスケッチ「SD_Test」を実行中のシリアルモニタ
サンプルスケッチ「SD_Test」がmicroSDカードに書き込んだファイル
サンプルスケッチ「SD_Test」がmicroSDカードに書き込んだファイル

スケッチ2:経過時間millis()と2つのBME280データ(温度、湿度、気圧)のmicroSDカード書込み

サンプルスケッチ「SD_Test」を参考に、屋内用と屋外用の2つのBME280センサで測った温度、湿度、気圧データをファイル名「logdata.txt」でSDカードに書き込むスケッチ2を作りました。

以下の7つのデータをカンマセパレータ形式で記録します。
(1)Arduino IDE実行後の経過時間millis()を秒変換(0から1秒ごとにカウントアップ)
(2)~(4)屋内 BME280センサ の温度、湿度、気圧
(5)~(7)屋外 BME280センサ の温度、湿度、気圧

スケッチ2で使ったmillis()は、Arduino Referenceには「Arduino ボードが現在のプログラムを実行し始めてから経過したミリ秒数を返します。この数は、約 50 日後にオーバーフロー (ゼロに戻る) になります」と記載があります。次のスケッチ3では「RTCから取得した年/月/日と時:分:秒」で記録するように修正しています。

スケッチの実行。シリアルモニタを開くと進捗確認できる
スケッチの実行。シリアルモニタを開くと進捗確認できる
microSDカードに記録されたファイル「logdata.txt」内のデータ
microSDカードに記録されたファイル「logdata.txt」内のデータ

スケッチ2は屋内と屋外の2つのBME280センサで測った温度、湿度、気圧データをmicroSDカードにファイル名「logdata.txt」でCSV形式で書き込みます。ファイルの拡張子はcsvでなくtxtにしています。

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

BME280_logging_SD-card.ino
※ここをクリックするとコード表示を開閉できます。
#include "FS.h"     // SDカードで利用
#include "SD.h"     // SDカードで利用
#include "SPI.h"    // SDカードで利用
#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;  // 屋外センサ

void setup() {
Serial.begin(115200);

// SDカードにデータ格納用のファイルを作成
    while (!Serial) {
    }
    if(!SD.begin()){
        Serial.println("Card Mount Failed");
        return;
    }
     writeFile(SD, "/logdata.txt", "--->writeFile");
    appendFile(SD, "/logdata.txt", "--->appendFile\n");

// ----- BME280温湿度・圧力計 -----
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(void){
// BME280データファイルの生成、SDカードのファイルにデータ追記
// 実行後の経過時間millis()を秒に変換
    String dataString = "";
    dataString += String(millis() / 1000);  
// 測定データ1:屋内BME280の温度
    dataString += ",";  // カンマセパレータ
    float temp1 = bme.readTemperature();
    if(!isnan(temp1)){
      dataString += String(temp1);
    }else{
      dataString += " ";
    }
// 測定データ2:屋内BME280の湿度
    dataString += ",";  // カンマセパレータ
    float humi1 = bme.readHumidity();
    if(!isnan(humi1)){
      dataString += String(humi1);
    }else{
      dataString += " ";
    }
// 測定データ3:屋内BME280の圧力
    dataString += ",";  // カンマセパレータ
    float press1 = bme.readPressure() / 100.0F;
    if(!isnan(press1)){
      dataString += String(press1);
    }else{
      dataString += " ";
    }
// 測定データ4:屋外BME280の温度
    dataString += ",";  // カンマセパレータ
    float temp2 = bme2.readTemperature();
    if(!isnan(temp2)){
      dataString += String(temp2);
    }else{
      dataString += " ";
    }
// 測定データ5:屋外BME280の湿度
    dataString += ",";  // カンマセパレータ
    float humi2 = bme2.readHumidity();
    if(!isnan(humi2)){
      dataString += String(humi2);
    }else{
      dataString += " ";
    }
// 測定データ6:屋外BME280の圧力
    dataString += ",";  // カンマセパレータ
    float press2 = bme2.readPressure() / 100.0F;
    if(!isnan(press2)){
      dataString += String(press2);
    }else{
      dataString += " ";
    }
// 最後に改行
    dataString += "\n";
// SDカードのファイルにデータ追記
    appendFile(SD, "/logdata.txt", dataString);
    Serial.println(dataString);
// 調整
delay(1000);
}

// SDカードスロットにはFAT32で初期化済みのSDカードを挿しておく
// SDカードにwriteFile
void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\n", path);
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
    file.close();
}

// SDカードにappendFile
void appendFile(fs::FS &fs, const char * path, String message){
    Serial.printf("Appending to file: %s\n", path);
    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("Failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("Message appended");
    } else {
        Serial.println("Append failed");
    }
    file.close();
}

スケッチ3:RTCで取得した時刻から1分毎に2つのBME280データをmicroSDカード書込み、LCD表示(2022/4/12スケッチ修正)

[2022/04/12]
1分毎(秒の桁が0秒時)にmicroSDカードに記録するようにコード修正しました。

スケッチ3は、ESP32-DevKitCと二酸化炭素センサ SCD30モジュールを使ってCO2濃度をLCD表示:環境モニタ(5) のスケッチに、RTCで取得した時刻から1分毎(秒の桁が0秒時)に、microSDカードにCSV形式で RTCから取得した(年/月/日、時:分:秒) と 2つのBME280データ(温度、湿度、気圧)を記録するように変更しました。

ESP32-DevKitCとmicroSDカードリーダを VSPI 接続で結線
ESP32-DevKitCとmicroSDカードリーダを VSPI 接続で結線。 スケッチ3を実行中、 1分間隔でSDカードに記録

スケッチ3の実行結果です。1分毎に RTCから取得した(年/月/日、時:分:秒) と 2つのBME280データ(温度、湿度、気圧)がmicroSDカードにCSV形式で書き込まれていることが確認できました。シリアルモニタをつないでいると書き込み状況をモニタできます。

スケッチ3でmicroSDカードに記録された測定データ
スケッチ3でmicroSDカードに記録された測定データ
シリアルモニタに表示された測定データ
シリアルモニタに表示された測定データ
ESP32_DS3232RTC_BME280_SD_LCD.ino
※ここをクリックするとコード表示を開閉できます。
#include "FS.h"     // SDカードで利用
#include "SD.h"     // SDカードで利用
#include "SPI.h"    // SDカードで利用
#include <Wire.h>
#include <WiFi.h>
#include <time.h>
#include <LiquidCrystal_I2C.h> // https://github.com/johnrickman/LiquidCrystal_I2C
#include <DS3232RTC.h>         // https://github.com/JChristensen/DS3232RTC
#include <esp_sntp.h>
#include <Adafruit_Sensor.h>   // https://github.com/adafruit/Adafruit_Sensor
#include <Adafruit_BME280.h>   // https://github.com/adafruit/Adafruit_BME280_Library
#include <SparkFun_SCD30_Arduino_Library.h>   // https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library

SCD30 airSensor;
DS3232RTC myRTC(false);
LiquidCrystal_I2C lcd(0x27, 20, 4);    // DS3231時計、SCD30表示
LiquidCrystal_I2C lcd2(0x26, 20, 4);   // 屋内BME280表示、屋外BME280表示
Adafruit_BME280 bme;   // 屋内センサ
Adafruit_BME280 bme2;  // 屋外センサ
float temp;
float pressure;
float humid;
float temp2;
float pressure2;
float humid2;
// 2022/03/27:曜日の文字数を9文字固定に変更
const char* weekStr[7] = {"Sunday   ","Monday   ","Tuesday  ","Wednesday","Thursday ","Friday   ","Saturday "};
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;


void setup() {
struct tm timeInfo;
Serial.begin(115200);
myRTC.begin();
lcd.init();
lcd.backlight();
lcd2.init();
lcd2.backlight();
// ----- SCD30 CO2センサ -----
airSensor.begin();  // SCD30センサ初期化
// LCD表示
lcd.setCursor(0,2);
lcd.print("Wait 10 seconds");
delay(10000);  // センサ安定化のため10秒待機
while (!airSensor.dataAvailable()) {
// LCD表示
lcd.setCursor(19,2);
lcd.print(" ");
delay(500);
}
// ----- DS3231時計 -----
//WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
lcd.setCursor(0,0);
lcd.print("."); // 進捗表示
delay(500);
}
// WiFi接続の表示
lcd.clear();
lcd.print("WiFi connected");
delay(2000);
lcd.clear();
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
lcd.clear();
lcd.print("JST synchro.");
delay(2000);
lcd.clear();
// 内蔵RTCの時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
lcd.print(">"); // 進捗表示
delay(1000);
}
//内蔵RTC時刻 = NTP時刻の表示
lcd.clear();
lcd.print("Time matched");
delay(2000);
lcd.clear();
// 内蔵RTCの時刻の取得
getLocalTime(&timeInfo);
// 内蔵RTCの時刻をDS3231に時刻設定
setTime(timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec,
timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
myRTC.set(now());
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
// SDカードにデータ格納用のファイルを作成
while (!Serial) {
}
if(!SD.begin()){
Serial.println("Card Mount Failed");
return;
}
writeFile(SD, "/logdata.txt", "--->writeFile");
appendFile(SD, "/logdata.txt", "--->appendFile\n");
// ----- BME280温湿度・圧力計 -----
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(void){
// ----- SCD30 CO2センサ -----
if (airSensor.dataAvailable()){
// lcd表示
lcd.setCursor(0,2);
lcd.print("                    ");
lcd.setCursor(0,2);
lcd.print("co2:");
lcd.print(airSensor.getCO2());
lcd.print("ppm");
lcd.setCursor(0,3);
lcd.print("                    ");
lcd.setCursor(0,3);
lcd.print("  T:");
lcd.print(airSensor.getTemperature(), 1);
lcd.print("'C  H:");
lcd.print(airSensor.getHumidity(), 1);
lcd.print("%");
lcd.setCursor(19,2);
}
// ----- DS3231時計 -----
// RTCから時刻取得
tmElements_t tm;
myRTC.read(tm);
// 時計LCD表示
lcd.setCursor(0,0);
lcd.print(tm.Year + 1970);
lcd.print("/");
lcdzeroSup(tm.Month);
lcd.print("/");
lcdzeroSup(tm.Day);
lcd.print(" ");
lcd.setCursor(11,0);
lcd.print(weekStr[tm.Wday - 1]);
lcd.setCursor(0,1);
lcdzeroSup(tm.Hour);
lcd.print(":");
lcdzeroSup(tm.Minute);
lcd.print(":");
lcdzeroSup(tm.Second);
// ----- BME280温湿度・圧力計 -----
// 屋内センサ
temp=bme.readTemperature();
pressure=bme.readPressure() / 100.0F;
humid=bme.readHumidity();
lcd2.setCursor(0,0);
lcd2.print("Press1:");
lcd2.print(pressure,1);
lcd2.print("hPa");
lcd2.setCursor(0,1);
lcd2.print("  T:");
lcd2.print(temp,1);
lcd2.print("'C  H:");
lcd2.print(humid,1);
lcd2.print("%");
// 屋外センサ
temp2=bme2.readTemperature();
pressure2=bme2.readPressure() / 100.0F;
humid2=bme2.readHumidity();
lcd2.setCursor(0,2);
lcd2.print("Press2:");
lcd2.print(pressure2,1);
lcd2.print("hPa");
lcd2.setCursor(0,3);
lcd2.print("  T:");
lcd2.print(temp2,1);
lcd2.print("'C  H:");
lcd2.print(humid2,1);
lcd2.print("%");

// 2022/4/12:1分毎にSDカード記録に変更
if( String(tm.Second) == "0" ){
// データ格納ファイル生成
String dataString = "";
// RTCの年月日と時分秒を記録
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);
dataString += String(d_mes);
dataString += ",";  // カンマセパレータ
dataString += String(t_mes);
// 測定データ1:屋内BME280の温度
dataString += ",";  // カンマセパレータ
float temp1 = bme.readTemperature();
if(!isnan(temp1)){
dataString += String(temp1);
}else{
dataString += " ";
}
// 測定データ2:屋内BME280の湿度
dataString += ",";  // カンマセパレータ
float humi1 = bme.readHumidity();
if(!isnan(humi1)){
dataString += String(humi1);
}else{
dataString += " ";
}
// 測定データ3:屋内BME280の圧力
dataString += ",";  // カンマセパレータ
float press1 = bme.readPressure() / 100.0F;
if(!isnan(press1)){
dataString += String(press1);
}else{
dataString += " ";
}
// 測定データ4:屋外BME280の温度
dataString += ",";  // カンマセパレータ
float temp2 = bme2.readTemperature();
if(!isnan(temp2)){
dataString += String(temp2);
}else{
dataString += " ";
}
// 測定データ5:屋外BME280の湿度
dataString += ",";  // カンマセパレータ
float humi2 = bme2.readHumidity();
if(!isnan(humi2)){
dataString += String(humi2);
}else{
dataString += " ";
}
// 測定データ6:屋外BME280の圧力
dataString += ",";  // カンマセパレータ
float press2 = bme2.readPressure() / 100.0F;
if(!isnan(press2)){
dataString += String(press2);
}else{
dataString += " ";
}
// 最後に改行
dataString += "\n";
// SDカードのファイルにデータ追記
appendFile(SD, "/logdata.txt", dataString);
Serial.println(dataString);
}
// delay()値を調整
delay(100);
}

// ----- DS3231時計 -----
// 先頭のゼロ(0)を空白に置換
void lcdzeroSup(int digit)
{
if(digit < 10)
lcd.print(' ');
lcd.print(digit);
}

// SDカードスロットにはFAT32で初期化済みのSDカードを挿しておく
// SDカードにwriteFile
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}

// SDカードにappendFile
void appendFile(fs::FS &fs, const char * path, String message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}

ユニバーサル基板に実装(2022/4/23追加)

ブレッドボード上のテストで動作確認が取れたので、ユニバーサル基板に実装しました。各モジュールはユニバーサル基板に直接はんだ付けしないで、分割ロングピンソケットを介して実装しています。

環境センサをユニバーサル基板化
ユニバーサル基板で実装した環境センサ(表面)
環境センサをユニバーサル基板化
ユニバーサル基板で実装した環境センサ(裏面)
よかったらシェアしてね!
  • URLをコピーしました!
目次