0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Embassy・embedded-hal・probe-rsで実践するRust no_std組み込み開発2026

0
Last updated at Posted at 2026-04-15

Embassy・embedded-hal・probe-rsで実践するRust no_std組み込み開発2026

Rustのno_std環境を使った組み込み開発は、C/C++に代わるメモリ安全な選択肢として注目を集めています。2026年現在、Embassy(非同期ランタイム)、embedded-hal(ハードウェア抽象化)、probe-rs(デバッグツール)の3本柱が成熟し、実用的な開発が可能になりました。

本記事では、MLエンジニアがエッジAI推論デバイスやセンサーデバイスの開発に取り組む際に必要な、Rust no_std組み込み開発の実践知識を体系的に解説します。

この記事でわかること

  • Rustのno_std環境とは何か、std環境との違いと使い分け
  • Embassy async/awaitによるヒープ不要の非同期タスク管理の実装方法
  • embedded-halを使ったポータブルなドライバ設計パターン
  • probe-rs + defmtによる効率的なデバッグ・ログ出力の構築方法
  • heaplessクレートで実現するヒープ不要のデータ構造活用

対象読者

  • 想定読者: Rustの基礎文法を理解しているMLエンジニア
  • 必要な前提知識:
    • Rustの所有権・ライフタイム・トレイトの基本理解
    • Pythonでの組み込み/IoT開発経験があると望ましい(MicroPython等)
    • マイコン(MCU)の概念(GPIO、UART、SPI、I2C)の基礎知識

MLエンジニア向けの補足: 組み込み開発はPythonのnumpytorchの世界とは大きく異なります。OSが存在しない「ベアメタル」環境では、メモリアロケータもファイルシステムもありません。Pythonでいうimportに相当する標準ライブラリの大半が使えない世界です。本記事では、こうした違いを意識しながら解説を進めます。

結論・成果

Rust no_std環境での組み込み開発により、以下の効果が報告されています。

  • メモリ使用量: C実装と比較して同等〜30%削減(所有権システムによる不要なバッファ排除)
  • バグ検出: コンパイル時にメモリ安全性エラーの大半を検出(nullポインタ参照、データ競合、use-after-freeを言語仕様で排除)
  • 開発効率: cargoによるビルド・依存管理の一元化で、C/C++のMakefile + CMake環境と比較してビルド設定の工数を削減
  • デバッグ効率: defmtによるログ出力はフラッシュ消費を従来のフォーマット文字列方式と比較して大幅に削減(フォーマット文字列をホスト側で保持する仕組み)

no_std環境の基礎を理解する

stdとno_stdの違い

Rustの標準ライブラリは3層構造になっています。no_std環境では最下層のcoreクレートのみが利用可能です。

レイヤ 内容 no_stdでの利用
std ファイルI/O、ネットワーク、スレッド、ヒープ割当 利用不可
alloc VecStringBox(ヒープ割当が必要) アロケータ設定で利用可(非推奨)
core プリミティブ型、OptionResult、イテレータ、トレイト 利用可

MLエンジニア向けの補足: Pythonでいえば、stdnumpyosモジュール込みのフル環境、corebuiltinsのみの環境に相当します。no_stdではヒープメモリ(Pythonのlistdictが内部で使う動的メモリ)が原則使えません。

no_std環境の最小構成は以下のようになります。

// main.rs - no_std最小構成
#![no_std]
#![no_main]

use panic_halt as _; // パニック時にCPUを停止

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    // 組み込みでは main は永久ループ(戻り値なし = ! 型)
    loop {
        cortex_m::asm::wfi(); // Wait For Interrupt: 割込みまでCPUをスリープ
    }
}

#![no_std]は「標準ライブラリを使わない」宣言です。#![no_main]は「OSのエントリポイント(main関数の呼び出し規約)を使わない」宣言で、代わりに#[entry]マクロでエントリポイントを指定します。戻り値の!型(never型)は、この関数が決してリターンしないことを型レベルで保証しています。

なぜno_stdが必要なのか

組み込みマイコンには以下の制約があります。

  • RAM: 数KB〜数百KB(STM32F4で最大192KB、ESP32-C3で400KB)
  • フラッシュ: 数百KB〜数MB
  • OS: 存在しない(ベアメタル)
  • MMU: 多くのマイコンには仮想メモリ機構がない

stdライブラリはOSのシステムコール(ファイル操作、スレッド生成、ネットワーク通信)に依存しているため、OSのないベアメタル環境では使用できません。no_stdはこの制約に対応するRustの仕組みです。

よくある間違い: 「no_stdではRustの強みである安全性が犠牲になる」と誤解されがちですが、これは正しくありません。所有権システム、ライフタイム、パターンマッチングなどRustの安全性を支える機能はすべてcoreクレートに含まれており、no_stdでもフルに活用できます。犠牲になるのはヒープアロケーションと、それに依存するコレクション型(VecString等)だけです。

開発環境のセットアップ

Cortex-Mマイコン(STM32等)向けの開発環境を構築してみましょう。

# Rustツールチェインのインストール(未インストールの場合)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Cortex-M4F(FPU付き)ターゲットの追加
# Pythonでいう仮想環境のターゲット指定に相当
rustup target add thumbv7em-none-eabihf

# デバッグツール probe-rs のインストール
cargo install probe-rs-tools

# プロジェクトテンプレートの生成
cargo install cargo-generate
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart

ターゲット名の命名規則は以下のとおりです。

ターゲット 対応アーキテクチャ 代表的なマイコン
thumbv6m-none-eabi Cortex-M0 / M0+ STM32F0, RP2040
thumbv7m-none-eabi Cortex-M3 STM32F1, STM32F2
thumbv7em-none-eabi Cortex-M4 / M7(FPUなし) STM32F3(一部)
thumbv7em-none-eabihf Cortex-M4F / M7F(FPU付き) STM32F4, STM32H7
riscv32imc-unknown-none-elf RISC-V ESP32-C3

注意: eabieabihfの違いはFPU(浮動小数点演算ユニット)のハードウェアサポートの有無です。eabihfを指定するとハードウェアFPUを使った浮動小数点演算が可能になり、ML推論のような数値計算では性能差が顕著になります。ターゲットマイコンのデータシートでFPUの有無を確認してください。

Embassyで非同期組み込み開発を実装する

Embassyとは

Embassyは、Rust のasync/awaitを組み込み環境で実現するフレームワークです。従来の組み込み開発ではRTOS(リアルタイムOS)を使ってマルチタスクを実現していましたが、Embassyはコンパイル時にタスクをステートマシンに変換するasync/awaitの仕組みを活用し、ヒープ割り当てなし・スタック共有で効率的なマルチタスクを実現します。

MLエンジニア向けの補足: Pythonのasyncioと概念は似ていますが、決定的な違いがあります。Pythonのasyncioはヒープ上にコルーチンオブジェクトを生成しますが、Embassyのタスクはコンパイル時にサイズが確定し、静的メモリ上に配置されます。ランタイムオーバーヘッドがほぼゼロです。

Embassyプロジェクトのセットアップ

Embassy を使う STM32F4 プロジェクトのCargo.tomlは以下のようになります。

# Cargo.toml
[package]
name = "embassy-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread"] }
embassy-time = { version = "0.4", features = ["tick-hz-32_768"] }
embassy-stm32 = { version = "0.3", features = ["stm32f411ce", "time-driver-any", "memory-x"] }
cortex-m = { version = "0.7" }
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }

非同期タスクの実装

LEDの点滅とセンサー読み取りを並行実行する例を見てみましょう。

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::Timer;
use defmt::info;
use {defmt_rtt as _, panic_probe as _};

// LED点滅タスク: 500msごとに点滅を繰り返す
#[embassy_executor::task]
async fn blink_task(mut led: Output<'static>) {
    loop {
        led.set_high();
        info!("LED ON");
        Timer::after_millis(500).await;
        
        led.set_low();
        info!("LED OFF");
        Timer::after_millis(500).await;
    }
}

// センサー読み取りタスク: 1秒ごとに温度データを取得
#[embassy_executor::task]
async fn sensor_task(mut adc: embassy_stm32::adc::Adc<'static, embassy_stm32::peripherals::ADC1>) {
    let mut pin = unsafe { embassy_stm32::peripherals::PA0::steal() };
    loop {
        let raw_value = adc.blocking_read(&mut pin);
        // 12bit ADC: 0-4095 → 温度換算(例: LM35の場合 10mV/℃)
        let voltage_mv = (raw_value as f32) * 3300.0 / 4095.0;
        let temperature = voltage_mv / 10.0;
        info!("Temperature: {} C (raw: {})", temperature, raw_value);
        
        Timer::after_secs(1).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    
    let led = Output::new(p.PC13, Level::Low, Speed::Low);
    
    // ADCの初期化
    let adc = embassy_stm32::adc::Adc::new(p.ADC1);
    
    // タスクを並行起動
    spawner.spawn(blink_task(led)).unwrap();
    spawner.spawn(sensor_task(adc)).unwrap();
}

なぜEmbassyを選んだか:

  • ヒープ不要: タスクのメモリはコンパイル時に静的に確保される。RTOSのようにスタックサイズを手動で見積もる必要がない
  • 型安全: ペリフェラル(GPIO、ADC等)の所有権がRustの型システムで管理される。同じピンを2つのタスクから同時に操作しようとするとコンパイルエラーになる
  • 効率性: await時にCPUはスリープ状態になり、割込みで復帰する。ビジーウェイトと比べて消費電力を削減できる

注意点:

Embassyのタスクは'staticライフタイムを要求します。これはタスクがプログラム全体の寿命を持つことを意味し、ローカル変数の参照をタスクに渡すことはできません。ペリフェラルはmain関数で初期化し、タスクに所有権ごと移動(move)させる設計が基本です。

EmbassyとRTICの使い分け

Rust組み込みにはEmbassy以外にRTIC(Real-Time Interrupt-driven Concurrency)というフレームワークもあります。用途に応じた使い分けが重要です。

観点 Embassy RTIC
プログラミングモデル async/await(協調的) 割込み駆動(プリエンプティブ)
リアルタイム性 ソフトリアルタイム ハードリアルタイム
メモリ管理 静的タスク割当 静的リソース管理
学習コスト async/awaitに慣れていれば低い 割込み優先度の理解が必要
適用場面 IoTセンサー、通信デバイス モーター制御、産業制御
デッドロック 設計次第で発生しうる 静的解析でデッドロックフリーを保証

トレードオフ: Embassyは記述の容易さと生産性に優れますが、ハードリアルタイム制約(マイクロ秒単位の応答保証)が必要な場合はRTICが適しています。RTICは静的優先度解析により、デッドロックが発生しないことをコンパイル時に保証します。

embedded-halでポータブルなドライバを設計する

embedded-halの役割

embedded-halは、マイコンのペリフェラル(GPIO、SPI、I2C、UART等)に対する共通のトレイト(インターフェース)を定義するクレートです。

MLエンジニア向けの補足: Pythonのプロトコル(__iter____len__等)やABC(Abstract Base Class)に相当します。「SPI通信ができるデバイスならこのメソッドを持つ」という契約を定義し、ドライバがハードウェアの具体的な実装に依存しないようにします。

この設計により、ドライバを一度書けば STM32、ESP32、nRF52 など異なるマイコンで再利用できます。

I2Cセンサードライバの実装例

BME280(温湿度・気圧センサー)のドライバをembedded-halのトレイトを使って実装してみましょう。

// bme280.rs - embedded-halトレイトに依存したポータブルなドライバ
use embedded_hal::i2c::I2c;

const BME280_ADDR: u8 = 0x76;
const REG_CHIP_ID: u8 = 0xD0;
const REG_CTRL_MEAS: u8 = 0xF4;
const REG_PRESS_MSB: u8 = 0xF7;

pub struct Bme280<I2C> {
    i2c: I2C,
    address: u8,
}

#[derive(Debug)]
pub struct SensorData {
    pub temperature_c: f32,
    pub pressure_hpa: f32,
    pub humidity_pct: f32,
}

#[derive(Debug)]
pub enum Bme280Error<E> {
    I2cError(E),
    InvalidChipId(u8),
}

impl<E> From<E> for Bme280Error<E> {
    fn from(e: E) -> Self {
        Bme280Error::I2cError(e)
    }
}

impl<I2C: I2c> Bme280<I2C> {
    pub fn new(i2c: I2C) -> Self {
        Self {
            i2c,
            address: BME280_ADDR,
        }
    }

    pub fn init(&mut self) -> Result<(), Bme280Error<I2C::Error>> {
        // チップIDの確認(0x60 が正しい値)
        let mut buf = [0u8; 1];
        self.i2c.write_read(self.address, &[REG_CHIP_ID], &mut buf)?;
        if buf[0] != 0x60 {
            return Err(Bme280Error::InvalidChipId(buf[0]));
        }

        // 測定モードの設定: 温度x1 / 気圧x1 / 通常モード
        self.i2c.write(self.address, &[REG_CTRL_MEAS, 0b00100111])?;
        Ok(())
    }

    pub fn read_sensor(&mut self) -> Result<SensorData, Bme280Error<I2C::Error>> {
        let mut buf = [0u8; 8];
        self.i2c.write_read(self.address, &[REG_PRESS_MSB], &mut buf)?;

        // 生データから物理値への変換(補正係数の適用は簡略化)
        let press_raw = ((buf[0] as u32) << 12) | ((buf[1] as u32) << 4) | ((buf[2] as u32) >> 4);
        let temp_raw = ((buf[3] as u32) << 12) | ((buf[4] as u32) << 4) | ((buf[5] as u32) >> 4);
        let hum_raw = ((buf[6] as u16) << 8) | (buf[7] as u16);

        Ok(SensorData {
            temperature_c: (temp_raw as f32) / 16384.0 - 25.0,
            pressure_hpa: (press_raw as f32) / 256.0,
            humidity_pct: (hum_raw as f32) / 1024.0 * 100.0,
        })
    }
}

なぜこの設計か:

  • ジェネリクスによる抽象化: I2C: I2cというトレイト境界により、STM32のI2Cでも、ESP32のI2Cでも、テスト用のモックI2Cでも動作する
  • ゼロコスト抽象化: Rustのジェネリクスはモノモーフィゼーション(使用される具体型ごとに特殊化されたコードを生成)されるため、仮想関数テーブル(vtable)のオーバーヘッドがない
  • エラー型の伝搬: Bme280Error<E>でI2Cエラーをラップし、ドライバ固有のエラー(InvalidChipId)と区別できる

マイコン固有HALとの接続

上記のドライバをSTM32F4で使用する場合の接続コードです。

use embassy_stm32::i2c::I2c;
use embassy_stm32::time::Hertz;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());

    // I2Cペリフェラルの初期化(SCL=PB6, SDA=PB7, 100kHz)
    let i2c = I2c::new_blocking(
        p.I2C1,
        p.PB6,  // SCL
        p.PB7,  // SDA
        Hertz(100_000),
        Default::default(),
    );

    // embedded-halトレイトを実装したI2Cインスタンスをドライバに渡す
    let mut bme280 = Bme280::new(i2c);
    bme280.init().unwrap();

    loop {
        match bme280.read_sensor() {
            Ok(data) => {
                defmt::info!(
                    "Temp: {} C, Press: {} hPa, Hum: {} %",
                    data.temperature_c,
                    data.pressure_hpa,
                    data.humidity_pct
                );
            }
            Err(e) => {
                defmt::error!("Sensor read failed: {:?}", e);
            }
        }
        Timer::after_secs(2).await;
    }
}

ハマりポイント: I2Cのプルアップ抵抗を忘れると通信が不安定になります。多くの開発ボードにはプルアップ抵抗が搭載されていますが、ブレッドボード配線では4.7kΩの外部プルアップ抵抗が必要です。通信エラーが頻発する場合、まずハードウェア配線を確認してください。

probe-rsとdefmtで効率的にデバッグする

defmtによる軽量ログ出力

組み込み環境ではprintln!が使えません。代わりにdefmt(deferred formatting)を使います。defmtはフォーマット文字列をホストPC側に保持し、デバイスからは文字列のインデックスと変数データのみを送信する仕組みです。

従来のsprintfベースのログでは、フォーマット文字列("Temperature: %d C"等)がすべてフラッシュメモリに格納されます。defmtではフォーマット文字列はELFバイナリのメタデータとしてホストPC側に保持されるため、デバイスのフラッシュ消費を抑えられます。

defmtの使い方

use defmt::{info, warn, error, debug, trace};
use defmt_rtt as _; // RTT(Real-Time Transfer)経由での出力を有効化

// 基本的なログ出力
info!("System initialized");
info!("ADC value: {}", adc_value);

// 構造体のフォーマット(defmt::Format トレイトを derive)
#[derive(defmt::Format)]
struct SensorReading {
    channel: u8,
    value: u16,
    timestamp_ms: u32,
}

let reading = SensorReading {
    channel: 0,
    value: 2048,
    timestamp_ms: 1500,
};
info!("Sensor: {:?}", reading);

// ログレベルによるフィルタリング
// DEFMT_LOG=info でビルドすると debug/trace は除外される
debug!("Detailed state: buffer_len={}", buf.len());
trace!("Raw bytes: {:x}", &raw_data[..]);

// タイムスタンプの自動付与
// defmt-rtt と embassy-time を連携させると自動でタイムスタンプが付く

probe-rsによるフラッシュ書き込みとログ確認

# ビルド + フラッシュ書き込み + RTTログ表示を1コマンドで実行
cargo run --release

# .cargo/config.toml に以下を設定しておく
# [target.thumbv7em-none-eabihf]
# runner = "probe-rs run --chip STM32F411CEUx"

.cargo/config.tomlの設定例です。

# .cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F411CEUx"
rustflags = [
    "-C", "link-arg=-Tlink.x",      # リンカスクリプト
    "-C", "link-arg=-Tdefmt.x",     # defmtのシンボル情報
]

[build]
target = "thumbv7em-none-eabihf"

[env]
DEFMT_LOG = "info"  # ログレベルの設定

なぜprobe-rs + defmtを選んだか:

  • 統合ツールチェイン: フラッシュ書き込み・RTTログ・デバッグを1つのツールで完結。OpenOCD + GDB + カスタムログ実装という従来のC開発と比較して、セットアップが大幅に簡素化される
  • 型安全なログ: defmtのフォーマット文字列はコンパイル時に型チェックされる。Cのprintfでの%d%sの取り違えのような実行時エラーが発生しない
  • VS Code統合: probe-rs VS Code拡張により、ブレークポイント、変数ウォッチ、ステップ実行がGUI上で可能

制約: probe-rsが対応するデバッグプローブはST-Link、J-Link、CMSIS-DAPです。中華製の安価なST-Link互換品でも動作しますが、ファームウェアが古い場合は更新が必要な場合があります。

heaplessクレートでヒープ不要のデータ構造を活用する

なぜheaplessが必要か

no_std環境ではヒープアロケータがデフォルトで存在しないため、std::vec::Vecstd::string::Stringが使えません。heaplessクレートは、コンパイル時に容量が確定する固定サイズのデータ構造を提供します。

std型 heapless型 違い
Vec<T> heapless::Vec<T, N> 最大N要素の固定容量。push時にfullならErrを返す
String heapless::String<N> 最大Nバイトの固定容量文字列
VecDeque<T> heapless::Deque<T, N> 固定容量の両端キュー
HashMap<K, V> heapless::LinearMap<K, V, N> 線形探索のマップ(小規模向け)
BinaryHeap<T> heapless::BinaryHeap<T, _, N> 固定容量の優先度キュー

MLエンジニア向けの補足: NumPyの固定サイズ配列(np.zeros(100))に近い概念です。Pythonのlistは動的にサイズが変わりますが、heaplessのVecは最大容量が型パラメータで決まり、それを超えるとエラーが返ります。メモリの動的確保が不要なため、処理時間が予測可能(ハードリアルタイム向き)です。

実装例: センサーデータのリングバッファ

use heapless::Vec;
use heapless::String;
use heapless::spsc::Queue;
use core::fmt::Write;

// 固定容量のセンサーデータバッファ
struct SensorBuffer {
    // 最大32個のf32値を格納
    readings: Vec<f32, 32>,
    // 最大128バイトのログメッセージ
    log_message: String<128>,
}

impl SensorBuffer {
    fn new() -> Self {
        Self {
            readings: Vec::new(),
            log_message: String::new(),
        }
    }

    fn add_reading(&mut self, value: f32) -> Result<(), f32> {
        // 容量超過時はErrで通知(パニックしない)
        self.readings.push(value)
    }

    fn average(&self) -> Option<f32> {
        if self.readings.is_empty() {
            return None;
        }
        let sum: f32 = self.readings.iter().sum();
        Some(sum / self.readings.len() as f32)
    }

    fn format_status(&mut self) -> &str {
        self.log_message.clear();
        // core::fmt::Write トレイトで文字列フォーマット
        write!(
            self.log_message,
            "readings={}, avg={:.1}",
            self.readings.len(),
            self.average().unwrap_or(0.0)
        )
        .ok();
        self.log_message.as_str()
    }
}

// タスク間通信: SPSC(Single Producer Single Consumer)キュー
// 割込みハンドラ → メインタスクのデータ受け渡しに使用
static SENSOR_QUEUE: Queue<f32, 16> = Queue::new();

// 割込みハンドラ側(プロデューサー)
fn adc_interrupt_handler(value: f32) {
    let mut producer = unsafe { SENSOR_QUEUE.split().0 };
    let _ = producer.enqueue(value); // fullなら破棄
}

// メインタスク側(コンシューマー)
async fn process_sensor_data() {
    let mut consumer = unsafe { SENSOR_QUEUE.split().1 };
    let mut buffer = SensorBuffer::new();
    
    loop {
        while let Some(value) = consumer.dequeue() {
            let _ = buffer.add_reading(value);
        }
        
        if buffer.readings.len() >= 32 {
            defmt::info!("{}", buffer.format_status());
            buffer.readings.clear();
        }
        
        Timer::after_millis(100).await;
    }
}

注意点:

heapless::Vecpushstd::vec::Vecと異なり、容量超過時にパニックではなくErrを返します。これは組み込み環境では望ましい動作です。パニックが発生するとシステム全体が停止するため、エラーハンドリングで回復可能にしておくことが重要です。

SPSCキューは単一プロデューサー・単一コンシューマー専用です。複数の割込みハンドラからデータを送る場合はheapless::mpmc::MpMcQueueを使用してください。ただし、MPMCキューはCAS(Compare-And-Swap)操作を使うため、アーキテクチャがアトミック操作をサポートしている必要があります(Cortex-M3以上)。

実践的なプロジェクト構成とビルド設定を整備する

プロジェクトのディレクトリ構成

実用的なno_stdプロジェクトの推奨構成です。

my-embedded-project/
├── .cargo/
│   └── config.toml          # ターゲット・ランナー設定
├── src/
│   ├── main.rs              # エントリポイント・タスク定義
│   ├── drivers/
│   │   ├── mod.rs
│   │   ├── bme280.rs         # センサードライバ
│   │   └── display.rs        # ディスプレイドライバ
│   └── tasks/
│       ├── mod.rs
│       ├── sensor.rs          # センサー読取タスク
│       └── communication.rs   # 通信タスク
├── memory.x                   # メモリレイアウト定義
├── Cargo.toml
├── build.rs                   # ビルドスクリプト
└── Embed.toml                 # probe-rs設定(任意)

メモリレイアウトの定義

マイコンのフラッシュとRAMのアドレス・サイズをmemory.xで定義します。

/* memory.x - STM32F411CE のメモリレイアウト */
MEMORY
{
    FLASH : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   : ORIGIN = 0x20000000, LENGTH = 128K
}

ハマりポイント: memory.xのアドレスとサイズはマイコンのデータシートと正確に一致させる必要があります。RAMサイズを実際より大きく設定すると、実行時にスタックオーバーフローが発生し、原因の特定が困難になります。STM32の場合、STM32CubeMXでメモリマップを確認できます。

ビルドプロファイルの最適化

組み込みではバイナリサイズの最適化が重要です。

# Cargo.toml のプロファイル設定
[profile.release]
opt-level = "s"       # サイズ最適化("z"はさらに小さいが遅い場合がある)
lto = true            # Link-Time Optimization: 不要コードの除去
codegen-units = 1     # 単一コンパイル単位: LTOの効果を最大化
debug = 2             # リリースビルドでもデバッグ情報を保持(defmtに必要)
overflow-checks = false  # 整数オーバーフローチェックを無効化(サイズ削減)

[profile.dev]
opt-level = 1         # デバッグビルドでも最低限の最適化(サイズ対策)
opt-level バイナリサイズ(目安) 実行速度 用途
"0" 最大 最遅(最適化なし) デバッグ専用
"1" 中程度 dev(デバッグ+最低限の最適化)
"2" 速い 速度重視のリリース
"s" 中〜速い サイズ重視のリリース(推奨)
"z" 最小 遅い場合がある フラッシュが極めて限られる場合

トレードオフ: opt-level = "z"は最小バイナリサイズを実現しますが、ループの展開やインライン化が抑制されるため、特にDSP処理やMLの推論ループでは"s"より遅くなることがあります。ベンチマークで確認してから採用してください。

よくある問題と解決方法

組み込みRust開発で遭遇しやすいトラブルとその対処法をまとめます。

問題 原因 解決方法
can't find crate for 'std' no_std環境で依存クレートがstdを使っている 依存クレートのdefault-features = falseを確認。features = ["no_std"]等のフラグを有効化
フラッシュ書き込み失敗 デバッグプローブの接続不良・チップ名の誤り probe-rs infoで接続確認。--chipオプションの型番を正確に指定
バイナリが大きすぎてフラッシュに収まらない 最適化不足・不要なクレートの依存 opt-level = "s" + lto = trueを設定。cargo bloatでサイズ分析
スタックオーバーフロー 再帰やスタック上の大きな配列 flip-linkを導入(スタックオーバーフローを検出可能にする)。大きなバッファはstaticに置く
割込みハンドラ内でのパニック ロック取得の失敗・アサーション 割込みハンドラではtry_lockを使い、失敗時はスキップ。panic_probeでパニック位置を特定
I2C/SPI通信が不安定 プルアップ抵抗の欠如・クロック速度の不一致 ハードウェア配線確認。クロック速度を下げてテスト(100kHz → 10kHz)

cargo-bloatによるバイナリサイズ解析

バイナリサイズが問題になった場合、cargo-bloatで関数ごとのサイズを確認できます。

# インストール
cargo install cargo-bloat

# 関数ごとのサイズ上位20件を表示
cargo bloat --release -n 20

# クレートごとのサイズを表示
cargo bloat --release --crates

フォーマット関連(core::fmt)が大きなサイズを占める場合があります。defmtを使うことでcore::fmtへの依存を排除し、バイナリサイズを削減できます。

まとめと次のステップ

まとめ:

  • no_std環境はOSのないマイコン向けのRust開発モード。coreクレートの範囲でRustの安全性機能をフル活用できる
  • Embassyasync/awaitベースの非同期ランタイム。ヒープ不要・静的タスク割当でRTOS代替として実用的
  • embedded-halはハードウェア抽象化の標準トレイト。ドライバをマイコン非依存で設計でき、エコシステムの再利用性を高める
  • probe-rs + defmtはデバッグの統合ツールチェイン。フラッシュ書き込み・ログ・ステップ実行を1つのツールで完結
  • heaplessクレートはヒープ不要の固定容量データ構造。リアルタイム制約のある環境で予測可能なメモリ使用を実現

次にやるべきこと:

  1. The Embedded Rust Bookを一読し、cortex-m-quickstartテンプレートでLED点滅を動かす
  2. Embassy Bookのチュートリアルで非同期タスクの基本を習得する
  3. 実際のセンサー(BME280等)をI2C接続し、embedded-halベースのドライバを自作する

制約と留意点:

  • Rustの組み込みエコシステムはC/C++と比較してまだ発展途上であり、特定のマイコン向けHALクレートの完成度にはばらつきがあります
  • コンパイル時間がC/C++より長くなる傾向があり、cargo build --releaseで数分かかる場合があります
  • 商用RTOSが必要な認証(DO-178C等)に対応するRustツールチェインの認定は進行中ですが、2026年時点では限定的です

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?