第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 知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
这张图,基本概括了本章的核心内容。大家可以把这张图存下来,以后做系统设计时,拿出来对照一下,看看自己有没有遗漏。
好了,第二章的内容就到这里。记住,实时系统不是玄学,它是一套可以量化的工程方法。你只要把硬实时和软实时分清楚,选对RTOS,用好调度策略,管好中断,你的运动控制代码,就能跑得又稳又准。
公众号:蓝海资料掘金营,微信deep3321