异常

异常与中断是一种硬件机制, 处理器通过该机制来来异步处理事件或错误(例如执行无效指令). 异常意味着抢占, 涉及异常处理程序, 这些子处理程序使为了响应触发事件的信号而执行的子线程.

cortex-m-rt库提供了一个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变量追踪一共触发了多少次, 并且用semihostingCOUNT的值输出到主机上.

NOTE 你可以在任何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提供的, 并且被标记为#[no_mangle], 所以你可以在"DefaultHandler"中打一个断点来捕获未处理的异常.

也使用exception属性来覆盖DefaultHandler

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

irqn参数是正在处理的异常. 负数表示正在处理的是Cortex-M异常. 0或正数表示正在处理设备特定的异常, 又被叫做中断.

硬件故障处理

HardFault有一点特殊. 当程序进入无效状态的时候, 会引发此异常, 所以这个函数不会return, 因为这可能会导致未定义行为. 另外, 在调用用户定义的HardFault函数时, 运行时会会做一些方便debug的工作.

结果是HardFault函数必须要像这样: fn(&ExceptionFrame) -> !. 函数的参数是一个指向由异常入栈的寄存器的指针. 这些寄存器是出现异常时处理器状态的快照, 可以用来诊断故障.

这有一个产生非法操作的例子: 读取不存在的内存地址.

NOTE 该程序在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