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 实战中的中断管理策略
说了这么多理论,咱们来点实际的。我在做姿态控制系统时,中断管理一般遵循这几个原则:
- IMU数据中断优先级最高——控制周期的命脉
- 电机反馈中断次之——速度环需要及时响应
- 通信中断再次之——丢包可以重传
- 定时器中断最低——用于非关键任务调度
中断处理函数里,我习惯只做最必要的事:
// 好的中断处理:只做标记,不做处理
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%。嗯,那次教训让我养成了每次迭代都检查中断优先级的习惯。
好了,中断管理这块就讲到这里。记住:中断优先级是骨架,嵌套抢占是肌肉,临界区保护是神经。三者配合好了,你的姿态控制系统才能跑得稳、跑得快。