· 6分で読了

ArduinoとESP32で作る空気質モニタリングアプリ(3)- Arduinoの落とし穴編

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

本記事はシリーズの第3回だ:

  1. センサー紹介編 - DHT11とMH-Z14A
  2. データ通信編 - UART
  3. Arduinoの落とし穴編
  4. (未公開)WiFi編:デバッグ時間を節約するため、最初からWiFiとBluetooth機能が搭載されているESP32開発ボードを追加で購入した
  5. (未公開)MQTT編:データを他のデバイスへ送信するため、軽量でコンパクトなプロトコルであるMQTTを使用した
  6. (未公開)Grafana / Web編:データベースに保存したデータは、やっぱりカッコいい方法で可視化したい!ここではGrafana + PrometheusとSvelteを使ってデータを表示した。

今回は、Arduinoやその言語仕様への不慣れさから踏んでしまったいくつかの落とし穴について話していく。

CPUのデータ読み込み順序の違い(Big EndianとLittle Endian)

CPUの命令セットアーキテクチャが異なるため、データを読み込む順序も異なってくる。これはエンディアン(Endianness、バイト順)と呼ばれる。上位バイトから読み込むものをビッグエンディアン(Big-Endian)、下位バイトから読み込むものをリトルエンディアン(Little-Endian)と呼ぶ。以下の図を見ればよりわかりやすいだろう:

big-endian and little-endian difference

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 と同じ効果が得られるはずだった。

short 指標測試結果

だがテストしてみると、非常に奇妙な数値になってしまった。おそらくバイトの読み込み順序の違いが原因だろう。おとなしくビットシフトを使うようにしたら、正しい値を取得することができた:

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_tcharint の違いがよく分からなくなりがちなので、ここで一度整理しておこう。

  1. byteunsigned char は同じものであり、どちらも変数に8ビットのメモリを割り当てる。唯一の違いは、unsigned char の持つ意味合いがユーザーにとってより広義に感じられるのに対し、byte はその数値が 0 〜 255 の間(unsigned)であることをより明確に表現している点だ。Arduinoでは一貫性(consistency)のために byte の使用を推奨している。
  2. uint8_tbyte、そして unsigned char は実のところほぼ同じ概念であり、エイリアスのようなものだ。C/C++ではプラットフォームによってintが8ビットや16ビットとして定義されることがあるため、より厳密な意味を表現するために通常 uint8_t が定義されている。

ヘッダーファイルを活用して関数を分割する

Arduinoのプロジェクトを作るときはコードの規模が比較的小さいため、すべての機能を loopsetup の中に詰め込んでもそれほど読みにくくはならない。しかし、回路が少し複雑になってくると、すべての実装を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に不慣れだったため、できる限り資料を調べて説明を尽くしたものの、不完全な点や誤りがあるかもしれない。その際は遠慮なくご指摘いただければ幸いだ。

関連記事

他のトピックを探索