第4章:中断系统与异常处理
中断系统,说白了就是CPU的“紧急通道”。
我刚开始做嵌入式那会儿,总觉得中断就是个回调函数。后来在Cortex-M上栽了个跟头,才明白这里面门道有多深。今天咱们就把它彻底讲透。
4.1 Cortex-M中断向量表
每个Cortex-M芯片都有一张“中断向量表”。它放在Flash的最开头,也就是地址0x00000000处。这张表里存的是啥?是各种异常和中断的入口地址。
我习惯把这张表想象成一个电话本。CPU遇到异常时,就像拨号一样,直接查表找到对应的处理函数。
向量表布局(Cortex-M3/M4/M7):
| 偏移地址 | 异常类型 | 优先级 | 说明 |
|---|---|---|---|
| 0x00 | 栈顶地址 | - | 初始MSP值 |
| 0x04 | Reset | -3(最高) | 系统复位入口 |
| 0x08 | NMI | -2 | 不可屏蔽中断 |
| 0x0C | HardFault | -1 | 硬错误 |
| 0x10 | MemManage | 可编程 | 内存管理错误 |
| 0x14 | BusFault | 可编程 | 总线错误 |
| 0x18 | UsageFault | 可编程 | 用法错误 |
| ... | ... | ... | ... |
| 0x40+ | 外部中断 | 可编程 | 外设中断(如GPIO、定时器) |
嗯,这里要注意:前16个是系统异常,后面的才是外设中断。不同芯片的外设中断数量不一样,具体得看数据手册。
4.2 NVIC配置
NVIC(嵌套向量中断控制器)是Cortex-M的中断管家。它负责中断的使能、挂起、优先级管理。
我见过不少新手直接操作NVIC寄存器,结果把中断优先级搞乱了。其实Rust的cortex-m-rt crate已经封装好了,咱们直接用就行。
// 使用cortex-m-rt配置NVIC
use cortex_m::peripheral::NVIC;
use stm32f4xx_hal::pac::Interrupt;
fn configure_interrupt() {
let mut nvic = unsafe { NVIC::new() };
// 使能USART1中断
nvic.enable(Interrupt::USART1);
// 设置优先级(0-15,数值越小优先级越高)
nvic.set_priority(Interrupt::USART1, 5);
// 清除挂起位(防止残留中断)
nvic.clear_pending(Interrupt::USART1);
}
我个人习惯在初始化阶段就把所有中断配置好。别等到运行时再改优先级,容易出问题。
小技巧:NVIC的优先级分组也很重要。Cortex-M支持抢占优先级和子优先级。我一般用4位抢占优先级,0位子优先级,这样逻辑最清晰。
4.3 中断优先级
中断优先级是个容易踩坑的地方。Cortex-M用4位来表示优先级,但具体用几位,取决于芯片厂商的实现。
我曾经在一个项目里,把两个中断都设成了优先级0。结果高频率的中断把低频率的饿死了。你想想看,这多坑。
避坑指南:我曾经在STM32F103上遇到过优先级反转的问题。一个低优先级中断占用了共享资源,高优先级中断一直等不到,系统直接卡死。解决办法是:给共享资源加互斥锁,或者用临界区保护。
优先级规则其实很简单:
- 数值越小,优先级越高
- 抢占优先级高的可以打断低的
- 相同优先级的中断,按向量表顺序执行
- 子优先级只在同抢占优先级时生效
4.4 Rust中断处理函数编写
在Rust里写中断处理函数,比C语言优雅多了。咱们用#[interrupt]属性宏,编译器会自动生成正确的向量表条目。
use cortex_m_rt::entry;
use stm32f4xx_hal::pac::interrupt;
// 全局变量,用于中断中传递数据
static mut COUNTER: u32 = 0;
// 定义中断处理函数
#[interrupt]
fn TIM2() {
// 注意:中断函数必须是unsafe的
// 因为可能访问全局可变变量
unsafe {
COUNTER += 1;
// 清除中断标志位
// 具体操作取决于外设
}
}
#[entry]
fn main() -> ! {
// 配置定时器...
// 使能中断...
loop {
// 主循环
}
}
这里有个关键点:中断函数里不能使用println!这类阻塞操作。我见过有人这么干,结果中断响应时间直接爆炸。
中断函数编写原则:
- 尽量短小精悍,别在里面做复杂计算
- 用原子操作或临界区保护共享数据
- 别调用可能阻塞的函数
- 及时清除中断标志位
4.5 实战:按键中断示例
咱们写个完整的按键中断例子。用GPIO的上升沿触发中断,每按一次按键,LED状态翻转。
use cortex_m_rt::entry;
use stm32f4xx_hal::{
pac,
prelude::*,
gpio::Edge,
interrupt,
};
static mut LED_STATE: bool = false;
#[interrupt]
fn EXTI0() {
unsafe {
LED_STATE = !LED_STATE;
// 清除挂起位
// 具体寄存器操作...
}
}
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
// 配置PA0为输入,上升沿触发
let button = gpioa.pa0.into_pull_up_input();
button.make_interrupt_source();
button.trigger_on_edge(Edge::RISING);
button.enable_interrupt();
// 配置LED引脚...
loop {
// 主循环可以干别的事
}
}
嗯,这个例子虽然简单,但涵盖了中断配置的核心流程。我在实际项目中,经常用这种模式来处理按键、传感器触发等场景。
4.6 异常处理
除了中断,Cortex-M还有异常处理机制。HardFault是最常见的异常,通常是因为访问了非法地址或者执行了未定义指令。
我调试过最头疼的一个HardFault,是因为栈溢出。Rust的栈空间默认只有几KB,递归调用一深就炸了。
调试技巧:在HardFault处理函数里,可以读取堆栈帧来定位问题。用cortex-m-rt的HardFault异常处理,配合GDB调试,能快速找到问题根源。
// 自定义HardFault处理
#[exception]
fn HardFault(ef: &cortex_m_rt::ExceptionFrame) -> ! {
// 打印异常帧信息
// 可以在这里读取PC、LR等寄存器
loop {
// 死循环,等待调试器介入
}
}
说白了,异常处理就是系统的最后一道防线。我建议每个项目都实现自定义的HardFault处理,至少能打印出错误信息,方便调试。
4.7 知识体系总览
下面这张图,是我梳理的中断系统核心逻辑。你看一遍,基本就全明白了。
这张图把中断的完整流程串起来了。从外设触发,到NVIC仲裁,再到CPU执行处理函数,每一步都很清晰。
本章核心要点:
- 中断向量表是中断系统的“电话本”,放在Flash开头
- NVIC负责中断的使能、挂起和优先级管理
- 优先级数值越小,优先级越高,别搞反了
- Rust的
#[interrupt]属性宏让中断处理更安全 - 异常处理是最后防线,一定要实现自定义HardFault
中断系统这块,说白了就是“配置好、别乱动”。我见过太多人把中断优先级改来改去,最后系统跑飞了。记住:初始化阶段一次性配好,运行时别动优先级。
好了,这一章的内容就到这儿。中断系统是嵌入式开发的基石,搞懂了它,后面学RTOS、驱动开发都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321