异常

异常和中断是处理器处理异步事件和致命错误(例如执行无效指令)的硬件机制。异常意味着抢占,并涉及异常处理程序,这些处理程序是响应触发事件的信号而执行的子例程。

cortex-m-rt crate 提供了一个 exception 属性来声明异常处理程序。

// Exception handler for the SysTick (System Timer) exception
#[exception]
fn SysTick() {
    // ..
}

除了 exception 属性外,异常处理程序看起来像普通的函数,但还有一个区别:exception 处理程序不能由软件调用。 按照前面的示例,语句 SysTick(); 将导致编译错误。

这种行为基本上是预期的,它需要提供一个功能:在 exception 处理程序中声明的 static mut 变量是安全的。

#[exception]
fn SysTick() {
    static mut COUNT: u32 = 0;

    // `COUNT` has transformed to type `&mut u32` and it's safe to use
    *COUNT += 1;
}

如您所知,在函数中使用 static mut 变量会使其 不可重入。从多个异常/中断处理程序或从 main 和一个或多个异常/中断处理程序直接或间接调用不可重入函数是未定义的行为。

安全的 Rust 绝不能导致未定义的行为,因此不可重入函数必须标记为 unsafe。但我刚刚说过 exception 处理程序可以安全地使用 static mut 变量。这是如何做到的?这是可能的,因为 exception 处理程序不能由软件调用,因此不可重入是不可能的。

请注意,exception 属性通过将函数内部的静态变量定义包装到 unsafe 块中,并为我们提供相同名称的 &mut 类型的新适当变量来转换这些定义。因此,我们可以通过 * 解引用引用来访问变量的值,而无需将它们包装在 unsafe 块中。

一个完整的示例

这是一个使用系统定时器大约每秒引发一次 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, HStdout},
};

#[entry]
fn main() -> ! {
    let p = cortex_m::Peripherals::take().unwrap();
    let mut syst = p.SYST;

    // configures the system timer to trigger a SysTick exception every second
    syst.set_clock_source(SystClkSource::Core);
    // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
    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<HStdout> = None;

    *COUNT += 1;

    // Lazy initialization
    if STDOUT.is_none() {
        *STDOUT = hio::hstdout().ok();
    }

    if let Some(hstdout) = STDOUT.as_mut() {
        write!(hstdout, "{}", *COUNT).ok();
    }

    // IMPORTANT omit this `if` block if running on real hardware or your
    // debugger will end in an inconsistent state
    if *COUNT == 9 {
        // This will terminate the QEMU process
        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 crate 提供,并标记为 #[no_mangle],因此您可以在“DefaultHandler”上设置断点并捕获未处理的异常。

可以使用 exception 属性覆盖此 DefaultHandler

#[exception]
fn DefaultHandler(irqn: i16) {
    // custom default handler
}

irqn 参数指示正在处理哪个异常。负值表示正在处理 Cortex-M 异常;零或正值表示正在处理特定于设备的异常,即中断。

硬故障处理程序

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() -> ! {
    // read a nonexistent memory location
    unsafe {
        ptr::read_volatile(0x3FFF_FFFE 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: 0x3ffffffe,
    r1: 0x00f00000,
    r2: 0x20000000,
    r3: 0x00000000,
    r12: 0x00000000,
    lr: 0x080008f7,
    pc: 0x0800094a,
    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]) 导致了异常。ExceptionFramer0 字段将告诉您寄存器 r0 的值当时是 0x3fff_fffe