【Arduino工作〜発展編】AVR-RustでAtmega328pをLチカ!レベル2


2021/10/27
2021/11/04
蛸壺の中の工作室|AVR-RustでAtmega328pをLチカ!レベル2

前回(RustでATmega328pのプログラムをビルドしてみる)の内容で、AVR-RustプロジェクトのArduino Uno向けのラッパーライブラリであるruduinoを使ってLチカを試しました。

今回の記事では、このruduinoの内部の実装を参考にしながら、もう少しだけ応用範囲を拡げられるように、AVR-Rustライブラリを使った1レベル上のLチカを実装する手順を紹介します。


マイコンの書込構成(おさらい)

サラのAtmega328pにプログラムを書き込む際に利用するピンと、書込装置を正しく接続することが重要になってきます。

以前の記事でも述べたようにSPI(ISP)モードでのAVRマイコンへの書き込みは以下の図の6つのピンを利用します。

合同会社タコスキングダム|蛸壺の中の工作室

AVRマイコンをSPI(ISP)モードで書き込む場合には、専用ライターが必要になります。

どのようなAVRマイコンでも安定して書き込めるように、また何かあったらヒューズビットも書き込みできるように、例えば
ISP MkIIのような製品を少し高価でも一つ持っておくと良いと思います。

著者の場合はAVRだけでなくSAMも使うので正規品のAtmel-ICEを使っています。Atmel-ICEはロイヤリティもありこちらはかなり高価ですが、Cortexベースの組込開発にもスムーズに移行できるのでオススメです。

正規品をセールスで出ているのを狙って買うか、以下のようなサードパーティ製の互換品を購入することもできます。

ちなみに付属品のJTAGケーブルのアクセサリーは失くすと、色んなプログラミングモードでピンの番号の対応を確認しなくてはならないという地味に辛い作業をしなくてはならないので、大切に保管しましょう。

以下は、Atmel-ICEとAVRマイコンの接続例を模した図になります。

合同会社タコスキングダム|蛸壺の中の工作室

avrdudeの準備

AVRマイコンへの実行プログラム書き込みを行うコマンドはavrdudeになります。

前回の記事ではLinuxOS環境へのインストール手順を説明しておりましたので、詳細はそちらでご確認ください。

書込装置とマイコンへの接続をテストするためにAtmega328pのメモリ情報を出力します。

            $ sudo avrdude -c atmelice_isp -P usb -p m328p -v
avrdude: Version 6.3-20171130
         Copyright (c) 2000-2005 Brian Dean, http://www.bdmicro.com/
         Copyright (c) 2007-2014 Joerg Wunsch

         System wide configuration file is "/etc/avrdude.conf"
         User configuration file is "/root/.avrduderc"
         User configuration file does not exist or is not a regular file, skipping

         Using Port                    : usb
         Using Programmer              : atmelice_isp
avrdude: Found CMSIS-DAP compliant device, using EDBG protocol
         AVR Part                      : ATmega328P
         Chip Erase delay              : 9000 us
         PAGEL                         : PD7
         BS2                           : PC2
         RESET disposition             : dedicated
         RETRY pulse                   : SCK
         serial program mode           : yes
         parallel program mode         : yes
         Timeout                       : 200
         StabDelay                     : 100
         CmdexeDelay                   : 25
         SyncLoops                     : 32
         ByteDelay                     : 0
         PollIndex                     : 3
         PollValue                     : 0x53
         Memory Detail                 :

                                  Block Poll               Page                       Polled
           Memory Type Mode Delay Size  Indx Paged  Size   Size #Pages MinW  MaxW   ReadBack
           ----------- ---- ----- ----- ---- ------ ------ ---- ------ ----- ----- ---------
           eeprom        65    20     4    0 no       1024    4      0  3600  3600 0xff 0xff
           flash         65     6   128    0 yes     32768  128    256  4500  4500 0xff 0xff
           lfuse          0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           hfuse          0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           efuse          0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           lock           0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           calibration    0     0     0    0 no          1    0      0     0     0 0x00 0x00
           signature      0     0     0    0 no          3    0      0     0     0 0x00 0x00

         Programmer Type : JTAG3_ISP
         Description     : Atmel-ICE (ARM/AVR) in ISP mode
         Vtarget         : 5.0 V
         SCK period      : 8.00 us

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: safemode: lfuse reads as 62
avrdude: safemode: hfuse reads as D9
avrdude: safemode: efuse reads as FF

avrdude: safemode: lfuse reads as 62
avrdude: safemode: hfuse reads as D9
avrdude: safemode: efuse reads as FF
avrdude: safemode: Fuses OK (E:FF, H:D9, L:62)

avrdude done.  Thank you.
        
エラーもなくAtmega328pのメモリにアクセスできているようなので、これで書き込み準備はOKです。

続いてRustでのプログラムの実装をやっていきましょう。


Rustでのプログラム実装

まず今回のプロジェクトの構造は以下のようにしています。

            $ tree
.
├── Cargo.toml
├── src
│   └── main.rs
└── avr-atmega328p.json
        
これはほとんどruduinoでやったときプロジェクト構造と変わらないというか、もともとこれがAVR-Rustの作法であるので、当然と言えば当然です。

まずはruduinoを参考にしながら
Cargo.tomlの中身を作成します。

            [package]
name = "std-blink"
version = "0.1.0"
authors = ["tacoskingdom<contact@tacoskingdom.com>"]
edition = "2018"

[dependencies]
avr-std-stub = "1.0.2"
avrd = "0.3.1"
avr_delay = { git = "https://github.com/avr-rust/delay" }
        
パッケージの名前は適当にstd-blinkという名前にしておきます。

次にAtmega328p用にツールチェーンやビルド設定ファイル・
avr-atmega328p.jsonを作成します。

            {
    "arch": "avr",
    "cpu": "atmega328p",
    "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8",
    "env": "",
    "executables": true,
    "linker": "avr-gcc",
    "linker-flavor": "gcc",
    "linker-is-gnu": true,
    "llvm-target": "avr-unknown-unknown",
    "no-compiler-rt": true,
    "os": "unknown",
    "position-independent-executables": false,
    "exe-suffix": ".elf",
    "eh-frame-header": false,
    "pre-link-args": {
      "gcc": ["-mmcu=atmega328p"]
    },
    "late-link-args": {
      "gcc": ["-lgcc", "-lc"]
    },
    "target-c-int-width": "16",
    "target-endian": "little",
    "target-pointer-width": "16",
    "vendor": "unknown"
}
        
このファイルについてはこちらでも触れましたが、ターゲットCPU毎に作り直す必要があります。

Atmega328p以外のAVRマイコンを使う場合には、そちらを参考にしてください。

avr_delayライブラリについて

※ 2021/11/04更新

C組込では良く利用する
delayメソッドも、AVR-RustでRustプログラムを組み直す場合には、avr-delayライブラリに置き換えられるか考える必要があります。

このライブラリは
delay / delay_ms / delay_usの3つのメソッドを持つシンプルなものですが、じっくり内部構造を見ると、delay_ms / delay_usメソッドはまた別のavr-configというライブラリに依存していることが分かります。

この
avr-configは環境変数として定義したAVR_CPU_FREQUENCY_HZの値を読み取って、ライブラリを組み入れたRustプロジェクトで統一的に利用できるようになる仕組みです。

手元の開発環境によっては、dockerコンテナ内部でのRustupビルドであったり、Nightly Rustであったりすると、上手くavr-configが環境変数を読み取ってくれないような挙動があって、
delay_ms及びdelay_usの利用は信用性が弱いと感じました。

ですので、avr-delayライブラリを利用したい場合には、
delayメソッドを積極的利用する方が好ましいと言えます。

delayメソッドで悩ましいのは、その引数の用法と実遅延時間の換算方法です。

delayメソッドの実装を良く観察すると、以下のようになっています。

            pub fn delay(count: u32) {
    // Our asm busy-wait takes a 16 bit word as an argument,
    // so the max number of loops is 2^16
    let outer_count = count / 65536;
    let last_count = ((count % 65536)+1) as u16;
    for _ in 0..outer_count {
        // Each loop through should be 4 cycles.
        unsafe {llvm_asm!("1: sbiw $0,1
                      brne 1b"
                     :
                     : "w" (0)
                     :
                     :)}
    }
    unsafe {llvm_asm!("1: sbiw $0,1
                      brne 1b"
                 :
                 : "w" (last_count)
                 :
                 :)}
}
        
引数のcountがforループのイタレーション数を表しています。

ループ内でasm命令を直接呼び出すことで、ループ1回に付き4クロックだけ進みます。※厳密に言うと初回は13クロックほどのオーバヘッドクロック数が消費されています。よほど時間の厳密性が気になるならば初回のクロック数も考慮することになります。

またこのループは
u16分の216=655362^{16} = 65536カウントで一周します。

例えば、マイコンのシステムクロック数が
16MHzであったとするとループ一回にかかる時間Δt\Delta tは、

Δt=416 [MHz]=0.25 [μs]\displaystyle{ \begin{aligned} \Delta t &= \frac{4}{16\ \mathrm{[MHz]}} \\ &= 0.25\ \mathrm{[\mu s]} \end{aligned} }Eq. (1)

となり、ここからdelayメソッドで1秒を待ちたい場合には、

1Δt=1[s]0.25[μs]=4×106[Counts]\displaystyle{ \begin{aligned} \frac{1}{\Delta t} = \frac{1 \mathrm{[s]}}{0.25 \mathrm{[\mu s]}} &= 4 \times 10^{6} \mathrm{[Counts]} \end{aligned} }Eq. (2)

と換算できます。

つまりクロック周波数16MHzなら
delay(4000000)で1秒間遅延できる、というように考えます。

以下の表は各周波数ごとの1秒待つ際のdelayメソッド早見表になります。

クロック数[Hz]

カウント数/1s

16M

4000000

8M

2000000

4M

1000000

2M

500000

1M

250000

0.5M

125000

main.rsの実装

ではいよいよ本記事の主題であったAVR-Rustのプログラミングの肝の部分であるsrc/main.rsの中身を作成していきましょう。

今回はポートBの
PB1からLチカさせてみます。

一般なc言語でのマイコンのプログラミングにおいて、生のIOレジスタで入出力制御させるために
ビット演算が欠かせません。

別のブログの記事でビット演算の基礎をまとめたので、あまりビット演算の親しみの無い方はそちらもご覧ください。

なお、Atmega328pを使った開発を進める上で、ruduinoのソースコードを良く眺めると色々と実装のヒントになる部分が多いです。

Atmega328pのコアライブラリ(Rust実装)である
ruduino/src/cores/atmega328p.rsをじっくり眺めると、DDRB(レジスタアドレス: 0x24)PORTB(レジスタアドレス: 0x25)が既に定義されていることも分かります。

どうプログラミング分からなくなったら、ruduinoの内部実装を観察してみてください。

では以下にPB1ポートからLチカするためのコード例を示します。

            #![feature(llvm_asm, lang_items, unwind_attributes)]
#![no_std]
#![no_main]

extern crate avr_delay;
extern crate avr_std_stub;
extern crate avrd;

//👇①
use avrd::atmega328p::{DDRB, PORTB};

use avr_delay::{delay};

//👇Rust組込関数のwrite_volatileからメモリへの書き込みを行う
use core::ptr::write_volatile;

#[no_mangle]
pub extern "C" fn main() {
    unsafe {
        //👇②
        write_volatile(DDRB, 0b00000010);
    }
    let mut out = 0b00000000;
    loop {
        unsafe {
            //👇③
            write_volatile(PORTB, out);
        }
        //システムクロック周波数1MHzで1秒待つ
        delay(250000);
        //👇④
        out ^= 1 << 1;
    }
}
        

Rust独特の文法まで解説していくのは不可能ですので、Rust自体の勉強は別でやってもらうとして、今回は実装の主要なポイントだけ解説します。

まず①の箇所では、
DDRBPORTBのアドレス番号を全てのAVRマイコンのデバイス定義を全て網羅してあるライブラリ・avrdから取得しています。

②の箇所で
write_volatile関数から、DDRBへの書き込みをしています。

PB1はDDRBの1ビット目のピンですので、
0b00000010もしくは0x02を書き込むことで、PB1が出力ポートに設定されます。

③の箇所も同様で、
write_volatile関数から、PORTBのレジスタにデータを書き込んでPB1ポートをON・OFFしています。

あとは1秒間隔でLチカさせるように、無限ループ内で、
delay(250000)を挟んで、④の箇所でPB1だけビット反転させています。

c言語での実装と比べて若干雰囲気が違いますが、Rustでもおおよそc言語と近い感覚で組込開発が可能になっています。

ビルドしてみる

Cargoによるビルドの手順は前回の記事内容と同様です。

詳しいCargoビルドの話はそちらを見ていただくとして、ここでは先程のRustによるソースコードをビルドしてみます。

            $ export AVR_CPU_FREQUENCY_HZ=16000000
$ cargo build -Z build-std=core --target avr-atmega328p.json --release
    Updating crates.io index
    Updating git repository `https://github.com/avr-rust/delay`
  Downloaded proc-macro2 v1.0.30
  #...中略
  Downloaded 13 crates (1.8 MB) in 0.72s
   Compiling std-blink v0.1.0 (/usr/src/app/std-blink)
    Finished release [optimized] target(s) in 0.31s
        
上手くビルドが成功すると、target/[デバイスのタイトル]/releaseフォルダ以下に[パッケージ名].elfというバイナリファイルが生成されるはずです。


実行ファイルの書き込み

先程ビルドしてtarget/avr-atmega328p/releaseフォルダ以下に生成されたstd-blink.elfを書き込んでみます。

あとこのstd-blink.elfをavrdudeコマンドでマイコンに書き込む手順は
別記事で説明した内容と一緒です。

以下はAtmel-ICEからマイコンへ書き込むときのコマンド例です。

            $ sudo avrdude -p m328p -c atmelice_isp -P usb -U flash:w:target/avr-atmega328p/release/std-blink.elf:e

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "target/avr-atmega328p/release/std-blink.elf"
avrdude: writing flash (2446 bytes):

Writing | ################################################## | 100% 0.72s

avrdude: 2446 bytes of flash written
avrdude: verifying flash memory against target/avr-atmega328p/release/std-blink.elf:
avrdude: load data flash data from input file target/avr-atmega328p/release/std-blink.elf:
avrdude: input file target/avr-atmega328p/release/std-blink.elf contains 2446 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.72s

avrdude: verifying ...
avrdude: 2446 bytes of flash verified

avrdude: safemode: Fuses OK (E:FF, H:D9, L:62)

avrdude done.  Thank you.
        
書き込みに成功するとすぐにPB1からLチカが始まります。

合同会社タコスキングダム|蛸壺の中の工作室

なお最終的には外部の16MHz水晶振動子からクロック周波数をもらうつもりですが、まだ発振器には未接続の状態です。

書き込んだ直後は8MHz校正用内部クロック(工場出荷状態では分周1/8なので1MHz)でチカチカさせています。

繰り返しますと、
delay関数はシステムクロック周波数に依存していますので、指定した時間間隔とは食い違いが起こっている場合には、クロック周波数の設定を良く確認してください。


まとめ

以上、最後まで読んでいただきましてありがとうございます。

今回は簡単なAVR-Rustのライブラリ群を利用して、Lチカだけをやってみました。

Rustで組込開発を行うのに、AVRマイコンは最適な学習教材かと思っているので、今後はもうちょい応用的な例を取り上げながら、AVR-Rustのプログラミングを深堀していく予定です。


参考サイト

The AVR-Rust project | Guthub

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。