ソフトウェアタスクと spawn
RTIC におけるソフトウェアタスクの概念は、ハードウェアタスク と多くの共通点があります。中核となる違いは、ソフトウェアタスクが特定の割り込みベクタに明示的に束縛されるのではなく、ソフトウェアタスクの意図した優先度で動作する「ディスパッチャ」割り込みベクタに束縛される点です(以下を参照)。
ハードウェア タスクと同様に、関数に付ける #[task] 属性はその関数をタスクとして宣言します。この属性に binds = InterruptName 引数がない場合、その関数は ソフトウェアタスク として宣言されます。
静的メソッド task_name::spawn() はソフトウェアタスクを spawn(開始)し、より高い優先度のタスクが実行中でなければ、そのタスクは直ちに実行を開始します。
ソフトウェア タスク自体は async な Rust 関数として与えられ、これによりユーザーは将来のイベントを任意に await できます。これにより、リアクティブプログラミング(ハードウェア タスクによる)と逐次的なプログラミング(ソフトウェア タスクによる)を組み合わせられます。
ハードウェア タスクは run-to-completion で実行されて終了するものと想定される一方、ソフトウェア タスクは一度開始(spawn)されると、任意のループ(実行パス)が少なくとも 1 つの await(yield 操作)によって中断されることを条件に、永続的に実行できます。
ディスパッチャ
同じ優先度レベルにあるすべての ソフトウェア タスクは、ソフトウェアタスクをディスパッチする async executor として動作する 1 つの割り込みハンドラを共有します。このディスパッチャのリスト dispatchers = [FreeInterrupt1, FreeInterrupt2, ...] は #[app] 属性の引数であり、そこで空いていて使用可能な割り込みの集合を定義します。
ディスパッチャとして機能する各割り込みベクタには 1 つの優先度レベルが割り当てられるため、ディスパッチャのリストはソフトウェアタスクで使用されるすべての優先度レベルをカバーする必要があります。
例: ソフトウェアタスクに 3 つの異なる優先度を使用するアプリケーションでは、dispatchers = 引数には少なくとも 3 つのエントリが必要です。
提供されたディスパッチャが不足している場合、またはディスパッチャのリストと ハードウェア タスクに束縛された割り込みの間で衝突が発生した場合、フレームワークはコンパイルエラーを出します。
以下の例を参照してください。
//! examples/spawn.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
hprintln!("init");
foo::spawn().unwrap();
(Shared {}, Local {})
}
#[task]
async fn foo(_: foo::Context) {
hprintln!("foo");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example spawn
init
foo
ソフトウェア タスクが run-to-completion で終了していれば、そのタスクを再度 spawn できます。
以下の例では、idle タスクから ソフトウェア タスク foo を spawn しています。ソフトウェア タスクの優先度は 1(idle より高い)なので、ディスパッチャは foo を実行します(idle をプリエンプトします)。foo は run-to-completion で終了するため、foo タスクを再度 spawn しても問題ありません。
技術的には、async executor は foo の future を poll し、この場合その future は completed 状態になります。
//! examples/spawn_loop.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
hprintln!("init");
(Shared {}, Local {})
}
#[idle]
fn idle(_: idle::Context) -> ! {
for _ in 0..3 {
foo::spawn().unwrap();
hprintln!("idle");
}
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
loop {}
}
#[task(priority = 1)]
async fn foo(_: foo::Context) {
hprintln!("foo");
}
}
$ cargo xtask qemu --verbose --example spawn_loop
init
foo
idle
foo
idle
foo
idle
すでに spawn 済みのタスク(実行中のタスク)を spawn しようとすると、エラーになります。ここで注目すべき点は、このエラーが foo タスクが実際に実行される前に報告されることです。これは、ソフトウェア タスクの実際の実行がディスパッチャ割り込み(SSIO)によって処理される一方で、その割り込みは init タスクを抜けるまで有効にならないためです。(init はクリティカルセクションで実行され、つまりすべての割り込みが無効化されていることを思い出してください。)
技術的には、completed 状態にない future に対する spawn はエラーと見なされます。
//! examples/spawn_err.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
hprintln!("init");
foo::spawn().unwrap();
match foo::spawn() {
Ok(_) => {}
Err(()) => hprintln!("Cannot spawn a spawned (running) task!"),
}
(Shared {}, Local {})
}
#[task]
async fn foo(_: foo::Context) {
hprintln!("foo");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example spawn_err
init
Cannot spawn a spawned (running) task!
foo
引数の受け渡し
次のように、spawn 時に引数を渡すこともできます。
//! examples/spawn_arguments.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
foo::spawn(1, 1).unwrap();
assert!(foo::spawn(1, 4).is_err()); // The capacity of `foo` is reached
(Shared {}, Local {})
}
#[task]
async fn foo(_c: foo::Context, x: i32, y: u32) {
hprintln!("foo {}, {}", x, y);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example spawn_arguments
foo 1, 1
発散タスク
タスクは 2 つのシグネチャのいずれかを取れます: async fn({name}::Context, ..) または async fn({name}::Context, ..) -> !。後者は 発散 タスク、つまり決して return しないタスクを定義します。発散タスクの主な利点は、'static なコンテキストを受け取り、local リソースが 'static ライフタイムを持つことです。さらに、このシグネチャを使うことでタスクの意図が明示され、短命なタスクと無期限に実行されるタスクを明確に区別できます。.await によって制御を明け渡し、同じ優先度レベルの他のタスクを飢餓状態にしないよう注意してください。
優先度ゼロのタスク
RTIC では、タスクは互いにプリエンプティブに実行され、優先度ゼロ(0)が最も低い優先度です。優先度ゼロのタスクは、厳密なリアルタイム要件のないバックグラウンド処理に使用できます。
概念的には、そのようなタスクはアプリケーションの main スレッドで実行されるものと見なせるため、関連するリソースには Send トレイト境界は要求されません。
//! examples/zero-prio-task.rs
#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
use core::marker::PhantomData;
use panic_semihosting as _;
/// Does not impl send
pub struct NotSend {
_0: PhantomData<*const ()>,
}
#[rtic::app(device = lm3s6965, peripherals = true)]
mod app {
use super::NotSend;
use core::marker::PhantomData;
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
x: NotSend,
}
#[local]
struct Local {
y: NotSend,
}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
hprintln!("init");
async_task::spawn().unwrap();
async_task2::spawn().unwrap();
(
Shared {
x: NotSend { _0: PhantomData },
},
Local {
y: NotSend { _0: PhantomData },
},
)
}
#[task(priority = 0, shared = [x], local = [y])]
async fn async_task(_: async_task::Context) {
hprintln!("hello from async");
}
#[task(priority = 0, shared = [x])]
async fn async_task2(_: async_task2::Context) {
hprintln!("hello from async2");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
$ cargo xtask qemu --verbose --example zero-prio-task
init
hello from async
hello from async2
注意: 優先度ゼロの ソフトウェア タスクは [idle] タスクと共存できません。理由は、
idleが優先度ゼロの、復帰しない Rust 関数として実行されるためです。したがって、優先度ゼロの executor が同じ優先度の ソフトウェア タスクに制御を渡す方法がありません。
アプリケーション側の安全性: 技術的には、RTIC フレームワークは completed 状態の future を持つ ソフトウェア タスクに対して poll が決して実行されないことを保証しており、これにより async Rust の健全性の規則に従います。