ochalog

RubyとMediaWikiとIRCが好き。

Raspberry Pi Picoで組込みRust開発(開発環境構築編)

先日の出張の際に、秋葉原千石電商Raspberry Pi Pico(以下「Pico」)が550円で販売されているのを見つけて、思わず買ってしまいました。Picoは公式にはC/C++とMicro Pythonでの開発に対応していますが、プロセッサのRP2040はArm Cortex-M0+プロセッサなので、組込みRust開発ができそうです。調べてみると、既に対応するクレートが開発されており、英語の導入記事がいくつか見つかりました。以前から組込みRust開発に関心があったので、これらを参考にしてまずクロス開発環境の構築から試してみました。

開発環境構築の前に:はんだ付け

Picoは以下の写真のようにピンヘッダが実装されていない状態で販売されています。

f:id:ochaochaocha3:20211226023138p:plain
Pico基板:ピンヘッダが実装されていない

(「Raspberry Pi Pico datasheet」より引用)

通常はブレッドボードに挿して開発するでしょうから、まずピンヘッダをはんだ付けする必要があります。ピンの太さについて、1〜20ピンおよび21〜40ピン(上の写真の横方向に20個ずつ並ぶピン)ではブレッドボードに挿せるように細ピンを、デバッグ用のピン(上の写真の左端、縦方向の3つのピン)では通常の太さを選択します。例えば秋月電子あたりで以下の部品を購入し、内側の穴に挿入してはんだ付けします。

部品名 メーカー 品番 数量 備考
細ピンヘッダ 1×20 Useconn Electronics Ltd. PHA-1x20SG 2
ピンヘッダ 1×40(40P) Useconn Electronics Ltd. PH-1x40SG 1 3ピン分をカットして使用

はんだ付けが難しい場合、価格は少々高くなりますが、スイッチサイエンスからピンヘッダ実装済みのPicoが販売されているので、それを購入するのもよいでしょう。

ピンヘッダのはんだ付けが完了したら、ブレッドボードに挿入します。PCとUSBケーブルで接続するので、ケーブルが出る方の端に配置すると、残りの部分に部品を置きやすくなります。

f:id:ochaochaocha3:20211226023252j:plain
はんだ付け後のPicoをブレッドボードに挿入する

それでは、以下より開発環境を構築していきます。

実行環境

  • Mac mini (2018)
  • macOS Big Sur 11.5.2
  • HomebrewでGitをインストール済み

Macでの手順を記述しますが、Linuxでも同様に実行できると思います。

開発に必要なツールの準備

まず、Rustのツールチェインをはじめとした、開発に必要なツールをインストールします。

Rustツールチェインの準備

https://rustup.rs/ の手順に従い、Rustのツールチェインを準備します。インストール済みの場合は rustup update で更新しておきます。その後、rustuprustccargo といったコマンドを実行できることを確認します。

$ rustup --version
rustup 1.24.3 (ce5817a94 2021-05-31)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.57.0 (f1edd0429 2021-11-29)`

$ rustc --version
rustc 1.57.0 (f1edd0429 2021-11-29)

$ cargo --version
cargo 1.57.0 (b2e52d7ca 2021-10-21)

コンパイルターゲットおよびツールの準備

続いて、プログラムのビルドや転送に必要な、コンパイルターゲットおよびツールをインストールします。

$ rustup target install thumbv6m-none-eabi
$ cargo install flip-link elf2uf2-rs

rustup target install は、指定されたコンパイルターゲットに関連するツールチェインをインストールします。thumbv6m-none-eabi は、Cortex-M0+等を含むターゲットトリプル(「プロセッサアーキテクチャ-OS-ABI」という形式のターゲットプラットフォーム表記)です。none がOSなしを示します。

flip-linkは、組込みプログラム実行時のスタックオーバーフローを防止するため、リンク時にメモリ配置を変更するツールです。

elf2uf2-rsは、ビルドしたプログラムをUF2というPicoに書き込める形式に変換するツールです。また、接続されたPicoを自動で探してプログラムを書き込む機能もあります。

LED点滅(Lチカ)プログラムの開発

開発に必要なツールを用意できたので、ようやく組込みプログラムを開発できます。ここでは定番のLED点滅(Lチカ)プログラムを開発してみましょう。

プロジェクトテンプレートの利用

組込みRust開発では、複数種類のクレート(マイクロアーキテクチャクレート、ペリフェラルアクセスクレート、HALクレート、ボードクレート)を利用します。Pico用としては、ペリフェラルアクセスクレート以下の開発が進められています(GitHub: rp-rs)。Pico用のクレートを使用するプロジェクトテンプレート(GitHub: rp-rs/rp2040-project-template)が用意されていますので、これを基に開発すると便利です。また、このテンプレートにはPico基板上のLEDを1秒間隔で点滅(500 ms点灯、500 ms消灯)させるプログラムが含まれています。今回はこれを利用してLチカプログラムを開発します。

プロジェクトテンプレートを利用する際には、cargo-generateが便利です。cargo-generateは、プロジェクトテンプレートのダウンロードおよびプロジェクト用のディレクトリ作成を自動で行ってくれます。まずはこれをインストールします。

$ cargo install cargo-generate

cargo-generateをインストールできたら、実行してプロジェクトを作成します。オプション --git--branch でGitリポジトリおよびブランチを指定します。また、オプション --name でプロジェクト名を指定します。今回はプロジェクト名を pico-blink としてみました。

# 以下、$HOME: ホームディレクトリ、$WORK_DIR: 作業用ディレクトリ

# 作業用ディレクトリに戻る
$ popd

# プロジェクトを作成する
$ cargo generate \
  --git https://github.com/rp-rs/rp2040-project-template \
  --branch main \
  --name pico-blink
⚠️   Unable to load config file: $HOME/.cargo/cargo-generate.toml
🔧   Generating template ...
(中略)
🔧   Moving generated files into: `$WORK_DIR/pico-blink`...
✨   Done! New project created $WORK_DIR/pico-blink

プロジェクトを作成できたら、そのディレクトリに移動します。

$ cd pico-blink
クレートの依存性の修正(必要な場合のみ)

この記事を書いている2021年12月の時点ではPico用のクレートが積極的に更新されており、プロジェクトテンプレートがそれに追従できていない場合があります。例えばボードクレート名が「pico」から「rp-pico」に変更されたときは、テンプレートのままではプロジェクトをビルドできなくなりました(現在は修正済み)。そのような場合は、Cargo.toml[dependencies] 節や src/main.rsuse 宣言を確認・修正します。

参考:Update dependencies to released versions of crates · rp-rs/rp2040-project-template@b03e3ee

Cargo.toml の変更

Lチカプロジェクト用に Cargo.toml を変更します。特に package.name はビルド時の出力ファイル名にも影響しますので、変えておきましょう。

--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,8 +1,8 @@
 [package]
-authors = ["the rp-rs team"]
+authors = ["ocha"] # 自分の名前
 edition = "2018"
 readme = "README.md"
-name = "rp2040-project-template"
+name = "pico-blink" # プロジェクト名
 version = "0.1.0"
 resolver = "2"

この段階で、Gitのコミットを作成しておきます。

$ git init
$ git add -A
$ git commit -m "最初のコミット"

プロジェクトのビルド

プロジェクトテンプレートにLチカプログラムが含まれていますので、そのままビルドしてみましょう。次のコマンドでビルドを実行します。

$ cargo build
# ELFファイル target/thumbv6m-none-eabi/debug/pico-blink が生成される

ビルドに失敗した場合は、開発ツールが適切にインストールされているか見直します。

プログラムのPicoへの書き込みおよび実行

ビルドが成功したら、プログラムをPicoに書き込んで実行してみましょう。PicoはPCにUSB(コネクタはマイクロB型)で接続すると、USBストレージとして認識されます。このストレージにUF2形式のファイルをコピーすれば、開発したプログラムがPicoに書き込まれます。

PicoをPCに接続する際は、以下に示すBOOTSELボタンを押しながら行います。これは、既にPicoにプログラムが書き込まれている場合、そのまま接続するとそのプログラムが実行されてしまい、プログラムを書き込めないためです。BOOTSELボタンを押しながら接続すると、書き込まれたプログラムは実行されず、新たなプログラムを書き込めるようになります。

f:id:ochaochaocha3:20211226023406j:plain
BOOTSELボタンを押しながらUSBでPCに接続

BOOTSELボタンを押しながらPicoを接続すると、Macでは「RPI-RP2」という名前のドライブとしてマウントされました。ここにUF2形式のファイルをコピーして書き込みたいところですが、ビルドで生成されたのはELF形式のファイルですので、そのままでは書き込めません。

先ほどインストールしたツールのうち、elf2uf2-rsはELF形式からUF2形式への変換を行います。また、-d というオプションを付けて実行することで、変換後にPicoのドライブへのコピーを自動で行ってくれます。今回はこれをランナーとして指定して実行します。

ランナーは .cargo/config.toml で指定できます。プロジェクトテンプレートには、elf2uf2-rsを使う場合の設定もコメントの形で用意されています。以下のようにコメントの位置を変更して、elf2uf2-rsを指定します。

--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -2,8 +2,8 @@
 # probe-run is recommended if you have a debugger
 # elf2uf2-rs loads firmware over USB when the rp2040 is in boot mode
 [target.'cfg(all(target_arch = "arm", target_os = "none"))']
-runner = "probe-run --chip RP2040"
-# runner = "elf2uf2-rs -d"
+# runner = "probe-run --chip RP2040"
+runner = "elf2uf2-rs -d"

 rustflags = [
   "-C", "linker=flip-link",

.cargo/config.toml を変更したら、いよいよプログラムの書き込み・実行です。cargo run を実行します。

$ cargo run
    Finished dev [optimized + debuginfo] target(s) in 0.04s
     Running `elf2uf2-rs -d target/thumbv6m-none-eabi/debug/pico-blink`
Found pico uf2 disk /Volumes/RPI-RP2
Transfering program to pico
30.00 KB / 30.00 KB [=====================================] 100.00 % 17.19 MB/s

elf2uf2-rsがUF2ファイルをコピーすると、Picoにプログラムが書き込まれ、自動的にマウント解除されます。書き込み後、すぐにプログラムが実行されます。以下のようにPicoの基板上のLEDが点滅すれば成功です。

LEDの点滅を確認できたら、この状態でコミットします。

$ git commit -a -m 'ランナーとしてelf2uf2-rsを使う'
リセットボタンの設置

Picoにプログラムを書き込む際、毎回USBケーブルを抜き挿ししてリセットするのは面倒です。別の手段として、PicoのRUNピン(ピン番号30)をGNDに落としてリセットする方法があります。この方法ならばUSBケーブルの抜き挿しは不要となり、便利です。

PicoのRUNピンを制御するには、押すとGNDに接続されるタクトスイッチを用意するのが手軽です。次の回路図のようにタクトスイッチを接続して、リセットボタンとします。RUNピンはRP2040内部の約50 kΩの抵抗でプルアップされるので、プルアップ抵抗の追加は不要です。

f:id:ochaochaocha3:20211226023717p:plain
リセットボタンの設置:回路図

以下はブレッドボードでの接続例です。

f:id:ochaochaocha3:20211224042030j:plain
リセットボタンの設置:ブレッドボードでの接続例

接続後、リセットボタンを押してLチカプログラムが再起動することを確認します。また、BOOTSELボタンを押しながらリセットボタンを押すと、PicoのUSBストレージがマウントされてプログラムを書き込めるようになることも確認します。

Lチカプログラムの内容

Lチカプログラムは src/main.rs に記述されています。初期状態のソースコードは以下のとおりです(説明のため、コメントを追加してあります)。内容を見てみましょう。

//! Blinks the LED on a Pico board
//!
//! This will blink an LED attached to GP25, which is the pin the Pico uses for the on-board LED.

// 1. クレートレベルアトリビュート
#![no_std]
#![no_main]

// 2. use宣言
use cortex_m_rt::entry;
use defmt::*;
use defmt_rtt as _;
use embedded_hal::digital::v2::OutputPin;
use embedded_time::fixed_point::FixedPoint;
use panic_probe as _;

// Provide an alias for our BSP so we can switch targets quickly.
// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change.
use rp_pico as bsp;
// use pro_micro_rp2040 as bsp;

use bsp::hal::{
    clocks::{init_clocks_and_plls, Clock},
    pac,
    sio::Sio,
    watchdog::Watchdog,
};

// 3. エントリポイント
#[entry]
fn main() -> ! {
    // 4. 変数の準備、初期設定
    info!("Program start");

    // 4.1. シングルトンパターン
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();

    let mut watchdog = Watchdog::new(pac.WATCHDOG);
    let sio = Sio::new(pac.SIO);

    // 4.2. クロックとPLLの設定
    // External high-speed crystal on the pico board is 12Mhz
    let external_xtal_freq_hz = 12_000_000u32;
    let clocks = init_clocks_and_plls(
        external_xtal_freq_hz,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    // 4.3. ビジーウェイトの抽象化
    let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer());

    // 4.4. ピンの集合
    let pins = bsp::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // 4.5. LEDピン
    let mut led_pin = pins.led.into_push_pull_output();
    // 4. ここまで

    // 5. メインループ
    loop {
        info!("on!");
        led_pin.set_high().unwrap();
        delay.delay_ms(500);
        info!("off!");
        led_pin.set_low().unwrap();
        delay.delay_ms(500);
    }
}

1. クレートレベルアトリビュート

クレートレベルアトリビュートとして、#![no_std] および #![no_main] を使用しています。前者は標準ライブラリとして最低限の機能のみ持つcoreクレートを使用することを、後者は通常の main() 関数を使わないことを宣言します。OSを使用しないフリースタンディング環境では、C言語開発でもよく見るような指定です。

2. use 宣言

use 宣言で、よく使う要素を簡潔に書けるようにしています。cortex_m_rtembedded_halといったCortex-M向け開発で典型的と思われるものに加えて、いくつか見慣れない要素がありました。

defmtは、knurling-rsプロジェクトによる軽量ログ出力フレームワークです。今回は使用していませんが、デバッガ(もう1枚のPicoなど)を接続したときのログ出力で活用できます。panic_probeも、パニック時にデバッガへバックトレースを出力するという点で関連しています。

bsp と別名を付けられている、rp_picoがボードクレートです。コメントで、別名を付けているのはボードクレートを変更しても残りの部分を変更せずに済むようにするためと書かれています。bsp::hal はRP2040のHALクレート(rp2040_hal)、 bsp::hal::pac はRP2040のペリフェラルアクセスクレート(rp2040_pac)です。sio は「Single-cycle IO」の略で、GPIO等に1サイクルでアクセスする機能を提供します。

3. エントリポイント

cortex_m_rt クレートが提供する #[entry] アトリビュートで、最初に実行する関数を指定しています。#![no_main] アトリビュートによって通常の main() 関数を使わないことを宣言したため、これが必要になります。関数名は main でなくても問題ありません。

main() 関数の型 ! はnever型と呼ばれ、値を返さない(return しない)ことを示します。#[entry] アトリビュートがこの型([unsafe] fn() -> !)を要求しています。

4. 変数の準備、初期設定

main() 関数の最初で、変数の準備や初期設定を行っています。let mut pac から let pins までは、お決まりのパターンとなりそうです。

4.1. シングルトンパターン
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();

paccore で見られる take().unwrap() という部分は、Rustでのシングルトンパターンの書き方です。Rustの所有権システムを利用して、インスタンスがここだけに存在することを保証します。これにより、アクセスの競合が発生しなくなります。

4.2. クロックとPLLの設定
let external_xtal_freq_hz = 12_000_000u32;
let clocks = init_clocks_and_plls(
    external_xtal_freq_hz,
    pac.XOSC,
    pac.CLOCKS,
    pac.PLL_SYS,
    pac.PLL_USB,
    &mut pac.RESETS,
    &mut watchdog,
)
.ok()
.unwrap();

init_clocks_and_plls 関数では、クロックとPLL(Phase Locked Loop、周波数を逓倍する回路)を初期状態に設定します。Pico基板に実装されている外部クロックの周波数は12 MHzですが、PLLによって125 MHzまで速くなります。また、USB用に48 MHzクロックも生成します。

4.3. ビジーウェイトの抽象化
let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer());

delaycortex_m::delay::Delay)は、Cortex-MのSysTickタイマを利用したビジーウェイトを提供します。後のメインループ内で、500 msの待機に使われています。new 関数の周波数を指定する引数が読みやすくなっていますが、これはembedded_timeクレートが提供する表現のようです。C++std::chronoみたいですね。

4.4. ピンの集合
let pins = bsp::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
);

pins は、その名のとおりピンの集合の構造体です。ボードクレートのおかげで、Picoのデータシートに書かれているピン名をほぼそのまま使えます

4.5. LEDピン
let mut led_pin = pins.led.into_push_pull_output();

led_pin は、Pico基板上のLEDが接続されているピンを表すインスタンスです。into_push_pull_output() メソッドで、出力ピンとして設定しています。Pico基板上のLEDは正論理となっているので、Hレベルを出力すると点灯し、Lレベルを出力すると消灯します。

5. メインループ

loop { } 内がメインループです。デバッグ出力を除けば、以下のようにおなじみの手順です。

loop {
    // LEDを点灯させる(Hレベル出力)
    led_pin.set_high().unwrap();
    // 500 ms待機する
    delay.delay_ms(500);

    // LEDを消灯させる(Lレベル出力)
    led_pin.set_low().unwrap();
    // 500 ms待機する
    delay.delay_ms(500);
}

出力ピンおよびビジーウェイトが抽象化されているので、とても読みやすいコードになっています。ピンの出力レベル変更(set_high()set_low())は Result 型を返しますが、クレートが安定していればまず失敗しないでしょうから、unwrap()Ok を期待しておきます。

LEDの点灯パターンの変更

LチカプログラムのビルドとPicoへの書き込みができたので、今度は動作を変えてみましょう。例えば、LEDの点灯パターンを次のように変更します。

  1. 以下を3回繰り返す。
    1. 100 ms点灯させる。
    2. 100 ms消灯させる。
  2. 400 ms待機する。

この場合、メインループは次のように書けます。繰り返しには for 式と範囲を使ってみました。RubyやSwiftに近い見た目です。

loop {
    for _ in 0..3 {
        // LEDを点灯させる
        led_pin.set_high().unwrap();
        delay.delay_ms(100);

        // LEDを消灯させる
        led_pin.set_low().unwrap();
        delay.delay_ms(100);
    }

    delay.delay_ms(400);
}

変更後、cargo run で書き込み、実行すると、以下のようにLEDが点滅します。

LEDの点灯パターンが変わったことを確認できたら、この状態でコミットします。

$ git commit -a -m 'LEDの点灯パターンを変更する'

まとめ・感想

Raspberry Pi Picoで組込みRust開発を行うための環境構築手順をまとめました。開発に必要なツールをインストールした後、Pico用のクレートを利用するプロジェクトテンプレートを使用してLチカプログラムを開発しました。プログラムのPicoへの書き込みおよび実行には elf2uf2-rs を使用しました。プログラム書き込み前のUSBケーブルの抜き挿しを省略するため、RUNピンをGNDに落とすリセットボタンを設置しました。

今回は開発環境構築の段階なので、多少のインストール作業があるのは分かっていましたが、その作業が rustupcargo でほぼ完結するというのは想像以上に楽でした。また、v1には届いていないものの、開発時に利用するクレートがしっかりと構成されていて、Rustの表現力の高さを感じることができました。次回はPicoに部品を接続して、GPIOで制御することに挑戦してみたいと思います。

参考文献