8. 插补任务优先级设计:实时性分析、任务优先级分配策略、中断与任务协同

大家好,我是你们的老朋友。今天我们来聊聊插补任务优先级设计。说实话,这个题目看起来简单,但里面坑特别多。我见过不少项目,算法写得漂亮,最后却因为优先级没设计好,导致电机一顿一顿的,甚至直接飞车。

为什么会这样?说白了,实时性没分析透。你想想看,插补任务本质上是个硬实时任务。每个周期必须算完,算不完就丢脉冲,丢脉冲就丢位置。在数控系统里,丢一个脉冲可能就导致工件报废。

8.1 实时性分析:先搞清楚你的时间预算

做优先级设计之前,我习惯先做实时性分析。这不是走形式,是真的要算清楚。

拿一个三轴联动系统举例。假设插补周期是1ms,也就是1000Hz。那么每个周期里,CPU要干这些事:

  • 读取编码器反馈(约50μs)
  • 执行插补算法(约200μs)
  • 计算速度规划(约100μs)
  • 输出脉冲指令(约50μs)
  • 任务切换开销(约30μs)

加起来大概430μs。嗯,这里要注意,这只是理想情况。实际项目中,中断嵌套、缓存未命中、总线争用都会吃掉你的时间。我建议至少留出30%的余量。也就是说,1ms周期里,你的任务执行时间最好控制在700μs以内。

核心原则:插补任务的WCET(最坏情况执行时间)必须小于插补周期的70%。否则,一旦出现极端情况,系统就会丢步。

我在项目中遇到过一件事。有个同事把插补周期设成500μs,觉得CPU主频够高没问题。结果一跑起来,偶尔会出现周期超时。查了半天,发现是某个中断服务程序里有个浮点运算,偶尔触发异常处理,多花了200μs。从那以后,我定了个规矩:中断服务程序里绝对不做浮点运算,全部用定点数。

8.2 任务优先级分配策略:谁先谁后,心里要有数

RTOS里,优先级分配是个艺术活。我个人习惯用「速率单调调度」的思路——周期越短的任务,优先级越高。但实际项目中,还要考虑任务的重要性。

我一般把任务分成三个等级:

优先级等级 典型任务 周期/触发条件 说明
高(实时中断级) 编码器捕获、脉冲输出 硬件触发 由硬件中断直接处理,不经过RTOS调度
中(硬实时任务) 插补计算、速度规划 1ms周期 RTOS最高优先级任务,不可被抢占
低(软实时任务) 状态显示、通信处理 10-100ms周期 可被抢占,允许偶尔延迟

你可能会问:为什么插补任务不直接放在中断里?嗯,这个问题问得好。中断服务程序讲究短平快,而插补算法往往涉及查表、插值、加减速计算,代码量不小。放在中断里会阻塞其他中断,得不偿失。我建议的做法是:中断只做最轻量的数据采集,然后通过信号量或消息队列唤醒插补任务。

我的经验:插补任务的优先级,设为RTOS中仅次于Tick中断的级别。这样既能保证实时性,又不会干扰系统时钟。

8.3 中断与任务协同:别让中断抢了任务的饭碗

中断和任务的关系,有点像老板和员工。中断是老板,随时可以打断你。但老板不能事无巨细都自己干,否则员工就没事做了。

我常用的协同模式是这样的:

  1. 中断层:处理硬件事件,比如编码器Z信号捕获、限位开关触发。只做最少的处理——读寄存器、清标志、发信号量。代码量控制在20行以内。
  2. 任务层:处理插补计算、轨迹规划、IO控制。这些任务由中断唤醒,但执行在任务上下文中,可以调用RTOS的API,也可以做阻塞操作。
  3. 守护层:一个低优先级的监控任务,定期检查插补任务是否超时。如果发现异常,立即触发急停。

举个例子,编码器中断来了:

// 中断服务程序 - 编码器捕获
void Encoder_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 1. 读取编码器计数值(硬件相关,尽量快)
    uint32_t pulse_count = TIM->CNT;
    
    // 2. 存入环形缓冲区(无锁操作)
    ringbuf_push(&encoder_buf, pulse_count);
    
    // 3. 唤醒插补任务
    vTaskNotifyGiveFromISR(xInterpolationTaskHandle, 
                           &xHigherPriorityTaskWoken);
    
    // 4. 请求上下文切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

你看,中断里只做了三件事:读数据、存数据、发通知。真正的插补计算在任务里做。这样设计的好处是:中断响应快,不会丢失脉冲;任务执行稳定,不会被频繁打断。

避坑指南:我曾经在一个项目里,把插补计算直接放在定时器中断里。结果因为计算量太大,导致低优先级中断(比如串口接收)被饿死。系统跑着跑着就死机了。后来改成中断+任务模式,问题迎刃而解。

8.4 优先级反转:一个容易被忽视的问题

优先级反转,说白了就是高优先级任务被低优先级任务阻塞了。在插补系统里,这可能是致命的。

举个例子:插补任务(高优先级)和显示任务(低优先级)共享一个数据缓冲区。显示任务正在读缓冲区,突然被中断打断。中断唤醒了插补任务,插补任务想写缓冲区,但发现锁被显示任务占着,只能等。这一等,可能就超时了。

我的解决方案是:

  • 能不共享就不共享。插补任务的数据,全部用私有内存。显示任务要的数据,通过消息队列拷贝一份过去。
  • 必须共享时,用无锁数据结构。比如环形缓冲区、双缓冲。避免使用互斥锁。
  • 如果非要用锁,那就用优先级继承协议。RTOS一般都有这个选项,打开它。

8.5 知识体系总览

说了这么多,我画了一张图,帮你把整个优先级设计的逻辑串起来:

插补任务优先级设计知识体系 实时性分析 优先级分配策略 中断与任务协同 WCET计算 周期余量设计 速率单调调度 三级优先级划分 中断轻量化 任务上下文处理 关键问题:优先级反转 & 死锁预防 无锁数据结构 优先级继承协议 消息队列拷贝 图8-1 插补任务优先级设计知识体系

这张图把整个优先级设计的脉络理清楚了。从实时性分析开始,到优先级分配,再到中断协同,最后落到优先级反转这个关键问题上。你照着这个思路去设计,基本不会出大问题。

好了,关于插补任务优先级设计,我就讲这么多。记住一句话:实时性分析是基础,优先级分配是手段,中断协同是保障。三者缺一不可。