异常

异常和中断,是处理器用来处理异步事件和致命错误(e.g. 执行一个无效的指令)的一种硬件机制。异常意味着抢占并涉及到异常处理程序,即响应触发事件的信号的子程序。

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

1. // SysTick (System计时器)异常的异常处理函数
2. #[exception]
3. fn SysTick() {
4. // ..
5. }

除了 exception 属性,异常处理函数看起来和普通函数一样,但是有一个很大的不同: exception 处理函数 不能 被软件调用。在先前的例子中,语句 SysTick(); 将会导致一个编译错误。

这么做是故意的,因为异常处理函数必须具有一个特性: 在异常处理函数中被声明为static mut的变量能被安全(safe)地使用。

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

5. // `COUNT` 被转换到了 `&mut u32` 类型且它用起来是安全的
6. *COUNT += 1;
7. }

就像你可能已经知道的那样,在一个函数里使用static mut变量,会让函数变成非可重入函数(non-reentrancy)。从多个异常/中断处理函数,或者从main函数和多个异常/中断处理函数中,直接或者间接地调用一个非可重入(non-reentrancy)函数是未定义的行为。

安全的Rust不能导致未定义的行为出现,所以非可重入函数必须被标记为 unsafe。然而,我刚说了exception处理函数能安全地使用static mut变量。这怎么可能?因为exception处理函数 不 能被软件调用因此重入(reentrancy)不会发生,所以这才变得可能。

注意,exception属性,通过将静态变量封装进unsafe块中且为我们提供了名字相同的,类型为 &mut 的,新的合适的变量,转换了函数中静态变量的定义。因此我们可以通过 * 解引用访问变量的值而不需要将它们打包进一个 unsafe 块中。

一个复杂的例子

这里有个例子,使用系统计时器大概每秒会抛出一个 SysTick 异常。异常处理函数使用 COUNT 变量追踪它自己被调用了多少次,然后使用半主机模式(semihosting)打印 COUNT 的值到主机控制台上。

注意: 你能在任何Cortex-M设备上运行这个例子;你也能在QEMU运行它。

1. #![deny(unsafe_code)]
2. #![no_main]
3. #![no_std]

5. use panic_halt as _;

7. use core::fmt::Write;

9. use cortex_m::peripheral::syst::SystClkSource;
10. use cortex_m_rt::{entry, exception};
11. use cortex_m_semihosting::{
12. debug,
13. hio::{self, HStdout},
14. };

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

21. // 配置系统的计时器每秒去触发一个SysTick异常
22. syst.set_clock_source(SystClkSource::Core);
23. // 这是关于LM3S6965的配置,其有一个12MHz的默认CPU时钟
24. syst.set_reload(12_000_000);
25. syst.clear_current();
26. syst.enable_counter();
27. syst.enable_interrupt();

29. loop {}
30. }

32. #[exception]
33. fn SysTick() {
34. static mut COUNT: u32 = 0;
35. static mut STDOUT: Option<HStdout> = None;

37. *COUNT += 1;

39. // 惰性初始化(Lazy initialization)
40. if STDOUT.is_none() {
41. *STDOUT = hio::hstdout().ok();
42. }

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

48. // 重要信息 如果运行在真正的硬件上,去掉这个 `if` 块,
49. // 否则你的调试器将会以一种不一样的状态结束
50. if *COUNT == 9 {
51. // 这将终结QEMU进程
52. debug::exit(debug::EXIT_SUCCESS);
53. }
54. }
1. tail -n5 Cargo.toml
1. [dependencies]
2. cortex-m = "0.5.7"
3. cortex-m-rt = "0.6.3"
4. panic-halt = "0.2.0"
5. cortex-m-semihosting = "0.3.1"
1. $ cargo run --release
2. Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
3. 123456789

如果你在Discovery开发板上运行这个例子,你将会在OpenOCD控制台上看到输出。还有,当计数到达9的时候,程序将 会 停止。

默认异常处理函数

exception 属性真正做的是,覆盖 了一个特定异常的默认异常处理函数。如果你不覆盖一个特定异常的处理函数,它将会被 DefaultHandler 函数处理,其默认的是:

1. fn DefaultHandler() {
2. loop {}
3. }

这个函数是 cortex-m-rt crate提供的,且被标记为 #[no_mangle] 因此你能在 “DefaultHandler” 上放置一个断点并捕获 unhandled 异常。

可以使用 exception 属性覆盖这个 DefaultHandler:

1. #[exception]
2. fn DefaultHandler(irqn: i16) {
3. // 自定义默认处理函数
4. }

irqn 参数指出了被服务的是哪个异常。一个负数值指出了被服务的是一个Cortex-M异常;0或者一个正数值指出了被服务的是一个设备特定的异常,也就是中断。

硬错误(Hard Fault)处理函数

HardFault异常有点特别。当程序进入一个无法工作的状态时,这个异常被触发,因此它的处理函数 不能 返回,因为这么做可能导致一个未定义的行为。在用户定义的 HardFault 处理函数被调用之前,运行时crate还做了一些工作去提供可调试性。

结果是,HardFault处理函数必须有下列的签名: fn(&ExceptionFrame) -> ! 。处理函数的参数是一个指针,它指向被异常推入栈中的寄存器。这些寄存器是异常被触发那刻,处理器状态的一个记录,能被用来分析一个硬错误。

这里有个执行不合法操作的案例: 读取一个不存在的存储位置。

注意: 这个程序在QEMU上将不会工作,i.e. 它将不会崩溃,因为 qemu-system-arm -machine lm3s6965evb 不对读取存储的操作进行检查,且读取无效存储时将会开心地返回 0。

1. #![no_main]
2. #![no_std]

4. use panic_halt as _;

6. use core::fmt::Write;
7. use core::ptr;

9. use cortex_m_rt::{entry, exception, ExceptionFrame};
10. use cortex_m_semihosting::hio;

12. #[entry]
13. fn main() -> ! {
14. // 读取一个无效的存储位置
15. unsafe {
16. ptr::read_volatile(0x3FFF_FFFE as *const u32);
17. }

19. loop {}
20. }

22. #[exception]
23. fn HardFault(ef: &ExceptionFrame) -> ! {
24. if let Ok(mut hstdout) = hio::hstdout() {
25. writeln!(hstdout, "{:#?}", ef).ok();
26. }

28. loop {}
29. }

HardFault处理函数打印了ExceptionFrame值。如果你运行这个,你将会看到下面的东西打印到OpenOCD控制台上。

1. $ openocd
2. (..)
3. ExceptionFrame {
4. r0: 0x3ffffffe,
5. r1: 0x00f00000,
6. r2: 0x20000000,
7. r3: 0x00000000,
8. r12: 0x00000000,
9. lr: 0x080008f7,
10. pc: 0x0800094a,
11. xpsr: 0x61000000
12. }

pc值是异常时程序计数器(Program Counter)的值,它指向触发了异常的指令。

如果你看向程序的反汇编:

1. $ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
2. (..)
3. ResetTrampoline:
4. 8000942:       movw    r0, #0xfffe
5. 8000946:       movt    r0, #0x3fff
6. 800094a:       ldr     r0, [r0]
7. 800094c:       b       #-0x4 <ResetTrampoline+0xa>

你可以在反汇编中搜索程序计数器0x0800094a的值。你将会看到一个读取操作(ldr r0, [r0])导致了异常。ExceptionFrame的r0字段将告诉你,那时寄存器r0的值是0x3fff_fffe 。