はじめに
気圧計と私
気圧計が欲しい! ──小中学生の頃,私はそう思っていました。気圧は当たり前ながら天気に深く関わり,台風の際などに大きく変動する重要なパラメータであるにもかかわらず,それを測る気圧計は,温度計や湿度計のようにその辺のお店で簡単に手に入るものではなかったからです。どこかのホームセンターでやっと見かけた(アナログ)気圧計には1万円近いお値段が付けられており,当時の私のお財布事情ではおいそれと手を出すことはできませんでした。
SCP1000とMARY基板
それから何年もの後…,2011年頃でしたでしょうか,秋月電子通商で気圧センサSCP1000をDIP化したモジュールが売り出されました。これが1,000円 + αほどのお値段(現在は700円のようです)でしたので,すぐに購入し,(何年か放置した後にやっと)ついに気圧計を作ることにしました。このSCP1000を当時のトラ技増刊の付録となっていたMARY基板に繋げ,さらにこれをRaspberry Piとシリアル通信することで,台風の気圧推移などを記録できました。mbedのC++言語でSPI通信する方法など,さまざまに苦労した記憶がよみがえります。
携帯できる気圧計を
それからまた何年かが経ち,M5Stackと出会った筆者は,その中身であるESP32マイコンを触るようになり(Arduino IDEです),ESP32-DevKit-Cなどの開発ボードと温湿度気圧センサBME280やBME680を繋げて,WiFi経由でGoogle スプレッドシートに記録するなどして遊んでおりました。
しかしそれらはみなUSBケーブルによる給電が必要でした。簡単に持ち運べる気圧計,できればオフラインで何かに記録できるものを作りたいと思い,次の2つを作ることとしました。携帯型気圧計です。これであれば,例えば高いビルに上った際や,電車が地下に潜った際などの気圧変化を観測できますね。
本記事では前者,M5Stack版(M5Stack気圧ロガー)について記述します。
目次
完成イメージ
ハードウェア構成
まず,図1に完成したM5Stack気圧ロガーを示します。


使い方
電源を入れるとLPS25Hから読み取った温度と気圧を表示します。測定・画面の更新は0.5秒に1回としました。なお,電源オンの前にmicroSDカードを挿入しておいてください。LPS25Hが未接続,またはmicroSDカードが未挿入の場合には画面にメッセージを表示します(一応,このメッセージの後でもそれぞれを接続・挿入すれば正常起動するはずです)。後述しますが,測定周期の0.5秒はdelay関数で作っているのではなく,測定を担うFreeRTOSのタスクが0.5秒毎の割り込みで動作するようにして実現しました。
Aボタンを押すと,温度と気圧をmicroSDに記録します。記録に当たっては,Rnnnn.CSV(nnnnには0000 ~ 9999までの数が入る)というファイル名のうち,microSDカードに存在していない最も若い番号のファイル名を見つけ出し,そのファイルを作成して0.5秒毎に記録していきます。Bボタンを押すと記録を停止します。
測定例
持ち運べる気圧計,かつ1分毎などではなく0.5秒毎の記録が可能とあれば,台風などではなく,高度変化に伴う気圧変化を観測するのが面白そうです。図3は,都内某オフィスビルのエレベータに乗って,とある中層階から地上まで降りた際の気圧変化です。このときには途中の階で止まらなかったため,一定の傾きで綺麗に気圧が上昇したことが分かりますね。

図4は,非常に地下深いことで悪名高いつくばエクスプレス秋葉原駅の地上入口からプラットフォームまで,複数のエスカレータで下って行った場合の気圧変化です。

こんな感じで楽しい測定データを採り放題ですね。持ち運べない,1分毎の記録,では見えなかった世界です*4。
ソフトウェア構成
ソフトウェアのつくりはごく簡単ですが,私個人としてはM5Stackを含むESP32マイコンやArduino対応マイコンでmicroSDカードへのデータ記録が初めてでしたので,少し詰まったところがありました。また,液晶画面への表示とLPS25Hからのデータ読み取りがうまく両立できず,結局対症療法したところがあります。
プログラム*5全文についてはGitHubに置きましたのでそちらをご確認ください。1つの.inoファイルで完結しています。
ライブラリ類
M5Stackの標準的な開発環境としてArduino IDEを用いました。今回は初めてM5Stack.hではなくM5Unifed.hを使ってみます。また,温度・気圧センサLPS25HについてはSparkFunのライブラリ(SparkFun_LPS25HB_Arduino_Library.h)に頼ることとしました。M5Stack.hではなくM5Unified.hを使う場合,SDカードのためのSD.hを別途includeしなければならないようでした。また,I2C通信については,ArduinoのWireクラスを使用するためにはやはり別途Wire.hをincludeする必要があるようです。この辺りは筆者はドシロートなので,誤りがあればご指摘ください。
#include <Wire.h> #include <SD.h> #include <M5Unified.h> #include <SparkFun_LPS25HB_Arduino_Library.h> // Define instant for LPS25HB sensor LPS25HB sensor;
LPS25Hセンサはsensorというインスタンスを通じて操作できるように宣言しています。
FreeRTOSのタスク
あれこれ試行錯誤している頃には,loop関数の中で温度・気圧の読み取り,液晶画面への表示を処理しておりました。loop関数の最後にdelay(500);を置いてあげれば,適当な間隔で測定・表示を繰り返すことができます。しかし,いざ温度や気圧の測定結果をmicroSDカードに記録しようとした場合,何秒間隔で測定したデータか分からなくなってしまいます(delay関数に与えたミリ秒単位での時間に加えて,測定,データ転送,表示の時間を要してしまうため)。そこで,ある程度目星がついたところで,主な処理をloop関数から引き揚げ,FreeRTOSのタスクとして一定時間毎に動かすこととしました。
このため,いくつかのグローバル変数を定義しました。microSDカードに記録する際のファイルへのハンドルを格納するfile,ファイル名を格納するfileName,現在microSDカードに記録することになっているかを示すisRecording,記録開始からの時間を表すt_recです。
File file; char fileName[16]; bool isRecording = false; float t_rec = 0.0;
FreeRTOSのタスクとしては,タイマ割り込みで呼ばれるonTimer関数を作り,onTimer関数から主な処理を記述したmeasure関数に通知を送るようにしました。これは以前に制作したESP32による商用電源品質監視装置と同じ仕組みです。今回は0.5秒毎としたので,以前に比べれば1000倍以上低速です😅
onTimer関数はFreeRTOSのタイマ割り込みで呼ばれる関数です。この中に多くの処理をそのまま書いてしまうとうまくいかないようで,ここからxTaskNotifyFromISR関数でタスクとして設定したmeasure関数に通知を送ります。一方,measure関数の中にwhile (true)で作った無限ループがあり,その先頭で xTaskNotifyWait関数を使って通知が届くのを待ちます。無限ループの最後まで処理が進むとまたここに戻り,次の通知が来るまで待ちます。
/ ---------------------------------------- // The ISR // ---------------------------------------- void IRAM_ATTR onTimer() { BaseType_t taskWoken; // Wake up the task xTaskNotifyFromISR(taskHandle_measure, 0, eIncrement, &taskWoken); } // ---------------------------------------- // The measure function // ---------------------------------------- void measure(void *pvParameters) { // Define a variable to store notified value uint32_t ulNotifiedValue; while (true) { // Wait to be woken up xTaskNotifyWait(0, 0, &ulNotifiedValue, portMAX_DELAY); // Acquire LCD control M5.Display.startWrite(); // Show measurement mark on display, become red if recording is ongoing if (isRecording) { M5.Display.fillCircle(8, 8, 5, TFT_RED); } else { M5.Display.fillCircle(8, 8, 5, COLOR_7SEG_ON); } // Obtain sensor data float t, p; if (sensor.isConnected() == false) { Serial.println("LPS25H not found."); Serial.println(""); while (1) { // Infinate loop } } else { t = sensor.getTemperature_degC(); p = sensor.getPressure_hPa(); } // Send data to serial communication Serial.printf("%5.2f degC, %7.2f hPa", t, p); if (isRecording) { Serial.printf(", %6.1f sec", t_rec); } Serial.printf("\n"); // Define buffer to store values char buf[16]; int widthText, heightText; (中略) // Record to microSD card if flag is raised if (isRecording) { // Release LCD control to open file M5.Display.endWrite(); // Write to microSD file = SD.open(fileName, FILE_APPEND); if (t_rec == 0.0) { file.printf("Time [s], Temperature [degC], Pressure [hPa]\n"); } file.printf("%6.1f, %6.3f, %8.3f\n", t_rec, t, p); file.close(); // Acquire LCD control again after closing file M5.Display.startWrite(); // Show message on LCD M5.Display.setFont(&fonts::Font2); M5.Display.setTextColor(COLOR_7SEG_ON, COLOR_7SEG_OFF); M5.Display.setCursor(20, 201); M5.Display.printf("Recording to "); M5.Display.printf(fileName); M5.Display.printf(", %6.1f sec...", t_rec); // Increment recording time t_rec += 0.5; } else { // Delete message on LCD M5.Display.setFont(&fonts::Font2); M5.Display.setTextColor(COLOR_7SEG_ON, COLOR_7SEG_OFF); char buf[] = "Recording to /R0000.CSV, 00000.0 sec..."; widthText = M5.Display.textWidth(buf); heightText = M5.Display.fontHeight(); M5.Display.fillRect(20, 201, widthText, heightText, COLOR_7SEG_OFF); } // Clear measurement mark on display M5.Display.fillCircle(8, 8, 5, COLOR_7SEG_OFF); // Release LCD control M5.Display.endWrite(); } }
ここで,35行目にあるように,毎度毎度,LPS25Hが接続されているかsensor.isConnectedメソッドを呼ぶようにしました。こうしないと,液晶画面の描画とLPS25Hが両立できませんでした。原因はいまだ不明です…。液晶画面はSPI,LPS25HはI2Cで接続しており,互いに異なるGPIOに繋がっているため問題ないと思うのですが…。
さて,61行目からはbool型のisRecordingを見て,これがtrueであれば温度と気圧をmicdoSDカードに記録します。液晶画面とmicroSDカードはともにSPIバスに繋がっているため,画面描画を高速化するM5.Display.startWrite()を呼んだ後だと記録できません。そこで,ここでM5.Display.endWrite()を呼んでSPIバスを開放します。そのあと,ファイルを開いて,最初(時刻が零秒)であればヘッダを書き込み,その後は温度と気圧をカンマ区切りCSVとして1行ずつ書いていきます。ファイルを閉じたら再びM5.Display.startWrite()を呼び,記録中のファイル名と,記録開始からの時間を表示するようにしました。
最後に,M5.Display.endWrite()を再び呼んで,measure関数のタスクは次の通知を待ちます。
後述しますが,microSDカードに記録するか否か,どのファイルに記録するのかについてはloop関数内で決めています。
setup関数
setup関数では様々な初期化の後,タスクの作成とタイマ割り込みの設定をしています。また,液晶画面の表示のうち,毎度更新する必要がない部分(「Temperature」や「hPa」など)をここで描画しています。ちなみに「°C」の「°」はフォントにないため,M5.Display.drawCircleメソッドで描きました(82行目)。
M5Unified.hを使用する上で重要なポイントは,もし,ArduinoのI2CライブラリであるWireを使用するのであれば,9行目のようにM5.In_I2C.release()を呼んで,I2Cバスを開放しなければならない点です。最初はこれが分からず,しばらく悩みました。
110行目以降,measure関数をタスクとして設定し,タイマ割り込みにonTimer関数を設定しています。123行目のtimer = timerBegin(1000000)の「1000000」は「1000000 Hz」すなわち1 MHzを表し,2行下のtimerAlarm(timer, 500000, true, 0)の「500000」は「500000周期」すなわち0.5秒を意味します。この辺りの書きっぷりはこの数年で変わったようですが,筆者も詳細は理解していません。
なお,気圧センサLPS25Hには初期状態で±5 ~ ±10 hPa程度の誤差があることがあるようですので*6,家にある他の気圧センサと概ね合うように,65,66行目でオフセットを設定しています。
// ---------------------------------------- // The setup function // ---------------------------------------- void setup() { // Initialize M5Stack auto cfg = M5.config(); M5.begin(cfg); M5.Power.begin(); M5.In_I2C.release(); // This is very important to allow Wire to take control of I2C over M5Unified!! M5.Display.setBrightness(128); // Set up serial communication Serial.begin(115200); Serial.println("------------------------------------------------------------"); Serial.println("M5Stack + LPS25H air pressure and temperature meter"); Serial.println("------------------------------------------------------------"); // Initialize microSD card while (!SD.begin(GPIO_NUM_4, SPI, 15000000)) { // Show message to serial communication Serial.println("microSD card not found."); Serial.println(""); // Show message on LCD screen M5.Display.setFont(&fonts::Font4); M5.Display.setTextColor(COLOR_7SEG_ON, COLOR_7SEG_OFF); M5.Display.setCursor(10, 10); M5.Display.printf("microSD card not found."); // Wait for a while for microSD to be mounted delay(500); } // Set up I2C bus Wire.begin(); // Begin LPS25H sensor sensor.begin(Wire, LPS25HB_I2C_ADDR_DEF); // Confirm LPS25H connection while (true) { if (!sensor.isConnected()) { // Show message to serial communication Serial.println("LPS25H not found."); Serial.println(""); // Show message to LCD screen M5.Display.setFont(&fonts::Font4); M5.Display.setTextColor(COLOR_7SEG_ON, COLOR_7SEG_OFF); M5.Display.setCursor(10, 50); M5.Display.printf("LPS25H not found."); // Wait for a while for LPS25H to be connected delay(500); continue; } else { // Begin sensor again, just in case... sensor.begin(Wire, LPS25HB_I2C_ADDR_DEF); break; } } // Set the offset value to the RPDS register to subtract approximately 7 hPa from the raw value uint8_t offsetPressure[2] = {0xa0, 0x00}; sensor.write(LPS25HB_REG_RPDS_L, offsetPressure, 2); // Clear display first M5.Display.clearDisplay(); // Acquire LCD control M5.Display.startWrite(); // Show labels on LCD screen, temperature M5.Display.setTextColor(COLOR_LABEL, TFT_BLACK); M5.Display.setFont(&fonts::Orbitron_Light_24); M5.Display.setCursor(20, 14); M5.Display.printf("Temperature"); M5.Display.setTextColor(COLOR_UNIT, TFT_BLACK); M5.Display.setCursor(250, 74); M5.Display.printf("C"); M5.Display.drawCircle(247, 75, 3, COLOR_UNIT); // Show labels on LCD screen, pressure M5.Display.setTextColor(COLOR_LABEL, TFT_BLACK); M5.Display.setFont(&fonts::Orbitron_Light_24); M5.Display.setCursor(20, 106); M5.Display.printf("Pressure"); M5.Display.setTextColor(COLOR_UNIT, TFT_BLACK); M5.Display.setCursor(250, 166); M5.Display.printf("hPa"); // Show function name above Button A and B on LCD screen M5.Display.setFont(&fonts::Font2); M5.Display.setTextColor(COLOR_7SEG_ON, COLOR_7SEG_OFF); M5.Display.setCursor(46, 220); M5.Display.printf("Record"); M5.Display.setCursor(145, 220); M5.Display.printf("Stop"); // Show battery "BATT." on LCD screen M5.Display.setFont(&fonts::Font2); M5.Display.setTextColor(COLOR_7SEG_ON, TFT_BLACK); M5.Display.setCursor(261, 2); M5.Display.printf("BATT."); // Release LCD control M5.Display.endWrite(); // Create a task combined with a timer xTaskCreateUniversal( measure, // Function as a task "measure", // Name of task 8192, // Stack memory sie NULL, // Parameters 5, // Priority &taskHandle_measure, // Task handler PRO_CPU_NUM // Core to execute this task ); // Set timer interrption, Timer at 1 MHz (1 microsec), Interruption for 500000 cycles (0.5 sec) hw_timer_t *timer = NULL; timer = timerBegin(1000000); timerAttachInterrupt(timer, &onTimer); timerAlarm(timer, 500000, true, 0); }
loop関数
loop関数では,M5StackのAボタンやBボタンが押されたかどうかを判定し,microSDカードに記録するか否かを管理しているisRecordingの値を反転させています。試行錯誤の段階ではこの部分もmeasure関数に入っていたのですが,measure関数が動いている期間がごく僅かであるためか,ボタンの反応が今一つだったため,ここだけはloop関数で処理することにしました。最後のdelayも10 msのみですので,かなり頻繁にボタンの様子を確認していますが,もっといいやり方があったかもしれませんね(それこそ割り込みにするとか)。
1つ大事な機能としては,Aボタンが押されて記録が開始される際に,microSDカード内の「Rxxxx.CSV」というファイルを,R0000.CSV,R0001.CSV,R0002.CSV...と順にスキャンしていき,最初に存在しなかったファイルをこれから記録するファイル名として決定する点です(すでに存在しているファイルは過去の記録なので上書きしない)。
ここで若干詰まったのは,「どうせmicroSDカードのルートディレクトリに保存するだけでしょ」と思い,パス名の先頭に「/」(スラッシュ)を付けなかったことです。10行目ですね。「/」がないとエラーは出ないにも関わらず全く記録されず,microSDカードを変えてみたり,画面描画を一時的に止めてみたりと右往左往してしまいました。考えてみれば「カレントディレクトリ」という概念がないので,ルートディレクトリである「/」から始まる絶対パスで書かないとダメなんですね…。「/」を付けたところ,無事にデータを採ることができ,図3,4のようなグラフを描くことができました。
// ---------------------------------------- // The loop function // ---------------------------------------- void loop() { // Observe button status to raise or lower flag for recording M5.update(); if (M5.BtnA.wasClicked() && !isRecording) { isRecording = true; for (int i = 0; i < 1000; i++) { sprintf(fileName, "/R%04d.CSV", i); if(SD.exists(fileName)) { continue; } else { break; } } } else if (M5.BtnB.wasClicked()) { isRecording = false; t_rec = 0.0; } delay(10); }
まとめ
M5Stackによる気圧ロガーのハードウェア構成(と言うほどでもありませんが)について述べ,FreeRTOSによる定周期割り込みを使った温度・気圧の測定に関するソフトウェア構成について説明しました。また,microSDカードへのデータの記録の仕方についても簡単ですが記述しました(インターネット上に情報は多いと思います)。
今回はM5Stackの上側のピンソケットに直接載せてしまいましたが,余裕があればM5Stack Proto Moduleを購入して,LPS25H(またはLPS25HBを新たに導入)をそこに収納して見た目すっきりにしたいと思っています。
画面の見た目については今回,あまり記述しませんでしたが,fonts::Font7を使った7セグメントLED風の表示は薄いグレー色で「8888.88」を下に重ねて消灯セグメントを表現したり,バッテリ残量や充電中のアイコンを表示したりと,いろいろこだわったポイントでもあります。
次回はPICマイコンと温湿度気圧センサBME280を用いたボタン電池(CR2032)駆動のコンパクト気圧計について書きます。