リソースの使用
RTIC フレームワークは共有リソースとタスクローカルリソースを管理し、unsafe コードを使用せずに永続的なデータ保存と安全なアクセスを可能にします。
RTIC のリソースは #[app] モジュール内で宣言された関数からのみ参照可能であり、フレームワークはリソースのアクセス可能性を(タスクごとに)ユーザーが完全に制御できるようにします。
システム全体のリソースは、#[app] モジュール内の 2 つ の struct に #[local] および #[shared] 属性を付けることで宣言します。これらの構造体の各フィールドは、それぞれ異なるリソース(フィールド名で識別)に対応します。この 2 種類のリソースの違いについては以下で説明します。
各タスクは、対応するメタデータ属性で local 引数と shared 引数を使って、アクセスする予定のリソースを宣言する必要があります。各引数にはリソース識別子のリストを指定します。列挙したリソースは、Context 構造体の local フィールドおよび shared フィールドを通じてコンテキストから利用可能になります。
init タスクは、システム全体の(#[shared] および #[local])リソースの初期値を返します。
#[local] リソース
#[local] リソースは特定のタスクからローカルにアクセス可能なリソースであり、そのタスクだけがロックやクリティカルセクションなしでそのリソースにアクセスできます。これにより、通常はドライバーや大きなオブジェクトであるリソースを #[init] で初期化し、その後特定のタスクに渡すことができます。
したがって、あるタスクの #[local] リソースには、ただ 1 つのタスクだけがアクセスできます。同じ #[local] リソースを複数のタスクに割り当てようとすると、コンパイル時エラーになります。
#[local] リソースの型は、init から対象タスクへ送られ、スレッド境界をまたぐため、Send トレイトを実装していなければなりません。
以下に示すアプリケーション例には、foo、bar、idle の 3 つのタスクがあり、それぞれが自身の #[local] リソースにアクセスできます。
//! examples/locals.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {
local_to_foo: i64,
local_to_bar: i64,
local_to_idle: i64,
}
// `#[init]` cannot access locals from the `#[local]` struct as they are initialized here.
#[init]
fn init(_: init::Context) -> (Shared, Local) {
foo::spawn().unwrap();
bar::spawn().unwrap();
(
Shared {},
// initial values for the `#[local]` resources
Local {
local_to_foo: 0,
local_to_bar: 0,
local_to_idle: 0,
},
)
}
// `local_to_idle` can only be accessed from this context
#[idle(local = [local_to_idle])]
fn idle(cx: idle::Context) -> ! {
let local_to_idle = cx.local.local_to_idle;
*local_to_idle += 1;
hprintln!("idle: local_to_idle = {}", local_to_idle);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
// error: no `local_to_foo` field in `idle::LocalResources`
// _cx.local.local_to_foo += 1;
// error: no `local_to_bar` field in `idle::LocalResources`
// _cx.local.local_to_bar += 1;
loop {
cortex_m::asm::nop();
}
}
// `local_to_foo` can only be accessed from this context
#[task(local = [local_to_foo], priority = 1)]
async fn foo(cx: foo::Context) {
let local_to_foo = cx.local.local_to_foo;
*local_to_foo += 1;
// error: no `local_to_bar` field in `foo::LocalResources`
// cx.local.local_to_bar += 1;
hprintln!("foo: local_to_foo = {}", local_to_foo);
}
// `local_to_bar` can only be accessed from this context
#[task(local = [local_to_bar], priority = 1)]
async fn bar(cx: bar::Context) {
let local_to_bar = cx.local.local_to_bar;
*local_to_bar += 1;
// error: no `local_to_foo` field in `bar::LocalResources`
// cx.local.local_to_foo += 1;
hprintln!("bar: local_to_bar = {}", local_to_bar);
}
}
この例を実行するには:
$ cargo xtask qemu --verbose --example locals
bar: local_to_bar = 1
foo: local_to_foo = 1
idle: local_to_idle = 1
#[init] と #[idle] のローカルリソースは 'static ライフタイムを持ちます。これは、どちらのタスクも再入可能ではないため安全です。
タスクローカルな初期化済みリソース
ローカルリソースは、#[task(local = [my_var: TYPE = INITIAL_VALUE, ...])] のようにリソース指定内で直接指定することもできます。これにより、#[init] で初期化する必要のないローカルを作成できます。
#[task(local = [..])] リソースの型は、スレッド境界をまたがないため、Send でも Sync でもある必要はありません。
以下の例では、さまざまな使い方とライフタイムを示しています。
//! examples/declared_locals.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965)]
mod app {
use cortex_m_semihosting::debug;
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init(local = [a: u32 = 0])]
fn init(cx: init::Context) -> (Shared, Local) {
// Locals in `#[init]` have 'static lifetime
let _a: &'static mut u32 = cx.local.a;
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
(Shared {}, Local {})
}
#[idle(local = [a: u32 = 0])]
fn idle(cx: idle::Context) -> ! {
// Locals in `#[idle]` have 'static lifetime
let _a: &'static mut u32 = cx.local.a;
loop {}
}
#[task(binds = UART0, local = [a: u32 = 0])]
fn foo(cx: foo::Context) {
// Locals in `#[task]`s have a local lifetime
let _a: &mut u32 = cx.local.a;
// error: explicit lifetime required in the type of `cx`
// let _a: &'static mut u32 = cx.local.a;
}
}
アプリケーションを実行することもできますが、この例はライフタイムの性質を示すことだけを目的としているため、出力はありません(アプリケーションをビルドするだけで十分です)。
$ cargo build --target thumbv7m-none-eabi --example declared_locals
#[shared] リソースと lock
#[shared] リソースにデータ競合なしでアクセスするにはクリティカルセクションが必要です。そのため、渡される Context の shared フィールドは、そのタスクからアクセス可能な各共有リソースについて Mutex トレイトを実装します。このトレイトには lock という 1 つのメソッドしかなく、そのクロージャ引数をクリティカルセクション内で実行します。
lock API によって作成されるクリティカルセクションは動的優先度に基づいています。つまり、他のタスクがそのクリティカルセクションをプリエンプトできないように、コンテキストの動的優先度を一時的に ceiling 優先度まで引き上げます。この同期プロトコルは Immediate Ceiling Priority Protocol (ICPP) として知られており、RTIC の Stack Resource Policy (SRP) ベースのスケジューリングに準拠しています。
以下の例では、優先度 1 から 3 の 3 つの割り込みハンドラがあります。低い優先度を持つ 2 つのハンドラは shared リソースをめぐって競合し、そのデータにアクセスするにはそのリソースのロック取得に成功する必要があります。shared リソースにアクセスしない最も高い優先度のハンドラは、最も低い優先度のハンドラが作成したクリティカルセクションを自由にプリエンプトできます。
//! examples/lock.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [GPIOA, GPIOB, GPIOC])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
shared: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
foo::spawn().unwrap();
(Shared { shared: 0 }, Local {})
}
// when omitted priority is assumed to be `1`
#[task(shared = [shared])]
async fn foo(mut c: foo::Context) {
hprintln!("A");
// the lower priority task requires a critical section to access the data
c.shared.shared.lock(|shared| {
// data can only be modified within this critical section (closure)
*shared += 1;
// bar will *not* run right now due to the critical section
bar::spawn().unwrap();
hprintln!("B - shared = {}", *shared);
// baz does not contend for `shared` so it's allowed to run now
baz::spawn().unwrap();
});
// critical section is over: bar can now start
hprintln!("E");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
#[task(priority = 2, shared = [shared])]
async fn bar(mut c: bar::Context) {
// the higher priority task does still need a critical section
let shared = c.shared.shared.lock(|shared| {
*shared += 1;
*shared
});
hprintln!("D - shared = {}", shared);
}
#[task(priority = 3)]
async fn baz(_: baz::Context) {
hprintln!("C");
}
}
$ cargo xtask qemu --verbose --example lock
A
B - shared = 1
C
D - shared = 2
E
#[shared] リソースの型は Send でなければなりません。
Multi-lock
lock の拡張として、また右方向へのインデントの増大を抑えるため、ロックはタプルとして取得できます。以下の例はその使用方法を示しています。
//! examples/mutlilock.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [GPIOA])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
shared1: u32,
shared2: u32,
shared3: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
locks::spawn().unwrap();
(
Shared {
shared1: 0,
shared2: 0,
shared3: 0,
},
Local {},
)
}
// when omitted priority is assumed to be `1`
#[task(shared = [shared1, shared2, shared3])]
async fn locks(c: locks::Context) {
let s1 = c.shared.shared1;
let s2 = c.shared.shared2;
let s3 = c.shared.shared3;
(s1, s2, s3).lock(|s1, s2, s3| {
*s1 += 1;
*s2 += 1;
*s3 += 1;
hprintln!("Multiple locks, s1: {}, s2: {}, s3: {}", *s1, *s2, *s3);
});
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example multilock
Multiple locks, s1: 1, s2: 1, s3: 1
共有 (&-) アクセスのみ
デフォルトでは、フレームワークはすべてのタスクがリソースへの排他的な可変アクセス (&mut-) を必要とすると仮定しますが、shared リストで &resource_name 構文を使うことで、タスクがリソースへの共有アクセス (&-) だけを必要とすることを指定できます。
リソースへの共有アクセス (&-) を指定する利点は、異なる優先度で実行される複数のタスクがそのリソースをめぐって競合している場合でも、リソースにアクセスするためのロックが不要になることです。欠点は、そのタスクがリソースへの共有参照 (&-) しか得られず、実行できる操作が制限されることですが、共有参照で十分な場合には、この方法によって必要なロックの数を減らせます。単純なイミュータブルデータに加えて、リソース型自体が適切なロックやアトミック操作によって安全に内部可変性を実装している場合にも、この共有アクセスは有用です。
この RTIC のリリースでは、異なるタスクから 同じ リソースに対して排他的アクセス (&mut-) と共有アクセス (&-) の両方を要求することはできない点に注意してください。そうしようとすると、コンパイルエラーになります。
以下の例では、キー(たとえば暗号鍵)を実行時にロード(または作成)し(init から返される)、その後、異なる優先度で実行される 2 つのタスクから、いかなる種類のロックも使わずに使用します。
//! examples/only-shared-access.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
key: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
foo::spawn().unwrap();
bar::spawn().unwrap();
(Shared { key: 0xdeadbeef }, Local {})
}
#[task(shared = [&key])]
async fn foo(cx: foo::Context) {
let key: &u32 = cx.shared.key;
hprintln!("foo(key = {:#x})", key);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
#[task(priority = 2, shared = [&key])]
async fn bar(cx: bar::Context) {
hprintln!("bar(key = {:#x})", cx.shared.key);
}
}
$ cargo xtask qemu --verbose --example only-shared-access
bar(key = 0xdeadbeef)
foo(key = 0xdeadbeef)
共有リソースへのロックフリーアクセス
#[shared] リソースが 同じ 優先度で動作するタスクからのみアクセスされる場合、それにアクセスするためにクリティカルセクションは 不要 です。この場合、リソース宣言にフィールドレベル属性 #[lock_free] を追加することで、lock API の使用を省略できます(以下の例を参照)。
Rust のaliasing規則に従うため、リソースには複数の不変参照を通じてアクセスするか、単一の可変参照を通じてアクセスするかのいずれかのみが可能です(ただし、同時に両方は不可です)。
異なる優先度で動作するタスク間で共有されるリソースに #[lock_free] を使用すると、コンパイル時 エラーになります – lock API を使用しないと、前述の aliasing 規則に違反するためです。同様に、各優先度について、共有リソースにアクセスできる ソフトウェア タスクは 1 つだけです(async タスクは、同じ優先度で動作するほかの ソフトウェア または ハードウェア タスクに実行を譲る可能性があるためです)。しかし、この単一タスク制約の下では、そのリソースは実質的にもはや shared ではなく、むしろ local であるとみなせます。したがって、#[lock_free] を付与した共有リソースを使用すると コンパイル時 エラーになります – 適用できる場合は、代わりに #[local] リソースを使用してください。
//! examples/lock-free.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965)]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
#[shared]
struct Shared {
#[lock_free] // <- lock-free shared resource
counter: u64,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
rtic::pend(Interrupt::UART0);
(Shared { counter: 0 }, Local {})
}
#[task(binds = UART0, shared = [counter])] // <- same priority
fn foo(c: foo::Context) {
rtic::pend(Interrupt::UART1);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" foo = {}", counter);
}
#[task(binds = UART1, shared = [counter])] // <- same priority
fn bar(c: bar::Context) {
rtic::pend(Interrupt::UART0);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" bar = {}", counter);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example lock-free
foo = 1
bar = 2