おもちゃラボ

Unityで遊びを作ってます

【Arduino】アセンブラ入門 その1

ArduinoはAVR(ATMega328)というマイコンの外側にIO用の電子回路をくっつけただけの構造なので、もちろんAVRの文法に従ってアセンブラでプログラムを書くことが出来ます。

このご時世アセンブラて!と思わないこともないですが、ディスプレイのドライバだったりネットワークのパケット処理だったり、結構速度を求められる場面では役に立つことがあります。

普通にC言語で書く場合と比べると10倍以上の速度の差が出ることもざらにあるので、覚えておいて損はないですよ〜

今回の記事の内容は次のとおりです。

まずは何もしないアセンブラを書いてみよう

アセンブラでプログラムを書く、といっても何もプログラムのすべてをアセンブラにする必要はありません。遅い所だけを狙って、その処理だけをアセンブラで書く。プログラムを高速化する時の鉄則ですね〜。

このように、プログラムの一部だけをアセンブラにすることをインラインアセンブラと呼びます。この記事では基本的にインラインアセンブラの書き方を紹介しています。

まずは、アセンブリ言語版ハローワールド(?)の何もしない命令を書いてみましょう。次のプログラムを書いてください。

void setup()
{
	asm( "nop \n" );
}

これだけ?と思ったかもしれませんね。はい、なんとコレだけです(笑)インラインでアセンブラを書くにはasmの()の中にアセンブラで文字列としてプログラムを書くだけです。

nopはNo Operationの略で「何もしない命令」です。実行しても何も起こりませんが、ちゃんとコンパイルも通ってプログラムも実行できている、ということが大切です。

変数?いやレジスタを使ってみよう

アセンブラのプログラムが思いのほか簡単にかけることがわかったところで、つぎは変数を使ってみましょう。といっても、アセンブラに変数という概念はなくて、ハードウエアとして実装されている「レジスタ」に値を入れたり、取り出したりして使うことになります。

次のプログラムを入力してみてください。実はかなりお行儀が悪いプログラムなのですが、それは次のお話・・・ということで(笑)

volatile byte a=0;

void setup() {
  Serial.begin(9600);
 
  asm (
    "ldi r23, 37  \n"
    "sts (a), r23 \n"
  );
 
  Serial.print("a = "); Serial.println(a);
}

ここでは37という数値を23番レジスタに格納し、それを変数aに代入しています。なんとなく、雰囲気で分かっちゃいますね(といって説明を省こうとする布石だったりする)。

Arduinoには次の3種類のメモリが用意されています。

  • EEPROM
  • SRAM
  • フラッシュメモリ

1つ目はEEPROM、電源を切ってもデータが消えないメモリです。2つ目がSRAMで、データや一時変数などの値が置かれるメモリになります。3つ目がフラッシュメモリ、これはArduinoで作ったプログラムを置く所ですね。

上で書いたレジスタはSRAMの一部として実装されています。Arduinoには32個のレジスタ(r0〜r31)があり、それぞれ8bitの値を格納することが出来ます。

f:id:nn_hokuson:20170911231349j:plain

上のプログラムではLDI命令(Load Immediate)を使っています。LDIは定数をレジスタに格納する命令です。ここでは37という定数を、23番レジスタに格納しています。LDI命令で指定できるレジスタはr16〜r31までなので注意してください。

アセンブラの2行目ではSTS命令(Store direct to data Space)を使っています。STS命令はレジスタの値をSRAMに置かれている変数に代入する命令です。ここでは23番レジスタに入っている値を変数aに代入しています。

まとめると、アセンブラを実行すると

  1. 37という値が23番レジスタに格納され
  2. 23番レジスタの値が変数aに代入されます

したがって、最後にシリアルコンソールに表示される値は37になります。

37

今回、変数aはグローバル変数として宣言しました。これは変数の値をSRAM内に置くためです。もしローカル変数としてsetup関数の中で宣言すると、SRAMには置かれない(stackに配置される)ため、STS命令でエラーが出てしまいます。

Clobber(クラバー)を使って安全にレジスタを使おう

先程のプログラムでは、37という値を格納するレジスタを「えいや」で23番レジスタと決めました。もし、運悪くこの23番レジスタが使われていた場合は、そこにあった数値は37で上書きされて消えてしまいます。

このようなレジスタのバッティングを防ぐため、Clobberという構文があります。Clobberで使用するレジスタを宣言しておくと、コンパイラに「このレジスタは使わないでね〜」と伝えることが出来ます。

クローバの書き方が少し特殊で次のようになります。

volatile byte a=0;

void setup() {
  Serial.begin(9600);
 
  asm (
    "ldi r23, 37  \n"
    "sts (a), r23 \n"
    :::"r23"
  );
 
  Serial.print("a = "); Serial.println(a);
}

コロン3つに続けて、使用するレジスタの名前を文字列として記述します。ここでは23番レジスタを使うので"r23"と書いています。

コロン3つってなんやねん、となりますよね?。私もなりました。このあたりで一般的なインラインアセンブラの書き方を紹介しておきます。インラインアセンブラは次の4つのパートから成り立っています。

asm (
  "プログラム"
  "プログラム"
  ・・・・・
  :"出力":"入力":"Clobber"
);

プログラムのパートはこれまで書いてきたアセンブラプログラムになります。「出力」と「入力」のパートは後ほど説明しますが、簡単に言うと「インラインアセンブラに入力する値のリストと出力する値を書く場所」になります(そのままだ・・・)。入出力のレジスタ宣言に続けて、最後にClobberを書きます。

上の例では出力と入力のパートに何も記述していないので「:::"r23"」という不思議な書き方になってしまっているのです。

不思議な書き方ですが、このように書くことで23番レジスタは自分のプログラムで使うと宣言し、安全にプログラムを実行することが出来ます。

2つの値をスワップしよう

最後に今回のまとめとして2つの値をスワップするプログラムを作ってみましょう。C言語で普通に書くと次のようなプログラムになります。

volatile byte a = 73;
volatile byte b = 41;
 
void setup() {
  Serial.begin(9600);
 
  byte tmp c;
  c = a;
  a = b;
  b = c;
     
  Serial.println(a);
  Serial.println(b);
}

一時変数cを使って値を入れ替えているだけです。なんというか、めっちゃオーソドックス、普通中の普通のプログラムですね。

実行すると次のように変数aと変数bの値が入れ替わって表示されます。

41
73

これをインラインアセンブラで書き換えてみましょう。

volatile byte a = 73;
volatile byte b = 41;
 
void setup() {
  Serial.begin(9600);
 
  asm (
    "lds r17, (a) \n"
    "lds r19, (b) \n"
    "sts (b), r17 \n"
    "sts (a), r19 \n"
    : : : "r17", "r19"
  );
 
  Serial.println(a);
  Serial.println(b);
}

このプログラムでは、まず変数の値をレジスタに格納するためにLDS命令を使っています。LDS命令はLoad Direct from data spaceの略で、SRAMに置かれたデータをレジスタに読み込みます。LDI命令(値をレジスタに読み込む)とは異なるので注意してください。

  • LDI命令:数値をレジスタに読み込む
  • LDS命令:変数の値をレジスタに読み込む

aと変数bの値を17番と19番のレジスタに格納してから,STS命令を使ってレジスタの値を変数に書き戻しています。

このプログラムでは17番レジスタと19番レジスタを使っているので、クローバには"r17"と"r19"を指定しています。このように複数のレジスタを使う場合には「,」で区切ってレジスタ番号を書きます。

まとめ

今回はインラインアセンブラの書き方やレジスタの役割、レジスタに値を格納する方法、クローバの意味などを説明しました。次回はインラインアセンブラへの入出力について説明していきます。

[asin:B00E1EA6XM:detail]