AVR アセンブリ入門

AVRマイコンは、Atmel社(現在はMicrochip Technology社)によって1996年頃に開発された、8ビットRISCアーキテクチャのマイクロコントローラファミリです。シンプルながらも高性能で、特にホビー用途や組み込みシステムのプロトタイピングで広く利用されてきました。Arduinoの初期モデルに搭載されていたことでも有名です。

では、なぜ今、C言語や他の高水準言語が主流の中で、アセンブリ言語を学ぶのでしょうか? いくつかの理由があります:

  • ハードウェアの深い理解: アセンブリ言語は、マイコンのアーキテクチャ(レジスタ、メモリ、命令セットなど)と直接対話します。これにより、ハードウェアがどのように動作するのかを根本的に理解することができます。
  • パフォーマンスの最適化: アセンブリ言語を使うことで、コードの実行速度やメモリ使用量を極限まで最適化できます。特にリソースが限られたマイコンでは、この点が重要になることがあります。
  • 高水準言語の限界を超える: コンパイラが対応していない特定のハードウェア機能を利用したい場合や、非常に精密なタイミング制御が必要な場合に、アセンブリ言語が役立ちます。
  • デバッグ能力の向上: 高水準言語で書かれたコードが期待通りに動作しない場合、コンパイラが生成したアセンブリコードを理解することで、問題の原因を特定しやすくなります。

この記事では、AVRアセンブリ言語の基本的な概念から、簡単なプログラムを作成して動かすまでを解説します。AVRマイコンの内部構造、主要な命令、開発環境のセットアップ、そして実際にLEDを点滅させるプログラム(Lチカ)の作成を通じて、アセンブリプログラミングの世界への第一歩を踏み出しましょう。

AVRアセンブリを理解するためには、まずAVRマイコンの基本的な構造(アーキテクチャ)を知る必要があります。AVRは「修正ハーバード・アーキテクチャ」を採用しており、プログラムメモリとデータメモリが物理的に分離されています。

レジスタ (Registers)

レジスタは、CPU内部にある高速な記憶領域で、演算やデータの一時的な格納に使用されます。AVRマイコンにはいくつかの種類のレジスタがあります。

  • 汎用レジスタ (General Purpose Registers): R0からR31までの32個の8ビットレジスタです。これらのレジスタは、データ操作の中心となります。多くの命令は、これらのレジスタ間で直接データをやり取りできます。特にR16からR31は、即値(定数)を直接ロードする命令 (`LDI`) などでよく使われます。R26:R27、R28:R29、R30:R31のペアは、それぞれX, Y, Zポインタレジスタとして、間接アドレッシングに使用できます。
  • ステータスレジスタ (Status Register – SREG): 演算結果の状態(ゼロ、負、キャリーなど)を示すフラグが格納される8ビットのレジスタです。条件分岐命令は、このSREGのフラグを参照して動作を決定します。主なフラグには、C(キャリー)、Z(ゼロ)、N(ネガティブ)、V(オーバーフロー)、S(符号)、H(ハーフキャリー)、T(ビットコピー用)、I(グローバル割り込み許可)があります。
  • プログラムカウンタ (Program Counter – PC): 次に実行される命令が格納されているプログラムメモリのアドレスを指し示すレジスタです。通常は命令が実行されるごとに自動的にインクリメントされますが、ジャンプ命令やコール命令によって任意のアドレスに変更することもできます。
  • スタックポインタ (Stack Pointer – SP): スタックと呼ばれるデータメモリ上の一時的なデータ格納領域の、現在使用中の最上位アドレスを指し示すレジスタです。サブルーチン呼び出し時の戻りアドレスの保存や、一時的なデータの退避・復帰(プッシュ/ポップ)に使用されます。AVRでは、SPはデータメモリ空間(SRAM)を指します。多くのAVRデバイスでは、SPHとSPLの2つの8ビットI/Oレジスタで構成され、16ビットのアドレスを扱います。

メモリ空間 (Memory Space)

AVRマイコンは、主に3種類のメモリ空間を持っています。

  • Flashメモリ (プログラムメモリ): プログラムコードを格納するための不揮発性メモリ(電源を切っても内容が消えない)です。命令はここから読み出されて実行されます。AVRは、チップ上にFlashメモリを搭載した初期のマイコンファミリの一つです。サイズはデバイスによって異なります(例: ATtiny2313は2KB, ATmega328Pは32KB)。
  • SRAM (データメモリ): プログラム実行中に変数やデータを一時的に格納するための揮発性メモリ(電源を切ると内容が消える)です。汎用レジスタやI/Oレジスタも、このデータメモリ空間にマッピングされています。スタックもこのSRAM上に確保されます。サイズはデバイスによって異なり、Flashメモリよりも小さいのが一般的です(例: ATtiny2313は128バイト, ATmega328Pは2KB)。
  • EEPROM (データEEPROM): 設定値など、電源を切っても保持したい少量のデータを格納するための不揮発性メモリです。Flashメモリと同様に書き換え回数に制限がありますが、プログラムメモリとは独立して読み書きできます。SRAMやFlashメモリとは別のアドレス空間を持ち、通常は専用のI/Oレジスタ経由でアクセスします。サイズはデバイスによります(例: ATtiny2313は128バイト, ATmega328Pは1KB)。

I/Oポート (Input/Output Ports)

マイコンが外部の世界とやり取りするための窓口がI/Oポートです。AVRマイコンの各ピンは、通常いくつかのポート(例: PORTA, PORTB, PORTC, PORTD)にグループ化されています。各ポートには、関連する3つのI/Oレジスタがあります(xはポート名A, B, C, D…)。

  • DDRx (Data Direction Register): 各ピンを入力(0)にするか出力(1)にするかを設定します。
  • PORTx (Port Data Register): ピンが出力に設定されている場合、そのピンから出力する値(High=1, Low=0)を書き込みます。ピンが入力に設定されている場合、内蔵プルアップ抵抗を有効(1)にするか無効(0)にするかを設定します。
  • PINx (Port Input Pins Register): ピンが入力に設定されている場合、そのピンの現在の状態(High/Low)を読み取ります。このレジスタは読み取り専用です(書き込んでも影響はありません)。

これらのレジスタはデータメモリ空間内の特定のI/Oアドレスに割り当てられており、`IN`, `OUT`命令や、特定のI/Oアドレス範囲(0x00-0x1F)に対しては `SBI`, `CBI` などのビット操作命令でアクセスできます。また、`LDS`, `STS`命令を使ってSRAMアドレス経由でもアクセス可能です。

レジスタ 役割 備考
汎用レジスタ (R0-R31) データ演算・一時格納 8ビット x 32本
SREG 演算結果の状態フラグ格納 条件分岐で使用
PC 次に実行する命令のアドレス 分岐命令で変更可能
SP スタックのトップアドレス SRAM上を指す
DDRx ポートの入出力方向設定 0:入力, 1:出力
PORTx 出力値設定 / プルアップ設定 出力時: H/L設定, 入力時: プルアップON/OFF
PINx 入力ピンの状態読み取り 読み取り専用

AVRアセンブリプログラムを作成し、マイコンで実行するには、いくつかのツールが必要です。幸いなことに、多くのツールは無料で利用可能です。

必要なツール

  • アセンブラ (Assembler): アセンブリ言語のコード(.asmファイル)を、マイコンが理解できる機械語(オブジェクトファイルやHEXファイル)に変換するプログラムです。
    • avr-as: GNU Binutilsに含まれるアセンブラで、AVR-GCCツールチェーンの一部です。Linux, macOS, Windowsで利用可能です。
    • Microchip Studio (旧Atmel Studio): Microchip社が提供するWindows向けの統合開発環境(IDE)で、アセンブラ、C/C++コンパイラ、シミュレータ、デバッガ機能が含まれています。内部的にはAVR-GCCツールチェーンを使用しています。
    • avra: 独立したAVRアセンブラで、マクロ機能などが強化されています。クロスプラットフォームで利用可能です。
  • リンカ (Linker): アセンブラが生成したオブジェクトファイルを結合し、最終的な実行可能ファイル(通常はELF形式)を作成します。(avr-gccツールチェーンに含まれる `avr-ld`)
  • HEXファイル生成ツール: リンカが生成したELFファイルから、マイコンへの書き込みに使用されるIntel HEX形式のファイルを生成します。(avr-gccツールチェーンに含まれる `avr-objcopy`)
  • 書き込みツール (Programmer Software): 生成されたHEXファイルを、ハードウェアプログラマ(書き込み器)経由でマイコンのFlashメモリに書き込むためのソフトウェアです。
    • avrdude: 様々な書き込み器に対応した、広く使われているコマンドラインツールです。Linux, macOS, Windowsで利用可能です。
    • Microchip Studio: 純正の書き込み器(AVRISP mkII, PICkit など)に対応した書き込み機能を持っています。
  • ハードウェアプログラマ (書き込み器): PCとAVRマイコンを接続し、プログラムを書き込むための物理的なデバイスです。USBasp, AVRISP mkII (および互換機), PICkit 4/5 など、様々な種類があります。Arduinoボード自体をISPプログラマとして使用することも可能です。
  • テキストエディタ または IDE: アセンブリコードを記述するためのエディタ。シンプルなテキストエディタでも構いませんが、シンタックスハイライト機能などがあると便利です。VSCodeにAVR関連の拡張機能を入れるのも良い選択肢です。Microchip Studioは高機能なIDEです。
  • シミュレータ (Simulator): 実際のハードウェアなしに、PC上でプログラムの動作をシミュレーションするツールです。デバッグに非常に役立ちます。
    • Microchip Studio Simulator: IDEに内蔵されています。
    • simavr: コマンドラインベースのオープンソースシミュレータです。

簡単なセットアップ例 (AVR-GCC + avrdude)

ここでは、クロスプラットフォームで利用可能なコマンドラインツールを使った基本的なセットアップの流れを示します。

  1. AVR-GCC ツールチェーンのインストール:
    • Windows: Microchip Studioをインストールするのが簡単です。あるいは、独立したAVR-GCCツールチェーン (例: Zak Kemble氏のビルドなど) を探してインストールします。パスを通す必要があるかもしれません。
    • macOS: Homebrew を使って `brew install avr-gcc avrdude` を実行するのが簡単です。または CrossPack を利用する方法もあります。
    • Linux (Debian/Ubuntu): `sudo apt-get update && sudo apt-get install gcc-avr binutils-avr avr-libc avrdude` を実行します。
  2. テキストエディタの準備: お好みのテキストエディタ(VSCode, Sublime Text, Atom, Vim, Emacsなど)を用意します。
  3. ハードウェアプログラマと接続: 使用する書き込み器(例: USBasp)をPCに接続し、必要であればドライバをインストールします。書き込み器とターゲットのAVRマイコンをISPケーブルで接続します(MOSI, MISO, SCK, RESET, VCC, GND)。
  4. 動作確認: ターミナル(コマンドプロンプト)を開き、`avr-gcc –version` や `avrdude -c usbasp -p m328p` (マイコンに合わせて `-p` オプションを変更) などのコマンドを実行して、ツールが認識されているか、書き込み器とマイコンが通信できるかを確認します。

これで、アセンブリコードを書いて、アセンブルし、マイコンに書き込む準備が整いました!

AVRアセンブリ言語は、マイコンに特定の操作を行わせるための命令(インストラクション)の集まりです。各命令は、通常「ニーモニック (Mnemonic)」と呼ばれる短い英単語(例: `LDI`, `ADD`)と、操作対象を示す「オペランド (Operand)」で構成されます。AVRはRISCアーキテクチャを採用しており、多くの命令は1クロックサイクルで実行されます。

以下に、よく使われる基本的な命令をカテゴリ別に紹介します。ここではATmega328Pなどで使われる一般的な命令を例示しますが、デバイスによってはサポートされていない命令もあります。詳細は各デバイスのデータシートや AVR Instruction Set Manual を参照してください。

データ転送命令 (Data Transfer Instructions)

レジスタ、メモリ、I/Oポート間でデータを移動させる命令です。

  • `LDI Rd, K` (Load Immediate): 8ビットの即値(定数)Kを、汎用レジスタRd(R16~R31のみ)にロードします。
    ; R16に数値 10 (0x0A) をロードする
    LDI R16, 10
  • `MOV Rd, Rr` (Move Register): 汎用レジスタRrの内容を、汎用レジスタRdにコピーします。
    ; R1の内容をR0にコピーする
    MOV R0, R1
  • `LDS Rd, address` (Load Direct from Data Space): データ空間(SRAM)の指定された16ビットアドレス `address` から8ビットデータを読み込み、汎用レジスタRd(R0~R31)にロードします。
    ; SRAMの 0x0100 番地からデータを読み込み R2 にロードする
    LDS R2, 0x0100
  • `STS address, Rr` (Store Direct to Data Space): 汎用レジスタRrの内容を、データ空間(SRAM)の指定された16ビットアドレス `address` に書き込みます。
    ; R3 の内容を SRAM の 0x0101 番地に書き込む
    STS 0x0101, R3
  • `IN Rd, P` (Input from I/O Location): I/Oアドレス空間のポートアドレスP(0x00~0x3F)からデータを読み込み、汎用レジスタRd(R0~R31)にロードします。
    ; ポートBの入力ピンの状態 (PINB) を R20 に読み込む (PINBのI/Oアドレスはデバイスによる)
    ; 例えば ATmega328P では PINB は 0x03 (I/Oアドレス) = 0x23 (SRAMアドレス)
    IN R20, 0x03  ; I/Oアドレスを指定
  • `OUT P, Rr` (Output to I/O Location): 汎用レジスタRrの内容を、I/Oアドレス空間のポートアドレスP(0x00~0x3F)に書き込みます。
    ; R21 の内容を ポートD データレジスタ (PORTD) に書き込む (PORTDのI/Oアドレスはデバイスによる)
    ; 例えば ATmega328P では PORTD は 0x0B (I/Oアドレス) = 0x2B (SRAMアドレス)
    OUT 0x0B, R21 ; I/Oアドレスを指定
  • `PUSH Rr` / `POP Rd` (Push/Pop Register on Stack): レジスタRrの内容をスタックに退避 (PUSH) / スタックからレジスタRdに復帰 (POP) します。サブルーチン呼び出し時のレジスタ内容保護などに使います。
    PUSH R16  ; R16の内容をスタックへ
    ; ... 何らかの処理 ...
    POP R16   ; スタックからR16へ内容を戻す

算術演算命令 (Arithmetic Instructions)

加算、減算などの算術演算を行います。

  • `ADD Rd, Rr` (Add without Carry): Rd = Rd + Rr の加算を行います。SREGのフラグが変化します。
    ADD R1, R2 ; R1 = R1 + R2
  • `ADC Rd, Rr` (Add with Carry): Rd = Rd + Rr + C (キャリーフラグ) の加算を行います。多バイト長の加算に使用します。
    ; 16ビット加算 R17:R16 = R17:R16 + R19:R18
    ADD R16, R18  ; 下位バイトを加算
    ADC R17, R19  ; 上位バイトをキャリー付きで加算
  • `SUB Rd, Rr` (Subtract without Carry): Rd = Rd – Rr の減算を行います。
    SUB R5, R6 ; R5 = R5 - R6
  • `SBC Rd, Rr` (Subtract with Carry): Rd = Rd – Rr – C (キャリーフラグ、減算ではボローとして扱われる) の減算を行います。多バイト長の減算に使用します。
  • `SUBI Rd, K` (Subtract Immediate): Rd = Rd – K (即値) の減算を行います。(R16~R31のみ)
    SUBI R17, 5 ; R17 = R17 - 5
  • `INC Rd` (Increment): Rd = Rd + 1 のインクリメントを行います。キャリーフラグは変化しません。
    INC R20 ; R20 を 1 増やす
  • `DEC Rd` (Decrement): Rd = Rd – 1 のデクリメントを行います。
    DEC R20 ; R20 を 1 減らす

論理演算命令 (Logical Instructions)

AND, OR, XOR などのビット単位の論理演算を行います。

  • `AND Rd, Rr` (Logical AND): Rd = Rd & Rr (ビットごとの論理積) を行います。特定のビットをクリアするのに使えます。
    ; R22 の下位4ビットをクリアする (R22 = R22 & 0xF0)
    LDI R23, 0xF0
    AND R22, R23
  • `ANDI Rd, K` (Logical AND with Immediate): Rd = Rd & K (即値) を行います。(R16~R31のみ)
  • `OR Rd, Rr` (Logical OR): Rd = Rd | Rr (ビットごとの論理和) を行います。特定のビットをセットするのに使えます。
    ; R24 のビット0とビット1をセットする (R24 = R24 | 0x03)
    LDI R25, 0x03
    OR R24, R25
  • `ORI Rd, K` (Logical OR with Immediate): Rd = Rd | K (即値) を行います。(R16~R31のみ)
  • `EOR Rd, Rr` (Exclusive OR): Rd = Rd ^ Rr (ビットごとの排他的論理和) を行います。特定のビットを反転するのに使えます。また、`EOR R1, R1` は `R1` をゼロクリアする常套手段です。
    EOR R1, R1 ; R1 を 0 にする
  • `COM Rd` (One’s Complement): Rd の全ビットを反転します (Rd = ~Rd)。
  • `NEG Rd` (Two’s Complement): Rd の2の補数を計算します (Rd = 0 – Rd)。

分岐命令 (Branch Instructions)

プログラムの実行フローを制御します。ジャンプや条件分岐、サブルーチン呼び出しなどがあります。

  • `RJMP k` (Relative Jump): 無条件に、現在のPCからの相対位置kへジャンプします。近距離のジャンプに使われます。
    Loop:
      ; ... 何かの処理 ...
      RJMP Loop ; Loopラベルへ戻る
  • `JMP k` (Jump): 無条件に、指定された絶対アドレスkへジャンプします。より広範囲のジャンプが可能ですが、命令長が長くなります。(一部のデバイスのみ)
  • `RCALL k` (Relative Call): 現在のPCからの相対位置kにあるサブルーチンを呼び出します。戻りアドレスがスタックに積まれます。
    RCALL DelaySubroutine ; DelaySubroutine を呼び出す
    ; ... 処理再開 ...
    
    DelaySubroutine:
      ; ... 遅延処理 ...
      RET ; 呼び出し元に戻る
  • `CALL k` (Call): 指定された絶対アドレスkにあるサブルーチンを呼び出します。(一部のデバイスのみ)
  • `RET` (Return from Subroutine): サブルーチンから呼び出し元に戻ります。スタックから戻りアドレスをPCにポップします。
  • `BREQ k` (Branch if Equal): SREGのZフラグ(ゼロフラグ)が1の場合、相対位置kへ分岐します。直前の比較(`CP`, `CPI`)や演算(`SUB`, `DEC`など)の結果がゼロだった場合に分岐します。
    CPI R16, 10  ; R16 と 10 を比較
    BREQ EqualLabel ; もし R16 == 10 なら EqualLabel へジャンプ
  • `BRNE k` (Branch if Not Equal): SREGのZフラグが0の場合、相対位置kへ分岐します。直前の比較や演算の結果がゼロでなかった場合に分岐します。
    DEC R20      ; R20 をデクリメント
    BRNE LoopLabel ; R20 が 0 でなければ LoopLabel へジャンプ
  • その他条件分岐: `BRLO`(未満), `BRSH`(以上), `BRLT`(負), `BRGE`(非負), `BRMI`(マイナス), `BRPL`(プラス), `BRVS`(オーバーフローセット), `BRVC`(オーバーフロークリア) など、SREGの各フラグに対応した多くの条件分岐命令があります。
  • `CP Rd, Rr` (Compare): Rd と Rr を比較します (内部的に Rd – Rr を実行)。結果はレジスタには格納されず、SREGのフラグのみが変化します。条件分岐命令の直前で使われます。
  • `CPI Rd, K` (Compare with Immediate): Rd と 即値K を比較します。(R16~R31のみ)

ビット操作命令 (Bit Manipulation Instructions)

レジスタやI/O空間の特定のビットを操作します。

  • `SBI P, b` (Set Bit in I/O Register): I/Oアドレス空間P(0x00~0x1F)のビットb(0~7)を1にセットします。ポートの特定のピンをHighにするのによく使われます。
    ; PORTBのビット5 (PB5) を High にする (PORTBのI/Oアドレスが0x18の場合)
    SBI 0x18, 5
  • `CBI P, b` (Clear Bit in I/O Register): I/Oアドレス空間P(0x00~0x1F)のビットbを0にクリアします。ポートの特定のピンをLowにするのによく使われます。
    ; PORTBのビット5 (PB5) を Low にする
    CBI 0x18, 5
  • `SBRC Rr, b` (Skip if Bit in Register is Clear): 汎用レジスタRrのビットbが0の場合、次の1命令をスキップします。
    ; R20のビット3が0なら、次のRJMP命令をスキップ
    SBRC R20, 3
    RJMP SkipTarget
  • `SBRS Rr, b` (Skip if Bit in Register is Set): 汎用レジスタRrのビットbが1の場合、次の1命令をスキップします。入力ピンの状態確認などに使われます。
    ; PINBのビット0 (PB0) を読み込む (例えば R22 に IN R22, PINB)
    ; R22のビット0が1 (ボタンが押されていないなど) なら、次の命令をスキップ
    SBRS R22, 0
    RCALL ButtonPressedHandler ; スキップされなければボタン処理へ
  • `BST Rd, b` / `BLD Rd, b` (Bit Store/Load): RdのビットbをSREGのTフラグにコピー(BST) / Tフラグの内容をRdのビットbにコピー(BLD)します。ビット単位のデータ操作に使えます。

これらの命令はAVRアセンブリのほんの一部です。しかし、これらを組み合わせることで、多くの基本的な処理を実装できます。命令の正確な動作、影響を受けるフラグ、実行に必要なクロックサイクル数などを知るためには、公式の命令セットマニュアルを参照することが不可欠です。

理論を学んだら、次は実践です! マイコンプログラミングの「Hello, World!」とも言える、LED点滅(Lチカ)プログラムをAVRアセンブリで作成してみましょう。

目標

AVRマイコン(ここでは例としてATmega328Pを想定)の特定のピンに接続されたLEDを、一定の間隔で点滅させます。

ハードウェア設定 (概念)

物理的な接続が必要です(ここでは詳細な回路図は省略します)。

  1. ATmega328Pを用意します(例: Arduino Unoボード上のマイコン)。
  2. 例えば、デジタルピン13 (マイコンのPB5ピンに対応) にLEDを接続します。通常、電流制限抵抗(例: 220Ω~1kΩ)を直列に挿入します。LEDのアノード(長い足)をPB5ピンに、カソード(短い足)を抵抗経由でGND(グラウンド)に接続します。
  3. マイコンに電源(通常5V)とGNDを接続します。
  4. ISPプログラマを接続するためのピン(MOSI, MISO, SCK, RESET, VCC, GND)にアクセスできるようにします。

コード解説

以下は、ATmega328PのPB5ピン(Arduino Unoのデジタルピン13)に接続されたLEDを約0.5秒間隔で点滅させるアセンブリコードの例です。

; --- Lチカプログラム for ATmega328P ---
; Target: ATmega328P
; Clock: 16MHz (Arduino Uno 標準)
; LED: Connected to PB5 (Digital Pin 13)

; --- 定義ファイルと初期設定 ---
.include "m328pdef.inc" ; ATmega328P用の定義ファイル (レジスタ名など)

.def temp = R16          ; 一時変数としてR16を使用
.def delay_cnt1 = R17    ; 遅延ループカウンタ1
.def delay_cnt2 = R18    ; 遅延ループカウンタ2
.def delay_cnt3 = R19    ; 遅延ループカウンタ3

.cseg                   ; コードセグメントを開始
.org 0x0000             ; プログラム開始アドレス (リセットベクタ)
  rjmp main             ; mainラベルへジャンプ (割り込みを使わない場合)

; --- メインプログラム ---
main:
  ; スタックポインタの初期化 (必須ではないが、サブルーチンを使う場合は必要)
  ldi temp, high(RAMEND) ; SRAMの最終アドレスの上位バイト
  out SPH, temp
  ldi temp, low(RAMEND)  ; SRAMの最終アドレスの下位バイト
  out SPL, temp

  ; --- I/Oポートの初期化 ---
  ; PB5 (D13) を出力に設定
  sbi DDRB, DDB5        ; DDRBレジスタのビット5を1にする

; --- メインループ ---
loop:
  ; LEDを点灯 (PB5をHighにする)
  sbi PORTB, PORTB5     ; PORTBレジスタのビット5を1にする

  ; 遅延処理呼び出し (約0.5秒)
  rcall delay_ms_500

  ; LEDを消灯 (PB5をLowにする)
  cbi PORTB, PORTB5     ; PORTBレジスタのビット5を0にする

  ; 遅延処理呼び出し (約0.5秒)
  rcall delay_ms_500

  ; ループの先頭に戻る
  rjmp loop

; --- 遅延サブルーチン (約500ms @ 16MHz) ---
; 簡易的なループによる遅延。正確性は高くない。
delay_ms_500:
  ldi delay_cnt3, 100  ; 外側ループカウンタ (約5ms * 100 = 500ms)
outer_loop:
  ldi delay_cnt2, 250  ; 中間ループカウンタ
middle_loop:
  ldi delay_cnt1, 200  ; 内側ループカウンタ (約25us * 200 = 5ms)
inner_loop:
  nop                    ; 1クロック消費
  nop                    ; 1クロック消費
  dec delay_cnt1         ; カウンタ1減算 (1クロック)
  brne inner_loop        ; 0でなければループ (2クロック or 1クロック)
  ; 内側ループは約 4 * 200 = 800 クロック -> 50us @ 16MHz (※要調整)
  ; ※ 上記計算は簡易。正確には命令サイクル数を数える必要あり。
  ;   もっと正確な遅延にはタイマーを使うべき。

  dec delay_cnt2         ; カウンタ2減算
  brne middle_loop       ; 0でなければループ

  dec delay_cnt3         ; カウンタ3減算
  brne outer_loop        ; 0でなければループ

  ret                    ; サブルーチンから戻る

コードのポイント:

  • `.include “m328pdef.inc”`: マイコン固有のレジスタ名やビット名を定義したファイルを取り込みます。これにより、`PORTB` や `DDB5` のようなシンボル名が使えます。このファイルは通常、AVR-GCCツールチェーンに含まれています。
  • `.def`: レジスタに別名(エイリアス)を付けます。コードが読みやすくなります。
  • `.cseg`, `.org 0x0000`: コードセグメント(プログラムを配置するメモリ領域)を指定し、開始アドレスをリセットベクタである0番地に設定します。
  • `rjmp main`: リセット後、すぐに`main`ラベルの処理にジャンプします。
  • スタックポインタ初期化: `SPH`, `SPL`レジスタにSRAMの最終アドレスを設定します。`RCALL`や`PUSH`/`POP`命令を使う場合は必須です。`RAMEND`は定義ファイルで定義されている定数です。
  • I/Oポート初期化 (`sbi DDRB, DDB5`): `DDRB`レジスタの`DDB5`ビット(5番目のビット)を1にセットします。これにより、PB5ピンが出力モードになります。`sbi`命令はI/Oアドレス0x00-0x1Fのレジスタに対して使えます (ATmega328PではDDRBは該当)。
  • メインループ (`loop`):
    • `sbi PORTB, PORTB5`: PB5ピンの出力をHigh (1) にします。LEDが点灯します。
    • `rcall delay_ms_500`: 遅延サブルーチンを呼び出します。
    • `cbi PORTB, PORTB5`: PB5ピンの出力をLow (0) にします。LEDが消灯します。
    • `rcall delay_ms_500`: 再び遅延サブルーチンを呼び出します。
    • `rjmp loop`: `loop`ラベルに戻り、点滅を繰り返します。
  • 遅延サブルーチン (`delay_ms_500`):
    • 3重のループ (`ldi`, `dec`, `brne`) を使って時間を稼ぎます。`nop` (No Operation) 命令は、何もしませんが1クロックサイクルを消費するため、微調整に使われます。
    • この遅延方法は、クロック周波数に依存し、精度も高くありません。正確な時間制御にはタイマー割り込みを使うのが一般的です。
    • `ret`: サブルーチンの最後に記述し、呼び出し元に戻ります。

アセンブルと書き込み (コマンドライン例)

上記コードを `blink.asm` という名前で保存した場合、avr-gccツールチェーンとavrdudeを使って以下のようにアセンブル・書き込みできます。

  1. アセンブル (.asm -> .o):
    avr-as -mmcu=atmega328p blink.asm -o blink.o
    `-mmcu`で使用するマイコンを指定します。
  2. リンク (.o -> .elf):
    avr-ld -o blink.elf blink.o
    (単純なプログラムならリンカは不要な場合もありますが、通常はこのステップを含めます)
  3. HEXファイル生成 (.elf -> .hex):
    avr-objcopy -O ihex -R .eeprom blink.elf blink.hex
    `-O ihex`でIntel HEX形式を指定します。`-R .eeprom`でEEPROMセクションを除外します。
  4. マイコンへの書き込み (.hex -> Flash):
    avrdude -c usbasp -p m328p -U flash:w:blink.hex:i
    `-c`で書き込み器の種類(例: `usbasp`, `avrispmkII`, `arduino`)、`-p`でマイコンの種類(例: `m328p`, `t2313`)、`-U flash:w:blink.hex:i`でHEXファイルをFlashメモリに書き込むことを指定します。

書き込みが成功すれば、PB5ピンに接続されたLEDがチカチカと点滅し始めるはずです! これが、AVRアセンブリプログラミングの第一歩です。

プログラムが期待通りに動作しないことはよくあります。特にアセンブリ言語のように低レベルなプログラミングでは、小さなミスが大きな問題につながることもあります。そこで重要になるのが「デバッグ」です。デバッグとは、プログラム中の誤り(バグ)を見つけて修正する作業のことです。

AVR開発では、主にシミュレータを使ったデバッグと、実機を使ったデバッグ(インサーキット・エミュレーション)があります。ここでは、特にシミュレータを用いたデバッグの基本的な手法について説明します。

デバッグの重要性

  • 問題の早期発見: シミュレータを使えば、実際のハードウェアに書き込む前に、プログラムの論理的な誤りを発見できます。
  • 内部状態の可視化: レジスタの値、メモリの内容、I/Oポートの状態などをステップごとに確認できるため、プログラムがどのように動作しているかを詳細に追跡できます。
  • 効率的な修正: 問題箇所を特定しやすいため、修正作業を効率的に進められます。

シミュレータの使い方

AVR開発でよく使われるシミュレータには、Microchip Studioに内蔵されているものや、オープンソースのsimavrなどがあります。

Microchip Studio Simulator:

  1. Microchip Studioでプロジェクトを開き、ビルドしてエラーがないことを確認します。
  2. デバッグメニューから「Start Debugging and Break」(F5) を選択するか、ツールバーの緑色の再生ボタンの隣にあるドロップダウンから「Simulator」を選択してデバッグを開始します。
  3. デバッグが開始されると、コードエディタ上で現在の実行行がハイライトされ、各種デバッグウィンドウ(レジスタ、メモリ、I/Oビュー、ウォッチ式など)が表示されます。

simavr (コマンドライン):

simavrはコマンドラインツールですが、GDB(GNUデバッガ)と連携して強力なデバッグ機能を提供します。

  1. まず、デバッグ情報付きでELFファイルをビルドする必要があります。avr-gcc (avr-asを含む) に `-g` オプションを付けてアセンブル・リンクします。
    avr-as -g -mmcu=atmega328p blink.asm -o blink.o
    avr-gcc -g -mmcu=atmega328p blink.o -o blink.elf
    (avr-gcc をリンカとして使うと C ライブラリ等がリンクされる場合があるので注意。単純なアセンブリなら `avr-ld -g` でも良いかもしれません。)
  2. 別のターミナルでsimavrをGDBサーバモードで起動します。
    simavr -m atmega328p -f 16000000 --gdb blink.elf
    `-m`でマイコン、`-f`でクロック周波数、`–gdb`でGDBサーバを有効にします。ポート1234で接続を待ち受けます。
  3. 別のターミナルでAVR用のGDB (`avr-gdb`) を起動し、simavrに接続します。
    avr-gdb blink.elf
    (gdb) target remote localhost:1234
  4. これでGDBのコマンドを使ってデバッグできます。

基本的なデバッグ手法

シミュレータやデバッガを使って、以下の基本的な操作を行うことができます。

  • ステップ実行 (Step Over/Step Into):
    • Step Over (F10): 現在の行を実行し、次の行で停止します。サブルーチン呼び出し (`RCALL`, `CALL`) がある場合、サブルーチン全体を実行して戻ってきた次の行で停止します。
    • Step Into (F11): 現在の行を実行します。サブルーチン呼び出しがある場合、そのサブルーチンの最初の行に移動して停止します。サブルーチンの内部動作を確認したい場合に使います。
    • Step Out (Shift+F11): 現在実行中のサブルーチンを最後まで実行し、呼び出し元に戻った次の行で停止します。
  • ブレークポイント (Breakpoints): コード中の特定の行にブレークポイントを設定すると、プログラムの実行がその行に到達した時点で一時停止します。特定の処理の前後の状態を確認したい場合に便利です。Microchip Studioでは行番号の左側の余白をクリック、GDBでは `break <ラベル名 or 行番号>` コマンドで設定できます。 (例: `break main`, `break loop`)
  • レジスタ/メモリ監視 (Watch/Memory View):
    • レジスタウィンドウ: 汎用レジスタ (R0-R31)、SREG、PC、SPなどの現在の値をリアルタイムで確認できます。値が期待通りに変化しているかを確認します。
    • メモリウィンドウ: SRAMやプログラムメモリ(Flash)、EEPROMの内容を指定したアドレス範囲で表示します。変数の値やスタックの状態を確認するのに役立ちます。
    • I/Oビュー: DDRx, PORTx, PINx などのI/Oレジスタの状態を専用のウィンドウで確認できます。ポートの設定や入出力が正しく行われているかを確認します。
    • ウォッチウィンドウ: 特定のレジスタやメモリアドレス、式を登録しておき、その値の変化を常に監視できます。
    GDBでは `info registers` でレジスタ一覧、`x/<フォーマット> <アドレス>` でメモリ内容表示、`print <レジスタ名 or 式>` で値表示などが可能です。
  • 実行制御 (Continue/Run):
    • Continue/Run (F5): プログラムの実行を再開し、次のブレークポイントに到達するかプログラムが終了するまで実行を続けます。GDBでは `continue` または `c`。

デバッグは試行錯誤のプロセスです。これらの基本的な手法を駆使して、プログラムの動作を注意深く観察し、期待とのずれを見つけることがバグ修正への近道です。焦らず、一つずつ確認していきましょう。

AVRアセンブリの基本的な概念と「Lチカ」プログラムの作成方法を学びました。しかし、これは広大なAVRの世界の入り口に過ぎません。さらに深く学び、より複雑なプロジェクトに挑戦するためには、以下のリソースや学習ステップが役立つでしょう。

AVR 命令セットマニュアル (Instruction Set Manual)

AVRアセンブリプログラミングにおける最も重要で信頼できる情報源です。Microchip社のウェブサイトからダウンロードできます。

  • 内容: 全てのAVR命令について、ニーモニック、オペランド、動作内容、影響を受けるSREGフラグ、実行に必要なクロックサイクル数、オペコード(機械語表現)などが詳細に記述されています。
  • 重要性: 特定の命令の正確な動作や副作用を理解するため、また、効率的なコードを書くために不可欠です。異なるAVRデバイス間で命令セットに差異がある場合もあるため、ターゲットデバイスのマニュアルを確認することが重要です。
  • 入手先: Microchip Technology公式サイト で “AVR Instruction Set Manual” を検索してください。例えば、DS40002198のようなドキュメント番号で見つかります。(直接リンク: AVR Instruction Set Manual PDF – リンク切れの可能性あり)

データシート (Datasheet)

使用する特定のAVRマイコン(例: ATmega328P, ATtiny85)のデータシートも必読です。

  • 内容: ピン配置、電気的特性、メモリマップ、各内蔵ペリフェラル(タイマー、ADC、UART、SPI、I2Cなど)の詳細な機能、関連するI/Oレジスタとその設定方法などが記載されています。
  • 重要性: アセンブリから特定のハードウェア機能を制御するためには、データシートのレジスタ情報が不可欠です。
  • 入手先: Microchip Technology公式サイトで、使用するデバイス名(例: “ATmega328P”)で検索します。

オンラインリソースとコミュニティ

  • チュートリアルサイト:
    • AVR Freaks: AVRに関するフォーラム、プロジェクト、チュートリアルなどが豊富なコミュニティサイト(英語)。
    • Instructables AVR Assembler Tutorials: コマンドラインツールを使った実践的なチュートリアル集(英語)。
    • 国内の個人ブログや技術サイトにも、AVRアセンブリに関する記事が多数存在します(例: 「AVR アセンブリ チュートリアル」などで検索)。
  • 書籍: AVRマイコンやアセンブリ言語に関する入門書や解説書も参考になります(ただし、情報は最新か確認が必要です)。

実践的なプロジェクト

知識を定着させる最良の方法は、実際に手を動かして何かを作ってみることです。

  • タイマーを使った正確なLED点滅: 遅延ループではなく、内蔵タイマーを使ってより正確な周期でLEDを点滅させてみましょう。タイマー割り込みを使うと、点滅させながら他の処理も行えます。
  • スイッチ入力の読み取り: スイッチを接続し、その状態を読み取ってLEDの点灯・消灯を制御します。チャタリング対策(ソフトウェアデバウンス)も実装してみましょう。
  • 7セグメントLEDの制御: 数字を表示できる7セグメントLEDを制御してみます。ダイナミック点灯方式にも挑戦してみましょう。
  • UART通信: PCとシリアル通信を行い、マイコンからメッセージを送信したり、PCからのコマンドを受信したりします。
  • ADC (アナログ-デジタル変換): 可変抵抗(ポテンショメータ)やセンサーからのアナログ電圧を読み取り、その値に応じてLEDの明るさを変える(PWM制御)などの処理をします。

小さなプロジェクトから始めて、徐々に複雑なものに挑戦していくことで、AVRアセンブリのスキルを着実に向上させることができます。

この記事では、AVRアセンブリ言語の入門として、その基礎となるAVRマイコンのアーキテクチャ、開発環境の準備、基本的な命令セット、そして簡単な「Lチカ」プログラムの作成とデバッグ手法について解説しました。

アセンブリ言語の学習は、一見すると難しく、現代のソフトウェア開発の主流からは外れているように見えるかもしれません。しかし、その学習を通じて得られるハードウェアへの深い理解、コード最適化の技術、そして問題解決能力は、どのようなレベルのプログラミングにおいても必ず役立つ貴重なスキルとなります。特にリソースが限られた組み込みシステムの世界では、アセンブリの知識が決定的な差を生むこともあります。

最初は戸惑うことも多いかもしれませんが、命令セットマニュアルやデータシートを片手に、実際にコードを書き、シミュレータや実機で動かしてみる経験を重ねることが重要です。小さな成功体験を積み重ねながら、AVRアセンブリの世界を探求してみてください。きっと、コンピュータがどのように動いているのか、その本質に触れる面白さを発見できるはずです。

Happy Assembling!

参考情報

より詳細な情報については、以下の公式ドキュメント等を参照してください。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です