Rustで組み込み開発を始めよう!Baker link. Devで簡単スタート

※この画像のプログラムはRustで書かれています

Raspberry Pi財団が独自に開発したARM Cortex M0+デュアルコアのRP2040マイコンをベースに、デバッガを搭載したRust言語学習用開発ボードです。

  • 強力なデバッガ💪: Raspberry Pi Debug Probeと同等のデバッガを搭載し、効率的なデバッグが可能です。
  • 配線要らずのチュートリアル📚: LEDやボタンの配線を一切せずに本チュートリアルを開始できます。初心者でも安心して学べます。
  • 簡単セットアップ🚀: Freeで提供しているBaker link. Envを利用することで、Dockerコンテナ上で動作するポータブルな開発環境を簡単に構築し、すぐに開発を開始できます。

Baker link. Devは、スイッチサイエンスでお買い求めいただけます。 リンク

Baker link. Devを使えば、LEDの点灯からセンサーの読み取りまで、Rustでの組み込み開発を実践的に学ぶことができます。例えば、以下のようなことに挑戦できます:

  • LEDの点滅
  • 温度センサーのデータ取得🌡
  • モーターの制御⚙
  • ディスプレイの表示
  • I2C、SPIインターフェスのデバイスとの通信
  • etc..

さあ、Baker link. DevでRustの組み込み開発を始めましょう!

寄付のお願い

Baker link.にご興味を持っていただきありがとうございます。本プロジェクトは、Baker link. Labのメンバーの皆様からの温かいご支援によって成り立っています。この度、Baker link. Devの製作および販売においては、Baker linkプロジェクトやRust言語、そしてそれを用いた組み込み開発の普及を最優先としたため、利益を追求しておりません。私たちの活動にご関心をお寄せいただけるだけでも大変ありがたく存じますが、もしご寄付を賜ることができましたら、プロジェクトのさらなる発展と技術の普及に大きな力となります。


皆様のご支援が、未来の技術革新を支える礎となります。どうか、私たちの夢を共に実現するために、ご協力をお願い申し上げます。何卒よろしくお願い申し上げます。

開発環境

本チュートリアルでは、Baker link. Envを使ったポータブルなRust環境で組み込み開発を行なって行きます。 Baker link. Envの詳細はこちら

開発環境構成イメージ図 Baker link Env

Baker link. Envは、次のツールを必要とします。

開発環境構築の流れ

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#BB2528',
      'primaryTextColor': '#fff',
      'primaryBorderColor': '#7C0000',
      'lineColor': '#F8B229',
      'secondaryColor': '#006100',
      'tertiaryColor': '#fff'
    }
  }
}%%
flowchart LR
    Output([Rancher DesktopとVisual Studio Codeのインストール])
    Input([probe-rsのインストール])
    Interrup([Baker link. Envのインストール])
    Output --> Input --> Interrup

Rancher DesktopとVisual Stdio Codeのインストール

Rancher Desktop、Visual Studio Codeは、公式リンクのインストーラーでインストールできます。

またprobe-rsは、OSによってインストール方法を異なります。

probe-rsのインストール

Windowsの場合

  1. PowerShell(管理者)を起動して次のコマンドを実行して実行権限を取得します。 コマンド実行後にYを入力してEnterを押してください。
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
  1. 実行権限を取得した後に次のコマンドを実行してprobe-rsをインストールします。
irm https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.ps1 | iex

Macの場合

次のコマンドをターミナルで実行すればprobe-rsがインストールされます。

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh

Release PageにBaker link. EnvのWindowsとMacのインストーラーがあります。

release_page

.exeがWindows用、.dmgがMac用のインストーラーです。 対象のインストーラーをダウンロードして、実行してください。

「壊れているため開けません」の対処法(Mac)

MacでBaker link. Envを起動した際に発生する「壊れているため開けません」のエラーへの対処方法について説明します。

ターミナルで次のコマンドを実行してください。

xattr -d com.apple.quarantine "/Applications/Baker link. Env.app"

※このコマンドの実行は、一度だけで問題ありません。

Info

この問題を回避するためのコマンドを紹介しましたが、根本的な解決策として、証明書の導入を検討しています。これにより、恐らくこのターミナル操作なしでアプリを実行できます。 また、寄付を通じてプロジェクトを支援していただけると、証明書の導入やさらなる開発に役立てることができます。 ご支援をお待ちしております🙇‍♂️

Baker link. Envの使い方

Baker link. Envは、3つの機能があります。

  • プロジェクトを作成する機能
  • DAP Serverを起動する機能
  • Log機能

baker-link-env-page

これらの機能は、プロジェクト作成DAP Serverの起動の順序で行います。もし、動作がおかしいなと思った時は出力されるLogをご確認ください。

Rancher Desktopの起動

本開発環境では、Rancher Desktopの起動が必須です。

Baker link. Envを起動すると自動でRancher Desktopが起動する仕様になっています。 もし起動していなかったら、Rancher Desktopのアプリのアイコンをクリックして起動状態にしてください。

プロジェクトの作成

  1. Baker link. EnvのCreate ProjectProject nameに好きなプロジェクト名を入力します。

  2. createをクリックして、プロジェクトの作成先のフォルダーを選択します。

    (Visual Stduio Code openにチェックが入っているとプロジェクト作成後に、VS Codeが自動起動します。)

  3. VS Codeの起動直後に左下のコンテナーで再度開くをクリックしてください。

dev_container_load

DAP Severの起動&デバック

プロジェクトがDev Containerで立ち上がったら、次はBaker link. Devを接続し、probe-rsのDAP Serverを起動させ、デバックを動作させます。

  1. PCとBaker link. DevをUSBで接続してください。

  1. 接続したらBaker link. EnvのDAP ServerのRunクリックしてServerを起動してください。

  1. F5を押すと.vscode/launch.jsonprobe-runという設定が動作します。 もう一度F5を押すと、プログラムが実行されます。 VS Code上でログが表示されていることも確認できるかと思います。

pj_run

今画面に見えているのは、3色のLEDが光りながらログに何色が点灯しているか表示するプログラムです。

チュートリアルの流れ

組み込みシステムでは、通常のソフトウェアの概念にプラスして、入力/出力(I/O)と割り込み処理があります。 本チュートリアルでは、出力、入力、割り込みの順に解説します。

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#BB2528',
      'primaryTextColor': '#fff',
      'primaryBorderColor': '#7C0000',
      'lineColor': '#F8B229',
      'secondaryColor': '#006100',
      'tertiaryColor': '#fff'
    }
  }
}%%
flowchart LR
    Output([① 出力(3色点灯)])
    Input([② 入力(ボタン入力)])
    Interrup([③ 割り込み(入力割り込み)])
    Output --> Input --> Interrup

コードのフォルダー構成

作成されたプロジェクトのフォルダー構成は以下の通りです。 (他にもファイルがありますが、必要なファイルだけ記載しています)

.
|-- Cargo.toml  # Cargoによるライブラリのインストールを管理するためのファイル
|-- examples # LEDやButtonの使い方の参考コード
|   |-- traffic_light.rs
|   |-- traffic_light_button.rs
|   `-- traffic_light_button_irq.rs
|-- memory.x # 書き込み先のチップのメモリマップ設定ファイル
└── src # ソースコードフォルダ
    └── main.rs # mainソースコードファイル

コードを書くときは、src内のファイルを編集することになります。 それ以外は設定ファイルなので、ビルド設定を変更したいときに編集します。

出力 3色点灯光(Lチカ) コードの解読

本チャプターのコード: examples/traffic_light.rs

LEDの配線

もしRasberry Pi Picoを利用している場合は、LEDを配線してください。

※Baker link. Envの場合は、配線不要です。何も気にせずに、コーディングをお楽しみください。

コードの読解

src/main.rsがRustのコードになります。 コードの構成は、以下のように分かれています。

  • ①マクロの宣言
  • ②useの宣言
  • ③bootローダー関連
  • ④定数
  • ⑤main関数
  • ⑥プログラム開始ログ
  • ⑦各設定のinit
  • ⑧無限loop、ログ出力、LED PinのON/OFF、delay処理

次の節から、①〜⑧の順に説明します。

// ①マクロの宣言
#![no_std]
#![no_main]

// ②useの宣言
use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use rp2040_hal as hal;

use hal::pac;

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;

// ③bootローダー関連
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

// ④定数
const XTAL_FREQ_HZ: u32 = 12_000_000u32;

// ⑤main関数
#[rp2040_hal::entry]
fn main() -> ! {
    // ⑥プログラム開始ログ
    info!("Program start!");

    // ⑦各設定のintit
    let mut pac = pac::Peripherals::take().unwrap();

    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

    let sio = hal::Sio::new(pac.SIO);

    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let mut green_led = pins.gpio22.into_push_pull_output();
    let mut orange_led = pins.gpio21.into_push_pull_output();
    let mut red_led = pins.gpio20.into_push_pull_output();

    // ⑧無限loop、ログ出力、LED、PinのON/OFF、delay処理
    loop {
        info!("green");
        green_led.set_high().unwrap();
        timer.delay_ms(2000);
        green_led.set_low().unwrap();

        info!("orange");
        for _ in 1..4 {
            orange_led.set_high().unwrap();
            timer.delay_ms(500);
            orange_led.set_low().unwrap();
            timer.delay_ms(500);
        }
        orange_led.set_low().unwrap();

        info!("red");
        red_led.set_high().unwrap();
        timer.delay_ms(2000);
        red_led.set_low().unwrap();
    }
}

main.rs

main.rsは、最初に呼ばれるコードです。 その他のコード(例えばsrc/sub.rs)はまたmain.rs以外の別のコードから呼ばれる可能性もありますが辿っていけば最後はこのmain.rsから呼ばれることになります。

①マクロ宣言

このコード部分の話

#![no_std]
#![no_main]

マクロ(#![no_std]#![no_main]

Rustのマクロは、コンパイル時に処理されます。 コンパイルする時にコードを上書きしてくれるイメージです。 これは、C言語の#defineに少し似ています。 書き方は、#![macro_name]です。 他にも関数のような使い方ができるマクロもあり、info!(argument)がそれに当たります。

そして、最初の2行に#![no_std]#![no_main]が記載されているかと思いますが、これもマクロです。

#![no_std]

#![no_std]は、Rustのstdクレートの代わりにcoreクレートをリンクします。 このstdクレートは、WindowsやLinuxといったOS上で動作するソフトウェアに対して利用できます。 一方で、今回の組み込みソフトウェアでは、OSなしのベアメタル環境になるめstdクレートが利用できません。 そのため、OSなしの環境でも動作するcoreクレートに切り替える必要があるわけです。

#![no_main]

#![no_main]は、Rustコンパイラの通常のエントリポイント(main関数)を利用しないことを意味します。 通常のmain関数を利用しない代わりに、今回のコードでは#[rp2040_hal::entry]というマクロを宣言しています。 つまり、RP2040専用のエントリポイントを利用しているということです。

Point

少し難しい説明でしたが、ベアメタル(OSなし)の環境ではstdクレートとmain関数が利用できないため、#![no_std]#![no_main]を宣言する必要があると覚えておけば初めは問題ないです。

②use宣言

このコード部分の話

use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use rp2040_hal as hal;

use hal::pac;

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;

use

大雑把に言えば「use=ライブラリ(クレート)宣言の省略」です。 以降、その意味についてと利用の仕方について説明します。

他の言語だと外部のライブラリの呼び出しはincludeimportといった宣言したりします。 Rustは、これらのライブラリ呼び出し宣言をせずとも、クレート名::利用したい関数(例:cortex_m::delay::Delay::new())といったように::を利用すれば参照したいクレート(ライブラリ)の関数を呼び出すことができます。

しかし、これの欠点は::で繋いでいくと文字数が増えて読みづらくなることです。 そこで、useの出番です。 useを利用すれば、先頭の単語を省略して関数等を利用できます。

useの詳細は、こちらのページに記載しています。

③bootローダー関連

このコード部分の話

#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H

bootローダーとは

このLチカのプログラムは、bootローダーというものを動作させる必要があります。 bootローダーとは、電源投入直後に動作するプログラムでのことで、組み込みシステムではよく出てきます。 初めはrp2040のチップの中にある書き換え不可能なブートローダーが実行されます。 その後2段階目のブートローダーとしてrp2040_boot2::BOOT_LOADER_GENERIC_03Hが呼ばれることになります。 このrp2040_boot2は、RP2040のRust専用のブートローダーでUartなどの周辺回路(ペリフェラル)の初期化処理を行っています。

対象のシンボルを.boot2というセクションに配置します。 .boot2は、memory.xというファイルに記載されています。

// memory.x
MEMORY {
    BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
    FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
    RAM   : ORIGIN = 0x20000000, LENGTH = 256K
}

EXTERN(BOOT2_FIRMWARE)

SECTIONS {
    /* ### Boot loader */
    .boot2 ORIGIN(BOOT2) :
    {
        KEEP(*(.boot2));
    } > BOOT2
} INSERT BEFORE .text;

これは、どのメモリーの何番地に何を入れるかの設定になります。 これを見てみると.boot2は、0x10000000から0x10000099まで入ることが分かります。

#[used]

ファイルに静的に保持するための宣言です。 次のpub static BOOT2:[u8;256] = rp2040_boot2::BOOT_LOADER_GENRIC_03HBOOT2をプログラムに静的に保持してくれます。

Point

少し分かりずらかったかもしれないですが、ブートローダーを.boot2のアドレス領域に書き込んでいることが分かっていただければ、とりあえず問題ないです。

④定数(const)

このコード部分の話

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

const

constによる宣言はコンパイル時にインラインの定数として扱われるようになります。

つまり、次のようにconstを宣言しておけば、

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

次のコードが

fn sample() -> u32{
    let num: u32 = XTAL_FREQ_HZ + 1;
    return num
}

コンパイル時には、次のコードになるという意味です。

fn sample() -> u32{
    let num: u32 = 12_000_000u32 + 1;
    return num
}

⑤main関数

このコード部分の話

#[rp2040_hal::entry]
fn main() -> !{
    ...
}

main関数

Rustはmain.rsの中に記載されているmain関数がはじめに呼ばれる関数です。 他の関数は、このmain関数から呼ばれたりmain関数が読んだ関数がまた次の関数を呼ぶことで呼ばれるようになります。

ちなみにWindowsやMacなどのOS上で動作するアプリを作成する際のmain関数の場合は次の通りです。

fn main{
    ...
}

これに比べるとRP2040のmain関数は#[rp2040_hal::entry]があったり、-> !があります。

#[rp2040_hal::entry]

#[rp2040_hal::entry]の次の関数をRP2040のプログラムのmain関数として扱うことができます。

-> !

!は値を返さない関数であることを示しています。 つまり、永遠にreturnをしないと言うことを明示しています。

⑥プログラム開始ログ

このコード部分の話

info!("Program start!");

info!

PC側にログを出力するための関数です。 このinfo!("Program start!")は実行すると、Visual Stadio Codeのターミナル画面にProgram startと出力されます。

また今回のコードの中に記載されてないですが、変数の中身の出力もできます。

たとえば次の通りに、i32の値を出力できます。

let cnt = 10;
info!("cnt: {}", cnt);

変数の中身を確認するのに大変便利なので、コードを状況を調べるのに活用してみてください。

⑦各設定のinit

このコード部分の話

let mut pac = pac::Peripherals::take().unwrap();

let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

let clocks = hal::clocks::init_clocks_and_plls(
    XTAL_FREQ_HZ,
    pac.XOSC,
    pac.CLOCKS,
    pac.PLL_SYS,
    pac.PLL_USB,
    &mut pac.RESETS,
    &mut watchdog,
)
.ok()
.unwrap();

let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

let sio = hal::Sio::new(pac.SIO);

let pins = hal::gpio::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
);

let mut green_led = pins.gpio22.into_push_pull_output();
let mut orange_led = pins.gpio21.into_push_pull_output();
let mut red_led = pins.gpio20.into_push_pull_output();

ペリフェラルオブジェクトの取得

let mut pac = pac::Peripherals::take().unwrap();

この宣言で、ペリフェラルへのアクセスを簡単にしてくれるペリフェラルオブジェクトを::takeメソッドで取得しています。

ペリフェラルとは

ペリフェラルとは、マイコンに内蔵された装置のことを指しています。 RP2040では、次のペリフェラルがあります。

  • GPIO:入出力
  • UART:シリアル通信
  • SPI:チップ間の通信
  • I2C:チップ間の通信
  • PWM:PWMの出力
  • USBコントローラー:USBの通信
  • PIO:プログラマブルな入出力

ウォッチドックオブジェクトのインスタンス

let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

この宣言で、ウォッチドックへのアクセスを簡単にしてくれるウォッチドックオブジェクトを::newメソッドでインスタンスしています。 また::newメソッドの引数でpac.WATCHDOGを渡してます。 これは、ペリフェラルオブジェクトの一部であるWATCHODG利用してウォッチドックオブジェクトを生成していることを意味してます。

ウォッチドックとは

ウォッチドックとは、その装置が動作しているか定周期で確認してくれる機能のことです。 一定時間動作がなかった時には、再起動などをしてくれます。

クロックオブジェクトの初期化&取得

let clocks = hal::clocks::init_clocks_and_plls(
    XTAL_FREQ_HZ,
    pac.XOSC,
    pac.CLOCKS,
    pac.PLL_SYS,
    pac.PLL_USB,
    &mut pac.RESETS,
    &mut watchdog,
)
.ok()
.unwrap();

この宣言で、クロックオブジェクトの初期化(pllsも初期化)と取得を::init_clocks_and_pllsメソッドで行っっています。 `

timerの初期化

let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

マイコンは、CPUを動作させるクロックを用いて時間測定しており、1クロックでカウントアップする回路のカウント数×1クロックの時間(クロックサイクル時間)で計算しています。 Timerは、この時間に関係する処理を簡易化しており、Timerの初期化でclocksを引数で渡しています。 また何秒遅らせる(delay)かといった処理も、このTimerで行うことができます。

SIOの初期化

let sio = hal::Sio::new(pac.SIO);

専用のペリフェラルを利用しているため、pac.SIOを引数で渡して初期化しています。

SIOとは

SIO(Single-cycle I/O)は、CPUの1バス・サイクルでGPIO等にアクセス可能な高速なI/Oです。

GPIOの初期化

let pins = hal::gpio::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
);

GPIOの初期化をしています。この後のLEDへの出力設定をするには必要な設定になります。

GPIOの出力設定(LED)

let mut green_led = pins.gpio22.into_push_pull_output();
let mut orange_led = pins.gpio21.into_push_pull_output();
let mut red_led = pins.gpio20.into_push_pull_output();

GPIOの20、21、22をpush-pullのoutputで設定しています。 今回は、信号機のように赤、オレンジ、緑を光らせるため、変数名をred_ledorange_ledgreen_ledにして見やすくしました。

GPIOとは

GPIO(General Purpose Input/Output)は、汎用入出力ピンのことです。 これらのピンは、マイクロコントローラやシングルボードコンピュータなどで使用され、入力または出力として設定できます。

⑧無限loop、ログ出力、LED PinのON/OFF、delay処理

このコード部分の話

loop {
    info!("green");
    green_led.set_high().unwrap();
    timer.delay_ms(2000);
    green_led.set_low().unwrap();

    info!("orange");
    for _ in 1..4 {
        orange_led.set_high().unwrap();
        timer.delay_ms(500);
        orange_led.set_low().unwrap();
        timer.delay_ms(500);
    }
    orange_led.set_low().unwrap();

    info!("red");
    red_led.set_high().unwrap();
    timer.delay_ms(2000);
    red_led.set_low().unwrap();
}

Loop

loopは、文字通り無限に繰り返されるという意味です。 今回のコードでは、breakがないのでloop{}の中の処理が無限に繰り返されます。

LEDのHigh、Low

green_led.set_high().unwrap();
...
green_led.set_low().unwrap();

.set_high()で、GPIOをHighにします。

unwrap()

.set_high()の後ろに.unwrap()がついていますが、これりは意味があります。

set_high()は、返り値にResultを持つため次のようにエラー処理を書く必要があります。

match green_led.set_high(){
    Ok(_) => {},
    Err(e) => {
        // Error
    },
};

重要な箇所においては、このエラー処理は適切に記述するべきだと思います。 しかし、カジュアルにコーディングを楽しみたい、それほど重要でない場所にはunwrap()という文法を用いることで、Okの時はその中身を取り出してくれます。(これがunwrapの意味です)

一方で、Errの時はpanicになるので使い方には注意が必要です。

タイマーのDelay

timer.delay_ms(500);

500msで待つ処理です。文字通りです。

for文

for _ in 1..4 {
    ...
}

forは、決まった回数繰り返すという文法です。 このforは、1..4つまり1,2,3(4より小さいという意味です)の値を_に入れて繰り返し処理をするというものです。

また_は、その値を使わずに_に入れておくという意味になりますので、今回は3回処理を繰り返すというforになります。

3色点灯(Lチカ)のまとめ

動作させたのは、緑のLEDを2秒点灯、オレンジのLEDを3回点滅、赤のLEDを2秒間点灯を無限に繰り返すプログラムでした。

お気づきかと思いますが、これは時間式の信号機に似ている処理になります。

次の章では、入力処理について学ぶためにこの信号機の処理にボタン入力を加えていきます。

入力 ボタン入力

本チャプターのコード: examples/traffic_light_button.rs

先ほどの時差式信号のプログラムにButtonの処理を加えてコードです。 追加されたのは、次の2つです。

  • ①Buttonの設定
  • ②Buttonの状態確認(input)

次の節から、①、②を説明します。

#![no_std]
#![no_main]

use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use rp2040_hal as hal;

use hal::pac;

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::{InputPin, OutputPin};

// bootloader code
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

#[rp2040_hal::entry]
fn main() -> ! {
    info!("Program start!");
    let mut pac = pac::Peripherals::take().unwrap();

    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

    let sio = hal::Sio::new(pac.SIO);

    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // LED:GPIO22(Green), GPIO21(orange), GPIO20(RED)
    let mut green_led = pins.gpio22.into_push_pull_output();
    let mut orange_led = pins.gpio21.into_push_pull_output();
    let mut red_led = pins.gpio20.into_push_pull_output();

    // ①Buttonの設定
    let mut button = pins.gpio23.into_pull_up_input();

    loop {
        info!("red");
        red_led.set_high().unwrap();
        timer.delay_ms(2000);

        // ②Buttonの状態確認(input)
        if button.is_low().unwrap() {
            red_led.set_low().unwrap();
            info!("green");
            green_led.set_high().unwrap();
            timer.delay_ms(2000);
            green_led.set_low().unwrap();

            info!("orange");
            for _ in 1..4 {
                orange_led.set_high().unwrap();
                timer.delay_ms(500);
                orange_led.set_low().unwrap();
                timer.delay_ms(500);
            }
            orange_led.set_low().unwrap();
        }
    }
}

①Buttonの設定

このコード部分の話

let mut button = pins.gpio23.into_pull_up_input();

GPIOの設定(入力)

GPIO23をPull-Upの入力設定をしています。

Pull-Upで設定すると、Buttonを押した時(オープン)にLowになり、押してない時(クローズ)にHighになります。

Pull-UpとPull-Down

Pull-UpとPull-Downで抵抗の繋ぎ方が違います。

Pull-Upは、スイッチがオープン(非接触)のときに、入力を高い状態(通常は電源電圧)に引き上げるために使用されます。これにより、スイッチがオープンのときに入力が不確定な状態になるのを防ぎます。

一方、Pull-Downは、スイッチがオープンのときに、入力を低い状態(通常はグラウンド)に引き下げるために使用されます。これにより、スイッチがオープンのときに入力が不確定な状態になるのを防ぎます。

②Buttonの状態確認

このコード部分の話

if button.is_low().unwrap() {
    ...
}

is_low()

is_low()は、buttonの入力がLowの時にTrueを返します。 つまりButtonを押した時に{}の処理が実行されます。

set_high()の時にも説明しましたが、unwrap()はErrorの処理を省略しResultの中身のbool値だけを取り出す関数です。

unwrap解説

ボタン入力のまとめ

LEDの点灯からのButtonの入力確認は簡単だったと思います。

しかし、このプログラムには信号機として利用するには少し不便な欠陥があります。

それは、2秒間周期で一瞬しかButtonを検知されないことです。

loop {
    info!("red");
    red_led.set_high().unwrap();
    timer.delay_ms(2000);

    if button.is_low().unwrap() { // この処理が2秒周期で一瞬しかやってこない
       ... 
    }
}

この処理だと、Buttonを押したタイミングでたままた検知されるか、Buttonを長押ししないと反応しないようになっています。 押した時にif..{}の処理をしたいですよね?

次の章では、そんな希望を叶えてくれる割り込みについて解説します。

割り込み 入力割り込みのコードの解読

本チャプターのコード: examples/traffic_light_button.rs

前章から追加されたのは、次の2つです。

  • ①LED、Buttonの型、グローバル変数宣言
  • ②GIPOの設定(割り込み)
  • ③変数を格納
  • ④割り込み設定の登録
  • ⑤何もしない無限ループ
  • ⑥割り込み処理

次の節から、①〜⑥を説明します。

#![no_std]
#![no_main]

use defmt::*;
use defmt_rtt as _;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use hal::pac::interrupt;
use panic_probe as _;
use rp2040_hal as hal;

// bootloader code
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

// ①LED、Buttonの型、グローバル変数宣言
type GreenLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type RedLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio20, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type OrangeLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio21, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type ButtonPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio23, hal::gpio::FunctionSioInput, hal::gpio::PullUp>;
type DelayTimer = hal::Timer;
type LedAndButton = (GreenLedPin, RedLedPin, OrangeLedPin, ButtonPin, DelayTimer);
static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> =
    critical_section::Mutex::new(core::cell::RefCell::new(None));

#[rp2040_hal::entry]
fn main() -> ! {
    info!("Program start!");
    let mut pac = hal::pac::Peripherals::take().unwrap();

    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

    let sio = hal::Sio::new(pac.SIO);

    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // LED:GPIO22(Green), GPIO21(orange), GPIO20(RED)
    let green_led = pins.gpio22.into_push_pull_output();
    let orange_led = pins.gpio21.into_push_pull_output();
    let mut red_led = pins.gpio20.into_push_pull_output();
    red_led.set_high().unwrap();

    // ②GIPOの設定(割り込み)
    // Button:GPIO23
    let button = pins.gpio23.into_pull_up_input();
    button.set_interrupt_enabled(hal::gpio::Interrupt::EdgeLow, true);

    // ③変数を格納
    critical_section::with(|cs| {
        GLOBAL_PINS
            .borrow(cs)
            .replace(Some((green_led, red_led, orange_led, button, timer)));
    });

    // ④割り込み設定の登録
    unsafe {
        hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0);
    }

    // ⑤何もしない無限ループ
    // info!("red");
    loop {
        cortex_m::asm::wfi();
    }
}

// ⑥割り込み処理
#[hal::pac::interrupt]
fn IO_IRQ_BANK0() {
    static mut LED_AND_BUTTON: Option<LedAndButton> = None;

    if LED_AND_BUTTON.is_none() {
        critical_section::with(|cs| {
            *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take();
        });
    }

    if let Some(gpios) = LED_AND_BUTTON {
        let (green_led, red_led, orange_led, button, timer) = gpios;
        if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {
            info!("Button pressed");

            red_led.set_low().unwrap();

            info!("green");
            green_led.set_high().unwrap();
            timer.delay_ms(2000);
            green_led.set_low().unwrap();

            info!("orange");
            for _ in 1..4 {
                orange_led.set_high().unwrap();
                timer.delay_ms(500);
                orange_led.set_low().unwrap();
                timer.delay_ms(500);
            }
            orange_led.set_low().unwrap();

            info!("red");
            red_led.set_high().unwrap();
            button.clear_interrupt(hal::gpio::Interrupt::EdgeLow);
        }
    }
}

プログラムの大枠

プログラムの大枠は、次の通りです。

graph TD
    A[main] --> C[mainの何もしない無限ループ]
    C -->|"GPIO割り込み(Button割り込み)"| B[IO_IRQ_BANK0]
    B -->|IO_IRQ_BANK0の処理完了| C

main関数は、Pinの設定、割り込み設定を行い最後に何もしない無限ループで終わります。 Button割り込みがなければ、常に何もしない無限ループで待機し続けます。

Button割り込みがあった時に、IO_IRQ_BANK0関数が実行され、LEDが点灯し処理終了後にmain関数の何もしない無限ループに戻ります。

つまり割り込みがない限りは、何もしないプログラムになります。 また前章のボタンの入力をmain関数で検知する処理と比較して、GPIO割り込みというハードウェアとして備わっている機能に任せることができため、高速化つ確実にButtonを検知できます。

関数間でやり取りするためのグローバル変数の必要性

本章の処理では、main関数とIO_IRQ_BANK0関数の2つの関数がそれぞれ独立して実行されます。

Rustでは、main関数から別の関数を実行する場合は所有権を渡してPinの参照変更をし、LEDを光らせます。 今回の場合は、IO_IRQ_BANK0関数をmain関数が呼び出すのではなく、GPIO割り込み(Button割り込み)が呼び出し実行します。 そのため、main関数から所有権をIO_IRQ_BANK0に所有権を貸すことができません。 そこで次のようなグローバルな変数を宣言して、関数間の所有権と変数の参照変更をできるようにしています。

static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> =
    critical_section::Mutex::new(core::cell::RefCell::new(None));

①LED、Buttonの型、グローバル変数宣言

このコード部分の話

type GreenLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type RedLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio20, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type OrangeLedPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio21, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type ButtonPin =
    hal::gpio::Pin<hal::gpio::bank0::Gpio23, hal::gpio::FunctionSioInput, hal::gpio::PullUp>;
type DelayTimer = hal::Timer;
type LedAndButton = (GreenLedPin, RedLedPin, OrangeLedPin, ButtonPin, DelayTimer);
static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> =
    critical_section::Mutex::new(core::cell::RefCell::new(None));

グローバルな変数

static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> =
    critical_section::Mutex::new(core::cell::RefCell::new(None));

GLOBAL_PINSは、関数間でPin情報の変数を参照変更するためのグローバルな変数です。

関数を超えて変数の所有権、参照、変更のやり取りをするために、staticcritical_sectionMutexRefCellを利用します。

critical_sectionとは

critical_sectionは、並行処理で必要とされるグローバルな変数を制御するのに役立つ組み込み用のクレートです。 MutexRefCellは、stdが提供していますが、std_no環境である組み込みでは利用することができません。

そこでcritical_sectionが代わりにMutexRefCellを提供してくれています。

ジェネリック<T>

hal::gpio::Pin<><>は、ジェネリックと言われるもので、構造体の中で異なる複数の型の値を保持できる定義です。

struct Container<T>{
    value: T
}

Tが任意の型になります。

hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>は、hal::gpio::Pin<>というPinの型で、hal::gpio::bank0::Gpio22hal::gpio::FunctionSioOutputhal::gpio::PullDownの3つを指定しており、GPIO22、SioOutput、PullDownのPin設定であることを意味しています。

type

type GreenLedPin = hal::gpio::Pin<...>;

これはhal::gpio::Pin<...>GreenLedPinという名前に省略するよという意味です。 このtypeを定義することで、グローバル変数の記述が読みやすくなります。

static

staticは、静的変数を定義するために使われます。静的変数はプログラムの全体で一度だけ初期化され、プログラムの終了まで存在します。

Rustでは、static変数自体は変更不可能(イミュータブル)ですが、内部のデータが変更可能(ミュータブル)にすることが可能です。 GLOBAL_PINSは、critical_section::Mutexcore::cell::RefCellを使って内部のデータを安全に変更できるようにしています。

critical_section::Mutex

Mutexは、変更可能(ミュータブル)な変数として扱うためのものです。 そして、複数の関数から安全にアクセスできるようにするためのロック機構が備わっており、同時に複数の関数がデータにアクセスすることを防ぎます。

core::cell::RefCell

RefCellは、実行時に変更可能(ミュータブル)な借用をチェックするためもので、同じスコープ内での複数の変更可能(ミュータブル)な参照を可能にします。

Option

変数でNoneを扱うためのものです。 次のようにi32Optionでラップすることで、Noneを入れることができます。

let val: Opiton<i32> = None

Noneとは

Noneとは、「値が存在しない」ことを示す値です。

②GIPOの設定(割り込み)

このコード部分の話

let button = pins.gpio23.into_pull_up_input();
button.set_interrupt_enabled(hal::gpio::Interrupt::EdgeLow, true);

set_interrupt_enabled

GPIOの割り込みを有効にする関数です。 EdgeLowは検出する信号のパターンを表し、第二引数のtrueは有効にすることを示しています。

EdgeLow & EdgeHigh

  • EdgeLow: HighからLowになる角の部分で検出することを意味します。
  • EdgeHigh: LowからHighになる角の部分で検出することを意味します。

③変数を格納

このコード部分の話

critical_section::with(|cs| {
    GLOBAL_PINS
        .borrow(cs)
        .replace(Some((green_led, red_led, orange_led, button, timer)));
});

critical_setcion::with

critical_setcion::withは、critical_section::MutexであるGLOBAL_PINSの変数の操作をするための関数です。 この|cs|{...}の中のやり取りは、他のスレッドからブロックできます。 一連のLEDとButtonのpinの設定がされた、green_ledred_ledorange_ledbuttontimerを格納しています。

またcsは、Critical Sectionの略です。変数のためcsでないaaでも動作します。

クロージャ(|cs|{})

|cs|{}は、関数を引数として渡せるクロージャと言われるもので、csが関数の引数の役割を果たします。

他の言語では、関数を別で宣言して::with(function_name)のようにして関数名を書いて渡したりしますが、このクロージャを利用することでfunction_nameが不要になり、1つ関数名を考えなくて良くなります。

今回のような小規模なプログラムではメリットを感じないですが、大規模なプログラムになると全体の設計を把握して関数名を決定する必要があるため、1つ考える関数名が減るのはありがたいものです。

borrow

所有権限を借用することを示しています。 Rustでは、所有権を持つことで変数の値の変更することができるので、この操作が必要になります。

replace

値を置き換えることを意味しています。 ここでは、NoneからSome((green_led, red_led, orange_led, button, timer))に置き換えています。

Some

GLOBAL_PINSは、Optionの変数でした。Optionにした理由は、Noneを利用したかったためです。Someは、Optionで設定した変数に格納する際、宣言するものです。

逆に次のコードでは変数に格納数ことはできません。

   .replace((green_led, red_led, orange_led, button, timer));

④割り込み設定の登録

このコード部分の話

unsafe {
    hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0);
}

unsafe

rustの借用等の安全な文法を無視して、コードを書くことを明示しています。

あまり利用しないようが良いのですが、NVICへの登録には必要になります。

NVIC::unmask

IO_IRQ_BANK0とは、IO割り込みのBANK0を意味しており、

hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0);

では、NVICにIO_IRQ_BANK0を登録することを意味しています。

NVICとは

NVIC(Nested Vectored Interrupt Controller)は、主にARM Cortex-Mシリーズのマイクロコントローラに搭載されている割り込みコントローラです。NVICは、システム内で発生する複数の割り込みを効率的に管理し、優先順位に基づいて処理を行います。

⑤何もしない無限ループ

このコード部分の話

loop {
    cortex_m::asm::wfi();
}

cortex_m::asm::wfi

ARM Cortex-Mプロセッサのアセンブリ命令WFI(Wait For Interrupt)を呼び出します。 この命令は、プロセッサを低電力モードに移行させ、次の割り込みが発生するまで待機させるのに使用します。

つまり、このloopの処理は「省エネな状態で無限に割り込みを待ちづつける」という意味になります。

⑥割り込み処理

このコード部分の話

#[hal::pac::interrupt]
fn IO_IRQ_BANK0() {
    static mut LED_AND_BUTTON: Option<LedAndButton> = None;

    if LED_AND_BUTTON.is_none() {
        critical_section::with(|cs| {
            *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take();
        });
    }

    if let Some(gpios) = LED_AND_BUTTON {
        let (green_led, red_led, orange_led, button, timer) = gpios;
        if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {
            info!("Button pressed");

            red_led.set_low().unwrap();

            info!("green");
            green_led.set_high().unwrap();
            timer.delay_ms(2000);
            green_led.set_low().unwrap();

            info!("orange");
            for _ in 1..4 {
                orange_led.set_high().unwrap();
                timer.delay_ms(500);
                orange_led.set_low().unwrap();
                timer.delay_ms(500);
            }
            orange_led.set_low().unwrap();

            info!("red");
            red_led.set_high().unwrap();
            button.clear_interrupt(hal::gpio::Interrupt::EdgeLow);
        }
    }
}

hal::pac::interrupt

関数を割り込みの関数として表すマクロです。 次のIO_IRQ_BANK0を割り込みの関数として扱うことを意味しています。

IO_IRQ_BANK0

IO割り込みとして予約されている関数です。 割り込み登録をしたIO_IRQ_BANK0に紐づく関数です。このように割り込みの関数は、割り込みによって関数名が決められています。

割り込み関数内での借用

    static mut LED_AND_BUTTON: Option<LedAndButton> = None;

LED_AND_BUTTONstaticな変数として用意されます。 staticは、プログラムの最初に初期化されてからプログラムが終了するまで残り続ける変数を意味しています。

そしてIO_IRQ_BANK0の中で宣言されるということは、この関数が呼び出され終了した後もLED_AND_BUTTONは残り続け、再びIO_IRQ_BANK0が呼び出されると残り続けているLED_AND_BUTTONがそのまま再利用されます。

    if LED_AND_BUTTON.is_none() {
        critical_section::with(|cs| {
            *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take();
        });
    }

IO_IRQ_BANK0内で宣言したLED_AND_BUTTONNoneだった時に、Critical Sectionを借用し、GLOBAL_PINSLED_AND_BUTTONに入れる処理です。

LED_AND_BUTTONの前に*がついていますが、これはC言語のポインターに近い概念です。 LED_AND_BUTTONは、static mutになっていますがRustが自動的に&mutと解釈するため、これをmutとして扱う(&を外す)ために*が必要になります。

割り込みされたか確認

    if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {...}

IO_IRQ_BANK0は、GPIOの割り込みがあったと時に動作し、それがどのGPIOのPinであったか認識することはできません。 このif文は、buttonのPinでEdgeLowの割り込みがあったか確認をしています。 もし割り込みがあった場合は、{...}の処理を実行します。

割り込み情報のクリア

    button.clear_interrupt(hal::gpio::Interrupt::EdgeLow);

clear_interruptbuttonのPinに割り込みがあった情報をクリアします。

今回は、割り込みを検出するPinが1つしなかったため、必要性を感じないですが、複数の割り込みを検知するPinがあった場合、このclear_interruptをしないとすべてのGPIO割り込みでbuttonのPinの処理が実行されてしまいます。 そのため、このclear_interruptの処理は必須になります。

入力割り込みのまとめ

いつのタイミングで押しても動作するボタン式信号機が完成しました。

割り込みは、組み込み開発以外であまり馴染みがありませんが、リアルタイム制御をする上で非常に大切なシステムになります。 今回説明したIO割り込みの他にも、タイマー割り込みという時間で割り込み処理を行うものもあります。 ぜひ、ご自身で調べて活用してみてください。

F-Rust

本チャプターでは、 Rust言語初心者のBaker link. Devユーザーが学習を兼ねて、 Baker link. Devで試したコードを紹介します。 以降は、Rust言語初心者のBaker link. Devユーザーの有志で作成されています。 また、記入内容の誤植、訂正、加筆を求めます。

usb serial

本章では、usbシリアル通信を用いてrp2040とPCで通信できるようにします。 必要なツールは以下です。

用途ツール名
文字入出力ツールTera Term VT
デバッグツールBaker link. Dev
ターゲットRaspbery Pi pico

動作の内容は、Tera Termで入力した小文字アルファベットが大文字アルファベットとして返信されます。 以下は、Tera Termでの動作動画です。

本チャプターでは、Baker link. Devを外部マイコンへの書き込み機能で用います。

外部マイコンへの書き込み手順(Rasberry Pi Pico)

  1. Baker link. Envを起動し、プロジェクト名、createクリック、プロジェクト保存先を選択すると、数秒後にVisual Studio Codeが開きます。

  1. Visual Studio Codeの左下に表示される「コンテナ―で再度開く」をクリックしてください。すると、Dockerイメージのダウンロード&ビルド処理が開始されます。この処理が数分程度かかりますので、しばらくお待ちください。

  1. src/main.rsを開き、以下のプログラムに書き換えてください。 コードにはcopilotでコメントを添えています。
#![no_std] // 標準ライブラリを使用しないことを指定
#![no_main] // 標準のエントリーポイント(main関数)を使用しないことを指定

// RTT(リアルタイムトレース)を使用するための設定
use defmt_rtt as _;
// パニック時のプローブ設定
use panic_probe as _;
// RP2040のハードウェア抽象化レイヤーをインポート
use rp2040_hal as hal;
// スタートアップ関数のマクロ
use hal::entry;
// Peripheral Access Crateの短縮エイリアス、低レベルのレジスタアクセスを提供
use hal::pac;
// USBデバイスサポート
use usb_device::{class_prelude::*, prelude::*};
// USB通信クラスデバイスサポート
use usbd_serial::SerialPort;
// フォーマットされた文字列を書き込むために使用
use core::fmt::Write;
use heapless::String;

/// ベアメタルアプリケーションのエントリーポイント。
///
/// `#[entry]`マクロは、Cortex-Mのスタートアップコードがすべてのグローバル変数を初期化した後にこの関数を呼び出すことを保証します。
///
/// この関数はRP2040の周辺機器を設定し、USBシリアル経由で受信した文字をエコーします。
/// 
#[link_section = ".boot2"] // ブートローダーセクションを指定
#[used] // コンパイラにこの静的変数が使用されることを明示
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; // ブートローダーのデータ

const XTAL_FREQ_HZ: u32 = 12_000_000u32; // 外部クリスタルの周波数を定義
#[entry]
fn main() -> ! {
    // シングルトンオブジェクトを取得
    let mut pac = pac::Peripherals::take().unwrap(); // 周辺機器のハンドルを取得

    // ウォッチドッグドライバを設定 - クロック設定コードに必要
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); // ウォッチドッグタイマーを初期化

    // クロックを設定
    //
    // デフォルトでは125 MHzのシステムクロックを生成
    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ, // 外部クリスタルの周波数
        pac.XOSC, // 外部オシレータ
        pac.CLOCKS, // クロック制御
        pac.PLL_SYS, // システムPLL
        pac.PLL_USB, // USB PLL
        &mut pac.RESETS, // リセット制御
        &mut watchdog, // ウォッチドッグタイマー
    )
    .ok()
    .unwrap(); // クロック設定が成功したか確認

    let timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); // タイマーを初期化

    #[cfg(feature = "rp2040-e5")] // コンパイル時の条件付きコンパイル
    {
        let sio = hal::Sio::new(pac.SIO); // シリアル入出力を初期化
        let _pins = hal::Pins::new(
            pac.IO_BANK0, // IOバンク0
            pac.PADS_BANK0, // パッドバンク0
            sio.gpio_bank0, // GPIOバンク0
            &mut pac.RESETS, // リセット制御
        );
    }

    // USBドライバを設定
    let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
        pac.USBCTRL_REGS, // USBコントローラのレジスタ
        pac.USBCTRL_DPRAM, // USBコントローラのDPRAM
        clocks.usb_clock, // USBクロック
        true, // VBUS検出を有効にする
        &mut pac.RESETS, // リセット制御
    ));

    // USB通信クラスデバイスドライバを設定
    let mut serial = SerialPort::new(&usb_bus); // シリアルポートを初期化

    // 偽のVIDとPIDでUSBデバイスを作成
    let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd)) // USBデバイスを構築
        .strings(&[StringDescriptors::default()
            .manufacturer("Fake company") // メーカー名
            .product("Serial port") // 製品名
            .serial_number("TEST")]) // シリアル番号
        .unwrap()
        .device_class(2) // デバイスクラスコード(2は通信デバイス)
        .build();

    let mut said_hello = false; // ウェルカムメッセージを表示したかどうかのフラグ
    loop {
        // 最初にウェルカムメッセージを表示
        if !said_hello && timer.get_counter().ticks() >= 2_000_000 { // タイマーが2秒以上経過したか確認
            said_hello = true; // フラグを更新
            let _ = serial.write(b"Hello, World!\r\n"); // ウェルカムメッセージを送信

            let time = timer.get_counter().ticks(); // 現在のタイマー値を取得
            let mut text: String<64> = String::new(); // フォーマットされた文字列を格納するバッファ
            writeln!(&mut text, "Current timer ticks: {}", time).unwrap(); // タイマー値を文字列に書き込む

            // これは、シリアルポートに書き込まれるバイト数がUSBペリフェラルに利用可能なバッファよりも小さいため、信頼性があります。
            // 一般的には、転送されていないバイトが失われないように、戻り値を処理する必要があります。
            let _ = serial.write(text.as_bytes()); // タイマー値をシリアルポートに送信
        }

        // 新しいデータをチェック
        if usb_dev.poll(&mut [&mut serial]) { // USBデバイスのポーリング
            let mut buf = [0u8; 64]; // データを格納するバッファ
            match serial.read(&mut buf) { // シリアルポートからデータを読み込む
                Err(_e) => {
                    // 何もしない
                }
                Ok(0) => {
                    // 何もしない
                }
                Ok(count) => {
                    // 大文字に変換
                    buf.iter_mut().take(count).for_each(|b| {
                        b.make_ascii_uppercase(); // 文字を大文字に変換
                    });
                    // ホストに送り返す
                    let mut wr_ptr = &buf[..count]; // 書き込みポインタを設定
                    while !wr_ptr.is_empty() { // バッファが空になるまでループ
                        match serial.write(wr_ptr) { // シリアルポートに書き込む
                            Ok(len) => wr_ptr = &wr_ptr[len..], // 書き込んだ分だけポインタを進める
                            // エラーが発生した場合、未書き込みデータを破棄します。
                            // 一つの可能なエラーはErr(WouldBlock)で、これはUSB書き込みバッファがいっぱいであることを意味します。
                            Err(_) => break, // エラーが発生したらループを抜ける
                        };
                    }
                }
            }
        }
    }
}

// ファイルの終わり
  1. Baker link. EnvのRunをクリックしてください。するとバックグラウンドで、probe-rsのDAP Serverが起動します。

  1. Baker link. DevとRasberry Pi PicoをJST-SH型3ピンコネクタケーブルで接続してください。

  1. Raspberry Pi Picoに電源を供給するために、Raspberry Pi PicoとPCをUSBケーブルで接続します。

  1. 次のBaker link. Devを外部マイコン書き込みモードで接続するために、真ん中のボタンを押しながら、Baker link. DevとPCをUSBケーブルで接続してください。

外部マイコン書き込みモードで接続されると緑のLEDが点灯します。

  1. Visual Studio CodeでF5キーを押してください。すると、以下のようなアイコンが表示されます。

  1. もう一度、F5キーを押すと、プログラムが動作します。Tera Termの設定は、新しい接続でシリアル、ポートは書き込んだマイコンボードのポート番号を選択してください。また、設定->端末->ローカルエコーにチェックを入れると文字を記入できます。

rp-rsを参考にしました。

Q&A

Baker link. Devのデバッカー側のマイコン(RP2040)に公式ファームウェア以外のプログラムを書き込んだ後に、再度元の公式ファームウェアに戻したい時にどうすればよいか?

A. デバッカーのファームウェア更新 に記載している手順にしたがって、公式ファームウェアを書き込みし直してください。


Baker link. DevのUSBが途切れたり電源がOFFになったりする。

A. Baker link. Devの基板表面の「Baker link. Dev © 2024」(初版のRev表記なし版)の場合は、37 Pin(EN)のイネーブルピンが未接続(フロート状態)のままだと、ディスイネーブル状態(ENがHigh)の状態になる場合があります。 37 Pin(EN)をLowつまり、37 Pin(EN)と38 Pin(GND)を接続(ショート)しください。


Dev Containers

Dev Containersとは

Docker上で構築した仮想マシンに開発環境構築をし、LocalのVSCodeからアクセスできるVSCodeの機能です。 もし環境構築に失敗してもOSを初期化することをせずに、コンテナー(仮想マシン)を再構築するだけで良くなります。

またDockerfileを丁寧に調整すれば、同じ開発環境を他人に配布することさえできます。 今まで再現性のない環境開発構築に悩まされてきた人たちにとっては、革命的なツールです。

Developing inside a Container devcontainers

この環境を手軽に利用できるのが、Baker link. Envです。

Baker link. Env

Baker link. Envは、Dev Containersをセットアップしたテンプレートからプロジェクトを作成し、Localにインストールしたprobe-rsのDAP Serverを起動してくれます。

use

useの通常パターン

use embedded_hal::delay::DelayNs;

単純に省略しているパターンです。 こうすることでembedded_hal::delay::DelayNsDelayNsで呼び出し可能になります。

use*パターン

*を最後につけるとそれ以下のmod(モジュール)や関数を呼び出せるようになります。

use defmt::*;

イメージで説明すると、crate_aの下にモジュール(m1)があり、m1内に関数func3があった場合、それらが省略できるという意味です。

crate_a
├── m1 
└── func2
use crate_a::*;

fn main(){
    m1::func3(); // crate_a::M1::func3としなくてよい
    func2(); // crate_a::func2()としなくて良い
}

as

use rp2040_hal as hal;

asは、asの前のクレート名を変更する機能を持っています。この文だとrp2040_halからhalにクレート名を変更しています。

useas _パターン

use defmt_rtt as _;
use panic_probe as _;

これは、クレートをmain.rsに取り込んだ後に、as _でクレート名を_に変換することで、main.rsで直接呼び出しをできないようにしています。(Underscore Imports

defmt_rttの例で説明すると、defmt_rttdefmtをrttとして利用する設定にし、その後にas _をすることでmain.rsで直接参照できなくなるのでmain.rsに悪影響を与えなくなります。

光を灯せ(Lチカ) コンパイル設定

Cargo.toml

cargo generateで生成したCargo.tomlは次の通りになっているかと思います。

[package]
edition = "2021"
name = "blink"
version = "0.1.0"
license = "MIT OR Apache-2.0"

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = { version = "0.2.5", features = ["unproven"] }

defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }

# We're using a Pico by default on this template
rp-pico = "0.8"

# but you can use any BSP. Uncomment this to use the pro_micro_rp2040 BSP instead
# sparkfun-pro-micro-rp2040 = "0.6"

# If you're not going to use a Board Support Package you'll need these:
# rp2040-hal = { version="0.8", features=["rt", "critical-section-impl"] }
# rp2040-boot2 = "0.2"

# ~以下省略~

[package]

[package]には、この作成しているプログラム(blink)の情報が記載されています。 editionは、利用するRustのコンパイラーのエディション(バージョンに近いがい少し違う)です。ここは、基本変更の必要はないはずです。(もし、古いエディションのRustが利用したいときは、変更する必要があると思います。) それ以外として、nameはこのプログラムの名前、versionはこのプログラムのVersion、licenseはこのプログラムのライセンスです。これらの情報は直接プログラムに影響を与えるものではないので、任意の値で結構です!

[dependencies]

[dependencies]には、利用したいcrates.ioに存在するライブラリ(クレート)や自作ライブラリ(クレート)の利用情報を記載します。ここに書かれたライブラリ(クレート)は、src/の下に描かれるプログラムから参照できるようになり、さらにcrates.ioのライブラリ(クレート)は、インターネットから自動でダウンロードしコンパイルができるようになります。

Info

お気づきかもしれないですが、Rustではライブラリに近いものをクレートと呼んでいます。 これは、他の言語の経験がある方にとってには少し戸惑いがあると思いますが、慣れるとむしろクレートと聞いて少し嬉しい気持ちになったりします! また他にもトレイトと言われるものがあります。これは他の言語でいうインターフェイスに近いものになります。

それぞれ記載しているクレートについて説明します。

cortex-m

Cortex-Mに低レベルにアクセスする処理をするためのクレートです。

Info

Cortexとは、マイクロプロセッサの設計開発をしているARM社のCPUファミリーの名称で、Cortex-A、Cortex-R、Cortex-Mの3つのシリーズがあります。その中でも、Cortex-Mは、ローエンドの組み込み用プロセッサとして開発されてます。後ろについているMは、マイクロコントローラーのMを指しているようです。

cortex-m-rt

Cortex-M マイクロコントローラーのセットアップ用のコードと最小限のランタイムです。 cortex-mと基本セットで利用する必要があります。

embedded-hal

Rustの組み込み用のトレイトの集まりです。 組み込みで利用するCPUチップは、数多く存在するしますが、細かい違いはあるものも共通的な機能が多いです。このembedded-halは、それらの共通化に大きく役に立っています。 ただembeded-halには、直接的な処理の機能がないためこれを継承して作成されたクレートを呼び出して利用する必要があり、今回はrp2040-halを内包しているrp-picoがそれに当たります。

詳細は、The Embedded Rust Bookに記載されているのでそちらを見ていただきたいですが、プログラミングに慣れてない方が見ても難しい内容なので、初めは見なくても支障はありません。

Info

halとは、Hardware Abstraction Layerの略で、ハードウェア抽象化層と言われるものです。このhalがハードウェアごとの仕様の違いを吸収し、利用するCPUが異なっていてもユーザーには、別のCPUと同じ使用間でプログラムができるになります。

rp-pico

rp2040をHALであるrp2040-halをRaspberry Pi Picoにカスタマイズしたクレートです。 rp2040のGPIOやI2Cなどの制御処理を関数を呼び出すだけで利用できます。

defmt

組み込みプログラミングで効率的にロギングを利用するためのフレームワークです。 defmtという名前は、deferred formattingから来ているそうです。

defmt-rrt

RTT(Real-Time Transfer)プロトコルで、defmtのログメッセージを送信するクレートです。

デバッカーのファームウェア更新手順

  1. Baker link. Devの29 Pin(BOOTSEL 1)と28 Pin(GND)を接続(ショート)し、デバッカー側のマイコン(RP2040)を書き込みモードにしてください。
  2. releaseの*.uf2ファイルをダウンロードし、ドラック&ドロップで"RPI-RP2"(RP2040の書き込み用フォルダ)に*.uf2ファイルをコピーしてください。(これで書き込まれます)

Baker link. Devによる外部マイコン書き込み

Baker link. Devは、外部マイコン書き込みをサポートしており、SWDデバックに対応している他のARM系CPUにRustのプログラムを書き込み可能です。

本節では、Baker link.Devを利用してRaspberry Pi PicoにRustプログラムを書き込む例に、外部マイコンへの書き込み方法を説明します。

開発環境

Baker link. Devで利用してきたBaker link. Envをそのまま利用することが可能です。 Baker link. Envを含めた環境構築については、環境構築を参照ください。

外部マイコンへの書き込み手順(Rasberry Pi Pico)

  1. Baker link. Envを起動し、プロジェクト名、createクリック、プロジェクト保存先を選択すると、数秒後にVisual Studio Codeが開きます。

  1. Visual Studio Codeの左下に表示される「コンテナ―で再度開く」をクリックしてください。すると、Dockerイメージのダウンロード&ビルド処理が開始されます。この処理が数分程度かかりますので、しばらくお待ちください。

  1. src/main.rsを開き、以下のプログラムに書き換えてください。

このプログラムは、Raspberry Pi PicoのUSB付近のLED(GPIO25)をON/OFFするプログラムです。

#![no_std]
#![no_main]

use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use rp2040_hal as hal;

use hal::pac;

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;

#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

#[rp2040_hal::entry]
fn main() -> ! {
    info!("Program start!");
    let mut pac = pac::Peripherals::take().unwrap();

    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

    let sio = hal::Sio::new(pac.SIO);

    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let mut led = pins.gpio25.into_push_pull_output();

    loop {
        info!("LED on");
        led.set_high().unwrap();
        timer.delay_ms(2000);

        info!("LED off");
        led.set_low().unwrap();
        timer.delay_ms(2000);
    }
}
  1. Baker link. EnvのRunをクリックしてください。するとバックグラウンドで、probe-rsのDAP Serverが起動します。

  1. Baker link. DevとRasberry Pi PicoをJST-SH型3ピンコネクタケーブルで接続してください。

Warning

販売しているBaker link. Dev Rev.1のSWDのピン配置は、販売時期によって以下のようにSWDのCLKとDIOの配置が逆転しています。これはストレートケーブルからクロスケーブル対応にBaker link. Devのファームウェアが変更されたためです。正常に通信がされない場合は、CLKとDIOの接続を逆にして接続することをお勧めいたします。

また配線で解決できない場合は、CLKとDIOが逆のバージョンのファームウェアをインストールすることも可能です。

  1. Raspberry Pi Picoに電源を供給するために、Raspberry Pi PicoとPCをUSBケーブルで接続します。

  1. 次のBaker link. Devを外部マイコン書き込みモードで接続するために、真ん中のボタンを押しながら、Baker link. DevとPCをUSBケーブルで接続してください。

外部マイコン書き込みモードで接続されると緑のLEDが点灯します。

  1. Visual Studio CodeでF5キーを押してください。すると、以下のようなアイコンが表示されます。

  1. もう一度、F5キーを押すと、プログラムが動作します。

info!(...)のログが下の画面に表示されます。

LEDも点灯されます。

この方法は、SWDデバックに対応している他のARM系CPUでも対応可能なので是非別のCPUでも試してみてください!

Baker link. Dev以外のハードウェア

Baker link. Dev以外のハードウェアでも、本チュートリアルを試すことができます。 例えば、Rasberry Pi PicoRasberry Pi Debug Probeを組み合わせのパターンがあります。

LEDとButtonが別途必要

Rasberry Pi Pico & Rasberry Pi Debug Probeの構成の場合は、LEDやButtonを別途準備し接続する必要があります。

Baker link.dev