第3章:实时操作系统(RTOS)基础:FreeRTOS任务创建与调度、任务优先级与堆栈配置、中断管理
各位同学,咱们今天聊聊实时操作系统。说实话,我刚入行那会儿,对RTOS也是又爱又恨。爱的是它能让多任务并行,恨的是稍不留神就出bug。但做运动控制,你绕不开它。
为什么?因为运动控制对实时性要求极高。你想想看,电机每转一圈可能要发几千个脉冲,每个脉冲的时机都不能错。用裸机轮询?累死你也搞不定。这时候,RTOS就是你的救星。
3.1 FreeRTOS任务创建:从零开始跑一个任务
任务,说白了就是一个无限循环的函数。它有自己的栈空间,有自己的优先级。我习惯把任务想象成一个独立的小人,各干各的活。
创建任务的API长这样:
// 任务函数原型
void vTaskFunction(void *pvParameters)
{
// 任务初始化代码
for(;;)
{
// 任务主体代码
// 这里放你的运动控制逻辑
vTaskDelay(pdMS_TO_TICKS(10)); // 延时10ms
}
}
// 创建任务
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位:字)
void *pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄(输出)
);
这里有个坑,我踩过。堆栈深度单位是字,不是字节。在32位MCU上,一个字是4字节。所以如果你要分配1024字节的栈,深度要写256。我曾经因为搞混这个,程序跑着跑着就莫名其妙复位了。查了两天才发现是栈溢出。
3.2 任务调度:谁先跑,谁后跑?
FreeRTOS是个抢占式调度器。什么意思?就是高优先级的任务可以随时打断低优先级的任务。这就像公司里,老板(高优先级)一说话,员工(低优先级)就得闭嘴。
调度策略其实就三种:
- 抢占式调度:高优先级就绪,立刻切换
- 时间片轮转:同优先级任务轮流跑,每人一个时间片
- 协作式调度:任务主动让出CPU才切换(这个用得少)
我做运动控制时,通常这样分配优先级:
| 优先级 | 任务类型 | 典型任务 |
|---|---|---|
| 最高(5) | 硬实时任务 | 电机电流环、编码器读取 |
| 高(4) | 软实时任务 | 位置环、速度环计算 |
| 中(3) | 通信任务 | EtherCAT、CAN总线处理 |
| 低(2) | 人机交互 | 按键扫描、LCD刷新 |
| 最低(1) | 后台任务 | 日志记录、状态统计 |
嗯,这里要注意。优先级不是越高越好。你把所有任务都设成最高优先级,那跟裸机轮询没区别。调度器会频繁切换,反而降低效率。
3.3 堆栈配置:给任务一个安稳的家
每个任务都有自己的栈。栈里存什么?局部变量、函数调用返回地址、中断现场。如果栈不够大,程序就会跑飞。
我教大家一个估算方法:
- 先给一个保守值(比如512字)
- 跑起来后,用
uxTaskGetStackHighWaterMark()查看剩余栈空间 - 根据剩余量调整,留出30%余量
// 查看栈剩余空间
UBaseType_t uxHighWaterMark;
uxHighWaterMark = uxTaskGetStackHighWaterMark(xTaskHandle);
printf("剩余栈空间: %d 字\n", uxHighWaterMark);
uint8_t ucDummy[100],然后memset成0xAA。跑一段时间后检查这个数组有没有被改写。如果有,说明栈溢出了。这招虽然土,但很管用。
3.4 中断管理:别让中断抢了任务的饭碗
中断是RTOS里最敏感的话题。FreeRTOS里,中断服务函数(ISR)要遵守几个铁律:
- ISR里不能调用阻塞API(比如
vTaskDelay) - ISR里要用带
FromISR后缀的API - ISR要尽量短,把耗时操作交给任务
举个例子,编码器中断来了,你该怎么做?
// 错误做法:在ISR里做大量计算
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 计算速度、位置... 千万别!
// 这会阻塞其他中断,导致系统抖动
}
// 正确做法:只发信号,让任务去处理
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 通知运动控制任务
vTaskNotifyGiveFromISR(xMotionTaskHandle,
&xHigherPriorityTaskWoken);
// 如果需要上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
我曾经在一个项目里,把整个PID计算都放在中断里。结果呢?电机一跑起来,系统就卡死。后来才发现,中断占用了太多时间,导致低优先级任务永远得不到CPU。这就是典型的「中断饿死任务」。
3.5 实战中的避坑指南
最后,分享几个我这些年踩过的坑:
- 优先级反转:低优先级任务持有信号量,高优先级任务等信号量,中间优先级任务插队。解决办法:用优先级继承或使用互斥量。
- 看门狗喂食:别在低优先级任务里喂狗。万一高优先级任务卡住,狗就超时了。我习惯在空闲任务里喂狗。
- 任务栈对齐:有些MCU要求栈8字节对齐。创建任务时注意堆栈地址。
好了,这一章的内容就这些。记住,RTOS不是万能的,但用好了,你的运动控制系统会稳如老狗。下一章咱们聊聊更深入的东西。
本章小结:
- 任务创建要合理分配堆栈,留足余量
- 优先级设计遵循「硬实时 > 软实时 > 通信 > 人机交互」原则
- 中断只做通知,不做计算
- 善用
uxTaskGetStackHighWaterMark()监控栈使用