Edited at

ArduinoアセンブラでLチカ

More than 1 year has passed since last update.


なぜアセンブラ?

アセンブラを使うケースはあまりないとは思いますが、正確でスピードを要求される数百ナノ単位で厳しいスイッチングやクロック計測、アルゴリズムを高速化したいなどではArduino言語(C、C++)では対応できない場合があります。ArduinoUNOのクロックスピードは16Mhzですが、Arduino言語命令ではクロック数が多いので、目的に合わせて最適化できるアセンブラの出番となります。高級言語に低級言語を埋め込むのでインラインアセンブラといいます。


アセンブラとは?

アセンブラは処理を指定するオペランド(主命令)と処理対象を指定するオペコード(アドレスなどの引数)で構成されております。これらの、それぞれの記号のことをニーモニックと呼ぶ。機械語とアセンブラは1対1に対応し高速に動作する。レジスタは数値の記憶場所であり、マイコン内部にある高速なメモリのことで、フラッシュROMなどと比べると非常に少ないメモリとなります。アセンブラはレジスタをメモリやI/Oポートの演算(算術、論理、比較、分岐、シフト、入力命令)することが主となります。演算装置には、ALUと呼ばれる加算器、補数器が含まれており2つのレジスタを計算させる。


アドレッシング

アセンブラは命令(オペコード)とアドレス(オペランド)で構成されていますが、いくつかのアドレスの指定方法があります。レジスタやメモリなどは固有のアドレスが振られており、アドレスを元にしてデータが格納されている方法をアドレッシングと言います。アドレッシングには、下記の種類があります。

即値アドレス オペランドに値(データ)を使用する。(これを即値という)

直接アドレス 値が格納されている場所をアドレスで指定する。

間接アドレス 値が格納されているアドレスを格納されている場所をアドレスで指定する。

インデックスアドレス データを格納されている場所をインデックスレジスタに格納されている値とアドレスを演算して指定する。(配列を指定するのに有効)

ベースアドレス データを格納されている場所をベースレジスタに格納されている値とアドレスを演算して指定する。(メモリの先頭から指定するのに有効)

相対アドレス  データを格納されている場所を命令アドレスレジスタに格納されている値とアドレスを演算して指定する。(命令アドレスレジスタは、命令を読み出すために、次の命令が格納されたアドレス(プログラムカウンタ))


レジスタ

レジスタの値には高級言語ような型は存在しません。また、名前を決めることもできません。演算子などもなく、ひたすらアドレスやレジスタの値を計算や転送するだけ作業をさせることになります。レジスタはCPU内部にあり、レジスタには、CPUのレジスタである汎用レジスタ、SRAMのレジスタ、I/Oポートのレジスタなどがあり、汎用レジスタは自由に使え、処理が高速ですが、32個しかありません。同じ命令をさせる時、Arduino言語と比べると行数が増えます。アセンブラのデータサイズ単位はワードになります。AVRは、ハーバードアーキテクチャによりプログラム領域2ワード(2バイト)とデータ領域1ワード(1バイト)の空間が区別されておりそれぞれアドレスが異なる。アドレスと対応しているマイコン内部にあるバスは、プログラムメモリエリア16本とデータメモリエリア8本でバスの本数が合計24本があり、値のサイズは8ビット単位となる。メモリやレジスタは、下記のメモリ空間を持ちます。

ArduinoUNOは3種類のメモリがあります。パソコンとは違いこれらすべてワンチップマイコンに内臓されております。特徴としてデータエリアのアドレスにI/Oポートが割り振られている。


メモリ空間

レジスタと比べて低速ですが、大きな記憶領域を持っているのがメモリです。メモリの内容をコピーする命令をロード命令といい、レジスタの内容をコピーするのがストア命令となります。


プログラムメモリエリア (16ビット) 0x0000から0x0FFF

フラッシュメモリ(32KB)に書き込まれている。テキストエリアとも呼ぶ。速度は遅いが容量が大きくスケッチが書き込まれるメモリ。(書き込み後はロードしかできない。)起動後、SRAMに展開される。

他にもブートローダー(通常マイコンはライターが必要になりますが、Arduinoは、最初からブートローダが書き込みされております。安価なUSBシリアル通信でスケッチの書き換えができるプログラム)が書き込まれている。


データメモリエリア(8ビット)0x000から0x04FF

データメモリエリアには汎用レジスタ、I/Oレジスタ、SRAMの対してアドレスが割り振られている。


汎用レジスタ 0x000から0x001F

32個の汎用レジスタを持つ。R0からR31

R26,R27とR28,R29及びR30,R31の3組は、サイズが16ビットであるレジスタであり、インデックスレジスタとして利用出来る。


標準I/Oレジスタ 0x0020から0x005F

ポートレジスタ(PORTB,PORTC,PORTD)、CPUの処理結果が書き込まれる。


拡張I/Oレジスタ 0x0060から0x00FF

拡張I/Oレジスタは、EEPROMへアクセスするのに使われるレジスタ。

EEPROM(1KB)は、読み書き可能、電源を切っても消えないメモリ。10万回書き込みできるが、頻繁にデータが更新される用途には向かない。


内臓SRAM(2KB) 0x0100から0x08FF

レジスタ、プログラム、プログラムで使われるデータに使われる。

読み書き可能、電源を切ると消去されるメモリ。


メモリ領域

メモリには、グローバル変数などが記憶される静的領域、ローカル変数など動的に領域を確保するスタック領域、ヒープ領域などがあります。


プログラム領域

プログラム(スケッチ)が格納されている領域(フラッシュメモリ)


静的領域

静的領域は、グローバル変数,静的変数などプログラムが実行されるとサイズが変化しない領域。初期化されている変数と初期化されていない変数で領域が別れる。


スタック領域

スタック領域は、一時的に保存が必要なメモリ領域です。ローカル変数などに使われる。スタックポインタは、この仕組みを実現するためのレジスタです。この領域を指し示すアドレスが入っています。スタック領域のデータ構造は先出し後出しであり、アドレス番地が高い方から積み上がっていきます。使いすぎるとスタックオーバーフローが発生する。


ヒープ領域

ヒープ領域は、malloc,newなどで自由に確保できる領域。アドレスの低い方から確保される。データ構造はリストになる。解放し忘れるメモリリークには注意しなくてはいけない。


サブルーチン

サブルーチンは、まとまった処理を呼び出します。

サブルーチンと最初の呼び出し元をメインルーチンと呼びます。

サブルーチンの名前が付けられます。ラベルと言います。


書式

ラベル名:

無条件でサブルーチンを呼ぶには、

call 絶対アドレス

rcall 相対アドレス

ret サブルーチンからの復帰


インラインアセンブリの書式

インラインアセンブリの良いこところは、すべてアセンブリで書くより、必要なところだけアセンブラを使うことができる。必要に応じて/n/t(改行コード)が必要となる。アセンブラを記述するのは、asm();または、_asm_();関数に記述できる。また:で区切りを入れる。オペランドとオペコードには大文字や小文字は区別しません。また、アセンブラでの数値は基本アドレスですが、数値は即値という。


書式1


asm(オペコード オペランド

  :出力オペランド

  :入力オペランド);



1つの命令


sample

asm("オペコード オペランド");


※1つの命令のみは、/n/tは不要。


複数の命令

※行の終わりには/n/t必要ですが、asmの終わりには不要。


sample

asm("

オペコード1 オペランド1 /n/t
オペコード2 オペランド2 /n/t
");


書式2

オペランドの変数であるマクロ名とArduino言語の変数名を対応させます。

オペランド制約は、オペランドのアクセスを制限します。volatileは、コンパイラが最適化を防ぐ修飾子であり、出力されない無駄なアセンブラコードであると判断した場合削除をするのを防ぐためのものです。

asm volatile("オペコード オペランド[マクロA名]" "/n/t"

"オペコード オペランド[マクロB名]"

:[マクロA名]"オペラント制約"(変数名A)

:[マクロB名]"オペランド制約"(変数名B),

);


オペランド制約

Arduino言語とアセンブラ間で値を受け渡しするのにオペランドの値を決めます。

|オペランド制約|使用目的|値の範囲|

a Simple upper registers r16 to r23

b Base pointer registers pairs y, z

d Upper register r16 to r31

e Pointer register pairs x, y, z

q Stack pointer register

SPH:SPL r

Any register r0 to r31

t Temporary register r0

w Special upper register pairs r24, r26, r28, r30

x Pointer register pair X x (r27:r26)

y Pointer register pair Y y (r29:r28)

z Pointer register pair Z z (r31:r30)

G Floating point constant 0.0

I 6-bit positive integer constant 0 to 63

J 6-bit negative integer constant -63 to 0

K Integer constant 2

L Integer constant 0

l Lower registers r0 to r15

M 8-bit integer constant 0 to 255

N Integer constant -1

O Integer constant 8, 16, 24

P Integer constant 1

Q (GCC >= 4.2.x) A memory address based on Y or Z pointer with displacement.

R (GCC >= 4.3.x) Integer constant. -6 to 5


AVRのニーモニック

オペランド記号は上記のオペランド制約に準拠します。rは汎用レジスタとなります。各レジスタ間で使う命令が異なり、命令実行時間であるクロック数も違います。キャリーとは、桁あふれのことです。ldsは下記のコードで使われています。

|オペコード|オペランド| 

add r,r 算術論理演算 汎用レジスタの加算

and r,r 論理演算 汎用レジスタの論理積

asr r ビット移動 右移動し、ビット1はキャリーフラグに移動。

bclr I

bld r,I ビット移動 特別レジスタSREG Tフラグを汎用レジスタのビットIに転送。

brbc I,label

brbs I,label

bset I

bst r,I ビット移動 汎用レジスタrのビットIを特別レジスタSREG Tフラグへ転送。

cbi I,I ビットクリア Iの標準I/OをビットIをクリア(コードゼロ)する。

cbr d,I

com r

cp r,r 分岐命令 汎用レジスタ間rとrにおいて大小関係 前者r<後者rであれば次の行を実行される。

cdc r,r

cpi d,M

cpse r,r 分岐命令 汎用レジスタ間においてrとrが等しければ1行スキップします。等くなければ次の行が実行されます。

dec r 算術論理演算 汎用レジスタのデクリメント。値が1引き算される。

elpm t,z

eor r,r 汎用レジスタ間rとrで排他的論理和を行う。その演算結果は前者へ

in r,I データ転送命令 1バイトの標準I/Oレジスタの内容を汎用レジスタに転送する。

inc r 算術論理演算 汎用レジスタのインクリメント。値が1加算される。

ld r,e データ転送 1バイトのデータメモリを汎用レジスタに転送。

ldd r,b

ldi d,M データ転送 1バイトの定数を汎用I/Oレジスタへ代入する。

lds r,label データ転送 1バイトの拡張レジスタから汎用レジスタへ転送する。

lpm t,z

lsl r ビット移動 汎用レジスタを1つ左に移動する。

lsr r ビット移動 汎用レジスタを1つ右に移動する。

mov r,r データ転送命令 汎用I/Oレジスタの内容を転送する。

movw r,r データ転送命令 汎用レジスタ間において、連続する2バイトの汎用レジスタを2バイトの汎用レジスタへ転送。

mul r,r 汎用レジスタ間でに乗算させる。

neg r

or r,r 汎用レジスタ間で論理和で計算する。結果は前者rへ

ori d,M  汎用レジスタと定数Mを論理和を行う。結果はdへ

out I,r データ転送命令 1バイトの汎用レジスタの内容を標準I/Oレジスタに転送する。

pop r  データ転送命令 1バイトの標準I/Oレジスタの内容を汎用レジスタに転送する。

push r データ転送命令 1バイトの汎用レジスタからスタック領域へ転送する。

rol r ビット移動命令 7ビット目は、キャリーフラグ移動し左移動し、7ビット目は1ビット目に入る(左回転)。

ror r ビット移動命令 1ビット目は、キャリーフラグ移動し右移動し、1ビット目は7ビット目に入る(右回転)。

sbc r,r

sbci d,M

sbi I,I ビットセット Iの標準I/OレジスタをビットIにセット(コード1)する。

sbic I,I

sbiw w,I

sbr d,M

sbrc r,I

sbrs r,I

ser d ビットクリア すべてのビットをコード1にする。

st e,r データ転送 1バイトの汎用レジスタからデータメモリへ転送する。

std b,r

sts label,r データ転送 1バイトの汎用レジスタから拡張I/Oレジスタ(データメモリ(アドレス指定))へ転送する。

sub r,r 算術論理演算 汎用レジスタにrとrおいて引き算させる。

subi d,M 算術論理演算 汎用レジスタにrとM(定数)おいて引き算する。定数は0から255になる。

swap r 汎用レジスタの上下4ビットを交換する。(ビックエンディアン⇄リトルエンディアンに変換)


その他

nop 1クロック分、何もしない。UNOは、16MHzですので1クロック0.0625usになります。

sleep スリープモードに入り省電力モードへ入ります。

brne ラベル名 不一致で分岐


制約修飾子

オペランドの読み書きの許可を管理します。


読み出し専用

= Write-only operand, usually used for all output operands.


読み書きオペランド

+ Read-write operand


出力専用オペランド

& Register should be used for output only


特別レジスタ


ステータスレジスタ(命令結果によってビットが変わります。)

SREG Status register at address 0x3F


スタックポインタ(SP)スタック領域を指す。

SP_H Stack pointer high byte at address 0x3E

SP_L Stack pointer low byte at address 0x3D

tmp_reg Register r0, used for temporary storage

zero_reg Register r1, always zero


Lチカ

ArduinoUNOでLEDを点滅させる。DigitalPin2にLEDと抵抗(470Ω程度)を接続する。デジタルピン0から8は、入出力DDRD,01設定はPORTDが対応します。1秒何もしないdelay(1000)などの命令がありませんので、nopを実行する4クロックのサブルーチンを作り、扱える定数は255回なのでサブルーチンからサブルーチンを呼び出します。

(0.0625us * 4) ((255 * 255 * 60) + (255*60) + (255*60)) = 0.97sとなります。


LED_Flashing.ino

volatile uint8_t *port;

uint8_t High,Low,Mask;
uint8_t PinNumber = 2;

void setup() {

DDRD = 0x04;
PORTD = 0x00;

port = portOutputRegister(digitalPinToPort(PinNumber));
Mask = digitalPinToBitMask(PinNumber);
High = *port | Mask;
Low = *port | ~Mask;
ledFlashing();
}

void loop() {

}

void ledFlashing(){

asm volatile(
"LEDS:" "\n\t" //LEDS
"st %a[port], %[High]" "\n\t" //I/Oポートに転送
"rcall delay1000m" "\n\t" //1秒待ち
"st %a[port], %[Low]" "\n\t" //I/Oポートに転送
"rcall delay1000m" "\n\t" //1秒待ち
"rjmp LEDS" "\n\t" //繰り返し

"delay1000m:" "\n\t" //サブルーチン
"ldi R18,60" "\n\t" //汎用レジスタに定数60転送
"delay52:" "\n\t" //
"rcall delay1" "\n\t" //delay1を呼ぶ
"dec r18" "\n\t" //R18をデクリメント
"brne delay52" "\n\t" //R18が0でなければdelay52を呼ぶ
"ret" "\n\t" //復帰

"delay1:" "\n\t" //サブルーチン
"ldi r19,255" "\n\t" //R19に定数255を転送
"delay12:" "\n\t" //
"rcall delay4" "\n\t" //delay4へ
"dec r19" "\n\t" //R19をデクリメント
"brne delay12" "\n\t" //R19が0でなければdelay12
"ret" "\n\t" //復帰

"delay4:" "\n\t" //サブルーチン 4クロック分
"ldi r20,255" "\n\t" //R20に定数255を転送
"delay42:" "\n\t" //
"nop" "\n\t" //1クロック何もしない。
"dec r20" "\n\t" //R20をデクリメント
"brne delay42" "\n\t" //
"ret" //復帰
:[port]"+e"(port)
:[High]"r"(High),
[Low]"r"(Low)
);
}



実行結果

LEDs.JPG


応用例

アセンブラで書くと、Arduino UNOでもフルカラーLEDの定番WS2812B制御できちゃいます。このフルカラーLEDはとても美しく、配線もすくなく済み、高輝度でお勧めです。


LED_ws2812b.ino


volatile uint8_t *port;
uint8_t High,Low,Mask;
uint8_t PinNumber = 2;

void setup() {

DDRD = 0x04;//ポートの入出力を決める。PD0~P7ArduinoではD0~D7
PORTD = 0x00;//ポートのレジスタ。

port = portOutputRegister(digitalPinToPort(PinNumber));
Mask = digitalPinToBitMask(PinNumber);
High = *port | Mask;
Low = *port | ~Mask;
ledFlashing();
}

void loop() {

}

void ledFlashing(){

asm volatile(
"LEDS:" "\n\t"
"ldi r16,64" "\n\t"

"ledset:"
//緑
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
//赤
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
"rcall LED_t1" "\n\t"
//青
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"
"rcall LED_t0" "\n\t"

"dec r16" "\n\t"
"brne ledset" "\n\t"
"rcall RET" "\n\t"
"rcall RET" "\n\t"
"rcall RET" "\n\t"
"rcall RET" "\n\t"
"rcall RET" "\n\t"
"rcall RET" "\n\t"

//コードゼロ
"LED_t0:" "\n\t"
"st %a[port], %[High]" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"st %a[port], %[Low]" "\n\t" //I/Oポートに転送
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"ret" "\n\t" //復帰
//コードワン
"LED_t1:" "\n\t"
"st %a[port], %[High]" "\n\t" //I/Oポートに転送
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"st %a[port], %[Low]" "\n\t" //I/Oポートに転送
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"nop" "\n\t"
"ret" "\n\t" //復帰

//リセット
"RET:" " \n\t"
"ldi r20,255" "\n\t" //R20に定数255を転送
"st %a[port], %[Low]" "\n\t"
"delay:" "\n\t" //
"nop" "\n\t" //1クロック何もしない。
"dec r20" "\n\t" //R20をデクリメント
"brne delay" "\n\t" //
"ret" //復帰

:[port]"+e"(port)
:[High]"r"(High),
[Low]"r"(Low)
);
}



実行結果


Reference