3. RTOS任务模型:FreeRTOS任务创建与管理、任务优先级与调度、任务间通信机制
各位同学,咱们今天聊聊RTOS里的核心——任务模型。说白了,一个嵌入式实时系统,就是一堆任务在抢CPU时间片。你想想看,电机要转、编码器要读、通信要处理,这些事儿如果都挤在一个大循环里,迟早要出乱子。我刚开始做运动控制那会儿,就吃过这个亏。
3.1 任务创建与管理:从零开始造一个“小线程”
在FreeRTOS里,任务就是一个无限循环的函数。创建它,你得给它分配栈空间、指定优先级、传个参数。我个人习惯用xTaskCreate(),简单直接。
// 创建一个控制电机速度的任务
void vMotorControlTask(void *pvParameters) {
int32_t target_speed = *(int32_t*)pvParameters;
while(1) {
// 读取编码器,计算PID,输出PWM
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
}
}
// 在main里创建
xTaskCreate(
vMotorControlTask, // 任务函数
"MotorCtrl", // 任务名(调试用)
256, // 栈深度(字,不是字节!)
(void*)&speed, // 参数
3, // 优先级(0最低,configMAX_PRIORITIES-1最高)
NULL // 任务句柄(不需要就NULL)
);
嗯,这里要注意:栈大小别给少了。我在项目中遇到过,一个任务莫名其妙跑飞,查了半天发现是栈溢出。FreeRTOS有个uxTaskGetStackHighWaterMark(),可以帮你看看还剩多少栈空间。我建议每个任务至少留20%的余量。
3.2 任务优先级与调度:谁先跑,谁后跑?
FreeRTOS用的是抢占式调度。说白了,高优先级的任务一旦就绪,立马抢走CPU。低优先级的?等着吧。
在运动控制里,优先级怎么设?我一般这么分:
| 任务类型 | 优先级 | 典型周期 | 说明 |
|---|---|---|---|
| 电流环/位置环 | 最高(4-5) | 50-100μs | 硬实时,不能被打断 |
| 速度环/插补计算 | 高(3-4) | 1-10ms | 周期固定,抖动要小 |
| 通信处理(CAN/EtherCAT) | 中(2-3) | 1-10ms | 丢包可以重传,但别太慢 |
| 人机界面/日志 | 低(1-2) | 100ms+ | 慢点无所谓 |
你想想看,如果电流环被日志任务打断了,电机可能就抖一下。所以,优先级不是越高越好,而是“够用就行”。我见过有人把所有任务都设成最高优先级,结果系统直接卡死——高优先级任务互相抢,低优先级的永远没机会跑。
3.3 任务间通信机制:让任务们好好说话
任务之间怎么传数据?FreeRTOS给了你几样工具:队列、信号量、互斥量、事件组。我重点说说队列和信号量,运动控制里最常用。
3.3.1 队列:数据搬运工
队列就是个FIFO缓冲区。一个任务往里写,另一个任务往外读。我习惯用它来传递运动指令:
// 定义队列句柄
QueueHandle_t xMotionCmdQueue;
// 创建队列(每个元素是32位整数,队列深度10)
xMotionCmdQueue = xQueueCreate(10, sizeof(int32_t));
// 发送任务(比如上位机发来的目标位置)
void vCmdReceiverTask(void *pvParameters) {
int32_t target_pos;
while(1) {
// 从CAN总线接收指令
target_pos = receive_from_can();
// 发送到队列(如果队列满了,等100ms)
xQueueSend(xMotionCmdQueue, &target_pos, pdMS_TO_TICKS(100));
}
}
// 接收任务(插补器)
void vInterpolatorTask(void *pvParameters) {
int32_t cmd;
while(1) {
// 从队列读取(如果队列空,阻塞等待)
if(xQueueReceive(xMotionCmdQueue, &cmd, portMAX_DELAY) == pdPASS) {
// 执行插补计算
interpolate_to(cmd);
}
}
}
为什么用队列而不是全局变量?因为队列是线程安全的。全局变量?两个任务同时写,数据就乱了。我在项目中遇到过,编码器读数用全局变量传,结果位置环读到的是半新半旧的数据,电机直接震荡。从那以后,所有跨任务的数据我都用队列。
3.3.2 信号量与互斥量:资源锁与事件通知
信号量分两种:二值信号量和计数信号量。二值信号量就像个“旗子”,用来通知事件发生。计数信号量像个“令牌桶”,用来管理资源数量。
互斥量呢?它带优先级继承,能防止优先级反转。我举个例子:
// 共享资源:电机参数结构体
typedef struct {
float kp;
float ki;
float kd;
} MotorParams_t;
MotorParams_t g_motor_params;
SemaphoreHandle_t xParamMutex;
// 初始化互斥量
xParamMutex = xSemaphoreCreateMutex();
// 写参数任务(优先级3)
void vParamWriterTask(void *pvParameters) {
while(1) {
xSemaphoreTake(xParamMutex, portMAX_DELAY);
g_motor_params.kp = 1.5;
g_motor_params.ki = 0.1;
// ... 写操作
xSemaphoreGive(xParamMutex);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 读参数任务(优先级2)
void vParamReaderTask(void *pvParameters) {
float kp;
while(1) {
xSemaphoreTake(xParamMutex, portMAX_DELAY);
kp = g_motor_params.kp;
xSemaphoreGive(xParamMutex);
// 使用kp
vTaskDelay(pdMS_TO_TICKS(10));
}
}
嗯,这里要注意:互斥量的持有时间一定要短。我曾经在互斥量里调了个延时函数,结果高优先级任务被堵住,系统响应变慢。互斥量只用来保护“读-改-写”这种原子操作,别在里面干别的事。
xQueueSendFromISR()和xSemaphoreGiveFromISR(),不能用普通版本。我刚开始做时忘了这个,结果中断里调了xQueueSend(),系统直接死机。记住:带FromISR的才是中断安全的。
3.4 知识体系总览
说了这么多,咱们用一张图来总结一下RTOS任务模型的核心逻辑:
这张图把咱们今天讲的内容串起来了。任务创建是起点,管理是过程,调度是规则,通信是纽带。你想想看,没有通信机制,任务之间就是孤岛,运动控制根本跑不起来。
好了,今天就到这儿。记住:任务模型是RTOS的骨架,优先级是灵魂,通信是血脉。把这三点吃透了,你的运动控制代码就能跑得稳、跑得快。