什么是FRTOS?

将FRTOS想象成一个超级高效的”任务管家”,它核心的工作就是让你的单片机能够同时“一心多用”,通过任务调度、消息队列、信号量等机制,来合理地安排多个任务(比如同时读取传感器、刷新屏幕、进行网络通信)的执行顺序和时间,确保那些最紧急的任务能得到优先处理,从而让你的嵌入式项目运行得更可靠、更有序。

FRTOS实现任务跳转,只会执行主函数前半部分,后面只会执行RTOS中的任务

FRTOS基础

什么是任务栈

什么是栈?
栈就是单片机RAM里面一段连续的内存空间,大小一般在启动文件里面或者连接脚本里面定义,通过外在的调用初始化

什么是任务栈?
在江湖中、各路豪杰英雌距地分割。每个人都有自己的地盘。那我内部的吃喝开销不是我自己负责?
同样,栈也是一个个独立的,每个栈都有自己的独立栈空间,只是这个栈空间需要提前定义好,就是一个全局数组

1
2
3
4
5
6
7
8
9
/*定义任务栈的大小*/
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];

#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];

#define TASK3_STACK_SIZE 128
StackType_t Task3Stack[TASK3_STACK_SIZE];

任务函数

任务函数是一个独立的线程,一个完整的嵌入式应用,就是由多个这样的任务函数所组成的

任务函数有两个关键特征:

1.永不返回:通常,任务函数内部是一个无限的循环(for(;;) 或 while(1)),使得这个任务能够持续运行,永远不会return。

必须具有特定的原型:它的返回值必须是 void,并且接受一个 void 类型的参数。*

代码框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void myTaskFunction(void *pvParameters) // 必须符合这个函数原型
{
// 可选的初始化代码


for(;;) // 无限循环,永不返回
{
// 任务的主体工作代码...


// 通常需要调用一个延迟或阻塞函数,如 vTaskDelay(),
// 以便将CPU控制权主动交还给调度器,让其他任务有机会运行。
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延迟1000毫秒
}

// 理论上,如果任务被删除,才会执行到这里。
// 良好的习惯是在这里调用 vTaskDelete(NULL) 来清理任务。
vTaskDelete(NULL);
}

任务函数和普通FRTOS的区别:

1.调用方式:
。普通函数是通过程序中的显式调用来执行的,执行完后返回到调用点。
。FreeRTOS任务在创建后由操作系统调度器管理,并在特定条件下被运行,任务间的切换由操作系统完成。
2.运行方式:
。普通函数的执行是顺序且阻塞的。
。任务是非阻塞的,支持多任务并发。操作系统会根据任务优先级和时间片调度机制在不同任务之间切换。
3.上下文管理:
普通函数与调用点共享相同的堆栈和上下文。
每个任务有独立的堆栈空间和上下文信息,操作系统在任务切换时会保存和恢复这些信息。

任务函数就是你在FreeRTOS中为实现特定功能而写的“灵魂”,而之前你提供的“任务栈”则是这个灵魂运行时所必需的“身体和记忆空间”。调度器负责管理所有这些“灵魂”,让它们和谐地共享一个CPU。

什么是TCB?

TCB可以把它想象成一个任务的 “身份证” 或 “个人档案”。

TCB 是一个数据结构(一个 struct),由 FreeRTOS 内核在创建任务时自动分配。它包含了管理和调度一个任务所需要的 所有信息。

TCB里面有什么?
任务状态:当前任务是就绪、运行、阻塞还是挂起状态。

任务优先级:决定了调度器先运行哪个任务。

栈指针:指向任务的栈顶(就是你之前代码里定义的 Task1Stack 等数组),用于记录任务运行时和切换时的上下文(如变量、返回地址等)。

任务入口函数指针:指向你写的那个任务函数,告诉内核这个任务要执行什么代码。

任务名:用于调试时识别任务。

事件列表:记录任务正在等待什么事件(比如信号量、消息队列等)。

其他用于链表、时间戳等的内核管理信息。

TCB 是 FreeRTOS 用来代表一个任务的内核对象,是系统感知和管理任务的唯一依据。 你创建一个任务时,系统就会为它分配一个 TCB;删除一个任务时,就会释放它的 TCB

采用STM32F104(蓝桥杯开发板)+ CUBEMX学习STM32

CUBEMX中生成的FRTOS架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

//用于开启任务调度,初始化调度器
osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */


/*FRTOS.c的架构*/
//创建的两个任务
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

/* creation of myTask02 */
myTask02Handle = osThreadNew(StartTask02, NULL, &myTask02_attributes);


//osThreadNew(StartTask02, NULL, &myTask02_attributes);函数怎么用?

osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr)


osThreadFunc_t func:任务函数 一个函数指针typedef void ,指向任务函数(*osThreadFunc_t) (void *argument);

void *argument:传递给任务函数的参数
const osThreadAttr_t *attr 设置任务属性,可以设置为空
返回值:osThreadId_t任务的ID,通过任务的ID找到对应的ID

任务函数是一个死循环,因为任务需要一直保持运行

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工程
蓝桥杯嵌入式原理图:

点亮一个LED

CUBEMX配置:FRTOS第一次配置

如果出现以下错误找不到头文件错误

STM32错误

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

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14

void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */\
HAL_UART_Transmit(&huart1, (uint8_t *)"hello wi99ndows!\r\n", 16 , 0xffff);
for(;;)
{
osDelay(50);
}
/* USER CODE END StartDefaultTask */
}
//直接在任务轮询里面添加即可

STM

CUBEMX任务名介绍

STM

第二行Priority用来设置任务优先级
第三行:任务栈大小
第四行:任务的入口函数
第六行:传递给任务函数的参数
第七行:任务创建的方式

1
2
3
4
5
6
7
8
9
10
11
12
13

typedef struct {
const char *name; ///< name of the thread
uint32_t attr_bits; ///< attribute bits
void *cb_mem; ///< memory for control block
uint32_t cb_size; ///< size of provided memory for control block
void *stack_mem; ///< memory for stack
uint32_t stack_size; ///< size of stack
osPriority_t priority; ///< initial thread priority (default: osPriorityNormal)
TZ_ModuleId_t tz_module; ///< TrustZone module identifier
uint32_t reserved; ///< reserved (must be 0)
} osThreadAttr_t;

使用osThreadNew函数传参
我们看到osThreadNew函数第二参数, 是传入任务函数的参数

实现功能:利用串口发送六次后停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

int c=6;
defaultTaskHandle = osThreadNew(StartTask02, (void*)&c, &defaultTask_attributes);
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
/* Infinite loop */
int i=0;
int* cnt =(int*)argument;
for(;;)
{
for(i;i<*cnt;i++)
{
printf("hello world");
printf("%d", i);
osDelay(1000);
}
osDelay(1000);
}
/* USER CODE END StartTask02 */
}


任务优先级的使用

和中断不同的是FRTOS中的优先级越高,越先使用

任务的栈

函数调用栈

每当一个函数被调用时,函数的返回地址,参数以及局部变量会被保持在栈上,函数执行完后,栈会恢复道调用之前的状态

创建任务

动态创建任务与静态创建任务

动态和静态两种创建任务的方式这是在FreeRTOS 中创建任务的两种内存管理方式。

动态创建任务:FreeRTOS 内核自动分配任务栈和 TCB 所需的内存

优点不需要手动管理内存,适合任务数量不确定或频繁变化的情况。
缺点可能会生成内存碎片

静态创建任务:用户手动分配内存,包括 TCB 和 栈空间

无内存碎片,适合内存受限系统
要手动管理内存

CUBEMX生成的动态底层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

(xTaskCreate ((TaskFunction_t)func, name, (uint16_t)stack, argument, prio, &hTask) != pdPASS)


BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )

//(TaskFunction_t)func 任务函数指针
//const char * const pcName 任务名称
//const configSTACK_DEPTH_TYPE usStackDepth 任务栈大小
//void * const pvParameters 任务参数
//UBaseType_t uxPriority 任务优先级
//TaskHandle_t * const pxCreatedTask 任务句柄,通过任务句柄可以找到对应的任务
//BaseType_t 返回值,表示函数是否调用成功 成功返回pdPASS,失败返回pdFAIL

如何动态创建任务函数
1
2
3
4
5
6
7
8
9
10
11
12

TaskHandle_t MyTaskHandle;
xTaskCreate(StartMyTask, "MyTask", 256, (void*)&task_parameter, tskIDLE_PRIORITY + 2, &MyTaskHandle);

StartMyTask(void *pvParameters)
{
for(;;)
{

osdelay();
}
}

静态创建任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


StaticTask_t MyTaskTCB;
StackType_t MyTaskStack[256];

TaskHandle_t MyTaskHandle = xTaskCreateStatic(
StartMyTask, // 任务函数指针
"MyTask", // 任务名称
256, // 任务栈大小
(void*)&task_parameter,// 任务参数
tskIDLE_PRIORITY + 2, // 任务优先级
MyTaskStack, // 栈缓冲区
&MyTaskTCB // TCB缓冲区
);

静态创建任务和动态创建任务的区别:
动态创建只需指定栈大小,系统自动分配内存;静态创建必须额外提供栈和任务控制块的具体内存地址,由开发者预先分配好所有内存空间。

如何实现任务轮询

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
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
37

/* 调度模式配置 */
#define configUSE_PREEMPTION 1 /* 1=启用抢占式调度,0=协作式调度 */

/* 内存分配方式配置 */
#define configSUPPORT_STATIC_ALLOCATION 1 /* 1=支持静态内存分配(用户提供内存) */
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 1=支持动态内存分配(系统自动分配) */

/* 钩子函数配置 */
#define configUSE_IDLE_HOOK 0 /* 1=启用空闲任务钩子函数,0=禁用 */
#define configUSE_TICK_HOOK 0 /* 1=启用系统心跳钩子函数,0=禁用 */

/* 系统时钟配置 */
#define configCPU_CLOCK_HZ ( SystemCoreClock ) /* CPU时钟频率,通常等于系统核心时钟 */
#define configTICK_RATE_HZ ((TickType_t)1000) /* 系统心跳频率=1000Hz,即1个tick=1ms */

/* 任务系统配置 */
#define configMAX_PRIORITIES (56) /* 最大优先级数量,范围1-255 */
#define configMINIMAL_STACK_SIZE ((uint16_t)128) /* 空闲任务的最小栈大小(字) */
#define configTOTAL_HEAP_SIZE ((size_t)3072) /* 动态内存分配的总堆大小=3KB */
#define configMAX_TASK_NAME_LEN ( 16 ) /* 任务名称最大长度 */

/* 调试和跟踪配置 */
#define configUSE_TRACE_FACILITY 1 /* 1=启用可视化跟踪调试功能 */

/* 时间系统配置 */
#define configUSE_16_BIT_TICKS 0 /* 0=使用32位tick计数器,1=使用16位(已过时) */

/* 内核对象配置 */
#define configUSE_MUTEXES 1 /* 1=启用互斥锁功能 */
#define configQUEUE_REGISTRY_SIZE 8 /* 队列注册表大小,用于调试 */
#define configUSE_RECURSIVE_MUTEXES 1 /* 1=启用递归互斥锁 */
#define configUSE_COUNTING_SEMAPHORES 1 /* 1=启用计数信号量 */

/* 任务选择优化 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 0 /* 1=使用端口优化的任务选择(依赖硬件),0=通用C代码 */

FRTOS任务中的状态

FreeRTOS 任务拥有四种核心状态:运行态(正在CPU执行)、就绪态(已准备就绪,等待调度器分配CPU)、阻塞态(因等待事件或延时而暂停,不参与调度)以及挂起态(被强制暂停,只能手动恢复),任务根据系统调度和事件触发在这些状态间动态切换,确保多任务高效并发执行

111

就绪状态(Ready)

状态描述:任务已经准备就绪,具备所有运行条件,正在等待调度器分配 CPU 时间

处于就绪态

任务刚创建完成时

从阻塞态恢复(事件发生/延时到期)

从挂起态恢复

被更高优先级任务抢占后

阻塞态(Blocked)

状态描述:任务因等待特定事件而暂停执行,不参与调度,直到等待的条件满足。

特点:
❌ 不参与调度

✅ 自动恢复(事件触发)

💤 不消耗CPU资源

常见阻塞条件:信号量,消息队列,定时器等

运行态(Running)

状态描述:任务当前正在 CPU 上执行代码,占用处理器资源。

被调度器从就绪态选中执行

当前正在执行的任务

挂起态(Suspended)

没有办法进入就绪状态,任务被强制暂停执行,不参与任何调度,只能手动恢复。

动态表示四种状态

空闲任务和空闲钩子函数

空闲任务:空闲任务 是 FreeRTOS 自动创建的一个系统任务,它始终处于最低优先级,当且仅当系统中所有用户任务都处于阻塞或挂起状态时,调度器才会切换到空闲任务来运行,它的存在确保了 CPU 永远不会停止执行,同时为开发者提供了通过空闲任务钩子函数在系统“空闲”时执行后台维护或进入低功耗模式的机会

空闲钩子函数

什么是空闲钩子函数?

空闲钩子函数的作用:挂载到空闲任务上的自定义函数,在系统空闲时自动执行

如何开启空闲钩子函数?
空闲钩子函数的开启

任务控制块
TCB的基本概念

TCB(任务控制块) 是 FreeRTOS 中用于描述和管理一个任务所有信息的数据结构,每个任务都有自己独立的 TCB,相当于任务的”身份证”或”人事档案”

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
typedef struct tskTaskControlBlock
{
/* 1. 栈指针 */
volatile StackType_t *pxTopOfStack; // 当前栈顶位置

/* 2. 任务状态 */
eTaskState eCurrentState; // 当前状态:就绪、运行、阻塞、挂起

/* 3. 任务优先级 */
UBaseType_t uxPriority; // 任务优先级 (0 = 最低)

/* 4. 任务函数和参数 */
TaskFunction_t pvTaskCode; // 指向任务函数的指针
void *pvParameters; // 传递给任务的参数

/* 5. 任务名(用于调试) */
const char *pcTaskName; // 任务名称字符串

/* 6. 事件相关 */
void *pvEventList; // 事件列表项(等待队列、信号量等)

/* 7. 时间管理 */
TickType_t xTicksToDelay; // 剩余延时时间(如果处于阻塞态)

/* 8. 栈信息 */
StackType_t *pxStack; // 栈起始地址
uint16_t usStackDepth; // 栈深度

/* 9. 链表指针 */
struct tskTaskControlBlock *pxNextTask; // 就绪/阻塞链表中的下一个任务
struct tskTaskControlBlock *pxPreviousTask; // 就绪/阻塞链表中的前一个任务

} tskTCB;

任务的删除

FreeRTOS 实现任务删除主要通过 vTaskDelete() 函数完成,这是一个安全、有序的资源清理过程。

1
2
3
4

vTaskDelete(xTaskHandle); // xTaskHandle 是要删除的任务句柄
vTaskDelete(NULL);
// 参数为NULL表示删除自己

当任务被删除后,就会被移除任务调度列表
表示执行五次后,删除自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void 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 中最重要的任务间通信机制,它提供了安全、可靠的数据传递方式,支持异步通信和数据缓冲。

比喻理解:
就像邮局的信箱:发送者投递,接收者取件

就像生产线传送带:生产者放产品,消费者取产品

队列思维导图:
FRTOS

信号量

信号量: 是 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
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

osSemaphoreId_t osSemaphoreNew (uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr)


//表示:
osSemaphoreId_t osSemaphoreNew (
uint32_t max_count, // 最大计数值
uint32_t initial_count, // 初始计数值
const osSemaphoreAttr_t *attr // 信号量属性
);



//申请信号量,请求信号量
osStatus_t osSemaphoreAcquire (
osSemaphoreId_t semaphore_id, // 信号量句柄
uint32_t timeout // 超时时间
);
//返回值代表是否请求成功,请求信号量完成后,信号量就会减1。

//释放信号量
osStatus_t osSemaphoreRelease (
osSemaphoreId_t semaphore_id // 信号量句柄
);

//返回值代表是否成功,释放信号量后,信号量就会加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 */
}

信号量和队列的关系