1.今回の所要機能
ハードウェアの完成を目指して次の機能を持たせることにします。
①テキストファイル出力機能
CSV形式での計測データ記録を想定して、終端が改行コード'0x0a'でターミネートされた文字列のファイル出力。
②テキストファイル入力機能
ファイル上の可変長文字列を、'0x0a'を終端としてバッファへ読み込む。
③アナログセンサーの接続
唯一のアナログ入力ピンに土壌湿度センサーを接続して計測する。
2.使用する部品・測定器
(1)部品の規格・仕様
今回のセンサーは以下のものを使用します。
なお、マイクロSDカードスロットDIP化キットは秋月電子通商で購入しました。
・マイクロSDカードスロットDIP化キット | 1個 | 300円 |
・土壌湿度センサー(コンパレータ:YL-38 + プローブ:YL-69) | 1個 | ※ |
・配線材料 | 少々 |
〔マイクロSDカードスロットの仕様〕
ヒロセ電機のmicroSDカードスロットをブレッドボードやユニバーサル基板に取り付けられるようにするセットです。
付属のヘッダーピンを半田付けして使用します。
〔YL-38, YL-69の仕様〕
写真はAmazonの商品紹介ページのものです。HiLetgoからの海外直送で2017/02時点の価格は225円(送料別)とあります。
・電源電圧: 3.3~5V
・アナログ電圧出力: 0~4.2V(電源5V時)
・消費電流: 35mA(電源5V時)
・出力: アナログとデジタル
・デジタル出力: ポテンションメーターで設定された閾値
または基になる閾値を超えると1、そうでなければ0。
・アナログ出力値: 0~1023
(2)測定器
後で述べますが、ESP-WROOM-02のTOUTピンの最大電圧は1.0Vにする必要があります。これを実現するために可変抵抗器を使った分圧回路を作成します。可変抵抗器を調整して1.0Vに設定するために、電圧の計測ができるテスターが必要になります。
3.組み立てと配線
(1)microSDカード装置の配線
下図左はmicroSDカード装置の端子を表しています。この記号はとてもわかりにくいですね。ESP8266とはSPI通信でデータ授受をするので、SDカ ードの「SDモード」と「SPIモード」のインターフェイスを比べると、対応したピンの意味が明らかになります。右上の対比表を参考にしてください。
最終的に使用する端子と接続先は右下に示すとおりです。
MCUボードの右側空きスペースに、図のようにSDカード装置を取り付けます。SDカード装置からの配線は、VSSをブレッドボードのマイナスラインにジャンパーワイヤーでつなぐほかは、すべてジャンパーコードで接続します。
(2)計測ボードの配線
土壌湿度センサーYL-38の端子と接続先は下図のとおりです。
写真・左が土壌湿度センサーです。プローブYL-69は2線でYL-38と結ばれています。YL-38の出力側端子は、計測センサーをとりまとめている計測ボードに接続します。VCCとGNDはそれぞれ5VラインとGNDラインに接続しますが、AOもいったん計測ボード(ここでは23列目)に接続しています。そして写真・右のように、同じ列からジャンパーコードでMCUボードのTOUTピンに接続します。
(3)ボード間の配線
ESP-WROOM-02のTOUTピンは唯一のアナログポートですが、以前にも述べたように、0~1Vの電圧を10ビットの分解能でA/D変換します。したがって、電圧レベルが1.0Vに収まるように調整する必要があります。
写真・左のように10KΩと20KΩで分圧して、その結合点をTOUTへ接続しています。また20KΩには10KΩの可変抵抗器を直列に接続して、ここに計測ボードのYL-38のAOからのジャンパーコードを接続しています。TOUT接続の前に、結合点の最大電圧が1.0Vになるよう、テスターで計測しながら可変抵抗器を調整します。
こうして、写真・右のように結線されてハードウェアが完成です。
写真とは異なりますが、回路図ではタクトスイッチSW2と並列に抵抗器内蔵のLEDを配線しています。通電していると点灯し、スケッチ書き込みのためにSW2を押している間だけ消灯します。
〔回路図〕
(4)ESP-WROOM-02のピンの使用状況
これは回路図で明らかなのですが、最終的な接続状態をまとめておきます。
3.3V | 専用電源[3.3V]から供給 |
EN | Enable [High] |
IO0 | 動作モード設定 [Low/High] Flashタクトスイッチへ |
IO2 | 動作モード設定 [High] |
IO4 | I2C default pin (SDA) |
IO5 | I2C default pin (SCL) |
IO12 | SDカード(Data Output) |
IO13 | SDカード(Data Input) |
IO14 | SDカード(Serial Clock) |
IO15 | 動作モード設定 [Low] |
IO16 | SDカード(Chip Select) |
TOUT | Analog入力 1.0V分圧回路より |
RST | Resetタクトスイッチへ |
TXD | Serial TXD : シリアルUSB変換モジュールのRXDへ |
RXD | Serial RXD : シリアルUSB変換モジュールのTXDへ |
4.ソフト開発のポイント
(1)SDカード入出力方法の検討
計測データをSDカードに記録する方法はいろいろありますが、ここでは記録内容をエディターなどで簡単に確認できるテキストファイルにします。見やすさを考えると、各フィールドの編集は固定長にして、フィールド間をカンマで区切るCSV形式が良さそうです。そうすると、取り扱いが簡単で効率のよい固定長レコードの入出力ができることになります。しかしレコード長を固定してしまうと、将来のセンサー増設の都度レコード長を修正しなくてはならず面倒です。
もっとも単純なのは、レコード終端が改行文字'0x0a'('\n')でターミネートされるごく一般的なテキスト出力を行い、入力時には1文字ずつ読みながら'0x0a'が現れるとレコード終端と判定する方法でしょう。
ただ、SDカードの入出力はさほど速くないので、事前にちょっとしたテストを行ってみました。終端記号を含む50byteのレコードを10,000件記録したファイルを作成して、「①単純な50byteの固定長入力、②'0x0a'の出現まで1文字入力でレコードを構成、③RAM上に設定した1,000byteのバッファに一括入力しながら②の方法でバッファからレコードを切り出す方法」を試しました。それぞれ5回実行させて、その平均値を求めると次のようになりました。
①の方法: 1.941秒
②の方法: 3.617秒
③の方法: 2.020秒
一度に1万件ものデータを扱うケースは考えにくく、その場合でも4秒未満なら実用上問題ないだろうと判断し、今回は②の方法で決定しました。
このテスト結果を紹介したのは、場合によってRAM上にバッファを設ける方法が威力を発揮することが分かったからです。テスト③では、入力用メソッドの内部に static char buf[1000]; を設定して、1,000バイトずつ読み込んでいます。バッファからの取り出しは②と同じ方法ですが、フラッシュメモリー上のファイルに対する実質的な読み込みが500回に限定され、実質的な処理時間は②より44%も短縮されました。コンパイル結果を確認するとRAM消費量がその分だけ増えていますが、最速の①と遜色ない結果を得られることが明らかになりました。
(2)SDカード装置の操作
1~2行: SDカード装置を使用するために必要なSDライブラリーと、SDとの通信プロトコル制御用SPI
ライブラリーのヘッダーファイルをインクルードします。
5行目: SDカード装置のChip Select端子を接続したIO16ピンを指定します。
6行目: SDカードに作成するファイル名です。
8行目: SDが正常に稼働できる状態であるかを判定するものです。
11~12行: ファイル書き込み用のデータを定義しています。
18行目: 初期化処理でSDカード装置とSDライブラリを初期化します。処理が完了すると bSD_Enabledを
trueに設定します。
28~30行: SDカードに同名のファイルがすでに存在しているか調べて、存在すれば削除します。
37行目: 書き込みモードでファイルを開きます。オープンできるとファイルオブジェクトdfが作成されます。
40行目: ここでファイルにテストデータを書き込んでいます。
49行目: 反復処理では、SDカード装置が稼働可能状態の初回だけ、以下を実行します。
51行目: 先に書き込んだファイルを読み込みモードで開きます。オープンできるとファイルオブジェクトdfが
作成されます。
59行目: 後述のreadlnメソッドで、buf領域に30byte以内のテキストデータを読み込みます。
readlnメソッドから読み込み文字数にゼロ以下が通知されるまで繰り返します。
67行目: ここにreadlnメソッドを書いています。外部でオープンされたファイルオブジェクトを使用して、指定
されたバッファ領域に指定サイズ以下のテキストを読み込みます。
72行目: ファイルオブジェクトが利用可能な間、つまり読むべきデータがなくなるまで後続処理を繰り返します。
73~74行: ファイルから1文字(byte)読み込んでバッファに追加します。
75~78行: レコード終端の改行記号'0x0a'が見つかると、バッファの終端を'0x00'でターミネートして、入力
文字数を通知します。これでファイルからテキストデータを1行を読み込めました。
#include <SPI.h>
#include <SD.h>
// SD card drive & File name
#define SDCARD_DRIVE 16 // SD card chip select number
#define DATA_FILE "datafile.txt"
bool bSD_Enabled;
bool bAtFirst = true;
char* sData[] = {"1:Measured data-1", "2:Measured data-2", "3:Measured data-3", "4:Measured data-4",
"5:Measured data-5", "6:Measured data-6", "7:Measured data-7", "8:Measured data-8"};
void setup() {
Serial.begin(115200);
// Prepare SD card unit.
if (SD.begin(SDCARD_DRIVE))
bSD_Enabled = true;
else {
bSD_Enabled = false;
Serial.println("SD Drive does'nt work!");
return;
}
Serial.println("\nSD enabled!");
// Create file.
if (SD.exists(DATA_FILE)) {
Serial.print(" File exists: "); Serial.println(DATA_FILE);
if (SD.remove(DATA_FILE))
Serial.println(" File removed!");
else
Serial.println(" Couldn't remove file!");
}
// Write data file.
File df = SD.open(DATA_FILE, FILE_WRITE);
if (df) {
for (int i=0; i<8; i++)
df.println(sData[i]);
df.close();
}
else
Serial.println("Failed to create data file");
}
void loop() {
if (bSD_Enabled && bAtFirst) {
bAtFirst = false;
File df = SD.open(DATA_FILE, FILE_READ);
if (!df) {
Serial.println("Couldn't open data file!");
return;
}
Serial.println("Read file opened!");
char buf[31];
while (readln(&df, buf, 30) > 0) {
Serial.print(buf);
}
df.close();
Serial.println("File cloed!");
}
}
int readln(File* df, char* buf, int len)
{
*buf = '\0';
int pos = 0;
while (df->available()) {
char ch = df->read();
*(buf + pos) = ch;
if (ch == 0x0a) {
*(buf + pos + 1) = '\0';
return pos;
}
pos++;
if (pos >= len) {
Serial.println("Overflowed!!");
*buf = '\0';
return 0;
}
}
}
(3)土壌湿度センサー計測メソッド
1行目: アナログ入力ピン TOUTはこのように指定します。
8行目: アナログピンを指定して土壌湿度計測メソッドを呼んで、変数valに通知された計測値を取得します。
13行目: これが土壌湿度計測メソッドです。
15行目: アナログポートへの入力0~1.0Vはデジタル値0~1023として取り込まれます。これを1023から減算
して完全な乾燥状態を0、プローブ全体が水に浸された状態を1023とするよう変換しています。
16行目: 本来ならデジタル値0~1023が入力できるはずなのですが、シリアルモニターで観測していると56~
545の範囲に分布しています。とりあえず、それを0~100%に変換して通知しています。
#define ANALOG_PIN A0 // TOUT pin
void setup() {
Serial.begin(115200);
}
void loop() {
int val = measureSoilMoisture(ANALOG_PIN);
Serial.println(val);
delay(1000);
}
int measureSoilMoisture(int pin_no)
{
int val = 1023 - analogRead(pin_no);
return (int)map(val, 56, 545, 0, 100);
}
(4)ファイルのタイムスタンプの問題
スケッチが完成してテストをしている途中で、ファイルのタイムスタンプが「2000/01/01 1:00」に固定されていることがわかりました。これはいささか気持ちが悪くて何とかしようと調べた結果、SDライブラリーに換えてSdFatライブラリーを利用すれば何とかなりそうだとわかり、次の手順でインストールしました。
ここをクリックして「GitHub - greiman/SdFat: Arduino FAT16/FAT32 Library」表示される下図の画面から[Download ZIP]を指定します。
SdFat-master.zipがダウンロードされるので、解凍してできたSdFat-masterフォルダーを開き、SdFatフォルダーをまるごとArduino IDEのlibrariesフォルダーへコピーします。
librariesフォルダーの場所は、Arduino IDE画面の[ファイル]-[環境設定]を選択して表示される「環境設定」画面の下の方にある「以下のファイルを直接編集すれば~」の次の行をクリックして調べることができます。表示されるpreferences.txtに記述されている「\libraries」を探すとわかります。
一般には、次図のように深い位置に存在します。
Arduino IDEを再起動するとコピーしたSdFatが認識されます。[スケッチ]-[ライブラリをインクルード]で表示されるライブラリ一覧の中にSdFatが追加されているのがわかります。
続いてスケッチを修正します。
2行目: インクルード文をSDからSdFatに変更します。
3行目: 今までのSDの代わりにSdFatが動作するようSdFatのインスタンスSDを作成します。
13行目: SdFileクラスのdateTimeCallbackというメソッドに、コールバック関数dateTimeを指示します。
これで、以降のファイルクローズ時にdateTime関数が呼び出されてタイムスタンプが更新されます。
タイムスタンプにはシステムクロックの日時を使うので、I2C開始処理Wire.begin()の後に指定します。
20行目: このcloseでコールバック関数dateTimeが呼ばれます。
26行目: これがコールバック関数です。
31行目: システムクロックから日時情報を取得します。
38~39行: FAT_DATE, FAT_TIMEはそれぞれ、年月日と時刻を所定の内部形式に変換するための関数です。
#include <SPI.h>
#include <SdFat.h>
SdFat SD;
void setup() {
:
:
// Prepare I2C protocol.
Wire.begin();
:
:
// Set dateTime callback function to provide timestamp.
SdFile::dateTimeCallback(dateTime);
// Write data file.
File df = SD.open(DATA_FILE, FILE_WRITE);
if (df) {
for (int i=0; i<8; i++)
df.println(sData[i]);
df.close();
}
else
Serial.println("Failed to create data file");
}
void dateTime(uint16_t* date, uint16_t* time)
{
uint16_t year;
uint8_t month, day, hour, minute, second;
getDateTime(&dtClock);
year = 2000 + ((int)(dtClock.year >> 4)) * 10 + (int)(dtClock.year & 0x0f);
month = ((int)(dtClock.month >> 4)) * 10 + (int)(dtClock.month & 0x0f);
day = ((int)(dtClock.day >> 4)) * 10 + (int)(dtClock.day & 0x0f);
hour = ((int)(dtClock.hour >> 4)) * 10 + (int)(dtClock.hour & 0x0f);
minute = ((int)(dtClock.minute >> 4)) * 10 + (int)(dtClock.minute & 0x0f);
second = ((int)(dtClock.sec >> 4)) * 10 + (int)(dtClock.sec & 0x0f);
*date = FAT_DATE(year, month, day);
*time = FAT_TIME(hour, minute, second);
}
5.スケッチの実行
(1)実行結果と評価
下図のように、シリアルモニターにはすでにSDカードに存在する「datafile.txt」を削除した旨が表示され、テストデータをファイルに書き込みます。それを開いて読み込んで表示して、最後にファイルを閉じています。これらの処理は、起動直後に一度だけ実行されます。
続いて表示されているゼロは、机上に放り出されたプローブの反応です。乾ききった室内のテーブルでは、湿度ゼロということです。注意しなければならないのは、ここで得られる値は少々、いやかなりアバウトであるということです。この計測値で水分補給などの装置を制御する場合は、シリアルモニターの表示を眺めながら、実際の装置で何度も試してからのほうがいいでしょう。
SDカードについては、これでレコード長に左右されずにデータを弾力的に扱えるようになりました。
(2)スケッチ全景