ESP32-DevKitCとmicroSDカードリーダを VSPI 接続して、BME280センサで測った温度、湿度、気圧データを1分間隔でmicroSDカードに書き込んだ際の作業メモです。 PCに接続することなく、長期間の測定データを記録できて便利になりました。
追加で集めたパーツ
microSDカードスロットはネット通販サイトに多くありますが、秋月電子通商の「マイクロSDカードスロットDIP化キット」を使いました。microSDメモリーカードは手持ち品です。
# | 今回、追加で準備したパーツ | 個数 |
1 | マイクロSDカードスロットDIP化キット | 1 |
2 | 抵抗 10KΩ | 4 (手持ち) |
3 | microSDメモリーカード 2GB、16GB、32GB、64GB、128GB | 適量 (手持ち) |
4 | ジャンパーワイヤ | 適量 |
# | 環境モニタ(1)~(5)で使った主なパーツ | 個数 |
1 | ESP32-DevKitC ESP-WROOM-32開発ボード | 1 |
2 | DS3231SN I2C RTCモジュール | 1 |
3 | 2004 LCDモジュール 20×4キャラクタ 青 I2C I/F モジュール、バックライト付き、 バックライト調整用半固定ボリューム付き | 2 |
4 | I2Cバス用双方向電圧レベル変換モジュール (PCA9306) | 1 |
5 | XL4015 可変DC-DCステップダウンコンバータ (3個入り) | 1 (手持ち) |
6 | DC-POWER-JACK基板 | 1 |
7 | USBケーブル(USB A オス to microB オス) | 1 (手持ち) |
8 | コイン形リチウムイオン2次電池「LIR2032」 (4個入り) | 1 |
9 | スイッチングACアダプター 9V 1.3A 100~240V | 1 |
10 | BME280 温湿度・気圧センサーモジュール Vin、I2Cレベル変換回路付き | 2 |
11 | LTC4331 絶縁型I2C延長モジュール ※マスターとスレーブの2個セット | 1 |
12 | LANケーブル(RJ45)10m | 1 |
13 | SCD30 CO2センサ モジュール ※ピンヘッダ(1×4)を準備してはんだ付け | 1 |
14 | ピンヘッダ 1列タイプ 40ピン ※40ピンから4ピンをニッパでカット | 1 |
15 | サンハヤト ニューブレッドボード SAD-101 | 2 |
16 | サンハヤト ニューブレッドボード SAD-01 | 1 |
17 | ジャンパーワイヤ | 適量 |
ESP32-DevKitCとmicroSDカードスロットの結線(2022/4/21訂正)
各モジュール間をジャンパーワイヤで接続した際の結線図です。ESP32はシリアル・ペリフェラル・インタフェース(Serial Peripheral Interface, SPI)としてVSPI と HSPI の2つを内蔵していますが、VSPI 接続で結線しました。
マイクロSDカードスロットDIP化キットの端子番号とESP32-DevKitCのピン番号との接続を表にしました。 通信線4本(2、3、5、7)を10KΩでプルアップしています。 端子番号1、8、9、10は未使用です。
DIP化キット 端子番号 | DIP化キット 記号 | ESP32-DevKitC ピン番号 | VSPI | ESP32-DevKitC
2 | CD/DAT3 | GPIO5 | SS (Slave Select) |
3 | CMD | GPIO23 | MOSI (Master Out Slave In) |
4 | VDD | 3V3 | |
5 | CLK | GPIO18 | SCK |
6 | VSS | GND | |
7 | DAT0 | GPIO19 | MISO (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のインストール手順を別ページに纏めました。
スケッチ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実行結果 |
1 | 2GB | FAT16(FAT) | 正常 |
2 | 16GB | FAT32 | 正常 |
3 | 32GB | FAT32 | 正常 |
4 | 64GB | FAT32 | 正常 |
5 | 64GB | exFAT | 認識しない |
6 | 128GB | FAT32 | 正常 |
7 | 128GB | exFAT | 認識しない |
スケッチ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から取得した年/月/日と時:分:秒」で記録するように修正しています。
スケッチ2は屋内と屋外の2つのBME280センサで測った温度、湿度、気圧データをmicroSDカードにファイル名「logdata.txt」でCSV形式で書き込みます。ファイルの拡張子はcsvでなくtxtにしています。
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スケッチ修正)
スケッチ3は、ESP32-DevKitCと二酸化炭素センサ SCD30モジュールを使ってCO2濃度をLCD表示:環境モニタ(5) のスケッチに、RTCで取得した時刻から1分毎(秒の桁が0秒時)に、microSDカードにCSV形式で RTCから取得した(年/月/日、時:分:秒) と 2つのBME280データ(温度、湿度、気圧)を記録するように変更しました。
スケッチ3の実行結果です。1分毎に RTCから取得した(年/月/日、時:分:秒) と 2つのBME280データ(温度、湿度、気圧)がmicroSDカードにCSV形式で書き込まれていることが確認できました。シリアルモニタをつないでいると書き込み状況をモニタできます。
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追加)
ブレッドボード上のテストで動作確認が取れたので、ユニバーサル基板に実装しました。各モジュールはユニバーサル基板に直接はんだ付けしないで、分割ロングピンソケットを介して実装しています。