本稿はシリーズの3番目の記事です:
- センサー紹介 - DHT11とMH-Z14A
- データ通信 - UART
- Arduinoの落とし穴
- (未公開)WiFi編:デバッグ時間を節約するために、ESP32開発ボードを追加購入しました。WiFiとBluetooth機能が組み込まれています。
- (未公開)MQTT編:データを他のデバイスに送信するために、軽量で小さな通信プロトコルであるMQTTを使用しました。
- (未公開)Grafana / Web編:データをデータベースに保存するので、見栄えの良い方法で表示します。ここでは、Grafana + PromethusおよびSvelteを使用してデータを表示しています。
この記事では、Arduinoやプログラミング言語に不慣れな場合に遭遇するいくつかの落とし穴について説明します。
CPUのデータ読み取り順序の違い(ビッグエンディアンとリトルエンディアン)
CPUの命令セットアーキテクチャによって、データの読み取り順序も異なる場合があります。これはエンディアンネス(バイトオーダ)と呼ばれます。ビッグエンディアンは最上位バイトから読み取る方法であり、リトルエンディアンは最下位バイトから読み取る方法です。以下の図を見ればより明確になるでしょう:
Arduinoではリトルエンディアンがデータの読み取り順序として採用されています。ここでその回答を見つけることができます。メモリに関しては、上記で説明した以外のデータの読み取り順序の違いもビッグエンディアンとリトルエンディアンで表現することができます。
メモリは管理のために便宜上固定サイズに分割されるため、連続するデータを格納する際に空間が不足する場合は、次の空間にデータを配置します。値を取得するときにそれらを合算します。
この問題が発生したのは、ppm
をunsigned short
ポインターに直接設定したためです。これにより、正しい数値が自動的に計算され、ビットシフトを手動で書く必要がなくなるはずです。以下の例のようになります:
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
は0〜255の範囲(unsigned)の数字をより広く感じることができるということです。Arduinoでは、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を使用しない方が良いです。Arduinoでは、C++の機能はすべて使用できません。フォーラムのスレッドを参照してください。Arduinoでstdライブラリを動作させるための取り組みが行われているようですが、進捗状況は不明です。
この実験の結果、Arduinoを使用してより低レベルな知識を学ぶことは良い方法であることがわかりました。組み込みのSerial Portをデバッグに使用でき、USB接続してIDEをインストールすればすぐにコードをアップロードでき、GitHubでバックエンドの実装も見つけることができます。同時に、C/C++の知識を補完する時間を見つけたいと思いました(大学の講義でしか触れたことがありません)。
また、この記事はArduinoについてあまり詳しくないため、情報を補完し、説明を改善するために最善の努力をしましたが、不完全な部分や誤りがあるかもしれません。ご指摘いただければ幸いです。