3. RTOS中断管理机制:临界区保护、中断服务程序(ISR)设计原则、中断与任务通信

大家好,我是你们的老朋友。今天咱们来聊聊RTOS中断管理,这玩意儿在运动控制里有多重要呢?这么说吧,我见过不少项目,电机跑着跑着突然抽风,十有八九都是中断没处理好。说白了,中断就是CPU的“紧急通道”,运动控制对实时性要求极高,中断管理搞不好,电机抖一下,产品就废了。

这一节,我打算从三个核心点切入:临界区保护、ISR设计原则、中断与任务通信。嗯,咱们一个一个来。

3.1 临界区保护:别让共享数据“打架”

先问大家一个问题:你在中断里修改了一个全局变量,主循环也在读它,会发生什么?

我遇到过最典型的场景:一个编码器脉冲计数变量,ISR里累加,主任务里做速度计算。结果呢?主任务读到一半,ISR插进来改了值,读出来的数据就“半新半旧”,速度计算直接崩了。这就是典型的“临界区问题”。

临界区,说白了就是一段“不能被中断打扰”的代码。在RTOS里,保护临界区最常用的手段就是关中断

核心原则:临界区代码要短、要快,绝对不能在里面做延时、等待或者调用可能阻塞的API。我见过有人把printf放进临界区,结果打印一串字符,中断关了上百微秒,电机直接丢步。

来看看FreeRTOS里怎么做的:

// 进入临界区
taskENTER_CRITICAL();

// 这里操作共享变量
g_encoder_count++;

// 退出临界区
taskEXIT_CRITICAL();

这段代码一执行,当前CPU核的中断就被屏蔽了。等退出临界区,中断才重新打开。注意,这只是保护了当前核,多核系统要更复杂些。

我的小技巧:我习惯在临界区开头加个断言,检查中断是否真的被关了。比如在调试阶段,用GPIO拉高拉低来测量临界区耗时,超过10微秒就要优化。

还有一种更轻量的方式——暂停调度器。它只禁止任务切换,不关中断。适合那些只跟任务间共享数据、不涉及ISR的场景。

vTaskSuspendAll();
// 操作任务间共享数据
xQueueSend(myQueue, &data, 0);
xTaskResumeAll();

嗯,这里要注意:暂停调度器期间,不能调用任何可能阻塞的API,比如vTaskDelay。我曾经踩过这个坑,代码直接死锁。

3.2 中断服务程序(ISR)设计原则:短、平、快

ISR的设计,我总结成三个字:短、平、快

  • :代码行数尽量少,最好不超过20行。
  • :不要调用非中断安全的API,比如printf、malloc。
  • :执行时间要控制在微秒级,别在ISR里做复杂运算。

为什么会这样?因为ISR执行期间,其他中断(同优先级或更低优先级)都被屏蔽了。你想想看,如果ISR里跑了个浮点运算,耗时几十微秒,那电机的位置环就来不及响应了。

我给大家看一个反面教材,这是我早期犯过的错:

// 错误示范:千万别这么写!
void TIM2_IRQHandler(void) {
    // 清除中断标志
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    
    // 在ISR里做PID计算!大忌!
    float error = target_pos - current_pos;
    float output = Kp * error + Ki * integral + Kd * derivative;
    // ... 还有更多运算
}

正确的做法是什么?ISR只做“标记”和“通知”,把繁重的工作交给任务:

// 正确示范:ISR只做最轻量的事
void TIM2_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 清除中断标志
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    
    // 读取硬件寄存器,记录关键数据
    g_current_pos = TIM_GetCounter(TIM3);
    
    // 通知任务去处理
    vTaskNotifyGiveFromISR(xMotorControlTaskHandle, 
                           &xHigherPriorityTaskWoken);
    
    // 如果需要,触发上下文切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

避坑指南:我曾经在一个项目中,ISR里调用了xQueueSend(非FromISR版本),结果系统随机死机。查了两天才发现,在ISR里必须用带“FromISR”后缀的API。记住:ISR里只能用ISR专用API

3.3 中断与任务通信:搭好“桥梁”

ISR收集了数据,怎么交给任务?直接写全局变量?可以,但不够优雅,也不安全。RTOS提供了几种标准机制:

通信方式 适用场景 ISR中可用? 我的推荐
全局变量 + 临界区 简单数据,如计数值 是(需关中断) 适合极简场景
信号量(Semaphore) 事件通知,如“数据就绪” 是(xSemaphoreGiveFromISR) ★★★★★ 最常用
消息队列(Queue) 传递数据块,如编码器值 是(xQueueSendFromISR) ★★★★☆ 灵活
任务通知(Task Notification) 轻量级信号,比信号量更快 是(vTaskNotifyGiveFromISR) ★★★★★ 性能最优

我个人最偏爱任务通知。为什么?因为它比信号量快30%左右,而且不需要额外创建内核对象。在运动控制这种高频率中断场景下,每一微秒都很宝贵。

来看一个实际例子:电机编码器Z信号中断,用来校准原点。

// ISR:编码器Z信号触发
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        // 清除中断标志
        EXTI_ClearITPendingBit(EXTI_Line0);
        
        // 记录当前编码器位置
        g_z_signal_pos = TIM_GetCounter(TIM3);
        
        // 使用任务通知唤醒校准任务
        vTaskNotifyGiveFromISR(xCalibrationTaskHandle, 
                               &xHigherPriorityTaskWoken);
    }
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务:等待Z信号,执行校准
void vCalibrationTask(void *pvParameters) {
    uint32_t ulNotificationValue;
    
    for (;;) {
        // 等待ISR的通知,超时设为最大
        ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        if (ulNotificationValue > 0) {
            // 收到通知,执行校准逻辑
            // 这里可以安全地读取g_z_signal_pos
            taskENTER_CRITICAL();
            uint32_t pos = g_z_signal_pos;
            taskEXIT_CRITICAL();
            
            // 执行原点校准
            Motor_CalibrateHome(pos);
        }
    }
}

关键点:注意看,ISR里只做了两件事:记录硬件值、发送通知。所有复杂的校准逻辑都放在任务里。这就是“ISR轻量化”的精髓。

还有一个细节:中断优先级的设置。在运动控制中,我通常把编码器中断设为最高优先级,因为位置信息一旦丢失,整个控制环就乱了。而像按键、串口这类中断,优先级可以放低一些。

我的经验:在FreeRTOS中,中断优先级要跟RTOS的配置对齐。比如configMAX_SYSCALL_INTERRUPT_PRIORITY这个宏,决定了哪些中断可以调用RTOS的API。我习惯把运动控制相关的中断优先级设得比这个值高,这样它们不会被RTOS的临界区延迟。

最后,我画了一张图,帮大家理清中断管理的整体逻辑:

RTOS中断管理核心流程 硬件中断源 定时器/编码器/GPIO ISR(中断服务程序) 短平快:只做标记和通知 临界区保护 任务通知 轻量级,性能最优 信号量 / 消息队列 灵活,适合复杂数据 全局变量 + 临界区 简单直接,需谨慎 任务(Task)处理 执行复杂逻辑:PID/校准 电机控制输出

这张图把整个流程串起来了:硬件中断触发 → ISR快速响应(受临界区保护)→ 通过任务通知/信号量/队列与任务通信 → 任务执行复杂逻辑 → 最终输出到电机。每一步都有它的规矩,乱不得。

好了,这一节的内容就到这里。中断管理是RTOS运动控制的基石,搞懂了它,后面讲任务调度、时钟同步就轻松多了。记住:ISR要短,通信要快,临界区要小。这九个字,是我用无数个调试夜晚换来的。


专注资料整理