4、中断管理:中断优先级配置、中断嵌套与抢占、临界区保护

中断管理,说白了就是实时系统的命脉。你想想看,姿态控制这种活儿,传感器一秒钟给你来几千个数据,你要是用轮询去读,CPU啥也别干了。我做了这么多年嵌入式,见过太多因为中断没处理好导致炸机的案例。嗯,今天咱们就把中断优先级、嵌套抢占、临界区保护这三个硬骨头啃下来。

4.1 中断优先级配置——别让优先级变成玄学

中断优先级配置,是实时系统里最容易出问题的地方。我个人习惯是:把时间敏感的中断放最高优先级,把数据处理类的中断放低优先级。举个例子,姿态控制里,IMU数据中断通常比通信中断优先级高。为什么?因为IMU数据丢了,控制周期就断了;通信丢一包,大不了重传。

核心原则:中断优先级 = 时间敏感度 + 后果严重度

我在项目中遇到过一件事:有个同事把CAN通信中断优先级设得比IMU中断还高。结果呢?电机一堵转,CAN疯狂报错,IMU数据被挤得根本进不去,飞控直接炸了。后来我帮他改成IMU中断优先级最高,CAN中断降两级,问题就解决了。

配置优先级时,要注意芯片的具体实现。以STM32为例,NVIC支持4位优先级分组:

// 设置优先级分组:4位抢占优先级,0位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

// 配置IMU中断:抢占优先级0(最高)
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 配置CAN中断:抢占优先级2
NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);

避坑指南:我曾经在FreeRTOS里犯过一个低级错误——把中断优先级设得比FreeRTOS管理的最高优先级还高。结果呢?调度器根本管不了这个中断,临界区保护也失效了。记住:FreeRTOS管理的最高优先级是configMAX_SYSCALL_INTERRUPT_PRIORITY,高于这个的中断,RTOS管不着。

4.2 中断嵌套与抢占——谁抢谁,怎么抢?

中断嵌套,说白了就是高优先级中断打断低优先级中断。这玩意儿用好了是神器,用不好是灾难。我见过有人把所有中断优先级设成一样,结果中断来了排队处理,实时性一塌糊涂。

中断嵌套的规则其实很简单:

  • 高优先级中断可以打断低优先级中断——这是抢占的核心
  • 同优先级中断不能互相打断——按顺序执行
  • 中断嵌套层数有限——取决于堆栈大小,一般建议不超过3层

为什么会限制嵌套层数?你想想看,每嵌套一层,就要压栈一堆寄存器。如果嵌套太深,堆栈溢出,程序就飞了。我在项目中遇到过最夸张的一次,一个同事嵌套了5层中断,结果堆栈直接爆了,系统死机。后来我帮他改成用事件标志+任务处理,嵌套层数降到2层,问题就解决了。

中断嵌套的典型场景:

// 低优先级中断:处理传感器数据
void EXTI0_IRQHandler(void) {
    // 正在处理IMU数据...
    uint32_t data = spi_read(IMU_ADDR);
    
    // 此时高优先级中断来了!系统自动保存现场
    // 跳转到TIM1_IRQHandler执行
    // 执行完后返回这里继续
    
    sensor_buffer[head] = data;
    head = (head + 1) % BUFFER_SIZE;
}

// 高优先级中断:电机急停
void TIM1_IRQHandler(void) {
    // 电机过流!立即停止
    motor_stop();
    TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
}

警告:中断嵌套时,不要在中断里调用任何可能阻塞的函数。比如printf、malloc、信号量获取(带阻塞时间的)。我曾经在中断里调了printf,结果printf内部用了互斥锁,直接死锁了。嗯,那次调试花了我整整两天。

4.3 临界区保护——taskENTER_CRITICAL的正确用法

临界区保护,说白了就是防止中断和任务同时访问共享资源。FreeRTOS提供了taskENTER_CRITICAL和taskEXIT_CRITICAL这对宏。我刚开始用的时候,觉得这玩意儿很简单,不就是关中断嘛。后来才发现,用不好照样出问题。

taskENTER_CRITICAL的原理:

  • 关闭所有可管理的中断(优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的)
  • 保存当前中断状态到局部变量
  • 执行临界区代码
  • 恢复中断状态

典型用法:

// 共享资源:姿态数据
typedef struct {
    float roll;
    float pitch;
    float yaw;
    uint32_t timestamp;
} attitude_t;

attitude_t g_attitude;

// 中断中写入姿态数据
void IMU_IRQHandler(void) {
    // 注意:这里不能用taskENTER_CRITICAL!
    // 因为中断里调用这个函数会导致死锁
    g_attitude.roll = imu_read_roll();
    g_attitude.pitch = imu_read_pitch();
    g_attitude.yaw = imu_read_yaw();
    g_attitude.timestamp = xTaskGetTickCountFromISR();
}

// 任务中读取姿态数据
void vControlTask(void *pvParameters) {
    for(;;) {
        // 进入临界区,防止中断修改数据
        taskENTER_CRITICAL();
        
        float roll = g_attitude.roll;
        float pitch = g_attitude.pitch;
        float yaw = g_attitude.yaw;
        uint32_t ts = g_attitude.timestamp;
        
        taskEXIT_CRITICAL();
        
        // 使用姿态数据进行控制计算
        control_attitude(roll, pitch, yaw, ts);
        
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

关键点:taskENTER_CRITICAL只能在任务中使用,不能在中断中使用。中断里要用portENTER_CRITICAL_ISR。我曾经见过有人在中端里调taskENTER_CRITICAL,结果系统直接挂掉。嗯,那次排查花了我一上午。

临界区保护的几个注意事项:

场景 推荐方法 原因
任务间共享数据 taskENTER_CRITICAL 简单高效,适合短操作
中断与任务共享数据 taskENTER_CRITICAL + 原子操作 防止中断打断任务
中断间共享数据 portENTER_CRITICAL_ISR 中断专用API
长时间操作 信号量或互斥锁 临界区应尽量短,避免影响实时性

个人经验:临界区里的代码要尽量短,最好在10条指令以内。我一般只在临界区里做指针赋值、标志位设置这种原子操作。如果要做复杂的数据处理,我会用双缓冲或者无锁队列。记住:临界区越长,系统实时性越差

4.4 实战中的中断管理策略

说了这么多理论,咱们来点实际的。我在做姿态控制系统时,中断管理一般遵循这几个原则:

  1. IMU数据中断优先级最高——控制周期的命脉
  2. 电机反馈中断次之——速度环需要及时响应
  3. 通信中断再次之——丢包可以重传
  4. 定时器中断最低——用于非关键任务调度

中断处理函数里,我习惯只做最必要的事:

// 好的中断处理:只做标记,不做处理
void IMU_IRQHandler(void) {
    // 1. 读取数据到缓冲区
    imu_buffer[head] = spi_read(IMU_REG);
    head = (head + 1) % BUFFER_SIZE;
    
    // 2. 通知任务处理
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xImuSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 坏的中断处理:在中断里做复杂计算
void BAD_IMU_IRQHandler(void) {
    // 千万别这么干!
    float roll = calculate_roll(imu_buffer);
    float pitch = calculate_pitch(imu_buffer);
    control_attitude(roll, pitch, yaw);  // 这行代码会阻塞中断!
}

最后提醒:中断优先级配置不是一劳永逸的。系统功能增加后,一定要重新评估中断优先级。我曾经在一个项目里,前期中断优先级配得好好的,后期加了个新功能,忘了调整优先级,结果系统响应慢了50%。嗯,那次教训让我养成了每次迭代都检查中断优先级的习惯。

好了,中断管理这块就讲到这里。记住:中断优先级是骨架,嵌套抢占是肌肉,临界区保护是神经。三者配合好了,你的姿态控制系统才能跑得稳、跑得快。