一 RTOS入门

1.1 裸机与RTOS介绍

        裸机编程是指在嵌入式系统中,直接在硬件上运行代码,没有操作系统的支持。这种方式下,开发者需要完全掌握硬件资源,包括时钟、中断、外设等。任务调度和资源管理都由开发者手动管理。这就像手动操纵一辆汽车,想开车从城市A到城市B,你需要了解汽车的每个部件,掌握如何驾驶,包括油门、刹车、方向盘等。你需要手动决定何时加速、何时刹车、何时转弯。这就好比裸机编程,开发者需要亲自管理每个硬件资源,编写所有的控制逻辑。

        RTOS 全称是 Real Time Operating System,中文名就是实时操作系统,提供了任务调度、内存管理、中断处理等功能。RTOS能够让开发者更专注于应用层的开发,而不用亲自管理底层硬件资源。想从城市A到城市B,你可以选择坐出租车。在出租车上,你只需要告诉司机目的地,不用亲自操纵汽车的每个部分。司机会负责加速、刹车、转弯等操作。这就好比使用RTOS,开发者只需定义任务、调度和数据通信,RTOS会负责底层管理。

总的来说,裸机编程就像是自己开车,而使用RTOS则像是坐出租车,更专注于目的地而非具体的驾驶操作。

  • 任务调度:裸机编程需要手动调度任务,而RTOS提供自动的任务调度器。
  • 硬件管理:裸机编程需要开发者手动管理硬件资源,RTOS提供了抽象接口,简化了硬件管理。
  • 复杂性:裸机编程相对较复杂,需要深入了解硬件细节。RTOS提供了更高层次的抽象,简化了开发流程。

1.2 FreeRTOS简介

        RTOS是指一类系统,如 FreeRTOS,uC/OS,RTX,RT-Thread 等,都是 RTOS 类操作系统。

1.2.1 FreeRTOS优势

        FreeRTOS是一款受欢迎、广泛应用于嵌入式系统的RTOS,其开源、轻量级、可移植的特点使其成为许多嵌入式开发者的首选,主要优势如下:

  • 开源和免费:FreeRTOS是一款开源的RTOS,采用MIT许可证发布,可以免费使用、修改和分发。
  • 轻量级设计:FreeRTOS注重轻量级设计,适用于资源受限的嵌入式系统,不占用过多内存和处理器资源。
  • 广泛应用:FreeRTOS在嵌入式领域得到广泛应用,包括工业自动化、医疗设备、消费电子产品、汽车电子等。
  • 多平台支持:FreeRTOS的设计注重可移植性,可以轻松地移植到不同的硬件平台,支持多种处理器架构。
  • 丰富的功能:提供了多任务调度、任务通信、同步等功能,适用于复杂的嵌入式应用场景。

1.2.2 FreeRTOS介绍 

官网:FreeRTOS™ - FreeRTOS™,并且支持中文。

  • 任务调度:FreeRTOS通过任务调度器管理多个任务,支持不同优先级的任务,实现任务的有序执行。
  • 任务通信和同步:提供了队列、信号量等机制,支持任务之间的通信和同步,确保数据的安全传递。
  • 内存管理:提供简单的内存管理机制,适用于嵌入式环境,有效利用有限的内存资源。
  • 定时器和中断处理:支持定时器功能,能够处理中断,提供了可靠的实时性能。
  • 开发社区:拥有庞大的用户社区,开发者可以在社区中获取支持、解决问题,并分享经验。
  • 可移植性:设计注重可移植性,可以轻松地移植到不同的硬件平台,提高了代码的重用性。

二 FreeRTOS基础介绍 

2.1 任务调度简介

一个处理器核心在某一时刻只能运行一个任务,如果在各个任务之间迅速切换,这样看起来就像多个任务在同时运行。操作系统中任务调度器的责任就是决定在某一时刻要执行哪个任务。

FreeRTOS使用基于优先级的抢占式任务调度策略。

  • 抢占式调度:FreeRTOS采用抢占式调度方式,允许更高优先级的任务在任何时刻抢占正在执行的低优先级任务。这确保了高优先级任务能够及时响应,并提高了系统的实时性。
  • 时间片轮转:在相同优先级的任务之间,FreeRTOS采用时间片轮转策略。每个任务执行一个时间片(一个时间片大小,取决为滴答定时器中断频率),如果有其他同优先级的任务等待执行,则切换到下一个任务。这有助于公平地分配CPU时间。

但是并不是说高优先级的任务会一直执行,导致低优先级的任务无法得到执行。如果高优先级任务等待某个资源(延时或等待信号量等)而无法执行,调度器会选择执行其他就绪的高优先级的任务。

任务优先级相同的,按时间片调度,每个任务执行一个时间片(一次系统时钟中断)
任务执行不足一个时间片(或阻塞),没有用完的时间片不会再使用。

2.2 任务状态

FreeRTOS中任务共存在4种状态:

  • 运行态:当任务实际执行时,它被称为处于运行状态。如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。注意在STM32中,同一时间仅一个任务处于运行态。
  • 就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
  • 阻塞态:如果任务当前正在等待延时或外部事件,则该任务被认为处于阻塞状态。
  • 挂起态:类似暂停,调用函数 vTaskSuspend() 进入挂起态,需要调用解挂函数vTaskResume()才可以进入就绪态。

只有就绪态可转变成运行态,其他状态的任务想运行,必须先转变成就绪态。转换关系如下:
 

这四种状态中,除了运行态,其他三种任务状态的任务都有其对应的任务状态列表:

  • 就绪列表:pxReadyTasksLists[x],其中x代表任务优先级数值。
  • 阻塞列表:pxDelayedTaskList。
  • 挂起列表:xSuspendedTaskList。

列表类似于链表

以就绪列表为例。如果在32位的硬件中,会保存一个32位的变量,代表0-31的优先级。当某个位,置一时,代表所对应的优先级就绪列表有任务存在。

如果有多个任务优先级相同,会连接在同一个就绪列表上:

调度器总是在所有处于就绪列表的任务中,选择具有最高优先级的任务来执行。

三 FreeRTOS移植

3.1 FreeRTOS源码结构介绍

3.1.1 获取源码

3.1.2 源码结构介绍

3.2 FreeRTOS移植步骤

四 FreeRTOS的任务创建和删除

4.1 任务创建和删除API函数

任务的创建和删除本质就是调用FreeRTOS的API函数(通俗易懂一些就是别人写好的代码,或者编译好的程序,提供给你使用,就叫做API。),主要如下:

API函数

描述

xTaskCreate()

动态方式创建任务

xTaskCreateStatic()

静态方式创建任务

vTaskDelete()

删除任务

  • 动态创建任务:任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配。
  • 静态创建任务:任务的任务控制块以及任务的栈空间所需的内存,需用户分配提供。

4.1.1 动态创建任务函数 

1)函数说明

BaseType_t xTaskCreate

(

    TaskFunction_t pxTaskCode,                  /* 指向任务函数的指针 */

    const char * const pcName,                  /* 任务名字,最大长度configMAX_TASK_NAME_LEN */

    const configSTACK_DEPTH_TYPE usStackDepth,  /* 任务堆栈大小,默认单位2字节 */

    void * const pvParameters,                  /* 传递给任务函数的参数 */

    UBaseType_t uxPriority,                     /* 任务优先级,范围:0 ~ configMAX_PRIORITIES - 1 */

    TaskHandle_t * const pxCreatedTask          /* 任务句柄,就是任务的任务控制块 */

)  

参数说明:

  • pvTaskCode

    指向任务入口函数的指针(即实现任务的函数名称,请参阅如下示例)。 任务通常以无限循环的形式实现;实现任务的函数 绝不能尝试返回或退出。但是,任务可以 自行删除

  • pcName

    任务的描述性名称。此参数主要用于方便调试,但也可用于 获取任务句柄。任务名称的最大长度 由 FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN 定义。

  • uxStackDepth

    分配用作任务堆栈的字数(不是字节数!)。例如,如果 堆栈宽度为 16 位,uxStackDepth 为 100,则将分配 200 字节用作任务 堆栈。再举一例,如果堆栈宽度为 32 位,uxStackDepth 为 400, 则将分配 1600 字节用作任务堆栈。堆栈深度与堆栈宽度的乘积不得超过 size_t 类型变量所能包含的最大值。请参阅 常见问题:堆栈应该多大?

  • pvParameters

    作为参数传递给所创建任务的值。如果 pvParameters 设置为某变量的地址, 则在创建的任务执行时,该变量必须仍然存在, 因此,不能传递堆栈变量的地址。

  • uxPriority

    创建的任务将以该指定优先级执行。支持 MPU 的系统 可以通过在 uxPriority 中设置 portPRIVILEGE_BIT 位来选择以特权(系统)模式创建任务。 例如,要创建优先级为 2 的特权任务,请将 uxPriority 设置为 ( 2 | portPRIVILEGE_BIT )。应断言优先级 低于 configMAX_PRIORITIES。如果 configASSERT 未定义,则优先级默认上限为 (configMAX_PRIORITIES - 1)。

  • pxCreatedTask

    用于将句柄传递至由 xTaskCreate() 函数创建的任务。pxCreatedTask 是可选参数, 可设置为 NULL。

返回值说明如下:

  • pdPASS:任务创建成功。
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败。

2)动态创建任务步骤(代码思路)

  1. 将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1。

    FreeRTOSConfig.h
    #define configSUPPORT_DYNAMIC_ALLOCATION                1
  2. 定义函数入口参数。
    /**
     * @description: FreeRTOS入口函数:创建任务函数并开始调度
     * @return {*}
     */
    void FreeRTOS_Start(void)
    {
        xTaskCreate((TaskFunction_t)Start_Task,
                    (char *)"Start_Task",
                    (configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
                    (void *)NULL,
                    (UBaseType_t)START_TASK_PRIORITY,
                    (TaskHandle_t *)&start_task_handler);
        vTaskStartScheduler();
    }
  3. 编写任务函数。
    void Task1(void * pvParameters)
    {
        while(1)
        {
            ......
        }
    }
    
    void Task2(void * pvParameters)
    {
        while(1)
        {
            ......
        }
    }
    ....
  4. 启动任务函数
    void Start_Task( void * pvParameters )
    {
        taskENTER_CRITICAL();               /* 进入临界区 */
        xTaskCreate((TaskFunction_t         )   Task1,
                    (char *                 )   "Task1",
                    (configSTACK_DEPTH_TYPE )   TASK1_STACK_DEPTH,
                    (void *                 )   NULL,
                    (UBaseType_t            )   TASK1_PRIORITY,
                    (TaskHandle_t *         )   &task1_handler );
                    
        xTaskCreate((TaskFunction_t         )   Task2,
                    (char *                 )   "Task2",
                    (configSTACK_DEPTH_TYPE )   TASK2_STACK_DEPTH,
                    (void *                 )   NULL,
                    (UBaseType_t            )   TASK2_PRIORITY,
                    (TaskHandle_t *         )   &task2_handler );
                    
        xTaskCreate((TaskFunction_t         )   Task3,
                    (char *                 )   "Task2",
                    (configSTACK_DEPTH_TYPE )   TASK3_STACK_DEPTH,
                    (void *                 )   NULL,
                    (UBaseType_t            )   TASK3_PRIORITY,
                    (TaskHandle_t *         )   &task3_handler );
        vTaskDelete(NULL);                  
        taskEXIT_CRITICAL();                /* 退出临界区 */
    }

此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。

3)动态创建任务函数内部实现(底层)

  1. 申请堆栈内存&任务控制块内存。
  2. TCB结构体成员赋值。
  3. 添加新任务到就绪列表中。

任务控制块结构体成员介绍。

typedef struct tskTaskControlBlock       
{
    volatile StackType_t * pxTopOfStack; /* 任务栈栈顶,必须为TCB的第一个成员 */
    ListItem_t xStateListItem;                  /* 任务状态列表项 */
    ListItem_t xEventListItem;                  /* 任务事件列表项 */
    UBaseType_t uxPriority;                     /* 任务优先级,数值越大,优先级越大 */
    StackType_t * pxStack;                      /* 任务栈起始地址 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名字 */  
    …
    省略很多条件编译的成员
} tskTCB;

 任务栈栈顶,在任务切换时的任务上下文保存、任务恢复息息相关。每个任务都有属于自己的任务控制块,类似身份证。

4.1.2 静态创建任务函数

静态创建与动态创建最大的区别在于有些事需要手动去做:空闲任务(其他任务都处于阻塞当中会执行优先级较低的空闲任务占用CPU)和软件定时器。(动态自动创建)。

1)函数说明

TaskHandle_t xTaskCreateStatic
( 
    TaskFunction_t pxTaskCode,          /* 指向任务函数的指针 */
    const char * const pcName,          /* 任务函数名 */
    const uint32_t ulStackDepth,        /* 任务堆栈大小,单位是4字节 */
    void * const pvParameters,          /* 传递的任务函数参数 */
    UBaseType_t uxPriority,             /* 任务优先级 */
    StackType_t * const puxStackBuffer, /* 任务堆栈,一般为数组,由用户分配 */
    StaticTask_t * const pxTaskBuffer   /* 任务控制块指针,由用户分配 */
)

参数:

  • pxTaskCode

    指向任务入口函数的指针(即实现任务的函数名称, 请参阅如下示例)。

    任务通常以无限循环的形式实现; 实现任务的函数绝不能尝试返回或退出。但是,任务可以 自行删除

  • pcName

    任务的描述性名称。此参数主要用于方便调试,但也可用于 获取任务句柄

    任务名称的最大长度由 FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN

     定义。 
  • ulStackDepth

    puxStackBuffer 参数用于将 StackType_t变量的数组 传递至 xTaskCreateStatic()ulStackDepth必须设置为数组中的索引数。请参阅常见问题:堆栈应该多大? 

  • pvParameters

    作为参数传递给所创建任务的值。

    如果 pvParameters 设置为某变量的地址,则在创建的任务执行时,该变量必须仍然存在, 因此,不能传递堆栈变量的地址。 |

  • uxPriority

    创建的任务将以该指定优先级执行。支持 MPU 的系统 可以通过在 uxPriority中设置 portPRIVILEGE_BIT位来选择以特权(系统)模式创建任务。例如,要创建优先级为 2 的特权任务, 请将 uxPriority设置为 ( 2 | portPRIVILEGE_BIT )。应断言优先级低于 configMAX_PRIORITIES。如果 configASSERT未定义,则优先级 默认上限为 (configMAX_PRIORITIES - 1)。

  • puxStackBuffer

    必须指向至少包含 ulStackDepth 个索引的 StackType_t数组(见上述 ulStackDepth参数), 该数组将用作任务堆栈,因此必须持久存在 (不能在函数的堆栈上声明)。 

  • pxTaskBuffer

    必须指向 StaticTask_t类型的变量。该变量将用于保存新任务的数据 结构体 (TCB),因此必须持久存在(不能在函数的堆栈上声明)。

返回值如下:

  • NULL:用户没有提供相应的内存,任务创建失败。
  • 其他值:任务句柄,任务创建成功。

2)静态创建任务步骤 (代码思路)

        (1)将宏configSUPPORT_STATIC_ALLOCATION 配置为 1。
  FreeRTOSConfig.h

#define configSUPPORT_STATIC_ALLOCATION     1     //开启静态创建任务

        (2)定义空闲任务&定时器任务的任务堆栈及TCB。

        (3)实现接口函数:

                        vApplicationGetIdleTaskMemory()

                        vApplicationGetTimerTaskMemory()(如果开启软件定时器)

/* 两个接口函数 */
/* 1.空闲任务内存分配 
 * @param ppxIdleTaskTCBBuffer 静态分配的TCB缓冲区的句柄
 * @param ppxIdleTaskStackBuffer 为空闲任务静态分配的堆栈缓冲区的句柄
 * @param pulIdleTaskStackSize 指向分配的堆栈缓冲区中可容纳的元素数目的指针
 */
StaticTask_t IDLE_TASK_TCB;
StackType_t IDLE_TASK_STACK[configMINIMAL_STACK_SIZE];
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                    StackType_t ** ppxIdleTaskStackBuffer,
                                    uint32_t * pulIdleTaskStackSize )
{
    * ppxIdleTaskTCBBuffer = &IDLE_TASK_TCB;
    * ppxIdleTaskStackBuffer = IDLE_TASK_STACK;
    * pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;   //最小栈大小
}
/* 2.定时器任务内存分配 
 * @param ppxTimerTaskTCBBuffer   静态分配的TCB缓冲区的句柄
 * @param ppxTimerTaskStackBuffer 为空闲任务静态分配的堆栈缓冲区的句柄
 * @param pulTimerTaskStackSize   指向分配的堆栈缓冲区中可容纳的元素数目的指针*/
#define TIMER_STACK_DEPTH 200
StaticTask_t TIMER_TASK_TCB;
StackType_t TIMER_TASK_STACK[TIMER_STACK_DEPTH];
void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer,
                                     StackType_t ** ppxTimerTaskStackBuffer,
                                     uint32_t * pulTimerTaskStackSize )
{
    * ppxTimerTaskTCBBuffer = &TIMER_TASK_TCB;
    * ppxTimerTaskStackBuffer = TIMER_TASK_STACK;
    * pulTimerTaskStackSize = TIMER_STACK_DEPTH;
}

        (4)定义函数入口参数。

/* 启动任务的参数配置 */
#define START_STACK_DEPTH 128
#define START_TASK_Priority 1
TaskHandle_t start_task_handler;
StaticTask_t start_task_tcb;
StackType_t start_task_stack[START_STACK_DEPTH];
void Start_Task(void * pvParameters);
/// @brief 入口函数:创建启动任务,启动调度器
void FreeRTOS_Start(void)

    start_task_handler = xTaskCreateStatic((TaskFunction_t)Start_Task,            //指向任务函数的指针
                                           (char *)"Start_Task",                 //任务函数名
                                           (uint32_t) START_STACK_DEPTH,          //任务堆栈大小,单位4字节
                                           (void *)NULL,                         //传递的任务函数的参数
                                           (UBaseType_t)START_TASK_Priority,     //任务优先级
                                           (StackType_t *)start_task_stack,        //任务堆栈
                                           (StaticTask_t *)&start_task_tcb);        //任务控制块指针

    /* 2.启动调度器 */
    vTaskStartScheduler();
}

        (5)编写任务函数。

此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。

4.1.3 删除函数

1)函数说明

void vTaskDelete( TaskHandle_t xTaskToDelete )

参数说明:xTaskToDelete待删除任务的任务句柄。当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。

该函数用于删除已被创建的任务,被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除。

需要注意的是,空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。

2)删除任务流程

  1. 使用删除任务函数,需将宏INCLUDE_vTaskDelete 配置为 1
  2. 入口参数输入需要删除的任务句柄(NULL代表删除本身)

3)内部实现过程

(1)获取所要删除任务的控制块

        通过传入的任务句柄,判断所需要删除哪个任务,NULL代表删除自身。

(2)将被删除任务,移除所在列表

        将该任务在所在列表中移除,包括:就绪、阻塞、挂起、事件等列表。

(3)判断所需要删除的任务

        如果删除任务自身,需先添加到等待删除列表,内存释放将在空闲任务执行;如果删除其他任务,释放内存,任务数量--。

(4)更新下个任务的阻塞时间

        更新下一个任务的阻塞超时时间,以防被删除的任务就是下一个阻塞超时的任务。

五 FreeRTOS的任务挂起和恢复

5.1 任务的挂起和恢复的API函数

  • vTaskSuspend():挂起任务, 类似暂停,可恢复
  • vTaskResume():恢复被挂起的任务
  • xTaskResumeFromISR():在中断中恢复被挂起的任务

5.1.1 任务挂起函数vTaskSuspend()

void vTaskSuspend( TaskHandle_t xTaskToSuspend )

  • xTaskToSuspend:待挂起任务的任务句柄,为NULL表示挂起任务自身。
  • 需将宏INCLUDE_vTaskSuspend配置为 1。

5.1.2 任务恢复函数vTaskResume()

void vTaskResume( TaskHandle_t xTaskToResume )

  • INCLUDE_vTaskSuspend必须定义为 1。
  • 不论任务被使用 vTaskSuspend() 挂起多少次,只需调用 vTaskResume() 一次,即可使其继续执行。被恢复的任务会重新进入就绪状态。

5.1.3 任务恢复函数xTaskResumeFromISR()(中断中恢复)

(1)函数说明

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )

返回值如下:

  • pdTRUE:任务恢复后需要进行任务切换。
  • pdFALSE:任务恢复后不需要进行任务切换。

(2)注意事项

  • pdTRUE:任务恢复后需要进行任务切换。
  • pdFALSE:任务恢复后不需要进行任务切换。
  • INCLUDE_vTaskSuspend 和 INCLUDE_xTaskResumeFromISR 必须定义为 1。
  • 在中断服务程序中调用FreeRTOS的API函数时,中断的优先级不能高于FreeRTOS所管理的最高任务优先级。

六 FreeRTOS中断管理

6.1 FreeRTOS中断管理

6.1.1 FreeRTOS的中断管理

         在STM32中,中断优先级是通过中断优先级配置寄存器的高4位 [7:4] 来配置的。因此STM32支持最多16级中断优先级,其中数值越小表示优先级越高,即更紧急的中断。(任务调度的任务优先级相反,是数值越大越优先)。

        FreeRTOS可以与STM32原生的中断机制结合使用,但它提供了自己的中断管理机制,主要是为了提供更强大和灵活的任务调度和管理功能。

        FreeRTOS中,将PendSV(任务切换任务调度时用到的中断)和SysTick设置最低中断优先级(数值最大,15),保证系统任务切换不会阻塞系统其他中断的响应。

        FreeRTOS利用BASEPRI(中断屏蔽管理寄存器)寄存器实现中断管理,屏蔽优先级低于某一个阈值的中断。比如: BASEPRI设置为0x50(只看高四位,也就是5),代表中断优先级在5~15内的均被系统屏蔽(由FreeRTOS管理),0~4的中断优先级正常执行(不由FreeRTOS管理)。

在中断服务函数中调用FreeRTOS的API函数需注意:

  • 中断服务函数的优先级需在FreeRTOS所管理的范围内,阈值由configMAX_SYSCALL_INTERRUPT_PRIORITY指定。
  • 建议将所有优先级位指定为抢占优先级位,方便FreeRTOS管理。
  • 在中断服务函数里边需调用FreeRTOS的API函数,必须使用带“FromISR”后缀的函数。
     

6.1.2 FreeRTOS的开关中断

 FreeRTOS 开关中断函数其实是宏定义,在 portmacro.h 中有定义,如下:

#define portDISABLE_INTERRUPTS()                  vPortRaiseBASEPRI()  
#define portENABLE_INTERRUPTS()                   vPortSetBASEPRI( 0 ) 

6.1.3 FreeRTOS的临界区代码

        临界段代码,又称为临界区,指的是那些必须在不被打断的情况下完整运行的代码段。例如,某些外设的初始化可能要求严格的时序,因此在初始化过程中不允许被中断打断。在FreeRTOS中,进入临界段代码时需要关闭中断,在处理完临界段代码后再重新开启中断。FreeRTOS系统本身包含许多临界段代码,并对其进行了保护。在编写用户程序时,有些情况下也需要添加临界段代码以确保代码的完整性。

与临界段代码保护有关的函数有 4 个:

  • taskENTER_CRITICAL() :进入临界段。
  • taskEXIT_CRITICAL() :退出临界段。
  • taskENTER_CRITICAL_FROM_ISR() :进入临界段(中断级)。
  • taskEXIT_CRITICAL_FROM_ISR():退出临界段(中断级)。

        进入和退出临界段是成对使用的。每进入一次临界段,全局变量uxCriticalNesting都会加一,每调用一次退出临界段,uxCriticalNesting减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这确保了在存在多个临界段代码的情况下,不会因为某个临界段代码的退出而破坏其他临界段的保护。只有当所有的临界段代码都退出时,中断才会被重新使能。

6.1.4 挂起和恢复任务调度器

挂起和恢复任务调度器, 调用此函数不需要关闭中断:

  • vTaskSuspendAll():挂起任务调度器。
  • xTaskResumeAll():恢复任务调度器。

与临界区不同的是,挂起任务调度器时未关闭中断(而临界区会全局禁用中断;这种方式仅仅防止了任务之间的资源争夺,中断仍然可以直接响应;挂起调度器的方法适用于临界区位于任务与任务之间的情况;这样既不需要延迟中断,同时又能确保临界区的安全性。

6.2 FreeRTOS中断管理实验

一些宏的配置

FreeRTOSConfig.h

/* 中断嵌套行为配置 */
#ifdef __NVIC_PRIO_BITS
    #define configPRIO_BITS __NVIC_PRIO_BITS
#else
    #define configPRIO_BITS 4                        //抢占优先级占用位数
#endif

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15                  /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5                   /* FreeRTOS可管理的最高中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY                 ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )          
#define configMAX_SYSCALL_INTERRUPT_PRIORITY            ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_API_CALL_INTERRUPT_PRIORITY           configMAX_SYSCALL_INTERRUPT_PRIORITY

七 FreeRTOS时间片调度

7.1 时间片调度简介

        在FreeRTOS中,同等优先级的任务会轮流分享相同的CPU时间,这个时间被称为时间。在这里,一个时间片的长度等同于SysTick中断的周期。
        默认抢占式调度开启,时间片轮转没开。
        时间片轮转开启:阻塞不会立即切换,等时间片到了才会切换。

时间片调度的相关宏

#define configUSE_TIME_SLICING                          1  //开启时间片调度
#define configUSE_PREEMPTION                            1  //启用抢占式调度
#define configTICK_RATE_HZ          ( ( TickType_t ) 1000 )  
//FreeRTOS频率,默认1000Hz 1s/1000=1ms 即一个时间片1ms

八 FreeRTOS任务相关API函数

8.1 FreeRTOS任务相关API函数介绍

函数

描述

uxTaskPriorityGet()

获取任务优先级

vTaskPrioritySet()

设置任务优先级

uxTaskGetNumberOfTasks()

获取系统中任务的数量

uxTaskGetSystemState()

获取所有任务状态信息

vTaskGetInfo()

获取指定单个的任务信息

xTaskGetCurrentTaskHandle()

获取当前任务的任务句柄

xTaskGetHandle()

根据任务名获取该任务的任务句柄

uxTaskGetStackHighWaterMark()

获取任务的任务栈历史剩余最小值

eTaskGetState()

获取任务状态

vTaskList()

以“表格”形式获取所有任务的信息

vTaskGetRunTimeStats()

获取任务的运行时间

/*
  函数说明:通过指定句柄,获取任务优先级
  参数:任务句柄
  返回值类型:UBaseType_t
*/
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
/*
  函数说明:设置任务优先级
  参数1:任务句柄
  参数2:任务新优先级,最大值configMAX_PRIORITIES - 1
  返回值类型:空
*/
void vTaskPrioritySet( TaskHandle_t xTask,UBaseType_t uxNewPriority );
/*
  函数说明:获取系统中任务的数量,包括空闲任务和软件定时器任务(打开的话)
  参数:无
  返回值类型:UBaseType_t
*/
UBaseType_t uxTaskGetNumberOfTasks( void );
/*
  函数说明:任务信息统计
  定义宏configUSE_TRACE_FACILITY == 1
  参数1:TaskStatus_t结构体数组。包含任务名,任务句柄,任务编号,优先级等
  参数2:数组长度
  参数3:总的运行时间
  返回值类型:UBaseType_t
*/
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
                                  const UBaseType_t uxArraySize,
                                  configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime )
/*
  函数说明:获取单个任务状态信息
  需要定义宏configUSE_TRACE_FACILITY == 1
  参数1:句柄等
  参数2:任务状态结构体
  参数3:获取剩余栈的空间
  参数4:任务状态,要看状态必须是eInvalid
  返回值类型:空
*/
void vTaskGetInfo( TaskHandle_t xTask,
                   TaskStatus_t * pxTaskStatus,
                   BaseType_t xGetFreeStackSpace,
                   eTaskState eState );
/*
  函数说明:获取当前任务句柄
  需要定义宏( INCLUDE_xTaskGetCurrentTaskHandle == 1 ) || ( configUSE_MUTEXES == 1 )
  参数:空
  返回值类型:TaskHandle_t
*/
TaskHandle_t xTaskGetCurrentTaskHandle( void );
/*
  函数说明:根据任务名获取任务句柄
  需要定义宏:INCLUDE_xTaskGetHandle == 1
  参数:任务名
  返回值类型:TaskHandle_t句柄
*/
TaskHandle_t xTaskGetHandle( const char * pcNameToQuery );
/*
  函数说明:获取任务的任务栈的历史峰值时剩余的最小值
  需要定义宏:INCLUDE_uxTaskGetStackHighWaterMark == 1
  参数:句柄
  返回值类型:UBaseType_t
*/
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
/*
  函数说明:获取任务状态
  需要定义宏:( INCLUDE_eTaskGetState == 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_xTaskAbortDelay == 1 )
  参数:句柄
  返回值类型:eTaskState枚举类
*/
eTaskState eTaskGetState( TaskHandle_t xTask );
/*
  函数说明: 以‘表格’形式获取所有任务信息
  需要定义宏:( configUSE_TRACE_FACILITY == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 )
  参数:字符数组
  返回值类型:空
*/
void vTaskList( char * pcWriteBuffer );
/*
  函数说明: 获取任务所用处理时间量的信息。(需要一个时间标准如定时器2,优先级应高于FreeRTOS所管理的优先级,比系统滴答定时器精度(频率)高10~100倍)
每个任务有两个值:
   1.Abs时间(绝对时间):指实际任务所耗费的总时间,即任务处于“正在运行”状态的总时间
   2.%时间(时间百分比):占处理时间的百分比形式显示

  需要定义宏:
( configGENERATE_RUN_TIME_STATS == 1 ) 
&& ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 )    
&& ( configUSE_TRACE_FACILITY == 1 )

/* 运行时间和任务状态统计相关定义 */
#define configGENERATE_RUN_TIME_STATS  1     /* 1: 使能任务运行时间统计功能, 默认: 0 */
#if configGENERATE_RUN_TIME_STATS            //使能任务运行时间统计功能开启的话,配置以下宏
extern volatile unsigned long ulHighFrequencyTimerTicks;
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()  (    ulHighFrequencyTimerTicks = 0UL)
#define portGET_RUN_TIME_COUNTER_VALUE()     ulHighFrequencyTimerTicks   //计数器,在所创建的更高精度的定时器中累加
#endif
#define configUSE_TRACE_FACILITY             1                  //开启追踪
#define configUSE_STATS_FORMATTING_FUNCTIONS 1        //表格化统计信息的函数

  参数:字符数组
  返回值类型:空
*/
void vTaskGetRunTimeStats( char * pcWriteBuffer );

九 FreeRTOS时间管理

9.1 延时函数介绍

  • vTaskDelay():相对延时。从执行vTaskDelay()函数开始,直到指定延时的时间结束。
  • xTaskDelayUntil():绝对延时。将整个任务的运行周期视为一个整体,适用于需要以固定频率定期执行的任务。

假设有一个定时器,每隔1秒触发一次,希望在每次触发时执行某个任务。如果使用 vTaskDelay 来实现,那么你只能实现任务每秒执行一次,而不能确保任务在每秒的开始时刻执行。但如果你使用 xTaskDelayUntil,你可以指定任务在每秒的开始时刻执行,即使任务执行的时间不同。

/*
  函数说明: 绝对延时
  需要定义宏:
  参数1:
  参数2:
  返回值类型:BaseType_t
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                            const TickType_t xTimeIncrement )

开始前需要获取上次唤醒时间即开始时间
例:

void Task1(void * pvParameters)
{
    /* 一开始就获取开始时间(滴答计数器) */
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();

    while (1)
    {
        LED_Toggle(LED2_Pin);
        for_delay_ms(20);
        vTaskDelayUntil(&xLastWakeTime,500);
    }
}

十  FreeRTOS消息队列

10.1 队列简介

FreeRTOS 中的队列是一种用于任务之间通信的机制,它允许一个任务发送消息给另一个任务。队列是线程安全的数据结构,任务可以通过队列在彼此之间传递数据。有以下关键特点:

  • FIFO顺序:队列采用先进先出 (FIFO) 的顺序,即先发送的消息会被先接收。
  • 线程安全:队列操作是原子的,确保在多任务环境中的数据完整性。
  • 阻塞和非阻塞操作:任务可以通过阻塞或非阻塞的方式发送和接收消息。如果队列满了或者为空,任务可以选择等待直到有空间或者数据可用,或者立即返回。
  • 优先级继承:FreeRTOS 支持基于优先级的消息传递,确保高优先级任务在队列操作期间不会被低优先级任务阻塞。
  • 可变长度项:队列中的项可以是不同长度的数据块,而不是固定大小。

使用队列,任务可以通过发送消息来共享信息,从而更好地协调和同步系统中的不同部分。

10.2 队列相关API函数介绍

10.2.1 创建队列

函数

描述

xQueueCreate()

动态方式创建队列

xQueueCreateStatic()

静态方式创建队列

动态方式创建队列调用的函数 xQueueGenericCreate

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    #define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif
/*
  函数说明: 动态创建队列函数调用函数
  需要定义宏:configSUPPORT_DYNAMIC_ALLOCATION == 1
  参数1:队列长度
  参数2:队列大小
  参数3:队列类型
  返回值类型:QueueHandle_t队列的句柄
*/
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
                                   const UBaseType_t uxItemSize,
                                   const uint8_t ucQueueType )

动态创建队列时,FreeRTOS会在运行时从其内置的堆中为队列分配所需的内存空间。这种方式更加灵活,允许系统根据需要动态调整内存。

相反,静态创建队列要求用户在编译时手动为队列分配内存,而不依赖于FreeRTOS的堆管理。这使得内存的分配在编写代码时就能确定,因此在资源受限或对内存使用有严格要求的嵌入式系统中可能更为合适。

总体而言,动态创建提供了更大的灵活性,但可能会增加堆管理的复杂性。静态创建则更为直观,适用于在编译时就能确定内存分配的情况。选择使用哪种方式通常取决于系统的需求和设计考虑。

10.2.2 往队列写入消息

函数

描述

xQueueSend()

往队列的尾部写入消息

xQueueSendToBack()

同 xQueueSend()

xQueueSendToFront()

往队列的头部写入消息

xQueueOverwrite()

覆写队列消息(只用于队列长度为 1 的情况)

xQueueSendFromISR()

在中断中往队列的尾部写入消息

xQueueSendToBackFromISR()

同 xQueueSendFromISR()

xQueueSendToFrontFromISR()

在中断中往队列的头部写入消息

xQueueOverwriteFromISR()

在中断中覆写队列消息(只用于队列长度为 1 的情况)

/*
  函数说明: 往队列的尾部写入消息
  需要定义宏:( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )  
  参数1:队列句柄
  参数2:要发送到队列里的数据
  参数3:等待时间(写0阻塞式,非0等待时间)
  返回值类型:BaseType_t pdPASS/pdFAIL
*/
BaseType_t xQueueSend( xQueue, pvItemToQueue, xTicksToWait )

10.2.3 从队列读取消息

函数

描述

xQueueReceive()

从队列头部读取消息,并删除消息

xQueuePeek()

从队列头部读取消息

xQueueReceiveFromISR()

在中断中从队列头部读取消息,并删除消息

xQueuePeekFromISR()

在中断中从队列头部读取消息

10.3 队列操作实验

第一步 引入队列头文件

#include "queue.h" /* 队列相关的头文件 */

第二步 在任务创建前创建队列(可以在入口函数或者启动任务中)

/* 队列句柄 */
QueueHandle_t queue1;
/// @brief 入口函数:创建启动任务,启动调度器
void FreeRTOS_Start(void)
{
    /* 创建队列,要在任务开始前创建好 */
    queue1 = xQueueCreate(2,sizeof(uint8_t));
    if (queue1 != NULL)
    {
        printf("queue1_create_successful\r\n");
    }
    else
    {
        printf("queue1_create_fail\r\n");
    }

    ............

}

第三步 往队列中写数据

例如:
字符指针:char *p = "abcdefg";
字符数组:char a[ ] = "abcdef";
p本身有个地址addr1,而p的值为字符串首字符地址addr2
所以传入的参数应该为&p(p本身的地址)。
a本身没有自己的地址,只有首字符的地址。
&a,&a[0],a 都是字符串首字符的地址,无法取到a本身的地址。

注意
发送和接收函数
xQueueSend( xQueue, pvItemToQueue, xTicksToWait )
BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer, TickType_t xTicksToWait )
传入是变量本身的地址

往队列发送时,1.传参:传一个地址。2:处理:取地址对应的值,拷贝到队列中。
小数据(非引用类型)
例:uint32_t a=111;
1.传参:&a;(变量a的地址)
2.根据地址的取值:取到111并且拷贝到队列里。
大数据(引用类型:如指针)
char *p = "abcdefg";
1.传参数:&p;(指针的地址)
2.根据地址取值(指针的地址),拷贝字符数组的地址(指针指向的内容,也就是字符数组的地址)到队列里。

十一 信号量

11.1 信号量的简介

FreeRTOS中的信号量是一种用于任务间同步和资源管理的机制。信号量可以是二进制的(只能取0或1)也可以是计数型的(可以是任意正整数)。信号量的基本操作包括“获取”和“释放”。(底层使用的就是队列)

比如动车上的卫生间,一个卫生间同时只能容纳一个人,由指示灯来表示是否有人在使用。当我们想使用卫生间的时候,有如下过程:

  1. 判断卫生间是否有人使用(判断信号量是否有资源)
  2. 卫生间空闲(信号量有资源),那么就可以直接进入卫生间(获取信号量成功)
  3. 卫生间使用中(信号量没有资源),那么这个人可以选择不上卫生间(获取信号量失败),也可以在门口等待(任务阻塞)

信号量与队列的区别如下:

信号量

队列

主要用于管理对共享资源的访问,确保在同一时刻只有一个任务可以访问共享资源

用于任务之间的数据通信,通过在任务之间传递消息,实现信息的传递和同步。

可以是二进制信号量(Binary Semaphore)或计数信号量(Counting Semaphore)

存储和传递消息的数据结构,任务可以发送消息到队列,也可以从队列接收消息。

适用于对资源的互斥访问,控制任务的执行顺序,或者限制同时访问某一资源的任务数量。

适用于在任务之间传递数据,实现解耦和通信。

11.2 二值信号量

二值信号量(Binary Semaphore)是一种特殊类型的信号量,它只有两个可能的值:0和1。这种信号量主要用于实现对共享资源的互斥访问或者任务之间的同步。

  • 两个状态: 二值信号量只能处于两个状态之一,通常用0和1表示。当信号量的值为0时,表示资源不可用;当值为1时,表示资源可用。
  • 互斥访问: 常用于控制对共享资源的互斥访问,确保在同一时刻只有一个任务可以访问共享资源。任务在访问资源之前会尝试获取信号量,成功则继续执行,失败则等待。
  • 任务同步: 也可以用于任务之间的同步,例如一个任务等待另一个任务完成某个操作。

二值信号量相关函数:

函数

描述

xSemaphoreCreateBinary()

使用动态方式创建二值信号量

xSemaphoreCreateBinaryStatic()

使用静态方式创建二值信号量

xSemaphoreGive()

释放信号量

xSemaphoreGiveFromISR()

在中断中释放信号量

xSemaphoreTake()

获取信号量

xSemaphoreTakeFromISR()

在中断中获取信号量

void vSemaphoreCreateBinary(QueueHandle_t xSemaphore);
创建成功QueueHandle_t类型变量xSemaphore=pdPASS否则pdFALL

BaseType_t xSemaphoreGive( QueueHandle_t xSemaphore) ;
释放成功返回值 pdPASS

BaseType_t xSemaphoreTake( QueueHandle_t xSemaphore, TickType_t xBlockTime  );
获取成功返回值 pdPASS

v开头(主动释放一次 

11.3 计数型信号量

正如二进制信号量可以被认为是长度为 1 的队列那样,计数信号量也可以被认为是长度大于 1 的队列。 信号量的用户对存储在队列中的数据不感兴趣,他们只关心队列是否为空。

计数信号量通常用于两种情况:

  1. 事件计数:在此使用方案中,每次事件发生时,事件处理程序将“给出”一个信号量(信号量计数值递增) ,并且 处理程序任务每次处理事件(信号量计数值递减)时“获取”一个信号量。因此,计数值是 已发生的事件数与已处理的事件数之间的差值。在这种情况下, 创建信号量时计数值可以为零。
  2. 资源管理:在此使用情景中,计数值表示可用资源的数量。要获得对资源的控制权,任务必须首先获取 一个信号量——同时递减信号量计数值。当计数值达到零时,表示没有空闲资源可用。当任务使用完资源时, “返还”一个信号量——同时递增信号量计数值。在这种情况下, 创建信号量时计数值可以等于最大计数值。

计数型信号量相关函数:

函数

描述

xSemaphoreCreateCounting()

使用动态方法创建计数型信号量。

xSemaphoreCreateCountingStatic()

使用静态方法创建计数型信号量

uxSemaphoreGetCount()

获取信号量的计数值

xSemaphoreGive()

释放信号量

/*
参数uxMaxCount:最大计数值 
参数uxInitialCount :初始值
返回值:NULL创建失败  
*/
QueueHandle_t xSemaphoreCreateCounting(const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount );
/*
参数:信号量句柄
返回值:0获取失败 1获取成功
*/
UBaseType_t uxSemaphoreGetCount( QueueHandle_t xSemaphore ) ;

11.4 优先级翻转简介

优先级翻转是一个在实时系统中可能出现的问题,特别是在多任务环境中。该问题指的是一个较低优先级的任务阻塞了一个较高优先级任务的执行,从而导致高优先级任务无法及时完成。

典型的优先级翻转场景如下:

  • 任务A(高优先级):拥有高优先级,需要访问共享资源,比如一个关键数据结构。
  • 任务B(低优先级):拥有低优先级,目前正在访问该共享资源。
  • 任务C(中优先级):位于任务A和任务B之间,具有介于两者之间的优先级。

具体流程如下:

  1. 任务A开始执行,但由于任务B正在访问共享资源,任务A被阻塞等待。
  2. 任务C获得执行权,由于优先级高于任务B,它可以抢占任务B。
  3. 任务C执行完成后,任务B被解除阻塞,开始执行,完成后释放了共享资源。
  4. 任务A重新获取执行权,继续执行。

这个过程中,任务A因为资源被占用而被阻塞,而任务B却被中优先级的任务C抢占,导致任务B无法及时完成。这种情况称为优先级翻转,因为任务C的介入翻转了高优先级任务A的执行顺序。

11.4 互斥信号量

互斥信号量是包含优先级继承机制的二进制信号量。二进制信号量能更好实现实现同步(任务间或任务与中断之间), 而互斥信号量有助于更好实现简单互斥(即相互排斥)。

优先级继承是一种解决实时系统中任务调度引起的优先级翻转问题的机制。在具体的任务调度中,当一个高优先级任务等待一个低优先级任务所持有的资源时,系统会提升低优先级任务的优先级,以避免高优先级任务长时间等待的情况。

优先级继承无法完全解决优先级翻转,只是在某些情况下将影响降至最低。

不能在中断中使用互斥信号量,原因如下:

  • 互斥信号量使用的优先级继承机制要求从任务中(而不是从中断中)获取和释放互斥信号量。
  • 中断无法保持阻塞来等待一个被互斥信号量保护的资源。

互斥信号量相关函数:

函数

描述

xSemaphoreCreateMutex()

使用动态方法创建互斥信号量。

xSemaphoreCreateMutexStatic()

使用静态方法创建互斥信号量。

互斥信号量的获取和释放函数与二值信号量的相应函数相似,但有一个重要的区别:互斥信号量不支持在中断服务程序中直接调用。注意,当创建互斥信号量时,系统会自动进行一次信号量的释放操作。

第一步 开启互斥信号量的宏

#define configUSE_MUTEXES 1

十二 队列集

12.1 队列集

队列集(Queue Set)是 FreeRTOS 中的一种数据结构,用于管理多个队列。它提供了一种有效的方式,通过单个 API 调用来操作和访问一组相关的队列。

在多任务系统中,任务之间可能需要共享数据,而这些数据可能存储在不同的队列中。队列集的作用就是为了更方便地管理这些相关队列,使得任务能够轻松地访问和处理多个队列的数据。

队列集的特点和用法:

  • 集中管理多个队列:队列集允许你将多个相关联的队列组织在一起,方便集中管理。
  • 单一 API 调用:通过单一的 API 调用,任务可以同时操作多个队列,而无需分别处理每个队列。
  • 简化任务代码:对于需要处理多个相关队列的任务,使用队列集可以简化代码,提高可读性和维护性。
  • 提高系统效率:在需要协同工作的任务之间共享和传递数据时,队列集可以提高系统的效率。
  • 协同工作:任务可以更方便地协同工作,共享数据,实现更复杂的任务间通信和同步。

使用队列集时,你需要了解如何创建、添加和访问队列集,以及如何使用队列集 API 进行数据的发送和接收。队列集是 FreeRTOS 提供的一个强大工具,用于更灵活地组织和处理任务之间的数据流。

想象一下你有一个智能家居系统,有一个任务负责处理温度信息,另一个任务负责光照信息。你可能有两个队列,一个用于温度,一个用于光照。现在,通过队列集,你可以方便地管理这两个队列,让控制任务能够在需要时从这两个队列中获取信息,从而更智能地控制环境。

12.2 队列集相关API函数

函数

描述

xQueueCreateSet()

创建队列集

xQueueAddToSet()

队列添加到队列集中

xQueueRemoveFromSet()

从队列集中移除队列

xQueueSelectFromSet()

获取队列集中有有效消息的队列

xQueueSelectFromSetFromISR()

在中断中获取队列集中有有效消息的队列

十三 事件标志组

13.1 事件标准组简介

13.1.1 基本概念

当在嵌入式系统中运行多个任务时,这些任务可能需要相互通信,协调其操作。FreeRTOS中的事件标志组(Event Flags Group)提供了一种轻量级的机制,用于在任务之间传递信息和同步操作。

事件标志组就像是一个共享的标志牌集合,每个标志位都代表一种特定的状态或事件。任务可以等待或设置这些标志位,从而实现任务之间的协同工作。

2.事件位(事件标志)

事件位用于指示事件是否发生。 事件位通常称为事件标志。例如,应用程序可以:

2.事件组

  • 定义一个位(或标志), 设置为 1 时表示“已收到消息并准备好处理”, 设置为 0 时表示“没有消息等待处理”。
  • 定义一个位(或标志), 设置为 1 时表示“应用程序已将准备发送到网络的消息排队”, 设置为 0 时表示 “没有消息需要排队准备发送到网络”。
  • 定义一个位(或标志), 设置为 1 时表示“需要向网络发送心跳消息”, 设置为 0 时表示“不需要向网络发送心跳消息”。
     

事件组就是一组事件位。 事件组中的事件位通过位编号来引用。 同样,以上面列出的三个例子为例:

  • 事件标志组位编号为 0 表示“已收到消息并准备好处理”。
  • 事件标志组位编号为 1 表示“应用程序已将准备发送到网络的消息排队”。
  • 事件标志组位编号为 2 表示“需要向网络发送心跳消息”。

13.1.2 事件组和事件位数据类型

事件组由 EventGroupHandle_t 类型的变量引用。

在事件组中实现的位数(或标志数)取决于是使用 configUSE_16_BIT_TICKS 还是 configTICK_TYPE_WIDTH_IN_BITS 来控制 TickType_t 的类型:

  • 如果 configUSE_16_BIT_TICKS 设置为 1,则事件组内实现的位数(或标志数)为 8; 如果 configUSE_16_BIT_TICKS 设置为 0,则为 24。
  • 如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_16_BITS,则事件组内实现的位数(或标志数)为 8。
  • 如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_32_BITS,则为 24 。
  • 如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_64_BITS,则为 56。

对configUSE_16_BIT_TICKS或configTICK_TYPE_WIDTH_IN_BITS 的依赖源于 RTOS 任务内部实现中用于线程本地存储的数据类型。我们当前的版本不支持configTICK_TYPE_WIDTH_IN_BITS配置,只有configUSE_16_BIT_TICKS配置。

事件组中的所有事件位都 存储在 EventBits_t 类型的单个无符号整数变量中。 事件位 0 存储在位 0 中,事件位 1 存储在位1 中,依此类推。

下图表示一个 24 位事件组,使用 3 个位来保存前面描述的 3 个示例事件。 在图片中,仅设置了事件位2。

13.1.2 事件标志组和信号量的区别

事件标志组(Event Flags Group)和信号量(Semaphore)都是FreeRTOS中用于任务同步和通信的机制,但它们在用途和行为上有一些关键的区别。

事件标志组

信号量

主要用于任务之间的事件通知和同步。每个标志位通常代表一个特定的状态或事件,任务可以等待某些标志的发生或者设置标志来通知其他任务。

用于任务之间的资源控制和同步。信号量通常用来保护共享资源,控制对共享资源的访问,以及在任务之间提供同步。

每个标志位通常代表一个不同的事件,每个标志位只有两个状态,即已设置或未设置。

信号量是一个计数器,可以具有大于1的值,表示可用的资源数量。信号量的计数可以动态增减,而且可以用于实现互斥、同步等场景。

适用于需要向其他任务通知事件发生或等待特定事件的场景,例如数据准备就绪、某个条件满足等。

适用于需要对共享资源进行控制,限制同时访问某个资源的任务数量,以及确保任务按顺序访问共享资源的场景。

任务可以等待多个特定的标志位同时发生,或者等待任意一个标志位发生。

任务等待信号量的发放,当信号量的计数大于零时,任务可以继续执行。

总体来说,事件标志组更侧重于任务间的事件通知和同步,而信号量更侧重于资源的控制和同步。在设计中,根据具体需求选择合适的机制会更有利于系统的设计和性能。

13.2 事件标志组相关API函数

函数

描述

xEventGroupCreate()

使用动态方式创建事件标志组

xEventGroupCreateStatic()

使用静态方式创建事件标志组

xEventGroupClearBits()

清零事件标志位

xEventGroupClearBitsFromISR()

在中断中清零事件标志位

xEventGroupSetBits()

设置事件标志位

xEventGroupSetBitsFromISR()

在中断中设置事件标志位

xEventGroupWaitBits()

等待事件标志位

xEventGroupSync()

设置事件标志位,并等待事件标志位

1.引入头文库

#include "event_groups.h"

2.创建事件标志组

/*
1.函数说明:创建事件标志组
2.参数:void
3.返回值:返回值类型EventGroupHandle_t,为NULL则创建失败
*/
EventGroupHandle_t xEventGroupCreate( void )

3.在FreeRTOSConfig.h中配置影响事件标志组大小的宏

/*
如果 configUSE_16_BIT_TICKS 设置为 1,则事件组内实现的位数(或标志数)为 8;
如果 configUSE_16_BIT_TICKS 设置为 0,则为 24。
如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_16_BITS,则事件组内实现的位数(或标志数)为 8。
如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_32_BITS,则为 24 。
如果 configTICK_TYPE_WIDTH_IN_BITS 设为 TICK_TYPE_WIDTH_64_BITS,则为 56。
*/
#define configUSE_16_BIT_TICKS		0

4.将需要的位置1

/*
1.函数说明:设置事件标志位
2.参数1:事件标准组的句柄
  参数2:设置的bit位
3.返回值:返回值类型EventBits_t(实际为uint32_t)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                                const EventBits_t uxBitsToSet );

5.等待事件标志位

/*
1.函数说明:等待事件标志位
2.参数1:事件组句柄
  参数2:等待的标志位
  参数3:需不需要在等待到时给这位清零,参数为1清零
  参数4:需不需要等待参数2设置的标志位都为1,参数4为pdFALSE不需要等待参数2设置的标志位都为1
  参数5:等待时间
3.返回值:返回值类型EventBits_t(实际为uint32_t),事件组的值
*/
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait );

十四 任务通知

14.1 任务通知的简介

任务通知是 FreeRTOS 中一种用于任务间通信的机制,它允许一个任务(直接)向其他任务发送简单的通知或信号(之前学习的都是发送方向队列发送通知/信号,接收方从队列读通知/信号),以实现任务间的同步和协作。任务通知通常用于替代二值信号量或事件标志组,提供了更轻量级(没有中间的队列)的任务间通信方式。

大多数任务间通信方法通过中间对象,如队列、信号量或事件组。发送任务写入通信对象,接收任务从通信对象读取。当使用直接任务通知时,顾名思义,发送任务直接向接收任务发送通知,而无需中间对象。

每个 RTOS 任务都有一个任务通知组,每条通知均独立运行,都有“挂起”(阻塞)或“非挂起”的通知状态(3种状态 1.初始化 2.正在等待 3.已收到(发送方已发送)),以及一个 32 位通知值(一个组,每一个“格子”都有32位。可以实现多对一)。

每个“格子“有一个通知值,每个通知值有32位的数据。每个通知值和通知组一一对应。

常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 可设置任务通知组中的索引数量(”格子“数量,可以实现多对一,接收多个任务的任务通知)。在 FreeRTOS V10.4.0 版本前,任务只有单条任务通知(即只能一对一,只有一个接收通知的    “格子”),没有任务通知组。

向任务发送“任务通知” 会将目标任务通知设为“挂起”(阻塞)状态(接收方调用接收函数时,会将自己置为阻塞状态)。 正如任务可以阻塞中间对象 (如等待信号量可用的信号量),任务也可以阻塞任务通知, 以等待通知状态变为“挂起”。向任务发送“任务通知”也可以更新目标通知的值(可选),可使用下列任一方法:

  • 覆盖原值,无论接收任务是否已读取被覆盖的值。
  • 覆盖原值(仅当接收任务已读取被覆盖的值时)。先将通知值保存再覆盖新值。
  • 在值中设置一个或多个位。(类似事件标志组)
  • 对值进行增量(加 1)。(类似信号量)

RTOS 任务通知功能默认为启用状态,将configUSE_TASK_NOTIFICATIONS 设为0可以禁用。

14.2 任务相关API函数

任务通知相关函数如下:

函数

描述

xTaskNotify()

发送通知,带有通知值

xTaskNotifyAndQuery()

发送通知,带有通知值并且保留接收任务的原通知值

xTaskNotifyGive()

发送通知,不带通知值

xTaskNotifyFromISR()

在中断中发送任务通知

xTaskNotifyAndQueryFromISR()

vTaskNotifyGiveFromISR()

ulTaskNotifyTake()

获取任务通知,可选退出函数时对通知值清零(相当于二值信号量)或减1(相当于计数型信号量)

xTaskNotifyWait()

获取任务通知,可获取通知值和清除通知值的指定位

注意:发送通知有相关ISR函数,接收通知没有ISR函数,不能在ISR中接收任务通知。

/*
1.函数说明:发送通知(不带通知值)
2.参数1:通知任务的句柄
3.返回值:返回值类型BaseType_t,pdPASS:发送成功 
*/
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify );
/*
1.函数说明:获取任务通知
2.参数1:退出函数时要不要对通知值清零。
        pdFALSE:函数结束时,通知值减1
        pdTRUE:函数结束时,通知值清零(相当于二值信号量)
  参数2:阻塞等待
3.返回值:接收的通知值
*/
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
/*
1.函数说明:获取任务通知(多个能接收通知的“格子”)
           通过设置configTASK_NOTIFICATION_ARRAY_ENTRIES 默认为1
2.参数1:等待接收任务的句柄
  参数2:索引
3.返回值:返回值类型BaseType_t,pdPASS:发送成功 

*/
BaseType_t xTaskNotifyGiveIndexed(TaskHandle_txTaskToNotify, BaseType_t uxIndexToNotify);
/*
1.函数说明:获取任务通知
2.参数1:等待的索引位
  参数2:退出函数时要不要对通知值清零。
        pdFALSE:函数结束时,通知值减1
        pdTRUE:函数结束时,通知值清零(相当于二值信号量)
  参数3:阻塞等待
3.返回值:接收的通知值
*/
uint32_t ulTaskNotifyTakeIndexed(UBaseType_t uxIndexToWaitOn, 
                                 BaseType_t xClearCountOnExit, 
                                 TickType_t xTicksToWait);
/*
1.函数说明:发送通知,带有通知值
2.参数1:准备接收的任务的句柄(发送给谁)
  参数2:发送的值,与参数3配合使用
  参数3:枚举类型
    eNoAction = 0,            /* 通知任务而不更新其通知值. */
    eSetBits,                 /* 在任务通知值中设置位,将第二个参数指定的bit位置1 */
    eIncrement,               /* 增加任务的通知值 */
    eSetValueWithOverwrite,   /* 将任务的通知值设置为特定值,即使任务尚未读取前一个值*/
    eSetValueWithoutOverwrite /* 如果前一个值已被任务读取,则设置任务的通知值。*/
3.返回值:pdTURE发送成功
*/
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,uint32_t ulValue,eNotifyAction eAction );
/*
1.函数说明:获取任务通知,可获取通知值和清除通知值的指定位
2.参数1:接收前需不需要清除指定位的通知值(如果全清就填0xffffffff,设置0不清除)
  参数2:接收后需不需要清除指定位通知值
  参数3:接收值地址
  参数4:阻塞等待
3.返回值:pdTURE发送成功
*/
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                            uint32_t ulBitsToClearOnExit, 
                            uint32_t pulNotificationValue, 
                            TickType_t xTicksToWait );

十五 FreeRTOS软件定时器

15.1 软件定时器的简介

FreeRTOS 中的软件定时器是一种轻量级的时间管理工具,用于在任务中创建和管理定时器。软件定时器是基于 FreeRTOS 内核提供的时间管理功能实现的(参考FreeRTOS系统时钟节拍Systick),允许开发者创建、启动、停止、删除和管理定时器,从而实现在任务中对时间的灵活控制。

软件定时器与硬件定时器的主要区别如下:

软件定时器

硬件定时器

FreeRTOS提供的功能来软件模拟定时器,依赖系统的任务调度器来进行计时和任务调度(超时后,调用超时回调函数)

由芯片或微控制器提供,独立于 CPU,可以在后台运行,不受任务调度器的影响

精度和分辨率可能受到任务调度的影响,适用于对时间精度要求不高的场景

具有更高的精度和分辨率

不需要额外的硬件资源,但可能会增加系统的负载

占用硬件资源,不会增加 CPU 的负载

软件定时器能够让函数在未来的设定时间执行。由定时器执行的函数称为定时器的回调函数。从定时器启动到其回调函数执行之间的时间被称为定时器的周期。简而言之,当定时器的周期到期时,定时器的回调函数会被执行(调用后,会自动重置下一次的超时)。

定时器回调函数在定时器服务任务的上下文中执行,在定时器回调函数中不能调用导致阻塞的API函数。

软件定时器服务任务是任务调度器中的一个特殊任务,专门用于管理和维护软件定时器的正常运行。如果configUSE_TIMERS 设置为1(默认关闭),在开启任务调度器的时候,会自动创建软件定时器服务的任务。它主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数、处理软件定时器命令队列

15.2  软件定时器的状态

FreeRTOS 中的软件定时器有三种状态,分别是:

  • 未创建(Uncreated):软件定时器被创建之前的状态。在这个状态下,定时器的数据结构已经被定义,但尚未通过 xTimerCreate() 函数创建。
  • 已创建(Created):软件定时器已被成功创建,但尚未启动。在这个状态下,可以对定时器进行配置,如设置定时器的周期、回调函数等,但定时器并未开始计时。
  • 已运行(Running):软件定时器已经被启动,正在运行中。在这个状态下,定时器会按照预定的周期定时触发超时事件,执行注册的回调函数。

15.3 单次定时器和周期定时器

在 FreeRTOS 中,软件定时器主要有两种类型:一次性定时器和周期性定时器。

  • 一次性定时器(One-shot Timer): 这种定时器在触发一次超时后就会停止,不再执行。适用于只需在特定时间执行一次任务或动作的场景。
  • 周期性定时器(Periodic Timer): 这种定时器会在每个超时周期都触发一次,循环执行。适用于需要在固定的时间间隔内重复执行任务或动作的场景。

15.4 FreeRTOS软件定时器相关API函数

软件定时器相关函数如下:

函数

描述

xTimerCreate()

动态方式创建软件定时器

xTimerCreateStatic()

静态方式创建软件定时器

xTimerStart()

开启软件定时器定时

xTimerStartFromISR()

在中断中开启软件定时器定时

xTimerStop()

停止软件定时器定时

xTimerStopFromISR()

在中断中停止软件定时器定时

xTimerReset()

复位软件定时器定时

xTimerResetFromISR()

在中断中复位软件定时器定时

xTimerChangePeriod()

更改软件定时器的定时超时时间

xTimerChangePeriodFromISR()

在中断中更改定时超时时间

15.5 使用软件定时器

第一步:配置相关宏

/* 软件定时器相关定义 */
#define configUSE_TIMERS 1                                          /* 1: 使能软件定时器, 默认: 0。使能后需指定下面3个 */
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)        /* 定义软件定时器任务的优先级 */
#define configTIMER_QUEUE_LENGTH 5                                  /* 定义软件定时器命令队列的长度*/
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小*/

第二步:引入软件定时器头文件

#include "timers.h"

第三步:在启动任务中创建软件定时器

/*
1.函数说明:创建软件定时器
2.参数1:定时器名字
  参数2:定时器周期节拍(定时多久)
  参数3:自动重载 pdTRUE:周期性, pdFALSE:一次性
  参数4:ID,用于区分
  参数5:实际类型为函数指针,传入超时回调函数
3.返回值:定时器句柄,NULL创建失败
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName, 
                            const TickType_t xTimerPeriodInTicks,
                            const BaseType_t xAutoReload,
                            void * const pvTimerID,
                            TimerCallbackFunction_t pxCallbackFunction );

第四步:编写超时回调函数

/*
1.函数说明:超时回调函数
2.参数:软件定时器句柄
3.返回值:空
*/
void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

第五步:启动软件定时器/停止定时器

/*
1.函数说明:启动软件定时器
2.参数1:需要启动的定时器的句柄
  参数2:等待时间
3.返回值:pdFAIL启动失败
*/
BaseType_t xTimerStart(TimerHandle_t xTimer,const TickType_t xTicksToWait );
/*
1.函数说明:停止软件定时器
2.参数1:需要停止的定时器的句柄
  参数2:等待时间
3.返回值:pdFAIL启动失败
*/
BaseType_t xTimerStop(TimerHandle_t xTimer,const TickType_t xTicksToWait ) 

十六 Tickless低功耗模式

16.1 低功耗模式

FreeRTOS 的 Tickless 模式是一种特殊的运行模式,用于最小化系统的时钟中断频率,以降低功耗。在 Tickless 模式下,系统只在有需要时才会启动时钟中断,而在无任务要运行时则完全进入休眠状态,从而降低功耗。在滴答中断重启时,会对 RTOS 滴答计数值进行校正调整。

Tickless模式的实现方式通常依赖于微控制器的硬件特性,尤其是低功耗定时器或实时时钟单元。以下是 Tickless 模式的一般工作原理:

  • 空闲任务检测:FreeRTOS 会通过空闲任务(Idle Task)来检测系统是否有任务需要执行。如果没有任务需要执行,系统可以进入休眠状态。
  • 时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器。
  • 时钟中断处理:在时钟中断处理函数中,FreeRTOS 将检查任务的状态并决定是否继续执行。
  • 休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断。在休眠状态下,处理器可以进入更低功耗的模式。
  • 任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务。

在 Tickless 模式下,系统的时钟中断频率明显降低,从而降低了系统的平均功耗。Tickless 模式适用于那些对功耗要求较高、需要长时间运行在低功耗状态的嵌入式系统。比如:电池驱动设备、物联网(IoT)设备、低功耗传感器节点、无线通信模块等。

16.2 Tickless模式详解

STM32F103xC、STM32F103xD和STM32F103xE增强型产品支持三种低功耗模式,可以在要求低功耗、短启动时间和多种唤醒事件之间达到最佳的平衡。

(1)睡眠模式(Sleep Mode)

只有CPU停止,所有外设处于工作状态并可在发生中断/事件时唤醒CPU。

(2)停机模式(Stop Mode)

在保持SRAM和寄存器内容不丢失的情况下,停机模式可以达到最低的电能消耗。在停机模式下,停止所有内部1.8V部分的供电,PLL、HSI的RC振荡器和HSE晶体振荡器被关闭,调压器可以被置于普通模式或低功耗模式。可以通过任一配置成EXTI的信号把微控制器从停机模式中唤醒,EXTI信号可以是16个外部I/O 口之一、PVD的输出、RTC闹钟或USB的唤醒信号。

(3)待机模式(Standby Mode)

在待机模式下可以达到最低的电能消耗。内部的电压调压器被关闭,因此所有内部1.8V部分的供电被切断;PLL、HSI的RC振荡器和HSE晶体振荡器也被关闭;进入待机模式后,SRAM和寄存器的内容将消失,但后备寄存器的内容仍然保留,待机电路仍工作。从待机模式退出的条件是:NRST上的外部复位信号、IWDG复位、WKUP引脚上的一个上升边 沿或RTC的闹钟到时。

注意:在进入停机或待机模式时,RTC、IWDG和对应的时钟不会被停止。

主要使用睡眠模式,任何中断或事件都可以唤醒睡眠模式。Tickless低功耗模式通过调用指令 __WFI 实现睡眠模式

FreeRTOS系统中的所有其它任务都不在运行时(处于阻塞或挂起),会运行空闲任务。所以想不影响系统运行又降低功耗,可以在空闲任务执行的期间,让MCU 进入相应的低功耗模式。

由于滴答定时器频繁中断则会影响低功耗,所以FreeRTOS的Tickless低功耗模式会自动把滴答定时器的中断周期修改为低功耗运行时间,退出低功耗后再补上系统时钟节拍数

16.3 Tickless模式相关配置项

配置项

说明

configUSE_TICKLESS_IDLE

使能低功耗 Tickless 模式,默认0

configEXPECTED_IDLE_TIME_BEFORE_SLEEP

系统进入相应低功耗模式的最短时长,默认2

configPRE_SLEEP_PROCESSING(x)

在系统进入低功耗模式前执行的事务,比如关闭外设时钟

configPOSR_SLEEP_PROCESSING(x)

系统退出低功耗模式后执行的事务,比如开启之前关闭的外设时钟

16.4 使用低功耗模式

第一步:配置相关宏

#define configUSE_TICKLESS_IDLE                         1
#include "freertos_demo.h"                      //引入自定义函数所在的头文件
#define configPRE_SLEEP_PROCESSING( x )         PRE_SLEEP_PROCESSING()  //进入睡眠前的处理函数
#define configPOST_SLEEP_PROCESSING( x )        POST_SLEEP_PROCESSING() //退出睡眠后的处理函数
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2 //睡眠的最短时间 少于这个值不进入睡眠模式

第二步:编写进入/退出低功耗模式的处理函数

/* 宏对应的进入睡眠前处理函数 */
void PRE_SLEEP_PROCESSING()
{
.....
}

/* 宏对应的退出睡眠前处理函数 */
void POST_SLEEP_PROCESSING()
{
........
}
//空闲任务中对低功耗的处理
#if ( configUSE_TICKLESS_IDLE != 0 )
        {
            TickType_t xExpectedIdleTime;

            /* It is not desirable to suspend then resume the scheduler on
             * each iteration of the idle task.  Therefore, a preliminary
             * test of the expected idle time is performed without the
             * scheduler suspended.  The result here is not necessarily
             * valid. */
            xExpectedIdleTime = prvGetExpectedIdleTime();//计算期望睡眠的时间(可以睡多久,下一个执行任务时间-当前时间)

            if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) //如果睡眠时间大于宏定义的最短睡眠时间
            {
                vTaskSuspendAll();
                {
                    /* Now the scheduler is suspended, the expected idle
                     * time can be sampled again, and this time its value can
                     * be used. */
                    configASSERT( xNextTaskUnblockTime >= xTickCount );
                    xExpectedIdleTime = prvGetExpectedIdleTime();

                    /* Define the following macro to set xExpectedIdleTime to 0
                     * if the application does not want
                     * portSUPPRESS_TICKS_AND_SLEEP() to be called. */
                    configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING( xExpectedIdleTime );

                    if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
                    {
                        traceLOW_POWER_IDLE_BEGIN();
                        portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );//更改
                        traceLOW_POWER_IDLE_END();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TICKLESS_IDLE */
    }

启动调度器
        ==》空闲任务
                ==》计算可以睡眠的时间(时间=下一个执行时间-当前时间
                ==》disable-irq
                ==》睡眠前处理函数
                ==》__wfi进入睡眠
                ==》睡眠后处理函数

十七 FreeRTOS内存管理

在使用 FreeRTOS 创建任务、队列、信号量等对象时,通常都有动态创建和静态创建的方式。动态方式提供了更灵活的内存管理,而静态方式则更注重内存的静态分配和控制。

如果是动态创建的,那么标准 C 库 malloc() 和 free() 函数有时可用于此目的,但是有以下缺点:

  • 它们在嵌入式系统上并不总是可用。
  • 它们占用了宝贵的代码空间。
  • 它们不是线程安全的。
  • 它们不是确定性的 (执行函数所需时间将因调用而异)。
  • ...

所以更多的时候需要的不是一个替代的内存分配实现。一个嵌入式/实时系统的 RAM 和定时要求可能与另一个非常不同,所以单一的 RAM 分配算法将永远只适用于一个应用程序子集。为了避免此问题,FreeRTOS 将内存分配 API 保留在其可移植层,提供了五种内存管理算法:

  • heap_1:最简单,不允许释放内存。
  • heap_2:允许释放内存,但不会合并相邻的空闲块。
  • heap_3:简单包装了标准 malloc() 和 free(),以保证线程安全。
  • heap_4:合并相邻的空闲块以避免碎片化。包含绝对地址放置选项。
  • heap_5:如同 heap_4,能够跨越多个不相邻内存区域的堆。

17.2 FreeRTOS内存管理算法

17.2.1 heap_1算法

heap_1 是最简单的实现方式。内存一经分配,它不允许内存再被释放。尽管如此,heap_1.c 还是适用于大量嵌入式应用程序。这是因为许多小型和深度嵌入的应用程序在系统启动时创建了所需的所有任务、队列、信号量等,并在程序的生命周期内使用所有这些对象(直到应用程序再次关闭或重新启动)。任何内容都不会被删除。

17.2.2 heap_2算法

heap_2 使用最佳适应算法,并且与方案 1 不同,它允许释放先前分配的块,它不将相邻的空闲块组合成一个大块。

heap_2.c 适用于许多必须动态创建对象的小型实时系统 。

  • 如果动态地创建和删除任务,且分配给正在创建任务的堆栈大小总是相同的,那么 heap2.c 可以在大多数情况下使用。
  • 但是,如果分配给正在创建任务的堆栈的大小不是总相同,那么可用的空闲内存可能会被碎片化成许多小块,最终导致分配失败。

heap_2 使用最佳适应算法,该算法在空闲内存中选择与请求的内存大小最接近的块来分配内存。下面是一个简单的例子来说明最佳适应算法:

假设有一个空闲内存,其中包含以下块:

  • 大小为 20 字节的空闲块。
  • 大小为 15 字节的空闲块。
  • 大小为 25 字节的空闲块。

现在有一个任务请求分配 18 字节的内存。最佳适应算法将选择大小为 20 字节的块,因为它与请求的大小最接近。在选择这个块后,分配器可能会将该块分割为两部分,一部分大小为 18 字节,用于任务的内存,另一部分大小为 2 字节,留作未分配的块。

17.2.4 heap_3算法

heap_3使用 C 库的 malloc 和 free 函数来进行内存分配和释放。它通过分配固定大小的块来管理内存,这些块的大小在配置 FreeRTOS 时进行定义,不会动态改变。

假设我们使用 Heap_3 管理内存,其中块的大小固定为 32 字节。初始时,整个内存被分割成大小为 32 字节的块:

  • 块 1(32 字节)。
  • 块 2(32 字节)。
  • 块 3(32 字节)。

现在,有一个任务请求分配 20 字节的内存。Heap_3 算法将选择块 1,并将其分割成两部分:

  • 分配给任务的内存块(20 字节)。
  • 剩余未分配的块(12 字节)。

再假设另一个任务请求分配 40 字节的内存。由于没有足够大的块可供分配,heap_3 将返回分配失败的状态。

heap_3 的特点是块大小固定,这样可以简化内存管理。然而,也因为块大小不可变,可能导致内存碎片问题,即一些块可能无法完全被利用,从而浪费了一些内存。

17.2.4 heap_4算法

heap_4使用第一适应算法,并且会将相邻的空闲内存块合并成大内存块,减少内存碎片。

第一适应算法会在可用内存块中选择第一个足够大的内存块进行分配。

假设有一个内存块链表,其中包含以下顺序的内存块:

  • 大小为 40 字节的块。
  • 大小为 30 字节的块。
  • 大小为 15 字节的块。
  • 大小为 20 字节的块。

如果一个任务需要申请 25 字节的内存,第一适应算法将选择大小为 40 字节的块,因为它是第一个足够大以容纳任务需求的内存块。(如果是heap_2的最佳适应算法,会选择30字节的块)

17.2.5 heap_5算法

heap_5使用与 heap_4 相同的第一适应和内存合并算法,允许堆跨越多个不相邻(非连续)内存区域。适用于内存地址不连续的复杂场景。

17.3 FreeRTOS内存管理相关API函数介绍

内存管理相关函数如下(由算法管理调用):

函数

描述

void * pvPortMalloc( size_t  xWantedSize );

申请内存

void  vPortFree( void * pv );

释放内存

size_t  xPortGetFreeHeapSize( void );

获取当前空闲内存的大小

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐