C++は甘え
はじめに
高専の授業でArduinoを使うことがあった。普通ならC++とArduino IDEで適当に書くと思う。
この授業とは関係のない別の教員が「楽しちゃダメだから」と言っていたこともあり、楽せずアセンブリ言語を用いてArduinoのプログラムを書いてみることにする。
今回は授業内で利用していたArduino UNO R3を対象とする。
本記事ではLチカを例としてアセンブルと書き込み方法を説明したのち、命令やアセンブラの疑似命令を紹介する。不明瞭な点についてもできるだけ明瞭に書いているつもり。
アセンブラーと書き込みツール
アセンブラーはavraを利用する。
書き込みツールはavrdudeを利用する。
aptであれば以下のコマンドでインストール可能。
$ sudo apt install avra
$ sudo apt install avrdude
pacman+AURであれば以下のコマンドでインストールできる。avraはAUR、avraはpacmanにある。
$ yay -S avra avrdude
こんな感じ
https://avr.jp/ にある命令手引書を見ながら書いていく。すべて日本語になっているので非常にありがたい。
まずは内蔵LEDを点灯させるコードを示す。結果だけ言えばこのようなコードになるが、その前にPINBなどのレジスタについて解説する。
.equ DDRB = 0x04 ;DDRBを0x04と定義
.equ PORTB = 0x05 ;PORTBを0x05と定義
.org 0x0000 ; 0x0000番地に配置
jmp main ; mainへジャンプ
main:
sbi DDRB, 5 ;D13を出力に指定
loop:
sbi PORTB, 5 ;D13にHIGHを出力
rjmp loop ;メインループ
I/Oレジスタと入出力端子の関係
Arduino IDE等でDDRxやPORTx, PINxなどの意味が理解できる人は、それと完全に同じなので読み飛ばしてもらって構わない。
一応簡単な解説をする。
レジスタとArduino上のピンの関係
レジスタと役割の対応表は以下の通りになる。xにはB,C,Dのいずれかが入る。
key | value |
---|---|
DDRx | 入力か出力かを決定 (1にすると出力) |
PORTx | 入力値になる |
PINx | 出力値を書き込む |
DDRxの各ビットを0にしたうえで、PORTxに1を出力するとプルアップ抵抗ありの入力(INPUT_PULLUP
)になる。
また、Arduino上の入出力ピンとレジスタの関係は以下の通りになる。黄色背景に書かれているのがピンとレジスタの関係になる。この図はArduino公式サイトから見ることができる。
Arduino公式のピンアウト
例えばD2にHIGHを出力する場合、DDRDの3ビット目[1]を1にして、PORTDの3ビット目を1にすればよい。
C言語で書くと以下のような感じ。
pinMode(2, OUTPUT);
digitalWrite(2, HIGH);
// 上下は等価コード
DDRD |= 0b100;
PORTD |= 0b100;
レジスタをCPUから操作する
前述したようなDDRx
などのレジスタにアクセスする方法を説明する。
まずはCPU上に配置されたアドレスとの対応表を示す。
レジスタ | CPU上のアドレス |
---|---|
PINB | 0x03 |
DDRB | 0x04 |
PORTB | 0x05 |
PINC | 0x06 |
DDRC | 0x07 |
PORTC | 0x08 |
PIND | 0x09 |
DDRD | 0x0a |
PORTD | 0x0b |
Lチカを理解する
I/Oポートを理解できれば、Lチカはちょろい。
とりあえずもう一度内蔵LEDを光らせるプログラムを示す。
.equ DDRB = 0x04 ;DDRBを0x04と定義
.equ PORTB = 0x05 ;PORTBを0x05と定義
.org 0x0000 ; 0x0000番地に配置
jmp main ; mainへジャンプ
main:
sbi DDRB, 5 ;D13を出力に指定
loop:
sbi PORTB, 5 ;D13にHIGHを出力
rjmp loop ;メインループ
アセンブラの機能
main:
のようなものはラベルと言い、ジャンプ命令などのジャンプ先に利用できる。
このようにすることでアセンブラーがラベルに指定したジャンプ先などのアドレスを自動計算してくれる。これだけでだいぶ革命だと思う。
ピリオドから始まる命令はアセンブラ疑似命令と呼ぶ。
.equ
は定数を定義する。C言語でいう#define
的なものだが、定数のみを定義できる(式は定義できない)。ちなみにAVRアセンブラにも#define
はある[2]。これは式も定義できる。
CPUの命令
普通に書いてある命令はCPUの命令になる。機械語と1対1になったニーモニックと呼ばれる記法で書く。
「AVR命令一式手引書」に命令とニーモニックの詳細がある。
sbi 0x03, 5
cbi 0x03, 5
sbi
命令はI/Oレジスタの指定ビットを1にする命令。
sbi 0x03, 5
と書けばアドレス0x03のI/Oレジスタの6ビット目のビットを立てるということになる。
cbi
命令もある。これはsbi
の逆で、指定されたI/Oレジスタの指定ビットを0にする命令。
main:
jmp main
rjmp main
jmp
命令は無条件ジャンプの命令。メモリ上のどこにでもジャンプできるが、命令長が4バイトで、命令の実行時間が長い。
rjmp
命令は、その命令が実行されている番地から-2048~2047語[3]以内の無条件ジャンプになる。この命令は命令長が2バイトで、命令の実行時間が短い。近い命令にジャンプするならこっちの方がおすすめ。
どちらのジャンプ命令もラベルを書くだけでアセンブラーが良い感じにやってくれる。
Lチカを実行する
上の説明は必要になったら読めば問題ない。とりあえず実行してみる。
何回か見せているLED.asm
をコピペして適当なフォルダに置く。
以下のコマンドでアセンブル[4]ができる。LED.hex
が機械語のプログラムになる。
また、LED.lst
が出力される。これはアセンブルリストと呼び、アセンブリと実際に変換された機械語の対応が書かれている(実際に計算されたジャンプ先アドレスなども含まれる)。
$ avra -fI LED.asm -l LED.lst
以下のコマンドでプログラムをArduinoに書き込むことができる。
$ avrdude -c arduino -P シリアルポート -b 115200 -p atmega328p -D -U flash:w:LED.hex:i
シリアルポートの確認
シリアルポートはlsコマンドで調べられるが、筆者の環境ではなんかたくさん出てきたが、探すのがめんどくさいならArduino IDEから確認することができる。この画像では/dev/ttyACM0
がシリアルポートの値になる。
実行するとしっかり内蔵LEDが光っていることがわかる。
アセンブラでLチカ
いろいろな命令
以下に使いそうな命令を示す。
-
Rd
,Rr
は汎用レジスタを示す。K
は即値、k
は命令へのアドレス(絶対または相対)を示す。 -
b
はビットを示す。一番右のビットを0で数える。図というかテキストで示すとこんな感じ。bit | 0 1 0 1 0 1 0 1 | b | 7 6 5 4 3 2 1 0 |
-
Rxyz
は16ビットレジスタ(X
,Y
,Z
)を指す。(これはこの記事のみで使用) -
Ryz
は16ビットレジスタ(Y
,Z
)を指す。(これはこの記事のみで使用)
データ転送
構文 | 動作 |
---|---|
mov Rd, Rr |
Rr の値をRd にコピー |
movw Rd, Rr |
R(r+1):Rr の値をR(d+1):Rd にコピー |
ld Rd, Rxyz |
X,Y,Zレジスタの指すメモリの値をRd に代入 |
ldd Rd, Ryz+q |
Y,Zレジスタの値 + q の指すメモリの値をRd に代入(q=0~63) |
ldi Rd, K |
即値K をRd に代入 |
out A , Rr |
Rr の値をI/OレジスタA に出力 |
in Rd, A |
I/OレジスタA の値をRr に入力 |
演算
構文 | 動作 |
---|---|
inc Rd |
Rd に1加算 |
dec Rd |
Rd から1減算 |
add Rd, Rr |
Rd + Rr の値をRd に代入 |
adc Rd, Rr |
Rd + Rr + キャリーフラグの値をRd に代入 |
sub Rd, Rr |
Rd - Rr の値をRd に代入 |
subi Rd, K |
Rd - 即値K の値をRd に代入 |
mul Rd, Rr |
Rd とRr の符号なし乗算値の上位8ビットをr1 、下位8ビットをr0 に代入 |
muls Rd, Rr |
Rd とRr の符号あり乗算値の上位8ビットをr1 、下位8ビットをr0 に代入 |
neg Rd |
Rd の2の補数をRd に代入 (0 - Rdを実行) |
ビット演算
eor
命令はレジスタの0クリアにも利用できる。
構文 | 動作 |
---|---|
and Rd, Rr |
Rd AND Rr の値をRd に代入 |
or Rd, Rr |
Rd OR Rr の値をRd に代入 |
eor Rd, Rr |
Rd XOR Rr の値をRd に代入 |
andi Rd, K |
Rd AND 即値K の値をRd に代入 |
ori Rd, K |
Rd OR 即値K の値をRd に代入 |
条件分岐命令1
この条件分岐命令は比較命令を行った後か、演算後に利用する
構文 | 動作 | 備考 |
---|---|---|
cp Rd, Rr |
Rd - Rr を演算しフラグを更新する(レジスタは更新されない) |
分岐命令の前に使用する |
cpi Rd, K |
Rd - 定数K を演算しフラグを更新する(レジスタは更新されない) |
分岐命令の前に使用する |
breq k |
ゼロフラグ=1ならkに相対分岐(-64 ~ +63語以内) |
Rd == Rr の時分岐 |
brne k |
ゼロフラグ=0ならkに相対分岐(-64 ~ +63語以内) |
Rd != Rr の時分岐 |
brge k |
符号フラグ=0ならkに相対分岐(-64 ~ +63語以内) |
Rd >= Rr の時分岐 |
brlt k |
符号フラグ=1ならkに相対分岐(-64 ~ +63語以内) |
Rd < Rr の時分岐 |
条件分岐命令2
構文 | 動作 |
---|---|
cpse Rd, Rr |
Rd == Rr の時、その次の命令をスキップ |
sbic A, b |
IOレジスタA のb ビットが0の時、その次の命令をスキップ |
sbis A, b |
IOレジスタA のb ビットが1の時、その次の命令をスキップ |
sbrc Rr, b |
Rr のb ビットが0の時、その次の命令をスキップ |
sbrs Rr, b |
Rr のb ビットが1の時、その次の命令をスキップ |
スキップがある条件分岐命令は以下のような形で使う。
cpse r0, r1 ; r0 == r1の時次の命令がスキップされる
jmp else
nop ; r0 == r1の時
jmp exit
else:
nop ; r0 != r1の時
exit:
; 続き
いろいろ
SP
はスタックポインタを表す。
構文 | 動作 |
---|---|
call k |
スタックに復帰アドレスを格納してk にジャンプ(サブルーチン呼び出し) |
ret |
スタックから取得した復帰アドレスにジャンプ(サブルーチンから戻る) |
push Rr |
Rr の値をスタックに代入してSP を1減算 |
pop Rr |
1加算したSP の示すスタックからRr に代入 |
nop |
何もしない |
マクロ疑似命令
関数にするほどでもないけどプログラムをまとめたい時にマクロを利用する。
マクロは以下のような形で書く
; ex: detect PORT, BIT
.macro detect
detect:
sbis @0, $1
rjmp detect ; if PORT is 0
; PORT is 1
.endmacro
@0
, @1
のように書くと呼び出し時のパラメータを作ることができる(最大10個)。
マクロを使用すると以下のようになる。
detect PINB, 2
; 上のように書くとアセンブラが下に置き換える
detect:
sbis PINB, 2
rjmp detect
書くときに便利なやつ
今まではPORTxなどのI/Oレジスタを自分で定義していたが、定義されたファイルがある。
これがArduino UNO R3に使われているATmega328Pの定義ファイルになる。ここにはI/Oレジスタのアドレスなどが定義されているので、使うと便利。
このファイルをプログラムで使うには、プログラムと同じフォルダに定義ファイルを置いて以下の疑似命令を書くと利用できる。
.nolist ; ここからアセンブルリストに含めない
.include "m328Pdef.inc"
.list ; ここからアセンブルリストに含める
最後に
シリアル通信などはよくわからなかったので記事では除いている。
また、Arduino UNO R4ではチップがルネサス製に変わっているが、デジタルI/O出力だけでもR3よりも複雑そうなので今後の学習課題にしたい。日本語のマニュアル(1600ページ以上)があるだけましだとは思う。
皆さんも楽しいアセンブラライフを!
-
一番右のビットを1ビット目として数える ↩︎
-
https://avr.jp/ にある「AVRアセンブラ使用者の手引き」を参照 ↩︎
-
1語は多分2バイト。 ↩︎
-
アセンブラがアセンブリを機械語に変換することをアセンブルと呼ぶ。 ↩︎
Discussion