第4章:中断系统与异常处理

中断系统,说白了就是CPU的“紧急通道”。

我刚开始做嵌入式那会儿,总觉得中断就是个回调函数。后来在Cortex-M上栽了个跟头,才明白这里面门道有多深。今天咱们就把它彻底讲透。

4.1 Cortex-M中断向量表

每个Cortex-M芯片都有一张“中断向量表”。它放在Flash的最开头,也就是地址0x00000000处。这张表里存的是啥?是各种异常和中断的入口地址。

我习惯把这张表想象成一个电话本。CPU遇到异常时,就像拨号一样,直接查表找到对应的处理函数。

向量表布局(Cortex-M3/M4/M7):

偏移地址异常类型优先级说明
0x00栈顶地址-初始MSP值
0x04Reset-3(最高)系统复位入口
0x08NMI-2不可屏蔽中断
0x0CHardFault-1硬错误
0x10MemManage可编程内存管理错误
0x14BusFault可编程总线错误
0x18UsageFault可编程用法错误
............
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!这类阻塞操作。我见过有人这么干,结果中断响应时间直接爆炸。

中断函数编写原则:

  1. 尽量短小精悍,别在里面做复杂计算
  2. 用原子操作或临界区保护共享数据
  3. 别调用可能阻塞的函数
  4. 及时清除中断标志位

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-rtHardFault异常处理,配合GDB调试,能快速找到问题根源。

// 自定义HardFault处理
#[exception]
fn HardFault(ef: &cortex_m_rt::ExceptionFrame) -> ! {
    // 打印异常帧信息
    // 可以在这里读取PC、LR等寄存器
    loop {
        // 死循环,等待调试器介入
    }
}

说白了,异常处理就是系统的最后一道防线。我建议每个项目都实现自定义的HardFault处理,至少能打印出错误信息,方便调试。

4.7 知识体系总览

下面这张图,是我梳理的中断系统核心逻辑。你看一遍,基本就全明白了。

Cortex-M中断系统核心逻辑 外设中断源 GPIO/定时器/UART等 NVIC控制器 使能/挂起/优先级 抢占/子优先级分组 中断向量表 存储函数入口地址 地址0x00000000 CPU核心 自动保存上下文 中断处理函数 #[interrupt]属性宏 系统异常 HardFault/NMI等 中断触发流程:外设 → NVIC仲裁 → 查向量表 → CPU执行 → 处理函数 虚线表示异常处理路径,实线表示正常中断路径

这张图把中断的完整流程串起来了。从外设触发,到NVIC仲裁,再到CPU执行处理函数,每一步都很清晰。

本章核心要点:

  • 中断向量表是中断系统的“电话本”,放在Flash开头
  • NVIC负责中断的使能、挂起和优先级管理
  • 优先级数值越小,优先级越高,别搞反了
  • Rust的#[interrupt]属性宏让中断处理更安全
  • 异常处理是最后防线,一定要实现自定义HardFault

中断系统这块,说白了就是“配置好、别乱动”。我见过太多人把中断优先级改来改去,最后系统跑飞了。记住:初始化阶段一次性配好,运行时别动优先级。

好了,这一章的内容就到这儿。中断系统是嵌入式开发的基石,搞懂了它,后面学RTOS、驱动开发都会轻松很多。


公众号:蓝海资料掘金营,微信deep3321