header

Arduino言語 = C/C++言語? Arduinoコードの仕組み

Arduino言語はC言語

Arduinoのアプリケーションを起動すると、おもむろにプログラムを記述するためのウィンドウが表示されます。ここに入力する言語について、本家では稀に"Arduino language"、日本語による記事でもこれを汲んだのか「Arduino言語」として紹介されていますが、これは他言語と比して難解とされるC/C++言語を、そうであること意識する必要のないよう、巧みに覆い隠したものです。それが何より証拠には、Arduino言語とされるはずのものは、その気になればC/C++での記述が可能です。また、実行に最低限必要となるC/C++のコードは、ユーザに見えないかたちで実行前に書き加えられています。

avr-gcc + Wiring + α = Arduino language

Arduinoの心臓部は「AVR」という汎用のマイコンICが使用されています。例えばArduino Diecimilaの場合、AVRシリーズのATMEGA168というマイコンが搭載されており、ボードの中ほどに配置された28ピンのICに品番が刻印されているのが読み取れます。このICはサイズこそ小さいものの、ユーザが作成したプログラムを実行可能な、れっきとしたコンピュータです。また、これと併せて使用するためのavr-gccというコンパイラ(ユーザが記述した言語を、マイコンが理解できる言語に変換するツール)が以前から提供されていました。

avr-gccは「AVR用のGNU C コンパイラ」の略でして、その名が示すとおり本来はC/C++言語、あるいはアセンブラを扱うためのツールです。しかしこれらの言語は初心者にはやや敷居が高く、手軽に学習できるものではありません。そこで、AVRマイコンでよく使用される機能へのアクセスを容易にしたのが、Arduino、そしてその前身に当たるWiring用に開発されたライブラリです。

Arduino言語とは、このavr-gccに加えWiring用のライブラリと、Arduino用にさらに使い易くなった関数をすべて併せて構成したものと言えます。さらにArduinoの利点として、開発環境(プログラムの記述、マイコンへの転送を容易にするためのツール)までセットになっており、ユーザはC言語を扱っていることをほとんど意識することなく開発に取り組むことができます。

setup(), loop()... main()?

通常C/C++でプログラムを書く際には、エントリ・ポイント(最初に呼び出す関数)としてmain()関数、あるいはそれに相当するものがコード内のどこかしらに存在することを要求されます。しかし、ご存知のようにArduinoにおいて必須とされるのはC/C++でおなじみのmain()関数ではなく、

  • void setup()
  • void loop()
という二つの関数だけです。

Arduinoの開発環境はプログラムをコンパイルする前に、標準で付属するいくつかのライブラリをユーザが記述した内容にこっそりと付け加えます。そのうちの一つが、以下の場所にあります。

arduino-0011/hardware/cores/arduino/main.cxx
このファイルの中身を見てみましょう。

int main(void)
{
	init();

	setup();
    
	for (;;)
		loop();
        
	return 0;
}

先ほども申しましたC/C++でプログラムを書く際にエントリ・ポイントとして大抵必須となるmain()関数が、こちらのファイル内にはしっかりと存在しています。

Arduinoに搭載されたAVRマイコンが最初に実行するmain()関数にて、ユーザが記述することになっているsetup()、loop()関数が呼び出されている点に注目して下さい。ユーザがこの二つのいずれかを記述し忘れると、呼び出されるはずの関数が存在しないためエラーとなります。

では、main()関数内の一行目に戻り、init()関数の中身を見てみましょう。
この関数は以下のファイルにて定義されています。 arduino-0011/hardware/cores/arduino/wiring.c

実際はもう少しごちゃごちゃしていますが、ここではArduino Diecimilaには関係のない箇所は省略してあります。

void init()
{
	// 割込みを有効にする
	sei();
	
	// タイマーを0で初期化。このタイマーはmillis()、delay()に使用。
	timer0_overflow_count = 0;

	// on the ATmega168, timer 0 is also used for fast hardware pwm
	// (using phase-correct PWM would mean that timer 0 overflowed half as often
	// resulting in different millis() behavior on the ATmega8 and ATmega168)
	sbi(TCCR0A, WGM01);
	sbi(TCCR0A, WGM00);

	// タイマー0のPre scale factorを64に設定
	sbi(TCCR0B, CS01);
	sbi(TCCR0B, CS00);

	// タイマー0のオーバーフロー時の割込みを有効にする。
	sbi(TIMSK0, TOIE0);

	// timers 1 and 2 are used for phase-correct hardware pwm
	// this is better for motors as it ensures an even waveform
	// note, however, that fast pwm mode can achieve a frequency of up
	// 8 MHz (with a 16 MHz clock) at 50% duty cycle

	// タイマー1のPre scale factorを64に設定
	sbi(TCCR1B, CS11);
	sbi(TCCR1B, CS10);

	// put timer 1 in 8-bit phase correct pwm mode
	sbi(TCCR1A, WGM10);

	// タイマー2のPre scale factorを64に設定
	sbi(TCCR2B, CS22);

	// configure timer 2 for phase correct pwm (8-bit)
	sbi(TCCR2A, WGM20);

	// A/DコンバータののPre scale factorを128に設定
	// 16 MHz / 128 = 125 KHzとなるので、50-200 KHzの範囲内に収まります。
	// 注:クロック速度を変更すると、この箇所は正しく動作しません。
	//   正しい設定を行うにはF_CPUを参照するべきです。
	sbi(ADCSRA, ADPS2);
	sbi(ADCSRA, ADPS1);
	sbi(ADCSRA, ADPS0);

	// A/Dコンバータを有効にする
	sbi(ADCSRA, ADEN);

	// USARTを無効にする。
	UCSR0B = 0;
}

これもArduinoの環境を通して開発を行う分には意識する必要のないことですが、AVRマイコンには、ポートの入出力方向、クロック速度、割込み(interruption..後述)の有効・無効など、各種設定を行うための1ビット(0/1でオン/オフを指定する)のフラグがたくさん存在します。sbi()関数は、任意のビット、あるいはフラグを有効にするためのマクロです。

余談ですがArduinoがポートの入出力方向の切り替えに提供するpinMode()関数も、各ポートに該当する設定ビットへのアクセスを容易にするためのものです。

割込み(interrupts)

「割込み」とは、あらかじめ指定した条件が発生する際に、プログラムの進行を一旦中断してそのイベントに関連付けられた関数にジャンプするための仕組みです。中断されたプログラムは、ジャンプ先の関数から抜け出したあと、元の場所から実行を継続します。

さて、本来AVRマイコンは1クロックの間ウェイトする(何も行わずに待機する)命令こそ持ってはいるものの、delay()の変数のように「ミリ秒」などといった、人間に解り易い単位で待ち時間を指定することができません。

そこでArduinoは、AVRマイコンにあらかじめ備わっている「タイマー割込み」という機能を利用します。これは、プログラムの実行と並行して1クロック毎にカウンタの値を一つずつ増加し、カウンタが一定値に達するたびに前述の「割込み」を実行する機能があります。

また、通常何クロックが何ミリ秒に相当するかは当然CPUの動作速度に依存します。AVRマイコンは一定の範囲内であればユーザが任意に動作速度を変更できますが、Arduinoという一つの規格に沿ってAVRマイコンを扱う分には、大抵のユーザが一様に同じ速度(16MHz)で動作させることになります。Arduinoライブラリはこれらの点を最大限に利用して、経過時間を管理するための機能をユーザに提供しています。

勘のいい方ならばここまでのお話からお察しかも知れませんが、標準設定のArduinoはこのカウンタを回すために、絶えずプログラムを中断しては割込みルーチンを実行しています。
もしウェイト系の関数も、PWMの出力も使用しないのであれば、手動でタイマー割込みを無効にするのも手です。 割込みをすべて無効にするには、一行目にあるsei()関数(おそらく、Set Enable Interrupt)と対をなす、cli()関数を使用します(同様に、おそらくClear Interrupt)。 これら二つの関数は、割込みをすべて有効/無効にするためのフラグを操作します。

以上の内容がユーザが記述したコードに加えられた後にC/C++言語のコードとしてavr-gccに手渡され、AVRマイコンが理解できる言語に変換されます。

割込みに関する追記(08/10/11)

割込み発生時に実行する関数(ISR = Interrupt Service Routine)の定義には、旧式であるSIGNALマクロと、現在推奨されているISRを使う二つの異なる方法があります。
また、カッコ内にてイベントの種類を指定する方法も、各イベントにつき二つの方法があります。

例:USART受信完了時~
   旧:SIG_USART_RECV → 新:USART_RX_vect

この変更は、AVRの製品間で一貫性のなかったマクロ名に、ある程度統一性を持たせるために決定されたようです。現在のところは互換性維持のため、以前の指定方法でもプログラムは問題なく動作しますし、Arduinoの標準ライブラリ内でも多用されていますが、自分でプログラムを書く分には今後次の規約に沿い記述することが推奨されています。

  • SIGNALマクロに代わり、ISRマクロを使用する。
  • イベントを指定するマクロは新しい規約のものを使用する。
  • signal.hをインクルードしない。