整理了作为小白的我学FreeRTOS的笔记,希望对一些也在学习FreeRTOS的同学有所帮助。
目录
1. 什么是操作系统(Operating System, OS)?
6 .空闲任务(Idle Task )与钩子函数(Hook)
一、基础概念
-
1. 什么是操作系统(Operating System, OS)?
因为FreeRTOS是一个操作系统,所以这里先讲一下操作系统的概念;
操作系统是一种系统软件,它负责管理计算机的硬件资源,并为应用程序提供运行环境和服务。
它就像是硬件与应用程序之间的桥梁,帮助应用程序有序地使用 CPU、内存、输入输出设备等硬件资源。
当多个程序同时请求同一个资源(比如 CPU 或打印机)时,操作系统会像一个“领导”一样,通过调度算法和内存管理策略,决定哪个程序先使用资源,哪个程序要等待,从而保障系统的稳定性与效率。
-
2. 操作系统有哪些?
操作系统按照用途分主要有两大类——通用操作系统和实时操作系统;
(1)通用操作系统(General-Purpose OS)
通用操作系统是指面向大众用户或通用计算平台设计的操作系统,主要关注多功能、用户交互体验、任务并发处理、资源管理效率。它们运行在电脑、手机、服务器等设备上,具备图形界面、文件系统、网络功能、驱动支持等复杂服务,为各种应用程序提供统一平台。主流的通用操作系统其实我们每天都会接触到,例如有我们电脑上按照的Windows、Linux、Android,以及苹果手机的IOS系统等;
(2)实时操作系统(RTOS, Real-Time OS)
实时操作系统是一种专为嵌入式设备、工业控制等应用场景设计的轻量操作系统,它强调任务响应时间的可预测性和及时性。“实时”不一定意味着“快”,而是指对时间的控制非常严格,任务必须按预定时间点完成,否则就可能导致系统失效。RTOS系统有我们今天要学的FreeRTOS,还有国产开源的RT-Thread,除这两个外,其实还有其他的RTOS系统,由于目前我还没接触到,所以这里就不班门弄斧了。
(3)以下是通用操作系统和实时操作系统的主要区别:
对比角度 | 通用操作系统(GPOS) | 实时操作系统(RTOS) |
核心目标 | 强调功能丰富和用户体验 | 强调任务按时完成,响应及时 |
响应速度 | 响应时间不确定,可能延迟 | 响应时间可预测、低延迟 |
调度机制 | 按公平或效率调度,不保证准时执行 | 按优先级或实时需求调度,可抢占调度 |
资源占用 | 功能多,资源占用大 | 精简设计,资源占用小 |
适用场景 | 手机、电脑、服务器等 | 工业控制、机器人、汽车等对时效敏感场景 |
-
3. 什么是FreeROTS?
好,上文已经提到了RTOS的概念,那接下来正式开始我们要学习的内容重要基础概念——FreeRTOS是什么?
FreeRTOS英文全称是Free Real-Time Operating System,它是一个轻量级、开源的实时操作系统,常用于嵌入式系统,它提供了任务调度、多任务并行、任务间通讯、资源管理等功能,适用于复杂的嵌入式应用场景。
在执行程序时,如果你使用 FreeRTOS ,它会通过调度器管理这些任务的运行时间,自动切换任务(例如 A 任务等 I/O 的时候,切换执行 B 任务),并且提供了通信与同步机制(信号量、队列等),所以FreeRTOS 可以让你写的 MCU 程序变得有组织、能多任务并发、响应及时。
-
4. 裸机开发与FreeROTS开发的区别:
裸机开发是指不依赖操作系统,直接在硬件上编写和运行代码的编程方式,一切任务执行顺序、时机、优先级 —— 都是靠你手写代码来控制的。你写什么就执行什么,你写在哪儿它就在哪儿执行,想延时就用 delay() 函数,但一延时就整个程序停在那里。
下面,通过以下2个示例来说明裸机编程和FreeRTOS编程的区别:
1. 裸机编程示例(阻塞式执行)
#include <stdio.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
void app_main() {
while (1) {
ESP_LOGI("TASK", "任务A:读取温湿度传感器");
vTaskDelay(pdMS_TO_TICKS(1000)); // 任务A执行1秒
ESP_LOGI("TASK", "任务B:发送数据到串口");
vTaskDelay(pdMS_TO_TICKS(1000)); // 任务B执行1秒
}
}
//在这个代码中,任务 A 执行完再到任务 B,中途所有人都必须等当前任务完成,无法并发,延迟会影响系统响应;
2. FreeRTOS 示例(多任务并发):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void task_A(void *pvParameters) {
while (1) {
printf("任务A:读取温湿度传感器\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒,不会阻塞其他任务
}
}
void task_B(void *pvParameters) {
while (1) {
printf("任务B:发送数据到串口\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main() {
xTaskCreate(task_A, "Task A", 2048, NULL, 1, NULL);
xTaskCreate(task_B, "Task B", 2048, NULL, 1, NULL);
}
//两个任务交替运行,看起来是“同时”执行,实际上是由 FreeRTOS 调度器轮流让任务占用 CPU,响应更快,也更高效;
想象一下这是一个接力赛:
在裸机编程中,只有一个人能拿接力棒跑,必须等他(Task A)跑完并休息够了,接力棒才能交给下一个人(Task B)。其他人全程干等着,不管有没有更重要的事情要做。
而在FreeRTOS中,有一个“智能裁判”负责控制每个人轮流接棒,每个选手(Task)只能跑一小段时间(比如10毫秒),然后裁判就把接力棒换给下一个人。这样每个任务看似是在同时进行,但实际上只有一个人在跑步,但因为切换得很快,我们几乎察觉不到交接。
所以说,FreeRTOS 提供了一种“伪并发”机制,让多个任务井然有序地共享 CPU,提高系统响应速度,避免一个任务卡住拖慢整体。
-
5.FreeRTOS特点
上文对比了裸机开发和FreeRTOS的区别,相信你对它已经有大概的了解了,这里,再详细讲一下FreeRTOS的特点;
(1)轻量级,资源占用小
FreeRTOS 的内核代码非常小(通常 10~20KB 左右)。非常适合资源有限的嵌入式系统,比如 STM32、ESP32、ARM Cortex-M 系列等。
(2)实时性强
FreeRTOS支持抢占式调度,能及时响应高优先级任务。当有紧急任务时,低优先级任务会被立刻“打断”,系统能保证关键任务及时完成;
(3)多任务管理
可以同时管理多个任务(Task),每个任务像一个独立的小程序,开发更容易维护。
(4) 优先级调度
你可以设置任务的优先级,调度器根据优先级选择哪个任务先运行。高优先级任务随时都可以抢占低优先级任务。
(5) 任务间通信机制丰富
支持:队列(Queue)、信号量(Semaphore)、消息通知、事件组(Event Group)等。任务之间协作和共享数据更安全、更可靠。
(6) 可移植性强
FreeRTOS 是高度可移植的,能跑在上百种芯片和架构上(ARM、RISC-V、ESP32 等)。
以上这些特点,使得FreeRTOS成为当下最受欢迎的RTOS,毕竟——打个不恰当的比方,它就像是一个拿着很低月薪但是还要替老板统筹规划、让整个公司运作得井然有序的员工,哪个老板能拒绝呢?(开个玩笑)
二、FreeRTOS 的核心组成
-
1. 任务管理(Task Management)
任务(Task)是 FreeRTOS 中的最小执行单位,类似于“线程”。每个任务都是我们写的一个函数,FreeRTOS 会负责调度它们运行。
FreeRTOS并不是真正的让多任务同时运行,而是是通过“时间片轮转 + 优先级”快速切换任务,只是看起来像同时运行。
在ESP32中,我们使用 xTaskCreate() 函数来创建任务:
xTaskCreate(TaskFunction, // 参数1:任务函数;
"Task1", // 参数2:任务函数的别名;
1024, // 参数3:任务函数运行时的栈大小(注意不是字节)
NULL, // 参数4:传给任务函数的参数;
1, // 参数5:任务函数的优先级,最高优先级是 (configMAX_PRIORITIES - 1);
NULL); // 参数6:任务句柄;
-
2 .任务状态与生命周期(Task Status)
FreeRTOS 中的任务在运行过程中不是一直处于“运行”状态,而是会根据资源占用情况、时间、事件等在多个状态之间切换。一共有 5 种主要状态:
(1)Created(已创建)
通过 xTaskCreate() 创建后,任务最开始是处于created的状态,如果调度器尚未启动(vTaskStartScheduler()),任务不会立即运行,一旦调度器启动,任务状态转为 Ready;
(2)Ready(就绪)
表示任务已准备好运行,只等着调度器分配 CPU,有多个任务处于就绪态时,调度器会选取最高优先级的任务运行;
(3)Running(运行)
表示任务当前正在使用 CPU,FreeRTOS 是抢占式内核,高优先级任务可以随时打断低优先级任务,同优先级任务之间采用“时间片轮转”机制;
(4)Blocked(阻塞)
如果任务当前正在等待延时或外部事件,则该任务视为处于阻塞状态。就像是任务主动要“休息一段时间”了:比如当我们调用了:
-
vTaskDelay()(延时一段时间);
-
xQueueReceive()(等待队列数据);
-
xSemaphoreTake()(等待信号量);
被阻塞的任务不参与调度器的调度,释放 CPU 给其它任务,阻塞时间结束(如延时完毕,或数据到达)后任务转为 Ready;
(5)Suspended(挂起)
表示任务被“冻结”了,不会运行也不会被调度,需要我们手动调用:
vTaskSuspend(TaskHandle_t handle); // 挂起
vTaskResume(TaskHandle_t handle); // 恢复,调用后才可以进入就绪状态;
挂起态不依赖时间,也不会自动恢复,适合你需要手动控制某任务是否激活的场景(如节电模式)。
-
3.任务调度与调度器机制
调度器(Scheduler)决定了某个特定时刻执行哪个任务,并在任务之间切换。
(1)FreeRTOS默认使用固定优先级的抢占式调度策略,即高优先级任务一旦就绪,会立即打断低优先级任务,获取CPU。这样使得实时性更强,更适用于对时间敏感的场景;
(2)对于同优先级的任务,采用时间片轮转调度(Round Robin Scheduling),它是指当多个任务优先级相同时,FreeRTOS 会让每个任务轮流运行一小段时间(称为一个“时间片”),从而实现“公平调度”。时间片的长度由系统“滴答定时器”控制,默认情况下,每到一个系统滴答(一般为1ms或10ms),调度器就会判断是否要切换任务,如果当前的任务执行时间到了,那就切换到下一个同优先级的任务;
FreeRTOS 调度器的决策顺序:
①优先级不同:等级越高的就绪任务,调度器越优先选它执行。
比如有优先级 3 和优先级 1 的任务都就绪,调度器一定选优先级 3 的任务;
②优先级相同:时间片轮转调度”让它们轮流执行。每个任务获得一个固定的“时间片”(一个或多个 tick),用完就轮到下一个同等优先级的任务;
③任务状态变化触发调度器重新决策:以下情况会触发重新决策:
-
系统 Tick 中断:每次 Tick 到,判断是否需要切换任务;
-
任务调用 vTaskDelay()、taskYIELD():当前任务主动让出 CPU;
-
有任务被唤醒(如中断唤醒、信号量释放):调度器会立即检查是否有更高优先级的任务要抢占;
-
调用 xTaskCreate() 创建了新任务:调度器会立刻评估新任务是否抢占:
关于调度器的启动与关闭:启动调度器的函数是:vTaskStartScheduler();一旦启动,FreeRTOS 就开始自动管理任务运行;FreeRTOS 通常不支持手动关闭调度器(除非特殊配置)
-
4. 系统滴答
(1)Tick(滴答)是FreeRTOS中一个定时节拍,由硬件定时器周期性触发。Tick 通常以固定的时间间隔发生,例如 1ms、10ms 等,这个时间间隔叫做 Tick Rate,用 configTICK_RATE_HZ 来配置。
假设我们创建了一个任务:vTaskDelay(100);
如果 configTICK_RATE_HZ = 1000,则表示 1ms 一次 Tick,100*1ms=100ms,所以这个任务就会被阻塞 100ms,也就是 等 100 个 Tick。
(2)Tick 的来源(硬件定时器):
①在 ARM Cortex-M 等芯片中,Tick 通常由 SysTick 定时器产生;
②在 ESP32 中,Tick 通常由系统定时器或 Timer Group 产生中断。
(3)每一次“滴答"发生时,FreeRTOS的调度器就有机会检查是否有任务状态发生变化,比如:
-
有任务延时结束,转为就绪状态;
-
时间片轮转到下一个任务;
-
执行调度(上下文切换);
-
5. 上下文切换(Context Switch)
(1)上下文切换是指操作系统暂停当前正在运行的任务,把CPU交给另外一个任务运行的过程。在这个过程中,FreeRTOS会做2件事:
① 保存当前任务的上下文(寄存器、堆栈等),以便以后能恢复继续执行;
② 恢复新任务的上下文,让它从上次暂停的地方继续运行;
(2)上下文包含什么?
-
① CPU 寄存器:比如 PC(程序计数器)、SP(堆栈指针)、R0~R15 等;
-
② 堆栈指针:当前任务的局部变量、返回地址等保存在任务栈中;
-
③ PSW(程序状态字):用于标记任务当前的运行状态、优先级等;
FreeRTOS 使用每个任务的“任务栈”来保存这些上下文。
(3) 何时发生上下文切换?
①新任务唤醒且优先级更高时:比如中断唤醒了高优先级任务,当前任务会被打断切换;
②任务主动让出 CPU:比如调用 taskYIELD();
③当前任务进入阻塞/延时:比如调用 vTaskDelay() 后不再就绪,切换至其他就绪任务;
④Tick 中断触发,时间片到:同优先级任务使用完时间片后,轮换执行;
(4)上下文切换的性能开销大吗?
每次切换都需要保存/恢复上下文,存在时间开销(几十us到几百us,取决于架构);如果频繁切换会带来“任务切换负担”,影响实时性和效率,所以 FreeRTOS 推荐尽量不要让任务频繁主动让出 CPU(例如空转 taskYIELD())。
-
6 .空闲任务(Idle Task )与钩子函数(Hook)
(1)在FreeRTOS中,空闲任务是系统自动创建的一个特殊任务,用来确保始终存在一个能够运行的任务;
空闲任务优先级最低(默认为0),当没有其他就绪任务可以运行时,FreeRTOS 调度器就会调度空闲任务来运行;
空闲任务是启动调度器时由系统生成的,不需要你手动创建。它相当于“最后一个候补选手”,当其他任务都在休息或被阻塞时,它就上来占用 CPU,保证系统处于活动状态。
空闲任务可用于资源回收,你删除任务时,它负责清理被删除任务分配的堆栈资源;
空闲任务还可用来定义钩子函数,可以设置空闲任务钩子(vApplicationIdleHook())做低功耗处理或后台任务;
(2)空闲任务钩子函数(Idle Hook) 是什么?
首先,钩子函数(Hook Function)是指你提前定义好的一段函数代码,系统在某个特定时刻自动“调用”你这段代码;钩子函数是回调机制的一种,你不需要手动调用它,系统在某个“时间点”或“事件点”会自动调用,你只要“挂钩”上去(hook),它就会在合适时机被调用。
那空闲任务钩子函数指的是挂在空闲任务里的钩子函数。你可以选择写一段函数,FreeRTOS 会在空闲任务每次运行时自动调用你写的那段函数。
如果把Idle Task 比作一个“保安值班”,没活干时他在岗待命。那 Idle Hook 就像你在保安上岗时给他附带的“备忘清单”——“你要是闲着,就扫扫地、打个日志、测个温湿度什么的。”
(笔记整理中......)