Arduinoを卒業しavr-gccで電子オルゴールを作って遊んでいたが、機能を盛ろうとしたところ処理性能の壁にぶちあたったのでアセンブリを始めることにした
アセンブラの導入
アセンブラとはアセンブリ言語を機械語に翻訳するコンパイラのこと
avr-gccみたいなもん
avr-gccと同じくhexファイルを吐き出せればいつも通りavrdudeやmicronucleusで書き込むことができる
ところで今回のハードウェアはmicronucleusを導入したATTiny85である
詳しくはこちら
Microchip Studio
WindowsではMicrochip純正のIDEがあるようでこれを使うのが一番という話ではある
試しにインストールしたところVisualStudioやPICのコンパイラのインストールが始まった
流石に余分なものばかり入れられると困るしそもそも私はアンチIDE教信者なので、avr-gccと同じように純粋なコマンドラインツールを探すことにした
もちろんこの後徹底的にアンインストールした
AVRA
Microchip Studioの次にメジャーなアセンブラ
Microchip Studioでのアセンブリに対応しているとのこと
Windows以外にもMac、Linuxにも対応している模様
ただし実行ファイルは配布されていないので自分でmakeする必要がある
Windows環境でのmakeにつまずいたので別記事にまとめた
好みに合っていそうなのでひとまずこれを使う
アセンブリ
アセンブリはコマンド本体(二―モック)とその引数を書き連ねることでプログラムを作成する
二―モックについては本体のデータシートとavr命令一覧を見ればだいたい理解できる
細かな仕様はavraがエラーを吐いてくれたので言われたら直す程度でも良いかも
どちらも調べればpdfで出てくる
ところでレジスタの操作が必須になるわけだがこれはC言語も同じなので詳しい説明は省略
例がないとどうにも説明し辛いのでとりあえず進む
LEDの点灯
.include "tn85def.inc"
.cseg
rjmp main
main:
ldi r16,0b00000010
out DDRB,r16
ldi r16,0b00000010
out PORTB,r16
命令は基本的に上から順に実行される
.~~
は疑似命令と呼ばれるもので二―モックと違って直接機械語に翻訳されるものではない
C言語のマクロみたいな感覚
~~:
はラベルと呼ばれるものでこれが書かれた位置のメモリの番地を表す
メモリの相対座標が必要な命令などに数字の代わりにラベル名を入れるとコンパイル時に置き換えてくれる
C言語の関数と違って飽くまでもプログラム上の場所のみを表していることに注意
.include
はファイル読み込みの疑似命令
アセンブリではincludeの略で.inc
が定番らしい
tn85def.inc
はATTiny85のレジスタの番地などが書かれているようでこれはavraに付属している
.cseg
はプログラム領域の開始の疑似命令
いまいちよく分かっていないがこれは書かなきゃいけないようなので書いた
ここからプログラム本体を書き始める
rjmp
は実行場所を指定した場所に移動する命令
rjmp main
はmainラベルの場所まで移動する
つまりこのプログラムではまったく意味をなしていないが(ラベルは処理自体には影響しない)
これはmicronucleusが先頭にrjmp
が見つからないとエラーを吐いたのでつけたもの
電源を投入すると0番地から実行されるが、割り込みを有効にすると実行が先頭にまとまった対応した番地に勝手に移動する
つまり先頭の数行は割り込みが飛んでくる可能性があるわけで、基本的にすべてrjmp
にしておくのが普通ということだろう
ldi
はレジスタへの指定した数(即値)の代入
out
はIOレジスタへの代入
本当はoutiのようなIOレジスタに即値代入できる命令があれば嬉しいのだがそんなものはなかった
r16
は16番汎用レジスタのこと
レジスタはCPU内にある「メモリ」より高速なメモリであり数値の計算や受け渡しに使う
このうち汎用レジスタはユーザーが勝手に使えるレジスタで今回使うATTiny85の場合8bit*32個用意されていてr0
~r31
の名前が振られている
このうち最後の6個はメモリの番地指定に特化した用途で最初の16個は即値の代入ができない
つまり何も考えずに使えるのはr16
~r25
の10個のみである
ということでIOレジスタに即値を代入する動作を実現するためにldi
でr16
に即値を代入してout
でr16
の値をIOレジスタに代入している
これでPB1に繋がっているLEDが点灯する
Lチカ
Lチカには指定時間待つ動作が必要
Arduinoでは指定した時間だけ待つ関数が存在するが、アセンブリにそんな便利なものはない
方法としてはタイマー使う方法と使わない方法で大きく二通り、タイマーを使う方法では割り込みを使う方法と使わない方法で二通りありそうだが、今回はタイマーを使わない方法で行く
各命令はそれぞれ決まったクロックだけ時間を消費する
つまり上手く命令を組み合わせることで時間を消費することができる
アセンブリにも関数のような機能は存在するのでそれを使う
.include "tn85def.inc"
.cseg
rcall delay10us ;3
delay10us:;3+1+1+1+((1+2)*52-1)+4=165clk=10us@16.5MHz
nop ;1
nop ;1
ldi r20,52 ; 1
_l:
dec r20 ;1
brne _l ;!Z?2:1
ret ;4
;~~
はコメントで書いてある数字は消費するクロック数
rcall
でサブルーチン(関数)に入ってret
で戻る
nop
は何もしないをする命令
dec
はデクリメント
brne
は不一致で条件分岐
ステータスレジスタという特殊なレジスタがあり、これは前の命令の結果を持っている
各ビットに真偽値で保持しているわけだが、ここにゼロフラグ(Z
)というものがある
これは直前の命令の結果が0だったか否かを保持していてbrne
はここを見て分岐するか否かを判断する
つまり今回はdec
を実行したときに結果が0でなければ戻るということである
これらの処理のクロック数を合計すると165クロックとなり、これは16.5MHzで10usに相当する
同じ要領でこれを繰り返せば任意の遅延が作れるが面倒なので
を使ってLチカを作る
.include "tn85def.inc"
.cseg
rjmp setup
setup:
ldi r16,0b00000010
out DDRB,r16
loop:
ldi r16,0b00000010
out PORTB,r16
rcall delay500
ldi r16,0b00000000
out PORTB,r16
rcall delay500
rjmp loop
delay500:
ldi r20,42
ldi r21,219
ldi r22,41
_delay500: dec r22
brne _delay500
dec r21
brne _delay500
dec r20
brne _delay500
rjmp PC+1
ret
おしまい
このLチカをコンパイルすると42Bになります
無駄がないというのは気分がいいですね
続く?