4. 中断管理:中断服务程序(ISR)设计原则、中断与任务的同步、临界区保护

中断管理,说白了就是RTOS的「神经反射弧」。

你想想看,电机编码器突然来了一个脉冲,或者驱动器报了个过流故障——这些外部事件不会等你任务调度完再处理。它们需要立刻响应。这就是中断存在的意义。

我个人习惯把中断比作「急诊室」。任务调度是普通门诊,按号排队。中断是急救通道,来了就得先处理。但急诊室也有规矩,不能乱来。这一章我们就聊聊这些规矩。

4.1 中断服务程序(ISR)设计原则

ISR怎么写?很多新手上来就在中断里做复杂运算。我见过有人把PID计算直接塞进ISR,结果系统直接崩了。为什么?因为中断服务程序必须短小精悍。

核心原则就三条:

  • 快进快出:ISR里只做最紧急的事。比如读取寄存器、清除中断标志、发送一个信号量。复杂计算丢给任务去做。
  • 不能阻塞:ISR里绝对不能调用阻塞型API。比如等待信号量、获取互斥量——这些都会让系统死锁。
  • 优先级合理:高优先级中断不能长时间霸占CPU。否则低优先级中断和任务都饿死了。

避坑指南:我曾经在一个伺服驱动项目里,把电流环的PI计算放在了ISR里。结果中断执行时间超过了中断周期,导致中断嵌套爆炸。最后整个系统看门狗复位。从那以后,我强制规定:ISR里只做「读-发信号-清标志」三步走。

来看一个典型的ISR模板:

// 正确的ISR写法
void TIM1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 1. 读取硬件状态(必须快)
    uint32_t ulEncoderValue = TIM1->CNT;
    
    // 2. 发送信号量通知任务处理
    xSemaphoreGiveFromISR(xEncoderSemaphore, &xHigherPriorityTaskWoken);
    
    // 3. 清除中断标志
    TIM1->SR = ~TIM_SR_UIF;
    
    // 4. 请求上下文切换(如果需要)
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

注意看,我没有在ISR里做任何数学运算。只是读取、发信号、清标志。真正的编码器数据处理,交给等待信号量的任务去做。

4.2 中断与任务的同步

中断和任务之间怎么配合?这是RTOS运动控制的核心问题。

我常用的同步机制有三种:

同步方式 适用场景 注意事项
信号量(Semaphore) 中断通知任务:有数据来了 ISR中必须用FromISR版本
消息队列(Queue) 中断向任务传递数据 队列长度要够,防止溢出
事件标志组(Event Group) 多个中断触发同一个任务 注意位宽限制

举个例子。电机控制中,编码器Z信号到来时,需要同步位置零点。我会这样设计:

// 编码器Z信号中断
void EXTI_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 发送消息到队列,包含当前编码器值
    uint32_t ulZeroPos = TIM2->CNT;
    xQueueSendFromISR(xZeroPosQueue, &ulZeroPos, &xHigherPriorityTaskWoken);
    
    EXTI->PR = EXTI_PR_PR0;
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务侧接收
void vZeroSyncTask(void *pvParameters)
{
    uint32_t ulReceivedPos;
    for(;;)
    {
        if(xQueueReceive(xZeroPosQueue, &ulReceivedPos, portMAX_DELAY) == pdTRUE)
        {
            // 在这里做位置同步,不占用中断时间
            vUpdateHomePosition(ulReceivedPos);
        }
    }
}

为什么会这样设计?因为位置同步可能需要做滤波、校准、甚至写入EEPROM。这些操作放在ISR里就是灾难。通过队列,中断只负责「通知」,任务负责「处理」。

个人经验:我建议队列长度至少设为2。为什么?因为如果任务处理慢,中断又来了,至少能缓存一次。我曾经因为队列长度设为1,丢掉了编码器Z信号,导致位置同步偶尔失败。查了两天才找到原因。

4.3 临界区保护

临界区是什么?就是一段「不能被中断打断」的代码。

你想想看,如果任务A正在更新一个全局变量,突然来了个中断,ISR也修改了这个变量。等中断返回,任务A继续执行——这时候变量值已经乱了。这就是竞态条件。

RTOS提供了两种保护方式:

  • 关中断:最粗暴,但最有效。适合保护极短的代码段。
  • 使用互斥量(Mutex):更优雅,但ISR里不能用。

看一个实际例子。在运动控制中,我们经常需要读取电机的位置和速度。这两个值必须「同时」读取,否则算出来的速度就不准。

// 错误的做法:没有保护
void vReadMotorState(void)
{
    // 如果此时发生中断,ISR修改了g_ulPosition
    g_ulPosition = TIM2->CNT;      // 可能被中断打断
    g_ulVelocity = TIM3->CCR1;     // 中断返回后,位置和速度不匹配
}

// 正确的做法:临界区保护
void vReadMotorState(void)
{
    taskENTER_CRITICAL();
    g_ulPosition = TIM2->CNT;
    g_ulVelocity = TIM3->CCR1;
    taskEXIT_CRITICAL();
}

警告:临界区里绝对不能做耗时操作!关中断意味着系统无法响应任何外部事件。我见过有人把printf放进临界区,结果串口打印时中断被关了几毫秒,电机直接失步。记住:临界区只保护「读-改-写」这种原子操作,代码行数控制在5行以内。

嗯,这里还要注意一个细节。FreeRTOS里,taskENTER_CRITICAL() 会关掉所有可屏蔽中断。但有些高优先级中断(比如系统滴答定时器)可能不受影响。如果你的硬件有NMI(不可屏蔽中断),那临界区也保护不了它。

4.4 中断嵌套与优先级

中断嵌套是个双刃剑。用好了能提高实时性,用不好就是死锁。

我个人的原则是:尽量少用嵌套。为什么?因为嵌套会增加中断延迟的不确定性。在运动控制中,确定性比绝对速度更重要。

来看一个典型的中断优先级配置:

中断源 优先级 说明
系统滴答定时器 最高(0) RTOS心跳,必须准时
电机电流环 高(1) 控制周期短,10-50μs
编码器位置捕获 中(2) 需要及时,但可容忍少量延迟
通信接口(CAN/SPI) 低(3) 可以缓冲,延迟影响小

这个配置的核心思想是:周期越短、实时性要求越高的中断,优先级越高。电流环如果被耽误,电机就会抖动。通信慢一点,顶多是数据延迟。

避坑指南:我曾经在一个六轴机器人项目里,把所有中断都设成了相同优先级。结果多个中断同时触发时,系统不知道先处理哪个,导致电流环和编码器中断互相干扰。后来我按照控制周期重新分配优先级,问题就解决了。记住:优先级不是随便设的,要根据实时性需求来排。

4.5 中断延迟的测量与优化

中断延迟——从硬件触发中断到ISR开始执行的时间——是衡量RTOS实时性的关键指标。

怎么测?我常用的方法是在ISR入口处翻转一个GPIO,然后用示波器看。简单粗暴,但有效。

// 测量中断延迟
void TIM1_IRQHandler(void)
{
    // 进入ISR,立即翻转GPIO
    GPIOA->ODR ^= GPIO_PIN_0;
    
    // ... 实际处理代码 ...
    
    // 退出前再翻转一次
    GPIOA->ODR ^= GPIO_PIN_0;
}

用示波器测量GPIO的脉冲宽度,就能知道ISR的执行时间。再对比中断触发信号和GPIO上升沿的时间差,就是中断延迟。

影响中断延迟的因素有哪些?

  • 关中断时间:临界区越长,延迟越大
  • 中断嵌套:高优先级中断会抢占低优先级的
  • 硬件设计:中断控制器、总线仲裁都会引入延迟

优化建议:

  • 把临界区压缩到极致。我习惯用「三行原则」:读、写、清标志,三行搞定。
  • 避免在ISR里调用复杂API。比如printfmalloc——这些函数内部可能关中断。
  • 如果硬件支持,把频繁触发的中断合并。比如多个编码器通道共用一个中断,在ISR里统一处理。

个人经验:我做过一个测试,在Cortex-M4上,FreeRTOS的关中断时间大约是0.2μs。如果临界区里只做简单的变量赋值,中断延迟可以控制在1μs以内。但如果临界区里调用了函数,延迟可能飙升到5μs以上。所以我的原则是:临界区里不调用任何函数,包括内联函数。

4.6 本章小结

中断管理是RTOS运动控制的基石。记住三个核心点:

  • ISR要短小精悍,只做「读-通知-清标志」
  • 中断和任务通过信号量、队列同步,别在ISR里做复杂运算
  • 临界区保护共享数据,但别在里面待太久

嗯,这些原则说起来简单,做起来需要经验积累。我刚开始做运动控制时,也踩过不少坑。但只要记住「快进快出、同步分离、保护得当」这十二个字,基本不会出大问题。

中断管理核心知识体系 中断管理 ISR设计原则 快进快出 不能阻塞 优先级合理 中断与任务同步 信号量 消息队列 事件标志组 临界区保护 核心原则:快进快出 · 同步分离 · 保护得当