アセンブリでLEDを点灯させる【後編】

後編ということで、前回の続きをだらだらと書きたいと思う。
2つのLEDを交互に点灯させたい、ということなので一定間隔で点灯させるために、ディレイ処理を行う必要がある。
C言語であれば_delay_ms関数が使えるのでかなり簡単に実装できるが、アセンブリとなるとクロック周期をカウントしていく必要があるようだ。

指定秒数待機するルーチンを実装する

いろいろと参考サイトを見ていくと、下記のような実装が一番シンプルなようだ。
AVRŽŽ—p‹L-assembly

delay1s:
ldi  r16, 100
mov  r2,  r16
dly2:
ldi  r16, 100
mov  r1,  r16
dly1:
ldi  r16, 200
mov  r0,  r16
dly0:
nop
dec  r0
brne dly0
dec  r1
brne dly1
dec  r2
brne dly2
ret

最初にこれを見せられたらちんぷんかんぷんである。
まず上から順に実行されていくので、r2に100、r1に100、r0に200とレジスタにそれぞれの値が入っていく模様。

NOP命令「無操作」と書いてある。何もせずに1クロック消費するということだ。

DEC命令「汎用レジスタを減少」と書いてある。要はデクリメント(-1減算)ということみたいだ。

BRNE命令「不一致で分岐」と書いてある。0の時に実行されるラベルを指定すると、そこにジャンプするようだ。

RET命令「サブルーチンからの復帰」と書いてある。サブルーチンとして呼び出された場合に、呼び出し元にジャンプするようだ。

これでだいたい分かると思うが、r2で100ループ、r1で100ループ、r0で200ループと、子ルーチンが呼び出されていく。
nop、dec r0、で2クロック、brne dly0の呼び出しは、条件成立時は2、不成立時は1と変化するようだ。ほとんど成立して動作するので、2クロックとカウントしていいと思う。

4 * 100 * 100 * 200 = 8000000 = 8Mhz となる。

解説しているサイトによると、内側のループ処理に5クロックかかるので、

5 * 100 * 100 = 50000 = 50Khz となるようだ。1%未満の誤差ようなのでおよそ1秒となる。

また、r0〜r15までは、使える命令が限定されている。r16~r31については何でも使えるようで使い分けが重要みたいだ。
例えば、LDI命令はr16以上でしか使えないので、一度r16を経由させたあとにMOV命令でr0に値をコピーしている。

待機処理を入れてLEDを交互に点灯させる

先ほどの待機ルーチンを使ってみて以下のように書いてみた。

.include "tn2313def.inc"
main:
ldi   r16, 0b00011000
out   DDRD, r16
ldi   r16, 0b00010000
out   PORTD, r16
rcall delay1s
ldi   r16, 0b00001000
out   PORTD, r16
rcall delay1s
rjmp  main
delay1s:
ldi  r16, 100
mov  r2,  r16
dly2:
ldi  r16, 100
mov  r1,  r16
dly1:
ldi  r16, 200
mov  r0,  r16
dly0:
nop
dec  r0
brne dly0
dec  r1
brne dly1
dec  r2
brne dly2
ret

RCALL命令「PC相対サブルーチン呼び出し」と書いてある。そのままの意味でサブルーチンの呼び出しである。
さっそくマイコンに転送してみたところ、非常にゆっくり点灯しているようだ…。
なぜ…という感じだが、クロック周波数がおかしいのかもしれない。

マイコンに設定されいてるクロック周波数を確認してみることにした。

マイコンのクロック周波数を確認する

「CKDIV8」というワードが重要なようだ。
調べてみると、CPUクロックの分周比を設定します、と書いてある。

マイコンに設定されているヒューズ情報を確認する必要があるので、以下のコマンドを実行してみた。

# 対話モードに入る
$ avrdude -c avrispmkii -P usb -p attiny2313 -t
$ read lfuse
0000   64
$ read hfuse
0000   df

これだけでは意味がさっぱりである。
ヒューズビットの意味を調べる必要があるので、以下のデータシートを確認してみた。
http://www.avr.jp/user/DS/PDF/tiny2313.pdf

lfuseは、ヒューズの下位ビット。hfuseは、上位ビットとなる。
「CKDIV8」が設定されているのは、下位ビットのほうになる。

ヒューズビットについては以下のサイトが参考になるようだ。
◆ヒューズビット

要は、7ビット目が「0」になっていると、クロックが1/8になってしまうらしい…なんてこったい。
初期設定では全てこのようになっているとのことだ。
設定されている値をビット値に変えてみると…

64 = 0110 0100

7ビット目が0である…そういうことか。こいつを1に書き換えてやる必要がある。

1110 0100 = e4

16進数でe4という値に書き換えることで、1/8動作を変更できる。
以下のコマンドを実行して書き換えてみたところ無事に正常な動作となった。
なお、ヒューズの書き換えは失敗すると、動作しなくなってしまう場合もあるようなので注意して欲しい。

$ avrdude -c avrispmkII -P usb -p t2313 -U lfuse:w:0xe4:m

あと、こんなサイトも見つけた。
マイコンの種類を選択するとWeb上で適切な値を表示してくれるようだ。便利そう。
Engbedded AVR Fuse Calculator

今回利用したコードもGitHubにアップしてみた。
pontago/avr-LedTest-asm · GitHub

アセンブリでLEDを点灯させる【前編】

前回の記事で無事にLEDを点灯させることができた。
コードは全てC言語で書いたが、もう一つの方法としてアセンブリで書いてみることにした。
アセンブリなんてコードを見ただけでウンザリしてしまう。

でもやってみる前から諦めてもしょうがない。
この機会に少し調べてみることにした。

開発環境を整える

アセンブリコードをコンパイルするためのコマンドは、最初に導入したCrossPackAVRというものに含まれているavr-asコマンドを使うことができる。
しかし、このコマンドでコンパイルできるアセンブリはGasと言われるもので、
いわゆるよく知られている、アセンブリコードのそれとは違うらしい。

そこで今回は、avraというアセンブラを導入してみることにした。
AVRA Home Page

下記サイトを参考にしながらインストール。
のぅわんべたぁ|AVRアセンブリ

まず、最新版をダウンロード後に展開し、srcディレクトリの中で以下のコマンドを実行してみた。
参考通りに上手くいかず、automakeの部分でコケてしまったので、touchコマンドでエラーが出るファイルを作ってみたところ成功。

$ touch NEWS README AUTHORS ChangeLog
$ aclocal
$ autoconf
$ automake -a
$ ./configure --prefix=/opt/local
$ sudo make install

これで開発するための環境は用意できた。
また、includesディレクトリに.inc拡張子のついたファイル群が入っているようで、マイコンの種類に応じた定義ファイルが格納されているようだ。
レジスタやポートに応じたアドレス、値がマッピングされているようで、初めにインクルードして使う感じだと思う。

LEDを点滅させる

なんと言っても最初はLEDを点滅させるところからだろう。
マイコンにはLEDを2つ接続しているが、手始めに一つ点灯させてみる。

.include "tn2313def.inc"
main:
ldi   r16, 0b00011000
out   DDRD, r16
ldi   r16, 0b00001000
out   PORTD, r16
rjmp  main

なんとこれだけである…実に簡単だ。

まず、.includeで定義ファイルをインクルード。コロンが付く行はラベルなので解説は不要だと思う。
ちなみにドットから始まるものは擬似命令と呼ばれるもので機械語に変換されることはないらしい。
続いて、各命令について調べてみた。

使える命令セットは以下のURLからダウンロード出来る模様。
http://www.avr.jp/user/DS/PDF/AVRinst.pdf

LDI命令は「即値バイト定数を汎用レジスタに取得」と書いてある。
レジスタr16に右オペランドの値を入れると考えればいいと思う。

OUT命令は「汎用レジスタからI/Oレジスタに設定」と書いてあった。
そのままで、I/Oレジスタに汎用レジスタの値を設定するときに使う命令のようだ。

RJMP命令は「PC相対無条件分岐」と書いてある。
要は無条件でこの場所に飛ばすってことみたいだ。今回の場合はmainラベルに飛ばすので、ループさせるということになる。
いわゆるGOTOみたいな感じだろう。

DDRDや「0b00011000」などの値は、前回の記事で説明していたと思うので今回は省く。
上のコードをavraコマンドでコンパイルしてみた。

$ avra main.asm
AVRA: advanced AVR macro assembler Version 1.3.0 Build 1 (8 May 2010)
Copyright (C) 1998-2010. Check out README file for more info
AVRA is an open source assembler for Atmel AVR microcontroller family
It can be used as a replacement of 'AVRASM32.EXE' the original assembler
shipped with AVR Studio. We do not guarantee full compatibility for avra.
AVRA comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of avra under the terms
of the GNU General Public License.
For more information about these matters, see the files named COPYING.
Pass 1...
Pass 2...
done
Used memory blocks:
Code      :  Start = 0x0000, End = 0x0004, Length = 0x0005
Assembly complete with no errors.
Segment usage:
Code      :         5 words (10 bytes)
Data      :         0 bytes
EEPROM    :         0 bytes

こんなメッセージが表示されて成功した。
最初にエラーが出たのだけど、tn2313def.incファイル内にある「#」から始まる行の先頭に「;」を付けたところエラーは消えた。

# vimでこんな感じに処理した
:%s/^#/;#/g

出来上がったmain.hexというファイルを開いてみたところ、かなり小さくてびっくりした。わずか63バイトである…。

:020000020000FC
:0A00000008E101BB08E002BBFBCFE2
:00000001FF

バイナリの転送はavrdudeを使って以下のようなコマンドを実行した。

$ avrdude -c avrispmkII -P usb -p t2313 -U flash:w:main.hex:i

簡単なものだけど一発で成功したので抵抗感も薄らいだ。

アセンブリと聞くだけで拒否反応を示してしまいたくなるが、命令セットを覚えていくことで少しずつ理解が深まっていき、単純なコードながら深さと楽しさがあるように思える。

長くなってしまいそうなので、次の記事で2つのLEDを一定間隔で切り替えて点灯させる、ということをやってみたいと思う。

今回書いたコードもGitHubに上げてみた。
pontago/avr-LedTest-asm · GitHub

参考サイト
アセンブラなんて簡単じゃないか(1/3) − @IT MONOist
解説 AVRアセンブラ講座 (1)|freeml byGMO