💡

組み込みRust入門

2024/12/18に公開

はじめに

こちらは広島大学の技術系サークルの18日目のアドカレです。
https://adventar.org/calendars/10901
そして、今回の記事はHiCoder内で組み込みRust講習会を行ったときに話した内容をまとめています。
想定読者は、プログラミングをチョットデキて、Rustの所有権について他サイト等で学んできた人です。
この記事ではコードを一切書かず、コマンドを叩くだけなので、是非やってみてください。
私自身も勉強中なので、内容に誤りがあったらコメントで教えてくださいm(_ _)m8

この記事で必要なもの

組み込みとは

組み込みで、マイコンを利用した電気回路を制御することができます。
例えば、LED点灯やモーター制御などの単純な操作から、車載システムやIoTデバイスの制御まで幅広く応用されています。

代表例として、Raspberry Pi Pico(ラズピコ)やESP32が有名です。

https://www.raspberrypi.com/products/raspberry-pi-pico/
https://www.espressif.com/en/products/socs/esp32

なぜ組み込みでRust?

Rustを組み込み開発に利用する理由として、以下のメリットがあります。

Rustのメリット

  1. 安全性
    • Rustの型安全性と所有権システムにより、バグを未然に防止
    • ライフタイムを活用したメモリ安全性の確保
  2. 高いパフォーマンス
    • 手動でメモリ管理を行う必要がなく、それでいて高速な処理を実現
  3. 並行処理
    • 所有権によりデータ競合を防ぎ、並行処理が比較的容易
  4. ツールサポート
    • Cargoによるパッケージ管理で、インストールがめちゃくちゃ楽

Rustのデメリット

  1. 学習コストが高い
    • Rustの所有権やライフタイムの概念は、初心者には難解
  2. 情報の少なさ
    • Rustでも十分マニアックなのに、さらに組み込みなので、使用者がさらに少ない
    • 英語文献も少なく、さらに日本語資料は限られる
  3. コミュニティの小規模さ
    • CやPythonに比べるとまだ発展途上

組み込みRustを試してみよう

まずは、ラズピコを使った簡単なLED点滅(Lチカ)を行いましょう。
ラズピコのテンプレートがあるので用いましょう。

セットアップから実行

  1. rp2040-project-templateを開く
  2. このサイトの「Installation of development dependencies」に書かれているものを上からすべてインストール
  3. cargo install cargo-generateでcargo-generateをインストール
  4. cargo generate https://github.com/rp-rs/rp2040-project-templateでテンプレートを作成
    • Project Nameは適当
    • Flashing Methodはelf2uf2-rsを指定
  5. ラズピコをPCと接続しcargo runを実行(ラズピコはブートモード[1]にする)

簡単にLチカできた!

コードの解説

現在[2]、生成されたコードは以下のようになっています。

#![no_std]
#![no_main]

use bsp::entry;
use defmt::*;
use defmt_rtt as _;
use embedded_hal::digital::OutputPin;
use panic_probe as _;

use rp_pico as bsp;

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

#[entry]
fn main() -> ! {
    info!("Program start");
    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);

    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();

    let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());

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

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

    loop {
        info!("on!");
        led_pin.set_high().unwrap();
        delay.delay_ms(500);
        info!("off!");
        led_pin.set_low().unwrap();
        delay.delay_ms(500);
    }
}

Lチカ(LED点滅)の本質的な部分は以下のループです。

loop {
    info!("on!");
    led_pin.set_high().unwrap();
    delay.delay_ms(500);
    info!("off!");
    led_pin.set_low().unwrap();
    delay.delay_ms(500);
}

このコードはシンプルで、「LEDをオンにして500ms待つ → オフにして500ms待つ」を繰り返しています。
しかし、その周辺のコードが大切ではないかというと、そういうわけではなく非常に重要です。

今回はこれらを解説します。

  • エントリーポイントやno_std環境のセットアップ
  • HAL(Hardware Abstraction Layer)
  • Peripheral Access Crate(PAC)

組み込みRustの重要な概念

#![no_std]

組み込みRustでは標準ライブラリstdは基本的に使えません。
stdが使えないとは、OSが存在しないということであり、printデバッグをしようとも容易ではありません。

#![no_main] と #[entry]

no_std環境では通常のmain関数が使えないため、エントリーポイントを#[entry]で指定します。

#![no_main]
use bsp::entry;

#[entry]
fn main() -> ! {
    // エントリーポイント
}

これで、mainの上から実行されるようになります。

HAL (Hardware Abstraction Layer)

組み込みRust最大の魅力といっても過言ではない、抽象化(Abstraction)の活用です。
抽象化は、複雑な構造をシンプルにすることができ、コードの再利用性や保守性を向上させます。

プログラマーは日常的に抽象化の恩恵を受けています。
例えば、パッケージや高級言語[3]は低レベルの詳細を隠蔽し、使いやすいインターフェースを提供しています。

組み込みRustでは、HAL(Hardware Abstraction Layer)は、マイコン固有の低レベル操作を簡略化し、抽象化を上手く使っています。

マイコンとボードの違い

その前に!!
マイコンとボードが分かるとHALの理解の助けになるので解説します。

マイコン: CPU、メモリ、GPIOなどを統合した1つのチップ
ボード: マイコンに電源供給や周辺回路を搭載し、すぐに利用できる形にまとめたもの

ラズピコでは、ラズベリーが印字されている石がマイコンでRP2040と言い、ボードが緑の板全体です。

rp_pico::halの解説

rp-pico はラズピコのボードのクレートです。
ボードのクレートをBoard Support Package略してBSPと言います。

rp-picoのコードを見てみると、すごく簡単に書かれており、読んでみるとrp2040_halを使って書かれています。

rp2040_halはボードではなく、RP2040マイコンの機能をまとめたクレートで、RP2040を用いたさまざまなハードウェアの差分を吸収してくれるため実装がすごく簡単に済みます。

他にも、抽象化レイヤー(HAL)を用いてさまざまなRP2040ボードを記述しているので、ほとんど同じコードで異なるRP2040ボードが書けるため、学習コストが大幅に下がります。

embedded_halの解説

C言語の組み込み開発するときの問題として、ボードやマイコンが違ってもどれも似たような機能を持っているはずなのに、開発者が異なるためコードの書き方が変わってしまい、異なったマイコンを使って製品を作成したいのに学び直しをする必要があります。
Pythonで開発する問題点としては、異なるマイコンで同じような書きかたをできますが、対応しているマイコン、ボードはとても少ないです。

そこで、Embedded devices Working Group (WG)が書き方を統一するために embedded_halクレートを作成しています。[4]

rp2040-halやesp-halなどのマイコンのHALは、embedded-halを参照して書いているので同じような書きかたで書くことができます。

たとえば、embedded-halを参照して書かれているarduino-unoでのLチカのコードは

/*!
 * Blink the builtin LED - the "Hello World" of embedded programming.
 */
#![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);

    // Digital pin 13 is also connected to an onboard LED marked "L"
    let mut led = pins.d13.into_output();
    led.set_high();

    loop {
        led.toggle();
        arduino_hal::delay_ms(100);
        led.toggle();
        arduino_hal::delay_ms(100);
        led.toggle();
        arduino_hal::delay_ms(100);
        led.toggle();
        arduino_hal::delay_ms(800);
    }
}

でラズピコのLチカコード多少異なりますが書きかたが似ていることがわかります。

組み込みで所有権を賢く使う

let mut pac = pac::Peripherals::take().unwrap();
// .....
let pins = rp_pico::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
);

何度もpacがよばれていますが、これは何でしょうか?

pacはPeripheral Access Crateの略で、マイコンの周辺機器にアクセスするためのインターフェースを提供し、一度しか使用されないことを保証します。

例えば、

let mut pac = pac::Peripherals::take().unwrap();
// .....
let pins1 = rp_pico::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
);

let pins2 = rp_pico::Pins::new(
    pac.IO_BANK0,
    pac.PADS_BANK0,
    sio.gpio_bank0,
    &mut pac.RESETS,
); // Error

pins1が作られる際に、pac.IO_BANK0の所有権はpins1に移動します。
そのため、pins2で再びpac.IO_BANK0を使用しようとすると、所有権がすでに移動済みであるためエラーが発生します。

なぜこの制限が必要か?

pins1pins2が同時に存在すると、データ競合や無効なメモリアクセスが発生する可能性があります。
小規模なコードでは気付きやすいですが、大規模なコードベースではこれが致命的なエラーにつながることがあります。

Rustの所有権システムは、このような問題をコンパイル時に検出し、未然に防ぐ役割を果たします。

安全性と実装のトレードオフ

この「一度しか使えない」という制限のおかげで、安全なコードが書ける一方、実装は少し複雑になります。
しかし、この仕組みのおかげで、組み込みシステムでもデータ競合のない安全なコードが保証されます。

終わりに

これで、組み込みRustの重要な概念を話しました。
私の卒論が終わり予定が落ち着いたら、今後はPWMを用いたスピーカー制御や、私なりのDocs.rsの活用方法、データシートの読み方などについても解説記事を書いていく予定です。
組み込みRustの普及の一助となれば嬉しいです!

脚注
  1. ブートモードをするには、ラズピコのボタンを押しながらmicroBケーブルを差し込む ↩︎

  2. embedded-halのver1が最近でたので、もうそろそろ組み込みRustの安定時期がくるかもしれないが、現在は非互換な変更がバリバリに行われるので注意 ↩︎

  3. 私たちは、機械語でレジスタ操作を直接書かなくていいのは、抽象化された高級言語のおかげでレジスタ操作を意識する必要が無くなったからです。 ↩︎

  4. どのように実装されているかを知りたいならトレートを学んでみてください。どのように抽象化されているかイメージができると思います。 ↩︎

Discussion