2019年08月10日 | 更新。 |
スケッチ(sketch)とは、Arduinoを制御するプログラムの事です。Arduinoは、芸術家などコンピュータに詳しくない人にでも、簡単に使える事を目指しているマイコンですので、プログラムの事を、写生図や下絵を意味するスケッチという、独特の用語で表わします。
スケッチの記述言語はC++に独自の変更を施した、いわゆるArduino言語と呼ばれるものです。C++には、プログラミングの初心者には理解しずらい、関数のプロトタイプ宣言や、分割コンパイル、makefileなどの概念がありますが、Arduino言語はこれらを隠ぺいして、初心者にも扱いやすくしています。
この記事では、主に、Arduino言語が標準的なC++言語とどの様に違うのかという観点から、Arduinoのスケッチの特徴を説明します。
簡単なスケッチの例として、シリアルポート(UART)に9600bpsで"Hello, world!"というメッセージを出力し続けるスケッチを、リスト1に示します。
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello, World!");
}
このスケッチには、Arduinoの持っている性質が端的に表れています。
マイコンのプログラムに慣れた人なら理解できると思いますが、シリアル通信のプログラムを作るには、普通はかなりの量の初期化ルーチンを記述しなければなりません。また、プログラムを作るために、マイコンの仕様書(データシート)をかなり読む必要があります。
Arduinoでは、ハードウェアを抽象化し、ハードウェアの制御に関する関数群やオブジェクト群を標準で用意する事により、リスト1に示す様に、たった数行でHello, world!のスケッチが記述できます。マイコンの仕様書を読む必要もありません。
パソコンのCUI用のプログラムをC++で記述するのに、かなり近い感覚でスケッチを記述できますので、プログラミングの敷居が大きく下がります。
この事により、ソフトウェアとハードウェアの両面を勉強しなければならない、Arduino利用者の負担を軽減しています。
参考:ハードウェアの勉強の負担を軽減する点においては、シールドが大きな役割を果たしています。
また、ハードウェアを抽象化して扱う事により、色々な種類のマイコンを搭載した複数の機種のArduinoで、スケッチを共有できるようになりました。
例えば、リスト1のスケッチには、使用するマイコンのUARTモジュールの仕様(どのレジスタにどんな値を書き込むと何が起こるかなど)を知らないと書けない記述が一切ありません。
Arduinoができる以前は、搭載するマイコンのアーキテクチャが変更になれば、プログラムをほぼ一から作り直さなければならないのが常識だったため、Arduinoはプロトタイピングの時間短縮に劇的な効果をもたらしました。
参考:Arduinoができる以前も、マイコンのメーカーがUARTなどの周辺機器を操作するための関数群を提供する事は行われていましたが、アーキテクチャが違うマイコンを使ったり、さらには別のメーカーのマイコンを使ったりする場合、その様な関数群に互換性がないという問題がありました。
その様な大きな利点がある一方で、Arduinoのスケッチには、そのマイコン特定の機能を使ったり、マイコンの性能を限界まで引き出すのが難しいという欠点があります。
Arduino IDEは、プログラム言語としてはC++を使いますが、コンパイラにソースコードを渡す前に、独特の前処理をします。
例えば、基本的な型の宣言や入出力関数などのよく使うライブラリのヘッダファイルは、暗黙的にインクルードされます。そのため、#include指令が少なくなり、ソースコードが見やすくなります。
具体的には、全てのスケッチには、Arduino.hというヘッダファイルが暗黙的にインクルードされます。
例えば、リスト1に示したスケッチの場合、リスト2の様に解釈されます。
#include <Arduino.h> // この行がコンパイルの前処理により暗黙的に挿入される
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello, World!");
}
もちろん、実際にリスト2の様に#include <Arduino.h>
をスケッチの先頭に記述しても問題はありません。
さらに、このArduino.hの中で、C++の標準ライブラリや、Arduinoが使用しているマイコンのメーカーなどが配布しているライブラリのヘッダファイルをインクルードしています。
リスト3に示すのは、Arduino IDE 1.0.1に付属するArduino.hの冒頭部分です。Arduino.hが、さらに色々なヘッダファイルをインクルードしている様子が分かります。
#ifndef Arduino_h
#define Arduino_h
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <avr/pgmspace.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "binary.h"
参考:avr/で始まるヘッダファイルは、Atmel社(現:Microchip Technology社)の8ビットマイコンであるAtmel AVR(通称、AVRマイコン)用の標準関数群のヘッダファイルです。これらの標準関数は、AVR Libcというライブラリとして配布されている物です。(AVR Libcのライセンスが再配布を許容しているので、Arduino IDEの中にAVR Libcが添付されています) Arduino IDE 1.0.1の頃は、AVRマイコンを搭載したArduino(Arduino Unoなど)しかなかったので、Arduino.hというヘッダファイルはひとつしかありませんでしたが、最近のArduino IDEでは、色々なマイコンをサポートしていますので、マイコンの種類ごとに、別のディレクトリのArduino.hをインクルードします。
C++のプログラムでは、インクルードしたヘッダファイル(例えばArduino.h)の中で、使用したい別のライブラリのヘッダファイル(例えばmath.h)が間接的にインクルードされている事が分かっている場合でも、ソースファイル中で、使用するヘッダファイルを全て明示的にインクルードする(例えばソースファイルでArduino.hとmath.hの両方をインクルードする)場合が多いです。そうしておかないと、ヘッダファイルの仕様変更で、ヘッダファイル中で間接的にインクルードするヘッダファイルに変更があった場合、ソースファイルを変更する必要が生じ、ソースファイルの保守性が低下するからです。
ところがArduinoの場合、プログラム初心者を対象にしているので、Arduino専用のArduino.hで宣言されている関数であれ、C++の標準ライブラリのmath.hで宣言されている関数であれ、Arduinoを使える時に、自分で作らなくても最初から使える「天から降ってきた」関数群として、区別しない文化があります。(さらにいうと、「天から降ってきた」関数群のヘッダファイルは、わざわざインクルードしなくていいということになっています)
現に、Arduino.hで定義されているdigitalWrite関数も、math.hの中で定義されているsin関数も、Arduino公式サイト(arduino.cc)のArduino言語の関数リファレンスのページでは同格に扱っています。sin関数のページを見ても、sin関数がmath.hで宣言されている関数だという類の説明がないので、(C++に詳しくない)読者は、「sin関数を使う時はmath.hをインクルードしよう」という発想がわかないと思います。
Serialというグローバルなオブジェクト変数(オブジェクトのインスタンス)は、暗黙的に宣言されます。(例えばリスト1では、何の宣言もなくSerial変数を使っているのが分かると思います)
Serialは、UARTを使った送信や受信をカプセル化したオブジェクトのインスタンスです。
参考:内部的にはSerial変数はSerial_クラスの変数として宣言されていますが、Arduinoの利用者はオブジェクトの概念を理解していない場合も多く(言い換えればArduino言語がオブジェクトの概念の習得を要求しない様に設計されており)、Serial変数が何型かという事は、ライブラリの開発者でない限り、通常は問題になりません。それどころか、ArduinoのSerialの説明のページには、Serialが変数という事すら載っていません。多くのArduinoユーザーは、「Serial.で始まる名称の関数群を使うとUARTで通信できる」という具合に認識していると思います。
Arduino(を含む多くのマイコンボード)には、原則的に液晶ディスプレイなどの表示装置がありません。またArduinoは、デバッガーを使ってデバッグを行う事を前提にしていません。
ただし、スケッチの書き込みのために、内蔵USB-シリアル変換器を通じてパソコンとつながっていますので、UART経由でパソコンと情報のやり取りができる前提になっています。
そこで、スケッチのデバッグには、UARTを利用したいわゆるprintfデバッグが必須に近いデバッグ法になります。
注:Arduino ProやArduino Pro Miniなど、実際に制作物の中に組み込むのが前提に作られているArduinoの場合は、コストとサイズと消費電力の低減のためにUSB-シリアル変換器が外付けになっている物もあります。Arduino ProやArduino Pro Miniは、USBシリアル変換器が外付けになっており、サイズが小さくなっている点を除いては、Arduino Unoと同機能、同性能になっているので、まずArduino Unoでプロトタイピングした後に、実際に制作物に組み込むのはArduino ProやArduino Pro Miniという位置づけになっているものと思われます。
参考:printfデバッグとは、printf関数により、変数の内容などの情報を標準出力に出力して確認するデバッグ手法の事です。ただし、Arduino言語にはprintf関数が実装されていませんので、代わりに、後述するSerial.print関数(またはSerial.println関数)を使う事になります。printf関数の書式文字列を利用したい場合には、sprintf関数を使って、出力したい文字列をいったん文字列変数に書き出し、その文字列変数を、Serial.print関数(またはSerial.println関数)に渡します。printf関数が使えないのは、Arduinoユーザーから評判の悪い点のひとつです。
UARTを操って通信するには、通常ハードウェアやソフトウェアの知識が要求されます。しかしArduinoの場合は、フロー制御やパリティチェック機能については省略あるいは簡略化した上で、Serialというオブジェクト変数にUARTの機能を集約して、初心者にでもUARTが扱えるようにしています。
参考1:ArduinoではUART関係の端子がRXとTXしかないので、ハードウェアフロー制御ができません。
参考2:Serial.begin関数でパリティビットの設定はできるものの、受信時にパリティチェックでエラーが起こったところで、そのエラーをうまく処理する機構がないので、受信時のパリティチェックにはあまり意味がありません。送信時には、送信相手が正しくパリティチェックの処理を実装していれば、パリティチェックがうまく機能します。
シリアルポートを使ってprintfデバッグをするには、Arduinoから送られてきた情報をパソコンの画面に表示するためのターミナルソフトが必要になります。Arduino IDEには、シリアルモニタという、簡易型のターミナルソフトが内蔵されています。(図1参照)
Serial変数で使えるメンバ関数は多くありますが、UART(シリアルポート)経由で情報を送信する時によく使われるのは、Serial.begin関数とSerial.print関数とSerial.println関数の3つ(Serial.print関数とSerial.println関数は、文字列の送信時に改行するかどうかの違いしかないので、実質2つ)です。
この章では、Serial.bigin関数、Serial.print関数、およびSerial.println関数の3つの関数について説明します。
UART経由で情報を受信する場合には、Serial.read関数やSerial.available関数なども使いますが、ここでは説明しません。
Serial.end関数(シリアルポートを閉じてRXとTXのピンを開放する関数)も基本的な関数ですが、シリアルポートは開いたままで使う事が多く、使用頻度が低いため、ここでは説明しません。
参考:前述の様に、Serial変数で使えるメンバ関数は、Arduinoユーザーが「Serial.で始まる名前の関数」という具合に認識している場合が多くあります。またarduino.ccの例えばSerial.begin関数のページを見ても、見出しが"begin()"ではなく"Serial.begin()"とSerial.付きになっています。そこで、このページでも例えばbegin関数ではなく、Serial.begin関数と表記します。(begin関数は他のクラスでも使われるので、単にbegin関数と呼ぶのでは曖昧になってしまいます。Serial変数はSerial_クラスのインスタンスなので、本来はSerial_::begin関数と呼ぶのが正しいと思います。しかし、前述の様に、Serial変数がSerial_クラスのインスタンスである事はほとんど認知されていません)
シリアルポートを開き、UART経由で通信できるようにする関数です。引数により、通信速度、データ長、パリティ、ストップビットに関する設定ができます。
void Serial.begin(uint32_t baud)
void Serial.begin(uint32_t baud, uint8_t config)
SERIAL_5N1
(5ビット、パリティなし、ストップビット1ビット)SERIAL_6N1
(6ビット、パリティなし、ストップビット1ビット)SERIAL_7N1
(7ビット、パリティなし、ストップビット1ビット)SERIAL_8N1
(8ビット、パリティなし、ストップビット1ビット、デフォルト値)SERIAL_5N2
(5ビット、パリティなし、ストップビット2ビット)SERIAL_6N2
(6ビット、パリティなし、ストップビット2ビット)SERIAL_7N2
(7ビット、パリティなし、ストップビット2ビット)SERIAL_8N2
(8ビット、パリティなし、ストップビット2ビット)SERIAL_5E1
(5ビット、偶数パリティ、ストップビット1ビット)SERIAL_6E1
(6ビット、偶数パリティ、ストップビット1ビット)SERIAL_7E1
(7ビット、偶数パリティ、ストップビット1ビット)SERIAL_8E1
(8ビット、偶数パリティ、ストップビット1ビット)SERIAL_5E2
(5ビット、偶数パリティ、ストップビット2ビット)SERIAL_6E2
(6ビット、偶数パリティ、ストップビット2ビット)SERIAL_7E2
(7ビット、偶数パリティ、ストップビット2ビット)SERIAL_8E2
(8ビット、偶数パリティ、ストップビット2ビット)SERIAL_5O1
(5ビット、奇数パリティ、ストップビット1ビット)SERIAL_6O1
(6ビット、奇数パリティ、ストップビット1ビット)SERIAL_7O1
(7ビット、奇数パリティ、ストップビット1ビット)SERIAL_8O1
(8ビット、奇数パリティ、ストップビット1ビット)SERIAL_5O2
(5ビット、奇数パリティ、ストップビット2ビット)SERIAL_6O2
(6ビット、奇数パリティ、ストップビット2ビット)SERIAL_7O2
(7ビット、奇数パリティ、ストップビット2ビット)SERIAL_8O2
(8ビット、奇数パリティ、ストップビット2ビット)なし
シリアルポートに文字列を送信します。引数には文字列以外にも整数や文字、浮動小数点なども渡せますが、いずれも人の読める文字列に変換されます。浮動小数点の場合は、小数点以下第2位まで送信するのがデフォルトの動作です。バイト型のデータは1文字として送信されます。文字列やString型は、そのまま送信されます。以下に例を示します。
Serial.print(78)
を実行すると"78"を送信します。Serial.print(1.23456)
を実行すると"1.23"を送信します。Serial.print('N')
を実行すると"N"を送信されます。Serial.print("Hello world.")
を実行すると"Hello world."を送信されます。オプションの第2引数によって、数字の基数(何進数か)が設定できます。使用できる値はBIN(2進数)、OCT(8進数)、DEC(10進数)、HEX(16進数)です。浮動小数点の場合は、第2引数は小数点以下の桁数を指定します。以下に例を示します。
Serial.print(78, BIN)
を実行すると"1001110"を送信します。Serial.print(78, OCT)
を実行すると"116"を送信します。Serial.print(78, DEC)
を実行すると"78"を送信します。Serial.print(78, HEX)
を実行すると"4E"を送信します。Serial.print(1.23456, 0)
を実行すると"1"を送信します。Serial.print(1.23456, 2)
を実行すると"1.23"を送信します。Serial.print(1.23456, 4)
を実行すると"1.2346"を送信します。参考:10進数の78は、2進表記では1001110、8進表記では116、16進表記では4Eになります。
size_t Serial.print(const __FlashStringHelper *val)
(F("Hello World.")の様な、F()マクロを使ってフラッシュメモリ上に記録される文字列定数の場合)
size_t Serial.print(const String &val)
(String型の場合)
size_t Serial.print(const char *val)
(文字列の場合)
size_t Serial.print(char val)
(文字の場合)
size_t Serial.print(uint8_t val, int format = DEC)
(符号なし8ビット整数の場合)
size_t Serial.print(int16_t val, int format = DEC)
(符号あり16ビット整数の場合)
size_t Serial.print(uint16_t val, int format = DEC)
(符号なし16ビット整数の場合)
size_t Serial.print(int32_t val, int format = DEC)
(符号あり32ビット整数の場合)
size_t Serial.print(uint32_t val, int format = DEC)
(符号なし32ビット整数の場合)
size_t Serial.print(double val, int format = 2)
(浮動小数点の場合)
size_t print(const Printable &val)
(Printable型の場合)
注:Printable型(Printableクラス)というのは、Serial.print関数やSerial.println関数などで表示(送信)できる抽象クラスです。Printableクラスを継承したクラスでprintToという抽象メンバ関数を実装すれば、色々な型のメンバ変数を持ったオブジェクトを表示できるようになります。詳しくはArduino ForumのPrintable classesというスレッドをご覧ください。
送信したバイト数を返します。型はsize_t型です。
Serial.println関数は、送信する文字列の最後に2バイトの改行コード(ASCIIコードの13番と10番)を付け加える以外は、Serial.print関数と同じ働きをします。詳しくはSerial.print関数の節をご覧ください。
C++では必須のmain関数を、Arduino言語では記述しません。その代わり、setup関数とloop関数を記述する事になっています。
Arduinoの様な、組み込み用途に使うマイコンでは、プログラムの処理が終わったら停止する様な使い方はあまりしません。たいていは、電源を切るまで同じ動作をし続ける様にプログラムを組みます。その場合は、プログラムは初期化部と繰り返し処理部の2つの部分に分割できる事になります。
Arduinoのスケッチでは、setup関数が初期化部の処理に、loop関数が繰り返し処理部のループ1回分の処理に相当します。
つまり、リスト4に示す様なmain関数が暗黙的に宣言されていると考える事ができます。
int main()
{
// 厳密にいうとマイコンや各種ライブラリの初期化処理がここ(setup関数の前)に入る
setup();
for(;;) { // for文を使って無限ループを記述
loop();
// Arduinoの種類によれば、ここ(loop関数の直後)に以下の処理が入る。ただし、デフォルトではserialEventRunという関数は特に何もしないので、下の行があっても具体的に何かの処理をする訳ではない。
// if (serialEventRun) serialEventRun();
}
}
main関数を書かない代わりに、setup関数とloop関数を書くArduinoのスケッチの流儀は、ユーザーに組み込みプログラムの組み方を意識させる、よい流儀だと思います。
参考:パソコン上で、主にグラフィック描画をしながら、初心者がプログラミングを学ぶためのプログラム言語(および開発環境)に、Processingというものがあります。実はArduino IDEは、そのProcessingから派生してできた開発環境です。Processingの時代から、プログラムを初期化部のsetup関数と繰り返し部のループ1回分に相当するdraw関数(Arduinoのloop関数に相当)に分割する考え方があったので、その考え方は組み込み用途を意図してできたものではないと推測されます。しかし結果的に、スケッチをsetup関数とloop関数に分割する考え方は、組み込み用途にマッチしています。
リスト4に示した様なmain関数が暗黙的に宣言されると説明しましたが、リスト4とリスト1を単純にくっつけて作ったリスト5のスケッチを、Arduino IDEでコンパイルすると、エラーが出ずにコンパイルが終了します。(Arduino IDE 1.8.9で、Arduino Unoをターゲットにしてコンパイルしました)
int main()
{
// 厳密にいうとマイコンや各種ライブラリの初期化処理がここ(setup関数の前)に入る
setup();
for(;;) { // for文を使って無限ループを記述
loop();
// Arduinoの種類によれば、ここ(loop関数の直後)に以下の処理が入る。ただし、デフォルトではserialEventRunという関数は特に何もしないので、下の行があっても具体的に何かの処理をする訳ではない。
// if (serialEventRun) serialEventRun();
}
}
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello, World!");
}
コンパイルエラーが出ないどころか、コンパイルしたスケッチをArduino Unoに書き込んで実行すると、正常に動作して(正常に動作した様に見えて)、シリアルモニタには"Hello World!"の文字列が延々と表示され続けます。
私にはmain関数が2重定義されて、リンクエラーが出そうな気がしますし、正常にリンクされたとしても、setup関数の前の初期化ルーチンが処理されないために、スケッチが正常に動かない気がするのですが、どういう訳か動作します。
ただ、リスト1をコンパイルするとスケッチがフラッシュメモリを1492バイト使用し、グローバル変数がRAMを202バイト使用するのに対して、リスト5をコンパイルすると、スケッチが1166バイト使用し、グローバル変数がRAMを193バイト使用します。どういう訳かメモリ消費量が減るので、何かの処理が省略されてしまっているようです。
さらに話を進めて、setup関数やloop関数を使わない、リスト6の様なスケッチを作ると、これもエラーなくコンパイルされ、正常に動作している様に見えます。
int main()
{
Serial.begin(9600);
for(;;) { // for文を使って無限ループを記述
Serial.println("Hello, World!");
}
}
リスト6のスケッチのメモリ消費量は、リスト5のメモリ消費量と全く同じなので、リスト5のスケッチではsetup関数やloop関数の呼び出しがインライン展開されて、関数呼び出しが削除されてしまったものと考えられます。
リスト6の様なArduinoの流儀を完全に無視したスケッチの書き方が、常に正常に動作する保証はないのですが、曲がりなりにもコンパイルできて、実行できるというのは驚きです。
関数の本体が、使う場所よりも後方で宣言されていても、その関数のプロトタイプ宣言をする必要はありません。コンパイルの前処理でプロトタイプ宣言が自動的に挿入されます。
例えば、リスト7のスケッチでは、dblという関数は、使用される場所よりも後方で宣言されていますが、プロトタイプ宣言を行っていません。
void setup() {
Serial.begin(9600);
Serial.println(dbl(5)); // 10と表示される
}
void loop() {
}
int dbl(int n) { // 引数の倍を返す関数
return 2*n;
}
もちろん、次の様なプロトタイプ宣言をsetup関数の前で行ってもエラーにはなりません。
int dbl(int);
非標準のライブラリを使用する場合には、そのライブラリのヘッダファイルを明示的にインクルードする必要があります。例えばリスト8は、リスト1と同じ処理をソフトウェアシリアルによって実現するスケッチですが、冒頭でSoftwareSerial.hというヘッダファイルをインクルードしています。
#include <SoftwareSerial.h>
SoftwareSerial MySerial(0,1);
void setup() {
MySerial.begin(9600);
}
void loop() {
MySerial.println("Hello, World!");
}
明示的にヘッダファイルをインルードする必要のあるライブラリは、後からArduino IDEにインストール(登録)したライブラリだけではなく、Arduino IDEに付属しているライブラリの一部も含みます。例えばリスト8で使っているソフトウェアシリアルのライブラリも、Arduino IDEに付属しているライブラリですが、SoftwareSerial.hをインクルードする必要があります。
この様に、後からArduino IDEにインストールしたライブラリと、Arduino IDEに付属しているものの、ヘッダファイルを明示的に宣言しない限り有効にならないライブラリを合わせて、ここでは非標準ライブラリと呼んでいます。
SoftwareSerial.hをインクルードするだけで、ソフトウェアシリアルライブラリがコンパイルおよびリンクされる様になっているため、makefileを記述する必要が全くなくなっています。
参考:ライブラリのヘッダファイルをインクルードする場合は、パスを指定せずにヘッダファイルのファイル名を山括弧<>で囲むだけで構いません。山括弧で囲んだヘッダファイルをインクルードすると、Arduino IDEは、C++の標準ライブラリやArduinoの標準ライブラリに同名のヘッダファイルがないか探した後に、Arduino IDEにインストールされた全てのライブラリのディレクトリから同名のヘッダファイルを探します。ライブラリのディレクトリから同名のヘッダファイルが見つかると、ヘッダファイルをインクルードすると共に、そのライブラリをmakefileに自動的に挿入します。(Arduinoのライブラリはコンパイル後のオブジェクトファイルではなく、ソースファイルの形で配布されます) この方法は、使用者に要求する知識のレベルを下げられるというメリットはあるものの、Arduino IDEがソースファイルのコンパイル時に行う前処理が重くなったり、別々のライブラリの開発者が偶然ヘッダファイルを同じファイル名にした場合に、どちらのライブラリを使うかを明示的に示す方法がないなどのデメリットもあります。
以上説明してきた様に、Arduino言語には、初心者がプログラミングしやすくなる様な工夫が色々してあります。またその工夫により、通常のC++のソースファイルと比較すると、Arduinoのスケッチでは記述量が減ります。
上級者が本格的に開発するには、Arduino言語を使うよりも素のC++を使う方が、プログラムの保守性がよくなったり、ハードウェアの性能を限界まで引き出す様なきめの細かい記述ができたりと、いい事もあります。
しかしそれでも、プロトタイピングに限っていえば、Arduinoは、プロの開発者が使う場合でも、開発時間を短縮するポテンシャルを持っているのです。