Arduino MEGA 2560 Rev3(以下、MEGA)と ILI9486を搭載した3.5インチ480×320液晶シールドのmicroSDカードスロットをSPI接続、CO2濃度センサSCD30と気圧・温湿度センサBME280、RTCモジュールDS3231をI2C接続しました。240×320の解像度があるので文字の描画も精細です。
3.5インチ480×320液晶シールドへの表示とmicroSDカードへの記録は10秒毎、RTCモジュールの時刻表示は1分間隔で行うスケッチをプログラムした際のメモです。
MEGAのフラッシュメモリはUnoの32kBから256kBに増えているのでライブラリやFreeFONTを使っても余裕があります。
SPI変換シールド基板を挟んでMEGAと3.5インチ480×320液晶シールドを直結
3.5インチ480×320液晶シールドのmicroSDカードスロットはSPI接続です。この液晶シールドはUno Rev3(以下、Uno)のSPI(デジタルピンD11〜D13)接続を前提に作られているので、SPI変換シールド基板を挟んで、MEGAのICSP端子経由でSPIに割り当てられているデジタルピンD51〜D53に接続変更します。
Uno Rev3 | MEGA Rev3 | 3.5TFT Pin Label | 3.5TFT Pin Description |
D10 | D53 | SD_SS | SD card SPI bus chip select signal, low level enable |
D11 | D51 | SD_DI | SD card SPI bus MOSI signal |
D12 | D50 | SD_DO | SD card SPI bus MISO signal |
D13 | D52 | SD_SCK | SD card SPI bus clock signal |
3.5インチ480×320液晶シールドのピン配列の詳細は下記に纏めました。
スイッチサイエンスのAmazon店で販売されているArduino用バニラシールド基板ver.21を使って、Unoに合わせて作られた3.5インチ480×320液晶シールド基板のSPI端子(D11〜D13)をMEGAのICSP端子に結線するSPI変換シールド基板を作ります。
SPI変換シールド基板のICSPパターンには6Pソケットをはんだ付けします。6Pソケットは手持ちがなかったので1列x42ピンを切って使っています。
ICSP端子にはCSピンの割り当てはないので、別途MEGAのデジタルピンD53をつなぐ必要があります。今回、CSとしてSPI変換シールド基板のD10パターンからジャンパーワイヤで引き出してD53ピンに挿します。
MEGAでは使わないSPI変換シールド基板のピン4本(D10~D13)をカットします。
ILI9486を搭載した3.5インチ480×320液晶シールド基板の裏面には、5Vから3.3Vを生成する3端子レギュレータ(U3)、信号線のレベル変換チップ(U1、U2)が実装されています。
SDカードソケットは、小型のmicroSDカード。FAT32でフォーマットしておきます。64GBや128GBのSDカードを使う際には、32GB以下の容量でパーティションを切るか、32GB以上のFAT32に対応した Raspberry Pi Imager の「Erase」を使ってフォーマットしています。
Arduino MEGA 2560 Rev3とセンサをI2Cで接続
MEGAの基板コネクタ端のデジタルピンにI2C(D20:SDA、D21:SCL)があります。この場所は3.5インチ480×320液晶シールド基板のコネクタで塞がらないのでジャンパーワイヤで引き出せます。
I2C(SCL、SDA)、3.3VとGNDの4線をBME280センサ、SCD30センサ、RTCモジュールDS3231と結線します。
CO2濃度センサSCD30の電源電圧の仕様は3.3 V – 5.5 Vですが、I2CのVIH (Input high level voltage))は1.75 V – 3.0 Vの仕様なのでI2Cバス用双方向電圧レベル変換モジュール(秋月電子、PCA9306)を入れてます。I2Cバス用双方向レベル変換モジュールのVREF2とVREF1には5V、3.3Vの電圧を加えます(VREF2が高電圧側)。
MEGAの3.3V(3V3ピン)が3.5インチ480×320液晶シールドのコネクタで塞がってジャンパーワイヤを引き出せないので、DC-DC降圧モジュール(MP1584EN)で5Vから3.3Vを供給しています。DC-DC降圧モジュールのIN+とIN-間に5Vを印可したとき、OUT+とOUT-間が「3.3V」になるようにモジュール上の半固定ボリュームを調整しておきます。INとOUT側のジャンパーピンは、余ったワイヤ線などをはんだ付けして自作します。
SCD30センサモジュールの消費電流は比較的大きい(平均19mA、最大(測定時)70mA)ので電源電圧の低下に注意します。今回使ったDS3231とBME280モジュールは3.3Vと5Vの両方で動作するので、SCD30センサ含めて、電源容量に余裕のあるDC-DC降圧モジュールの3.3V出力から給電しています。
Handling and Assembly Guide for SCD30 のp2に、マウントの向きについて「SCD30は、上向きにも下向きにも取り付けることができます」との記載があるので、橙色に点滅する発光面が見えるように取り付けています。
結線図
Arduino MEGA 2560 Rev3、3.5インチ480×320液晶シールド、BME280、SCD30、DS3231で作る環境モニタの結線図です。SPI変換シールド基板、3.5インチ480×320液晶シールド基板の記載は省略しています。MEGAとコネクタ接続で3階建てです。
センサの測定データを液晶表示してmicroSDカードに記録するスケッチ
Arduinoボードの開発環境となる「Arduino IDE」をインストール、各種センサや液晶ディスプレイとArduinoとの接続を容易にする「ライブラリ」を活用してスケッチを作成します。
ライブラリ
先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。
液晶モジュールのライブラリ
ILI9486を搭載した液晶モジュールのライブラリとしてMCUFRIEND_kbv.h(MCUFRIEND_kbv-master.zip)を、文字表示用にAdafruit_GFX.h(Adafruit-GFX-Library-master.zip)を使っています。
GitHubサイトからzipファイルをPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
Adafruit-GFX-Libraryのフォント「FreeMonoBold24pt7b」や「FreeMonoBold12pt7b」で描画したフォントは拡大しても見やすいです。
SCD30センサライブラリ
SparkFunサイトのライブラリ sparkfun/SparkFun_SCD30_Arduino_Library を利用させていただきました。 サイトの「Code」プルダウンから「SparkFun_SCD30_Arduino_Library-main.zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」でダウンロードしたzipファイルを指定します。
BME280センサライブラリ
BME280センサ(0x76)から測定データを取得するライブラリとしてGitHub掲載の adafruit/Adafruit_BME280_Library と adafruit/Adafruit_Sensor を利用させていただきました。
DS3231ライブラリ、TimeLib.hライブラリ
RTCモジュールに必要なライブラリ「DS3232RTC.h」を取り込みます。名称はDS3232ですがライブラリには「Arduino Library for Maxim Integrated DS3232 and DS3231 Real-Time Clocks」の記載があり、DS3231SNでも使えます。
GitHub「JChristensen/DS3232RTC」サイトの「Code」プルダウンから「Download ZIP」で、PCに「DS3232RTC-master.zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」でダウンロードした「DS3232RTC-master.zip」を指定します。
Time.h、TimeLib.hライブラリは、GitHub「PaulStoffregen/Time」サイトの「Code」プルダウンから「Download ZIP」でPCに「Time-master.zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」で「Time-master.zip」を指定します。
スケッチ(2022/10/09修正)
スケッチを起動すると全画面の白から各モジュールの初期化ステータスを表示した後、測定データの表示画面に遷移します。microSDカードやBME280、SCD30との接続に失敗すると赤文字でエラー表示します。
測定データの表示スペースは、気圧は4桁、温湿度は整数部2桁、少数点以下1桁、CO2濃度は最大5桁です。測定結果の液晶表示とmicroSDカードへの書き込みは10秒毎、時計の表示は1分おきです。
「℃」の「 °」は、2倍に拡大したグレイ色のドット「.」にスクリーン色と同じ黒色の1倍の「.」を座標をずらして重ねて丸孔を描画しています。
tft.setTextColorの背景色指定が効かなくて、測定データの数字表示が重ね文字になりました。気圧、温度、湿度は測定値が変わったら前の表示データをtft.fillRectでスクリーン色と同じ黒四角で消してから最新値を表示する処理を入れていますが、描画時はレトロなパタパタ時計のような表示遅れがあります。
画面描画やセンサ測定の処理で1秒の制御が安定しなかったので、時計の秒表示は止めています。
MEGA_MCUFRIEND_kbv_BME280_SCD30_DS3231_SD.ino
※ここをクリックするとコード表示を開閉できます。
//3.5 INCH TFT Display Shield with Arduino MEGA 2560 Rev3
#include <Adafruit_GFX.h> // Core graphics library
#include <MCUFRIEND_kbv.h> // Hardware-specific library
MCUFRIEND_kbv tft;
#include <Fonts/FreeMonoBold24pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeMonoBoldOblique18pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#define BLACK 0x0000
#define RED 0xF800
#define GREEN 0x07E0
#define WHITE 0xFFFF
#define GREY 0x8410
#define YELLOW 0xFFE0
#define BLUE 0x001F
#define ORANGE 0xFD20
#include <SPI.h>
#include <SD.h>
File dataFile;
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h> // BME280_Library
Adafruit_BME280 bme; // 気圧・温湿度センサ
float pres;
float temp;
float humi;
String op;
String ot;
String oh;
String np;
String nt;
String nh;
#include <SparkFun_SCD30_Arduino_Library.h>
SCD30 airSensor; // CO2濃度センサ
float f_CO2;
String s_CO2;
#include <time.h> // Timeライブラリ
#include <DS3232RTC.h> // DS3232、DS3231用ライブラリ
DS3232RTC myRTC(false);
const char* weekStr[7] = {"(Sun)","(Mon)","(Tue)","(Wed)","(Thu)","(Fri)","(Sat)"};
String otd;
String ntd;
String oth;
String nth;
String otm;
String ntm;
String ots;
String nts;
void setup(void){
struct tm timeInfo;
myRTC.begin();
// myRTC.get()よりcompileTime()が新しい時はcompiletimeをmyRTC.set
time_t time_now, compiletime;
time_now = myRTC.get();
compiletime = compileTime();
if (time_now < compiletime) {
myRTC.set(compiletime); // set compiled time to RTC
}
// DS3231に手動設定(書込み時間を加算した値で調整)
// setTime(9, 45, 0, 4, 5, 2022); // 時、分、秒、日、月、年
// myRTC.set(now());
// TFTライブラリ初期化
tft.reset();
uint16_t ID = tft.readID();
if (ID == 0xD3D3) ID = 0x9486; //force ID if write-only display
tft.begin(ID);
tft.setRotation(1);
tft.fillScreen(BLACK);
tft.setFont(&FreeMonoBold12pt7b);
tft.setTextSize(1);
tft.setCursor(0, 40);
tft.setTextColor(WHITE,BLACK);
tft.print("TFT ID...");
tft.setTextColor(GREEN,BLACK);
tft.print(ID,HEX);
delay(1000);
// SDカードマウント確認
tft.setCursor(0, 80);
tft.setTextColor(WHITE,BLACK);
tft.print("Initializing SD card...");
if (!SD.begin(53)) { // CS-pin 53
tft.setTextColor(RED,BLACK);
tft.print("failed!");
while (1);
}
tft.setTextColor(GREEN,BLACK);
tft.print("done.");
delay(1000);
// SDカードファイル書き込み
tft.setCursor(0, 120);
tft.setTextColor(WHITE,BLACK);
tft.print("File written...");
dataFile = SD.open("/datalog.txt", FILE_WRITE);
dataFile.println("datalog.txt");
dataFile.close();
tft.setTextColor(GREEN,BLACK);
tft.print("datalog.txt");
delay(1000);
// Wireライブラリ初期化
Wire.begin();
// BME280初期化
tft.setCursor(0, 160);
tft.setTextColor(WHITE,BLACK);
tft.print("BME280...");
if (!bme.begin(0x76)) {
tft.setTextColor(RED,BLACK);
tft.print("connection failed");
while (1);
}
tft.setTextColor(GREEN,BLACK);
tft.print("connected");
delay(1000);
// SCD30センサ初期化
airSensor.begin();
tft.setCursor(0, 200);
tft.setTextColor(WHITE,BLACK);
tft.print("SCD30...");
tft.setTextColor(GREEN,BLACK);
tft.print("connected");
delay(1000);
// 項目名の表示
tft.fillScreen(BLACK);
tft.setFont(&FreeMonoBold24pt7b);
tft.setTextSize(1);
tft.setTextColor(WHITE,BLACK);
tft.setCursor(0, 40);
tft.print("Pres:");
tft.setTextColor(GREY,BLACK);
tft.setCursor(350, 40);
tft.print("(hPa)");
tft.setTextColor(WHITE,BLACK);
tft.setCursor(0, 95);
tft.print("Temp:");
tft.setTextColor(GREY,BLACK);
tft.setCursor(350, 95);
tft.print("( ");
tft.setCursor(395, 95);
tft.print("C)");
tft.setTextSize(2);
tft.setCursor(359, 75);
tft.print(".");
tft.setTextColor(BLACK,BLACK);
tft.setTextSize(1);
tft.setCursor(374, 74);
tft.print(".");
tft.setTextColor(WHITE,BLACK);
tft.setCursor(0, 150);
tft.print("Humi:");
tft.setTextColor(GREY,BLACK);
tft.setCursor(350, 150);
tft.print("(%)");
tft.setTextColor(WHITE,BLACK);
tft.setCursor(23, 200);
tft.print("CO");
tft.setFont(&FreeMonoBold18pt7b);
tft.setCursor(85, 205);
tft.print("2");
tft.setFont(&FreeMonoBold24pt7b);
tft.setCursor(112, 200);
tft.print(":");
tft.setTextColor(GREY,BLACK);
tft.setCursor(350, 200);
tft.print("(ppm)");
}
void loop(void){
// RTCから時刻取得
tmElements_t tm;
char d_mes[12] ;
char t_mes[12] ;
char t3_mes[12] ;
myRTC.read(tm);
sprintf(d_mes, "%04d/%02d/%02d", tm.Year + 1970, tm.Month, tm.Day);
sprintf(t_mes, "%02d:%02d", tm.Hour, tm.Minute);
sprintf(t3_mes, "%02d:%02d:%02d", tm.Hour, tm.Minute, tm.Second);
tft.setFont(&FreeMonoBold24pt7b);
tft.setTextColor(GREEN,BLACK);
tft.setTextSize(1);
tft.setCursor(30, 265);
tft.print(d_mes);
tft.setCursor(310, 265);
tft.println( weekStr[tm.Wday - 1] );
if ((String(tm.Hour) == "0") && (String(tm.Minute) == "0") && (String(tm.Second) == "0")) {
tft.fillRect(30,233,410,40,BLACK); // 1日毎に黒背景で文字消去
}
tft.setFont(&FreeMonoBoldOblique18pt7b);
tft.setTextColor(WHITE,BLACK);
tft.setTextSize(2);
if ((String(tm.Minute) == "0") && (String(tm.Second) == "0")) {
tft.fillRect(133,274,90,55,BLACK); // 60分毎に黒背景で文字消去
}
if (String(tm.Second) == "0") {
tft.fillRect(260,274,90,55,BLACK); // 60秒毎に黒背景で文字消去
}
tft.setCursor(130, 320);
tft.println(t_mes);
// -------- 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")){
// ----- BME280センサからデータ取得 -----
pres=bme.readPressure() / 100.0F;
temp=bme.readTemperature();
humi=bme.readHumidity();
tft.setFont(&FreeMonoBoldOblique18pt7b);
tft.setTextSize(2);
tft.setTextColor(ORANGE,BLACK);
// ----- 液晶画面に気圧、温度、湿度の測定値を表示 -----
// 気圧表示(前後で測定値が変わったら黒背景で前の文字消去)
np = String(pres, 0);
if( np != op ) {
tft.fillRect(145,0,200,55,BLACK);
}
tft.setCursor(145, 45);
tft.print(np); // 気圧
op = np;
// 気温表示(前後で測定値が変わったら黒背景で前の文字消去)
nt = String(temp, 1);
if( nt != ot ) {
tft.fillRect(145,55,200,55,BLACK);
}
tft.setCursor(145, 105);
tft.print(nt); // 気温
ot = nt;
// 湿度表示(前後で測定値が変わったら黒背景で前の文字消去)
nh = String(humi, 1);
if( nh != oh ) {
tft.fillRect(145,110,200,55,BLACK);
}
tft.setCursor(145, 160);
tft.print(nh); // 湿度
oh = nh;
// ----- SCD30センサからデータ取得 -----
if (airSensor.dataAvailable()){
f_CO2 = airSensor.getCO2();
s_CO2 = String(f_CO2, 0);
// CO2表示
tft.setTextColor(YELLOW,BLACK);
tft.fillRect(145,165,215,55,BLACK);
tft.setCursor(145, 215);
tft.print(s_CO2); // CO2
}
// ----- SDカードへの書き込み用データファイルの生成 -----
// データ格納ファイル生成
String dataString = "";
// RTCの年月日と時分秒を記録
dataString += String(d_mes);
dataString += ","; // カンマセパレータ
dataString += String(t3_mes);
// 測定データ1:BME280の気圧
dataString += ","; // カンマセパレータ
if(!isnan(pres)){
dataString += String(pres,0);
}else{
dataString += " ";
}
// 測定データ2:BME280の温度
dataString += ","; // カンマセパレータ
if(!isnan(temp)){
dataString += String(temp,1);
}else{
dataString += " ";
}
// 測定データ3:BME280の湿度
dataString += ","; // カンマセパレータ
if(!isnan(humi)){
dataString += String(humi,1);
}else{
dataString += " ";
}
// 測定データ4:SCD30の湿度
dataString += ","; // カンマセパレータ
if(!isnan(f_CO2)){
dataString += String(f_CO2,0);
}else{
dataString += " ";
}
// ----- SDカードのdatalog.txtにdataStringを追加 -----
dataFile = SD.open("/datalog.txt", FILE_WRITE);
dataFile.println(dataString);
dataFile.close();
}
}
// function to return the compile date and time as a time_t value
// from alarm_ex1.ino in Arduino DS3232RTC Library sample sketch by Jack Christensen.
time_t compileTime() {
const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char compMon[4], *m;
strncpy(compMon, compDate, 3);
compMon[3] = '\0';
m = strstr(months, compMon);
tmElements_t tm;
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; //add fudge factor to allow for compile time
}
SCD30センサのセルフキャリブレーション
SCD30センサは換気の良い場所で電源を入れたままで一定時間連続測定してセルフキャリブレーションします。400ppm台の数値に落ち着くと思います。
SparkFun SCD30 CO₂ センサー ライブラリ
注: SCD30 には、自動セルフキャリブレーション ルーチンがあります。Sensirion は、セルフキャリブレーションを完了するために、少なくとも 1 日 1 時間の「新鮮な空気」で 7 日間の連続測定を推奨しています。Note: The SCD30 has an automatic self-calibration routine. Sensirion recommends 7 days of continuous readings with at least 1 hour a day of ‘fresh air’ for self-calibration to complete.
https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
Interface Description Sensirion SCD30 Sensor Module p.13/21
1.4.6 自動セルフキャリブレーション (ASC) の (非) アクティブ化
継続的な自動セルフキャリブレーションは、次のコマンドで (非) アクティブにすることができます。 初めてアクティブ化する場合、アルゴリズムが ASC の初期パラメーター セットを見つけることができるように、最低 7 日間必要です。 センサーは、毎日少なくとも 1 時間は新鮮な空気にさらす必要があります。 また、その間、センサーを電源から切り離すことはできません。そうしないと、キャリブレーション パラメータを見つける手順が中止され、最初からやり直す必要があります。 正常に計算されたパラメータは SCD30 の不揮発性メモリに保存され、再起動後も以前に見つかった ASC のパラメータが引き続き存在するという効果があります。1.4.6 (De-)Activate Automatic Self-Calibration (ASC)
https://sensirion.com/media/documents/D7CEEF4A/6165372F/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
Continuous automatic self-calibration can be (de-)activated with the following command. When activated for the first time a period of minimum 7 days is needed so that the algorithm can find its initial parameter set for ASC. The sensor has to be exposed to fresh air for at least 1 hour every day. Also during that period, the sensor may not be disconnected from the power supply, otherwise the procedure to find calibration parameters is aborted and has to be restarted from the beginning. The successfully calculated parameters are stored in non-volatile memory of the SCD30 having the effect that after a restart the previously found parameters for ASC are still present.