第2章:实时系统基础——硬实时与软实时、RTOS选型、任务调度策略、中断管理

各位同学,欢迎来到第二章。

上一章我们聊了聊运动控制的整体轮廓。今天,咱们要扎进一个更核心的话题——实时系统。说白了,就是你的代码怎么在规定时间内,把活儿干完,还不掉链子。

我刚开始做运动控制那会儿,总觉得“实时”就是个噱头。直到有一次,我调试一个六轴机械臂,程序跑着跑着,电机突然抖了一下,工件直接飞出去了。嗯,从那以后,我再也不敢小看“实时”这两个字。

2.1 硬实时 vs 软实时:你的系统能容忍“迟到”吗?

先问大家一个问题:你的系统,能容忍任务偶尔晚几毫秒完成吗?

这个问题,直接决定了你选什么样的RTOS,以及怎么设计你的代码。

硬实时(Hard Real-Time):任务必须在截止时间前完成。晚一毫秒,就是系统故障。比如:伺服驱动器的电流环、安全急停逻辑。

软实时(Soft Real-Time):任务偶尔晚一点,系统性能下降,但不会崩溃。比如:人机界面刷新、日志记录。

我在做数控系统时,遇到过最典型的例子:

  • 插补计算:这是硬实时。每个周期必须算出下一个点的位置,否则电机就会抖动,甚至丢步。
  • 状态显示:这是软实时。屏幕上的坐标晚刷新100毫秒,操作员顶多觉得卡,机器不会坏。

我的个人习惯:在设计初期,我会把所有任务按“硬实时”和“软实时”分类。硬实时的任务,我会单独放在一个高优先级任务里,并且严格控制它的执行时间。软实时的任务,我通常会放在低优先级,或者用空闲时间处理。

2.2 RTOS选型:FreeRTOS vs RT-Thread,我该怎么选?

市面上RTOS很多,但做运动控制,我个人最常用的是FreeRTOS和RT-Thread。为什么?

先看一张对比表,大家心里有个数:

特性 FreeRTOS RT-Thread
内核大小 极小(4-9KB) 中等(10-20KB)
任务调度 抢占式+时间片轮转 抢占式+时间片轮转+优先级继承
中断延迟 极低(适合硬实时) 低(有中断嵌套支持)
组件生态 较少,需自己移植 丰富(文件系统、网络协议栈等)
学习曲线 平缓 中等
典型应用 传感器、简单电机控制 复杂运动控制、工业控制器

我个人怎么选?

  • 如果你做的是简单的步进电机控制,或者资源极其受限的MCU:我建议用FreeRTOS。它轻量、稳定,中断延迟控制得非常好。我曾经在一个Cortex-M0上跑FreeRTOS,控制一个直流无刷电机,效果很稳。
  • 如果你做的是多轴联动、带EtherCAT总线、或者需要文件系统记录轨迹:我建议用RT-Thread。它的组件生态太方便了,尤其是设备驱动框架,能省下不少移植时间。我记得有一次做四轴雕刻机,用RT-Thread的FinSH控制台调试,简直不要太爽。

避坑指南:我曾经在一个项目里,为了省事,直接用了FreeRTOS的默认配置。结果发现,中断服务函数里调用了printf,导致系统卡死。后来才意识到,FreeRTOS的中断服务函数里,不能调用可能导致阻塞的API。嗯,这个坑,大家一定要记住。

2.3 任务调度策略:抢占式、时间片、还是合作式?

调度策略,说白了就是“谁先干活,谁后干活”。

在运动控制里,我几乎只用抢占式调度。为什么?

你想想看,一个高优先级的插补任务来了,如果低优先级的显示任务还在慢悠悠地跑,那电机肯定要抖。抢占式调度,就是让高优先级任务“插队”,确保关键任务第一时间被执行。

但抢占式调度也有代价——优先级反转。这是个大坑。

举个例子:

  • 任务A(高优先级)和任务C(低优先级)共享一个互斥锁。
  • 任务B(中优先级)不共享锁,但一直在运行。
  • 如果C先拿到了锁,然后被B抢占,A就只能等C释放锁。但C又被B抢占了,所以A一直等不到锁。

结果就是:高优先级的A,被中优先级的B“饿死”了。

我的解决方案:使用优先级继承协议。RT-Thread原生支持,FreeRTOS需要自己实现。或者,更简单的方法——尽量减少高优先级任务对共享资源的依赖。我在做运动控制时,会把插补计算用的数据,设计成“无锁队列”,避免优先级反转。

另外,时间片轮转我也偶尔用。比如,两个同优先级的通信任务,一个收数据,一个发数据。用时间片轮转,能让它们公平地分享CPU时间。

2.4 中断管理:别让你的ISR变成“定时炸弹”

中断,是实时系统的灵魂。但也是最容易出问题的地方。

我见过太多新手,在中断服务函数里做大量计算,结果导致系统响应变慢,甚至丢失中断。

记住一个原则:ISR要短、快、轻

  • :只做最必要的事,比如读取硬件寄存器、清除中断标志。
  • :不要调用可能导致阻塞的API(比如printf、malloc)。
  • :把复杂的处理逻辑,放到任务里去做。

具体怎么做?我常用的模式是“中断+信号量”

// 中断服务函数
void TIM_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 清除中断标志
    TIM_ClearITPendingBit(TIMx, TIM_IT_Update);
    // 释放信号量,通知任务处理
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    // 如果唤醒的任务优先级更高,则进行上下文切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务函数
void vTaskFunction(void *pvParameters) {
    while(1) {
        // 等待信号量
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            // 在这里做复杂的处理,比如计算插补点
            vCalculateInterpolation();
        }
    }
}

我的经验:在ISR里,我只会做三件事:读寄存器、清标志、发信号量。剩下的,全部交给任务。这样,即使任务处理得慢一点,也不会影响下一个中断的到来。

另外,中断优先级的设置也很关键。在Cortex-M系列MCU里,我通常把定时器中断设为最高优先级,因为它负责运动控制的节拍。而串口中断,我会设得低一些,因为数据丢一帧,问题不大。

我曾经踩过的坑:有一次,我把两个中断设成了相同优先级,结果它们同时触发时,系统直接死机。后来查手册才发现,Cortex-M的中断优先级分组,需要仔细配置。嗯,从那以后,我每次都会检查NVIC的优先级分组设置。

2.5 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

实时系统基础:知识体系 实时系统基础 硬实时 vs 软实时 RTOS选型 任务调度策略 中断管理 硬实时(必须准时) 软实时(可容忍延迟) FreeRTOS RT-Thread 抢占式调度 时间片轮转 ISR:短、快、轻 中断+信号量模式 核心:保证确定性,避免优先级反转 ISR不做复杂计算,任务调度要合理

这张图,基本概括了本章的核心内容。大家可以把这张图存下来,以后做系统设计时,拿出来对照一下,看看自己有没有遗漏。

好了,第二章的内容就到这里。记住,实时系统不是玄学,它是一套可以量化的工程方法。你只要把硬实时和软实时分清楚,选对RTOS,用好调度策略,管好中断,你的运动控制代码,就能跑得又稳又准。


公众号:蓝海资料掘金营,微信deep3321