freeRTOS
FreeRTOS笔记
🎯1.创建项目模板
一、项目基础配置 1、选用stm32F103C8T6芯片
2、设置Rcc 高速时钟为 陶瓷晶振
3、设置SYS Debug为 Serial Wire、基准时钟为TIM4
4、设置HCLK为72M
5、找到Middleware and Software Packs(中间件和软件包)、选择FreeRTOS、选择CMSIS V2、配置参数保持默认
6、设置工程名、配置IDE为MDK-ARM
7、 在Code Generator中设置每个外设的初始代码的.c .h 分开(Generate peripheral initialization as a pair of'c/.h' files per periphera) 、其他默认
8、固件包版本设置为1.85 (为了防止 在编译时出现这个错误 #include CMSIS_device_header )
9、生成代码
10、选择下载器
11、设置复位后运行
🎯2.创建任务
- 在 STM32 + FreeRTOS 项目中,任务(Task)通常不会直接写在
main.c
里,而是 分模块存放,以保持代码的整洁和可维护性。通常会按照以下方式组织任务代码:
方案 1:把任务代码放在单独的 C 文件
这种方式最常见,适用于多个任务的管理。
📌 步骤:
- 在
Src/
目录下创建task_xxx.c
,在Inc/
目录下创建task_xxx.h
- 在
task_xxx.c
里写任务代码 - 在
task_xxx.h
里声明任务函数 - 在
main.c
里调用xTaskCreate()
创建任务
示例:创建 LED 任务
📌 目录结构
css复制编辑📁 Core
├── 📂 Inc
│ ├── main.h
│ ├── task_led.h ✅ LED 任务头文件
├── 📂 Src
│ ├── main.c
│ ├── task_led.c ✅ LED 任务实现文件
✅ 任务文件:task_led.h
#ifndef __TASK_LED_H
#define __TASK_LED_H
// 定义任务函数
void LED_01(void *pvParameters);
void LED_02(void *pvParameters);
void LED_03(void *pvParameters);
#endif /* __TASK_LED_H */
✅ 任务实现文件:task_led.c
#include "task_led.h"
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "gpio.h"
#include "OLED.h"
// 实现任务函数
void LED_01(void *pvParameters)
{
OLED_ShowString(0,0,"PC13:",OLED_6X8);
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);
OLED_ShowBinNum(32,0,HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13),1,OLED_6X8);
OLED_Update();
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);
OLED_ShowBinNum(32,0,HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13),1,OLED_6X8);
OLED_Update();
}
}
void LED_02(void *pvParameters)
{
OLED_ShowString(0,10,"PA0:",OLED_6X8);
while(1)
{
vTaskDelay(pdMS_TO_TICKS(2000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);
OLED_ShowBinNum(25,10,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0),1,OLED_6X8);
OLED_Update();
vTaskDelay(pdMS_TO_TICKS(2000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);
OLED_ShowBinNum(25,10,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0),1,OLED_6X8);
OLED_Update();
}
}
void LED_03(void *pvParameters)
{
OLED_ShowString(0,20,"PA1:",OLED_6X8);
while(1)
{
vTaskDelay(pdMS_TO_TICKS(3000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
OLED_ShowBinNum(25,20,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1),1,OLED_6X8);
OLED_Update();
vTaskDelay(pdMS_TO_TICKS(3000)); // 延时 500ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
OLED_ShowBinNum(25,20,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1),1,OLED_6X8);
OLED_Update();
}
}
✅ 使用 freertos.c
统一管理任务
#include "task_led.h" // 引入 LED 任务头文件
void MX_FREERTOS_Init(void) {
/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
//此处用来获取任务句柄,defaultTaskHandle是默认任务的句柄
/* USER CODE BEGIN RTOS_THREADS */
xTaskCreate(LED_01,"LED_01",128,NULL,1,NULL);
xTaskCreate(LED_02,"LED_02",128,NULL,1,NULL);
xTaskCreate(LED_03,"LED_03",128,NULL,1,NULL);
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
/* USER CODE BEGIN RTOS_EVENTS */
/* add events, ... */
/* USER CODE END RTOS_EVENTS */
}
:
📌 xTaskCreate()
函数详解(FreeRTOS 任务创建)
xTaskCreate()
是 FreeRTOS 中的核心函数之一,用于创建一个任务(Task),并将其添加到调度器中。
✅ xTaskCreate()
函数原型
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务函数指针
const char *const pcName, // 任务名称(调试用)
const configSTACK_DEPTH_TYPE usStackDepth, // 任务栈大小
void *pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄(可选)
);
✅ xTaskCreate()
参数详解
参数 | 类型 | 作用 |
---|---|---|
pxTaskCode |
TaskFunction_t |
任务函数指针,即该任务的代码逻辑 |
pcName |
const char * |
任务名称(仅用于调试,在 FreeRTOS+Trace 中可见) |
usStackDepth |
configSTACK_DEPTH_TYPE |
任务栈大小,单位是 字(一般一个字 = 4 字节) |
pvParameters |
void * |
传递给任务的参数,若不需要参数则填 NULL |
uxPriority |
UBaseType_t |
任务优先级(数值越大优先级越高) |
pxCreatedTask |
TaskHandle_t * |
TCB结构体指针,任务句柄(用于删除任务、挂起任务等,若不需要可填 NULL ) |
任务优先级注意:
- FreeRTOS 任务优先级范围通常是 0 ~ (configMAX_PRIORITIES - 1),其中
configMAX_PRIORITIES
由 FreeRTOSConfig.h 定义。- 数值越大,优先级越高。
✅ xTaskCreate()
返回值
xTaskCreate()
返回值是 BaseType_t
类型:
返回值 | 含义 |
---|---|
pdPASS |
任务创建成功 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY |
内存不足,任务创建失败 |
2.2 一个函数创建多个任务
在 FreeRTOS 中,我们可以使用同一个函数创建多个任务,只需要在创建任务时传递不同的参数,或者使用不同的任务句柄。这样可以让多个任务运行相同的代码,但处理不同的数据或拥有不同的优先级。
- 使用参数区分任务
示例:创建 3 个任务,每个任务有不同的 ID
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 任务函数
void vTaskFunction(void *pvParameters)
{
int taskID = (int)pvParameters; // 获取任务 ID
while (1)
{
printf("Task %d is running\n", taskID);
vTaskDelay(pdMS_TO_TICKS(1000)); // 任务延时 1 秒
}
}
void app_main(void)
{
for (int i = 0; i < 3; i++)
{
// 创建多个任务,传递不同的参数
xTaskCreate(vTaskFunction, "Task", 128, (void *)i, 1, NULL);
}
}
解释
vTaskFunction()
是通用的任务函数,每个任务使用不同的 ID 作为参数。xTaskCreate()
在循环中被调用 3 次,创建 3 个相同的任务。- 每个任务都会打印自己的
taskID
,区别不同的任务。
输出
Task 0 is running
Task 1 is running
Task 2 is running
Task 0 is running
Task 1 is running
Task 2 is running
...
🎯3.堆和栈
📌3.1 通俗理解堆和栈
想象你在一个大办公室里工作,办公桌上有两个关键区域:
- 栈(Stack):你的办公桌 🖥️
- 堆(Heap):公司仓库 📦
🔹 栈 = 你的办公桌(任务的私人空间)
栈就像是你自己的办公桌,每个任务(员工)都有 自己的一张桌子。
- 桌子大小固定,不能随意扩展(任务创建时就分配了固定大小的栈)。
- 桌子上存放你当前要用的东西(局部变量、函数调用信息)。
- 工作时,你可以不断往桌子上放资料(压栈),用完就收起来(出栈)。
- 但如果资料太多,桌子放不下了(栈溢出),你的工作就会崩溃! 🚨
💡 栈的特点:
- 任务私有,每个任务的栈是独立的,互不干扰。
- 适合存放短期数据,比如局部变量和函数调用信息。
- 任务执行完后,栈自动清理,不需要手动回收。
🔹 堆 = 公司仓库(共享资源)
堆就像是公司的仓库,所有员工(任务)都可以申请东西(内存)。
- 堆是共享的,不属于任何一个任务。
- 你可以向仓库申请一堆文件(动态内存分配),但得记得还回去(释放内存),否则仓库会被堆满(内存泄漏)。
- 如果仓库被挤满了,新申请的文件就没地方放了(内存不足)。
- 仓库里的东西不会自动清理,你不手动归还就会一直占着地方。
💡 堆的特点:
- 适合存放长期需要的数据,比如任务栈(动态分配)、队列、消息、信号量等。
- 需要手动管理(
pvPortMalloc()
申请,vPortFree()
释放)。 - 用得不好会有 碎片化问题(内存被不规则占用,导致剩余空间无法使用)。
🔹 栈和堆的关键区别
对比项 | 栈(办公桌) 🖥️ | 堆(公司仓库) 📦 |
---|---|---|
空间大小 | 固定,创建任务时分配 | 可变,可以动态申请 |
数据存储 | 局部变量、函数调用信息 | 任务、队列、信号量等 RTOS 对象 |
分配方式 | 任务创建时一次性分配 | 运行时动态分配 |
访问权限 | 任务私有 | 全局共享 |
释放方式 | 任务结束后自动清理 | 需要手动释放 |
🔹 举个例子
你是一个软件工程师,正在用 FreeRTOS 开发一个智能灯控系统:
1️⃣ 你在自己的办公桌(栈)上写代码,放着笔记和一杯咖啡 ☕(局部变量)。 2️⃣ 突然老板让你做一个大项目,你去公司仓库(堆)借了台高性能服务器 🖥️(动态分配内存)。 3️⃣ 工作完成后,你把笔记收起来(栈自动清理),但你忘了归还服务器 🏢💻(内存泄漏)。 4️⃣ 时间长了,仓库被各种没归还的设备占满了,公司买不起新设备,系统崩溃! ❌(堆耗尽)。
所以 栈要足够大避免溢出,堆要及时释放避免内存泄漏!
📌3.2 以函数调用和局部变量来理解栈
3.2.1 函数调用是如何使用栈的?
当函数被调用时,CPU 会把一些关键信息存入栈,然后跳转到函数内部执行。主要存入 栈帧(Stack Frame) 的内容包括:
- 返回地址(LR) → 记录调用者的位置,以便函数执行完后能返回正确的地方。
- 传递参数 → 如果参数太多,部分参数也会存入栈。
- 局部变量 → 在函数内部声明的变量,在栈上分配空间。
代码示例
c复制编辑void funcB() {
int b = 20; // b 是局部变量,存入栈
}
void funcA() {
int a = 10; // a 也是局部变量,存入栈
funcB(); // 调用 funcB
}
int main() {
funcA(); // 调用 funcA
}
3.2.2 栈是如何工作的?
函数调用时,CPU 会创建 栈帧,执行完后再销毁,像“压栈”和“弹栈”一样。
🔹(1)main() 调用 funcA()
main()
先执行,funcA()
被调用时,创建 funcA 的栈帧:
markdown复制编辑| |
| a=10 | ← funcA 的局部变量
| 返回地址| ← LR(返回 main())
|_________| ← 栈顶
🔹(2)funcA() 调用 funcB()
funcB()
被调用时,会创建 funcB 的栈帧:
markdown复制编辑| |
| b=20 | ← funcB 的局部变量
| 返回地址| ← LR(返回 funcA())
|_________| ← 栈顶
🔹(3)funcB() 执行完毕,弹出栈帧
funcB()
结束,恢复 funcA 的栈帧:
markdown复制编辑| |
| a=10 | ← 仍然在栈上
| 返回地址| ← LR(返回 main())
|_________| ← 栈顶
🔹(4)funcA() 执行完毕,弹出栈帧
funcA()
结束,恢复main()
:
markdown复制编辑| |
| 返回地址| ← 回到 main()
|_________| ← 栈顶
3.2.3 局部变量为什么存入栈?
- 局部变量的作用范围(生命周期) 仅限于函数内部,函数执行完毕后,局部变量就会被回收。
- 栈的特性 适合这种临时变量的管理,因为栈是“先进后出”,后调用的函数先返回,所以局部变量可以快速分配和回收。
📌3.3 为什么freeRTOS中每个任务都需要有自己的栈?
在 FreeRTOS 这样的实时操作系统(RTOS)中,每个任务都需要独立的栈,这是因为 任务是独立运行的,并且 任务切换(上下文切换)需要保存和恢复任务的状态。我们从 函数调用、任务调度和中断处理 这几个角度来分析这个问题。
3.3.1 每个任务都有自己的函数调用栈
在裸机编程(单任务程序)中,所有函数共用一个栈,因为程序是单线程的,没有任务切换的概念。
但是在 FreeRTOS 中:
- 每个任务本质上是一个独立的函数,需要自己的一块栈空间来存储局部变量、返回地址等。
- 任务之间相互独立,不能共享栈,否则数据会互相覆盖,导致程序崩溃。
✅ 示例(任务代码):
void TaskA(void *pvParameters) {
int a = 10; // a 存入 TaskA 的栈
while(1) {
// 执行任务代码
}
}
void TaskB(void *pvParameters) {
int b = 20; // b 存入 TaskB 的栈
while(1) {
// 执行任务代码
}
}
🎯 问题:如果 TaskA 和 TaskB 共用一个栈,变量 a
和 b
就可能被覆盖,导致程序行为异常!
所以,每个任务必须有自己的栈,这样它的局部变量和函数调用不会受到其他任务的影响。
3.3.2 任务切换(上下文切换)需要保存和恢复栈
FreeRTOS 采用 时间片轮转调度 或 优先级调度,当一个任务运行时:
- 它的所有寄存器、局部变量、返回地址 都保存在 任务的栈 里。
- 当任务切换时,FreeRTOS 需要保存当前任务的栈,然后恢复下一个任务的栈,这样任务可以从上次中断的地方继续执行。
✅ 示例(任务切换过程): 假设有两个任务 TaskA
和 TaskB
:
- TaskA 运行时,CPU 使用 TaskA 的栈
- 任务切换(切换到 TaskB),FreeRTOS 保存 TaskA 的栈指针(SP),并加载 TaskB 的栈指针
- TaskB 继续执行,不会影响 TaskA 的数据
如果任务没有独立的栈,每次任务切换时,前一个任务的数据会被下一个任务覆盖,导致任务无法正确恢复。
3.3.3 FreeRTOS 如何管理任务栈?
在 FreeRTOS 中,每个任务创建时,需要手动指定栈大小。 示例:创建任务时分配栈
xTaskCreate(TaskA, "TaskA", 128, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 256, NULL, 1, NULL);
- "TaskA" 任务的栈大小为 128(单位是字,通常是 4 字节/字)
- "TaskB" 任务的栈大小为 256
- 任务创建时,FreeRTOS 会为其分配一块独立的栈空间
✅ 任务切换时:
- 任务
TaskA
运行时,CPU 使用TaskA
的栈 - 任务
TaskB
运行时,CPU 使用TaskB
的栈 - 任务不会相互影响,数据不会被覆盖
3.3.4 中断如何影响任务栈?
当中断发生时,CPU 需要保存任务的运行状态,然后执行中断服务程序(ISR)。 如果任务没有独立的栈,ISR 可能会覆盖任务的数据,导致任务崩溃。
FreeRTOS 任务调度的关键:
- 任务切换时,当前任务的栈会被保存(包括所有寄存器)
- 中断发生时,CPU 先使用当前任务的栈保存寄存器
- 中断返回后,任务可以继续运行,不受影响
✅ 示例(中断影响任务栈)
void USART1_IRQHandler(void) {
char data = USART1->DR; // 读取串口数据
// 这里的变量 data 存在任务的栈中,不会影响其他任务
}
- 如果任务栈是共享的,
data
可能会被其他任务覆盖,导致数据错误。
3.3.5 任务栈分配的两种方式
在 FreeRTOS 中,任务栈有两种方式:
- 静态分配(手动分配固定大小的内存)
- 动态分配(使用
pvPortMalloc()
在堆中分配栈)
✅ 静态分配示例
StackType_t myStack[128]; // 128 个字大小的栈
StaticTask_t myTaskControlBlock;
xTaskCreateStatic(TaskA, "TaskA", 128, NULL, 1, myStack, &myTaskControlBlock);
✅ 动态分配示例
xTaskCreate(TaskA, "TaskA", 128, NULL, 1, NULL);
- 动态分配的栈存放在 FreeRTOS 的堆区,可能会导致碎片化问题。
- 静态分配的栈存放在任务的数组中,避免了碎片化,推荐用于嵌入式开发。
3.3.6 结论
🔹 为什么 FreeRTOS 任务需要独立的栈?
✅ 保证任务的局部变量和函数调用不会被其他任务覆盖 ✅ 任务切换时,CPU 需要保存和恢复任务的运行状态 ✅ 中断发生时,需要保证任务的数据不会丢失 ✅ 多个任务同时运行时,每个任务必须有自己的独立栈空间
在 FreeRTOS 任务调度过程中,任务切换本质上是切换栈指针(SP),如果多个任务共用一个栈,任务切换后就会发生数据覆盖,导致任务无法正确恢复,因此 每个任务都必须有自己的独立栈! 🚀
🎯4. FreeRTOS 目录结构
使用STM32CubeMX创建的FreeRTOS工程中,FreeRTOS相关的源码如下:
├── Core
│ ├── Inc
│ │ ├── main.h
│ │ ├── freertos.h # FreeRTOS 头文件
│ ├── Src
│ │ ├── main.c
│ │ ├── freertos.c # FreeRTOS 任务初始化
│ │ ├── stm32fxxx_it.c # 中断处理(可能涉及 FreeRTOS)
│ │ ├── syscalls.c # 系统调用文件
│ │ ├── sysmem.c # 内存管理
│
├── Middlewares
│ ├── Third_Party
│ │ ├── FreeRTOS
│ │ │ ├── Source
│ │ │ │ ├── CMSIS_RTOS # CMSIS-RTOS API(如果启用了 CMSIS-RTOS 适配层)
│ │ │ │ ├── include # FreeRTOS 头文件
│ │ │ │ ├── portable # 适配不同硬件平台的代码
│ │ │ │ ├── tasks.c # 任务管理(核心)
│ │ │ │ ├── queue.c # 消息队列管理
│ │ │ │ ├── list.c # 任务调度所需的链表结构
│ │ │ │ ├── timers.c # 软件定时器
│ │ │ │ ├── event_groups.c # 事件组
│ │ │ │ ├── stream_buffer.c # 流缓冲区
│ │ │ │ ├── heap_x.c # 堆管理(不同实现)
│
├── Startup
│ ├── startup_stm32fxxx.s # 启动文件
│
├── FreeRTOSConfig.h # FreeRTOS 配置文件(重要)
4.1 重要文件介绍
📁 Core 目录
1️⃣ freertos.h
📌 作用:FreeRTOS 任务相关的头文件,包含了 CMSIS-RTOS 兼容层(如果启用了 CMSIS-RTOS
)。
2️⃣ freertos.c
📌 作用:
- 这个文件是
CubeMX 自动生成的
,主要用于 FreeRTOS 任务的初始化,包括:
- 任务创建
- 信号量、队列等 RTOS 组件的初始化
- 系统启动
📌 示例:freertos.c
中的任务创建
void MX_FREERTOS_Init(void) {
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
xTaskCreate(LED_Task, "LED", 128, NULL, 1, NULL);
}
🛑 注意:
- CubeMX 可能会覆盖
freertos.c
,如果要自定义任务,建议单独创建任务文件(例如task_led.c
)。
📁 Middlewares 目录
📌 这个目录存放的是 STM32CubeMX 自带的 FreeRTOS 库。
1️⃣ Source/include 目录
📌 作用:存放 FreeRTOS 的核心头文件,如:
FreeRTOS.h
→ FreeRTOS 主头文件task.h
→ 任务管理相关 APIqueue.h
→ 消息队列 APItimers.h
→ 软件定时器 APIsemphr.h
→ 信号量 APIevent_groups.h
→ 事件组 API
示例:任务创建
#include "FreeRTOS.h"
#include "task.h"
xTaskHandle taskHandle;
xTaskCreate(TaskFunction, "TaskName", 128, NULL, 1, &taskHandle);
2️⃣ Source/portable 目录
📌 作用:适配不同硬件平台(Cortex-M、ARM7、RISC-V等)。
常见的适配文件:
port.c
→ 处理任务切换和 CPU 寄存器恢复portmacro.h
→ 针对不同 CPU 架构的宏定义heap_x.c
→ 内存管理(不同的heap_x.c
代表不同的堆分配策略)
示例:任务切换
#define portYIELD() vPortYield()
3️⃣ Source/tasks.c
📌 作用:FreeRTOS 的核心任务管理代码,实现:
- 任务创建 (
xTaskCreate
) - 任务删除 (
vTaskDelete
) - 任务切换 (
vTaskSwitchContext
) - 任务状态管理
4️⃣ Source/queue.c
📌 作用:实现消息队列(任务间通信)。
示例:任务间消息传递
QueueHandle_t queue = xQueueCreate(10, sizeof(int));
xQueueSend(queue, &data, portMAX_DELAY);
xQueueReceive(queue, &recv_data, portMAX_DELAY);
5️⃣ Source/timers.c
📌 作用:实现软件定时器。
示例:创建定时器
TimerHandle_t timer = xTimerCreate("Timer", pdMS_TO_TICKS(1000), pdTRUE, NULL, TimerCallback);
xTimerStart(timer, 0);
FreeRTOSConfig.h
📌 作用:FreeRTOS 的配置文件,定义了 RTOS 的各种参数,如:
- 任务调度方式
- 最大优先级
- 堆管理
- 是否启用消息队列
📌 常见参数
#define configUSE_PREEMPTION 1 // 使能抢占式调度
#define configUSE_IDLE_HOOK 0 // 是否使用空闲任务 Hook
#define configUSE_TICK_HOOK 0 // 是否使用时钟钩子
#define configMAX_PRIORITIES 5 // 最大优先级
#define configMINIMAL_STACK_SIZE 128 // 最小栈大小
#define configTOTAL_HEAP_SIZE (10 * 1024) // 堆大小(10KB)
🛑 注意:
configTOTAL_HEAP_SIZE
影响任务堆栈,如果不够,任务可能无法创建。- 修改
FreeRTOSConfig.h
后,需要重新编译工程!
🎯5.内存管理
5.1 为什么要自己实现内存管理
在 FreeRTOS 中,内存管理是关键的一部分,主要原因如下:
- 标准库 malloc/free 可能导致碎片化
malloc()
和free()
可能导致内存碎片化,进而影响嵌入式系统的稳定性。- 由于嵌入式设备的 RAM 资源有限,碎片化会导致无法分配足够大的连续内存块,甚至系统崩溃。
- 实时性要求
malloc()
和free()
运行时间不确定,在 RTOS 中可能导致任务不可预测的延迟,影响实时性。- FreeRTOS 提供了 确定性分配的内存管理方案,可以避免意外的长时间阻塞。
- 不同应用场景的需求
- 嵌入式应用对内存管理的需求不同,FreeRTOS 允许用户根据需求选择合适的内存管理策略。
- FreeRTOS 预置了 5 种不同的
heap_x.c
内存管理方案,以适应不同应用。 - 简化调试和资源监测
- FreeRTOS 提供 内存使用监测,如
uxTaskGetStackHighWaterMark()
,便于分析内存占用情况,优化资源分配。
5.2 FreeRTOS 的 5 种内存管理方法
FreeRTOS 提供了 5 种不同的堆内存管理方案,用户可以根据需求选择合适的 heap_x.c
文件:
方法 | 文件 | 特点 | 适用场景 |
---|---|---|---|
heap_1 | heap_1.c |
仅分配,不释放(适用于不需要释放内存的系统) | 任务生命周期固定,初始化时分配内存 |
heap_2 | heap_2.c |
允许分配和释放,但可能产生碎片 | 适用于任务动态创建和删除,碎片化影响不大 |
heap_3 | heap_3.c |
直接使用标准 malloc() 和 free() |
适用于已有内存管理机制的系统 |
heap_4 | heap_4.c |
允许分配和释放,带 碎片整理功能 | 需要频繁分配/释放内存的系统 |
heap_5 | heap_5.c |
支持多个独立的内存区域(多段内存) | 适用于 非连续内存 或 外部 RAM |
💡 推荐选择:
- 如果系统不释放内存,用
heap_1.c
,最简单无碎片。 - 如果要释放内存但担心碎片,用
heap_4.c
,带碎片整理功能。 - 如果有多个 RAM 区域(如内部 RAM + 外部 RAM),用
heap_5.c
。
5.3 Heap 相关的函数
FreeRTOS 提供了一些 堆内存管理相关的 API,主要包括:
函数 | 作用 |
---|---|
pvPortMalloc(size_t xSize) |
分配 xSize 字节的内存 |
vPortFree(void *pv) |
释放 pv 指向的内存 |
xPortGetFreeHeapSize() |
获取当前剩余的堆大小(单位:字节) |
xPortGetMinimumEverFreeHeapSize() |
获取系统运行以来最小的可用堆大小 |
示例代码
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
void example_task(void *pvParameters)
{
void *ptr = pvPortMalloc(100); // 申请 100 字节
if (ptr != NULL)
{
printf("Memory allocated successfully\n");
vPortFree(ptr); // 释放内存
}
printf("Free heap size: %d bytes\n", xPortGetFreeHeapSize());
vTaskDelete(NULL);
}
🎯6.数据类型和编程规范
6.1 数据类型
每个移植的版本都含有自己的portmacro.h头文件,里面定义了2个数据类型:
TickType_t
:
FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
每发生一次中断,中断次数累加,这被称为tick count
tick count这个变量的类型就是TickType_t
TickType_t可以是16位的,也可以是32位的
FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
否则TickType_t就是uint32_t
对于32位架构,建议把TickType_t配置为uint32_t
BaseType_t
:
这是该架构最高效的数据类型
32位架构中,它就是uint32_t
16位架构中,它就是uint16_t
8位架构中,它就是uint8_t
BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE
6.2 变量名
FreeRTOS 变量名通常带有前缀,以表示其作用范围和类型。
前缀 | 含义 | 示例 |
---|---|---|
x |
变量(常用于一般变量) | xTaskHandle |
px |
指针类型变量 | pxQueue |
ux |
无符号变量 | uxPriority |
i |
有符号整型 | iIndex |
pc |
指向字符的指针 | pcTaskName |
us |
无符号 16 位变量 | usStackDepth |
ul |
无符号 32 位变量 | ulRunTimeCounter |
pv |
指针类型(void*) | pvPortMalloc |
pd |
FreeRTOS 逻辑值(pdTRUE /pdFALSE ) |
pdPASS |
6.3 函数名
函数名的前缀通常由 返回值类型 和 所在文件模块 组成,常见的规则如下:
前缀 | 文件模块 | 示例 |
---|---|---|
v |
返回 void |
vTaskDelay() |
x |
返回 BaseType_t |
xQueueSend() |
px |
返回指针类型 | pxTimerCreate() |
ux |
返回无符号整数 | uxQueueMessagesWaiting() |
e |
返回枚举类型 | eTaskGetState() |
pc |
返回 char* |
pcTaskGetName() |
6.4 宏的命名规则
FreeRTOS 宏的名称通常为 全大写,并可带有小写前缀,以表示定义位置。例如:
前缀 | 含义 | 示例 |
---|---|---|
port |
端口相关 | portTICK_PERIOD_MS |
config |
配置选项 | configUSE_PREEMPTION |
task |
任务相关 | taskYIELD() |
queue |
队列相关 | queueSEND_TO_BACK |
sem |
信号量相关 | semBINARY_SEMAPHORE |
pd |
逻辑值 | pdTRUE / pdFALSE |
通用 FreeRTOS 宏定义表
宏名称 | 作用 |
---|---|
configUSE_PREEMPTION |
使能抢占式调度(1:使能,0:关闭) |
configUSE_16_BIT_TICKS |
TickType_t 类型(1:16位,0:32位) |
configTICK_RATE_HZ |
系统滴答定时器频率(单位 Hz) |
configMAX_PRIORITIES |
任务的最大优先级数 |
configMINIMAL_STACK_SIZE |
任务最小栈大小 |
portTICK_PERIOD_MS |
1 / configTICK_RATE_HZ (滴答周期,单位 ms) |
pdPASS / pdFAIL |
任务或操作是否成功 |
pdTRUE / pdFALSE |
布尔值(1/0) |
pdMS_TO_TICKS(x) |
将毫秒转换为 tick |
taskENTER_CRITICAL() |
进入临界区(禁用中断) |
taskEXIT_CRITICAL() |
退出临界区(恢复中断) |
这些规范确保了 代码的可读性和一致性,并帮助开发者快速理解变量、函数和宏的用途。
🎯7.动态和静态创建任务
在 FreeRTOS 中,任务(Task)可以通过 动态 或 静态 方式创建。两者各有优缺点,适用于不同的应用场景。
7.1 动态创建任务
7.1.1 使用 xTaskCreate()
动态任务创建使用 xTaskCreate()
,它会在 FreeRTOS 堆(heap) 中分配任务控制块(TCB)和任务栈。
函数原型
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数
const char *const pcName, // 任务名
configSTACK_DEPTH_TYPE usStackDepth, // 任务栈大小(单位:字)
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄(可为 NULL)
);
示例代码
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 任务函数
void vTaskFunction(void *pvParameters)
{
while (1)
{
printf("Task is running\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟 1 秒
}
}
void app_main(void)
{
TaskHandle_t xTaskHandle = NULL;
// 创建任务
if (xTaskCreate(vTaskFunction, "Task1", 128, NULL, 1, &xTaskHandle) == pdPASS)
{
printf("Task created successfully\n");
}
else
{
printf("Task creation failed\n");
}
// vTaskDelete(xTaskHandle); // 可用于删除任务
}
特点 ✅ 优点 - 使用方便,堆中自动分配任务栈,无需手动管理内存。 - 适用于任务动态创建/删除的情况(如任务数不固定)。
❌ 缺点
- 依赖 堆内存 (heap_x.c
方案),可能引起 内存碎片化。
- 不适用于 RAM 受限的嵌入式系统(可能会导致 malloc()
失败)。
7.2 静态创建任务
7.2.1 使用 xTaskCreateStatic()
静态任务创建使用 xTaskCreateStatic()
,任务栈和 TCB(任务控制块)由用户手动分配在 全局/静态内存,不会使用堆。
函数原型
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pvTaskCode, // 任务函数
const char *const pcName, // 任务名
const uint32_t ulStackDepth, // 任务栈大小(单位:字)
void *const pvParameters, // 任务参数
UBaseType_t uxPriority, // 任务优先级
StackType_t *const puxStackBuffer, // 任务栈缓冲区(必须提供)
StaticTask_t *const pxTaskBuffer // 任务控制块(必须提供)
);
示例代码
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 静态分配任务所需的内存
StaticTask_t xTaskBuffer; // 任务控制块
StackType_t xTaskStack[128]; // 任务栈
// 任务函数
void vStaticTaskFunction(void *pvParameters)
{
while (1)
{
printf("Static Task is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
TaskHandle_t xStaticTaskHandle;
// 静态创建任务
xStaticTaskHandle = xTaskCreateStatic(vStaticTaskFunction, "StaticTask",
128, NULL, 1, xTaskStack, &xTaskBuffer);
if (xStaticTaskHandle != NULL)
{
printf("Static Task created successfully\n");
}
else
{
printf("Static Task creation failed\n");
}
}
特点
✅ 优点
- 不使用堆内存,不会引起 碎片化,适用于 RAM 受限设备。
- 任务栈分配在 静态变量,可以避免 malloc()
失败导致的崩溃。
❌ 缺点 - 手动管理栈,需要 提供缓冲区,使用 更复杂。 - 无法动态创建/删除(除非手动释放任务资源)。
7.3 选择动态 or 静态创建
对比项 | 动态创建 (xTaskCreate ) |
静态创建 (xTaskCreateStatic ) |
---|---|---|
内存来源 | 堆 (heap_x.c ) |
静态分配(全局变量) |
是否可能失败 | 可能因堆不足而失败 | 只要内存够,就不会失败 |
碎片化问题 | 可能会有碎片化 | 无碎片化 |
是否可删除任务 | 可以删除 (vTaskDelete ) |
不可删除 |
适用场景 | 任务数量不确定,运行时动态创建 | RAM 受限,任务固定不变 |
7.4 任务删除
如果任务是动态创建的(xTaskCreate
),可以调用 vTaskDelete()
删除任务,但静态任务无法直接删除。
删除动态任务
vTaskDelete(xTaskHandle);
若删除自身任务:
vTaskDelete(NULL); // NULL 表示删除当前任务
总结
- 动态创建(
xTaskCreate
):更灵活,但可能引发 碎片化问题,适用于 任务数量不固定 的应用。 - 静态创建(
xTaskCreateStatic
):更安全,无碎片化问题,适用于 RAM 受限 且 任务固定 的系统。
💡 推荐 - 任务固定,RAM 受限 👉 静态创建 - 任务动态创建/删除 👉 动态创建
🎯8.估算栈的大小
在 FreeRTOS 中,正确估算任务栈大小 非常重要。如果栈太小,可能会导致溢出(Stack Overflow),影响系统稳定性;如果栈太大,会浪费有限的 RAM 资源。
8.1 栈的单位
FreeRTOS 的栈大小是以字(word)为单位,而不是字节: - 对于 8-bit MCU(如 AVR):1 word = 1 byte - 对于 16-bit MCU(如 MSP430):1 word = 2 bytes - 对于 32-bit MCU(如 STM32、ESP32):1 word = 4 bytes
在 STM32F103(Cortex-M3)上: - 任务栈大小通常定义为 字(words),而不是字节。 - 如果 STM32F103C8T6 设定栈大小为 128,则实际占用 128 × 4 = 512 字节 RAM。
8.2 栈的组成
一个任务的栈需要存储:
1. 局部变量(如 int a, char buffer[32]
)。
2. 函数调用的返回地址(函数调用越深,栈占用越大)。
3. CPU 寄存器上下文(任务切换时保存的寄存器数据)。
4. 中断嵌套时的额外栈需求(如果任务需要处理中断)。
8.3 计算栈大小
估算任务栈大小的方法: 1. 计算局部变量的占用: - 如果有数组或结构体,计算它们的字节大小。 - 考虑递归,如果函数递归调用,会导致栈占用激增。
- 考虑任务切换保存的 CPU 寄存器:
-
Cortex-M3 需要保存 16 个寄存器(每个 4 字节),大约 64 字节 = 16 words。
-
考虑中断的影响:
- 如果任务要处理中断(特别是高优先级任务),栈要额外预留空间。
- 一般预留 32~64 字节(8~16 words) 以防止溢出。
示例估算 假设任务代码如下:
void vExampleTask(void *pvParameters)
{
char buffer[50]; // 50 字节
int data[10]; // 10 * 4 = 40 字节
while (1)
{
process_data(buffer, data);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
栈占用估算:
| 组件 | 大小(字节) | 转换为 words(32-bit MCU) |
| ------------------ | ------------ | -------------------------- |
| buffer[50]
| 50 | 13 words (向上取整) |
| data[10]
| 40 | 10 words |
| 任务上下文切换 | ~64 | 16 words |
| 中断安全余量 | ~32 | 8 words |
| 总计 | 186 | 47 words |
最终建议栈大小 = 64 words(256 字节),留一些余量。
8.4 监测任务栈使用情况
FreeRTOS 提供了几个方法来检查栈的使用情况:
方法 1:使用 uxTaskGetStackHighWaterMark()
- 该函数返回任务栈剩余的最小水位(从任务创建以来的最小剩余栈空间)。
- 如果返回值很小,说明栈快要溢出,需要增大栈大小。
示例代码
TaskHandle_t xTaskHandle;
void vTaskFunction(void *pvParameters)
{
while (1)
{
printf("Task running...\n");
// 获取栈的最小剩余水位
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
printf("Task stack high water mark: %d words\n", uxHighWaterMark);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
xTaskCreate(vTaskFunction, "Task1", 128, NULL, 1, &xTaskHandle);
}
输出示例
Task stack high water mark: 78 words
Task stack high water mark: 80 words
👉 如果返回值低于 10,说明栈快满了,需要增加栈大小。
方法 2:使用 configCHECK_FOR_STACK_OVERFLOW
在 FreeRTOSConfig.h
中启用栈溢出检测:
#define configCHECK_FOR_STACK_OVERFLOW 2
然后实现钩子函数:
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
printf("Stack overflow detected in task: %s\n", pcTaskName);
while (1); // 停止运行,方便调试
}
💡 注意:只有在 编译优化等级较低(如 -Og
) 时,这个方法才会更准确检测栈溢出。
8.5 FreeRTOS 任务栈大小推荐
应用场景 | 推荐栈大小(32-bit MCU,单位:words) | 备注 |
---|---|---|
简单任务(LED 闪烁) | 64(256 字节) | 仅调用 vTaskDelay() ,无局部数组 |
一般任务(带数组缓存) | 128(512 字节) | 任务有 50~100 字节局部变量 |
复杂任务(TCP/串口/文件系统) | 256(1 KB) | 任务涉及通信协议或大数据处理 |
无线通信(Wi-Fi/BLE) | 512(2 KB)或更大 | 需要处理大量数据包 |
总结
- FreeRTOS 栈大小单位是 words(在 STM32F103 上
1 word = 4 bytes
)。 - 任务栈需要考虑 局部变量、寄存器上下文、中断嵌套 影响。
- 可以用
uxTaskGetStackHighWaterMark()
检查任务栈剩余情况,如果数值过小,需要加大栈大小。 configCHECK_FOR_STACK_OVERFLOW
可以检测栈溢出,但不是 100% 可靠。- 推荐栈大小:
- 简单任务:64 words(256B)
- 普通任务:128 words(512B)
- 网络任务:256 words(1KB)以上
💡 建议
你现在用的是 STM32F103C8T6,它的 RAM 只有 20KB,建议你尽量使用静态任务(xTaskCreateStatic
)并合理设置栈大小,以避免 RAM 不足导致的崩溃。
👉 你目前的任务有哪些?需要帮助你估算合适的栈大小吗?
🎯9. 删除任务
在 FreeRTOS 中,我们可以使用 vTaskDelete(TaskHandle_t xTask)
来删除任务,以节省 RAM 资源或进行任务管理。
9.1 任务删除的规则
- 任务自己删除自己:
- 任务可以调用
vTaskDelete(NULL)
直接删除自己。 - 一个任务删除另一个任务:
- 需要
TaskHandle_t
任务句柄。 - 删除任务后,任务的栈不会自动释放(如果是静态任务,栈不会释放)。
- IDLE 任务回收任务的资源:
- 被删除的任务不会立即释放堆内存,FreeRTOS 的 IDLE 任务会回收它。
9.2 任务自己删除自己
void vTaskFunction(void *pvParameters)
{
while (1)
{
printf("Task running...\n");
vTaskDelay(pdMS_TO_TICKS(1000));
// 条件满足时删除自己
printf("Task deleting itself\n");
vTaskDelete(NULL);
}
}
void app_main(void)
{
xTaskCreate(vTaskFunction, "Task1", 128, NULL, 1, NULL);
}
效果
- 任务运行一段时间后,自己调用 vTaskDelete(NULL)
删除自己,不会再执行。
9.3 另一个任务删除任务
TaskHandle_t xTaskHandle;
void vTaskFunction(void *pvParameters)
{
while (1)
{
printf("Task running...\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vMonitorTask(void *pvParameters)
{
vTaskDelay(pdMS_TO_TICKS(5000)); // 5 秒后删除任务
printf("Deleting Task1\n");
vTaskDelete(xTaskHandle);
}
void app_main(void)
{
xTaskCreate(vTaskFunction, "Task1", 128, NULL, 1, &xTaskHandle);
xTaskCreate(vMonitorTask, "Monitor", 128, NULL, 2, NULL);
}
效果
- Task1
每秒运行一次。
- 5 秒后,Monitor
任务删除 Task1
,Task1
不再运行。
9.4 另一个任务删除任务(通过传递参数)
假设想要在LED_02
任务中删除PlaySund
任务,则需要将PlaySound
任务的句柄作为LED_02
任务的参数进行传递
TaskHandle_t xSoundTaskHandle;
TaskHandle_t xLightTaskHandle;
xTaskCreate(PlaySoundTask,"PlaySoundTask",128,NULL,osPriorityNormal,&xSoundTaskHandle);
//xSoundTaskHandle是参数
xTaskCreate(LED_02,"LightTask",128,xSoundTaskHandle,osPriorityNormal,&xLightTaskHandle);
void LED_02(void *pvParameters)
{
TaskHandle_t soundHandle = (TaskHandle_t)pvParameters; //首先获取句柄并强制转换。因为参数默认是void
if(soundHandle)
{
OLED_ShowString(0, 50, "SoundTask existed", OLED_6X8);
OLED_Update();
}
else
{
OLED_ShowString(0, 50, "SoundTask deleted", OLED_6X8);
OLED_Update();
}
while(1)
{
OLED_Clear();
OLED_ShowString(0,10,"PA0:",OLED_6X8);
vTaskDelay(pdMS_TO_TICKS(2000)); // 延时 2000ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);
OLED_ShowBinNum(25,10,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0),1,OLED_6X8);
OLED_Update();
vTaskDelay(pdMS_TO_TICKS(2000)); // 延时 2000ms
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);
OLED_ShowBinNum(25,10,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0),1,OLED_6X8);
OLED_Update();
if (soundHandle != NULL)
{
vTaskDelete(soundHandle); //删除任务
soundHandle = NULL; //清空句柄
OLED_ShowString(0, 50, "SoundTask deleteing..", OLED_6X8); // 显示删除信息
OLED_Update();
}
else
{
OLED_ShowString(0, 50, "SoundTask deleted", OLED_6X8); // 显示删除信息
OLED_Update();
}
}
}
9.5 删除静态任务
静态任务的控制块和栈不会自动释放,但任务仍然可以被删除。
StaticTask_t xTaskBuffer;
StackType_t xStack[128];
TaskHandle_t xTaskHandle;
void vTaskFunction(void *pvParameters)
{
while (1)
{
printf("Task running...\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
xTaskHandle = xTaskCreateStatic(vTaskFunction, "Task", 128, NULL, 1, xStack, &xTaskBuffer);
vTaskDelay(pdMS_TO_TICKS(5000));
printf("Deleting static task\n");
vTaskDelete(xTaskHandle);
}
注意
- 任务被删除后,
xStack
仍然占用 RAM,需要手动清零(如果有需求)。
9.6 任务删除后的影响
任务类型 | 删除后栈释放情况 |
---|---|
动态任务 (xTaskCreate ) |
IDLE 任务回收堆内存 |
静态任务 (xTaskCreateStatic ) |
栈不会释放,需要手动处理 |
9.7 任务删除的最佳实践
- 动态任务一般可以放心删除,因为 IDLE 任务会释放内存。
- 静态任务删除后,手动清理任务相关的变量,以防止后续误用。
- 确保任务不在使用共享资源(如
printf
、I2C)时删除,否则可能导致崩溃。
🎯10.任务的五种状态
FreeRTOS 任务的五种状态、描述及相关函数接口的表格:
状态 | 描述 | 相关函数接口 |
---|---|---|
运行(Running) | 任务正在 CPU 上执行。在单核系统中,任意时刻只有一个任务处于运行状态。 | xTaskCreate() / xTaskCreateStatic() |
就绪(Ready) | 任务具备运行条件,但 CPU 被其他高优先级任务占用,等待调度执行。 | vTaskResume() |
阻塞(Blocked) | 任务在等待某个事件(如信号量、消息队列、时间延迟)发生,暂时不能执行。 | vTaskDelay() |
挂起(Suspended) | 任务被显式挂起,不会被调度,即使满足运行条件也不会执行,需手动恢复。 | vTaskSuspend() / vTaskResume() |
终止(Deleted) | 任务已被删除,不再被调度,资源可能仍存在,通常需要手动释放内存。 | vTaskDelete() |
下面是每个任务状态的简要说明以及相应的代码示例:
10.1 运行(Running)
- 描述:任务处于运行状态,表示它正在 CPU 上执行。在单核系统中,一次只能有一个任务处于此状态。如果多个任务都有相同优先级,调度器将按时间片轮转的方式调度它们。
- 代码示例:创建一个任务并使其运行。
void OnSound()
{
while (1)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 打开蜂鸣器
vTaskDelay(500); // 延时 500ms
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 关闭蜂鸣器
vTaskDelay(500); // 延时 500ms
}
}
......
xTaskCreate(OnSound, "OnSound", 64, NULL, 1, &SoundTaskHandle);
10.2 就绪(Ready)
- 描述:任务已准备好运行,但由于 CPU 被其他高优先级任务占用,它处于就绪状态。任务会等待调度器分配 CPU 时间来执行。
- 代码示例:调用
vTaskResume()
使任务从挂起状态变为就绪状态。
vTaskResume(SoundTaskHandle);
10.3 阻塞(Blocked)
- 描述:任务正在等待某个事件或条件(如信号量、队列等)发生,暂时不能继续执行。任务将一直阻塞,直到事件或条件满足,任务才会进入就绪状态。
- 代码示例:任务等待信号量,直到信号量可用。
SemaphoreHandle_t xSemaphore;
void vTask3(void *pvParameters) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
printf("Task 3 is running\n");
}
}
int main() {
xSemaphore = xSemaphoreCreateBinary(); // 创建二值信号量
xSemaphoreGive(xSemaphore); // 给信号量,任务将获得并继续执行
xTaskCreate(vTask3, "Task 3", 1000, NULL, 1, NULL);
vTaskStartScheduler(); // 启动调度器
return 0;
}
10.4 挂起(Suspended)
- 描述:任务被显式挂起,无法被调度执行。任务可以在任何时候被恢复,恢复后它会进入就绪状态。
- 代码示例:使用
vTaskSuspend()
将任务挂起,稍后恢复。
vTaskSuspend(SoundTaskHandle);
10.5 终止(Deleted)
- 描述:任务已经被删除,不再被调度执行,且任务的资源被释放。通常在任务完成时,通过
vTaskDelete()
删除任务。 - 代码示例:删除当前任务。
// 删除任务并重置句柄
if (SoundTaskHandle != NULL)
{
vTaskDelete(SoundTaskHandle); // 删除任务
SoundTaskHandle = NULL; // 重置句柄
}
🎯11.FreeRTOS任务的管理与调度
在 FreeRTOS 内部,任务调度和管理的核心机制之一是 链表(linked list)。FreeRTOS 使用多个链表来管理任务的状态,并根据任务的优先级和时间片来进行调度。以下是 FreeRTOS 如何使用链表管理任务的详细讲解:
11.1 FreeRTOS 任务管理中的链表结构
在 FreeRTOS 中,任务调度器维护多个 任务链表 来存储不同状态的任务,包括: - 就绪任务链表(Ready List) - 阻塞任务链表(Blocked List) - 挂起任务链表(Suspended List) - 终止任务链表(Deleted List)
11.2 任务控制块(TCB)
每个任务在 FreeRTOS 内部对应一个 任务控制块(Task Control Block, TCB),它是一个结构体,其中包含: - 任务的 栈指针 - 任务的 优先级 - 任务的 状态 - 任务的 延时信息(如果任务处于阻塞状态) - 任务的 链表节点(List Item),用于将 TCB 插入到不同的任务链表中
TCB 结构体示意:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 任务栈指针 */
ListItem_t xStateListItem; /* 任务在不同任务链表中的节点 */
UBaseType_t uxPriority; /* 任务优先级 */
char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名 */
} tskTCB;
11.3 就绪任务链表(Ready List)
- 就绪链表存储所有 处于“就绪”状态 的任务,即可立即运行的任务。
- FreeRTOS 采用 数组 + 链表 的结构来存储就绪任务,每个优先级对应一个链表(提高调度效率)。
- 调度器选择当前最高优先级的任务链表中的第一个任务执行。
示例:
/* FreeRTOS 使用数组来存储不同优先级的就绪任务链表 */
List_t pxReadyTasksLists[configMAX_PRIORITIES];
任务切换过程:
1. 调度器遍历 pxReadyTasksLists
,找到 最高优先级 的非空链表。
2. 从该链表的 头部 取出任务(任务按照时间片轮转原则排队)。
3. 任务执行后,如果未阻塞或删除,则重新放回链表尾部(时间片轮转)。
11.4 阻塞任务链表(Blocked List)
- 任务调用
vTaskDelay()
或等待某个资源(如队列、信号量)时,会被放入阻塞任务链表。 - FreeRTOS 维护一个 按时间排序的阻塞任务链表,即 延迟队列,链表头部的任务是最快应该被唤醒的任务。
插入阻塞链表的过程
void vTaskDelay(const TickType_t xTicksToDelay)
{
/* 获取当前任务的 TCB */
TCB_t *pxCurrentTask = pxCurrentTCB;
/* 计算任务需要被唤醒的时间点 */
TickType_t xTimeToWake = xTickCount + xTicksToDelay;
/* 将任务插入到阻塞链表,并按照 xTimeToWake 排序 */
listINSERT(&xDelayedTaskList, &pxCurrentTask->xStateListItem);
/* 任务状态变为阻塞,切换到其他任务 */
taskYIELD();
}
任务解锁
- 在系统 tick
中断中,每次 tick
发生,阻塞链表的头部任务的延时值减少 1。
- 如果某个任务的延时结束,则它会被移出阻塞链表,放入就绪链表,等待调度。
11.5 挂起任务链表(Suspended List)
vTaskSuspend()
会将任务移动到 挂起链表,挂起状态的任务不会被调度。- 任务挂起后,即使有资源可用,也不会被恢复,除非显式调用
vTaskResume()
。 vTaskResume()
会将任务从 挂起链表 移动回 就绪链表。
任务挂起代码
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
/* 将任务移出就绪链表 */
uxListRemove(&xTaskToSuspend->xStateListItem);
/* 插入到挂起链表 */
vListInsertEnd(&xSuspendedTaskList, &xTaskToSuspend->xStateListItem);
}
11.6 终止任务链表(Deleted List)
vTaskDelete()
删除任务时,会把任务从所有链表移除,并放入 终止任务链表。- 任务删除后,实际的内存释放由 空闲任务 处理,以避免任务删除时直接释放内存导致的风险。
删除任务
void vTaskDelete(TaskHandle_t xTaskToDelete)
{
/* 任务移出所有链表 */
uxListRemove(&xTaskToDelete->xStateListItem);
/* 任务被放入删除链表,等待空闲任务清理 */
vListInsertEnd(&xDeletedTaskList, &xTaskToDelete->xStateListItem);
}
11.7 FreeRTOS 调度器的任务切换
FreeRTOS 使用 抢占式调度 和 时间片轮转 机制进行任务切换,关键点如下:
1. 调度器遍历“就绪任务链表”,找到最高优先级任务。
2. 时间片轮转:相同优先级的任务按照FIFO 规则调度,即每次调度都会将任务移到链表尾部。
3. 任务阻塞/挂起:
- 任务因 vTaskDelay()
或等待资源(如 xSemaphoreTake()
)进入 阻塞链表。
- 任务因 vTaskSuspend()
进入 挂起链表。
4. 任务唤醒:
- tick
中断检测阻塞链表头部任务是否超时,若超时则移入 就绪链表。
- vTaskResume()
显式将任务从挂起链表移入 就绪链表。
11.8 总结
任务链表 | 作用 | 任务如何进入 | 任务如何退出 |
---|---|---|---|
就绪链表 | 存放可执行任务 | 任务创建、阻塞结束、恢复 | 调度后执行或任务阻塞 |
阻塞链表 | 存放等待事件的任务 | vTaskDelay() 或等待信号量 |
事件发生或超时 |
挂起链表 | 存放挂起任务 | vTaskSuspend() |
vTaskResume() |
终止链表 | 存放已删除任务 | vTaskDelete() |
空闲任务回收 |
FreeRTOS 通过多个链表管理任务状态,并使用 优先级调度 + 时间片轮转 进行任务切换,从而保证实时系统的高效运行。这种基于链表的任务管理方式,使得 FreeRTOS 能够灵活、高效地处理多任务调度。
🎯12.空闲任务
任务通常来说必须写入死循环中,任务如果不是死循环,会发生什么?
- 答:系统会关闭所有中断并进入死循环,所有任务都无法继续进行。
示例 1:非死循环任务
void vTaskFunction(void *pvParameters)
{
printf("Task is running...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1 秒
printf("Task completed.\n");
// 没有死循环,函数执行完毕后直接返回
}
解决方案
使用 vTaskDelete(NULL)
主动删除任务
如果任务不需要永久运行,可以在结束前调用 vTaskDelete(NULL)
来删除自己。
比如,如果想要打开关闭某颗LED:
if (IRRecvd() == '4')
{
xTaskCreate(onlight,"onlight",64,NULL,24,NULL);
}
if (IRRecvd() == '5')
{
xTaskCreate(offlight,"offlight",64,NULL,24,NULL);
}
void onlight()
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);
vTaskDelete(NULL); //非死循环任务只要最后自杀就不会影响其他任务
}
void offlight(TaskHandle_t LightTaskHandle)
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);
vTaskDelete(NULL); //非死循环任务只要最后自杀就不会影响其他任务
}
解释:
vTaskDelete(NULL)
会将当前任务从所有链表中移除,并放入终止任务链表。- 空闲任务(Idle Task) 负责清理终止任务链表中的任务,释放 TCB 和任务栈。
🎯13.两种延时函数
在 FreeRTOS 中,vTaskDelay
和 vTaskDelayUntil
都用于任务的延时调度,但它们的工作方式有所不同,适用于不同的使用场景。
1. vTaskDelay()
- 基于相对时间的延时,即从调用
vTaskDelay
的时刻开始计算。 - 调用后,任务会进入阻塞态(Blocked),直到指定的时间周期过去,任务才会变为就绪态(Ready)。
- 可能会导致周期性任务的时间偏移(漂移),因为每次调用的起点是当前时间,而不是固定的时间点。
示例:
void Task1(void *pvParameters)
{
for (;;)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000ms(1秒)
}
}
问题:
如果任务的执行时间不稳定(比如偶尔执行 50ms,偶尔执行 70ms),下一次 vTaskDelay(1000)
的起点就会不同,导致任务的执行时间逐渐漂移。
2. vTaskDelayUntil()
- 基于绝对时间的延时,任务在固定的时间间隔执行,避免时间漂移。
- 适用于周期性任务,每次运行时,都会参考上一次的执行时间来计算下一次的执行时间点。
示例:
void Task2(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount(); // 记录当前时间
for (;;)
{
printf("Task2 is running\n");
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1000)); // 以 1000ms 周期运行
}
}
优势: - 无论任务执行时间长短,都能保证任务在固定时间间隔运行(假设系统负载不超出调度能力)。 - 适用于需要严格时间控制的周期性任务(如采样、通信、信号处理等)。
3. 对比总结
函数 | 计算方式 | 适用场景 | 是否可能时间漂移 |
---|---|---|---|
vTaskDelay |
相对延时(当前时刻+延时时间) | 一般任务,延时后继续执行 | 可能发生漂移 |
vTaskDelayUntil |
绝对延时(基于上次唤醒时间) | 周期性任务,保证固定时间间隔 | 避免时间漂移 |
选择建议:
- 如果只是简单的延时,不在乎精确的周期性,可以使用 vTaskDelay
。
- 如果需要任务按照严格的时间间隔执行(例如 100ms、500ms、1秒等),应该使用 vTaskDelayUntil
。
这样可以保证 FreeRTOS 任务的时间控制更加精准,避免时间漂移导致的不稳定性。
🎯14.同步与互斥
在 FreeRTOS 中,提供了多种同步和互斥的机制,主要包括 信号量、互斥量、事件组、消息队列、任务通知 等。不同的机制适用于不同的场景,下面进行详细介绍。
1. 信号量(Semaphore)
作用:
- 主要用于任务间同步,确保任务按一定的顺序执行。
- 也可以用于简单的互斥,但不支持优先级继承(可能导致优先级反转)。
分类
(1)二值信号量(Binary Semaphore)
- 只能取 0 或 1,类似一个任务间的信号(事件通知)。
- 任务 A 释放信号,任务 B 获取信号,完成同步。
示例(任务间同步):
SemaphoreHandle_t xBinarySemaphore;
void Task1(void *pvParameters)
{
for (;;)
{
vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟任务处理
xSemaphoreGive(xBinarySemaphore); // 发送信号
}
}
void Task2(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY))
{
printf("Task2 received signal from Task1\n");
}
}
}
📌 适用于: 任务间事件同步(如中断触发任务)。
(2)计数信号量(Counting Semaphore)
- 允许多个任务获取信号,信号的值可大于 1。
- 可用于资源计数,如管理多个同类资源(线程池、固定连接数)。
示例(资源计数):
SemaphoreHandle_t xCountingSemaphore;
void Task(void *pvParameters)
{
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY))
{
printf("Task acquired a resource\n");
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(xCountingSemaphore); // 释放资源
}
}
📌 适用于: 资源池管理(如限制最大并发任务数)。
2. 互斥量(Mutex)
作用:
- 保护共享资源,防止多个任务同时访问。
- 具备优先级继承机制,可防止优先级反转问题。
- 适用于串口、I/O 设备、共享变量等场景。
示例(串口互斥访问):
MutexHandle_t xMutex;
void Task1(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xMutex, portMAX_DELAY))
{
printf("Task1 is using the shared resource\n");
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(xMutex);
}
}
}
📌 适用于: 保护临界资源(如文件系统、通信接口)。
3. 递归互斥量(Recursive Mutex)
作用: - 允许同一个任务多次获取同一个互斥量,每次获取都必须对应一个释放。 - 适用于需要多层调用的函数锁定资源,如一个任务在不同函数中多次访问同一个资源。
示例:
MutexHandle_t xRecursiveMutex;
void RecursiveFunction()
{
if (xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY))
{
printf("Recursive function accessing resource\n");
xSemaphoreGiveRecursive(xRecursiveMutex);
}
}
void Task(void *pvParameters)
{
if (xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY))
{
RecursiveFunction(); // 可以多次获取互斥量
xSemaphoreGiveRecursive(xRecursiveMutex);
}
}
📌 适用于: 递归函数、同一任务多次调用资源。
4. 事件组(Event Group)
作用: - 位操作的同步机制,用于任务之间的多事件同步。 - 任务可以等待多个事件发生(如等待多个传感器状态)。 - 支持事件组合(任意/全部满足时唤醒任务)。
示例(等待多个传感器就绪):
EventGroupHandle_t xEventGroup;
#define SENSOR_1_READY (1 << 0)
#define SENSOR_2_READY (1 << 1)
void Task(void *pvParameters)
{
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(xEventGroup, SENSOR_1_READY | SENSOR_2_READY,
pdTRUE, pdTRUE, portMAX_DELAY);
if ((uxBits & (SENSOR_1_READY | SENSOR_2_READY)) == (SENSOR_1_READY | SENSOR_2_READY))
{
printf("Both sensors are ready!\n");
}
}
📌 适用于: 任务等待多个事件(如多传感器同步)。
5. 消息队列(Queue)
作用: - 用于任务之间传递数据,同时实现同步。 - 任务 A 发送数据,任务 B 从队列接收数据。 - 可用于生产者-消费者模式。
示例(任务间消息通信):
QueueHandle_t xQueue;
void Task1(void *pvParameters)
{
int data = 100;
xQueueSend(xQueue, &data, portMAX_DELAY);
}
void Task2(void *pvParameters)
{
int receivedData;
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY))
{
printf("Received: %d\n", receivedData);
}
}
📌 适用于: 任务间数据传输(如传感器数据、网络数据)。
6. 任务通知(Task Notification)
作用: - 轻量级任务同步,比信号量和队列更高效。 - 每个任务都有一个32 位的通知值,可用于任务间通信。 - 适用于简单的事件触发场景。
示例(任务间通知):
void Task1(void *pvParameters)
{
vTaskDelay(pdMS_TO_TICKS(1000));
xTaskNotifyGive(xTaskHandle2); // 发送通知
}
void Task2(void *pvParameters)
{
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等待通知
printf("Task2 received notification\n");
}
📌 适用于: 轻量级任务同步(如中断通知任务)。
总结
机制 | 作用 | 适用场景 | 额外特点 |
---|---|---|---|
信号量(Semaphore) | 同步(任务/中断通知) | 任务间事件触发 | 计数信号量适合资源管理 |
互斥量(Mutex) | 互斥(保护共享资源) | 串口、I/O 设备、共享变量 | 支持优先级继承 |
递归互斥量(Recursive Mutex) | 递归锁 | 任务内多次调用共享资源 | 适用于递归函数 |
事件组(Event Group) | 位操作同步 | 任务等待多个事件 | 可设置“或/与”触发条件 |
消息队列(Queue) | 数据传输+同步 | 生产者-消费者模式 | 适用于较大数据量传输 |
任务通知(Task Notification) | 轻量级同步 | 任务间简单触发 | 效率高于信号量 |
💡 选择建议:
- 同步任务 → 信号量
- 共享资源保护 → 互斥量
- 多事件等待 → 事件组
- 任务间数据传递 → 消息队列
- 高效同步 → 任务通知
这样可以根据不同需求,选取最合适的 FreeRTOS 机制,提高系统性能!