FRTOS学习
什么是FRTOS?
将FRTOS想象成一个超级高效的”任务管家”,它核心的工作就是让你的单片机能够同时“一心多用”,通过任务调度、消息队列、信号量等机制,来合理地安排多个任务(比如同时读取传感器、刷新屏幕、进行网络通信)的执行顺序和时间,确保那些最紧急的任务能得到优先处理,从而让你的嵌入式项目运行得更可靠、更有序。
FRTOS实现任务跳转,只会执行主函数前半部分,后面只会执行RTOS中的任务
FRTOS基础
什么是任务栈
什么是栈?
栈就是单片机RAM里面一段连续的内存空间,大小一般在启动文件里面或者连接脚本里面定义,通过外在的调用初始化什么是任务栈?
在江湖中、各路豪杰英雌距地分割。每个人都有自己的地盘。那我内部的吃喝开销不是我自己负责?
同样,栈也是一个个独立的,每个栈都有自己的独立栈空间,只是这个栈空间需要提前定义好,就是一个全局数组
1 | /*定义任务栈的大小*/ |
任务函数
任务函数是一个独立的线程,一个完整的嵌入式应用,就是由多个这样的任务函数所组成的
任务函数有两个关键特征:
1.永不返回:通常,任务函数内部是一个无限的循环(for(;;) 或 while(1)),使得这个任务能够持续运行,永远不会return。
必须具有特定的原型:它的返回值必须是 void,并且接受一个 void 类型的参数。*
代码框架:
1 |
|
任务函数和普通FRTOS的区别:
1.调用方式:
。普通函数是通过程序中的显式调用来执行的,执行完后返回到调用点。
。FreeRTOS任务在创建后由操作系统调度器管理,并在特定条件下被运行,任务间的切换由操作系统完成。
2.运行方式:
。普通函数的执行是顺序且阻塞的。
。任务是非阻塞的,支持多任务并发。操作系统会根据任务优先级和时间片调度机制在不同任务之间切换。
3.上下文管理:
普通函数与调用点共享相同的堆栈和上下文。
每个任务有独立的堆栈空间和上下文信息,操作系统在任务切换时会保存和恢复这些信息。任务函数就是你在FreeRTOS中为实现特定功能而写的“灵魂”,而之前你提供的“任务栈”则是这个灵魂运行时所必需的“身体和记忆空间”。调度器负责管理所有这些“灵魂”,让它们和谐地共享一个CPU。
什么是TCB?
TCB可以把它想象成一个任务的 “身份证” 或 “个人档案”。
TCB 是一个数据结构(一个 struct),由 FreeRTOS 内核在创建任务时自动分配。它包含了管理和调度一个任务所需要的 所有信息。
TCB里面有什么?
任务状态:当前任务是就绪、运行、阻塞还是挂起状态。
任务优先级:决定了调度器先运行哪个任务。
栈指针:指向任务的栈顶(就是你之前代码里定义的 Task1Stack 等数组),用于记录任务运行时和切换时的上下文(如变量、返回地址等)。
任务入口函数指针:指向你写的那个任务函数,告诉内核这个任务要执行什么代码。
任务名:用于调试时识别任务。
事件列表:记录任务正在等待什么事件(比如信号量、消息队列等)。
其他用于链表、时间戳等的内核管理信息。
TCB 是 FreeRTOS 用来代表一个任务的内核对象,是系统感知和管理任务的唯一依据。 你创建一个任务时,系统就会为它分配一个 TCB;删除一个任务时,就会释放它的 TCB
采用STM32F104(蓝桥杯开发板)+ CUBEMX学习STM32
CUBEMX中生成的FRTOS架构
1 |
|
任务函数是一个死循环,因为任务需要一直保持运行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */\
for(;;)
{
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
osDelay(10)//休眠函数,释放CPU资源
使用FRTOS实现串口发送
配置链接:STM32CubeMX生成第一个freeRTOS工程
蓝桥杯嵌入式原理图:

CUBEMX配置:
如果出现以下错误找不到头文件错误

那么多半是在FRTOS设置使用的新的库,我只需要把这一项关闭就行

1 |
|

CUBEMX任务名介绍

第二行Priority用来设置任务优先级
第三行:任务栈大小
第四行:任务的入口函数
第六行:传递给任务函数的参数
第七行:任务创建的方式
1 |
|
使用osThreadNew函数传参
我们看到osThreadNew函数第二参数, 是传入任务函数的参数
实现功能:利用串口发送六次后停止
1 |
|
任务优先级的使用
和中断不同的是FRTOS中的优先级越高,越先使用
任务的栈
函数调用栈
每当一个函数被调用时,函数的返回地址,参数以及局部变量会被保持在栈上,函数执行完后,栈会恢复道调用之前的状态
创建任务
动态创建任务与静态创建任务
动态和静态两种创建任务的方式这是在FreeRTOS 中创建任务的两种内存管理方式。
动态创建任务:FreeRTOS 内核自动分配任务栈和 TCB 所需的内存
优点不需要手动管理内存,适合任务数量不确定或频繁变化的情况。
缺点可能会生成内存碎片静态创建任务:用户手动分配内存,包括 TCB 和 栈空间
无内存碎片,适合内存受限系统
要手动管理内存
CUBEMX生成的动态底层代码:
1 |
|
如何动态创建任务函数
1 |
|
静态创建任务
1 |
|
静态创建任务和动态创建任务的区别:
动态创建只需指定栈大小,系统自动分配内存;静态创建必须额外提供栈和任务控制块的具体内存地址,由开发者预先分配好所有内存空间。
如何实现任务轮询
FreeRTOS 通过 基于优先级的抢占式调度 实现任务轮询:调度器借助硬件定时器中断周期性地触发,检查所有任务状态,始终让 最高优先级的就绪任务 获得 CPU 使用权;每个任务必须 主动调用如 osDelay 等阻塞函数 来让出 CPU,从而保证同等优先级的任务能通过 时间片轮转 公平共享处理器,实现多任务的并发执行与平滑切换
比喻:FreeRTOS 就像一个永不疲倦的超级指挥家,它手握一个精准的节拍器(系统心跳),指挥着整个乐队(多个任务)。每一位乐手(任务)都必须在演奏完自己的一个小节后,主动停下来(调用如 osDelay 这样的函数),把舞台让给指挥。指挥则遵循一条黄金法则:谁的任务更紧急、谁等得最久,就让谁接下来表演。通过这种方式,指挥家确保了所有乐手都能轮流上场,既不会有人一直霸占麦克风,也不会有人被彻底冷落,从而奏出一曲和谐流畅的多任务交响乐
任务调度算法
抢占式任务调度
抢占式任务调度运行在任务执行期间,基于任务优先级动态抢占CPU资源,暂停当前任务并切换到其他任务,FROTS可以在任何时候根据任务优先级来决定哪个任务应当运行
抢占式任务调度实验:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */\
//HAL_UART_Transmit(&huart1, (uint8_t *)"hello wi99ndows!\r\n", 16 , 0xffff);
for(;;)
{
printf("text\n\r");
myTask02Handle = osThreadNew(StartTask02, NULL, &myTask02_attributes);
printf("11212\n\r");
osDelay(2000);
}
/* USER CODE END StartDefaultTask */
}
/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask02 */
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
/* Infinite loop */
int i=0;
int* cnt =(int*)argument;
for(;;)
{
printf("hello world\n\r");
osDelay(2000);
}
/* USER CODE END StartTask02 */
}
关于FreeRTOSConfig.h
FreeRTOSConfig.h文件是FreeRTOS的配置文件,它允许用户根据具体的应用需求来定制和优化FreeRTOS的运行时行为。
1 |
|
FRTOS任务中的状态
FreeRTOS 任务拥有四种核心状态:运行态(正在CPU执行)、就绪态(已准备就绪,等待调度器分配CPU)、阻塞态(因等待事件或延时而暂停,不参与调度)以及挂起态(被强制暂停,只能手动恢复),任务根据系统调度和事件触发在这些状态间动态切换,确保多任务高效并发执行

就绪状态(Ready)
状态描述:任务已经准备就绪,具备所有运行条件,正在等待调度器分配 CPU 时间
处于就绪态
任务刚创建完成时
从阻塞态恢复(事件发生/延时到期)
从挂起态恢复
被更高优先级任务抢占后
阻塞态(Blocked)
状态描述:任务因等待特定事件而暂停执行,不参与调度,直到等待的条件满足。
特点:
❌ 不参与调度
✅ 自动恢复(事件触发)
💤 不消耗CPU资源
常见阻塞条件:信号量,消息队列,定时器等
运行态(Running)
状态描述:任务当前正在 CPU 上执行代码,占用处理器资源。
被调度器从就绪态选中执行
当前正在执行的任务
挂起态(Suspended)
没有办法进入就绪状态,任务被强制暂停执行,不参与任何调度,只能手动恢复。
空闲任务和空闲钩子函数
空闲任务:空闲任务 是 FreeRTOS 自动创建的一个系统任务,它始终处于最低优先级,当且仅当系统中所有用户任务都处于阻塞或挂起状态时,调度器才会切换到空闲任务来运行,它的存在确保了 CPU 永远不会停止执行,同时为开发者提供了通过空闲任务钩子函数在系统“空闲”时执行后台维护或进入低功耗模式的机会

什么是空闲钩子函数?
空闲钩子函数的作用:挂载到空闲任务上的自定义函数,在系统空闲时自动执行
如何开启空闲钩子函数?

任务控制块
TCB的基本概念
TCB(任务控制块) 是 FreeRTOS 中用于描述和管理一个任务所有信息的数据结构,每个任务都有自己独立的 TCB,相当于任务的”身份证”或”人事档案”
1 | typedef struct tskTaskControlBlock |
任务的删除
FreeRTOS 实现任务删除主要通过 vTaskDelete() 函数完成,这是一个安全、有序的资源清理过程。
1 |
|
当任务被删除后,就会被移除任务调度列表
表示执行五次后,删除自己:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */\
//HAL_UART_Transmit(&huart1, (uint8_t *)"hello wi99ndows!\r\n", 16 , 0xffff);
int i=0;
for(;;)
{
for(i=0;i<5;i++){
printf("text\n\r");
printf("11212\n\r");
osDelay(2000);}
vTaskDelete(NULL);
}
/* USER CODE END StartDefaultTask */
}
同步和互斥
同步和互斥是多任务系统中解决任务协作和资源共享问题的两种核心机制
同步
同步 = 任务间的协作机制,确保多个任务按照预期的顺序执行
就像工厂的流水线:A工序完成后,B工序才能开始
就像接力赛跑:前一个选手交棒,后一个选手才能起跑
互斥
互斥 = 对共享资源的独占访问,防止多个任务同时访问临界资源。
防止多个任务同时修改同一个全局变量
保护共享硬件资源(如串口、SPI、I2C等)
避免数据竞争和不一致
队列
队列 是 FreeRTOS 中最重要的任务间通信机制,它提供了安全、可靠的数据传递方式,支持异步通信和数据缓冲。
比喻理解:
就像邮局的信箱:发送者投递,接收者取件就像生产线传送带:生产者放产品,消费者取产品
队列思维导图:

信号量
信号量: 是 FreeRTOS 中用于任务同步和资源管理的核心机制,它可以看作是一个计数器,用于控制对共享资源的访问或协调任务间的执行顺序
二进制信号量:值只有 0 和 1,用于互斥访问
void StartDefaultTask(void argument)
{
/ USER CODE BEGIN StartDefaultTask /
/ Infinite loop /\
//HAL_UART_Transmit(&huart1, (uint8_t )”hello wi99ndows!\r\n”, 16 , 0xffff);
for(;;)
{
//1.使用前先要获取信号量
osSemaphoreAcquire(myBinarySem01Handle,1000);
//2.操作信号量
data++;
printf(“data:%d\r\n”,data);
//3.释放信号量
osSemaphoreRelease(myBinarySem01Handle);
osDelay(100);
}
/ USER CODE END StartDefaultTask /
}:值可以大于 1,用于资源池管理
什么是共享资源:
共享资源:全局变量,串口,定时器
STM32CUBEMX创建信号量的封装函数:
1 |
|
用信号量保护硬件资源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */\
//HAL_UART_Transmit(&huart1, (uint8_t *)"hello wi99ndows!\r\n", 16 , 0xffff);
for(;;)
{
//1.使用前先要获取信号量
osSemaphoreAcquire(myBinarySem01Handle,1000);
//2.操作信号量
data++;
printf("data:%d\r\n",data);
//3.释放信号量
osSemaphoreRelease(myBinarySem01Handle);
osDelay(100);
}
/* USER CODE END StartDefaultTask */
}





