第4章:实时操作系统RTOS基础:FreeRTOS任务创建、调度、同步与通信机制
好,咱们进入正题。这一章聊FreeRTOS,说白了就是给单片机装上「多任务大脑」。你想想看,一个机器人要同时处理传感器数据、控制电机、还要跟电脑通信,用裸机轮询的方式,迟早要出问题。我最早做四轴飞行器的时候,就吃过这个亏——主循环里一个延时卡住,飞机直接翻跟头。
FreeRTOS的好处是啥?它帮你把CPU时间切成片,每个任务轮流跑。感觉就像多个程序在同时运行。嗯,这里要注意,这不是真正的并行(单核芯片做不到),但足够用了。
4.1 任务创建:让代码「活」起来
任务在FreeRTOS里就是一个无限循环的函数。创建任务用xTaskCreate(),我习惯把任务栈大小设得稍微大一点——曾经因为栈溢出导致系统随机死机,查了三天才找到原因。
void vTask1(void *pvParameters) {
while(1) {
// 你的任务代码
vTaskDelay(pdMS_TO_TICKS(100)); // 延时100ms
}
}
void main() {
xTaskCreate(
vTask1, // 任务函数
"Task1", // 任务名字(调试用)
128, // 栈深度(单位是字,不是字节)
NULL, // 参数
1, // 优先级(数字越大优先级越高)
NULL // 任务句柄
);
vTaskStartScheduler(); // 启动调度器
}
这里有个坑:任务函数绝对不能返回!一旦return,系统就会触发断言。我见过新手在任务里写return 0;,结果整个系统崩了。正确的做法是用vTaskDelete(NULL)来结束自己。
4.2 任务调度:谁先跑?跑多久?
FreeRTOS默认是抢占式调度。什么意思?高优先级的任务随时可以打断低优先级的任务。我打个比方:你正在写代码(低优先级任务),老板突然让你去开会(高优先级任务),你必须放下手头工作先去开会。
优先级从0到configMAX_PRIORITIES-1,数字越大优先级越高。我个人习惯把控制类任务(比如电机PID)设高优先级,把显示、日志这类任务设低优先级。
| 优先级 | 典型任务 | 说明 |
|---|---|---|
| 3(最高) | 电机控制 | 实时性要求极高,1ms内必须响应 |
| 2 | 传感器采集 | 10ms周期读取IMU数据 |
| 1 | 通信处理 | 处理串口、CAN总线数据 |
| 0(最低) | 日志输出 | 慢一点没关系 |
同优先级的任务采用时间片轮转。每个任务跑一个tick(通常1ms),然后切换到下一个。如果你想让某个任务多跑一会儿,可以调用taskYIELD()主动让出CPU。
4.3 任务同步:别让它们打架
多个任务访问同一个资源时,就会出问题。比如两个任务都要往串口发数据,结果数据混在一起,变成乱码。这时候就需要同步机制。
FreeRTOS提供了三种同步方式:
- 信号量(Semaphore):就像厕所的钥匙,谁拿到谁用,用完归还
- 互斥量(Mutex):信号量的变种,带优先级继承,防止优先级反转
- 事件组(Event Group):多个条件同时满足才触发
我项目中用得最多的是互斥量。看个例子:
SemaphoreHandle_t xMutex;
void vTaskWrite(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 临界区:只有拿到互斥量才能执行
printf("Task %d is writing...\n", (int)pvParameters);
xSemaphoreGive(xMutex); // 释放互斥量
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void main() {
xMutex = xSemaphoreCreateMutex();
// 创建两个任务,都写串口
xTaskCreate(vTaskWrite, "Write1", 128, (void*)1, 1, NULL);
xTaskCreate(vTaskWrite, "Write2", 128, (void*)2, 1, NULL);
vTaskStartScheduler();
}
4.4 任务通信:数据怎么传?
任务之间不仅要同步,还要传数据。FreeRTOS提供了队列(Queue)机制,说白了就是一个先进先出的缓冲区。
我举个例子:传感器任务采集到数据,通过队列发给控制任务。这样两个任务解耦了,传感器任务不用管数据被谁用,控制任务也不用管数据从哪来。
QueueHandle_t xQueue;
void vSensorTask(void *pvParameters) {
int32_t sensor_value = 0;
while(1) {
sensor_value = read_sensor(); // 读取传感器
xQueueSend(xQueue, &sensor_value, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void vControlTask(void *pvParameters) {
int32_t received_value;
while(1) {
if(xQueueReceive(xQueue, &received_value, portMAX_DELAY) == pdTRUE) {
// 收到数据,执行控制算法
control_motor(received_value);
}
}
}
void main() {
xQueue = xQueueCreate(10, sizeof(int32_t)); // 队列长度10,每个元素4字节
xTaskCreate(vSensorTask, "Sensor", 128, NULL, 2, NULL);
xTaskCreate(vControlTask, "Control", 128, NULL, 1, NULL);
vTaskStartScheduler();
}
队列的大小要仔细设计。太小了容易丢数据,太大了浪费内存。我一般根据数据产生速率来算:如果传感器10ms产生一个数据,控制任务5ms处理一个,那队列长度设2就够了。
4.5 实际项目中的避坑指南
做了这么多年嵌入式,我总结了几条FreeRTOS的「血泪教训」:
- 栈大小别省:任务栈设小了,系统会随机崩溃。用
uxTaskGetStackHighWaterMark()查看栈使用情况,留出30%余量。 - 中断里别干重活:中断服务函数要短小精悍,只做标记或发信号量。真正的处理放到任务里做。
- 优先级别乱设:高优先级任务不能有阻塞调用(比如等待队列),否则低优先级任务永远没机会跑。
- 注意优先级反转:低优先级任务拿着互斥量,高优先级任务在等,中间优先级的任务趁机抢跑。用互斥量(带优先级继承)可以解决。
我记得有一次做机器人底盘控制,电机任务优先级设得太高,结果它一直占着CPU,其他任务全部饿死。后来把电机任务里加了个vTaskDelay(1),问题就解决了。有时候,适当的「让一让」反而让系统更稳定。
好了,这一章就到这里。下一章咱们聊聊中断管理——怎么在RTOS里优雅地处理硬件中断。记住,多任务系统就像管理一个团队,每个任务各司其职,调度器就是那个项目经理。用好了,事半功倍;用不好,系统崩溃。