SNSへはこちら

AVRでアセンブリ(1) - 導入とLチカ

皆さん、お久しぶりです。ココ最近鬼のように忙しかったのでブログ更新はおろか、マイコンにさえ触れられていないのですが、今日になって暇ができたのでホッとしているところです。

さて、本記事から新シリーズを始めたいと思います。またしても時代に逆行したネタ、AVR マイコンでアセンブリを楽しむことを目的にしたシリーズです。コード自体は大昔に全てできているので、楽な企画ですよ。

Motivation

ではなぜ AVR マイコンでアセンブるのか?について簡単にご説明します。
これまで僕は ARM マイコンでアセンブリをする とかいうアホなことをやっていましたが、いやはややりづらい、やりづらい
やっぱり、アセンブリが一番気軽にできるのは AVR ではないかと思ったわけです。
何故かと言うと、以下のような理由があります。

  • IO レジスタのアドレス値をレジスタにロードする必要がない
    • 単にビットをセット/クリアするだけなら、アクセスする命令を即値で記述できる
  • 軽量のマクロアセンブラが存在
  • Arduino ボードなら、シリアル通信等の結線がボード内で済んでいて楽

一番最初は大きなメリットです(実際に ARM 等でアセンブリを書いて面倒臭さを実感すれば分かる)。というかこれが気軽にできると述べている理由です。
ARM だったら、アドレス値を1命令内でロードできるものは存在せず、PC 相対でコードの末尾に配置した即値アドレスを指定して読ませる必要があります。これが面倒臭さの極みなのです。

ということで、本シリーズの始まりです〜〜
対象のマイコンは ATmega328P とします。

環境構築

ツールのインストール

必要なツールは以下の通りです。

  • avra: AVR 用のマクロアセンブラ
  • avrdude: AVR への書き込み

Mac で Homebrew を導入している方は、以下で全て終わります。

$ brew install avra avrdude

インクルードファイルのダウンロード・配置

このリンクから各デバイス用のインクルードファイルをダウンロードしてください。この .inc ファイルは後ほどアセンブリソースファイルにてインクルードしますので、近い場所に置いてください。IO レジスタのアドレスなどが含まれています。

必要なドキュメントのダウンロード

まずは ATmega328P のデータシートが必要だと思います。さらに AVR アーキテクチャの命令セットについては こちらに日本語翻訳 PDF がありますので、有り難く頂戴しましょう。

さあアセンブろう

早速、基本のLチカをやっていきましょう。ひとまず、僕の持っている AVR で1秒ごとに LED をチカチカさせるプログラムを示します。解説は書き込み方法を示してからにします。なお、インクルードファイルはこのアセンブリファイルよりも1階層上に置いておきました

コード

.include "../m328pdef.inc"
.device atmega328p

.def cnt = R16
.def tmp = R17
; 0x 及び $ を前置すると16進数表記となる
; その他 0b が2進数で、何もつけないとデフォルトで10進数となる
.org 0x0000
    rjmp main

; サブルーチン

wait_1s:
    ldi r22, 100
    wait_1s2:
        rcall wait_10ms
        dec r22
        brne wait_1s2

wait_10ms:
    ldi r21, 100
    wait_10ms2:
        rcall wait_0_1ms
        dec r21
        brne wait_10ms2
        ret

wait_0_1ms:
    ldi r20, 200
    wait_0_1ms2:
        nop
        dec r20
        brne wait_0_1ms2
        ret

main:
    sbi DDRB, PB5
    body:
        sbi PORTB, PB5
        rcall wait_1s
        cbi PORTB, PB5
        rcall wait_1s
        rjmp body

書けたら次のコマンドを使って .hex ファイルを生成です。

$ avra wait.asm # wait.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 = 0x0014, Length = 0x0015

Assembly complete with no errors.
Segment usage:
   Code      :        21 words (42 bytes)
   Data      :         0 bytes
   EEPROM    :         0 bytes

なんと、42 bytes ですね!すごい。実行後はカレントディレクトリに wait.hex とかいう hex ファイルがあると思うので、それを使って書き込みします。なお実行バイナリは .cof という coff 形式のものです。今回は使いませんが。
書き込みコマンドは以下。FT232RL を使ったものはこの記事で説明していますので端折ります。Arduino をつかう時はこんな感じ。

$ avrdude -c arduino -p m328p -P /dev/cu.wchusbserial1420 -U flash:w:wait.hex

シリアルポートは環境によって異なるので各自で変えてください。僕の持っている Arduino UNO 互換ボード(「UNO」とだけ記載してある)に載っているチップではこのようなポート名です(確か Mac ではドライバが必要だったような...)。

書き込みが完了すると、PD5 にて LED がチカチカすると思います。これであなたはマイコン脳内に命令を直接語りかける事ができたのです

コードの説明

それではコードの説明といきましょう。なんだか記事をここまで書いておいて息切れしてきていますが、頑張ります。

冒頭

まずは冒頭の疑似命令(ディレクティブ)について。

.include "../m328pdef.inc"
.device atmega328p

.def cnt = R16
.def tmp = R17

まず .include では、スペースを挟んで指定したファイルを読み込みます。これはアセンブル前に展開されるので、実際のマイコンの動作に影響があるわけではありません。ご安心あれ。また、; 以降はコメントと見なされます。
続いて、次の行の .device では、アセンブル時にメモリ境界等のエラーを検出させるために記述しています。というか、これを書かないと avra が警告を吐いてくるので、黙らせるために書いているという感じです。
更に、.def はマクロを設定できます。この場合では r16cnt と呼ぶぞ!と宣言しています。

ところで、.include は C言語のそれと同一であるため、ファイルの内容がそのまま展開されるのでたとえば次のようなことも可能です。

; test.inc

.def tmp = r16

こうやってファイル test.inc を作っておいて、次のように .include しても問題ないわけです。

; main.asm
.include "test.inc"

ldr r15, tmp ; test.inc 内の表記によりtmpがr16に置換される

ベクタテーブル

続いて以下の記述です。

.org 0x0000
    rjmp main

こちらは見出しにあるように、ベクタテーブルの記述をしています。AVR はベクタテーブルが配置されたアドレス上に ジャンプ命令 (コール命令ではない)を書く必要があります。上の例では、コードサイズを削減するために相対ジャンプ(rjmp, 2bytes)を用いています。絶対ジャンプ(jmp, 4bytes)を使うと、次のベクタにはみ出してしまいます。

.org では、「直下の機械語を指定アドレスに配置するよ〜」という意味で、この場合はすなわち 「rjmp main0x0000 に配置せよ」ということになります。この 0x0000 はデータシートにかかれている値です。以下のように。

このマイコンは、リセット時には 0x0000 番地にジャンプして、そこに書かれた命令を実行するみたいですね。ということで、リセット時から開始させたい命令へのジャンプ命令はこれ以外の番地に書き込んでもうまく動きません。

main部

ちょっと wait のところはすっ飛ばして、まずは main を見ましょう。

main:
    sbi DDRB, PB5
    body:
        sbi PORTB, PB5
        rcall wait_1s
        cbi PORTB, PB5
        rcall wait_1s
        rjmp body

まず sbi という命令を用いています。これは、IO レジスタに直接ビットを立てるという命令で、2クロックで処理が完了します。ここでは PORTBPB5 を書き込んでいます。これらはマクロなので sbi PORTB, PB5 展開すると、

sbi 0x05, 5 ; *(0x04) |= (1 << 5) 

となります(PORTB の 5ビット目を立てている)。このように即値を直接オペランドに持ってきても構いません。DDRB は GPIO の入出力設定をするレジスタです。1 にすると出力になります。

ここで、後のハマりどころとして sbi 命令の注意を述べておきます。便利な命令なのですが、IO レジスタのアドレスが 32 以上のものには書き込みを実行できません。なので、例えば TWI のレジスタに処理をしたい場合は ロード → OR → ストア を行わなければならないので注意が必要です。

あ、そうそう。何となく分かる人は多いと思いますが、AVR の命令は全て op dest, src の順で書きます。

続いて rcall 命令です。これは先程の rjmp とは異なり、現在のプログラムカウンタ値に1を加算したものをスタックに Push してくれます(なんだかよくわからないという人は、「関数を呼ぶための命令なんだな」と思っておきましょう)。
呼び出された先から帰ってくるためには、ret 命令を使います。便利。

cbisbi に似ていて、今度はIO レジスタの指定ビットをクリアするという動作をします。

最後に rjmp によって無限ループを形成しています。

時間稼ぎのwait部

最後に脳筋ループの部分です。

wait_1s:
    ldi r22, 100
    wait_1s2:
        rcall wait_10ms
        dec r22
        brne wait_1s2

wait_10ms:
    ldi r21, 100
    wait_10ms2:
        rcall wait_0_1ms
        dec r21
        brne wait_10ms2
        ret

wait_0_1ms:
    ldi r20, 200
    wait_0_1ms2:
        nop
        dec r20
        brne wait_0_1ms2
        ret

main からは wait_1s のみを呼ぶようにしています。早速存在する ldi 命令は Load Immediate ということで、汎用レジスタに即値をロードする命令です。
dec は Decrement であり、汎用レジスタから1を引いてストアする命令です。
brne は見た目そのまんまの分岐命令となっていて、dec によって Not Equal(すなわちデクリメントした結果がゼロでなかった)時のジャンプアドレスを指定しています。

最後に、時間稼ぎが終わったら ret でメインルーチンに戻る処理をします。

ところで

低レイヤでマイコンを弄っている人はおそらく「スタックポインタの初期化は要らないのか?」と思われたでしょう。通常であればリセット直後に初期化を行わないと、push に相当する命令を実行した時に動作が未定になってしまいます。
AVR 全てに当てはまるかはわかりませんが、ATega328P においてはスタックポインタの初期化は不要です

この画像のように、リセット時には SP0x08FF に初期化されています。これはメモリ空間において SRAM 領域の末尾アドレスです。最初からこのアドレスを指しているので、別途書き換える必要はないというわけなんですね〜

ということで、皆さんにも AVR アセンブリを楽しんでもらいたいという思いのもと、シリーズ化して記事を書きました。
今後は STM32 の記事みたいに、ペリフェラルの設定を行うコードを記述したいと思います。同時に、新しく出てきた命令があればその都度説明を入れるつもりです。