IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> FreeRTOS笔记—第三章 任务管理 -> 正文阅读

[嵌入式]FreeRTOS笔记—第三章 任务管理

3.1 任务基本概念及状态

3.1.1 什么是任务

????????FreeRTOS任务同我日常生活所说的任务概念是一样,指定担任的工作;指定担负的责任。只是FreeRTOS任务是指函数(函数也是为了完成某种功能)。

????????以日常生活为例,比如你做了两件事情,写作业,回复朋友信息。写作业、回复信息就是两个任务。

3.1.2 什么是多任务运行

????????一段时间段内,同时执行多个相对独立的工作(代码)。

????????例如,写一下作业,再回复一下信息,一段时间内同时执行了两件事,这就是多任务运行。

3.1.3 任务状态(State)

? ? ? ? 任务状态简单理解就是任务代码所处的状况(工作状况)。

????????例如,当写作业处于进行状态时,回复信息就处于暂定状态;当回复信息处于进行状态时,写作业处于暂停状态。

????????任务的状态分为2中:运行(Runing)、非运行(Not Running)。对于非运行的状态,还可以继续细分为 就绪状态(Ready),阻塞状态(Blocked),暂停状态(Suspended)。

运行(Running):任务正在执行,此时占用处理器。例如,正在写作业就是运行状态,

就绪(Ready):这个任务完全准备好了,随时可以运行,只是还轮不到它,等待调度,新创建的任务会初始化为就绪态。例如,手机充满电了,随时都可以发货收信息了。

阻塞(Blocked):任务当前正在等待某个事件,比如信号量或外部中断。例如,停止写作业,等待信息,写作业就卡主了(整个状态没在做事了,写作业处于阻塞状态,等收到信息回复完后,再写)。

在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:

  • 任务要等待某个事件,事件发生后它才能运行

  • 在等待事件过程中,它不消耗CPU资源

  • 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件

    • 可以等待一段时间:我等2分钟

    • 也可以一直等待,直到某个绝对时间:我等到下午3点

  • 同步事件:这事件由别的任务,或者是中断程序产生

    • 例子1:任务A等待任务B给它发送数据

    • 例子2:任务A等待用户按下按键

    • 同步事件的来源有很多(这些概念在后面会细讲):

      • 队列(queue)

      • 二进制信号量(binary semaphores)

      • 计数信号量(counting semaphores)

      • 互斥量(mutexes)

      • 递归互斥量、递归锁(recursive mutexes)

      • 事件组(event groups)

      • 任务通知(task notifications)

在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:

  • 10ms之内有数据到来:成功返回

  • 10ms到了,还是没有数据:超时返回

暂停(Suspended)(挂起):处于挂起态的任务对调度器而言是不可见的。例如,不想理朋友了,手机丢在一边去了(把手机挂到一边去了)

FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。

函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend ); //参数如果为NULL,表示暂停自己。

要退出暂停状态,只能由别人来操作:

  • 别的任务调用:vTaskResume

  • 中断程序调用:xTaskResumeFromISR

实际开发中,暂停状态用得不多。

3.1.4 任务优先级(Priority)

写作业,回信息? 任务优先级一样,轮流做;

厨房着火了,什么都别说了,先灭火:优先级更高。

????????FreeRTOS优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。高优先级的任务先运行。相同优先级的、可运行的任务轮流执行。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。 一般不超过32。

3.1.5 栈(Stack)

  • 写作业时,我要记得写到那里了

  • 回信息时,我要记得刚才聊的是啥

  • 做不同的任务,这些细节不一样

  • 对于人来说,当然是记在脑子里

  • 对于程序,是记在栈里

  • 每个任务有自己的栈

3.2 创建任务

3.2.1 创建任务时使用的函数

函数原型:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数(一个死循环函数)
                        const char * const pcName, // 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。长度为:configMAX_TASK_NAME_LEN
                        const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,每个任务都有自己的栈。单位u32,输入10表示是40个字节
                        void * const pvParameters, // 调用任务函数时传入的参数
                        UBaseType_t uxPriority,    // 优先级
                        TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

参数说明:

参数描述
pvTaskCode函数指针,可以简单地认为任务就是一个C函数。
它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)"
pcName任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。
长度为:configMAX_TASK_NAME_LEN
usStackDepth每个任务都有自己的栈,这里指定栈大小。
单位是word,比如传入100,表示栈大小为100 word,也就是400字节。
最大值为uint16_t的最大值。
怎么确定栈的大小,并不容易,很多时候是估计。
精确的办法是看反汇编码。
pvParameters调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
uxPriority优先级范围:0~(configMAX_PRIORITIES – 1)
数值越小优先级越低,
如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务控制块地址。此参数就用来保存这个任务句柄。其他API函数可能会使用到这个句柄,比如修改它的优先级,不操作可以传入NULL。
返回值成功:pdPASS;
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
注意:文档里都说失败时返回值是pdFAIL,这不对。
pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。

3.2.3 示例1: 创建任务

/**
  ******************************************************************************
  * @brief   任务函数 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
void vTask1( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T1 run\r\n"); //打印任务1的信息
        vTaskDelay(5);
	}
}

void vTask2( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T2 run\r\n"); //打印任务2的信息
        vTaskDelay(5);
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task2", 128, NULL, 1, NULL);

    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

模拟仿真运行结果如下:

?注意:

  • task 2先运行!

  • 要分析xTaskCreate的代码才能知道原因:更高优先级的、或者后面创建的任务先运行。

3.2.4 示例2: 传参一个任务函数创建多个任务

/**
  ******************************************************************************
  * @brief   任务函数 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
void vTaskFunction( void *pvParameters )
{
	const char *pcTaskText = pvParameters;

	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf(pcTaskText); //打印任务的信息
        vTaskDelay(5);
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    static const char *pcTextForTask1 = "T1 run\r\n";
    static const char *pcTextForTask2 = "T2 run\r\n";
    
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTaskFunction, "Task1", 128, (void *)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task2", 128, (void *)pcTextForTask2, 1, NULL);

    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

模拟仿真运行结果如下:

?注,多个任务可以使用同一个函数,差别如下:

  • 栈不同

  • 创建任务时可以传入不同的参数

3.3?删除任务

3.3.1 删除任务时使用的函数

函数原型:

void vTaskDelete( TaskHandle_t xTaskToDelete ); //xTaskToDelete 要删除的任务的句柄。传递NULL将导致调用任务被删除。

参数说明:要删除的任务的句柄。传递NULL将*导致调用任务被删除。

参数描述
xTaskToDelete?要删除的任务的句柄。使用xTaskCreate创建任务时可以带回一个句柄。
也可传入NULL,这表示删除自己。

3.3.2 示例: 删除任务

  • 创建任务1:任务1的大循环里,创建任务2,然后休眠一段时间

  • 任务2:打印一句话,然后就删除自己

/**
  ******************************************************************************
  * @brief   任务函数 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
TaskHandle_t xTask2Handle; //任务1句柄

void vTask2( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T2 Delete\r\n"); //打印任务1的信息
        vTaskDelete(xTask2Handle); // 可以直接传入参数NULL, 这里只是为了演示函数用法
	}
}

void vTask1( void *pvParameters )
{
    const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
    BaseType_t ret;
    
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T1 run\r\n"); //打印任务2的信息
        
        ret = xTaskCreate( vTask2, "Task2", 128, NULL, 2, &xTask2Handle );
		if(ret != pdPASS)
        {
			printf("Create Task2 Failed\r\n");
        }
        
		// 如果不休眠的话, Idle任务无法得到执行
		// Idel任务会清理任务2使用的内存
		// 如果不休眠则Idle任务无法执行, 最后内存耗尽,系统崩溃
        vTaskDelay(xDelay100ms);
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    
    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

模拟仿真运行结果如下:

?任务运行图:

  • main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。

  • 任务2的优先级最高,它马上执行。

  • 任务2打印一句话后,就删除了自己。

  • 任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用vTaskDelay()进入Block状态

  • 任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)

  • 时间到后,任务1变为最高优先级的任务继续执行。

  • 如此循环。

任务1的代码中,需要注意的是:xTaskCreate的返回值。

  • 很多手册里说它失败时返回值是pdFAIL,这个宏是0

  • 其实失败时返回值是errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,这个宏是-1

  • 为了避免混淆,我们使用返回值跟pdPASS来比较,这个宏是1

3.4 任务优先级和Tick

3.4.1 任务优先级?

优先级的取值范围优先级越高任务运行顺序
0~(configMAX_PRIORITIES – 1)数值越大高优先级先运行;同优先级,可运行任务,轮流执行
? ? ? ?调度器方法configMAX_PRIORITIES? ? ? ? ? ? ? ? ?大小? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?方法使能
使用C语言实现时对所有的架构都是同样的代码没有限制,但取值越大越浪费 内存 和 时间。configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为 0
使用汇编实现时针对架构相关的优化。取值不能超过 32configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为 1

3.4.2 Tick频率与同级任务轮流

FreeRTOS的Tick频率由FreeRTOSConfig.h文件中configTICK_RATE_HZ决定。决定了任务轮流间隔时间。Tick定时由硬件定时器中断产生,STM32可由内核Tick滴答定时器产生。

#define configTICK_RATE_HZ			( ( TickType_t ) 1000 ) // 1000HZ 就是 1 ms

同优先级的任务怎么切换呢?请看下图:

  • 任务2从t1执行到t2

  • 在t2发生tick中断,进入tick中断处理函数:

    • 选择下一个要运行的任务

    • 执行完中断处理函数后,切换到新的任务:任务1

  • 任务1从t2执行到t3

  • 从下图中可以看出,任务运行的时间并不是严格从t1,t2,t3哪里开始

3.4.3 利用Tick时基延时

使用Tick来衡量时间,比如:

vTaskDelay(2);  // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms

//可使用pdMS_TO_TICKS宏把ms转换为tick,即configTICK_RATE_HZ改变了,我们也不用去修改代码。
vTaskDelay(pdMS_TO_TICKS(100));	 // 等待100ms

注意,基于Tick实现的延时并不精确,比如vTaskDelay(2)的本意是延迟2个Tick周期,有可能经过1个Tick多一点就返回了。取决任务代码量,vTaskDelay()用于要求不严的场合。

如下图:

3.4.3 示例: 优先级实验

创建3个任务:

  • 任务1、任务2:优先级相同,都是1

  • 任务3:优先级最高,是2

/**
  ******************************************************************************
  * @brief   任务函数 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
void vTask1( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T1\r\n"); //打印任务的信息
	}
}

void vTask2( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T2\r\n"); //打印任务的信息
	}
}


void vTask3( void *pvParameters )
{
	const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf( "T3\r\n"); //打印任务的信息
        vTaskDelay(xDelay3000ms); // 如果不休眠的话, 其他任务无法得到执行
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task2", 128, NULL, 1, NULL);
	xTaskCreate(vTask3, "Task3", 128, NULL, 2, NULL);
    
    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

运行情况如下图所示:

  • 任务3优先执行,直到它调用vTaskDelay主动放弃运行

  • 任务1、任务2:轮流执行(任务1运行一遍后,调度时间还没到,继续运行任务1,所以任务1运行两次才轮到任务2;任务2同理)

?调度情况如下图所示:

3.4.4 示例: 代码中修改优先级实验

/* 使用参数xTask来指定任务,设置为NULL表示获取自己的优先级 */
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ); // 获得任务的优先级


/* 使用参数xTask来指定任务,设置为NULL表示设置自己的优先级; 参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1) */
void vTaskPrioritySet( TaskHandle_t xTask,
                       UBaseType_t uxNewPriority ); // 设置任务的优先级

/**
  ******************************************************************************
  * @brief   任务函数 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    Task1,Task2都不会进入阻塞或者暂停状态根据优先级决定谁能运行
  ******************************************************************************
  */
TaskHandle_t xTask2Handle;
 
void vTask1( void *pvParameters )
{
    UBaseType_t uxPriority = uxTaskPriorityGet( NULL ); //得到自己的优先级
    
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		printf("T1\r\n"); //打印任务的信息
        
		printf("Task2 priority  high\r\n" );
		vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) ); //提升Task2的优先级高于Task1, Task2会即刻执行
		
		/* 
            如果Task1能运行到这里,表示它的优先级比Task2高
            那就表示Task2肯定把自己的优先级降低了
 		*/
		printf("Task2 priority low\r\n" );
	}
}

void vTask2( void *pvParameters )
{
    UBaseType_t uxPriority = uxTaskPriorityGet( NULL ); //得到自己的优先级
    
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		/* 
            能运行到这里表示Task2的优先级高于Task1
            Task1提高了Task2的优先级
        */
		printf("T2\r\n"); //打印任务的信息
        
        
		printf("Task2 priority  low\r\n" );
		vTaskPrioritySet( xTask2Handle, ( uxPriority - 2 ) ); //降低Task2自己的优先级,让它小于Task1, Task1得以运行
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 2, NULL);
	xTaskCreate(vTask2, "Task2", 128, NULL, 1, &xTask2Handle);
    
    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

?调度情况如下图所示:

  • 1:一开始Task1优先级最高,它先执行。它提升了Task2的优先级。

  • 2:Task2的优先级最高,它执行。它把自己的优先级降低了。

  • 3:Task1的优先级最高,再次执行。它提升了Task2的优先级。

  • 如此循环。

  • 注意:Task1的优先级一直是2,Task2的优先级是3或1,都大于0。所以Idel任务没有机会执行。

3.4.4 示例: 代码中修改优先级实验

3.5 Delay函数

3.5.1 两个Delay函数

函数原型如下:

// vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
void vTaskDelay( const TickType_t xTicksToDelay ); // xTicksToDelay: 等待多少个Tick周期



/*  vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
    pxPreviousWakeTime: 上一次被唤醒的时间 Tick Count
    xTimeIncrement:     等待多少个Tick 周期
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                            const TickType_t xTimeIncrement );

下面画图说明:

  • 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断

  • 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断

    • 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会

    • 所以可以使用xTaskDelayUntil来让任务周期性地运行

3.5.2 示例: Delay

创建2个任务:

  • Task1:

    • 高优先级

    • 设置变量flag为1,然后调用vTaskDelay(xDelay50ms);vTaskDelayUntil(&xLastWakeTime,?xDelay50ms);

  • Task2:

    • 低优先级

    • 设置变量flag为0

/**
  ******************************************************************************
  * @brief   任务 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
volatile char flag;

void vTask1( void *pvParameters )
{
	const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
	TickType_t xLastWakeTime;
    int i;
    
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
        flag = 1;
        
		for(i=0; i<5; i++) printf("T1\r\n"); //打印任务的信息
        
#if 0 //使用条件开关来选择Delay函数,1改为0就可以使用vTaskDelayUntil
		vTaskDelay(xDelay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
	}
}

void vTask2( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
        flag = 0;
		printf("T2\r\n"); //打印任务的信息
	}
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 2, NULL);
	xTaskCreate(vTask2, "Task2", 128, NULL, 1, NULL);

    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

使用Keil的逻辑分析观察flag变量的bit波形,如下:

  • flag为1时表示Task1在运行,flag为0时表示Task2在运行,也就是Task1处于阻塞状态

  • vTaskDelay:指定的是阻塞的时间

  • vTaskDelayUntil:指定的是任务执行的间隔、周期

3.6 空闲任务(Idle任务)及其钩子函数

3.6.1 介绍

????????在使用?vTaskStartScheduler()?函数来创建、启动调度器时,这个函数内部会创建空闲任务。

空闲任务特点:

  • 空闲任务优先级为0:它不能阻碍用户任务运行

  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

空闲任务作用:

  • 释放被删除的任务的内存

  • 当用户任务处于阻塞状态时,调度器必需找个任务运行,所以要提供个永远不阻塞的空闲任务

  • 空闲任务的循环每执行一次,就会调用一次钩子函数。空闲任务钩子函数(Idle Task Hook Functions)供用户实现一些特殊功能。

3.6.2 空闲任务的钩子函数

空闲任务的钩子函数的作用:

  • 执行一些低优先级的、后台的、需要连续执行的函数

  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。

  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

空闲任务的钩子函数的限制:

  • 不能导致空闲任务进入阻塞状态、暂停状态

  • 如果你会使用?vTaskDelete()?来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

使能空闲任务钩子函数:FreeRTOSConfig.h中,将 configUSE_IDLE_HOOK 宏定义为 1

空闲任务钩子函数:vApplicationIdleHook()

3.7 调度算法

3.7.1 重要概念

????????正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理系统中,任何时间里只能有一个任务处于运行状态。非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。

????????就绪状态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。

????????阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的事件、同步事件。所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"?方法很多,有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。

3.7.2 配置调度算法

????????所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。通过配置文件FreeRTOSConfig.h来配置调度算法:其中 configUSE_TICKLESS_IDLE 是一个高级选项,用于关闭Tick中断来实现省电。现在假设configUSE_TICKLESS_IDLE被设为0,先不使用这个功能。

????????调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。轮转调度并不保证任务的运行时间是公平分配的,还可以细化时间的分配方法。

从3个角度统一理解多种调度算法:

  • 可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)

    • 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。

    • 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)

      • 当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。

      • 其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点

  • 可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)

    • 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片

    • 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占

  • 在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:configIDLE_SHOULD_YIELD)

    • 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务

    • 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊

列表如下:

配置项说明配置项配置方式
ABCDE
常用很少用很少用很少用几乎不用
是否 可抢占configUSE_PREEMPTION11110
是否 时间片轮转configUSE_TIME_SLICING1100x
是否 空闲任务让步configIDLE_SHOULD_YIELD1010x
关闭Tick中断来实现省电configUSE_TICKLESS_IDLE00000

注:

  • A:可抢占+时间片轮转+空闲任务让步

  • B:可抢占+时间片轮转+空闲任务不让步

  • C:可抢占+非时间片轮转+空闲任务让步

  • D:可抢占+非时间片轮转+空闲任务不让步

  • E:合作调度

配置项所在文件

configUSE_PREEMPTION、configIDLE_SHOULD_YIELD配置项在FreeRTOSConfig.h中已定义,只需更加需要修改!

configUSE_TIME_SLICING、configUSE_TICKLESS_IDLE配置项在FreeRTOS.h中,若FreeRTOSConfig.h中 没有定义 则使用FreeRTOS.h中的默认值。因为FreeRTOS.h中先包含了FreeRTOSConfig.h文件。

3.7.3 示例: 调度

修改配置项来观察效果。创建了3个任务:Task1、Task2的优先级都是0,跟空闲任务一样,Task3优先级最高为2。程序里定义了4个全局变量,当某个的任务执行时,对应的变量就被设为1,可以通过Keil的逻辑分析仪查看任务切换情况:

/**
  ******************************************************************************
  * @brief   任务 函数
  * @param   pvParameters: 任务参数指针
  * @retval  None
  * @note    
  ******************************************************************************
  */
static volatile char flagIdleTaskrun = 0;  // 空闲任务运行时flagIdleTaskrun=1
static volatile char flagTask1run = 0;     // 任务1运行时flagTask1run=1
static volatile char flagTask2run = 0;     // 任务2运行时flagTask2run=1
static volatile char flagTask3run = 0;     // 任务3运行时flagTask3run=1

//任务1、任务2代码如下,它们是"连续任务"(continuous task):
void vTask1( void *pvParameters ) //连续任务
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		printf("T1\r\n"); //打印任务的信息			
	}
}

void vTask2( void *pvParameters )
{
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
        
		printf("T2\r\n"); //打印任务的信息
	}
}

void vTask3( void *pvParameters )
{
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );	
    
    pvParameters = pvParameters; //使用一下,防止编译出错
	for( ;; ) //任务函数的主体一般都是无限循环,
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
        
		printf("T3\r\n"); //打印任务的信息
        vTaskDelay( xDelay5ms ); //如果不休眠的话, 其他任务无法得到执行
	}
}

void vApplicationIdleHook(void) //空闲任务的钩子函数
{
	flagIdleTaskrun = 1;
	flagTask1run = 0;
	flagTask2run = 0;
	flagTask3run = 0;	
	
	/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
	printf("Id\r\n");				
}

/**
  ******************************************************************************
  * @brief   main 函数
  * @param   None
  * @retval  None
  * @note    自定义了一个主循环函数,在自动生成的main()函数中调用。为了减少修改自动生成的函数
  ******************************************************************************
  */
void MainLoop(void)
{
    printf("FreeRTOS Test\r\n");

	xTaskCreate(vTask1, "Task1", 128, NULL, 0, NULL);
	xTaskCreate(vTask2, "Task2", 128, NULL, 0, NULL);
	xTaskCreate(vTask3, "Task3", 128, NULL, 2, NULL);
    
    vTaskStartScheduler(); //开启任务调度.一旦启动操作系统,CPU就会跑到任务当中去执行代码
    for( ;; );
}

3.7.4 对比效果: 抢占与否

FreeRTOSConfig.h中,定义这样的宏,对比逻辑分析仪的效果:

// 实验1:抢占
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// 实验2:不抢占
#define configUSE_PREEMPTION		0
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

从下面的对比图可以知道:

  • 抢占时:高优先级任务就绪时,就可以马上执行

  • 不抢占时:优先级失去意义了,既然不能抢占就只能协商了,图中任务1一直在运行(一点都没有协商精神),其他任务都无法执行。即使任务3的vTaskDelay已经超时、即使它的优先级更高,都没办法执行。

3.7.5 对比效果: 时间片轮转与否

FreeRTOSConfig.h中,定义这样的宏,对比逻辑分析仪的效果:

// 实验1:时间片轮转
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// 实验2:时间片不轮转
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      0
#define configIDLE_SHOULD_YIELD		1

从下面的对比图可以知道:

  • 时间片轮转:在Tick中断中会引起任务切换

  • 时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换,可以看到任务1、任务2都运行了很长时间。

3.7.6 对比效果: 空闲任务让步

FreeRTOSConfig.h中,定义这样的宏,对比逻辑分析仪的效果:

// 实验1:空闲任务让步
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// 实验2:空闲任务不让步
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		0

从下面的对比图可以知道:

  • 让步时:在空闲任务的每个循环中,会主动让出处理器,从图中可以看到flagIdelTaskrun的波形很小

  • 不让步时:空闲任务跟任务1、任务2同等待遇,它们的波形宽度是差不多的

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-05-15 11:40:37  更:2022-05-15 11:40:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 0:31:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码