基板の温度分布測定用にArduinoでサーモグラフィカメラを作る - 完成編
タイトル通り、何とか年内に完成と言える所まで漕ぎ着けました。日々少しつづとは言え、UNO R4 での動作確認 から GUI モックアップ作成 を経て約5ヶ月、長かったです 😮💨
これまでの検討経緯
まずはこの5ヶ月間に実施した、検討用リポジトリのまとめです。
-
Arduino-UNO-R4/MLX90640
MLX90640 センサボードの動作確認を行ったコードです。この時点ではまだ UNO R4 しか所有していませんでした。 -
GFX ライブラリのベンチマーク
新たに入手した XIAO ESP32-S3 で4つの GFX ライブラリのベンチマークを実施し、ターゲットを LovyanGFX と TFT_eSPI の2つに絞り込みむこととしました。 -
SD カードライブラリの動作検証
各 GFX ライブラリと、Espressif 標準の SD カードライブラリ や SdFat との相互運用性を検証しました。この時の検討結果とは異なり、現在は SdFat の方が高速で、動画を 16 FPS で記録出来ています。 -
Arduino-XIAO-ESP32/FreeRTOS
ESP32 の2つのコアを同期、非同期で動かす実験です。検討の結果、センサ入力をコア1、レンダリングをコア0で、セマフォとメッセージキューを使って同期的に動かす仕様に決定しました。 -
Arduino-XIAO-ESP32/MLX90640_GUI_mockup
当初目論んだ LVGL を動かせなかったため、独自に作成した GUI のモックアップです。 -
MLX90640Viewer
Processing で作成した、MLX90640 専用の動画再生ビューワーです。 -
Arduino-XIAO-ESP32/MLX90640
今回の最終成果物であるサーモグラフィカメラのリポジトリです。
最終仕様のまとめ
若干のジャンクなノウハウを含め、改良点について書き留めたいと思います。
スプライトによるレンダリングの高速化
従来は drawPixel()
や fillRect()
メソッドで直接ディスプレイのインスタンスに描画していましたが、一旦メモリ上に生成したスプライトに描画した後、pushSprite()
で一気に転送するようにした所、描画時間を劇的に高速化(168msec → 48msec)できました。
さらに DMA 転送も効かせたかったのですが、以下の理由により断念しています。
LovyanGFX の DMA 転送について
ソースコードを完全に読み切れてはいませんが、initDMA()
により、writePixels()
、writePixelsDMA()
、pushPixelsDMA()
、pushImage()
、pushImageDMA()
および pushSprite()
の DMA 転送が有効になるようです(まだ他にもあるカモですが…)。
ただし、ラングシップさんの「ESP32のヒープメモリ管理 その1」によれば、
DMA対応メモリは、SPIやI2Sなどにハードウエアを利用したDMA転送で使える領域のメモリです。 SPI接続のPSRAMはDMA転送で利用できないので除外されます。
とのことです。事実、LGFX_Sprite::setPsram(true)
などでスプライトを PSRAM 上に生成した場合、push_sprite()
の定義により、DMA が無効となっています。
今回、192×144 ピクセルのスプライトまでは主メモリ上に確保でき DMA が効きましたが、256×192 ピクセル(単純計算で 256×192×2 (RGB565) = 96KB)が確保できず「黒い画面」となりました。GUI 関連で相当数の静的変数を宣言しているためメモリ不足となったようです。そこで止むを得ず DMA は断念し、スプライトは PSRAM 上に確保することにしました。
ちなみに 192×144 ピクセルで initDMA()
を無効化/有効化した時のレンダリング結果を以下に示します。僅か 2msec(26msec → 24msec)ほどですが、確かに効果はありました。
TFT_eSPI の DMA 転送について
どんな場合に TFT_eSPI::initDMA(true)
が効くのか、LovyanGFX ほどソースコードを読み切れてはいませんが、わずかながら効果がありました(256×192 ピクセルの場合)。LovyanGFX と同様、DMA を有効にした場合、下記コードによりスプライトは PSRAM 上には生成されません。
逆に TFT_eSPI::initDMA(false)
の場合には、TFT_eSPI コンストラクタ中の設定 によりスプライトが PSRAM 上に確保されます。
ただし CONFIG_SPIRAM_SUPPORT
は、ESP-IDF の ビルド時に定義されるシンボル で、Arduino IDE でのコンパイル時には未定義となります(CONFIG_SPIRAM
は参照可)。恐らく ESP-IDF の古いバージョンからの仕様変更に追従できていない TFT_eSPI のバグでしょう。
LavyanGFX とは違い、256×192 ピクセルのスプライトを主メモリ上に確保しても「黒い画面」にはなりませんが、やはりメモリ不足の懸念があるので DMA は有効化せず、User_Setup.h
に以下を追加してスプライトを PSRAM 上に確保する設定としています。
本当に PSRAM では DMA が効かないのか?
ラングシップさんの情報を疑うワケではありませんが、出典を調べないと気が済まないタチなので…。特に ESP32 は派生がいくつかあるので、ESP32-S3 について調べてみました。出典は「ESP32-S3 Technical Reference Manual Version 1.6(2024-12-10版)」です。
「3. GDMA Controller (GDMA)」には以下の記述があります。
原文:
General Direct Memory Access (GDMA) is a feature that allows peripheral-to-memory, memory-to-peripheral, and memory-to-memory data transfer at a high speed. The CPU is not involved in the GDMA transfer, and therefore it becomes more efficient with less workload.
日本語訳:
GDMA (General Direct Memory Access) は、周辺機器からメモリ、メモリから周辺機器、 メモリからメモリへのデータ転送を高速に実行できる機能です。CPU は GDMA 転送に 関与しないため、作業負荷が軽減され、効率が向上します。
一方、無印 ESP32 の Technical Reference Manual Version 5.2(2024.08版)には、GDMA の記述は一切ありません。
また「3.4.9 Accessing External RAM」には、外部 RAM の特定アドレスで GDMA が効きそうな記述があります。
原文:
Any transmit and receive channels of GDMA can access 0x3C000000 ~ 0x3DFFFFFF in external RAM.
日本語訳:
GDMA の送信および受信チャネルはすべて、外部 RAM の 0x3C000000 ~ 0x3DFFFFFF にアクセスできます。
そこで ESP32-S3 のメモリマップ を観ると、0x3C000000
~ 0x3DFFFFFF
は PSRAM 領域であることが分かります。
(このメモリマップは、Espressif 開発者ポータルの2024年8月20日付け記事「ESP32’s family Memory Map 101」に掲載されたマップとは、かなり異なっています)
次に LovyanGFX を対象に、以下のコードでスプライトを PSRAM 上に確保し、そのアドレスを調べてみます(諸々省略しています)。
結果は次の通り、GDMA の対象領域に入っていることが分かりました。
ということで、少なくとも ESP32-S3 には GDMA という機構があり、PSRAM でも DMA が効く可能性があることが分かりました。この件は、継続調査としたいと思います
フレームレートとノイズ
MLX90640 のデータシート 「12.3. Noise performance and resolution」に掲載されたフレームレートとノイズの関係を示すグラフ(Ta = 25°C)によれば、高フレームレートほどノイズの影響を受け易く、バラツキの標準偏差(RMS 値)が大きくなることが分かります。
とかく高フレームレートに注目しがちですが、今回の主目的である「基板の温度分布測定」には低フレームレートによる観測が適しています。そこで対象に合わせてフレームレートを設定できるようにしました。実際の画像を見れば、その効果は一目瞭然と思います。
ヒートマップ
MLX60960 用ライブラリ Adafruit_MLX90640 の例題 には、紫 〜 赤まで虹色のヒートマップが定義されています。これはコレで境界がハッキリして分かり易いものの、温度の変化に対する明るさの変化が直感的ではありません。そこで Matplotlib のカラーマップから inferno
(「灼熱地獄」と言うネーミングがお気に入りです)の RGB 値をサンプリングしました。
作成したRGB グラフを元に、カラーマップを任意の階調数で作れるよう、各値を多項式近似しました。B(青)をそれなりに再現するには6次以上が必要ですが、R(赤)、G(緑)と同じ3次多項式での近似としています。実際、2.4 インチ、16ビット(RGB565)のディスプレイでは区別できません。
温度画像データの記録と再現
Adafruit の記事 に刺激を受け、キャプチャ画面をビットマップに保存する機能に加え、連続画像を記録する機能を実装しました。記事では1フレームずつビットマップを保存していますが、32×24×4 (float) バイトの連続した生データを1つのファイルとして SD カードに保存する仕様としています。
また本体内にビットマップのサムネイル表示と動画再生の機能を実装し、さらに PC 上で再生可能なビューワー MLX90640Viewer も作成しました。
ビューワーのレンダリング部分(バイリニア補間+ヒートマップ)は本体から移植し、本体の動画再生部分はビューワーから逆移植しています 💫
ビューワーのプログラミングは基本 Java ですが、ESP32(あるいは大抵の Arduino ボード)のバイトオーダーはリトルエンディアンで、Java はビッグエンディアンなので変換が必要です。この変換は JPCERT の「FIO12-J. リトルエンディアン形式のデータを読み書きするメソッドを用意する」を参考にしました。
個体情報の確認
メモリの使用状況とか、ファイルエクスプローラーで使ってる便利な std::vector
や std::sort
のサポート状況とかを知りたかったので組み込んでみました。
ハッキリ言ってどーでもよい機能なのですが、参考資料と共に一応紹介しておきますネ。
-
uxTaskGetStackHighWaterMark
タスクの残りスタックサイズを得る FreeRTOS の関数。 -
Miscellaneous System APIs
Espressif 公式の ESP-IDF プログラミングガイド。ソフトウェアによるリセット、MAC アドレスやメモリ関連情報の取得方法、チップや ESP-IDF のバージョン取得方法など、諸々。 -
ESP.h
複雑な構成の ESP32 メモリ情報を簡単に取得するための便利 API 集。 -
Which version of c++ is currently supported
Arduino フォーラムに投稿された、GNU C++ サポートバージョンの情報。
ESP32 個体情報を出力する参考コード
項目 | 内容 | 例 |
---|---|---|
MCU model | MCUのモデル記号、チップのリビジョン番号 | ESP32-S3 R2 |
ESP-IDF ver | ESP-IDFのバージョン番号 / C++バージョン番号 | 5.3.2 C++17 |
Task 1 stack | タスク1スタックの 起動後最小サイズ 1 | 5000 |
Task 2 stack | タスク2スタックの 起動後最小サイズ 1 | 5848 |
Heap total | ヒープメモリの総サイズ | 176140 |
Heap lowest | ヒープメモリの 起動後最小サイズ 1 | 122996 |
PSRAM total | PSRAMの総サイズ | 8388608 |
PSRAM lowest | PSRAMの 起動後最小サイズ 1 | 8242332 |
Sketch free | スケッチ用の空きサイズ | 3342336 |
Sketch size | スケッチのサイズ | 559328 |
ちょっと紛らわしいのですが、「C++バージョン番号」は、スケッチをコンパイルする時の xtensa 用 GNU コンパイラのバージョンです。ESP-IDF 自体は C++23 でビルドされています(出典:ESP-IDF Programming Guide / C++ Support)。
Espressif の ESP32 ボードパッケージ 3.1.0 では GNU コンパイラ 13.2.0 が使われていて、僕の Intel Mac では C++17(13.2.0 のデフォルト)でした。個体情報ではないので組み込む必要は全くないのですが、前バージョンの 3.0.7 が C++20 相当 😳(なぜ?)でしたので…
ちなみに他のバージョンが必要な場合(例えば -std=c++20
)は platform.txt
で設定可能です。ただし 13.2.0 の マニュアルには
Support is experimental, and could change in incompatible ways in future releases.
サポートは実験的なものであり、将来のリリースでは互換性のない方法で変更される可能性があります。
とあるので、ご注意を。
😎 お遊びデモ
新しいカメラを手にすると、色々と写したくなりますよね。我が家の愛犬とかご近所さんとか、撮影してみました。
まぁ、価格差を考えれば仕方のないことですが、サーモパイルアレイ の限界で、マイクロボロメータ には敵いませんネ
さて…
発熱が激しいという RPi 5 を購入したので、どんな具合か見たいというのが元々の目的だったのですが、未だ箱に入ったまま放置です。ま、正月休みにゆっくりと組み上げます。
また、LVGL にもリベンジしたいですね。LovyanGFX で動作させることが目標なので、成功したら報告したいと思います。
来年も良い年になりますように