以下の内容はhttps://negligible.hatenablog.com/entry/2025/09/22/215038より取得しました。


気圧計を作る ~ M5Stack気圧ロガー編 ~

はじめに

気圧計と私

気圧計が欲しい! ──小中学生の頃,私はそう思っていました。気圧は当たり前ながら天気に深く関わり,台風の際などに大きく変動する重要なパラメータであるにもかかわらず,それを測る気圧計は,温度計や湿度計のようにその辺のお店で簡単に手に入るものではなかったからです。どこかのホームセンターでやっと見かけた(アナログ)気圧計には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に気圧センサを接続し,データをmicroSDに記録するもの
  • PICマイコンに気圧センサを接続したシステムで,ボタン電池駆動できるもの(microSDへの記録は優先度低)

本記事では前者,M5Stack版(M5Stack気圧ロガー)について記述します。

目次

完成イメージ

ハードウェア構成

まず,図1に完成したM5Stack気圧ロガーを示します。

図1: M5Stack GrayとLPS25Hによる気圧ロガー
M5Stackの上側の端子にはI2CのSDA, SCLとなるGPIO21, 22と電源(5 V, 3.3 V, GND)がピンソケットとして出ています。ここに部品箱に転がっていた気圧センサLPS25H*1を接続することにしました。秋月電子通商の十字配線ユニバーサル基板(Dタイプ)でサッと*2回路を作ります。と言っても,M5Stackの3V3, GND, SDA, SCLをLPS25Hモジュールの相当するピンに繋げ,また,LPS25Hモジュールの4ピン,5ピンを3V3に接続するだけです(こうすることでLPS25HはI2Cモードとなり,かつスレーブアドレスが0x5dとなります)。使用しない5 Vはどこにもつながらないようにします*3。図3に十字配線ユニバーサル基板のカットパターンを示します。
図2: 十字配線ユニバーサル基板のカットパターン
十字配線ユニバーサル基板にはこういった簡単な基板を作る心理的ハードルを下げる効果がありますね。 ちなみに筆者のM5Stack Grayにはバッテリーモジュールを取り付けてありますので,試してみたところ半日以上は連続して記録できました。

使い方

電源を入れると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は,都内某オフィスビルのエレベータに乗って,とある中層階から地上まで降りた際の気圧変化です。このときには途中の階で止まらなかったため,一定の傾きで綺麗に気圧が上昇したことが分かりますね。

図3: 都内某オフィスビルのエレベータで中層階から地上に降りた際の気圧変化
地上ととある中層階では概ね9 hPaの気圧差がありました。

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

図4: つくばエクスプレス秋葉原駅の地上入口からプラットフォームまでの気圧変化
エスカレータに乗っている間は直線的に気圧が上がっていくこと,エスカレータに5回乗ったであろうことが読み取れます。また,4番目のエスカレータが長いこともわかりますね。地上とプラットフォームでは概ね3.5 hPaほど異なるようでした。

こんな感じで楽しい測定データを採り放題ですね。持ち運べない,1分毎の記録,では見えなかった世界です*4

ソフトウェア構成

ソフトウェアのつくりはごく簡単ですが,私個人としてはM5Stackを含むESP32マイコンArduino対応マイコンmicroSDカードへのデータ記録が初めてでしたので,少し詰まったところがありました。また,液晶画面への表示とLPS25Hからのデータ読み取りがうまく両立できず,結局対症療法したところがあります。

プログラム*5全文についてはGitHubに置きましたのでそちらをご確認ください。1つの.inoファイルで完結しています。

github.com

ライブラリ類

M5Stackの標準的な開発環境としてArduino IDEを用いました。今回は初めてM5Stack.hではなくM5Unifed.hを使ってみます。また,温度・気圧センサLPS25HについてはSparkFunのライブラリ(SparkFun_LPS25HB_Arduino_Library.h)に頼ることとしました。M5Stack.hではなくM5Unified.hを使う場合,SDカードのためのSD.hを別途includeしなければならないようでした。また,I2C通信については,ArduinoWireクラスを使用するためにはやはり別途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)駆動のコンパクト気圧計について書きます。

*1:秋月電子通商によるDIP化モジュール。現行最新機種はLPS25HBのようですが,I2C通信やレジスタの仕様は変わっていないようです。

*2:CQ出版社っぽい!

*3:後で使えるように少し広めのパターンを確保しました。使わなそうですが…。

*4:大げさです…

*5:そういえば本当は「スケッチ」と呼ぶんですよね…あまり言わなくなってしまった気がしますが…。

*6:秋月電子通商による補足資料より




以上の内容はhttps://negligible.hatenablog.com/entry/2025/09/22/215038より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14