FreeRTOS基础(三):动态创建任务

2024-06-01 12:20

本文主要是介绍FreeRTOS基础(三):动态创建任务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

     上一篇博客,我们讲解了FreeRTOS中,我们讲解了创建任务和删除任务的API函数,那么这一讲,我们从实战出发,规范我们在FreeRTOS下的编码风格,掌握动态创建任务的编码风格,达到实战应用!

目录

一、任务函数

二、动态创建任务的基本步骤

2.1 使能FreeRTOS的API函数

2.2  定义动态创建任务函数的入口参数

2.3 编写任务函数

2.4 主函数进行调用

2.5 补充  

2.6 任务执行顺序

四、动态创建任务的API函数解析(选学)

五、任务优先级

六、总结


一、任务函数

         不论是动态创建任务还是静态创建任务,我们FreeRTOS都是在任务之间切换执行,那么任务函数就是我们单独要实现的功能,根据功能的不同,把裸机系统分割为⼀个个独立的无限循环且无法返回的函数。我们把这种函数称之为任务。即:任务函数是没有返回值,并且是死循环的!任务的形式:如下:

void task1(void *arg)
{//初始化代码while(1) //⽆限循环且不能返回{具体实现的功能}//延时函数
}

1、为什么FreeRTOS的任务函数没有返回值?(可以将任务理解为线程)

1. 持续运行的任务

       FreeRTOS 任务设计为长期运行,不像普通函数那样有明确的结束点。在嵌入式系统中,任务(或者称为线程)通常负责特定的功能,这些功能需要一直运行。例如,处理传感器数据、管理通信协议或维护系统健康状态等。这些功能需要持续监控和响应外部事件或内部条件,因此任务函数通常设计为死循环。

2. 任务调度

       FreeRTOS 是一个实时操作系统,负责在多个任务之间进行调度。任务函数进入死循环后,会周期性地调用 FreeRTOS 提供的 API 函数(如 vTaskDelayxQueueReceive),这些 API 会将任务置于阻塞状态,直到特定条件满足(延时时间到或者信号量接收到)。这种设计允许 RTOS 进行有效的任务切换,确保系统的实时性和多任务处理能力。

3. 没有返回值

      由于任务函数设计为长期运行,因此它们不需要返回值。任务的结束通常不是通过函数返回来实现的,而是通过其他机制,如任务删除 (vTaskDelete)。任务函数的主要目的是在系统运行过程中持续执行特定操作,而不是像传统函数那样在执行完特定操作后返回。

4. 系统稳定性和资源管理

       任务函数设计为死循环还有助于系统的稳定性和资源管理。在 RTOS 中,任务的生命周期由系统管理,任务函数一旦启动,便由调度器根据优先级和调度策略进行管理。死循环的设计简化了任务的生命周期管理,避免了频繁创建和销毁任务带来的资源开销和复杂性。

2、为什么FreeRTOS任务函数的主体是一个死循环?

1、实时性:

       通过使用死循环,任务可以及时检查事件状态并作出相应的处理,以满足实时性

2、持续性:

       将任务放在一个循环中,可以持续执行。如果任务函数没有死循环,而是在任务完成后直接返回,那么任务将会自动退出。这可能导致任务被删除并释放资源,而无法再次调度执行

3、提高资源的利用率:

     只要任务不退出,就不需要重新获取资源,提高效率。

二、动态创建任务的基本步骤

2.1 使能FreeRTOS的API函数

      在使用FreeRTOS任务创建函数之前,我们需要在配置文件里(FreertosConfig.h)将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1,此时便支持动态创建。利用Ctrl+F搜索即可。

2.2  定义动态创建任务函数的入口参数

        通过上一讲我们知道动态创建任务的API函数如下:

其实,我们需要定义的入口参数就是这个API函数的参数,提前定义好,然后传入参数,他就会自动的为我们创建好对应的任务,并且处于一种就绪态。   从上面我们可以看到:

1、任务函数指针:

       其实就是函数名,我们知道函数名就是函数的入口地址,就是一个函数指针

2、任务名字:

        其实也就是函数名对应的字符串,要用双引号括起来

3、任务堆栈大小:

        动态创建任务,任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 自动从 FreeRTOS 管理的堆中分配,但是我们需要定义好任务栈的大小,使用宏:

#define     START_TASK_STACK_SIZE  128   //定义任务堆栈大小为128字(1字等于4字节)

4、传递给任务的参数:

       不需要传参,我们直接给NULL即可;

5、任务优先级:

        我们使用的是硬件的方式,因此,它要在0-31之间,使用宏定义即可:

#define     START_TASK_PRIO      1    //定义任务优先级,0-31根据任务需求

6、任务句柄:

        这个参数是指向任务控制块的指针,任务控制块TCB其实就是描述任务属性的一个结构体,一次他就是一个结构体指针,我们后续对任务的删除等操作,都是通过该任务句柄进行操作,因此,我们需要提前定义好,然后传入即可,使用宏即可:

TaskHandle_t   start_task_handler;    //定义任务句柄(结构体指针)

      从上面我们可以知道:其实我们只需要提前利用宏定义好三个参数即可,其他的参数只要任务函数编写好,便可以确定。示例如下:

/**********************START_TASK任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define        START_TASK_STACK_SIZE  128   //定义堆栈大小为128字(1字等于4字节)
#define        START_TASK_PRIO         1    //定义任务优先级,0-31根据任务需求
TaskHandle_t   start_task_handler;    //定义任务句柄(结构体指针)
void start_task(void* args);

注意:

  1. 为了编码规范,我们使用的宏都是大写,虽然较长,但是通俗易懂;
  2. 使用API函数进行任务创建,里面的参数需要进行强制转换,以免报错。
  3. 为了任务执行的顺序是按照我们设定好的优先级执行的,我们可以在创建任务的任务中,使用临界段保护,那么在这个任务体中,可以屏蔽中断(中断优先级在5-15之内)比如切换任务的PendSV,此时,我们创建任务的过程中,不会进行任务的调度,然后我们创建任务结束后,在打开临界段保护,此时不会对所有中断进行屏蔽,也就是任务切换PendSV(中断)才会进行任务调度。如下代码所示,在创建任务开始之前和创建任务之后加入,后面详细讲解。
  4. 动态创建任务函数,有返回值,我们可以在编程时,对返回值进行判断,由此可以知道任务是否创建成功!
#include "stm32f4xx.h"                  // Device header
#include "stdio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"/**********************START_TASK任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/#define        START_TASK_STACK_SIZE  128   //定义堆栈大小为128字(1字等于4字节)
#define        START_TASK_PRIO         1    //定义任务优先级,0-31根据任务需求
TaskHandle_t   start_task_handler;    //定义任务句柄(结构体指针)
void start_task(void* args);/**********************TASK1任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define  TASK1_STACK_SIZE  128            //定义堆栈大小为128字(1字等于4字节)
#define  TASK1_PRIO         2             //定义任务优先级,0-31根据任务需求
TaskHandle_t   task1_handler;           //定义任务句柄(结构体指针)
void task1(void* args);/**********************TASK2任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define  TASK2_STACK_SIZE  128            //定义堆栈大小为128字(1字等于4字节)
#define  TASK2_PRIO         3             //定义任务优先级,0-31根据任务需求
TaskHandle_t   task2_handler;           //定义任务句柄(结构体指针)
void task2(void* args);/**********************TASK3任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define  TASK3_STACK_SIZE  128            //定义堆栈大小为128字(1字等于4字节)
#define  TASK3_PRIO         4            //定义任务优先级,0-31根据任务需求
TaskHandle_t   task3_handler;           //定义任务句柄(结构体指针)
void task3(void* args);
开始任务用来创建其他三个任务,只创建一次,不能是死循环,同时创建完3个任务后删除开始任务本身
void start_task(void* args)
{taskENTER_CRITICAL();        /*进入临界区*/BaseType_t xReturn;        //定义接收函数返回值的变量xTaskCreate( (TaskFunction_t)         task1,(char *)     "task1",  ( configSTACK_DEPTH_TYPE)   TASK1_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK1_PRIO ,(TaskHandle_t *)  &task1_handler );//任务1创建结果的判断if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t)         task2,(char *)     "task2",  ( configSTACK_DEPTH_TYPE)   TASK2_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK2_PRIO ,(TaskHandle_t *)  &task2_handler );	//任务2创建结果的判断if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t)          task3,(char *)     "task3",  ( configSTACK_DEPTH_TYPE)   TASK3_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK3_PRIO ,(TaskHandle_t *)  &task3_handler );	//任务3创建结果的判断if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}vTaskDelete(NULL);    //删除开始任务自身,传参NULLtaskEXIT_CRITICAL();   /*退出临界区*///临界区内不会进行任务的调度切换,出了临界区才会进行任务调度,抢占式						
}

2.3 编写任务函数

    对每个任务具体实现的功能进行函数的实现:需要注意,任务函数没有返回值并且是死循环的!

/********其余三个任务的任务函数,无返回值且是死循环***********//***任务1:实现LED0每500ms翻转一次*******/
void task1(void* args)
{while(1){printf("任务1正在运行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_9 );vTaskDelay(500);       //FreeRTOS自带的延时函数,会进行任务切换调度}}/***任务2:实现LED1每500ms翻转一次*******/
void task2(void* args)
{while(1){printf("任务2正在运行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_10 );vTaskDelay(500);       //FreeRTOS自带的延时函数,会进行任务切换调度}}/***任务3:判断按键KEY0,按下KEY0,任务1删除*******/
void task3(void* args)
{while(1){printf("任务3正在运行!\n");if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==0)  //表示按键按下{if(task1_handler!=NULL)  //防止重复删除{printf("删除任务1!\n");vTaskDelete(task1_handler);    //删除任务1,传任务1的句柄task1_handler=NULL;}}	  vTaskDelay(10);    //FreeRTOS自带的延时函数,会进行任务切换调度}}

      此外,我们再自定义一个入口函数,用来创建开始任务,然后将要创建的任务全部放在这个开始任务中,主函数只需调用这个入口函数,即可在这个开始任务中 , 创建其他的任务,这样做,规范代码,梳理代码逻辑,清晰易懂任务的运行顺序!如下所示:

//FreeRTO入口例程函数,无参数,无返回值,用来创建开始任务
void freertos_demo(void)
{xTaskCreate( (TaskFunction_t)     start_task,(char *)     "start_task",  ( configSTACK_DEPTH_TYPE)   START_TASK_STACK_SIZE,(void *)      NULL,(UBaseType_t) START_TASK_PRIO ,(TaskHandle_t *)  &start_task_handler );vTaskStartScheduler();  //开启任务调度器}

2.4 主函数进行调用

        在完成上述的编写后,主函数内部只需要引入对应的头文件,然后在函数内部调用相应的函数对使用到的外设进行初始化,然后调用入口函数即可进行按照我们设定的优先级进行任务的调度,如下所示:

#include "stm32f4xx.h"                  // Device header
#include "stdio.h"
#include "myled.h"
#include "mykey.h"
#include "myusart.h"#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"    //可以用来单独存放任务函数的声明以及配置相关的宏定义,然后直接引入头文件使用extern TaskHandle_t Start_Handle;  
/*使用任务句柄可以对任务操作,如果没有添加上面的单独头文件存放,
那么使用其他文件的全局变量利用extern关键字引入即可。*/int main(void)
{//1、外设初始化My_UsartInit();LED_Init();KEY_Init();//2、调用入口函数freertos_demo();}

2.5 补充  

       为进行模块化的编程,我们可以将创建相应的头文件可以用来单独存放任务函数的声明以及任务配置相关的宏定义,然后在主函数直接引入头文件使用即可,这样工程结构清晰易懂!

2.6 任务执行顺序

        编写完程序后,一定要进行验证,验证程序是否按照我们设定的顺序及进行执行,类似于操作系统的线程同步问题!

       首先主函数调用入口函数,在入口函数内部创建开始任务函数,该开始任务进入就绪状态,启用任务调度器,调度器启动后,FreeRTOS 将接管系统控制,开始调度任务。此时CPU就会去执行开始任务,然后,在开始任务中创建三个任务,注意:由于使用了临界保护:taskENTER_CRITICAL();        /*进入临界区*/  它会对5-15优先级的中断进行屏蔽,即不会发生作用,其中PendSV是用来任务切换的内核中断,它的优先级是13,因此,会被屏蔽,也就是说,我在创建三个任务的过程中,不会进行其他任务的切换,保证我的开始任务创建其他的三个任务不会被打断!!!创建完三个任务后,它们都进入了就绪态,然后,再删除这个开始任务(因为每个任务只需要创建一次,多次创建占用堆栈内存,造成栈溢出!)此时,我在关闭临界区保护,taskEXIT_CRITICAL();   /*退出临界区*/,也就是打开所有中断,此时PendSV中断就会被打开,按照任务的优先级进行抢占式调度,分别执行任务3、任务2、任务1,在三个任务执行的过程中,加入适当的延时,他就会进行任务的切换,去就绪列表寻找优先级最高的任务去运行!

四、动态创建任务的API函数解析(选学)

五、任务优先级

     在 FreeRTOS 中,任务的优先级决定了任务在系统中的调度顺序和执行时机。设定任务优先级是 FreeRTOS 任务创建过程中一个重要的步骤。

1、优先级的范围

FreeRTOS 任务优先级的范围由 configMAX_PRIORITIES 宏定义。该宏在 FreeRTOSConfig.h 文件中定义。通常,优先级的范围是从 0 到 configMAX_PRIORITIES - 1,优先级数值越大,优先级越高。

2、注意事项

  1. 优先级的相对性:任务的优先级是相对的,系统中最高优先级的任务将获得最多的 CPU 时间。如果多个任务具有相同的优先级,调度器会按照时间片轮转或其他调度策略在它们之间切换。

  2. 优先级反转:在某些情况下,低优先级的任务可能会持有高优先级任务所需的资源,导致优先级反转问题。FreeRTOS 提供了优先级继承机制来解决这个问题。

  3. 优先级设定的策略:设定优先级时,需要考虑任务的重要性和时间敏感性。实时性要求高的任务应设定较高的优先级,而非实时任务可以设定较低的优先级。

  4. 避免过高优先级:设定任务优先级时要避免将所有任务都设为过高的优先级,这样会导致系统缺乏灵活性,可能导致低优先级任务得不到执行。

六、总结

         通过以上的介绍,是不是觉得相比裸机开发确实提升了不少的难度,这就是实时性带来的,万事有利必有弊,多看几遍,相信你对动态创建任务的过程会有清晰的认识,其实步骤也是非常简单的,接下来去实践吧!熟练后就不难了,万事开头难!

温馨提示: 

       对于某个需要知道具体函数的实现的,我们可以双击函数然后直接跳转到定义处,或者Ctrl+F 搜索,也可以去官网查看对应的使用实例:https://www.freertos.org/。

      至此,动态创建任务就已经讲解完毕!初次学习,循序渐进,一步步掌握即可!以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!

这篇关于FreeRTOS基础(三):动态创建任务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1020997

相关文章

Python Invoke自动化任务库的使用

《PythonInvoke自动化任务库的使用》Invoke是一个强大的Python库,用于编写自动化脚本,本文就来介绍一下PythonInvoke自动化任务库的使用,具有一定的参考价值,感兴趣的可以... 目录什么是 Invoke?如何安装 Invoke?Invoke 基础1. 运行测试2. 构建文档3.

解决Cron定时任务中Pytest脚本无法发送邮件的问题

《解决Cron定时任务中Pytest脚本无法发送邮件的问题》文章探讨解决在Cron定时任务中运行Pytest脚本时邮件发送失败的问题,先优化环境变量,再检查Pytest邮件配置,接着配置文件确保SMT... 目录引言1. 环境变量优化:确保Cron任务可以正确执行解决方案:1.1. 创建一个脚本1.2. 修

MySQL中my.ini文件的基础配置和优化配置方式

《MySQL中my.ini文件的基础配置和优化配置方式》文章讨论了数据库异步同步的优化思路,包括三个主要方面:幂等性、时序和延迟,作者还分享了MySQL配置文件的优化经验,并鼓励读者提供支持... 目录mysql my.ini文件的配置和优化配置优化思路MySQL配置文件优化总结MySQL my.ini文件

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显