3、中断管理:如何让电机响应更快更准

电机控制这活儿,说白了就是跟时间赛跑。你想想看,一个电机在高速旋转时,转子位置每毫秒都在变。如果你不能及时响应,轻则抖动,重则直接失步。我做了这么多年电机控制,最深的体会就是:中断管理做得好,电机控制就成功了一半

3.1 中断优先级:别让重要的事排队

RT-Thread默认的中断优先级配置,其实挺适合通用场景的。但电机控制不一样,它有自己的脾气。

我个人习惯把电机控制相关的中断分成三个梯队:

优先级 中断类型 典型用途
最高(0-1) PWM周期中断、霍尔传感器捕获 电流环、速度环核心计算
中等(2-3) 编码器Z信号、过流保护 位置校准、故障保护
较低(4-5) 通信接收、按键输入 参数调整、状态显示

我在项目中遇到过一个问题:某次调试无刷电机,电机在低速时一切正常,但一加速就开始抖动。查了半天,发现是UART接收中断和PWM中断抢优先级。UART中断处理时间太长,把PWM周期中断给堵住了。电流环更新不及时,电机自然就抖了。

注意:RT-Thread的中断优先级数值越小,优先级越高。别搞反了,我见过有人把电机中断设成最低优先级,结果电机根本转不起来。

3.2 中断嵌套:能省则省

很多人喜欢开中断嵌套,觉得这样响应更快。其实不然。中断嵌套多了,上下文切换的开销反而更大。

我的做法是:关键中断关嵌套,非关键中断随便

比如PWM周期中断,我一般会关掉其他中断的嵌套。这个中断执行时间很短,通常就几十微秒。你想想看,如果在这个中断里又被别的中断打断,那电流环的计算时间就不可控了。

/* 在PWM中断服务函数中 */
void PWM_IRQHandler(void)
{
    /* 进入中断时,硬件自动关中断 */
    /* 这里执行电流采样和FOC计算 */
    current_loop_calc();
    
    /* 退出中断时,硬件自动开中断 */
    /* 不需要手动操作 */
}

嗯,这里要注意:RT-Thread在中断中会做一些调度工作。如果你在中断里调用了rt_sem_release之类的函数,系统会在中断退出时检查是否需要切换任务。这个机制挺好的,但如果你在嵌套中断里用,可能会出问题。

小技巧:我习惯在PWM中断里只做最核心的计算,比如电流采样、Clark/Park变换、PI调节。其他像速度计算、位置估算这些,放到线程里去做。这样中断时间短,响应自然就快了。

3.3 中断延迟:看不见的杀手

中断延迟,说白了就是从中断触发到CPU开始执行中断服务函数的时间。这个时间如果太大,电机控制精度就会下降。

影响中断延迟的因素主要有三个:

  • 中断屏蔽时间:其他中断处理过程中关中断的时间
  • 中断优先级仲裁:高优先级中断抢占的时间
  • 硬件响应时间:CPU本身的中断响应延迟

我曾经在一个项目里,电机控制周期是100微秒。按理说够用了,但实际测下来,中断延迟有时候会到30微秒。这意味着什么?电流环的有效计算时间只剩下70微秒,稍微复杂点的算法就跑不完。

解决方法是:

  1. 精简中断服务函数:只做必要的事,其他事交给线程
  2. 使用零延迟中断:RT-Thread支持中断服务函数直接运行,不经过线程调度
  3. 避免在中断中调用耗时函数:比如打印、动态内存分配这些
/* 推荐的中断服务函数写法 */
void TIM_IRQHandler(void)
{
    /* 清除中断标志 */
    TIM_ClearFlag();
    
    /* 读取电流值 - 必须立即做 */
    current_adc = ADC_GetValue();
    
    /* 设置一个标志,让线程去处理 */
    motor_control_flag = 1;
    
    /* 千万不要在这里打印或延时 */
    /* rt_kprintf("current: %d\n", current_adc); // 错误! */
}
核心原则:中断里只做"必须现在做"的事,其他事能拖就拖。电机控制中,电流采样和PWM更新是必须立即做的,其他都可以放到线程里。

3.4 中断与线程的配合:分工明确

很多新手会把所有控制逻辑都塞到中断里,觉得这样响应快。其实这是误区。中断里代码越多,中断延迟越大,反而影响响应速度。

我常用的模式是:

  • 中断层:负责时间敏感的操作,比如电流采样、PWM更新、故障保护
  • 线程层:负责计算密集型操作,比如速度环、位置环、滤波算法

举个例子,FOC控制中:

/* 中断中 - 电流环 */
void PWM_IRQHandler(void)
{
    /* 采样电流 */
    ia = ADC_GetValue(ADC_CH_A);
    ib = ADC_GetValue(ADC_CH_B);
    
    /* Clark变换 */
    i_alpha = ia;
    i_beta = (ia + 2*ib) / sqrt3;
    
    /* Park变换 */
    id = i_alpha * cos(theta) + i_beta * sin(theta);
    iq = -i_alpha * sin(theta) + i_beta * cos(theta);
    
    /* PI调节 */
    vd = PI_Calc(id_ref, id);
    vq = PI_Calc(iq_ref, iq);
    
    /* 反Park变换 */
    v_alpha = vd * cos(theta) - vq * sin(theta);
    v_beta = vd * sin(theta) + vq * cos(theta);
    
    /* 更新PWM */
    PWM_Update(v_alpha, v_beta);
    
    /* 通知线程计算速度 */
    rt_sem_release(&speed_sem);
}

/* 线程中 - 速度环 */
void speed_loop_thread(void *parameter)
{
    while(1)
    {
        rt_sem_take(&speed_sem, RT_WAITING_FOREVER);
        
        /* 计算速度 */
        speed = encoder_get_speed();
        
        /* 速度PI调节 */
        iq_ref = PI_Calc(speed_ref, speed);
        
        /* 这里可以加滤波、限幅等处理 */
        iq_ref = limit_value(iq_ref, -MAX_CURRENT, MAX_CURRENT);
    }
}

你看,这样分工后,中断的执行时间非常短,大概就十几微秒。而速度环的计算虽然复杂,但放在线程里,不影响中断响应。

我的经验:中断和线程之间用信号量通信,比用全局变量更安全。信号量可以避免数据竞争,而且RT-Thread的信号量操作很快,不会影响实时性。

3.5 实测调优:用数据说话

理论说再多,不如实际测一测。我每次调完中断,都会用逻辑分析仪抓一下中断响应时间。

具体做法是:

  1. 在中断服务函数入口和出口各拉一个GPIO
  2. 用逻辑分析仪测量这个GPIO的脉冲宽度
  3. 这个宽度就是中断服务函数的执行时间

我曾经在一个项目中,通过这种方式发现PWM中断的执行时间从最初的45微秒优化到了12微秒。怎么做到的?就是把那些不必要的计算移到了线程里。

还有一个容易被忽略的点:中断优先级分组。RT-Thread默认使用4位优先级分组,也就是16级优先级。但有些MCU支持更多级。如果你用的MCU支持,可以试试把电机中断放在更高的优先级组里。

避坑指南:我曾经把中断优先级设得太高,结果系统滴答中断(SysTick)都被电机中断抢占了。系统时钟不准,所有延时函数都乱了套。所以,SysTick的中断优先级一定要比电机中断高,或者至少同级。

好了,关于中断管理,我就说这么多。记住一句话:中断是电机控制的命脉,管好中断,电机才能听话。下一章我们聊聊任务调度,看看怎么让多个控制任务和谐共处。