グローバル変数とのデータ共有
NOTE この内容の一部は、James Munns によるブログ記事 Interrupts Is Threads から(許可を得て)引用しています。この記事には、この トピックに関するより詳しい議論が含まれています。
前にも述べたように、割り込みが発生しても、こちらには何の引数も渡されず、 結果を返すこともできません。これにより、プログラムがペリフェラルや、メイン プログラムのほかの状態とやり取りするのが難しくなります。このベアメタル組み込み 特有の問題について考える前に、まずは “std” Rust におけるスレッドについて考えて みる価値があります。
“std” Rust: スレッドとのデータ共有
“std” Rust でも、スレッドを spawn するようなことをするときには、データ共有を 考える必要があります。
何かをスレッドに 渡したい ときは、所有権を伴ってクロージャに渡すことがあります。
#![allow(unused)]
fn main() {
// 現在のスレッドで文字列を作成する
let data = String::from("hello");
// 新しいスレッドを起動し、今作成した文字列の所有権を
// それに渡す
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1000));
println!("{data}");
});
}
何かを 共有したい うえで、元のスレッドからも引き続きアクセスしたい場合、通常は その参照を渡すことはできません。次のようにすると:
use std::{thread::{sleep, spawn}, time::Duration};
fn main() {
// 現在のスレッドで文字列を作成する
let data = String::from("hello");
// 渡すための参照を作る
let data_ref = &data;
// 新しいスレッドを起動し、今作成した文字列の所有権を
// それに渡す
spawn(|| {
sleep(Duration::from_millis(1000));
println!("{data_ref}");
});
println!("{data_ref}");
}
次のようなエラーが出ます:
error[E0597]: `data` does not live long enough
--> src/main.rs:6:20
|
3 | let data = String::from("hello");
| ---- binding `data` declared here
...
6 | let data_ref = &data;
| ^^^^^ borrowed value does not live long enough
...
10 | / spawn(|| {
11 | | sleep(Duration::from_millis(1000));
12 | | println!("{data_ref}");
13 | | });
| |______- argument requires that `data` is borrowed for `'static`
...
16 | }
| - `data` dropped here while still borrowed
現在のスレッドと、これから作成する新しいスレッドの両方に対して、データが十分長く
生存することを確実にする 必要があります。これは、次のように Arc
(Atomically Reference Counted なヒープ割り当て)に入れることで実現できます:
use std::{sync::Arc, thread::{sleep, spawn}, time::Duration};
fn main() {
// 現在のスレッドで文字列を作成する
let data = Arc::new(String::from("hello"));
let handle = spawn({
// 新しいスレッドに渡すため、ハンドルのコピーを作る。
// `data` と `new_thread_data` はどちらも
// 同じ文字列を指している!
let new_thread_data = data.clone();
move || {
sleep(Duration::from_millis(1000));
println!("{new_thread_data}");
}
});
println!("{data}");
// スレッドが停止するのを待つ
let _ = handle.join();
}
これは素晴らしいことです! これで、メインスレッドでも好きなだけ長くデータに アクセスできるようになります。では、両方の場所でデータを 変更したい 場合は どうでしょうか?
このためには通常、何らかの「内部可変性」が必要になります。つまり、変更するために
&mut を必要としない型です。デスクトップでは、通常 Mutex のような型を使い、
それを lock() してデータへの可変アクセスを取得します。
これは例えば次のようになります:
use std::{sync::{Arc, Mutex}, thread::{sleep, spawn}, time::Duration};
fn main() {
// 現在のスレッドで文字列を作成する
let data = Arc::new(Mutex::new(String::from("hello")));
// 元のスレッドからロックする
{
let guard = data.lock().unwrap();
println!("{guard}");
// ガードはスコープの終わりでここでドロップされる!
}
let handle = spawn({
// 新しいスレッドに渡すため、ハンドルのコピーを作る。
// `data` と `new_thread_data` はどちらも
// 同じ `Mutex<String>` を指している!
let new_thread_data = data.clone();
move || {
sleep(Duration::from_millis(1000));
{
let mut guard = new_thread_data.lock().unwrap();
// データを変更できる!
guard.push_str(" | thread was here! |");
// ガードはスコープの終わりでここでドロップされる!
}
}
});
// スレッドが停止するのを待つ
let _ = handle.join();
{
let guard = data.lock().unwrap();
println!("{guard}");
// ガードはスコープの終わりでここでドロップされる!
}
}
このコードを実行すると、次のように表示されます:
hello
hello | thread was here! |
なぜ “std” Rust ではこのようなことをしなければならないのでしょうか? Rust は、 次の 2 つのことを考えるよう私たちを助けてくれています:
- データが十分長く生存すること(場合によっては「永遠に」!)
- 一度に可変アクセスできるコードは 1 つだけであること
Rust が、十分長く生存しないかもしれないデータ、たとえばあるスレッドから別の スレッドへ借用されたデータへのアクセスを許してしまうと、問題が起こるかもしれません。 元のスレッドが終了したり panic したりしたあとで、2 つ目のスレッドがすでに無効に なったデータにアクセスしようとすると、データが壊れてしまう可能性があります。Rust が、 同じデータを 2 つのコード片が同時に変更しようとすることを許してしまうと、データ競合が 起きたり、データが壊れたりする可能性があります。
組み込み Rust: ISR とのデータ共有
組み込み Rust でも、割り込みハンドラとデータを共有する際には、同じことを気に しなければなりません! スレッドと同様に、割り込みはいつでも発生し得ます。これは、 ある共有データにアクセスするためにスレッドが目を覚ますようなものです。つまり、 割り込みと共有するデータは十分長く生存しなければならず、また ISR が実行されて 同じく そのデータを扱おうとしたときに、メインコードが ISR と共有されたデータを ちょうど処理している途中ではないよう、注意しなければなりません!
実際、組み込み Rust では、Rust におけるスレッドのモデル化と似たやり方で割り込みを モデル化します。同じ理由で、同じルールが適用されます。ただし、組み込み Rust には いくつか重要な違いがあります:
-
割り込みはスレッドとまったく同じように動くわけではありません。あらかじめ設定して おき、何らかのイベントが起こるまで待機します(たとえばボタンが押されたり、タイマーが 期限切れになったりするときです)。その時点で実行されますが、渡されたコンテキストには アクセスできません。
-
割り込みは、そのイベントが発生するたびに、複数回トリガーされる可能性があります。
割り込みに関数引数としてコンテキストを渡せないので、そのデータを保存する別の場所を
見つける必要があります。「ベアメタル」の組み込み Rust では、ヒープ割り当てにアクセス
できません。したがって、Arc やそれに類するものは使えません。
値渡しすることもできず、データを保存するヒープもないとなると、ISR がアクセスできる
共有データを置く場所は 1 つしかありません。それが static なグローバル変数です。
組み込み Rust における ISR データ共有: 「標準的な方法」
Rust ではグローバル変数はかなり二級市民的な扱いで、ローカル変数と比べると 多くの制限があります。グローバルな状態変数は次のように宣言できます:
#![allow(unused)]
fn main() {
static COUNTER: usize = 0;
}
もちろん、これはあまり実用的ではありません。COUNTER を変更できるようにしたいはずです。次のようにも
書けます
#![allow(unused)]
fn main() {
static mut COUNTER: usize = 0;
}
ただし、これであらゆるアクセスが unsafe になります。
#![allow(unused)]
fn main() {
unsafe { COUNTER += 1 };
}
ここで unsafe になるのには理由があります。COUNTER の更新の途中で割り込み
ハンドラが実行され、そのハンドラも COUNTER を更新しようとする状況を想像して
ください。いつもの混乱が起こります。明らかに、何らかのロックが必要です。
critical-section クレートは一種の Mutex 型を提供しますが、その API と操作は少し
変わっています。この章の Cargo.toml を見ると、cortex-m クレートで
critical-section-single-core 機能が有効になっていることがわかるでしょう。この
機能は、このシステムにはプロセッサコアが 1 つしかなく、したがってクリティカル
セクションの間は単に割り込みを無効化するだけで同期を行える、という前提を置いて
います。割り込みの外であれば、これによってグローバルにアクセスできるのはメイン
プログラムだけになります。割り込みの中であれば、メインプログラムはグローバルに
アクセスできず(プログラム制御は割り込みハンドラ内にあります)、さらにそれより
高い優先度の別の割り込みハンドラも発火できないことが保証されます。
critical_section::Mutex は、相互排他は提供するものの、それ自体では可変性を
提供しないという点で少し変わっています。データを可変にするには、内部可変な型
— 通常は RefCell — をこの mutex で保護する必要があります。この Mutex は、
.lock() しないという点でも少し変わっています。代わりに、他のプログラム実行が
阻止されていることを証明する「critical section token」を受け取るクロージャで
クリティカルセクションを開始します。このトークンを Mutex の borrow()
メソッドに渡すことで、アクセスが可能になります。
これらをすべて組み合わせると、ISR とメインプログラムの間で状態を共有できるように
なります(examples/count-once.rs)。
#![no_main]
#![no_std]
use core::cell::RefCell;
use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section::Mutex;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use microbit::{
hal::{
gpiote,
pac::{self, interrupt},
},
Board,
};
static COUNTER: Mutex<RefCell<usize>> = Mutex::new(RefCell::new(0));
/// This "function" will be called when an interrupt is received. For now, just
/// report and panic.
#[interrupt]
fn GPIOTE() {
critical_section::with(|cs| {
let mut count = COUNTER.borrow(cs).borrow_mut();
*count += 1;
rprintln!("count: {}", count);
});
panic!();
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
let button_a = board.buttons.button_a.into_floating_input();
// Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
// wire goes low).
let gpiote = gpiote::Gpiote::new(board.GPIOTE);
let channel = gpiote.channel0();
channel
.input_pin(&button_a.degrade())
.hi_to_lo()
.enable_interrupt();
channel.reset_events();
// Set up the NVIC to handle GPIO interrupts.
unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
pac::NVIC::unpend(pac::Interrupt::GPIOTE);
loop {
// "wait for interrupt": CPU goes to sleep until an interrupt.
asm::wfi();
}
}
まだ ISR から安全に return することはできませんが、いまやそれに対処できる位置に
います。GPIOTE を ISR と共有して、ISR が割り込みをクリアできるようにするのです。
グローバルを使ってペリフェラル(など)を共有する
解決すべき問題がもう 1 つあります。Rust のグローバルは、プログラム開始前に静的に
初期化されていなければなりません。カウンタなら簡単で、0 に初期化するだけでした。
しかし GPIOTE ペリフェラルを共有したい場合は、そうはいきません。その
ペリフェラルは Board 構造体から取り出して、プログラム開始後にセットアップ
しなければなりません。これに対する const イニシャライザは存在しません
(そして、妥当な形で存在し得るものでもありません)。
ボタンカウンタを少し書き換えてみましょう。まず、実際のカウントは AtomicUsize に
移します。いずれにせよ、こちらのほうがこのグローバルにはより自然な型です。次に、
critical-section-lock-mut クレートの LockMut 型を使って、グローバル変数
GPIOTE_PERIPHERAL を追加します。このクレートは、前の節のパターンを扱いやすく
したラッパーです。
メインプログラムが GPIOTE ペリフェラルをセットアップし、それを割り込みハンドラ から使えるようにできるようになったので、もう panic するのはやめて、ボタンが 押されるたびにカウンタを増やせるようになります。カウント表示はメインループに 移して、カウントが割り込みハンドラとプログラムの残りの部分との間で共有されて いることを示しましょう。
この例(examples/count.rs)を実行して、MB2 A ボタンを押すたびにカウントが 1 ずつ
増えることを確認してください。
#![no_main]
#![no_std]
use core::sync::atomic::{AtomicUsize, Ordering::AcqRel};
use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use microbit::{
hal::{
gpiote,
pac::{self, interrupt},
},
Board,
};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static GPIOTE_PERIPHERAL: LockMut<gpiote::Gpiote> = LockMut::new();
#[interrupt]
fn GPIOTE() {
let count = COUNTER.fetch_add(1, AcqRel);
rprintln!("ouch {}", count + 1);
GPIOTE_PERIPHERAL.with_lock(|gpiote| {
gpiote.channel0().reset_events();
});
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
let button_a = board.buttons.button_a.into_floating_input();
// Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
// wire goes low).
let gpiote = gpiote::Gpiote::new(board.GPIOTE);
let channel = gpiote.channel0();
channel
.input_pin(&button_a.degrade())
.hi_to_lo()
.enable_interrupt();
channel.reset_events();
GPIOTE_PERIPHERAL.init(gpiote);
// Set up the NVIC to handle GPIO interrupts.
unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
pac::NVIC::unpend(pac::Interrupt::GPIOTE);
loop {
// "wait for interrupt": CPU goes to sleep until an interrupt.
asm::wfi();
}
}
注記 割り込み処理を含む例は、常に
--releaseでコンパイルするのが よい考えです。長い割り込みハンドラは多くの混乱を招く可能性があります。
とはいえ、割り込みハンドラ内の rprintln!() は実際には悪い作法です。割り込み
ハンドラが出力コードを実行している間は、ほかの何も先に進めません。報告はメイン
ループに移し、wfi()(「割り込み待ち」)のすぐ後で行うことにしましょう。そう
すれば、割り込みハンドラが終了するたびにカウントが表示されます
(examples/count-bounce.rs)。
#![no_main]
#![no_std]
use core::sync::atomic::{
AtomicUsize,
Ordering::{AcqRel, Acquire},
};
use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use microbit::{
hal::{
gpiote,
pac::{self, interrupt},
},
Board,
};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static GPIOTE_PERIPHERAL: LockMut<gpiote::Gpiote> = LockMut::new();
#[interrupt]
fn GPIOTE() {
let _ = COUNTER.fetch_add(1, AcqRel);
GPIOTE_PERIPHERAL.with_lock(|gpiote| {
gpiote.channel0().reset_events();
});
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
let button_a = board.buttons.button_a.into_floating_input();
// Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
// wire goes low).
let gpiote = gpiote::Gpiote::new(board.GPIOTE);
let channel = gpiote.channel0();
channel
.input_pin(&button_a.degrade())
.hi_to_lo()
.enable_interrupt();
channel.reset_events();
GPIOTE_PERIPHERAL.init(gpiote);
// Set up the NVIC to handle GPIO interrupts.
unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
pac::NVIC::unpend(pac::Interrupt::GPIOTE);
loop {
// "wait for interrupt": CPU goes to sleep until an interrupt.
asm::wfi();
let count = COUNTER.load(Acquire);
rprintln!("ouch {}", count);
}
}
この例では、MB2 A ボタンを押すたびにカウントが 1 増えます。たぶん。特に MB2 が 古い場合(!)は、1 回押しただけでカウンタが数回分増えることがあるかもしれません。 これはソフトウェアのバグではありません。 たいていは。次の節では、何が起きて いる可能性があるのか、そしてそれにどう対処すべきかについて説明します。