ArduinoとESP32で作る空気質モニタリングアプリ(3)- Arduinoの落とし穴編
本記事はシリーズの第3回だ:
- センサー紹介編 - DHT11とMH-Z14A
- データ通信編 - UART
- Arduinoの落とし穴編
- (未公開)WiFi編:デバッグ時間を節約するため、最初からWiFiとBluetooth機能が搭載されているESP32開発ボードを追加で購入した
- (未公開)MQTT編:データを他のデバイスへ送信するため、軽量でコンパクトなプロトコルであるMQTTを使用した
- (未公開)Grafana / Web編:データベースに保存したデータは、やっぱりカッコいい方法で可視化したい!ここではGrafana + PrometheusとSvelteを使ってデータを表示した。
今回は、Arduinoやその言語仕様への不慣れさから踏んでしまったいくつかの落とし穴について話していく。
CPUのデータ読み込み順序の違い(Big EndianとLittle Endian)
CPUの命令セットアーキテクチャが異なるため、データを読み込む順序も異なってくる。これはエンディアン(Endianness、バイト順)と呼ばれる。上位バイトから読み込むものをビッグエンディアン(Big-Endian)、下位バイトから読み込むものをリトルエンディアン(Little-Endian)と呼ぶ。以下の図を見ればよりわかりやすいだろう:

Arduinoはリトルエンディアンを採用しており、ここで回答を見つけることができる。上で触れたメモリに限らず、データの読み込み順序が異なるものはすべてビッグエンディアンとリトルエンディアンで表現できる。
メモリは管理を容易にするため、OSがメモリ空間を固定サイズに分割している。そのため、連続したデータを格納する際に容量が足りなければ次の領域にデータを格納し、値を取得する際にそれらを結合する。
なぜ僕がこの問題に直面したかというと、当時は小賢しくも ppm を直接 unsigned short のポインタにしてしまえば、わざわざ自分でビットシフト(bit shift)を書かなくても正しい数値を自動で計算してくれるはずだと考えたからだ。例としては以下のような感じだ:
struct Co2Result {
byte startByte;
byte command;
byte high;
byte low;
unsigned short ppm;
};
Co2Result *result = malloc(sizeof(Co2Result));
result->startByte = response[0];
result->command = response[1];
result->high = response[2];
result->low = response[3];
result->ppm = &result->high;
short のサイズは2バイトなので、ポインタを result->high に向けて後方へ2バイト読み込めば、実質的に result->high<<8 + result->low と同じ効果が得られるはずだった。

だがテストしてみると、非常に奇妙な数値になってしまった。おそらくバイトの読み込み順序の違いが原因だろう。おとなしくビットシフトを使うようにしたら、正しい値を取得することができた:
result->ppm = (result->high << 8) + result->low;
C言語のデータ構造はプラットフォームによって異なる
例えば、int は2バイトの場合もあれば4バイトの場合もあるし、一般的な long も8バイトの場合もあれば4バイトの場合もある。Javaなどの言語では、データ型のサイズがプラットフォームによって異なる結果になることはない。データ型の定義がプログラミング言語の実装自体によって定められているからだ。そのためC/C++では、プラットフォームが必要に応じて異なるサイズのデータ型を定義できるように、uint8_t のような型が用意されている。
C言語を書き慣れている人にとっては当たり前のことかもしれないが、データ構造のサイズを求める際は、すべてのプラットフォームでデータ型のサイズが固定であると思い込まずに、sizeof を使って計算すべきだ。
byte 型を活用する
Arduinoには byte というデータ型が提供されている。実態の定義は unsigned char と同じだが、uint8_t、char、int の違いがよく分からなくなりがちなので、ここで一度整理しておこう。
byteとunsigned charは同じものであり、どちらも変数に8ビットのメモリを割り当てる。唯一の違いは、unsigned charの持つ意味合いがユーザーにとってより広義に感じられるのに対し、byteはその数値が 0 〜 255 の間(unsigned)であることをより明確に表現している点だ。Arduinoでは一貫性(consistency)のために byte の使用を推奨している。uint8_t、byte、そしてunsigned charは実のところほぼ同じ概念であり、エイリアスのようなものだ。C/C++ではプラットフォームによってintが8ビットや16ビットとして定義されることがあるため、より厳密な意味を表現するために通常uint8_tが定義されている。
ヘッダーファイルを活用して関数を分割する
Arduinoのプロジェクトを作るときはコードの規模が比較的小さいため、すべての機能を loop と setup の中に詰め込んでもそれほど読みにくくはならない。しかし、回路が少し複雑になってくると、すべての実装を1つのファイルに詰め込むのは少々散らかってしまう。
Arduino IDEの拡張子は .ino だが、本質的には裏で動いているのは C++ だ。そのため C++ の構文をサポートしているが、標準ライブラリをそのまま使うことはできない。モジュールを書いてArduinoの関数を使いたい場合は、Arduino.h をインクルードするだけでいい。
#include "Arduino.h"
#include <string.h>
また、vscode で書く場合、Arduinoの標準組み込みライブラリがうまく include できないことに気づくだろう。直接インクルードしてもコンパイルは通るが、エディタ上に赤波線が表示されてしまう。その場合は、.vscode/c_cpp_properties.json を追加することで、vscodeが正しいヘッダーファイルを見つけられるようにできる。以下に示す通りだ(macOS版の例。LinuxやWindowsの場合はArduinoのインストール場所を別途探す必要がある):
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/**",
"/Applications/Arduino.app/Contents/Java/hardware/**",
"/Applications/Arduino.app/Contents/Java/hardware/arduino/**"
],
"defines": [],
"macFrameworkPath": [
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks"
],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
その他
noInterrupet()とcli()は同じだ。Arduino.hの中では以下のように定義されている。
#define interrupts() sei()
#define noInterrupts() cli()
- Arduinoで直接
std:stringを使うと問題が発生する。Arduino上ではstdの使用を避けたほうが無難で、あの素晴らしいC++の機能たちはArduino上ではことごとく使えない。フォーラムの投稿を参考にするといいが、コミュニティでは標準ライブラリ(std library)をArduino上でも動作させようと試みている人たちがいるようだ。進捗がどうなっているかはわからないが。
今回の一連の実験を通して、Arduinoを使って低レイヤーの知識を学ぶのは非常に良いアプローチだと僕自身気づいた。デバッグ用のSerial Portがあらかじめ用意されており、USBを繋いでIDEをインストールすればすぐにコードをアップロードできて面倒な手間が省けるし、背後の実装コードもGitHubで見つけられる。同時に、時間を見つけてC/C++の知識を補強したいという気持ちにもなった(大学の講義で少し触った程度だったので)。
また、本記事は僕自身がArduinoに不慣れだったため、できる限り資料を調べて説明を尽くしたものの、不完全な点や誤りがあるかもしれない。その際は遠慮なくご指摘いただければ幸いだ。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。