2、实时系统基础:RTOS概念、任务调度、中断管理、时间确定性
各位工程师朋友,咱们直接切入正题。
做运动控制,说白了就是跟时间赛跑。你想想看,电机转一圈才几毫秒,位置偏差一旦累积,后果就是撞机、飞车。我见过太多人把精力全花在算法上,却忽略了底层的实时系统。结果呢?算法再漂亮,跑起来照样抖得像筛糠。
这一章,我们就来聊聊实时操作系统(RTOS)的那些事。它不是什么高深莫测的东西,就是一套保证「在规定时间内干完规定活」的规则。
2.1 什么是实时操作系统?
普通操作系统,比如Windows、Linux,讲究的是「公平」。每个程序轮流用CPU,谁也别饿着。但实时操作系统不一样,它讲究的是「确定性」。
什么叫确定性?就是我知道这个任务最多花3毫秒,那它绝对不会超过3.1毫秒。我在调试一个六轴机器人时,遇到过一个问题:上位机偶尔发一个指令,下位机要过好几十毫秒才响应。查了半天,原来是Linux的调度器在后台刷磁盘缓存,把运动控制线程给挤到一边去了。换成RTOS后,这个问题再没出现过。
核心区别一句话:普通OS追求平均性能,RTOS追求最差情况下的性能。
RTOS的几个关键特征:
- 可抢占式内核:高优先级任务能随时打断低优先级任务
- 任务优先级:每个任务都有明确的优先级,系统按优先级调度
- 中断响应时间可预测:从硬件中断触发到ISR开始执行,时间基本固定
- 任务切换时间确定:上下文切换的时间是已知的、固定的
2.2 任务调度——谁先跑,谁后跑?
任务调度是RTOS的核心。我习惯把任务分成三类:
- 硬实时任务:必须按时完成,否则系统崩溃。比如伺服驱动器的电流环,通常要求在10-50微秒内完成。
- 软实时任务:偶尔超时还能接受,但会影响性能。比如通信协议栈,偶尔丢一包数据还能重传。
- 非实时任务:什么时候跑完都行。比如日志记录、状态显示。
最常见的调度算法是优先级抢占式调度。说白了就是:谁优先级高,谁先跑。同优先级的任务,就轮流跑(时间片轮转)。
举个例子,一个典型的运动控制任务分配:
| 任务名称 | 优先级 | 周期 | 最大执行时间 |
|---|---|---|---|
| 电流环控制 | 最高(0) | 50 μs | 30 μs |
| 速度环控制 | 高(1) | 200 μs | 100 μs |
| 位置环控制 | 中(2) | 1 ms | 400 μs |
| 通信处理 | 低(3) | 10 ms | 2 ms |
| 日志记录 | 最低(4) | 100 ms | 5 ms |
避坑指南:我曾经在一个项目中,把通信任务的优先级设得比位置环还高。结果位置环偶尔被通信任务打断,导致电机在高速运行时出现抖动。后来花了整整两天才定位到这个问题。记住:运动控制任务永远要放在最高优先级。
2.3 中断管理——打断的艺术
中断是RTOS的另一个核心。没有中断,CPU就得不停地轮询,效率极低。
中断处理的原则:ISR里尽量少干活,把重活交给任务。
为什么?因为ISR会打断其他所有任务,包括高优先级的运动控制任务。如果ISR执行时间太长,就会导致运动控制任务超时。
我常用的做法是「上半部-下半部」模式:
- 上半部(ISR):只做最紧急的事,比如读取硬件寄存器、清除中断标志、发送一个信号量。
- 下半部(任务):等待信号量,然后处理复杂的逻辑,比如解析数据、更新状态机。
代码示例(基于FreeRTOS):
// 中断服务程序 - 上半部
void TIM_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 清除中断标志
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
// 读取编码器计数值(必须立即读取,否则可能被覆盖)
g_encoder_count = TIM_GetCounter(TIM2);
// 发送信号量,通知任务处理
xSemaphoreGiveFromISR(xEncoderSemaphore, &xHigherPriorityTaskWoken);
// 如果信号量唤醒的任务优先级更高,立即切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务 - 下半部
void EncoderTask(void *pvParameters) {
while(1) {
// 等待信号量
if(xSemaphoreTake(xEncoderSemaphore, portMAX_DELAY) == pdTRUE) {
// 处理编码器数据,计算速度、位置
ProcessEncoderData(g_encoder_count);
}
}
}
个人经验:ISR里千万不要调用printf、malloc这类函数。它们可能阻塞,也可能导致优先级反转。我见过一个同事在ISR里打印调试信息,结果系统直接死机。嗯,从那以后,我所有的调试输出都放在任务里做。
2.4 时间确定性——异常处理的基石
时间确定性,是实时系统最核心的特性。它意味着:无论系统负载如何,关键任务都能在规定时间内完成。
为什么这对异常处理这么重要?
你想想看,当系统检测到异常时(比如电机过流、位置超差),必须在几微秒内做出响应。如果系统的时间确定性不好,响应时间忽长忽短,那异常处理就无从谈起。
影响时间确定性的几个因素:
- 中断延迟:从硬件中断触发到CPU开始执行ISR的时间。包括:硬件延迟、CPU关中断时间、正在执行的指令完成时间。
- 任务切换时间:保存当前任务上下文、恢复新任务上下文的时间。这个时间应该是固定的。
- 优先级反转:低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。这是实时系统的大敌。
优先级反转的经典案例:任务A(高优先级)和任务C(低优先级)共享一个互斥锁。任务C先拿到锁,然后被任务B(中优先级)抢占。任务B一直运行,任务C无法释放锁,任务A就只能干等。结果就是:高优先级任务被中优先级任务间接阻塞了。
解决方案:使用优先级继承协议。当高优先级任务等待锁时,临时提升持有锁的任务的优先级,让它尽快释放锁。
为了直观展示实时系统的核心逻辑,我画了一张图:
这张图想表达的意思很简单:任务调度、中断管理、时间确定性,这三者缺一不可。它们共同支撑起实时异常处理这个上层建筑。
2.5 实战中的几个要点
最后,分享几个我在项目中积累的经验:
- 关中断的时间要严格控制。有些工程师喜欢在临界区关中断,一关就是几十微秒。这会导致其他中断无法响应,包括运动控制需要的定时器中断。我一般要求关中断时间不超过5微秒。
- 任务栈大小要留余量。RTOS的任务栈是静态分配的,如果栈溢出,系统会崩溃。我习惯在计算出的栈大小基础上,再加30%的余量。
- 使用看门狗。在运动控制中,我每个周期任务都会喂一次看门狗。如果任务超时,看门狗会复位系统,避免电机失控。
- 测量实际执行时间。不要相信理论计算。用示波器或者逻辑分析仪,实际测量每个任务的执行时间。你会发现,理论值和实际值往往差很多。
重要提醒:不要在一个RTOS里跑太多任务。任务越多,调度开销越大,时间确定性越差。我个人的经验是:运动控制相关的任务不超过5个,其他辅助任务不超过10个。超过这个数,就该考虑用多核处理器了。
好了,关于实时系统的基础,我们就聊到这里。记住一句话:实时系统不是万能的,但没有实时系统,运动控制的异常处理就是纸上谈兵。下一章,我们会深入具体的异常检测技术,看看如何在代码层面实现快速、准确的异常判断。