当前位置: 首页 > article >正文

freeRTOS学习笔记

FreeRTOS介绍

官网:https://freertos.org/

  • 任务调度:FreeRTOS通过任务调度器管理多个任务,支持不同优先级的任务,实现任务的有序执行。

  • 任务通信和同步:提供了队列、信号量等机制,支持任务之间的通信和同步,确保数据的安全传递。

  • 内存管理:提供简单的内存管理机制,适用于嵌入式环境,有效利用有限的内存资源。

  • 定时器和中断处理:支持定时器功能,能够处理中断,提供了可靠的实时性能。

  • 开发社区:拥有庞大的用户社区,开发者可以在社区中获取支持、解决问题,并分享经验。

  • 可移植性:设计注重可移植性,可以轻松地移植到不同的硬件平台,提高了代码的重用性。

任务调度

  • 抢占式调度:FreeRTOS任务优先级:优先级数值越大,优先级越高高优先级任务阻塞(延时或等待),会让其他就绪态中优先级最高的任务执行

  • 时间片轮转:任务优先级相同的,按照时间片调度,每个任务执行一个时间片(一次系统时钟中断)任务执行不足一个时间片(或阻塞),会立即切换到其他任务执行

  • 任务状态:

Ø 运行态:当任务实际执行时,它被称为处于运行状态。如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。注意在STM32中,同一时间仅一个任务处于运行态。

Ø 就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。

Ø 阻塞态:如果任务当前正在等待延时或外部事件,则该任务被认为处于阻塞状态。

Ø 挂起态:类似暂停,调用函数 vTaskSuspend() 进入挂起态,需要调用解挂函数vTaskResume()才可以进入就绪态。

只有就绪态可转变成运行态

在这里插入图片描述

命名规范:

数据类型重定义

portmacro.h

port+类型

portCHARchar
portSHORTshort
portLONGlong
portTickTypeunsigned short int / unsigned int
portBASE_TYPElong

变量名

类型缩写+变量名

cXX char

pcXX char *

xXXX portBASE_TYPE

函数名

返回值类型+文件名+功能名

vTaskPrioritySet()

xQueueReceive()

vSemaphoreCreateBinary()

头文件名+宏名

前缀对应头文件
portportable.h
tasktask.h
pdprojdefs.h
configFreeRTOSConfig.h
errprojdefs.h

FreeRTOS移植

(1) 下载源码包

  • 官网地址:https://www.freertos.org/

(2) source (基础的freeRTOS功能文件)

在这里插入图片描述

(3) portable (与内核硬件交互的文件 ) => 要根据芯片的内核类型修改不同的文件

在这里插入图片描述

(4) demo中的对应使用的芯片版本的config.h文件 => 选择对应的芯片型号STM32F103 选择

在这里插入图片描述

(5) include (添加所有的文件声明)

在这里插入图片描述

FreeRTOS的任务创建和删除案例

0. 准备
  • 选择时钟时不选Systic

在这里插入图片描述

  • stm32f1xx_it.c
  • 添加头文件
#include "FreeRTOS.h"
#include "task.h"
  • 注释掉函数 SVC_Handler PendSV_Handler
  • 在SysTick_Handler写
//   /* USER CODE BEGIN PendSV_IRQn 1 */
extern void xPortSysTickHandler( void );
//   /* USER CODE END PendSV_IRQn 1 */
// }

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE END SysTick_IRQn 0 */

  /* USER CODE BEGIN SysTick_IRQn 1 */
  if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
  {
    xPortSysTickHandler();
  }
  /* USER CODE END SysTick_IRQn 1 */
}
  • FreeRTOSConfig.h中宏定义
#define xPortPendSVHandler  PendSV_Handler
#define vPortSVCHandler     SVC_Handler
#define INCLUDE_xTaskGetSchedulerState   1
1. 动态创建

(1) 运行函数的本体 函数指针 task1

(2) 任务的名称 -> 随便起 只能用来看 没啥用

(3) 提供栈大小 -> 推荐使用的最小空间是128

(4) 优先级 -> 越大优先级越高

(5) 句柄 -> 填写进去一个指针 会对应的赋值 -> 是当前任务的唯一标识

(6) 函数指针的参数 -> 参数可以是任意类型的指针

例:

// 启动任务需要使用到的参数配置
// 1. 任务运行的函数指针
void Start_Task(void *pvParameters);
// 2. 任务名称
// 3. 任务堆栈大小
#define START_TASK_STACK_DEPTH 128
// 4. 传递进函数的参数  NULL
// 5. 任务的优先级
#define START_TASK_PRIORITY 1
// 6. 任务句柄  用于标识对应的任务
TaskHandle_t start_task;
...
    xTaskCreate((TaskFunction_t)Start_Task,
                (char *)"Start_Task",
                START_TASK_STACK_DEPTH,
                NULL,
                START_TASK_PRIORITY,
                &start_task);
    printf("启动任务创建程序\n");
2. 动态创建任务的流程

(0)配置文件中 #define configSUPPORT_DYNAMIC_ALLOCATION 1

(1) main -> freeRTOS_start() -> 创建启动任务 -> 启动调度器 ( 相当于打开freeRTOS操作系统 )

(2) 一旦启动调度器 -> 之后的代码都不会按照顺序执行了 -> 会根据优先级抢占执行

(3) 在启动任务当中 -> 进入临界区 提高当前代码的优先级 -> 任意创建不同优先级的任务 而不会被抢占 -> 记得删除自身

(4) 之后的任务都会严格按照调度器优先级来运行

#include "FreeRTOS_demo.h"

// 创建3个task任务
// (1) 每0.5s翻转一次LED1
// (2) 每0.5s翻转一次LED2
// (3) 接收按键的信息 => 能够删除task1

// 启动任务需要使用到的参数配置
// 1. 任务运行的函数指针
void Start_Task(void *pvParameters);
// 2. 任务名称
// 3. 任务堆栈大小
#define START_TASK_STACK_DEPTH 128
// 4. 传递进函数的参数  NULL
// 5. 任务的优先级
#define START_TASK_PRIORITY 1
// 6. 任务句柄  用于标识对应的任务
TaskHandle_t start_task;

// task1的参数配置
void Task1(void *pvParameters);
#define TASK1_STACK_DEPTH 128
#define TASK1_PRIORITY 2
TaskHandle_t task1;

// task2的参数配置
void Task2(void *pvParameters);
#define TASK2_STACK_DEPTH 128
#define TASK2_PRIORITY 3
TaskHandle_t task2;

// task3的参数配置
void Task3(void *pvParameters);
#define TASK3_STACK_DEPTH 128
#define TASK3_PRIORITY 4
TaskHandle_t task3;

void Free_RTOS_Start(void)
{
    /* 1. 创建第一个启动任务  作用是将3个task任务启动起来 */
    xTaskCreate((TaskFunction_t)Start_Task,
                (char *)"Start_Task",
                START_TASK_STACK_DEPTH,
                NULL,
                START_TASK_PRIORITY,
                &start_task);
    printf("启动任务创建程序\n");
    /* 2. 启动freeRTOS调度器 */
    vTaskStartScheduler();
}

// 启动任务用于将3个task创建出来
void Start_Task(void *pvParameters)
{
    /* 3. 已经进入到freeRTOS系统中  之后创建的任务task都会直接调度生效 */
    // 3.0 进入临界区  ->  将之后的代码的优先级提升到最高 -> 一直持续到退出临界区
    taskENTER_CRITICAL();
    // 3.1 创建task1
    xTaskCreate((TaskFunction_t)Task1,
                (char *)"Task1",
                TASK1_STACK_DEPTH,
                NULL,
                TASK1_PRIORITY,
                &task1);
    printf("task1创建完成\n");
    // 3.2 创建task2
    xTaskCreate((TaskFunction_t)Task2,
                (char *)"Task2",
                TASK2_STACK_DEPTH,
                NULL,
                TASK2_PRIORITY,
                &task2);
    printf("task2创建完成\n");
    // 3.3 创建task3
    xTaskCreate((TaskFunction_t)Task3,
                (char *)"Task3",
                TASK3_STACK_DEPTH,
                NULL,
                TASK3_PRIORITY,
                &task3);
    printf("task3创建完成\n");
    /* 4. 启动任务在完成了任务创建之后  需要将自己删除 */
    vTaskDelete(NULL);
    /* 5. 退出临界区 */
    taskEXIT_CRITICAL();
}

// 完成task1的具体任务
void Task1(void *pvParameters)
{
    while (1)
    {
        printf("任务1正在运行\n");
        // 每次调用使用 翻转LED1
        LED_Toggle(LED1_Pin);
        // 延迟500ms
        vTaskDelay(500);
    }
}

void Task2(void *pvParameters)
{
    while (1)
    {
        printf("任务2正在运行\n");
        // 每次调用使用 翻转LED2
        LED_Toggle(LED2_Pin);
        // 延迟500ms  -> 不能使用hal库的延时
        vTaskDelay(500);
    }
}

void Task3(void *pvParameters)
{
    while (1)
    {
        printf("任务3正在运行\n");
        if (Key_Detect() == KEY1_PRESS)
        {
            // 按键1被按下
            // 加健壮性判断
            if (task1 != NULL)
            {
                // 删除task1
                vTaskDelete(task1);
                task1 = NULL;
            }
        }

        // 高优先级的任务必须要有对应的阻塞 -> 分一部分时间片给低优先级
        vTaskDelay(10);
    }
}

3. 静态创建任务

创建静态任务的宏定义:

#define configSUPPORT_STATIC_ALLOCATION                 1

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

例:

// task1的参数配置
void Task1(void *pvParameters);
#define TASK1_STACK_DEPTH 128
#define TASK1_PRIORITY 2
TaskHandle_t task1;
StackType_t task1StackBuffer[TASK1_STACK_DEPTH];
StaticTask_t task1TCB;
...
task1 = xTaskCreateStatic((TaskFunction_t)Task1,
                (char *)"Task1",
                TASK1_STACK_DEPTH,
                NULL,
                TASK1_PRIORITY,
                task1StackBuffer,
                &task1TCB);

另外,创建静态函数还需要实现两个不可见任务:

  1. 空闲任务
  • vApplicationGetIdleTaskMemory()

StackType_t IdleTaskStackBuffer[configMINIMAL_STACK_SIZE];
StaticTask_t IdleTaskTCBBuffer;
/* 空闲任务内存分配 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                   StackType_t **ppxIdleTaskStackBuffer,
                                   uint32_t *pulIdleTaskStackSize)
{
    *ppxIdleTaskStackBuffer = IdleTaskStackBuffer;
    *ppxIdleTaskTCBBuffer = &IdleTaskTCBBuffer;
    *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
  1. 定时器任务
  • vApplicationGetTimerTaskMemory()
StackType_t TimerTaskStackBuffer[configMINIMAL_STACK_SIZE];
StaticTask_t TimerTaskTCBBuffer;
/* 软件定时器内存分配 */
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                        StackType_t **ppxTimerTaskStackBuffer,
                                        uint32_t *pulTimerTaskStackSize)
{
    *ppxTimerTaskStackBuffer = TimerTaskStackBuffer;
    *ppxTimerTaskTCBBuffer = &TimerTaskTCBBuffer;
    *pulTimerTaskStackSize = configMINIMAL_STACK_SIZE;
}

任务的挂起与恢复

任务的挂起与恢复的API函数

vTaskSuspend():挂起任务, 类似暂停,可恢复

vTaskResume():恢复被挂起的任务

xTaskResumeFromISR():在中断中恢复被挂起的任务

void vTaskSuspend( TaskHandle_t xTaskToSuspend );
void vTaskResume( TaskHandle_t xTaskToResume );

挂起与恢复所需宏定义:

#define INCLUDE_vTaskSuspend                            1
#define INCLUDE_xResumeFromISR                          1

中断管理

主要API:

 taskENTER_CRITICAL() //进入临界段。

 taskEXIT_CRITICAL() //退出临界段。


portDISABLE_INTERRUPTS() // 中断屏蔽失能

portENABLE_INTERRUPTS() // 中断屏蔽使能


 /* FreeRTOS可管理的最高中断优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
  • 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
  • 案例:设置管理的优先级范围:5~15。使用两个定时器,一个优先级为4,一个优先级为6。两个定时器每1s,打印一段字符串,当关中断时,停止打印,开中断时持续打印。

在这里插入图片描述

  • main.c
...
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM4) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */
  if (htim->Instance == TIM2)
  {
      // 定时器2触发
      printf("定时器2在运行  优先级是4\r\n");
  }
  else if (htim->Instance == TIM3)
  {
      // 定时器3触发
      printf("定时器3在运行  优先级是6\r\n");
  }
  /* USER CODE END Callback 1 */
}
  • demo.c
void Task3(void *pvParameters)
{
    while (1)
    {
        // 周期性屏蔽中断  ->  周期性打开
        // 屏蔽优先级为configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 到configLIBRARY_LOWEST_INTERRUPT_PRIORITY 5-15
        uint32_t delay = 50000000;
        while (delay--)
            ;
        // 关闭中断之前运行一会
        printf("关闭中断\r\n");
        portDISABLE_INTERRUPTS();
        // 关闭中断之后运行一行
        delay = 50000000;
        while (delay--)
            ;
        printf("开启中断\r\n");
        portENABLE_INTERRUPTS();
    }
}

实验现象:

在这里插入图片描述

时间片调度

  • FreeRTOSconfig.h
#define configUSE_TIME_SLICING 1
#define configUSE_PREEMPTION 1
// 对于相同优先级的任务   每次分配运行的时间等于 1/configTICK_RATE_HZ 
#define configTICK_RATE_HZ ((TickType_t)1000)

任务相关API

uxTaskPriorityGet : 获取任务优先级

vTaskPrioritySet : 设置优先级

uxTaskGetNumberOfTasks : 获取任务数量

uxTaskGetSystemState : 获取状态信息

vTaskList : 使用list表格打印信息

xTaskGetHandle : 通过用户名称获取任务句柄

uxTaskGetStackHighWaterMark : 获取任务栈的历史最小值


vTaskGetRunTimeStats : 时间统计函数

  • FreeRTOSconfig.h
// 定义能够设置优先级
#define INCLUDE_vTaskPrioritySet 1
// 定义能够获取优先级
#define INCLUDE_uxTaskPriorityGet 1

// 获取状态信息
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1

// 获取句柄
#define INCLUDE_xTaskGetHandle 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
  • FreeRTOS_demo.c
...
TaskStatus_t pxTaskStatusArray[5];
char listBuff[500];
void Task2(void *pvParameters)
{
    while (1)
    {
        /* 1. 获取任务优先级 */
        UBaseType_t taskPriority = uxTaskPriorityGet(task1);
        printf("task1的优先级是%d\r\n", taskPriority);
        taskPriority = uxTaskPriorityGet(task2);
        printf("task2的优先级是%d\r\n", taskPriority);

        /* 2. 设置优先级 */
        // 注意如果把别的任务的优先级调高  要注意这个任务里面有没有阻塞
        // 有可能调高之后导致卡在那个任务中  没办法再运行下面的代码
        vTaskPrioritySet(task2, 4);
        taskPriority = uxTaskPriorityGet(task2);
        printf("修改之后的task2的优先级是%d\r\n", taskPriority);

        /* 3. 获取任务数量 */
        UBaseType_t numOfTasks = uxTaskGetNumberOfTasks();
        printf("任务数量为%d\r\n", numOfTasks);

        /* 4. 获取状态信息 */
        uint32_t total_runtime;
        UBaseType_t taskNums;
        taskNums = uxTaskGetSystemState(pxTaskStatusArray, numOfTasks, &total_runtime);

        // 接收到的全部任务
        printf("任务名称\t任务编号\t当前状态\t当前优先级\t基础优先级\t句柄\r\n");

        for (uint16_t i = 0; i < taskNums; i++)
        {

            printf("%s\t\t%d\t\t%d\t\t%d\t\t%d\t\t%d\r\n",
                   pxTaskStatusArray[i].pcTaskName,
                   pxTaskStatusArray[i].xTaskNumber,
                   pxTaskStatusArray[i].eCurrentState,
                   pxTaskStatusArray[i].uxCurrentPriority,
                   pxTaskStatusArray[i].uxBasePriority,
                   (uint32_t)pxTaskStatusArray[i].xHandle);
        }

        /* 5. 使用list表格打印信息 -> 最推荐使用的方法查看整个freeRTOS的任务运行情况 */
        vTaskList(listBuff);
        printf("%s\r\n", listBuff);

        /* 6. 通过用户名称获取任务句柄 */
        TaskHandle_t task1_handle = xTaskGetHandle("Task1");
        printf("%d\r\n", task1_handle);

        /* 7. 获取任务栈的历史最小值  -> 栈空间的使用达到最大的时候剩余的空间 */
        UBaseType_t waterMark = uxTaskGetStackHighWaterMark(task1);
        printf("task1的历史最小值为%d\r\n", waterMark);

        // 末尾添加阻塞
        vTaskDelay(10000);
    }
}
  • 现象:

在这里插入图片描述

时间相关函数:

  • FreeRTOS_demo.c
// 完成task1的具体任务
// 低优先级的任务只能从剩下的50%时间中占比
// 运行时间250ms   延迟时间是500ms  ->  33% / 2 = 17%
void Task1(void *pvParameters)
{
    while (1)
    {
        // printf("任务1正在运行\n");
        // 每次调用使用 翻转LED1
        LED_Toggle(LED1_Pin);
        HAL_Delay(250);
        // 延迟500ms
        vTaskDelay(500);
    }
}
char runTimeBuff[500];

// 只有高优先级的任务  会直接从总时间中划分一部分
// 如果运行时间是10ms   阻塞时间也是10ms  ->  total占比50%
void Task2(void *pvParameters)
{
    while (1)
    {
        uint8_t res = Key_Detect();
        if (res == KEY1_PRESS)
        {
            vTaskGetRunTimeStats(runTimeBuff);
            printf("%s\r\n", runTimeBuff);
        }
        HAL_Delay(10);
        vTaskDelay(10);
    }
}
  • 现象:

在这里插入图片描述

延时函数

vTaskDelay():相对延时。从执行vTaskDelay()函数开始,直到指定延时的时间结束。

xTaskDelayUntil():绝对延时。将整个任务的运行周期视为一个整体,适用于需要以固定频率定期执行的任务。

消息队列

xQueueCreate : 创建队列

xQueueSend : 往队列的尾部写入消息

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

  • FreeRTOS_demo.c
// 队列使用是线程安全的   直接放在全局变量中使用即可
QueueHandle_t queue1;
QueueHandle_t big_queue;

// 提前创建队列
    // 0.1 创建能够存储一个字节的队列
    // 队列的长度   元素的大小

    // 源码结构:
    // (1) 计算队列本体的大小 + Queue_t头信息结构体的大小作为整个队列的大小
    // (2) 根据本身传入的参数  填写对应头信息的值  => 给Queue_t头信息结构体赋值
    queue1 = xQueueCreate(1, sizeof(uint8_t));

    // 0.2 创建一个能够存储长字符串的队列
    big_queue = xQueueCreate(2, sizeof(char *));

...
    
char str[100] = {"asdhkasjdhkahsdasjdkhakshd"};
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key;
    uint8_t value = 8;
    // char *str = "asdhkasjdhkahsdasjdkhakshd";
    // C语言底层  识别字符串指针和字符数组有本质区别
    // 字节数组  str  0号元素的地址 -> 字节的地址   str[0] -> 第一个元素的值
    // 将对应的字节数组的地址声明为字符串指针的时候
    // char * str1 = str;
    // 必须要声明出 后面的值是一个字符串地址
    char *str1 = &str[0];

    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            // 按下key1往queue1中写入uint8
            // 队列的句柄  创建队列的时候声明的元素类型的指针
            taskENTER_CRITICAL();

            // 源码:
            // (0) 声明需要用到的变量   校验填写的参数
            // -> 优先级最高 进入临界区 如果条件判断成功能写入 队列不满 (1) traceQUEUE_SEND( pxQueue ); prvCopyDataToQueue -> 条件成功写入数据
            // (2) 队列满了  ->  判断是否需要等待  -> 不等待直接返回队列满的错误
            //              -> 需要等待  先标记一个等待时间
            // 退出临界区
            // 挂起调度器
            // 设置等待 -> 阻塞自身
            // 恢复调度器 返回 -> 调度别的任务
            BaseType_t send_r = xQueueSend(queue1, &value, portMAX_DELAY);
            // 还没来得及运行下面的打印呢  就task2被打断了
            if (send_r == pdPASS)
            {
                printf("成功写入数据到queue1\n");
            }
            else
            {
                printf("失败写入数据到queue1\n");
            }
            taskEXIT_CRITICAL();
        }
        // 按下key3往big_queue中写入长字符串
        else if (key == KEY3_PRESS)
        {
            // 如果队列存储的元素是字符串  也要写对应元素的地址
            BaseType_t send_r = xQueueSend(big_queue, &str1, portMAX_DELAY);
            if (send_r == pdPASS)
            {
                printf("成功写入数据到big_queue\n");
            }
            else
            {
                printf("失败写入数据到big_queue\n");
            }
        }

        vTaskDelay(10);
    }
}

void Task2(void *pvParameters)
{
    uint8_t res = 0;
    while (1)
    {
        // 直接读取queue1 -> 如果读到数据打印
        // 如果队列为空  会判断是否有等待时间  有的话阻塞自身等待
        // 读队列数据源码:
        // 进入临界区  优先级最高 满足队列不为空 (1) prvCopyDataFromQueue( pxQueue, pvBuffer ); traceQUEUE_RECEIVE( pxQueue );
        // (2) 队列为空 -> 判断等待时间是否为0 -> 不愿意等 -> 返回错误 队列为空
        // (3) 队列不为空 -> 标记等待时间  -> 退出临界区  挂起调度器  -> 进入等待 阻塞自身
        // 返回 调度别的任务
        BaseType_t rece_r = xQueueReceive(queue1, &res, portMAX_DELAY);
        // 一旦有数据写入  ->  因为读取数据的优先级更高  所以强制打断了低优先级的任务
        if (rece_r == pdPASS)
        {
            printf("读取queue1成功,读到的数据是%d\n", res);
        }
        else
        {
            printf("读取queue1失败\n");
        }
    }
}

void Task3(void *pvParameters)
{
    char *buff;
    while (1)
    {
        // 读取队列中的数据  读不到等待  会阻塞自身
        BaseType_t rece_r = xQueueReceive(big_queue, &buff, portMAX_DELAY);
        if (rece_r == pdPASS)
        {
            printf("读取bigqueue成功,读到的数据是%s\n", buff);
        }
        else
        {
            printf("读取bigqueue失败\n");
        }
    }
}
  • 现象:

在这里插入图片描述

信号量

二值信号量

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

xSemaphoreGive : 释放信号量,才创建二值信号量时先释放一下

xSemaphoreTake : 获取信号量

  • FreeRTOS_demo.c
QueueHandle_t sem_bin;
// 创建二值信号量 => 本质就是一个队列  长度为1 => 元素的个数是0
    // 只关心队列的itemSize  0,1
    // 如果为0 => take的时候就会阻塞   调用give 0 -> 1
    // take就不阻塞
    sem_bin = xSemaphoreCreateBinary();
    // 二值信号量一般创建之后都要释放一下
    xSemaphoreGive(sem_bin);

...
    
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        // 每一次按下key1 => 执行一次释放信号量
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            HAL_Delay(10);
            if (Key_Detect() == KEY1_PRESS)
            {
                // 释放
                printf("释放一次\n");
                xSemaphoreGive(sem_bin);
            }
        }
        vTaskDelay(100);
    }
}

void Task2(void *pvParameters)
{
    BaseType_t res = 0;
    while (1)
    {
        // 一直获取信号量  -> 获取成功就打印出来
        // 获取不成功 => 一直等待 阻塞自身
        // (1) 第一次获取直接获取成功
        // (2) 第二次再次获取的时候 就阻塞了
        res = xSemaphoreTake(sem_bin, portMAX_DELAY);
        if (res == pdPASS)
        {
            printf("获取信号量成功\n");
        }
        else
        {
            printf("获取信号量失败\n");
        }
    }
}
  • 现象:

在这里插入图片描述

计数型信号量

xSemaphoreCreateCounting : 创建计数信号量

xSemaphoreGive : 获取信号量的计数值

  • FreeRTOS_demo.c
QueueHandle_t sem_count;
// 创建计数信号量
    sem_count = xSemaphoreCreateCounting(100, 0);

...
    
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        // 每一次按下key1 => 执行一次释放信号量
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            HAL_Delay(10);
            if (Key_Detect() == KEY1_PRESS)
            {
                // 释放  -> 计数型信号量的计数值 +1
                printf("释放一次\n");
                xSemaphoreGive(sem_count);
            }
        }
        vTaskDelay(100);
    }
}

void Task2(void *pvParameters)
{
    BaseType_t count = 0;
    while (1)
    {
        // 一直读取计数型信号量的值  根据值的不同 选择不同的条件判断
        count = uxSemaphoreGetCount(sem_count);
        printf("计数器的值为%d\n",count);
        if (count < 20)
        {
            printf("只能去欢乐谷\n");
        }
        else if (count < 50)
        {
            printf("只能去日本\n");
        }
        else if (count < 100)
        {
            printf("可以去英国\n");
        }
        else
        {
            printf("随便去哪浪\n");
        }

        vTaskDelay(2000);
    }
}
  • 现象:

在这里插入图片描述

互斥信号量
  • 互斥信号量是包含优先级继承机制的二进制信号量,解决优先级翻转问题

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

队列集

在config中配置:

#define configUSE_QUEUE_SETS 1

xQueueCreateSet : 创建队列集

xQueueAddToSet : 队列添加到队列集中

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

  • FreeRTOS_demo.c
QueueSetHandle_t queue_set;
QueueHandle_t queue;
QueueHandle_t sem_bin;

/* 0.创建队列集 队列和信号量 */
    // 填写队列集中有几个队列
    queue_set = xQueueCreateSet(2);
    // 创建队列
    queue = xQueueCreate(5, sizeof(uint8_t));
    // 创建信号量
    sem_bin = xSemaphoreCreateBinary();

    // 将队列和信号量放入到队列集中
    xQueueAddToSet(queue, queue_set);
    xQueueAddToSet(sem_bin, queue_set);

...
    
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    uint8_t value = 8;
    BaseType_t res = 0;
    while (1)
    {
        key = Key_Detect();
        // 写入数据的操作
        // (1) 按下key1  队列写入值
        if (key == KEY1_PRESS)
        {
            res = xQueueSend(queue, &value, portMAX_DELAY);
            if (res == pdPASS)
            {
                printf("向队列中写入%d成功\n", value);
            }
        }

        // (2) 按下key2  释放二值信号量
        else if (key == KEY2_PRESS)
        {
            res = xSemaphoreGive(sem_bin);
            if (res == pdPASS)
            {
                printf("释放信号量成功\n");
            }
        }
    }
}

void Task2(void *pvParameters)
{
    uint8_t value = 0;
    while (1)
    {
        // 队列集的好处是  ->  同时获取多个队列或信号量的值  不会让多个队列信号量直接互相阻塞
        // 使用队列集读取两个队列的数据
        // 队列集的句柄   等待时间
        QueueSetMemberHandle_t xReturn = xQueueSelectFromSet(queue_set, portMAX_DELAY);
        if (xReturn == queue)
        {
            // 读取队列里面新添加的数据
            xQueueReceive(queue, &value, portMAX_DELAY);
            printf("接收到队列中添加的数据%d\n", value);
        }
        else if (xReturn == sem_bin)
        {
            xSemaphoreTake(sem_bin, portMAX_DELAY);
            printf("获取信号量成功\n");
        }
    }
}
  • 现象 :

在这里插入图片描述

事件标志组

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

xEventGroupSetBits : 设置事件标志位

xEventGroupWaitBits : 等待事件标志位

  • FreeRTOS_demo.c
EventGroupHandle_t event_group;
#define EVENT_BIT0 (1 << 0)
#define EVENT_BIT1 (1 << 1)
 /* 创建event_group */
    event_group = xEventGroupCreate();
    if (event_group != NULL)
    {
        printf("创建事件标志组成功\n");
    }
...
    
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        // 必须要消抖才能看出来清空的效果
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            xEventGroupSetBits(event_group, EVENT_BIT0);
        }
        else if (key == KEY2_PRESS)
        {
            xEventGroupSetBits(event_group, EVENT_BIT1);
        }
    }
}

void Task2(void *pvParameters)
{
    EventBits_t res = 0;
    while (1)
    {
        // 读取事件标志组
        // 句柄  等待的位  读取完成之后释放需要清除为0  是否需要等待所有的位 等待时间

        // (1) 本身方法会挂起阻塞  -> 阻塞停止的条件  取决于xWaitForAllBits
        // (2) 判断满足不同条件的结果  -> 返回值那些位置为1
        res = xEventGroupWaitBits(event_group, EVENT_BIT0 | EVENT_BIT1, pdPASS, pdFAIL, portMAX_DELAY);
        if (res & EVENT_BIT0)
        {
            printf("按键1被按下\n");
        }
        else if (res & EVENT_BIT1)
        {
            printf("按键2被按下\n");
        }
        // 必须等待完成多个条件之后  代码才会继续往下
        res = xEventGroupWaitBits(event_group, EVENT_BIT0 | EVENT_BIT1, pdPASS, pdPASS, portMAX_DELAY);
        printf("两个按键都被按下过\n");
    }
}
  • 现象 :

在这里插入图片描述

任务通知

模拟信号量

xTaskNotifyGive : 发送通知,不带通知值

ulTaskNotifyTake : 获取任务通知,可选退出函数时对通知置清零或减1

  • FreeRTOS_demo.c
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        /* 任务1按下key1  发送任务通知 */
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            HAL_Delay(100);
            if (Key_Detect() == KEY1_PRESS)
            {
                // 发送任务通知  ->  给那个任务发送通知
                printf("按键按下 准备给task2发送任务通知\n");
                xTaskNotifyGive(task2);
            }
        }
    }
}

void Task2(void *pvParameters)
{
    while (1)
    {
        /* 任务2挂起等待通知  接收到通知之后  执行后续的代码 */
        // 在获取到之后释放要清空
        ulTaskNotifyTake(pdPASS, portMAX_DELAY);
        printf("获取到任务通知  代码能够继续向下执行\n");
    }
}
  • 现象

在这里插入图片描述


模拟消息邮箱
  • eAction选eSetValueWithOverwrite,模拟计数型信号量
  • eAction选eSetBits,模拟事件标志组

xTaskNotify : 发送通知,带有通知值

/**
        eNoAction = 0,  不对值进行修改  只是简单的通知
         eSetBits,       只对对应的位进行设置  相当于或计算
         eIncrement,     对原有的值+1
        eSetValueWithOverwrite, 直接覆盖写入通知值  不管之前的通知值有没有被读取
         eSetValueWithoutOverwrite 等待之前的通知值被读取使用之后  再进行覆盖写入
    */
xTaskNotify(xTaskToNotify, ulValue, eAction);

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

  • FreeRTOS_demo.c
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        /* 将按键按下得到的值封装起来 通过任务通知发送给task2 */
        key = Key_Detect();
        /**
            eNoAction = 0,  不对值进行修改  只是简单的通知
            eSetBits,       只对对应的位进行设置  相当于或计算
            eIncrement,     对原有的值+1
            eSetValueWithOverwrite, 直接覆盖写入通知值  不管之前的通知值有没有被读取
            eSetValueWithoutOverwrite 等待之前的通知值被读取使用之后  再进行覆盖写入
        */
        // 通知的task句柄  通知的value值  eAction具体的动作
        if (key != 0)
        {
            printf("按键已经按下 ,将按键值通知给task2\n");
            xTaskNotify(task2, key, eSetValueWithOverwrite);
        }
    }
}

void Task2(void *pvParameters)
{
    uint32_t res = 0;
    while (1)
    {
        /* 接收对应的任务通知  根据得到的值不同  进行不同的动作 */
        // 进入等待需要清空的位   退出时需要清空的位  接收value值的地址  等待的时间
        xTaskNotifyWait(0, 0xffffffff, &res, portMAX_DELAY);
        if (res & KEY1_PRESS)
        {
            printf("接收到通知,是key1按下 翻转led1\n");
            LED_Toggle(LED1_Pin);
        }
        else if (res & KEY2_PRESS)
        {
            printf("接收到通知,是key2按下 翻转led2\n");
            LED_Toggle(LED2_Pin);
        }
    }
}
  • 现象:

在这里插入图片描述

模拟事件标志组
  • FreeRTOS_demo.c
#define NOTIFY_BIT0 (1 << 0)
#define NOTIFY_BIT1 (1 << 1)

// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            // 按下key1  -> 在任务通知的第0位标记为1
            printf("按键1被按下  发送事件1通知\n");
            xTaskNotify(task2, NOTIFY_BIT0, eSetBits);
        }
        else if (key == KEY2_PRESS)
        {
            // 按下key2 -> 在任务通知的第1位标记为1
            printf("按键2被按下  发送事件2通知\n");
            xTaskNotify(task2, NOTIFY_BIT1, eSetBits);
        }
    }
}

void Task2(void *pvParameters)
{
    uint32_t event_group = 0; // 用作记录最终的结果值
    uint32_t res = 0;         // 用作每一次的通知接收值
    while (1)
    {
        // 等待接收task2自己的任务通知
        xTaskNotifyWait(0, 0xffffffff, &res, portMAX_DELAY);
        printf("已经接收到通知值为%d\n", res);
        event_group |= res;
        if (event_group == (NOTIFY_BIT0 | NOTIFY_BIT1))
        {
            event_group = 0;
            printf("同时实现两个条件\n");
            LED_Turn_On(LED1_Pin);
        }
    }
}
  • 现象

在这里插入图片描述

软件定时器

软件定时器硬件定时器
FreeRTOS提供的功能来模拟定时器,依赖系统的任务调度器来进行计时和任务调度由芯片或微控制器提供,独立于 CPU,可以在后台运行,不受任务调度器的影响
精度和分辨率可能受到任务调度的影响具有更高的精度和分辨率
不需要额外的硬件资源,但可能会增加系统的负载占用硬件资源,不会增加 CPU 的负载
  • API

xTimerCreate : 动态方式创建软件定时器

xTimerStart : 开启软件定时器定时

xTimerStop : 停止软件定时器定时

  • FreeRTOSconfig.h
/* 软件定时器相关定义 */
#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) /* 定义软件定时器任务的栈空间大小*/
案例 :

task1:用于按键扫描,并对软件定时器进行开启、停止操作

  • FreeRTOS_demo.c
TimerHandle_t timer1_handle;
TimerHandle_t timer2_handle;
// 回调函数在触发之后 通过优先级进入freeRTOS的就绪队列  谁的优先级高调度谁 
//  -> 软件定时器默认的优先级一般都是最高的 
// 如果程序中  有普通任务的优先级高于定时器任务  会造成定时器阻塞无法正常工作
// 如果程序中  有普通任务的优先级和定时器任务一样高 不会影响定时器工作
// 一定要在配置文件中给定时器设置最高任务优先级  ->  max - 1
uint16_t count1 = 0;
void timer1_callback(TimerHandle_t xTimer)
{ 
    printf("定时器1执行了%d次\n",count1++);
}
uint16_t count2 = 0;
void timer2_callback(TimerHandle_t xTimer)
{  
    printf("定时器2执行了%d次\n",count2++);
}

/* 创建定时器 */
// 0.1 一次性定时器
// 定时器名称 定时器周期  释放自动重装载 定时器id  使用的回调函数
uint8_t id1 = 1;
timer1_handle = xTimerCreate("timer1", 500, pdFALSE, (void *)&id1, timer1_callback);
// 0.2 周期性定时器
uint8_t id2 = 2;
timer2_handle = xTimerCreate("timer2", 2000, pdTRUE, (void *)&id2, timer2_callback);

...
 
// 完成task1的具体任务
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            HAL_Delay(50);
            if (Key_Detect() == KEY1_PRESS)
            {
                /* 启动定时器 */
                // 对定时器的启动停止等操作  本质上都是往定时器服务队列中发送对应的消息
                // 定时器服务会周期性读取队列中的消息  执行对应的任务
                printf("按键1被按下  启动定时器\n");
                xTimerStart(timer1_handle, portMAX_DELAY);
                xTimerStart(timer2_handle, portMAX_DELAY);
            }
        }
        else if (key == KEY2_PRESS)
        {
            HAL_Delay(50);
            if (Key_Detect() == KEY2_PRESS)
            {

                printf("按键2被按下 准备停止定时器\n");
                xTimerStop(timer1_handle, portMAX_DELAY);
                xTimerStop(timer2_handle, portMAX_DELAY);
            }
        }
    }
}
  • 实验现象

在这里插入图片描述

Tickless模式

  • Tickless 模式的工作原理:
  • 空闲任务检测:FreeRTOS 会通过空闲任务(Idle Task)来检测系统是否有任务需要执行。如果没有任务需要执行,系统可以进入休眠状态。

  • 时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器。

  • 时钟中断处理:在时钟中断处理函数中,FreeRTOS 将检查任务的状态并决定是否继续执行。

  • 休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断。在休眠状态下,处理器可以进入更低功耗的模式。

  • 任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务。

  • FreeRTOSConfig.h
// 打开系统低功耗
#define configUSE_TICKLESS_IDLE 1
// 配置进入到低功耗之前需要做的用户自定义事件
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
// 退出低功耗之后需要做的事件
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()

其中两个回调函数:

// 进入低功耗模式之前可以做的事情
// 用户只需要根据自己的需求  将一些硬件设备的时钟关闭来实现降低功耗
// 不要重复调用__wfi方法来进入睡眠模式  系统会在合适的位置自动进入
void PRE_SLEEP_PROCESSING()
{
    ...
}

// 退出低功耗模式之后能够做的事情
void POST_SLEEP_PROCESSING()
{
    ...
}
    

// 调度运行的逻辑

// (1) 运行task2 阻塞自身在获取二值信号量的位置

// (2) 运行task1 没有按键按下 进入到delay阻塞 -> 100ms

// (3) 系统会自动判断100ms > configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认2ms

// (4) 先执行POST_SLEEP_PROCESSING方法 -> 进入睡眠模式之前 用户自定义的省电

// (5) 进入睡眠模式 等待唤醒

// (6) delay100ms时间到了 会时钟中断唤醒 -> 自动修正时钟的计数+100 -> Free_RTOS_Start 用户自定义的唤醒之后的工作

// (7) 有按键按下 -> task1 释放二值信号量 -> task2抢占CPU 获取二值信号量

{

            printf("按键2被按下 准备停止定时器\n");
            xTimerStop(timer1_handle, portMAX_DELAY);
            xTimerStop(timer2_handle, portMAX_DELAY);
        }
    }
}

}


- 实验现象

[外链图片转存中...(img-91MTqypP-1730985086910)]

### Tickless模式

- Tickless 模式的工作原理:

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

- FreeRTOSConfig.h

```c
// 打开系统低功耗
#define configUSE_TICKLESS_IDLE 1
// 配置进入到低功耗之前需要做的用户自定义事件
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
// 退出低功耗之后需要做的事件
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()

其中两个回调函数:

// 进入低功耗模式之前可以做的事情
// 用户只需要根据自己的需求  将一些硬件设备的时钟关闭来实现降低功耗
// 不要重复调用__wfi方法来进入睡眠模式  系统会在合适的位置自动进入
void PRE_SLEEP_PROCESSING()
{
    ...
}

// 退出低功耗模式之后能够做的事情
void POST_SLEEP_PROCESSING()
{
    ...
}
    

// 调度运行的逻辑

// (1) 运行task2 阻塞自身在获取二值信号量的位置

// (2) 运行task1 没有按键按下 进入到delay阻塞 -> 100ms

// (3) 系统会自动判断100ms > configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认2ms

// (4) 先执行POST_SLEEP_PROCESSING方法 -> 进入睡眠模式之前 用户自定义的省电

// (5) 进入睡眠模式 等待唤醒

// (6) delay100ms时间到了 会时钟中断唤醒 -> 自动修正时钟的计数+100 -> Free_RTOS_Start 用户自定义的唤醒之后的工作

// (7) 有按键按下 -> task1 释放二值信号量 -> task2抢占CPU 获取二值信号量

// (8) 如果还是没有按键按下 -> 运行delay100 -> 倒头就睡


http://www.kler.cn/a/390408.html

相关文章:

  • Axure网络短剧APP端原型图,竖屏微剧视频模版40页
  • C/C++精品项目之图床共享云存储(3):网络缓冲区类和main
  • Android OpenGL ES详解——立方体贴图
  • 【JAVA】正则表达式中的中括弧
  • 【知识科普】SPA单页应用程序介绍
  • Vue7种组件之间通信方式
  • 如何优化Elasticsearch的查询性能?
  • Mac上无法访问usr/local的文件
  • 【含开题报告+文档+源码】基于SpringBoot的智慧养老医护管理系统
  • Android CarrierConfig 参数项和正则匹配逻辑
  • OAK相机:纯视觉SLAM在夜晚的应用
  • Python——设集P合为A={1,2,4,5},B={x|x**2-5*x+6=0},请计算出集合A与B的并,交,差。
  • 开源模型应用落地-glm模型小试-glm-4-9b-chat-智谱大模型开放平台(七)
  • FASTLIO2建图学习笔记
  • 网络为什么要分层:OSI模型与TCP/IP模型
  • 【大数据学习 | HBASE高级】region split机制和策略
  • GPU性能测试,环境搭建笔记,transformers/huggingface_hub改国内源,BertLayer import 报错
  • Spring Boot编程训练系统:前端与后端集成
  • Android Parcelable和Serializable的区别与联系
  • 面试基础算法题-日常面试足够
  • 网络管理之---3种网络模式配置
  • C++11新特性(二)
  • NFS服务、内核配置菜单
  • JVM学习之路(5)垃圾回收
  • 【Qt】QTreeView 和 QStandardItemModel的关系
  • SpringBoot基础系列学习(五):JdbcTemplate 访问数据库