例外
例外と割り込みは、プロセッサが非同期イベントや致命的なエラー (たとえば無効な命令の実行)を処理するためのハードウェア機構です。例外は プリエンプションを伴い、イベントを引き起こしたシグナルに応答して実行される サブルーチンである例外ハンドラが関与します。
cortex-m-rt クレートは、例外ハンドラを宣言するための exception 属性を
提供します。
// SysTick(システムタイマー)例外の例外ハンドラ
#[exception]
fn SysTick() {
// ..
}
exception 属性を除けば、例外ハンドラは普通の関数のように見えますが、
もう 1 つ違いがあります。exception ハンドラはソフトウェアから
呼び出せません。先ほどの例でいえば、SysTick(); という文は
コンパイルエラーになります。
この挙動は意図されたものであり、次の機能を提供するために必要です:
exception ハンドラの 内部 で宣言された static mut 変数は、使用しても
安全 です。
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` は型 `&mut u32` に変換されており、安全に使用できます
*COUNT += 1;
}
ご存じのとおり、関数内で static mut 変数を使うと、その関数は
非リエントラント になります。非リエントラントな関数を、
複数の例外 / 割り込みハンドラから直接または間接に、あるいは main と
1 つ以上の例外 / 割り込みハンドラから呼び出すことは未定義動作です。
Safe Rust は決して未定義動作を引き起こしてはならないため、非リエントラントな
関数には unsafe を付けなければなりません。ところが、先ほど私は
exception ハンドラは static mut 変数を安全に使えると述べました。
なぜこれが可能なのでしょうか。これは、exception ハンドラは
ソフトウェアから 呼び出せない ため、リエントランシーが起こりえないからです。
これらのハンドラはハードウェア自体によって呼び出され、ハードウェアは物理的に
同時実行されないものとみなされます。
その結果、組み込みシステムの例外ハンドラという文脈では、同じハンドラが同時に呼び出されないため、ハンドラが静的な可変変数を使用していてもリエントランシーの問題は生じません。
マルチコアシステムでは、複数のプロセッサコアが同時にコードを実行するため、
例外ハンドラ内であってもリエントランシーの問題が再び重要になります。各コアが独自の例外ハンドラ群を持つ場合でも、複数のコアが同じ例外ハンドラを同時に実行しようとする状況は起こりえます。
マルチコア環境でこの懸念に対処するには、共有リソースへのアクセスがコア間で
適切に調整されるよう、例外ハンドラ内で適切な同期機構を用いる必要があります。
これには通常、データ競合を防ぎ、データ整合性を維持するためのロック、
セマフォ、アトミック操作などの手法が含まれます
exception属性は、関数内の静的変数の定義をunsafeブロックで 包み、同じ名前の&mut型の適切な新しい変数を私たちに提供することで 変換することに注意してください。 そのため、参照を*でデリファレンスすることで、変数の値にアクセスする際に それらをunsafeブロックで囲む必要はありません。
完全な例
これは、システムタイマーを使って約 1 秒ごとに SysTick 例外を発生させる
例です。SysTick 例外ハンドラは、呼び出された回数を COUNT 変数で
追跡し、その後 COUNT の値をセミホスティングを使ってホストコンソールに
出力します。
注記: この例はどの Cortex-M デバイス上でも実行できます。また、 QEMU 上でも実行できます
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
debug,
hio::{self, HostStream},
};
#[entry]
fn main() -> ! {
let p = cortex_m::Peripherals::take().unwrap();
let mut syst = p.SYST;
// システムタイマーを設定し、毎秒 SysTick 例外を発生させる
syst.set_clock_source(SystClkSource::Core);
// これは、デフォルトの CPU クロックが 12 MHz の LM3S6965 向けの設定です
syst.set_reload(12_000_000);
syst.clear_current();
syst.enable_counter();
syst.enable_interrupt();
loop {}
}
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
static mut STDOUT: Option<HostStream> = None;
*COUNT += 1;
// 遅延初期化
if STDOUT.is_none() {
*STDOUT = hio::hstdout().ok();
}
if let Some(hstdout) = STDOUT.as_mut() {
write!(hstdout, "{}", *COUNT).ok();
}
// 重要: 実機で実行する場合は、この `if` ブロックを省略してください
// そうしないとデバッガーが不整合な状態になります
if *COUNT == 9 {
// これにより QEMU プロセスは終了します
debug::exit(debug::EXIT_SUCCESS);
}
}
tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-rt = "0.6.3"
panic-halt = "0.2.0"
cortex-m-semihosting = "0.3.1"
$ cargo run --release
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
123456789
これを Discovery ボードで実行すると、OpenOCD コンソールに出力が表示されます。 また、カウントが 9 に達してもプログラムは 停止しません。
デフォルトの例外ハンドラ
exception 属性が実際に行うのは、特定の例外に対するデフォルトの例外
ハンドラを オーバーライドする ことです。特定の例外のハンドラを
オーバーライドしない場合、その例外は DefaultHandler 関数によって処理され、
そのデフォルト実装は次のとおりです:
fn DefaultHandler() {
loop {}
}
この関数は cortex-m-rt クレートによって提供されており、
#[no_mangle] が付いているため、“DefaultHandler” にブレークポイントを置いて
未処理の 例外を捕捉できます。
exception 属性を使って、この DefaultHandler をオーバーライドすることも
できます:
#[exception]
fn DefaultHandler(irqn: i16) {
// カスタムデフォルトハンドラ
}
irqn 引数は、現在どの例外が処理されているかを示します。負の値は
Cortex-M の例外が処理されていることを示し、0 または正の値は
デバイス固有の例外、すなわち割り込みが処理されていることを示します。
ハードフォルトハンドラ
HardFault 例外は少し特別です。この例外はプログラムが無効な状態に
入ったときに発生するため、そのハンドラは 復帰できません。復帰すると
未定義動作になる可能性があるからです。また、ランタイムクレートは、
ユーザー定義の HardFault ハンドラが呼び出される前に、デバッグしやすさを
向上させるための少しの処理を行います。
その結果、HardFault ハンドラは次のシグネチャでなければなりません:
fn(&ExceptionFrame) -> !。ハンドラの引数は、例外によってスタックに
プッシュされたレジスタへのポインタです。これらのレジスタは、例外が発生した
瞬間のプロセッサ状態のスナップショットであり、ハードフォルトの診断に
役立ちます。
これは、不正な操作、つまり存在しないメモリアドレスの読み取りを行う 例です。
注意: このプログラムは QEMU では動作しません。つまりクラッシュしません。これは、
qemu-system-arm -machine lm3s6965evbがメモリロードをチェックせず、 無効なメモリの読み取りに対して平然と0を返すためです。
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use core::ptr;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;
#[entry]
fn main() -> ! {
// 存在しないメモリアドレスを読み取る
unsafe {
ptr::read_volatile(0x3FFF_0000 as *const u32);
}
loop {}
}
#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
if let Ok(mut hstdout) = hio::hstdout() {
writeln!(hstdout, "{:#?}", ef).ok();
}
loop {}
}
HardFault ハンドラは ExceptionFrame の値を出力します。これを実行すると、
OpenOCD コンソールに次のようなものが表示されます。
$ openocd
(..)
ExceptionFrame {
r0: 0x3fff0000,
r1: 0x00000003,
r2: 0x080032e8,
r3: 0x00000000,
r12: 0x00000000,
lr: 0x080016df,
pc: 0x080016e2,
xpsr: 0x61000000,
}
pc の値は、例外発生時のプログラムカウンタの値であり、
例外を引き起こした命令を指しています。
プログラムの逆アセンブリを見ると、
$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
(..)
ResetTrampoline:
8000942: movw r0, #0xfffe
8000946: movt r0, #0x3fff
800094a: ldr r0, [r0]
800094c: b #-0x4 <ResetTrampoline+0xa>
逆アセンブリの中でプログラムカウンタの値 0x0800094a を調べることができます。
そうすると、ロード命令 (ldr r0, [r0] ) が例外を引き起こしたことがわかります。
ExceptionFrame の r0 フィールドを見ると、その時点でレジスタ r0
の値が 0x3fff_fffe だったことがわかります。