异常
异常和中断是一种硬件机制,处理器通过它来处理异步事件和致命错误(例如,执行无效指令)。异常意味着抢占,并涉及异常处理程序,即为响应触发事件的信号而执行的子例程。
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
异常有点特殊。当程序进入无效状态时会触发此异常,因此其处理程序不能返回,因为这可能会导致未定义的行为。此外,运行时 crate 在调用用户定义的 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_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
。