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 文件

这种方式最常见,适用于多个任务的管理。

📌 步骤:

  1. Src/ 目录下创建 task_xxx.c,在 Inc/ 目录下创建 task_xxx.h
  2. task_xxx.c 里写任务代码
  3. task_xxx.h 里声明任务函数
  4. 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);
    }
}

解释

  1. vTaskFunction() 是通用的任务函数,每个任务使用不同的 ID 作为参数
  2. xTaskCreate() 在循环中被调用 3 次,创建 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 共用一个栈,变量 ab 就可能被覆盖,导致程序行为异常!

所以,每个任务必须有自己的栈,这样它的局部变量和函数调用不会受到其他任务的影响。

3.3.2 任务切换(上下文切换)需要保存和恢复栈

FreeRTOS 采用 时间片轮转调度优先级调度,当一个任务运行时:

  • 它的所有寄存器、局部变量、返回地址 都保存在 任务的栈 里。
  • 当任务切换时,FreeRTOS 需要保存当前任务的栈,然后恢复下一个任务的栈,这样任务可以从上次中断的地方继续执行

示例(任务切换过程): 假设有两个任务 TaskATaskB

  1. TaskA 运行时,CPU 使用 TaskA 的栈
  2. 任务切换(切换到 TaskB),FreeRTOS 保存 TaskA 的栈指针(SP),并加载 TaskB 的栈指针
  3. 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 中,任务栈有两种方式:

  1. 静态分配(手动分配固定大小的内存)
  2. 动态分配(使用 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 → 任务管理相关 API
  • queue.h → 消息队列 API
  • timers.h → 软件定时器 API
  • semphr.h → 信号量 API
  • event_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 中,内存管理是关键的一部分,主要原因如下:

  1. 标准库 malloc/free 可能导致碎片化
  2. malloc()free() 可能导致内存碎片化,进而影响嵌入式系统的稳定性。
  3. 由于嵌入式设备的 RAM 资源有限,碎片化会导致无法分配足够大的连续内存块,甚至系统崩溃。
  4. 实时性要求
  5. malloc()free() 运行时间不确定,在 RTOS 中可能导致任务不可预测的延迟,影响实时性。
  6. FreeRTOS 提供了 确定性分配的内存管理方案,可以避免意外的长时间阻塞。
  7. 不同应用场景的需求
  8. 嵌入式应用对内存管理的需求不同,FreeRTOS 允许用户根据需求选择合适的内存管理策略。
  9. FreeRTOS 预置了 5 种不同的 heap_x.c 内存管理方案,以适应不同应用。
  10. 简化调试和资源监测
  11. 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. 计算局部变量的占用: - 如果有数组或结构体,计算它们的字节大小。 - 考虑递归,如果函数递归调用,会导致栈占用激增。

  1. 考虑任务切换保存的 CPU 寄存器
  2. Cortex-M3 需要保存 16 个寄存器(每个 4 字节),大约 64 字节 = 16 words

  3. 考虑中断的影响

  4. 如果任务要处理中断(特别是高优先级任务),栈要额外预留空间。
  5. 一般预留 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_OVERFLOWFreeRTOSConfig.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 任务删除的规则

  1. 任务自己删除自己
  2. 任务可以调用 vTaskDelete(NULL) 直接删除自己。
  3. 一个任务删除另一个任务
  4. 需要 TaskHandle_t 任务句柄。
  5. 删除任务后,任务的栈不会自动释放(如果是静态任务,栈不会释放)。
  6. IDLE 任务回收任务的资源
  7. 被删除的任务不会立即释放堆内存,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 任务删除 Task1Task1 不再运行。

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 任务删除的最佳实践

  1. 动态任务一般可以放心删除,因为 IDLE 任务会释放内存。
  2. 静态任务删除后,手动清理任务相关的变量,以防止后续误用。
  3. 确保任务不在使用共享资源(如 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 中,vTaskDelayvTaskDelayUntil 都用于任务的延时调度,但它们的工作方式有所不同,适用于不同的使用场景。

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 机制,提高系统性能!