异常
异常和中断,是处理器用来处理异步事件和致命错误(e.g. 执行一个无效的指令)的一种硬件机制。异常意味着抢占并涉及到异常处理程序,即响应触发事件的信号的子程序。
cortex-m-rt crate提供了一个 exception 属性去声明异常处理程序。
// SysTick (System计时器)异常的异常处理函数
#[exception]
fn SysTick() {
// ..
}
除了 exception 属性,异常处理函数看起来和普通函数一样,但是有一个很大的不同: exception 处理函数 不能 被软件调用。在先前的例子中,语句 SysTick(); 将会导致一个编译错误。
这么做是有目的的,因为异常处理函数必须具有一个特性: 在异常处理函数中被声明为static mut的变量能被安全(safe)地使用。
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` 被转换到了 `&mut u32` 类型且它用起来是安全的
*COUNT += 1;
就像你可能已经知道的那样,在一个函数里使用static mut变量,会让函数变成非可重入函数(non-reentrancy)。从多个异常/中断处理函数,或者从main函数和多个异常/中断处理函数中,直接或者间接地调用一个非可重入(non-reentrancy)函数是未定义的行为。
翻译已过时 安全的Rust不能导致未定义的行为出现,所以非可重入函数必须被标记为 unsafe。然而,我刚说了exception处理函数能安全地使用static mut变量。这怎么可能?因为exception处理函数 不 能被软件调用因此重入(reentrancy)不会发生,所以这才变得可能。
As a result, in the context of exception handlers in embedded systems, the absence of concurrent invocations of the same handler ensures that there are no reentrancy issues, even if the handler uses static mutable variables.
In a multicore system, where multiple processor cores are executing code concurrently, the potential for reentrancy issues becomes relevant again, even within exception handlers. While each core may have its own set of exception handlers, there can still be scenarios where multiple cores attempt to execute the same exception handler simultaneously.
To address this concern in a multicore environment, proper synchronization mechanisms need to be employed within the exception handlers to ensure that access to shared resources is properly coordinated among the cores. This typically involves the use of techniques such as locks, semaphores, or atomic operations to prevent data races and maintain data integrity
注意,exception属性,通过将静态变量封装进unsafe块中并为我们提供了名字相同的,类型为&mut的,合适的新变量,转换了函数中静态变量的定义。因此我们可以通过*解引用访问变量的值而不需要将它们打包进一个unsafe块中。
一个完整的例子
这里有个例子,使用系统计时器大概每秒抛出一个 SysTick 异常。异常处理函数使用 COUNT 变量追踪它自己被调用了多少次,然后使用半主机模式(semihosting)打印 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);
// 这是关于LM3S6965的配置,其有一个12MHz的默认CPU时钟
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;
// 惰性初始化(Lazy initialization)
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 crate提供的,且被标记为 #[no_mangle] 因此你能在 “DefaultHandler” 上放置一个断点并捕获 unhandled 异常。
可以使用 exception 属性覆盖这个 DefaultHandler:
#[exception]
fn DefaultHandler(irqn: i16) {
// 自定义默认处理函数
}
irqn 参数指出了被服务的是哪个异常。一个负数值指出了被服务的是一个Cortex-M异常;0或者一个正数值指出了被服务的是一个设备特定的异常,也就是中断。
硬错误(Hard Fault)处理函数
HardFault异常有点特别。当程序进入一个无法工作的状态时,这个异常被触发,因此它的处理函数 不能 返回,因为这么做可能导致一个未定义的行为。在用户定义的 HardFault 处理函数被调用之前,运行时crate还做了一些工作以改进调试功能。
结果是,HardFault处理函数必须有下列的签名: fn(&ExceptionFrame) -> ! 。处理函数的参数是一个指针,它指向被异常推入栈中的寄存器。这些寄存器是异常被触发那刻,处理器状态的一个记录,能被用来分析一个硬错误。
这里有个执行不合法操作的案例: 读取一个不存在的存储位置。
注意: 这个程序在QEMU上不能起作用,i.e. 它不会崩溃,因为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值是异常时程序计数器(Program Counter)的值,它指向触发了异常的指令。
如果你看向程序的反汇编:
$ 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 。