「Raspberry Pi Pico」互換機の「RP2040-GEEK」を使用してI2Cで測定したデータを表示して、SDカードに保存するデータロガーの作り方を詳しく紹介します。
今回は「I2C温湿度センサー」で取得したデータを、グラフに表示しながら記録するデータロガーを製作します。グラフ表示の方法についても詳しく紹介しています。
「RP2040-GEEK」の基本的な使用方法や「ArduinoIDE」を使用した開発環境の準備等は以下のリンクで詳しく紹介しています。
1.RP2040-GEEKについて
・基本仕様
・端子配列・機能
2.I2C温湿度センサーの使い方
3.液晶表示のチラツキ防止「スプライト」
・サンプルプログラム
・Adafuit_GFXライブラリで「スプライト」する方法
4.グラフの表示方法
・サンプルプログラム(サイン波の表示)
・動作確認
・グラフ表示プログラム詳細
5.microSDカードリーダーの使い方
6.データロガーの製作
・配線図
・動作紹介
・サンプルプログラム
7.まとめ
1.RP2040-GEEKについて
「RP2040-GEEK」とは「Raspberry Pi Pico」の互換機で、コントローラーにはPicoと同じ「RP2040」が使用されています。
下画像のように「1.14 インチ(240×135)のカラー液晶表示器」と、「microSD(TF)カードリーダー」が搭載されており、これらを個別に配線するよりもコンパクトで安価に実現することができます。
使用方法もは「ラズパイPico」とほぼ同じですが、使用できる端子は「Pico」と比べると少なく、合計6点(GP2〜5,28,29)です。少ないですが、全て入出力端子として使用でき、各種通信(UART、I2C)、アナログ入力にも割り付けて使用できます。
「RP2040-GEEK」の基本的な使用方法は、以下のリンク先で詳しく紹介しています。
・基本仕様
「RP2040-GEEK」の基本仕様は「Raspberry Pi Pico」とほぼ同じで、以下表のようになります。
使用できる端子が「Pico」より少なく、合計6点となります。
項目 | 詳細 |
---|---|
コントローラ | RP2040 |
クロック | 最大133MHz (デュアルコア Arm Contrex M0+プロセッサ) |
Flash / SRAM | 2MB / 264kB |
電源電圧(USB) | DC5V±10% |
USB | USB Type-A |
入出力 | 多機能GPIO x 6 |
通信機能 | UART x 2 / I2C x 2 SPI x 2(液晶表示器とSDカードリーダーで使用) |
アナログ入力 | ADC(12bitアナログコンバータ) x 2 |
液晶表示器 | SPI1 1.14-inch 240×135(ST7789VW) Pixel 65K color IPS LCD display |
TF(microSD) カードリーダー | SPI0 SDIO interface |
寸法 | W61 x H25 x D9 (mm) |
・端子配列・機能
端子配列について、出力端子は全てコネクタになっていて使用できるのは「6ピン」分だけです。
コネクタは3箇所あり「Raspberry Pi Pico」の「BOOTSEL」ボタンと同じ機能の「BOOT」ボタンも側面にあります。
端子機能の詳細は以下表のようになります。
各端子番号の端子ごとに用途に応じた機能を割り付けて使用することができます。
2.I2C温湿度センサーの使い方
今回使用した温湿度センサーについて紹介します。
今回使用したものは下画像の「M5Stack」社製の「温度湿度気圧センサー ENV.IV」で温度、湿度だけでなく気圧も測定できます。
測定データは「I2C通信」で取得することができるため、「RP2040-GEEK」とは以下の配線図のように「I2C/ADC」コネクタ配線を使用して接続します。
実際のプログラムでの使用方法は、この次の液晶表示の「サンプルプログラム」内で、以下のように「getEnv()」関数として使用しています。(ライブラリ不要)
// グローバル変数宣言
float temperature = 0.0; // 温度データ格納用
float humidity = 0.0; // 湿度データ格納用
/************************* 温湿度取得関数 ************************/
void getEnv() {
uint8_t data[6]; // 温度、湿度受信データ格納用
// I2Cデータ送信
Wire.beginTransmission(SHT3X_ADDR); // デバイスアドレスを指定して I2C通信開始
Wire.write(0xFD); // 要求コマンド送信(高精度測定を指定)
Wire.endTransmission(true); // 書き込み完了
delay(10); // データ受信待ち(時間はデータシートによる:0.01s)
// I2Cデータ受信(温度、湿度それぞれ 測定データ:8bit ×2 + CRC:8bit で合計6byte)
Wire.requestFrom(SHT3X_ADDR, 6); // 受信データリクエスト(6byte)
for (int i = 0; i < 6; i++) { // 6byte分繰り返し
data[i] = Wire.read(); // 受信データ格納
}
// 受信データを温度、湿度に換算
int t_ticks = (data[0] << 8) | data[1]; // 温度計算用
int rh_ticks = (data[3] << 8) | data[4]; // 湿度計算用
temperature = -45 + 175 * ((float)t_ticks / 65535); // 温度換算
humidity = constrain(-6 + 125 * ((float)rh_ticks / 65535), 0, 100); // 湿度換算
Serial.printf("Temp:%.1f, Hum:%.1f\n", temperature, humidity); // 各データシリアル出力
}
上記プログラムの詳細や「温度湿度気圧センサー ENV.IV」の使用方法は「I2C通信」の使い方を紹介している、以下のリンク先で詳しく紹介しています。(前モデルのENV.IIIでの紹介ですが使い方は同じです。)
3.液晶表示のチラツキ防止「スプライト」
液晶画面に、頻繁に変化する値を表示する時に、表示データが点滅するような「チラツキ」を防止するための「スプライト」について先に紹介しておきます。
今回使用する「温湿度センサー」の動作確認も兼ねて、下画像のような温度と湿度を表示するだけのシンプルなサンプルプログラムを準備しました。
・サンプルプログラム
サンプルプログラムは以下になります。
コピペで貼り付けて書き込んでください。コピーは下の黒塗り部右上のアイコンクリックでもできます。
#include <Adafruit_GFX.h> // Adafruitのグラフィックスライブラリ
#include <Adafruit_ST7789.h> // ST7789液晶ディスプレイ用のライブラリ
#include <SPI.h> // SPI通信を行うためのライブラリ
#include <Wire.h> // I2C通信を行うためのライブラリ
#include <Fonts/FreeSans18pt7b.h> // フォント読み込み
#define TFT_WIDTH 240 // 画面幅
#define TFT_HEIGHT 135 // 画面高さ
#define TFT_CS 9 // TFT液晶のCSピン
#define TFT_DC 8 // TFT液晶のDCピン
#define TFT_MOSI 11 // TFT液晶のMOSIピン
#define TFT_SCK 10 // TFT液晶のSCKピン
#define TFT_RST 12 // TFT液晶のRSTピン
#define DISP_INTERVAL 500 // データ更新時間をms単位で設定(200以上 REC_INTERVAL 以下)
#define SHT3X_ADDR 0x44 // SHT3X(温度、湿度センサ)I2C通信アドレス
Adafruit_ST7789 tft = Adafruit_ST7789(&SPI1, TFT_CS, TFT_DC, TFT_RST); // TFT液晶のインスタンス設定
// スプライト(メモリ描画領域から一括表示)をcanvasとして準備
// 画面表示をtftではなくcanvasで指定して一括描画することでチラツキなく表示できる
GFXcanvas16 canvas(TFT_WIDTH, TFT_HEIGHT); // 16bitカラースプライト(オフスクリーンバッファ)
// グローバル変数宣言
unsigned long beforTime = 0; // 前回の時間(ms)格納用
float temperature = 0.0; // 温度データ格納用
float humidity = 0.0; // 湿度データ格納用
/************************* 温湿度取得関数 ************************/
void getEnv() {
uint8_t data[6]; // 温度、湿度受信データ格納用
// I2Cデータ送信
Wire.beginTransmission(SHT3X_ADDR); // デバイスアドレスを指定して I2C通信開始
Wire.write(0xFD); // 要求コマンド送信(高精度測定を指定)
Wire.endTransmission(true); // 書き込み完了
delay(10); // データ受信待ち(時間はデータシートによる:0.01s)
// I2Cデータ受信(温度、湿度それぞれ 測定データ:8bit ×2 + CRC:8bit で合計6byte)
Wire.requestFrom(SHT3X_ADDR, 6); // 受信データリクエスト(6byte)
for (int i = 0; i < 6; i++) { // 6byte分繰り返し
data[i] = Wire.read(); // 受信データ格納
}
// 受信データを温度、湿度に換算
int t_ticks = (data[0] << 8) | data[1]; // 温度計算用
int rh_ticks = (data[3] << 8) | data[4]; // 湿度計算用
temperature = -45 + 175 * ((float)t_ticks / 65535); // 温度換算
humidity = constrain(-6 + 125 * ((float)rh_ticks / 65535), 0, 100); // 湿度換算
Serial.printf("Temp:%.1f, Hum:%.1f\n", temperature, humidity); // 各データシリアル出力
}
// 初期設定 -----------------------------------------
void setup(void) {
Serial.begin(9600); // シリアル通信初期化
// I2C初期設定
Wire.setSDA(28); // SDAをGP28に設定
Wire.setSCL(29); // SCLをGP29に設定
Wire.begin(); // I2C通信初期化
// SPI1初期設定(TFT液晶用)
SPI1.setSCK(TFT_SCK); // TFTのSCK (GP10)
SPI1.setTX(TFT_MOSI); // TFTのMOSI (GP11)
// 入出力端子初期設定
pinMode(25, OUTPUT); // TFTバックライト用端子を出力に設定
digitalWrite(25, HIGH); // TFTバックライト点灯
// TFT液晶の初期設定
tft.init(TFT_HEIGHT, TFT_WIDTH); // TFT初期化(画面サイズ指定)
tft.setRotation(3); // 画面回転(0-3)
canvas.setTextSize(1); // 文字サイズ(1)
}
// メイン -----------------------------------------
void loop() {
unsigned long nowTime = millis(); // 経過時間を現在時間として取得(ms)
// 測定データ取得インターバル処理(条件成立時に連続記録)
if (nowTime - beforTime >= DISP_INTERVAL) { // 前回時間から現在時間がインターバル時間経過していたら
beforTime = nowTime; // 前回時間を現在時間で更新
getEnv(); // 温湿度取得関数呼び出し
// 測定データを画面に表示表示
canvas.setFont(&FreeSans18pt7b); // フォント指定
canvas.fillScreen(0x0020); // 背景の塗りつぶし(16bitカラーで指定)
canvas.setTextColor(ST77XX_WHITE); // 文字色
canvas.setCursor(15, 30); // カーソル位置
canvas.print("I2C DISPLAY"); // 温度表示
canvas.drawFastHLine(0, 40, 240, ST77XX_WHITE); // 線(指定座標から平行線)
canvas.setTextColor(ST77XX_ORANGE); // 文字色
canvas.setCursor(10, 80); // カーソル位置
canvas.printf("Temp : %.1fC'", temperature); // 温度表示
canvas.setCursor(10, 125); // カーソル位置
canvas.setTextColor(ST77XX_CYAN); // 文字色
canvas.printf("Hum : %.1f%%", humidity); // 湿度表示
// スプライト(メモリ内に描画した画面)をTFTに描画
tft.drawRGBBitmap(0, 0, canvas.getBuffer(), TFT_WIDTH, TFT_HEIGHT);
}
}
・Adafuit_GFXライブラリで「スプライト」する方法
「Adafruit_GFXライブラリ」には16ビットカラーのグラフィックデータをメモリ上に保持してから表示(スプライト)させる「GFXcanvas16」クラスがあります。
まず、サンプルプログラム「23行目」で以下のように、液晶画面サイズを設定して「canvas」という名前でインスタンスを作成します。
これだけでメモリ内に描画した画像を一括表示する「スプライト」が使用できます。
// スプライト(メモリ描画領域から一括表示)をcanvasとして準備
// 画面表示をtftではなくcanvasで指定して一括描画することでチラツキなく表示できる
GFXcanvas16 canvas(TFT_WIDTH, TFT_HEIGHT); // 16bitカラースプライト(オフスクリーンバッファ)
次に、初期設定としてサンプルプログラム「71〜74行目」で以下のように、「tft.〜」で基本的な画面の設定を行い、「canvas.〜」で表示する内容のプログラムを記述します。
「tft.〜」で指定したものはすぐに処理されますが、「canvas.〜」で指定したものはメモリ内の描画情報として処理されます。
// TFT液晶の初期設定
tft.init(TFT_HEIGHT, TFT_WIDTH); // TFT初期化(画面サイズ指定)
tft.setRotation(3); // 画面回転(0-3)
canvas.setTextSize(1); // 文字サイズ(1)
表示したい内容はサンプルプログラム「87〜101行目」で以下のように「canvas.〜」で指定することで、メモリ内に描画されていきます。
// 測定データを画面に表示表示
canvas.setFont(&FreeSans18pt7b); // フォント指定
canvas.fillScreen(0x0020); // 背景の塗りつぶし(16bitカラーで指定)
canvas.setTextColor(ST77XX_WHITE); // 文字色
canvas.setCursor(15, 30); // カーソル位置
canvas.print("I2C DISPLAY"); // 温度表示
.
.
.
canvas.printf("Hum : %.1f%%", humidity); // 湿度表示
最後に、サンプルプログラム「104行目」で以下のように、メモリ内に描画された「canvas」の画像をビットマップ画像として一括出力します。
// スプライト(メモリ内に描画した画面)をTFTに描画
tft.drawRGBBitmap(0, 0, canvas.getBuffer(), TFT_WIDTH, TFT_HEIGHT);
4.グラフの表示方法
今回制作したデータロガーでは、測定したデータをリアルタイムでグラフに表示しています。
グラフの表示にはそれなりに処理時間がかかるので、高速なデータを記録したい場合はデータ記録後に表示する必要がありますが、今回は見た目重視で表示しています^^
ここでは、グラフの表示方法の確認ということで、下画像のような「サイン波」を表示するサンプルプログラムを作成しました。動作確認しながら、グラフの表示方法を確認してみましょう。
・サンプルプログラム(サイン波の表示)
サンプルプログラムは以下になります。
コピペで貼り付けて書き込んでください。コピーは下の黒塗り部右上のアイコンクリックでもできます。
#include <Adafruit_GFX.h> // Adafruitのグラフィックスライブラリ
#include <Adafruit_ST7789.h> // ST7789液晶ディスプレイ用のライブラリ
#include <SPI.h> // SPI通信を行うためのライブラリ
#include <Wire.h> // I2C通信を行うためのライブラリ
#include <SD.h> // SDカード操作用ライブラリ
#include <Fonts/FreeSans9pt7b.h> // フォント読み込み
#include <Fonts/FreeSans12pt7b.h> // フォント読み込み
#define TFT_WIDTH 240 // 画面幅
#define TFT_HEIGHT 135 // 画面高さ
#define TFT_CS 9 // TFT液晶のCSピン
#define TFT_DC 8 // TFT液晶のDCピン
#define TFT_MOSI 11 // TFT液晶のMOSIピン
#define TFT_SCK 10 // TFT液晶のSCKピン
#define TFT_RST 12 // TFT液晶のRSTピン
#define DISP_INTERVAL 50 // データ更新時間をms単位で設定
Adafruit_ST7789 tft = Adafruit_ST7789(&SPI1, TFT_CS, TFT_DC, TFT_RST); // TFT液晶のインスタンス設定
// スプライト(メモリ描画領域から一括表示)をcanvasとして準備
// 画面表示をtftではなくcanvasで指定して一括描画することでチラツキなく表示できる
GFXcanvas16 canvas(TFT_WIDTH, TFT_HEIGHT); // 16bitカラースプライト(オフスクリーンバッファ)
// グローバル変数宣言
unsigned long beforTime = 0; // 前回の時間(ms)格納用
// グラフの設定
#define MAX_DATA_POINTS 200 // X軸最大記録数
float valueData[MAX_DATA_POINTS]; // 値データ記録配列
int16_t xAxisIndex = 0; // X軸インデックス
float valueMax = 50.0; // 最大値
float valueMin = 0.0; // 最小値
// サイン波の設定
float frequency = 0.05; // サイン波の周波数(調整可能)
float phase = 0.0; // 位相
float amplitude = (valueMax-valueMin)/2; // 振幅
float offset = valueMin + amplitude; // Y軸のオフセット
/************************* グラフ描画関数 ************************/
void drawGraph() {
// グラフ表示エリアを初期化
canvas.fillRect(35, 30, 200, 100, 0x0020); // グラフエリア塗りつぶし
canvas.drawFastHLine(34, 81, 202, 0x7BEF); // Y軸中心線
canvas.drawFastVLine(134, 29, 102, 0x7BEF); // X軸補助線1
canvas.drawFastVLine(84, 29, 102, 0x7BEF); // X軸補助線2
canvas.drawFastVLine(184, 29, 102, 0x7BEF); // X軸補助線3
// グラフ描画処理(map関数を使用して、測定データがグラフエリア内に収まるようにスケーリング)
for (int i = 0; i < xAxisIndex - 1; i++) {
int x1 = i; // グラフ描画の始点
int y1Value = map(valueData[i], valueMin, valueMax, 125, 30); // map(変換元の値,元の下限値,元の上限値,変換後の下限値,変換後の上限値)
int x2 = i + 1; // グラフ描画の終点
int y2Value = map(valueData[x2], valueMin, valueMax, 125, 30);
canvas.drawLine(x1+35, y1Value, x2+35, y2Value, ST77XX_CYAN); // 値データの線を描く
}
}
// 初期設定 -----------------------------------------
void setup(void) {
// SPI1初期設定(TFT液晶用)
SPI1.setSCK(TFT_SCK); // TFTのSCK (GP10)
SPI1.setTX(TFT_MOSI); // TFTのMOSI (GP11)
// 入出力端子初期設定
pinMode(25, OUTPUT); // TFTバックライト用端子を出力に設定
digitalWrite(25, HIGH); // TFTバックライト点灯
// TFT液晶の初期設定
tft.init(TFT_HEIGHT, TFT_WIDTH); // TFT初期化(画面サイズ指定)
tft.setRotation(3); // 画面回転(0-3)
canvas.setTextSize(1); // 文字サイズ(1)
// グラフエリア枠表示
canvas.setFont(&FreeSans9pt7b); // フォント指定
canvas.setTextColor(ST77XX_CYAN); // 文字色
canvas.setCursor(10, 40); // カーソル位置
canvas.printf("%.0f", valueMax); // Y軸値最大値表示
canvas.setCursor(10, 134); // カーソル位置
canvas.printf("%.0f", valueMin); // Y軸値最小値表示
canvas.drawRect(34, 29, 202, 102, ST77XX_WHITE); // グラフエリア外枠
// タイトル表示
canvas.drawFastHLine(0, 25, 240, ST77XX_WHITE); // 線(指定座標から平行線)
canvas.setFont(&FreeSans12pt7b); // フォント指定
canvas.setTextColor(ST77XX_WHITE); // 文字色
canvas.setCursor(0, 20); // カーソル位置
canvas.print(" Wave Disp Test"); // タイトル表示
canvas.setTextColor(ST77XX_CYAN); // 文字色
}
// メイン -----------------------------------------
void loop() {
unsigned long nowTime = millis(); // 経過時間を現在時間として取得(ms)
// インターバル時間ごとに実行
if (nowTime - beforTime >= DISP_INTERVAL) { // 前回時間から現在時間がインターバル時間経過していたら
beforTime = nowTime; // 前回時間を現在時間で更新
float value = amplitude * sin(phase) + offset; // サイン波の生成
phase += frequency; // 位相の更新
canvas.fillRect(190, 0, 50, 21, ST77XX_BLACK); // データ表示エリア黒塗りつぶし
// データを配列に保存
if (xAxisIndex < MAX_DATA_POINTS) { // x軸最大記録数がx軸インデックスより小さければ
valueData[xAxisIndex] = value; // 値データを配列に取得
xAxisIndex++; // X軸インデックスを+1
} else { // 配列が満杯になったら、配列データ全体をシフト(グラフを左へスクロール)
for (int i = 0; i < MAX_DATA_POINTS - 1; i++) { // 各データ配列の最終要素の手前まで繰り返し
valueData[i] = valueData[i + 1];
}
// 最後のデータは新しいデータで更新
valueData[MAX_DATA_POINTS - 1] = value; // 値データを最終要素に取得
}
drawGraph(); // グラフ描画関数呼び出し
// 測定データを画面に表示表示
canvas.setCursor(190, 20); // カーソル位置
canvas.printf("%4.1f", value); // 値表示
// スプライト(メモリ内に描画した画面)をTFTに描画
tft.drawRGBBitmap(0, 0, canvas.getBuffer(), TFT_WIDTH, TFT_HEIGHT);
}
}
・動作確認
プログラムを書き込んで実行すると、下画像のように液晶画面に「サイン波」が表示されていきます。
グラフ表示部の「X軸」は200pixelあり、50msごとに1pixel分(X軸全体で約10秒)の波形を表示していきます。
画面の右端まで波形が表示されると、波形は左へスクロールしながら表示されていきます。
・グラフ表示プログラム詳細
グラフ表示プログラムについて詳しく紹介します。
液晶表示方法についての詳細は「3.液晶表示のチラツキ防止(スプライト)」を参照してください。
まず、グラフ表示領域の設定を行うための変数をサンプルプログラム「30〜34行目」で以下ように宣言しています。
// グラフの設定
#define MAX_DATA_POINTS 200 // X軸最大記録数 ← 液晶画面のpixel数に依存します。
float valueData[MAX_DATA_POINTS]; // 値データ記録配列
int16_t xAxisIndex = 0; // X軸インデックス ← 0〜200まで+1しながら処理。
float valueMax = 50.0; // 最大値
float valueMin = 0.0; // 最小値
次に、表示する「サイン波」の設定を行うための変数をサンプルプログラム「37〜40行目」で以下ように宣言しています。
基本的なサイン波の情報を設定しています。今回は擬似的なデータ表示として使うのでそのまま使用していただければ良いと思います。興味のある方は各数値を色々変更してみてください。
// サイン波の設定
float frequency = 0.05; // サイン波の周波数(調整可能)
float phase = 0.0; // 位相
float amplitude = (valueMax-valueMin)/2; // 振幅
float offset = valueMin + amplitude; // Y軸のオフセット
以下サンプルプログラム「102,103行目」も基本的なサイン波を表示するためのプログラムなので、そのまま使用してください。
float value = amplitude * sin(phase) + offset; // サイン波の生成
phase += frequency; // 位相の更新
以下は測定したデータを取得するプログラムです。
サンプルプログラム「107〜117行目」は、「18行目」で指定した「データ更新時間(ms)」ごとに実行され、測定したデータを「valueData[]」配列の要素に順番に取得していきます。
測定データ数が「MAX_DATA_POINTS(今回は200)」を超えると、取得したデータ配列の要素全体を左へシフトし、最終要素に最新の測定データを取得します。これによりグラフをスクロール表示することができます。
// データを配列に保存
if (xAxisIndex < MAX_DATA_POINTS) { // x軸最大記録数がx軸インデックスより小さければ
valueData[xAxisIndex] = value; // 値データを配列に取得
xAxisIndex++; // X軸インデックスを+1
} else { // 配列が満杯になったら、配列データ全体をシフト(グラフを左へスクロール)
for (int i = 0; i < MAX_DATA_POINTS - 1; i++) { // 各データ配列の最終要素の手前まで繰り返し
valueData[i] = valueData[i + 1];
}
// 最後のデータは新しいデータで更新
valueData[MAX_DATA_POINTS - 1] = value; // 値データを最終要素に取得
}
測定データの取得が完了したら、サンプルプログラム「119行目」で以下のように「drawGraph()」関数を呼び出して、グラフの描画処理を実行します。
drawGraph(); // グラフ描画関数呼び出し
「drawGraph()」関数はサンプルプログラム「43〜59行目」で以下のように、まずグラフ表示エリアの「200×100pixel」の範囲を塗りつぶしてから、X軸とY軸の補助線を描きます。
次に配列の要素番号をX座標、値をY座標として、各要素の前後を1組に、始点終点の座標を指定して線を描いていきます。
void drawGraph() {
// グラフ表示エリアを初期化
canvas.fillRect(35, 30, 200, 100, 0x0020); // グラフエリア塗りつぶし
canvas.drawFastHLine(34, 81, 202, 0x7BEF); // Y軸中心線
canvas.drawFastVLine(134, 29, 102, 0x7BEF); // X軸補助線1
canvas.drawFastVLine(84, 29, 102, 0x7BEF); // X軸補助線2
canvas.drawFastVLine(184, 29, 102, 0x7BEF); // X軸補助線3
// グラフ描画処理(map関数を使用して、測定データがグラフエリア内に収まるようにスケーリング)
for (int i = 0; i < xAxisIndex - 1; i++) {
int x1 = i; // グラフ描画の始点
int y1Value = map(valueData[i], valueMin, valueMax, 125, 30); // map(変換元の値,元の下限値,元の上限値,変換後の下限値,変換後の上限値)
int x2 = i + 1; // グラフ描画の終点
int y2Value = map(valueData[x2], valueMin, valueMax, 125, 30);
canvas.drawLine(x1+35, y1Value, x2+35, y2Value, ST77XX_CYAN); // 値データの線を描く
}
}
5.microSDカードリーダーの使い方
microSDカードの操作はArduinoの「SD」ライブラリを使用します。
今回のデータロガーでは「書き込み処理」のみ使用していますが、読み込みや削除も簡単に行うことができます。
以下から使い方の例として初期設定から、書き込み、読み込み、削除する方法を関数例としてまとめました。
以下は初期設定の例となります。
データのやり取りは「SPI通信」で行われます。以下の例は「RP2040-GEEK」を使用した場合の使用例です。「SPI通信」の端子番号は使用するデバイスに合わせて設定する必要があります。
#include <SPI.h> // SPI通信を行うためのライブラリ
#include <SD.h> // SDカード操作用のライブラリ
// RP2040-GEEKの端子設定
#define SD_CS 23 // SD(TF)カードリーダのCSピン
#define SD_MISO 20 // SD(TF)カードリーダのMISOピン
#define SD_MOSI 19 // SD(TF)カードリーダのMOSIピン
#define SD_SCK 18 // SD(TF)カードリーダのSCKピン
// 初期設定 -----------------------------------------
void setup(void) {
Serial.begin(9600); // シリアル通信初期化
// SPI0初期設定(SDカード用)
SPI.setTX(SD_MOSI); // SD(TF)カードリーダのMOSI(GP19)
SPI.setRX(SD_MISO); // SD(TF)カードリーダのMISO(GP20)
SPI.setSCK(SD_SCK); // SD(TF)カードリーダのSCK(GP18)
// SDカードの初期化
if (!SD.begin(SD_CS)) {
Serial.println("SDカードの初期化に失敗しました");
return;
} else {
Serial.println("SDカードが初期化されました");
}
}
以下から書き込み、読み込み、削除方法を関数例としてまとめたものを紹介します。
いずれも以下のように「FILENAME」でファイル名を指定し、Fileクラスのインスタンスを「myFile」として宣言しておきます。
#define FILENAME "/data.csv" // SD(TF)カードに保存するファイル名
File myFile; // Fileクラスのインスタンスを宣言
書き込み例
書き込み実行時には、まず「SD.open()」関数で「ファイル名」と「書き込みモード」を指定してファイルを開きます。
ファイルが開けたら「myFile.print()」関数で書き込みデータを文字列で指定して書き込みを実行します。(「myFile」はファイルクラスのインスタンス名)
書き込みが完了したら「myFile.close()」関数でファイルを閉じます。
void writeSdData() {
myFile = SD.open(FILENAME, FILE_WRITE); // SDカードのファイルを書き込みモードで開く(なければ作成される)
// データ書き込み
if (myFile) { // ファイルが開けたら
myFile.print("data1, data2"); // csv形式(カンマ区切り)でデータ書き込み
myFile.close(); // ファイルを閉じる
} else { // ファイルが開けなければ
Serial.println("ファイルを開けませんでした");
}
}
読み込み例
読み込み実行時には、まず「SD.open()」関数で「ファイル名」と「読み込みモード」を指定してファイルを開きます。(「, FILE_READ」は省略可)
ファイルが開けたら「While文」で「myFile.available()」関数を実行して、データがある間「myFile.read()」で1文字づつ読み込んで処理します。(「myFile」はファイルクラスのインスタンス名)
全ての文字の読み込みが完了したら「myFile.close()」関数でファイルを閉じます。
void readSdData() {
myFile = SD.open(FILENAME, FILE_READ); // SDカードのファイルを開く
if (myFile) { // ファイルが開けたら
// データ読み込み表示
while (myFile.available()) { // ファイルにデータがあれば繰り返し
char data = myFile.read(); // データを1文字づつ読み込む
Serial.print(data); // シリアル出力
}
myFile.close(); // ファイルを閉じる
} else { // ファイルが開けなければ
Serial.println("ファイルが読み込めませんでした"); // シリアル出力
}
}
削除例
ファイルを削除する時には、「SD.exists()」関数でファイルが存在するか確認し、存在すれば「SD.remove()」関数でファイル名を指定して削除します。
void deleteSdData() {
if (SD.exists(FILENAME)) { // ファイルが存在すれば
// ファイルを削除
if (SD.remove(FILENAME)) { // ファイル削除を実行して成功したら
Serial.println("ファイルを削除しました");
} else { // ファイル削除に失敗したら
Serial.println("ファイルの削除に失敗しました");
}
} else { // ファイルがなければ
Serial.println("ファイルが見つかりません");
}
}
「RP2040-GEEK」でmicroSDカードにデータ保存、読み込み、削除をする方法は、以下リンク先でサンプルプログラムで紹介しています。
6.データロガーの製作
今回制作したデータロガーは下画像のようになります。
・単発記録ボタンを押すごとに測定データをcsv形式(カンマ区切り)でmicroSDカードに記録します。
・連続測定用の切り替えスイッチをONにすると、1秒間隔で連続してデータをcsv形式(カンマ区切り)でmicroSDカードに記録していきます。
・配線図
データロガーの配線図は下画像のようになります。
押しボタンスイッチは「単発記録用」で、押した時に1度だけデータを記録するために使用します。
切り替えスイッチはON/OFF状態を保持するスイッチで、ONの時に設定した間隔ごとにデータを記録し続けるために使用します。
・動作紹介
プログラムを実行すると下画像のような動作が確認できます。
グラフが常時表示され続け、データの記録は外付けのスイッチで操作します。
押しボタンスイッチを押すと、押した瞬間のみデータを記録します。
記録時には画面上の「REC」表示左の丸が赤丸になります。
切り替えスイッチをONすると指定した時間間隔でデータを記録し続けます。記録間隔と同期して「REC」表示左の丸が赤丸に点滅します。
・サンプルプログラム
プログラムは長くなったので、プログラムの詳細についてはここまでで紹介した、各部の使い方の例を参照してください。
サンプルプログラムは以下になります。
コピペで貼り付けて書き込んでください。コピーは下の黒塗り部右上のアイコンクリックでもできます。
#include <Adafruit_GFX.h> // Adafruitのグラフィックスライブラリ
#include <Adafruit_ST7789.h> // ST7789液晶ディスプレイ用のライブラリ
#include <SPI.h> // SPI通信を行うためのライブラリ
#include <Wire.h> // I2C通信を行うためのライブラリ
#include <SD.h> // SDカード操作用ライブラリ
#include <Fonts/FreeSans9pt7b.h> // フォント読み込み
#include <Fonts/FreeSans12pt7b.h> // フォント読み込み
#define TFT_WIDTH 240 // 画面幅
#define TFT_HEIGHT 135 // 画面高さ
#define TFT_CS 9 // TFT液晶のCSピン
#define TFT_DC 8 // TFT液晶のDCピン
#define TFT_MOSI 11 // TFT液晶のMOSIピン
#define TFT_SCK 10 // TFT液晶のSCKピン
#define TFT_RST 12 // TFT液晶のRSTピン
#define SD_CS 23 // SDカードリーダーのCSピン
#define SD_MISO 20 // SDカードリーダーのMISOピン
#define SD_MOSI 19 // SDカードリーダーのMOSIピン
#define SD_SCK 18 // SDカードリーダーのSCKピン
#define DISP_INTERVAL 200 // データ更新時間をms単位で設定(200以上 REC_INTERVAL 以下)
#define REC_INTERVAL 1000 // SDカードへのデータ記録時間をms単位で設定(データ更新時間以上でデータ更新時間で割り切れること)
#define SHT3X_ADDR 0x44 // SHT3X(温度、湿度センサ)I2C通信アドレス
Adafruit_ST7789 tft = Adafruit_ST7789(&SPI1, TFT_CS, TFT_DC, TFT_RST); // TFT液晶のインスタンス設定
File myFile; // Fileクラスのインスタンスを宣言
// スプライト(メモリ描画領域から一括表示)をcanvasとして準備
// 画面表示をtftではなくcanvasで指定して一括描画することでチラツキなく表示できる
GFXcanvas16 canvas(TFT_WIDTH, TFT_HEIGHT); // 16bitカラースプライト(オフスクリーンバッファ)
// グローバル変数宣言
unsigned long beforTime = 0; // 前回の時間(ms)格納用
bool swState = false; // スイッチ状態1保持用
int8_t dispIntervalCount = 0; // 表示時間間隔カウント
float temperature = 0.0; // 温度データ格納用
float humidity = 0.0; // 湿度データ格納用
// グラフの設定
#define MAX_DATA_POINTS 200 // X軸最大記録数
float temperatureData[MAX_DATA_POINTS]; // 温度データ記録配列
float humidityData[MAX_DATA_POINTS]; // 湿度データ記録配列
int16_t xAxisIndex = 0; // X軸インデックス
// 温度と湿度のスケーリング
float tempMax = 40.0; // 最大温度
float tempMin = 0.0; // 最小温度
float humidMax = 90.0; // 最大湿度
float humidMin = 20.0; // 最小湿度
/************************* CSVデータ書き込み関数 ************************/
void writeCsv() {
char buffer[32]; // 文字列格納用バッファ
sprintf(buffer, "%.1f,%.1f", temperature, humidity); // バッファに書き込み
myFile = SD.open("data.csv", FILE_WRITE); // CSVファイルを書き込みモードで開く(無ければ作成)
if (myFile) { // ファイルが開けたら
Serial.println("data.csvを開きました");
myFile.println(buffer); // 文字列を書き込み
myFile.close(); // ファイルを閉じる
} else { // ファイルが開けなければ
Serial.println("data.csvを開けませんでした");
}
}
/************************* 温湿度取得関数 ************************/
void getEnv() {
// 変数宣言
uint8_t data[6]; // 温度、湿度受信データ格納用
// I2Cデータ送信
Wire.beginTransmission(SHT3X_ADDR); // デバイスアドレスを指定して I2C通信開始
Wire.write(0xFD); // 要求コマンド送信(高精度測定を指定)
Wire.endTransmission(true); // 書き込み完了
delay(10); // データ受信待ち(時間はデータシートによる:0.01s)
// I2Cデータ受信(温度、湿度それぞれ 測定データ:8bit ×2 + CRC:8bit で合計6byte)
Wire.requestFrom(SHT3X_ADDR, 6); // 受信データリクエスト(6byte)
for (int i = 0; i < 6; i++) { // 6byte分繰り返し
data[i] = Wire.read(); // 受信データ格納
}
// 受信データを温度、湿度に換算
int t_ticks = (data[0] << 8) | data[1]; // 温度計算用
int rh_ticks = (data[3] << 8) | data[4]; // 湿度計算用
temperature = -45 + 175 * ((float)t_ticks / 65535); // 温度換算
humidity = constrain(-6 + 125 * ((float)rh_ticks / 65535), 0, 100); // 湿度換算
// 各データシリアル出力
// Serial.printf("Temp:%.1f, Hum:%.1f\n", temperature, humidity);
}
/************************* グラフ描画関数 ************************/
void drawGraph() {
// グラフ表示エリアを初期化
canvas.fillRect(35, 30, 200, 100, 0x0020); // グラフエリア塗りつぶし
canvas.drawFastHLine(34, 81, 202, 0x7BEF); // Y軸中心線
canvas.drawFastVLine(134, 29, 102, 0x7BEF); // X軸補助線1
canvas.drawFastVLine(84, 29, 102, 0x7BEF); // X軸補助線2
canvas.drawFastVLine(184, 29, 102, 0x7BEF); // X軸補助線3
// グラフ描画処理(map関数を使用して、測定データがグラフエリア内に収まるようにスケーリング)
for (int i = 0; i < xAxisIndex - 1; i++) {
int x1 = i; // グラフ描画の始点
int y1Temp = map(temperatureData[i], tempMin, tempMax, 125, 30); // map(変換元の値,元の下限値,元の上限値,変換後の下限値,変換後の上限値)
int y1Humid = map(humidityData[i], humidMin, humidMax, 125, 30);
int x2 = i + 1; // グラフ描画の終点
int y2Temp = map(temperatureData[x2], tempMin, tempMax, 125, 30);
int y2Humid = map(humidityData[x2], humidMin, humidMax, 125, 30);
canvas.drawLine(x1+35, y1Temp, x2+35, y2Temp, ST77XX_ORANGE); // 温度データの線を描く
canvas.drawLine(x1+35, y1Humid, x2+35, y2Humid, ST77XX_CYAN); // 湿度データの線を描く
}
}
/************dispIntervalCountが連続記録のトリガーに達したかの判定関数 ************/
bool shouldRecordData() {
// DISP_INTERVALを基準時間として、カウントしREC_INTERVAL時間に達したかを確認
return (dispIntervalCount == (REC_INTERVAL / DISP_INTERVAL));
}
// 初期設定 -----------------------------------------
void setup(void) {
Serial.begin(9600); // シリアル通信初期化
// I2C初期設定
Wire.setSDA(28); // SDAをGP28に設定
Wire.setSCL(29); // SCLをGP29に設定
Wire.begin(); // I2C通信初期化
// SPI0初期設定(SDカード用)
SPI.setTX(SD_MOSI); // SDカードリーダーのMOSI
SPI.setRX(SD_MISO); // SDカードリーダーのMISO
SPI.setSCK(SD_SCK); // SDカードリーダーのSCK
// SPI1初期設定(TFT液晶用)
SPI1.setSCK(TFT_SCK); // TFTのSCK (GP10)
SPI1.setTX(TFT_MOSI); // TFTのMOSI (GP11)
// 入出力端子初期設定
pinMode(25, OUTPUT); // TFTバックライト用端子を出力に設定
digitalWrite(25, HIGH); // TFTバックライト点灯
pinMode(2, INPUT_PULLUP); // 端子「GP2」を入力に設定
pinMode(3, INPUT_PULLUP); // 端子「GP3」を入力に設定
delay(1000); // シリアル通信安定待ち
// SDカードの初期化
if (!SD.begin(SD_CS, SD_SCK_MHZ(16))) {
Serial.println("SDカードの初期化に失敗しました");
return;
}
Serial.println("SDカードが初期化されました");
// TFT液晶の初期設定
tft.init(TFT_HEIGHT, TFT_WIDTH); // TFT初期化(画面サイズ指定)
tft.setRotation(3); // 画面回転(0-3)
canvas.setTextSize(1); // 文字サイズ(1)
// グラフエリア枠表示
canvas.setFont(&FreeSans9pt7b); // フォント指定
canvas.setTextColor(ST77XX_ORANGE); // 文字色
canvas.setCursor(10, 40); // カーソル位置
canvas.printf("%.0f", tempMax); // Y軸温度最大値表示
canvas.setCursor(10, 117); // カーソル位置
canvas.printf("%.0f", tempMin); // Y軸温度最小値表示
canvas.setTextColor(ST77XX_CYAN); // 文字色
canvas.setCursor(10, 57); // カーソル位置
canvas.printf("%.0f", humidMax); // Y軸湿度最大値表示
canvas.setCursor(10, 134); // カーソル位置
canvas.printf("%.0f", humidMin); // Y軸湿度最小値表示
canvas.drawRect(34, 29, 202, 102, ST77XX_WHITE); // グラフエリア外枠
}
// メイン -----------------------------------------
void loop() {
unsigned long nowTime = millis(); // 経過時間を現在時間として取得(ms)
// 測定データ取得インターバル処理(条件成立時に連続記録)
if (nowTime - beforTime >= DISP_INTERVAL) { // 前回時間から現在時間がインターバル時間経過していたら
beforTime = nowTime; // 前回時間を現在時間で更新
// データ表示部初期化
canvas.fillRect(0, 0, 240, 21, ST77XX_BLACK); // データ表示エリア黒塗りつぶし
canvas.drawCircle(9, 12, 7, ST77XX_WHITE); // RECの白丸
// REC部表示
canvas.setFont(&FreeSans9pt7b); // フォント指定
canvas.setTextColor(ST77XX_RED); // 文字色
canvas.setCursor(22, 17); // カーソル位置
canvas.print("REC"); // 画面表示実行
canvas.drawFastVLine(63, 0, 25, ST77XX_WHITE); // 線(指定座標から垂線)
// 連続記録処理
if (digitalRead(3) == LOW && shouldRecordData()) { // 連続記録ボタンONで連続記録のトリガーに達していれば
dispIntervalCount = 0; // 表示時間間隔カウント0リセット
Serial.println("CSVデータ連続書き込み中");
writeCsv(); // SDカードに書き込み実行関数呼び出し
canvas.fillCircle(9, 12, 6, ST77XX_RED); // 赤の塗りつぶし円
Serial.println(millis()); // 連続記録時間間隔確認用
}
// スイッチ処理(単発記録、連続記録初回)
if ((digitalRead(2) == LOW || digitalRead(3) == LOW) && swState == false) { // どちらかのスイッチがONでスイッチ状態がfalseなら
beforTime = nowTime; // 前回時間を現在時間で更新
dispIntervalCount = 0; // 表示時間間隔カウント0リセット
canvas.fillCircle(9, 12, 6, ST77XX_RED); // 赤の塗りつぶし円
getEnv(); // 温湿度取得関数呼び出し
writeCsv(); // SDカードに書き込み実行関数呼び出し
swState = true; // スイッチ状態をtrueへ
}
// スイッチ操作許可
if (digitalRead(2) == HIGH && digitalRead(3) == HIGH) { // スイッチが両方OFFなら
swState = false; // スイッチ状態をfalseへ
}
getEnv(); // 温湿度取得関数呼び出し
// データを配列に保存
if (xAxisIndex < MAX_DATA_POINTS) { // x軸最大記録数がx軸インデックスより小さければ
temperatureData[xAxisIndex] = temperature; // 温度データを配列に取得
humidityData[xAxisIndex] = humidity; // 湿度データを配列に取得
xAxisIndex++; // X軸インデックスを+1
} else { // 配列が満杯の場合、配列データ全体をシフトする(グラフを左へスクロール)
for (int i = 0; i < MAX_DATA_POINTS - 1; i++) { // 各データ配列の最終要素の手前まで繰り返し
temperatureData[i] = temperatureData[i + 1];
humidityData[i] = humidityData[i + 1];
}
// 最後のデータは新しいデータで更新
temperatureData[MAX_DATA_POINTS - 1] = temperature; // 温度データを最終要素に取得
humidityData[MAX_DATA_POINTS - 1] = humidity; // 湿度データを最終要素に取得
}
drawGraph(); // グラフ描画関数呼び出し
dispIntervalCount++; // データ表示時間間隔カウント+1
// 測定データを画面に表示表示
canvas.setFont(&FreeSans12pt7b); // フォント指定
canvas.setTextColor(ST77XX_ORANGE); // 文字色
canvas.setCursor(75, 20); // カーソル位置
canvas.printf("%.1fC'", temperature); // 温度表示
canvas.setTextColor(ST77XX_WHITE); // 文字色
canvas.print(" / "); // 区切り表示
canvas.setTextColor(ST77XX_CYAN); // 文字色
canvas.printf("%.1f%%", humidity); // 湿度表示
canvas.drawFastHLine(0, 25, 240, ST77XX_WHITE); // 線(指定座標から平行線)
// スプライト(メモリ内に描画した画面)をTFTに描画
tft.drawRGBBitmap(0, 0, canvas.getBuffer(), TFT_WIDTH, TFT_HEIGHT);
}
}
7.まとめ
「Raspberry Pi Pico」互換機の「RP2040-GEEK」を使用したデータロガーの作成方法を詳しく紹介しました。
「RP2040-GEEK」は「ラズパイPico」と同じように使用できて、TFTカラー液晶表示器とmicroSDカードリーダーを搭載しているため、これらを組み合わせたデバイスの製作には、個別に各部品を準備するよりもコンパクトで安価に製作することができます。
初心者の方でも作れるように以下の各機能の使用方法をそれぞれ詳しく紹介しました。
- RP2040-GEEKの基本機能や特長
- I2C温海度センサーの接続と利用方法
- 液晶表示のチラツキ防止技術「スプライト」
- グラフ表示の実装方法
- microSDカードによるデータの記録
まずはサンプルプログラムの「コピペ」で動作確認しながら、それぞれの機能の使い方を少しづつ理解して、オリジナルのデータロガーの作成に挑戦してみてください。
データロガーを使用することで測定データを可視化しながらデータの記録ができます。
自作の装置開発時のデータ収集や、安価で省電力、小型なデバイスのメリットを活かして、遠方で稼働している装置に忍ばせて動作状況を記録して確認する等、今までできなかった「ものづくり」が可能になると思います。
今回データロガーに最適なデバイスとして「RP2040-GEEK」を使用しましたが、多くのデータを表示するには少し液晶サイズが小さいと感じたので、本家の「ラズパイPico」でもう少し大きな液晶表示やタッチパネル操作についても今後紹介できればと思います。
コメント