STM32 + BME280で手のひら温湿度気圧計を作ってみた

10 min read読了の目安(約9600字

STM32G031J6M6 と BOSCH 社の温湿度気圧センサ BME280 を使って手のひらサイズの温湿度気圧計を製作しました。この記事はそのレポートになります。

特徴

  • BOSCH BME280搭載
    • 温度: -40.0 ~ 80.0 ℃ (精度 ±1.0 ℃)
    • 湿度: 0 ~ 100 % (精度 ±3 %)
    • 気圧: 300 ~ 1100 hPa (精度 ±1.0 hPa)
  • 上記3つの他に不快指数、推定高度も表示
  • 128x32 0.91インチ OLED (有機EL) ディスプレイを搭載
    • 制御IC: SSD1306
  • ボタン型電池 CR2032 で 42 時間稼働(計算値)
  • 質量: 約 40g (電池含む)
  • 製作時期: 2020年12月

先行調査


中央の基板が開発に用いた NUCLEO-F303K8

STM32 MCU を使うこと自体が初だったため、まずは NUCLEO-F303K8 (STM32F303K8) を使った調査・回路検証を行いました。 直接 STM32G031J6M6 にプログラムを書き込んで開発してもよいのですが、これまで Arduino を使った開発が主だったうえ、外部デバイスとの通信を行ったことがなかったため、どうしてもデバッグ環境が必要でした。STM32CubeMX でのピンアサイン、VSCode + PlatformIO によるデバッグ環境の整備で 1 週間ほど費やしました。

なお BME280 については 以前から Arduino で動かしていた ので、今回は正しく I²C による通信ができることのみ確認し、新たに使う OLED の調査がメインとなりました。

ハードウェア設計

今回やりたいことは以下の通りです。

  • 温度、湿度、気圧が OLED に表示されること
  • ボタンを押すと表示項目が切り替わること
  • 電池を搭載し、電源ボタンで入・切が変えられること
  • ICSP に対応すること

基板のサイズは以下の基準で決定しました。

  • 基板製造で最も安価な 100mm(幅) x 100mm(高さ) に入ること
    • 面付けにより上記サイズの中に 2枚 または 4枚 に分割できること
    • 4枚の場合、捨て板最小サイズ 5mm を考慮すると 1枚 あたり最大 50mm(幅) x 45mm(高さ)
  • 秋月電子の 45基板用アクリルパネル に適合すること

実際は取り付けるアクリルパネルの大きさが決定打となり、大きさは 43mm(幅) x 43mm(高さ) となりました。基板製造時は 100mm x 100mm の範囲に 4 つ同じ基板が作られます。

回路設計

回路と基板は以前から利用している Quadcept Community Edition を使いました。複雑な配線はなく、なおかつ両面 (Top, Bottom) だけなので無償版で十分です。

回路は非常に簡素で、BME280 と OLED と MCU を結線すればやりたいことがほとんどできてしまいます。I²C のプルアップ抵抗は BME280 モジュール中にあるため配置の必要すらありません。

回路図の拡大

基板設計

43mm x 43mm という小ささですが、回路自体が簡素なので配線と部品配置は至ってシンプルです。部品点数が多いと OLED の下のスペースに部品を配置しなくてはなりませんが、今回はその必要性も感じないほどでした。

ただし基板 Bottom 面にボタン電池ホルダーを配置する必要があったため、基板上半分の大部分にリード部品やスルーホールを配置できませんでした。BME280 モジュールも例外ではなく、これが後述する部品実装の難しさに繋がってしまいました。

ICSP 端子のピッチは 1.27mm で、秋月電子で販売されている 6極ケーブル が挿せる設計にしました。実際に使っているのは 5 ピン分です。

チップ部品は自分の手はんだレベルに合わせて 2012 または 1608 サイズにしました。

ソフトウェア開発

SSD1306BME280 を STM32 から扱うライブラリがちょうどあったので、これを使いました。

なお SSD1306 のフォントデータもライブラリのものを使いましたが、下記のファームウェアのサイズ問題により、そのままの使用はしませんでした。

フォントと画像データ圧縮

OLED の表示は 2 値のドット単位です。バッファとしては 128 x 32 x 2 = 8192 ビット = 1024 バイト が必要です。フォントデータは ASCII 文字のうち印字可能文字とスペースを含めて 95 文字、サイズ違いを含めると 3 種のフォントが存在し、合計で 285 グリフとなります。元のフォントデータ のままでは 10,260 バイトとなり、STM32G031J6M6 のフラッシュサイズの 32,768 バイトに対して 1/3 近くを専有してしまいます。

もちろん全 285 グリフを使うわけではないのですが、画像データの圧縮にも応用するため、今回は連長圧縮によりデータを格納・展開を行いました。データが 2値 であること、ほとんど黒ドット(消灯)であることで概ね 1/2 ほどにデータを圧縮しています。

不快指数の計算

完成品の MCU である STM32G031J6M6 (Cortex-M0+) は FPU を持っていません。つまり実数の計算はソフトウェア実装となります。実装自体はコンパイラによって展開され、簡単な実数計算は特に気をつける必要はありません。

たとえば、乾球温度 T_d (°C), 湿度 H (%) とすると不快指数 \textrm{DI} は、

\textrm{DI} = 0.81 T_d + 0.01 H (0.99 T_d - 14.3) + 46.3

と表現され[1]、これに対応するコードは

float di = 0.81 * temperature + 0.01 * humidity * (0.99 * temperature - 14.3) + 46.3;

となり、簡単な加算と乗算のみで計算できます。

推定高度の計算

ところが、地上気圧 P (hPa) とすると推定高度 H (m) は、

H = -44330.77 \left(\left\{\frac{P}{1013.25}\right\}^{0.190263} - 1\right)

となり[2][3]\frac{P}{1013.25} の 0.190263 乗という計算が出てきます。 これに対応するコードは

#include <math.h>
float altitude = -44330.77 * (pow(pressure_hpa / 1013.25, 0.190263) - 1.0);

となりますが、pow 関数の導入により著しくフラッシュサイズが増加し、制限の 32,768 バイトを突破してしまいます。最適化オプションに -Os を指定しても 3,024 バイトの増加となります。推定高度を計算するだけで 3KB 近くを専有するのは許容できないため、今回は \frac{P}{1013.25} の 0.190263 乗の部分を近似計算をすることにしました。

途中の計算

まず 0.190263 を2進数で表現すると

\begin{aligned} 0.190263 &\simeq 0.001100001011_2 \end{aligned}

となり、これを10進数で表すと

\begin{aligned} 0.001100001011_2 &= \frac{1}{8} + \frac{1}{16} + \frac{1}{512} + \frac{1}{2048} + \frac{1}{4096} \\ &= 0.1901855469... \end{aligned}

となり、目標の 0.190263 と近い値になります。
ところで \sqrt{x} の値はニュートン・ラフソン法により以下の漸化式で求められ、

\begin{aligned} a_{n+1} &= \frac{1}{2}\left(a_n + \frac{x}{a_n}\right) \\ &= \frac{a_n^2 + x}{2 a_n} \end{aligned}

これに対応するコードは以下のようになります。

float sqrt2(float x) {
  float a = x;

  for (uint8_t i = 0; i < 8; i++)  // 8回反復で計算打ち切り
    a = (a * a + x) / (2.0 * a);

  return a;
}

同様に \sqrt[4]{x}\sqrt[8]{x} は以下の漸化式で求められ、

\begin{aligned} a_{n+1} &= \frac{1}{4} \left(3 a_n + \frac{x}{a_n^3}\right) \end{aligned}
\begin{aligned} a_{n+1} &= \frac{1}{8} \left(7 a_n + \frac{x}{a_n^7}\right) \end{aligned}

これに対応するコードは以下のようになります。

float sqrt4(float x) {
  float a = x;

  for (uint8_t i = 0; i < 16; i++)  // 16回反復で計算打ち切り
    a = (3.0 * a) / 4.0 + x / (4.0 * a * a * a);
  return a;
}

float sqrt8(float x) {
  float a = x;

  for (uint8_t i = 0; i < 24; i++)  // 24回反復で計算打ち切り
    a = (7.0 * a) / 8.0 + x / (8.0 * a * a * a * a * a * a * a);

  return a;
}

x^{0.190263}\sqrt{x}\sqrt[4]{x}\sqrt[8]{x} だけを使うと以下のように近似できます。

\begin{aligned} x^{0.190263} &\simeq x^{\frac{1}{8} + \frac{1}{16} + \frac{1}{512} + \frac{1}{2048} + \frac{1}{4096}} \\ &= \sqrt[8]{x} \cdot \sqrt[16]{x} \cdot \sqrt[512]{x} \cdot \sqrt[2048]{x} \cdot \sqrt[4096]{x} \\ &= \sqrt[8]{x} \cdot \sqrt{\sqrt[8]{x}} \cdot \sqrt[8]{\sqrt[4]{\sqrt{\sqrt[8]{x}}}} \cdot \sqrt[4]{\sqrt[8]{\sqrt[4]{\sqrt{\sqrt[8]{x}}}}} \cdot \sqrt{\sqrt[4]{\sqrt[8]{\sqrt[4]{\sqrt{\sqrt[8]{x}}}}}} \end{aligned}

n 乗根の部分が深くなっていますが、コード上は値を使い回せるのでシンプルになります。

float pow1902(float x) {
  float p8    = sqrt8(x);
  float p16   = sqrt2(p8);
  float p512  = sqrt8(sqrt4(p16));
  float p2048 = sqrt4(p512);

  return p8 * p16 * p512 * p2048 * sqrt2(p2048);
}

なお、精度が float 分しかないので気圧の測定範囲のすべてをこの近似計算で網羅はできず、460 hPa (6,183m相当) までしか近似できません。しかし日本において標高 6,183m 相当の地点 または大気圧 460 hPa 相当の環境でこの基板が運用されるとは思い難いので、実用上問題ないという判断に至りました。

遊び心

計測結果を表示するだけならばどの温湿度気圧計でもできるので、表示方法に凝りました。数値の横に表示されるアイコンは計測結果によって変化します。

  • 温度: 温度計の表示が変化する
  • 湿度: 高いと水が満たされていく
  • 気圧: 高いと雲が白くなる
  • 推定高度: 高いほど山が白くなる
  • 不快指数: 高いほどキャラクターの表情が険しくなる

不快指数のキャラクターは kachin 氏作成の「ドール」モデルを使用しました。

前述の通りOLEDには白黒かつ高さ最大32ドットしか表示できないため、文字を極力少なくし、アイコンでわかりやすく表示する方針を採りました。


OLEDディスプレイに表示される表示画面

実際に出来上がったディスプレイ表示は上記のようになりました。基板側面のタクトスイッチを押すと、計測画面が切り替わります。画像は開発用に使った NUCLEO-F303K8 をデバッグモードで起動し、RAM上のデータをダンプして画像にしたものです。

基板発注・完成

例によって PCBGOGO にて基板を発注しました。今回は初めて面付けの依頼を行いました。前述の通り基板1枚につき4回路分の製造としました。捨て板(上下7mm)と面付け仕様(2x2)を伝え、具体的な面付けは実際の製造を行う PCBGOGO に依頼しました。

2020年12月8日に発注、12日に製造完了、15日頃に着荷しました。

部品の実装は手はんだで十分で、洗浄時間も含めれば 1 台あたり 2 時間はかかりません。

頒布

完成した基板は電池と説明書をつけて JapariMeter として 1,500円 で頒布しました。頒布と同時に以下のような意見もいただきました。

  • USBでも駆動できるようにしてほしい
  • キャリブレーション(較正)ができるようにしてほしい

電源の確保は悩ましい問題で、電池式と外部電源式のそれぞれに一長一短があります。電池式は基板単体で動作ができ利便性が増しますが、電池交換の必要があります。また、回路と基板の設計はどのような特性を持った電池を使用するかで大きく左右されます。一方の外部電源式は基板をコンパクトにできますが、常に外部からの電源接続が必要です。
機器の利用状況の想定として主に屋外で断続的に使われることを想定しており、そのため今回は電池式を採用した経緯があります。しかし、意外にも屋内で使われるという意見がいくつかあったので、これは今後の課題とします。

キャリブレーションについては優先度は低いです。理由として、そもそもこの温湿度気圧計はホビー用途であり、キャリブレーションを必要とする場面での使用を想定していないためです。

不満点

初めての STM32 MCU、初めての面付け基板ということもあり、部品実装時に気がついたミスや想定通りに行かなかった部分がありました。

ICSP回路の不備


部品が外されている状態の C1, C2

理想と期待に反して、部品実装後に ICSP 経由でプログラムの書き換えができません。C1, C2 の静電容量増加により書き込みの信号が変化してしまい、ST-Linkが正常にMCUを認識できないためです。そのため初回の書き込みは C1, C2 の実装前に行う必要があります。
使えるピン数が足りず、SWD (Serial Debug) ピンとボタン入力を同じにしたことが原因で、書き込み時は C1, C2 の影響を受けない回路にすべきでした。

次回の JapariRadio ではこの点が改善され、書き込み時はジャンパを外し、通常の利用時はジャンパをつけ、静電容量増加の影響を受けない設計にしています。

BME280実装が難しい

BME280 モジュールが実装される背面には電池ホルダーが配置されており、スルーホール実装ができませんでした。すなわち BME280 を表面実装する必要があり、通常のはんだ付けでは実装できません。はんだごての「こて先」を変えるなどして今回は対応しましたが、それでもランドを壊してしまったり、接触不良となって動作不能となる個体がありました。頒布時は全数の接触と実動作を確認していますが、この確認作業も大きな手間になり効率が下がってしまいます。

終わりに

構想半月、制作半月のハイスピードで基板を完成できました。誠にありがたいことに、自サークルの頒布物の中では人気が高く、売り切れとなりました。

今回の製作が、今後も STM32 の MCU を使った回路、基板製作の基礎になると考えています。不満点を解決し、より完成形に近づけるように活動していきます。この JapariMeter の次回作となる、JapariRadio のレポートも執筆予定です。

製作開始前から構想を聞いてくださり、頒布時のお手伝いも快く引き受けていただいたスティルマン氏に心から感謝します。


リンク

脚注
  1. 出典: 「屋外空間における温冷感指標に関する研究」(木内豪) 付録 A1 式より ↩︎

  2. 出典: SparkFun BME280 Arduino Libary の実装より ↩︎

  3. 係数 44330.77 の根拠についての指摘 ↩︎