RustのRTFM (Real Time For the Masses)を試してみる③

はじめに

組込みRust界の伝説japaric氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。

github.com

軽い気持ちで触り始めたのですが、読み進めるうちに、手続きマクロでゴリゴリ静的解析している変態実装であることがわかってきました。 少しずつフレームワークの実装にも突っ込んでいきたいですね。

リソース管理

cortex-m-rtクレートを使用する場合、割り込みとメイン関数間でのデータ共有に制限があります。 常に、全ての割り込みを無効化するcortex_m::interrupt::Mutexを使わなければなりません。 しかし、全ての割り込みを無効化することは、常に求められる条件ではありません。

例えば、2つの割り込みハンドラがデータを共有する場合、両者の優先度が同じで、プリエンプションが発生しないとすると、ロックは不要です。 RTFMでは、ソースコードを静的に解析することで、不要なロックをせずに、共有データにアクセスできるようになっています。 このような解析が可能な理由は、appアトリビュート内にアプリケーションの実装を、全て書くためです。

また、RTFMでは、動的な割り込み優先度の変更をサポートしていません。 そのため、全ての割り込みハンドラ間の優先度は静的に決定します。 これがうまいこと生きており、ある共有データを使用する割り込みハンドラ同士で、最も優先度の高いハンドラはロックを取得しなくても共有データにアクセスできます。

そろそろサンプルコードを見てみましょう。 共有データは、static変数として宣言します。

#[app(device = lm3s6965)]
const APP: () = {
    // A resource
    static mut SHARED: u32 = 0;

    #[init]
    fn init() {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);
    }

    #[idle]
    fn idle() -> ! {
        debug::exit(debug::EXIT_SUCCESS);

        // error: `SHARED` can't be accessed from this context
        // SHARED += 1;

        loop {}
    }

    // `SHARED` can be access from this context
    #[interrupt(resources = [SHARED])]
    fn UART0() {
        *resources.SHARED += 1;

        hprintln!("UART0: SHARED = {}", resources.SHARED).unwrap();
    }

    // `SHARED` can be access from this context
    #[interrupt(resources = [SHARED])]
    fn UART1() {
        *resources.SHARED += 1;

        hprintln!("UART1: SHARED = {}", resources.SHARED).unwrap();
    }
};

共有データにアクセスする関数は、アトリビュート内でresources = [<変数名>]を引数にします。 ここで、2つの割り込みハンドラUART0UART1は共に共有データSHAREDを使用します。 2つの割り込みは、割り込み優先度が同じで、プリエンプションも発生しません。 そのため、2つの割り込みハンドラでは、Mutexなしで共有データにアクセスできます。

割り込み優先度を設定する場合、同じ共有データを使用する割り込みハンドラのうち、優先度が最も高いハンドラだけはロックが不要です。

//! examples/lock.rs

#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    static mut SHARED: u32 = 0;

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::GPIOA);
    }

    // when omitted priority is assumed to be `1`
    #[interrupt(resources = [SHARED])]
    fn GPIOA(mut c: GPIOA::Context) {
        hprintln!("A").unwrap();

        // the lower priority task requires a critical section to access the data
        c.resources.SHARED.lock(|shared| {
            // data can only be modified within this critical section (closure)
            *shared += 1;

            // GPIOB will *not* run right now due to the critical section
            rtfm::pend(Interrupt::GPIOB);

            hprintln!("B - SHARED = {}", *shared).unwrap();

            // GPIOC does not contend for `SHARED` so it's allowed to run now
            rtfm::pend(Interrupt::GPIOC);
        });

        // critical section is over: GPIOB can now start

        hprintln!("E").unwrap();

        debug::exit(debug::EXIT_SUCCESS);
    }

    #[interrupt(priority = 2, resources = [SHARED])]
    fn GPIOB(mut c: GPIOB::Context) {
        // the higher priority task does *not* need a critical section
        *c.resources.SHARED += 1;

        hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap();
    }

    #[interrupt(priority = 3)]
    fn GPIOC(_: GPIOC::Context) {
        hprintln!("C").unwrap();
    }
};

ドキュメントによると、このプログラムの実行結果は、次のようになるようです。

$ cargo run --example lock
A
B - SHARED = 1
C
D - SHARED = 2
E

note: 手元の環境で実行すると、次の結果になります。要調査。レポジトリのCIは通っているようなので、手元環境の問題?

A D - SHARED = 2 B - SHARED = 2 C E

GPIOAの共有データアクセスは、クリティカルセクションのMutexを取得しており、割り込み禁止状態で行われます。

少し上記プログラムを改変して、どうなるか試してみます。

const APP: () = {
    static mut SHARED: u32 = 0;

// ...

    #[interrupt(resources = [SHARED])]
    fn GPIOA() {
        hprintln!("A").unwrap();

        // GPIOBと同じ方法でアクセスしてみます
        *c.resources.SHARED += 1;

        hprintln!("E").unwrap();

        debug::exit(debug::EXIT_SUCCESS);
    }

    #[interrupt(priority = 2, resources = [SHARED])]
    fn GPIOB(mut c: GPIOB::Context) {
        *c.resources.SHARED += 1;

        hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap();
    }

// ...
};

ビルドすると次のコンパイルエラーになります。

$ cargo run --example lock
error[E0614]: type `resources::SHARED<'_>` cannot be dereferenced
  --> examples/lock.rs:41:9
   |
41 |         *c.resources.SHARED += 1;
   |         ^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0614`.
error: Could not compile `cortex-m-rtfm`.

To learn more, run the command again with --verbose.

どうも無名の型になっているようで、詳細がつかみにくいですが、GPIOA()GPIOB()とで、resource::SHARED`の型が違う、ということがわかります。