【2024年】AVR-Rust 開発環境構築 3 (avr-hal, avr-device, ATmega 編)

# 1: はじめに

Rust で AVR マイコンをプログラミングする方法はいくつかあります。

* [avr-rust/ruduino](https://github.com/avr-rust/ruduino/)
  * 感触として、とりあえず Aruduino UNO をそのまま動かしたい方向け
  * 割り込みも使用できます
* [Rahix/avr-hal](https://github.com/Rahix/avr-hal)
  * 感触として、割り込みを意識せず抽象度高めに書きたい方向け
  * 本記事で1番目に紹介する方法
* [Rahix/avr-device](https://github.com/Rahix/avr-device)
  * 割り込みハンドラなどを具体的に動かすときに便利（抽象的ではない）
  * 本記事で2番目に紹介する方法

ここでは、ruduino ではなく avr-hal と avr-device を使った AVR-Rust プログラミングを試しています。

* AVR-Rust 環境構築
  1. [Rust nightly コンパイラ, avr-gcc 編](https://qiita.com/BerandaMegane/items/ae2b35d3c8411df5a506)
  1. [ruduino, ATmega328P 編](https://qiita.com/BerandaMegane/items/24aa106179eac2f3e639)
  1. 本記事（avr-hal, avr-device, ATmega 編）

こちらの環境は Windows 11 ですが、avr-gcc がインストールされ、ruduino の Blink サンプルがビルドできる状態であれば、どの OS でも問題ないと思われます。Blink サンプルをビルドするまでの手順は、前の記事を参考にしてください。

## 1-1: 本資料を活用できる対象者

Rust が初めてでも、資料を読みながらコマンド操作できれば問題ありません。

* AVR マイコンを C/C++ でプログラミングしたことがある方
* シェルやコマンドプロンプトでのコマンド操作にある程度慣れている方

## 1-2: 構築環境

外部ライブラリとして avr-hal を使用しています。

* avr-hal
    * GitHub - [Rahix/avr-hal](https://github.com/Rahix/avr-hal)
    * avr-hal のサブライブラリ
      * Arduino UNO 互換ボードで動かすなら arduino-hal
      * ATmega 系マイコンを抽象的に動かすなら atmega-hal
      * ATtiny 系マイコンを抽象的に動かすなら attiny-hal
* avr-device
    * GitHub - [Rahix/avr-device](https://github.com/Rahix/avr-device)

その他の環境

* Rust toolchain Nightly 2024-03-22
  * 後述しますが **バージョンによって、動いたり、動かなかったりします** ので、日付指定をおすすめします
* AVR ATmega328P-PU
  * 動作確認したマイコン
  * https://akizukidenshi.com/catalog/g/g103142/

```toml:rust-toolchain.toml
[toolchain]
channel = "nightly-2024-03-22"  # 動いた
components = ["rust-src"]
profile = "minimal"
```

# 2: Rust nightly コンパイラと avr-gcc インストール手順

こちらの記事で紹介しています。

https://qiita.com/BerandaMegane/items/ae2b35d3c8411df5a506

# 3: プロジェクトのビルド

## 3-1: aruduino-hal の利用 (avr-hal-template)

まずは、正常にビルドできるかどうかを確かめるため、公式ガイドを参考に、テンプレートからプロジェクトを作成してみます。
ここでは、プロジェクト名 avr-hal-blink として作成します。

https://github.com/Rahix/avr-hal-template

次のようなコマンドを実行し、プロジェクトを作成します。

```:必要なツールのインストール・実行
$ cargo install cargo-generate
$ cargo generate --git https://github.com/Rahix/avr-hal-template.git
（新しいプロジェクト名を指定します）
 Project Name: avr-hal-blink                                                                                 
 Destination: D:\user\Documents\Rust\avr-hal-blink ...
 project-name: avr-hal-blink ...
 Generating template ...
（テンプレートとして Arduino UNO を選択します）
✔  Which board do you use? · Arduino Uno
 Moving generated files into: `D:\user\Documents\Rust\avr-hal-blink`...
 Initializing a fresh Git repository
 Done! New project created D:\user\Documents\Rust\avr-hal-blink
```

カレントディレクトリの下に、次のようなディレクトリが生成されました。

* avr-hal-blink
  * .cargo/
  * avr-specs/
  * src/
  * Cargo.toml
  * rust-toolchain.toml

AVR-Rust は Nightly コンパイラのバージョンによって動いたり、動かなかったりします。バージョンは、rust-toolchain.toml ファイルで指定します。

```toml:rust-toolchain.toml
[toolchain]
channel = "nightly-2024-03-22"  # 最初から指定されていた
components = ["rust-src"]
profile = "minimal"
```

Arduino UNO は、マイコンに ATmega328P を使用していますが、ターゲットファイルは次のように指定されています。
```toml:.cargo/config.toml
[build]
target = "avr-specs/avr-atmega328p.json"  # ここで指定

[target.'cfg(target_arch = "avr")']
runner = "ravedude uno -cb 57600"

[unstable]
build-std = ["core"]
```

avr-atmega328p.json の中身は、`rustc --print target-spec-json -Z unstable-options --target avr-unknown-gnu-atmega328 > avr-atmega328p.json` の結果と似たような記述がされていました。

実際のプログラムですが、main.rs の中身は次のような D13 (PB5) の LED がチカチカするプログラムです。
```rust:main.rs
#![no_std]
#![no_main]

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    /*
     * For examples (and inspiration), head to
     *
     *     https://github.com/Rahix/avr-hal/tree/main/examples
     *
     * NOTE: Not all examples were ported to all boards!  There is a good chance though, that code
     * for a different board can be adapted for yours.  The Arduino Uno currently has the most
     * examples available.
     */

    let mut led = pins.d13.into_output();

    loop {
        led.toggle();
        arduino_hal::delay_ms(1000);
    }
}
```

実際にビルドしてみましょう。
```
$ cd avr-hal-blink/
$ cargo build --release
（rust-toolchain.toml で指定した Nightly コンパイラでビルドされる）
```

生成された `target/avr-atmega328p/release/avr-hal-blink.elf` を AVR マイコンへ書き込むと、LED が正常にチカチカしました。

## 3-2: atmega-hal の利用

Arduino UNO を使用していれば、先ほどのテンプレートのままでも良いのですが、自作の基板で動作させている場合や動作クロックを変更したい場合は、カスタマイズが必要になります。
ここでは、ATmega328P を動作クロック 20MHz で使用する前提でカスタマイズしてみます。

* サンプル (ATmega2560) [GitHub - Rahix/avr-hal/examples/atmega2560](https://github.com/Rahix/avr-hal/tree/main/examples/atmega2560)
* リファレンス - [atmega-hal](https://rahix.github.io/avr-hal/atmega_hal/index.html)

どうやってカスタマイズするのかというと、avr-hal の ReadMe [repository-structure](https://github.com/Rahix/avr-hal/tree/main?tab=readme-ov-file#repository-structure) を参考にします。

次の画像に載っていますが、現在のプロジェクトは arduino-hal を使用しています。
ここでは atmega-hal を使うようにカスタマイズします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125577/4ec82883-8eb1-50ec-c8bc-4e493a50855c.png)

[ATmega2560 サンプル Cargo.toml](https://github.com/Rahix/avr-hal/blob/main/examples/atmega2560/Cargo.toml) を参考にしながら、atmega-hal を使用するように Cargo.toml をカスタマイズしてみましょう。

Cargo.toml の [dependencies.arduino-hal] を削除し、[dependencies.embedded-hal-v0] と [dependencies.atmega-hal] を追加します。
```toml:Cargo.toml
# [dependencies.arduino-hal]  # 削除
# git = "https://github.com/rahix/avr-hal"
# rev = "3e362624547462928a219c40f9ea8e3a64f21e5f"
# features = ["arduino-uno"]

[dependencies.embedded-hal-v0]  # 追加
version = "0.2.3"
package = "embedded-hal"

[dependencies.atmega-hal]  # 追加
git = "https://github.com/rahix/avr-hal"
rev = "3e362624547462928a219c40f9ea8e3a64f21e5f"
features = ["rt", "atmega328p"]
```

サンプルの通りではありませんが、さまざまなサイトを参考にした結果 atmega-hal の features を `features = ["rt", "atmega328p"]` にすると上手く行きました。

[ATmega2560 サンプル main.rs](https://github.com/Rahix/avr-hal/blob/main/examples/atmega2560/src/bin/atmega2560-blink.rs) を参考に main.rs を書き換えます。
```rust:main.rs
#![no_std]
#![no_main]

use embedded_hal::delay::DelayNs;
use panic_halt as _;

// Define core clock. This can be used in the rest of the project.
type CoreClock = atmega_hal::clock::MHz20;  // 動作クロックを修正
type Delay = atmega_hal::delay::Delay<crate::CoreClock>;

// Below are examples of a delay helper functions
fn delay_ms(ms: u16) {
    Delay::new().delay_ms(u32::from(ms))
}

#[allow(dead_code)]
fn delay_us(us: u32) {
    Delay::new().delay_us(us)
}

#[atmega_hal::entry]   // arduino_hal を atmega_hal に修正
fn main() -> ! {
    let dp = atmega_hal::Peripherals::take().unwrap();
    let pins = atmega_hal::pins!(dp);

    let mut led = pins.pb5.into_output();  // ピン番号を修正

    loop {
        led.toggle();
        delay_ms(1000);
    }
}
```

```
$ cargo build --release
（ビルドする）
```

生成された `target/avr-atmega328p/release/avr-hal-blink.elf` を AVR マイコンへ書き込むと、LED (PB5) が正常にチカチカしました。

また、avr-hal-template を真似して、cargo-generate でテンプレートからすぐにコピーできるようにしてみました。

```
cargo generate --git https://github.com/BerandaMegane/atmega-hal-template.git
```

https://github.com/BerandaMegane/atmega-hal-template

## 3-2: avr-device の利用

atmega-hal を使用することで抽象度の高いプログラムを書けますが、割り込み処理など組み込み特有の処理は、avr-device を使用した抽象度の低い（低レイヤ、低レベル）プログラムになります。これをベアメタル開発と呼びます。
avr-device は低レイヤのペリフェラルアクセスクリート (PAC: Peripheral Access Crate) だそうですが、レジスタレベルでアクセスすることができます。

ここでは、arduino-hal (avr-hal-template) でLチカを確認したあと、atmega-hal と avr-device を組み合わせたプログラミングを行うための環境構築を紹介します。

Cargo.toml に、avr-device の depepndencies を追加します。追加後のファイル全体は次のとおりです。

```rust:Cargo.toml (変更後)
[package]
name = "avr-device-practice"
version = "0.1.0"
edition = "2021"

#[[bin]]
#name = "atmega-hal-practice"
#test = false
#bench = false

[dependencies]
panic-halt = "0.2.0"
ufmt = "0.2.0"
nb = "1.1.0"
embedded-hal = "1.0"

[dependencies.avr-device]  # 追加
version = "0.5.4"
features = ["atmega328p"]

[dependencies.embedded-hal-v0]  # 追加 (atmega-hal で必要)
version = "0.2.3"
package = "embedded-hal"

[dependencies.atmega-hal]  # 追加 (atmega-hal で必要)
git = "https://github.com/rahix/avr-hal"
rev = "3e362624547462928a219c40f9ea8e3a64f21e5f"
features = ["rt", "atmega328p"]

# The latest releases of `proc-macro2` do not support the rust toolchain that
# we use.  Thus, we must fix this dependency to an older version where our
# toolchain is still supported.  See https://github.com/Rahix/avr-hal/issues/537
[build-dependencies.proc-macro2]
version = "=1.0.79"

# Configure the build for minimal size - AVRs have very little program memory
[profile.dev]
panic = "abort"
lto = true
opt-level = "s"

[profile.release]
panic = "abort"
codegen-units = 1
debug = true
lto = true
opt-level = "s"
```

main.rs を次のように書き換えます。試行錯誤の結果なので、あまり解説はできません。
```rust:main.rs (変更後)
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

// 動作クロック周波数
use embedded_hal::delay::DelayNs;
type CoreClock = atmega_hal::clock::MHz1;
type Delay = atmega_hal::delay::Delay<crate::CoreClock>;

#[allow(dead_code)]
fn delay_ms(ms: u16) {
    Delay::new().delay_ms(u32::from(ms))
}

#[allow(dead_code)]
fn delay_us(us: u32) {
    Delay::new().delay_us(us)
}

// atmega_hal シリアル通信
use atmega_hal::{clock::Clock, pac, usart::{Baudrate, Usart}};

// パニックハンドラ
#[cfg(not(doc))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    // 割り込み無効
    avr_device::interrupt::disable();
    let dp = unsafe {avr_device::atmega328p::Peripherals::steal()};
    loop {
        const BLINK_HZ: u32 = 10;
        avr_device::asm::delay_cycles(CoreClock::FREQ / BLINK_HZ);
        dp.PORTB.portb.modify(|_, w| w.pb4().set_bit());
        avr_device::asm::delay_cycles(CoreClock::FREQ / BLINK_HZ);
        dp.PORTB.portb.modify(|_, w| w.pb4().clear_bit());
    }
}

// エントリポイント
// note: VSCode の rust-analyzer が赤線を出すため、別関数に分割
#[atmega_hal::entry]
fn main() -> ! {
    main_proc();
}

fn main_proc() -> ! {
    // ペリフェラルの取得
    let dp = unsafe {avr_device::atmega328p::Peripherals::steal()};

    // GPIO 出力設定
    // read-modify-write を行うため、write ではなく modify の必要がある
    // write だと、PB4 への操作しか反映されない
    dp.PORTB.ddrb.modify(|_, w| w.pb5().set_bit());
    dp.PORTB.ddrb.modify(|_, w| w.pb4().set_bit());
    
    // タイマ設定
    timer1_init();
    
    // シリアル設定
    let serial_dp = unsafe {atmega_hal::Peripherals::steal()};
    let pins = atmega_hal::pins!(serial_dp);
    let mut serial = Usart::new(
        serial_dp.USART0,
        pins.pd0,
        pins.pd1.into_output(),
        Baudrate::<crate::CoreClock>::new(9600)
    );
    ufmt::uwriteln!(&mut serial, "Hello from ATmega!\r\n").unwrap();

    // 全割り込み許可
    unsafe {avr_device::interrupt::enable();}  // sei();

    let mut led_state: bool = false;
    let mut count = 0;
    loop {
        count += 1;
        ufmt::uwriteln!(&mut serial, "software loop {}\r", count).unwrap();

        // LED 反転
        led_state = ! dp.PORTB.portb.read().pb4().bit();
        dp.PORTB.portb.modify(|_, w| w.pb4().bit(led_state));

        delay_ms(1000);
    }
}

// Timer1 初期設定
fn timer1_init() {
    let dp = unsafe {avr_device::atmega328p::Peripherals::steal()};
    let ticks = (CoreClock::FREQ / 1000 - 1) as u16;
    // タイマクリア
    dp.TC1.tcnt1.write(|w| w.bits(0));
    // 比較値
    dp.TC1.ocr1a.write(|w| w.bits(ticks));
    // 分周なし、直接カウント
    dp.TC1.tccr1b.write(|w| w.wgm1().bits(1));
    // CTCモード
    dp.TC1.tccr1a.write(|w| unsafe {w.bits(0)});
    // タイマ比較A 割り込み許可
    dp.TC1.timsk1.write(|w| w.ocie1a().set_bit());
    // タイマカウントスタート
    dp.TC1.tccr1b.modify(|_, w| unsafe {w.cs1().bits(1)});
}

// タイマ1 比較A 割り込みハンドラ
// note: VSCode の rust-analyzer が赤線を出すため、別関数に分割
#[avr_device::interrupt(atmega328p)]
fn TIMER1_COMPA() {
    timer1_compa_proc();
}

fn timer1_compa_proc() {
    static mut COUNTER: u32 = 0; 
    unsafe {
        // ペリフェラル取得
        let dp = avr_device::atmega328p::Peripherals::steal();

        // カウント
        COUNTER += 1;
        if COUNTER > 100 {
            COUNTER = 0;

            // LED　反転
            let state = ! dp.PORTB.portb.read().pb5().bit();
            dp.PORTB.portb.modify(|_, w| w.pb5().bit(state));
        }
    }
}
```

生成された .elf ファイルを AVR マイコンへ書き込むと、LED (PB5, PB4) が正常にチカチカしました。

* PB5 はタイマ1による割り込み処理で 5 Hz で点滅
* PB4 はソフトウェアのディレイ処理で 0.5 Hz で点滅
* UART で文字列を送信
* プログラムにバグがありパニックを起こしたときは、パニックハンドラの処理によって PB4 が 10 Hz で点滅

余裕があれば、プログラム解説記事も書きたいのですが、参考にした Example を紹介します。

* Cargo.toml について
  * ベース arduino-hal [GitHub - avr-hal-template](https://github.com/Rahix/avr-hal-template)
  * atmega-hal 追加部分 [GitHub - avr-hal/examples/atmega2560](https://github.com/Rahix/avr-hal/tree/main/examples/atmega2560) 
  * avr-device 追加部分 [crates.io - avr-device](https://crates.io/crates/avr-device)
* main.rs について
  * avr-device 追加部分 [GitHub - avr-device/exapmles/atmega328p](https://github.com/Rahix/avr-device/blob/main/examples/atmega328p/src/main.rs)
  * [Zenn - Rustで作るリアルタイムOS](https://zenn.dev/denham/articles/00a3158bc1a27a)

再度 avr-hal-template を真似して、cargo-generate でテンプレートからすぐにコピーできるようにしてみました。

https://github.com/BerandaMegane/rust-atmega328p-template

# 4: 最後に

この資料を書いているのは、書籍 [実践Rustプログラミング入門](https://www.shuwasystem.co.jp/book/9784798061702.html) を読み始めた程度の Rust 言語初心者です。書籍を読むと、Examples には書かれていない Rust 用語が分かりやすく紹介されているので助かります。
未だにクレートって何？という状態ではありますが、AVR マイコンを Rust で開発するまでの環境構築をまとめてみました。
AVR マイコンで Rust を動かそうと試行錯誤している人の助けになれば幸いです。私は試行錯誤に2週間かかりました。