一、RTOS简介

 1 RTOS(实时操作系统,Real-Time Operating System):

                是一种专为实时应用设计的操作系统,它保证任务在严格的时间限制内完成,适用于对响应时间有苛刻要求的场景(如工业控制、自动驾驶、医疗设备等)。  与普通操作系统(Windows、Linux、Android)不同,RTOS最重要的特点不是运行速度快,而是响应时间可预测(Deterministic)。  

  2 RTOS实时操作系统对比通用操作系统:

对比项

RTOS

通用操作系统

响应时间(中断、任务切换)

微秒级(μs)

毫秒级(ms)

确定性(可预测且严格符合预期)

严格保证

不保证

内核大小

几 KB ~ 几十 KB

几 MB ~ 几 GB

适用场景

嵌入式、实时控制

桌面、服务器、移动设备

典型代表

FreeRTOS、RT-Thread、Zephyr

Linux、Windows、macOS

3 RTOS优点:

        可以提高开发效率:

        ①比裸机程序开发简单,降低了时序依赖。

         ②架构更解耦,有利于多人协同开发。

        ③RTOS开源组件丰富,不需要重复造轮子。

4 RTOS 的关键组件:

        ①任务调度器(Scheduler):决定哪个任务何时运行(如优先级调度、时间片轮转)。

        ②同步机制:信号量(Semaphore)、互斥锁(Mutex)、消息队列(Queue)等。

        ③内存管理:动态内存分配(通常避免 malloc,改用静态或池分配)。

        ④中断处理:快速响应硬件中断,将耗时操作交给任务处理(通过中断服务例程,ISR)。

        ⑤时钟管理:提供精确的定时器(如 vTaskDelay() 在 FreeRTOS 中)。

5 RTOS程序框架和裸机框架对比:

      ①裸机程序框架

/*主函数*/
int main()
{
    init();//一些初始化
    /*死循环*/
    while(1)
    {
        do_something_1();//执行一些逻辑
        do_something_2();
    }
}

/*中断服务函数*/
IRQ_Handler()
{
    set_flag();//简短的标记操作
}

      ②RTOS程序框架:

/*主函数*/
int main()
{
    init();//一些初始化
    xTaskCreate(task1);
xTaskCreate(task2);
    vTaskStartScheduler(); //启动调度器
}

/*子任务1(死循环)*/
void task1()
{
    while(1)
    {
        do_something_1();//执行一些逻辑(如采集传感器信息)
        vTaskDelay();
    }
}

/*子任务2(死循环)*/
void task2()
{
    while(1)
    {
        do_something_2();//执行一些逻辑(如执行电机运动)
        vTaskDelay();
    }
}

/*中断服务函数*/
IRQ_Handler()
{
    set_event();//触发事件、信号量等
}

核心思路差异:

裸机框架:本质上是"一个大循环+若干中断"。所有任务逻辑都写在main()里的while(1)中,按顺序轮询执行;紧急或时间敏感的事情放到中断服务程序(ISR)里处理,ISR通常只做最少的事(比如置个标志位或拷贝数据),然后让主循环在下一次轮询时去处理具体逻辑。

RTOS框架:则是把"任务"抽象成独立的执行单元(每个任务有自己的栈、自己的"代码逻辑",写起来就像各自有一个独立的while(1)),调度器负责决    定每个时刻该运行哪个任务。任务之间通过队列、信号量、互斥锁等机制来同步和传递数据。

6 总结

        选裸机:逻辑简单(比如只有几个固定周期的任务)、资源极度受限(几KB RAM的小MCU)、对确定性要求不高但希望代码尽可能精简可控的场景,比如简单的传感器采集、LED控制板。

        选RTOS:任务数量较多、各任务节奏差异大(比如一个任务要1ms响应,另一个任务可以100ms轮询一次)、需要良好的模块化和可维护性、未来功能会持续迭代扩展的项目,比如带网络协议栈、多传感器融合、人机界面的中高端嵌入式产品。

        单片机引入RTOS,相对于裸机主要目的还是为了提高开发效率,使用时可以将各个功能模块分别设计为单独的任务,每个任务都是一个死循环,就相当于有多个main函数,达到并行的效果,这样很多场合就不需要考虑裸机中的时序问题了,比如我们在裸机下驱动SHT20温湿度传感器时,还需要使用状态机来避免影响其他功能模块的调用。

        单片机只有一个CPU(核),那怎么让多个人同时干活呢?其实每个子任务虽然都是死循环,但并不是每个子任务一直都在执行,每个子任务在执行期间,可能需要延时,也可能需要等另一个任务的数据到来,所以在某个任务在等待的时候,CPU就可以停止此任务,然后切换到其它任务执行,这样看起来就是多个人在同时干活了。

二、FreeRTOS内核概述与移植

FreeRTOS 源码概述

    核心文件 FreeRTOS的最核心文件只有2个:

    FreeRTOS/Source/tasks.c

    FreeRTOS/Source/list.c

其他文件的作用列表如下:

image2

2 FreeRTOS移植

2.1 下载freertos源代码

        网址 FreeRTOS™ - FreeRTOS™,下载带LTS后缀的,表示长期支持的版本。

 2.2 复制内核源代码

        目录下的相关文件到代码工程目录下,我们可以基于裸机模板工程。

2.3 对portable目录下的文件进行删减

    保留如下需要的目录:

2.4 对RVDS目录下的文件进行删减

        由于GD32F303是CM4F内核,所以在 RVDS目录下保留。 RVDS:CPU内核相关。

2.5 添加C文件到代码工程

        heap_4.c是位于MemMang目录下,port.c在ARM_CM4F下。

  FreeRTOS的5钟内存管理方法对比:

对比项 heap_1 heap_2 heap_3 heap_4 heap_5
支持动态申请
支持释放内存
合并空闲块 由C库决定
内存碎片 较严重 不确定 很少 很少
多RAM区域 依赖C库
实时性 ★★★★★ ★★★★☆ ★★☆☆☆ ★★★★★ ★★★★★
算法复杂度 最低 较低 不确定 中等 中等
推荐程度 ★★★☆☆ ★☆☆☆☆ ★★☆☆☆ ★★★★★ ★★★★★

添加内核头文件引用路径,内核相关头文件存在多个目录下,需要添加多项:

2.6 添加FreeRTOSConfig.h内核配置文件

        配置项的详细解释,可以到官网查询:定制 - FreeRTOS™

FreeRTOSConfig.h

/*
    FreeRTOS V9.0.0 - Copyright (C) 2016 Real Time Engineers Ltd.
    All rights reserved

    VISIT http://www.FreeRTOS.org TO ENSURE YOU ARE USING THE LATEST VERSION.

    This file is part of the FreeRTOS distribution.

    FreeRTOS is free software; you can redistribute it and/or modify it under
    the terms of the GNU General Public License (version 2) as published by the
    Free Software Foundation >>!AND MODIFIED BY!<< the FreeRTOS exception.

	***************************************************************************
    >>!   NOTE: The modification to the GPL is included to allow you to     !<<
    >>!   distribute a combined work that includes FreeRTOS without being   !<<
    >>!   obliged to provide the source code for proprietary components     !<<
    >>!   outside of the FreeRTOS kernel.                                   !<<
	***************************************************************************

    FreeRTOS is distributed in the hope that it will be useful, but WITHOUT ANY
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    FOR A PARTICULAR PURPOSE.  Full license text is available on the following
    link: http://www.freertos.org/a00114.html

    ***************************************************************************
     *                                                                       *
     *    FreeRTOS provides completely free yet professionally developed,    *
     *    robust, strictly quality controlled, supported, and cross          *
     *    platform software that is more than just the market leader, it     *
     *    is the industry's de facto standard.                               *
     *                                                                       *
     *    Help yourself get started quickly while simultaneously helping     *
     *    to support the FreeRTOS project by purchasing a FreeRTOS           *
     *    tutorial book, reference manual, or both:                          *
     *    http://www.FreeRTOS.org/Documentation                              *
     *                                                                       *
    ***************************************************************************

    http://www.FreeRTOS.org/FAQHelp.html - Having a problem?  Start by reading
	the FAQ page "My application does not run, what could be wrong?".  Have you
	defined configASSERT()?

	http://www.FreeRTOS.org/support - In return for receiving this top quality
	embedded software for free we request you assist our global community by
	participating in the support forum.

	http://www.FreeRTOS.org/training - Investing in training allows your team to
	be as productive as possible as early as possible.  Now you can receive
	FreeRTOS training directly from Richard Barry, CEO of Real Time Engineers
	Ltd, and the world's leading authority on the world's leading RTOS.

    http://www.FreeRTOS.org/plus - A selection of FreeRTOS ecosystem products,
    including FreeRTOS+Trace - an indispensable productivity tool, a DOS
    compatible FAT file system, and our tiny thread aware UDP/IP stack.

    http://www.FreeRTOS.org/labs - Where new FreeRTOS products go to incubate.
    Come and try FreeRTOS+TCP, our new open source TCP/IP stack for FreeRTOS.

    http://www.OpenRTOS.com - Real Time Engineers ltd. license FreeRTOS to High
    Integrity Systems ltd. to sell under the OpenRTOS brand.  Low cost OpenRTOS
    licenses offer ticketed support, indemnification and commercial middleware.

    http://www.SafeRTOS.com - High Integrity Systems also provide a safety
    engineered and independently SIL3 certified version for use in safety and
    mission critical applications that require provable dependability.

    1 tab == 4 spaces!
*/

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
 *
 * See http://www.freertos.org/a00110.html.
 *----------------------------------------------------------*/

/* USER CODE BEGIN Includes */   	      
/* Section where include file can be added */
/* USER CODE END Includes */ 

/* Ensure stdint is only used by the compiler, and not the assembler. */
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
    #include <stdint.h>
    extern uint32_t SystemCoreClock;
#endif

/***************************************************************************************************************/
/*                                        FreeRTOS基础配置配置选项                                              */
/***************************************************************************************************************/
#define configUSE_PREEMPTION											1            			//1使用抢占式内核,0使用协程
#define configUSE_TIME_SLICING										1									//1使能时间片调度(默认式使能的)
#define configUSE_PORT_OPTIMISED_TASK_SELECTION		1           			//1启用特殊方法来选择下一个要运行的任务
																																		//一般是硬件计算前导零指令,如果所使用的
																																		//MCU没有这些硬件指令的话此宏应该设置为0!
#define configCPU_CLOCK_HZ												(SystemCoreClock)	//CPU频率
#define configTICK_RATE_HZ												(1000)            //时钟节拍频率,这里设置为1000,周期就是1ms
#define configMAX_PRIORITIES											(4)               //优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高
#define configMINIMAL_STACK_SIZE									((uint16_t)128)   //空闲任务使用的栈大小
#define configMAX_TASK_NAME_LEN										(30)              //任务名字字符串长度

#define configUSE_16_BIT_TICKS										0                 //系统节拍计数器变量数据类型,
                                                                    //1表示为16位无符号整形,0表示为32位无符号整形
#define configIDLE_SHOULD_YIELD										1                 //为1时空闲任务放弃CPU使用权给其他同优先级的用户任务
#define configUSE_TASK_NOTIFICATIONS           	  1                 //为1时开启任务通知功能,默认开启
#define configUSE_MUTEXES													1                //为1时使用互斥信号量
#define configQUEUE_REGISTRY_SIZE									8                 //不为0时表示启用队列记录,具体的值是可以
                                                                    //记录的队列和信号量最大数目。
#define configCHECK_FOR_STACK_OVERFLOW						0                 //大于0时启用堆栈溢出检测功能,如果使用此功能
                                                                   //用户必须提供一个栈溢出钩子函数,如果使用的话
                                                                   //此值可以为1或者2,因为有两种栈溢出检测方法。
#define configUSE_RECURSIVE_MUTEXES								0                 //为1时使用递归互斥信号量
#define configUSE_MALLOC_FAILED_HOOK							0                  //1使用内存申请失败钩子函数
#define configUSE_APPLICATION_TASK_TAG						0                       
#define configUSE_COUNTING_SEMAPHORES							0                  //为1时使用计数信号量

/***************************************************************************************************************/
/*                                FreeRTOS与内存申请有关配置选项                                                */
/***************************************************************************************************************/
#define configSUPPORT_DYNAMIC_ALLOCATION        1                       //支持动态内存申请
#define configTOTAL_HEAP_SIZE					((size_t)(40 * 1024))   					//系统所有总的堆大小

/***************************************************************************************************************/
/*                                FreeRTOS与钩子函数有关的配置选项                                              */
/***************************************************************************************************************/
#define configUSE_IDLE_HOOK											0                       //1,使用空闲钩子;0,不使用
#define configUSE_TICK_HOOK											0                       //1,使用时间片钩子;0,不使用

/***************************************************************************************************************/
/*                                FreeRTOS与运行时间和任务状态收集有关的配置选项                                 */
/***************************************************************************************************************/
#define configGENERATE_RUN_TIME_STATS	      	 0                       //为1时启用运行时间统计功能
#define configUSE_TRACE_FACILITY							 1                       //为1启用可视化跟踪调试
#define configUSE_STATS_FORMATTING_FUNCTIONS	 1                       //与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数
                                                                        //prvWriteNameToBuffer(),vTaskList(),
                                                                        //vTaskGetRunTimeStats()
                                                                        
/***************************************************************************************************************/
/*                                FreeRTOS与协程有关的配置选项                                                  */
/***************************************************************************************************************/
#define configUSE_CO_ROUTINES 			           0                       //为1时启用协程,启用协程以后必须添加文件croutine.c
#define configMAX_CO_ROUTINE_PRIORITIES       (2)                     //协程的有效优先级数目

/***************************************************************************************************************/
/*                                FreeRTOS与软件定时器有关的配置选项                                            */
/***************************************************************************************************************/
#define configUSE_TIMERS				              1                               //为1时启用软件定时器
#define configTIMER_TASK_PRIORITY		         (configMAX_PRIORITIES-1)        //软件定时器优先级
#define configTIMER_QUEUE_LENGTH		          5                               //软件定时器队列长度
#define configTIMER_TASK_STACK_DEPTH	       (configMINIMAL_STACK_SIZE * 2)  //软件定时器任务堆栈大小


/***************************************************************************************************************/
/*                                FreeRTOS可选函数配置选项                                                      */
/***************************************************************************************************************/
#define INCLUDE_xTaskGetSchedulerState           1                       
#define INCLUDE_vTaskPrioritySet		             1
#define INCLUDE_uxTaskPriorityGet		             1
#define INCLUDE_vTaskDelete				               1
#define INCLUDE_vTaskCleanUpResources	           0
#define INCLUDE_vTaskSuspend			               1
#define INCLUDE_vTaskDelayUntil			             1
#define INCLUDE_vTaskDelay				               1
#define INCLUDE_eTaskGetState			               1
#define INCLUDE_xTimerPendFunctionCall	         0
#define INCLUDE_xTaskGetHandle                   1

/***************************************************************************************************************/
/*                                FreeRTOS与中断有关的配置选项                                                  */
/***************************************************************************************************************/
#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                       //系统可管理的最高中断优先级 5-15可管理
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/***************************************************************************************************************/
/*                                FreeRTOS与中断服务函数有关的配置选项                                          */
/***************************************************************************************************************/
#define xPortPendSVHandler 	PendSV_Handler
#define vPortSVCHandler 	SVC_Handler

/***************************************************************************************************************/
/*                                FreeRTOS断言                                          */
/***************************************************************************************************************/
#include "RTT_Debug.h" 

#define  USE_FULL_ASSERT

#ifdef  USE_FULL_ASSERT
  #define configASSERT(expr)    do { \
									if ((expr) == 0)  \
									{              \
										taskDISABLE_INTERRUPTS(); \
										DBG_Error("Wrong parameters value\n"); \
										while (1); \
									}              \
								} while (0)
#else
  #define configASSERT(expr) ((void)0U)
#endif

#endif /* FREERTOS_CONFIG_H */

2.7 配置任务调度相关的中断处理函数

        在FreeRTOSConfig.h中对PendSV和SVC服务函数进行宏定义,注意要删除gd32f30x_it.c文件中这两个服务函数代码(如有gd32f30x_it.c文件)。vPortSVCHandler和xPortPendSVHandler这两个函数的具体实现都在ARM_CM4F\port.c中。

        再添加对systick中断服务函数的修改,在ARM_CM4F\port.c中添加如下代码(如果其它文件中有systick中断服务函数,记得删除):

        systick初始化,在rtos自己代码中已经实现了,不需要再手动添加实现,rtos中的vTaskStartScheduler->xPortStartScheduler设置了优先级,xPortStartScheduler->vPortSetupTimerInterrupt设置了中断周期。

        systick中断服务函数为什么不像上面两个函数进行宏定义,是为了防止进入该中断时,rtos内核还没有开启调度就去执行相关程序。

2.8 配置中断优先级分组。

2.9 配置任务优先级范围

3 数据类型和编程规范

3.1 数据类型

        每个移植的版本都含有自己的 portmacro.h 头文件,里定义了2个数据类型:

        TickType_t:

        FreeRTOS配置了一个周期性的时钟中断: Tick Interrupt,每发生一次中断,中断次数累加,这被称为tick count,tick count这个变量的类型就是TickType_t,TickType_t可以是16位的,也可以是32位的。FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t是uint16_t ,否则TickType_t就是uint32_t。对于32位架构,建议把TickType_t配置为uint32_t。

        BaseType_t:

        这是该架构最高效的数据类型,32位架构中,它就是uint32_t,16位架构中,它就是uint16_t,8位架构中,它就是uint8_t,BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE。

3.2 变量名

          变量名有前缀:

变量名前缀

含义

c

char

s

int16_t,short

l

int32_t,long

x

BaseType_t,

其他非标准的类型:结构体、 task handle、queue handle等

u

unsigned

p

指针

uc

uint8_t,unsigned char

pc

char指针

3.3 函数名

        函数名的前缀有2部分:返回值类型、在哪个文件定义。

函数名前缀

含义

vTaskPrioritySet

返回值类型: void 在task.c中定义

xQueueReceive

返回值类型: BaseType_t 在queue.c中定义

pvTimerGetTimerID

返回值类型: pointer to void 在tmer.c中定义

3.4 宏的名

        宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。

image7

通用的宏定义如下:

image8

三、任务管理

1 任务基本概念

        任务(Task)是 FreeRTOS 中并发执⾏的基本单位,类似于裸机里的⼀个"无限循环的函数",但拥有自己独立的栈空间和上下文(寄存器状态)。每个任务由⼀个任务控制块 TCB(Task Control Block)描述,TCB 中记录了任务的栈指针、优先级、任务状态、任务名称等信息。

        任务函数的典型写法:

写法一:
static void LedTask(void *pvParameters)
{
    for (;;) // 任务函数本身绝不能return
  {
    LED_Toggle();
    vTaskDelay(pdMS_TO_TICKS(500)); // 延时,让出CPU给其他任务
  }
}

写法二:
static void WifiNetworkTask(void *pvParameters)
{
	while (1)
	{
		WifiNetworkHandle();
		vTaskDelay(1);
	}
}

       注意:

        ① 任务函数必须写成死循环( for(;;) 或 while(1) ),不能 return,否则会进⼊未定义⾏为。如果确实需要任务⾃我结束,应调用 vTaskDelete(NULL) 删除自己。

void ATaskFunction( void *pvParameters )
{
	/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
	int32_t lVariableExample = 0;
	
    /* 任务函数通常实现为一个无限循环 */
	for( ;; )
	{
		/* 任务的代码 */
	}

    /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
     * NULL表示删除的是自己
     */
	vTaskDelete( NULL );
    
    /* 程序不会执行到这里, 如果执行到这里就出错了 */
}

        这个函数不能返回。

        ③同一个函数,可以用来创建多个任务,也就是多个任务可以运行同一个函数。

        ④函数内部,尽量使用局部变量;每个任务都有自己的任务栈;每个任务运行这个函数时,任务A的局部变量放在A的任务栈里,任务B的局部变量放在B的任务栈里,不同任务的局部变量,都有自己的副本;函数使用全局变量、静态变量的话,只有一个副本即多个任务使用的是同一个副本,要防止冲突(互斥锁)。

2 创建任务

        创建任务时使用的函数:

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, //函数指针, 任务函数
                 const char * const pcName, //任务的名字
                 const configSTACK_DEPTH_TYPE usStackDepth, //栈大小,单位为word,10表示40字节
                 void * const pvParameters, //调用任务函数时传入的参数
                 UBaseType_t uxPriority,    //优先级
                 TaskHandle_t * const pxCreatedTask ); //任务句柄, 以后使用它来操作这个任务

参数说明:

参数 描述
pvTaskCode 函数指针,任务对应的 C 函数。指向任务入口函数的指针(即实现任务的函数名称,请参阅如下示例)。 任务通常以无限循环的形式实现;实现任务的函数 绝不能尝试返回或退出。但是,任务可以自行删除。 "vTaskDelete(NULL)"。
pcName 任务的名称,仅用于调试目的,FreeRTOS 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。
usStackDepth 每个任务都有自己的栈,usStackDepth 指定了栈的大小,单位为 word。例如,如果传入 100,表示栈的大小为 100 word,即 400 字节。最大值为 uint16_t 的最大值。确定栈的大小并不容易,通常是根据估计来设定。精确的办法是查看反汇编代码。
pvParameters 调用 pvTaskCode 函数指针时使用的参数:pvTaskCode(pvParameters)。
uxPriority 任务的优先级范围为 0~(configMAX_PRIORITIES – 1)。数值越小,优先级越低。如果传入的值过大,xTaskCreate 会将其调整为 (configMAX_PRIORITIES – 1)。
pxCreatedTask 用于保存 xTaskCreate 的输出结果,即任务的句柄(task handle)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。
返回值 成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。
调用示例:
TaskHandle_t xHandleLED = NULL;
xTaskCreate(LedTask, "LedTask", 128, NULL, 2, &xHandleLED);


void WifiNetworkTaskInit(void)
{
	xTaskCreate(WifiNetworkTask, "Wifi Network Task", 256, NULL, 1,  NULL);
}

3 创建任务demo例程

3.1 创建任务

void Task1(void *pvParameters)
{
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务1的信息 */
		printf("Task1 run\r\n");
		
		/* 延迟一会 */
		for (uint32_t i = 0; i < DELAY_LOOP_COUNT; i++ )
		{
		}
	}
}

void Task2( void *pvParameters )
{	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务2的信息 */
		printf("Task2 run\r\n");
		
		/* 延迟一会 */
		for (uint32_t i = 0; i < DELAY_LOOP_COUNT; i++ )
		{
		}
	}
}

int main(void)
{
	prvSetupHardware();
	
    xTaskCreate(Task2, "Task 2", 1000, NULL, 1, NULL);
	xTaskCreate(Task1, "Task 1", 1000, NULL, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

        运行结果如下:

        任务运行图:

         在t1:Task2进入运行态, 一直运行直到t2

        在t2:Task1进入运行态, 一直运行直到t3;在t3 Task2重新进入运行态

         注:更高优先级的、或者先创建的任务先运行。

3.2 使用任务参数

        我们说过,多个任务可以使用同一个函数,怎么体现它们的差别?

        ①栈不同。

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

void TaskFunction(void *pvParameters)
{
	char *pcTaskText = pvParameters;
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务的信息 */
		printf(pcTaskText);
		
		/* 延迟一会(比较简单粗暴) */
		/* 延迟一会 */
		for (uint32_t i = 0; i < DELAY_LOOP_COUNT; i++ )
		{
		}
	}
}

static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(TaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
	xTaskCreate(TaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

        代码分析:

        上述代码中的pcTaskText来自参数 pvParameters, pvParameters来自哪里?创建任务时传入的。 代码如下:

static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(TaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
	xTaskCreate(TaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

          使用xTaskCreate创建2个任务时,第4个参数就是pvParameters。不同的任务, pvParameters不一样。

3.3 删除任务

        删除任务的函数:

void vTaskDelete( TaskHandle_t xTaskToDelete );

        参数说明:

参数 描述
pvTaskCode 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。(注意:①删除自己: vTaskDelete(NULL) ;②被别的任务删除:别的任务执行 vTaskDelete(pvTaskCode) , pvTaskCode是自己的句柄;③删除别的任务:执行 vTaskDelete(pvTaskCode) , pvTaskCode是别的任务的句柄)
void Task2( void *pvParameters );
TaskHandle_t xTask2Handle = NULL;

void Task1(void *pvParameters)
{
	const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);		
	BaseType_t ret;
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务的信息 */
		printf("Task1 is running\r\n");
		
		ret = xTaskCreate( Task2, "Task 2", 1000, NULL, 2, &xTask2Handle );
		if (ret != pdPASS)
		{
			printf("Create Task2 Failed\r\n");
		}
		// 如果不休眠的话, Idle任务无法得到执行
		// Idel任务会清理任务2使用的内存
		// 如果不休眠则Idle任务无法执行, 最后内存耗尽
		vTaskDelay( xDelay100ms );
	}
}

void Task2( void *pvParameters )
{	
	/* 打印任务的信息 */
	printf("Task2 is running and going to delete itself\r\n");

	// 可以直接传入参数NULL, 这里只是为了演示函数用法
	vTaskDelete(xTask2Handle);
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(Task1, "Task 1", 1000, NULL, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

        运行结果如下:

        任务运行图:

        代码分析:

         main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。任务2的优先级最高,它马上执行。任务2打印一句话后,就删除了自己。任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用vTaskDelay()进入Block状态。任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)。时间到后,任务1变为最高优先级的任务继续执行。

        注意:如果在任务1的函数中,如果不调用vTaskDelay,则Idle任务用于没有机会执行,它就无法释放创建任务2是分配的内存。而任务1在不断地创建任务,不断地消耗内存,最终内存耗尽再也无法创建新的任务。

4 任务优先级

4.1 任务优先级

        FreeRTOS使⽤数字表⽰优先级, 高优先级的任务先运行, 数字越⼤优先级越高(与部分 RTOS 相反,注意区分)。最⼤优先级数由 configMAX_PRIORITIES 决定,取值范围为 0 ~ (configMAX_PRIORITIES - 1),  0通常留给空闲任务。

        注:①FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行。②对于相同优先级的、可运行的任务,轮流执行。

4.2 Tick 

         对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。在FreeRTOS中使用⼀个硬件定时器产⽣周期性中断(通常使⽤ Cortex-M 的 SysTick),每次中断称为⼀个Tick,由 configTICK_RATE_HZ 配置频率(常⻅1000,即1ms ⼀次。如果是100,那么时间片长度就是10ms。)。

        时间片:在 FreeRTOS 中,“时间片”是它同优先级任务调度的核心机制,通常被称为时间片轮转调度(Round-Robin Scheduling)。假设t1、t2、t3发生时钟中断,两次中断之间的时间被称为时间片(time slice、tick period),假设configTICK_RATE_HZ为1000,那么时间片长度就是1ms。

        相同优先级的任务怎么切换呢?

          ①任务2从t1执行到t2。

         ②在t2发生tick中断,进入tick中断处理函数:选择下一个要运行的任务,执行完中断处理函数后,切换到新的任务:任务1。

        ③任务1从t2执行到t3。

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

        在FreeRTOS中,常用Tick来衡量时间。

vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=1000, Tick周期时1ms, 等待2ms
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms

        注意:使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。

4.3 优先级demo例程

void vTask1( void *pvParameters )
{
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务的信息 */
		printf("T1 run\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务的信息 */
		printf("T2 run\r\n");				
	}
}

void vTask3( void *pvParameters )
{	
	const TickType_t delay3ms = pdMS_TO_TICKS( 3UL );		
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 打印任务的信息 */
		printf("T3 run\r\n");				

		// 如果不休眠的话, 其他任务无法得到执行
		vTaskDelay(delay3ms);
	}
}


int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

        运行结果如下:

        代码分析:任务3优先执行,直到它调用vTaskDelay主动放弃运行,休眠任务。任务1、任务2:轮流执行。任务3休眠时间结束,由阻塞态转化为就绪态,抢占任务2执行任务3,任务3为运行态。

4.4 修改优先级

        在FreerRTOS中,使用uxTaskPriorityGet来获得任务的优先级:

UBaseType_t  uxTaskPriorityGet(const  TaskHandle_t  xTask );

        注:使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。

        在FreerRTOS中,使用vTaskPrioritySet 运⾏时动态设置任务的优先级:

void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority );

        注:使用参数xTask来指定任务,设置为NULL表示设置自己的优先级。

        参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。

TaskHandle_t xTask2Handle = NULL;

void Task1( void *pvParameters )
{
	UBaseType_t uxPriority;
	
	/* Task1,Task2都不会进入阻塞或者暂停状态
	 * 根据优先级决定谁能运行
	 */
	
	/* 得到Task1自己的优先级 */
	uxPriority = uxTaskPriorityGet(NULL);
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		printf("Task 1 is running\r\n");

		printf("About to raise the Task 2 priority\r\n");
		
		/* 提升Task2的优先级高于Task1
		 * 注意:Task2会即刻执行
 		 */
		vTaskPrioritySet(xTask2Handle, (uxPriority + 1));
		
		/* 如果Task1能运行到这里,表示它的优先级比Task2高
		   那就表示Task2肯定把自己的优先级降低了
 		*/
	}
}

void Task2( void *pvParameters )
{
	UBaseType_t uxPriority;

	/* Task1,Task2都不会进入阻塞或者暂停状态
	 * 根据优先级决定谁能运行
	 */
	
	/* 得到Task2自己的优先级 */
	uxPriority = uxTaskPriorityGet(NULL);
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		/* 能运行到这里表示Task2的优先级高于Task1
		 * Task1提高了Task2的优先级
		 */
		printf( "Task 2 is running\r\n" );
		
		printf( "About to lower the Task 2 priority\r\n" );

		/* 降低Task2自己的优先级,让它小于Task1
		 * Task1得以运行
         * 注意:Task2会即刻执行
 		 */
		vTaskPrioritySet(NULL, (uxPriority - 2));
	}
}

int main(void)
{
	prvSetupHardware();
	
	/* Task1的优先级更高, Task1先执行 */
	xTaskCreate(Task1, "Task 1", 1000, NULL, 2, NULL);
	xTaskCreate(Task2, "Task 2", 1000, NULL, 1, &xTask2Handle);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

        运行结果如下:

        代码分析:

        Task1优先级最高,它先执行。它提升了Task2的优先级。Task2的优先级最高,它执行。它把自己的优先级降低了。Task1的优先级最高,再次执行。它提升了Task2的优先级。注意:Task1的优先级一直是2 ,Task2的优先级是3或1,都大于0。所以Idel任务没有机会执行。

        注:

5 任务状态

FreeRTOS 中每个任务在生命周期内始终处于以下四种状态之⼀:
        状态转换关系如下图:

①创建任务→就绪态:

        任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。

②就绪态→运行态:

        发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。

③运行态→就绪态:

        有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务。

④运行态→阻塞态:

        正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。

⑤阻塞态→就绪态:

        阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

⑥⑦⑧就绪态、阻塞态、运行态→挂起态:

        任务可以通过调用vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU的使用权,也不会参与调度,除非它从挂起态中解除。

⑨挂起态→就绪态:

        把 一 个挂起状态的任务恢复的唯一途径就是调用vTaskResume()或vTaskResumeFromISR() API函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

        注意:在任一时刻,CPU只能处理某一个任务,则该任务就处于运行态,对于其它任务,如果是自己想要延时或阻塞等待时,则处于阻塞态,如果是自己想要执行但因为优先级低而不能执行时,则处于就绪态。运行态的任务进入阻塞态,则就绪态的任务即可运行。阻塞态的解除阻塞进入就绪,若该任务的优先级更高,则可抢占当前处于运行的任务,使自己运行,使对方就绪。阻塞态的任务要想运行,必须先进入就绪态,再进入运行态。

5.1 阻塞态(Blocked)

        在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:任务要等待某个事件,事件发生后它才能运行,在等待事件过程中,它不消耗CPU资源在等待事件的过程中,这个任务就处于阻塞状态(Blocked)。

        在阻塞状态的任务,它可以等待两种类型的事件:时间相关的事件、同步事件(这事件由别的任务,或者是中断程序产生)。

        同步事件来源:队列、二进制信号量、计数信号量、互斥量、递归互斥量、递归锁、事件组、任务通知。

5.2 挂起态(Suspended)

        FreeRTOS中的任务也可以进入挂起态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );     

        注:

        参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。要退出暂停状态,只能由别人来操作:别的任务调用: vTaskResume, 中断程序调用: xTaskResumeFromISR。

5.3 就绪态(Ready)

        这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。

6 Delay函数

6.1 常用Delay函数:

①vTaskDelay

函数原型:

void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给 Tick */

vTaskDelayUntil

函数原型:BaseType_t xTaskDelayUntil(TickType_t *const pxPreviousWakeTime, const TickType_t xTimeIncrement );

/* pxPreviousWakeTime: 保存了上一次被唤醒的时间第一次使用前,该变量必须初始化为当前时间。之后这个变量会在vTaskDelayUntil()函数内自动更新

* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)

* 单位都是Tick Count */

 

  

注意:

  ①使用vTaskDelay(n)时,指的是任务阻塞到唤醒的间隔时间为n个Tick,相对延时:周期 = 执行任务时间 + Delay时间。

 ②使用xTaskDelayUntil(&Pre, n)时,指的是唤醒到唤醒的间隔时间为n个Tick,绝对周期

 对于大多数周期性采样、控制、通信任务,更推荐下面这种写法:

void SensorTask(void *pvParameters)
{
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(100);

    for (;;)
    {
        Sample_Sensor();              // 立即执行一次采样

        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

使用建议:

        ①使用vTaskDelay(n):不要求严格周期,例如:LED 闪烁、按键扫描、状态轮询等。

        ②使用vTaskDelayUntil():要求固定周期,例如:传感器采样、PID 控制、数据上传、周期性通信(MQTT 心跳)、工业控制等。

6.2 Delay函数demo例程:

static volatile int flag = 0;

void Task1( void *pvParameters )
{
	TickType_t delay50ms = pdMS_TO_TICKS(50UL);
	TickType_t xLastWakeTime;
	
	/* 获得当前的Tick Count */
	xLastWakeTime = xTaskGetTickCount();
			
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		flag = 1;
		
		/* 故意加入多个循环,让程序运行时间长一点 */
		for (int i = 0; i <5; i++)
			printf( "Task 1 is running\r\n" );

#if 0		
		vTaskDelay(delay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, delay50ms);
#endif		
	}
}

void Task2( void *pvParameters )
{
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		flag = 0;
		printf( "Task 2 is running\r\n" );
	}
}

int main( void )
{
	prvSetupHardware();
	
	/* Task1的优先级更高, Task1先执行 */
	xTaskCreate(Task1, "Task 1", 1000, NULL, 2, NULL);
	xTaskCreate(Task2, "Task 2", 1000, NULL, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

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

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

①vTaskDelay:指的是任务阻塞到唤醒的间隔时间:

②vTaskDelayUntil:指的是唤醒到唤醒的间隔时间:

7 空闲任务及其钩子函数

7.1 空闲任务介绍

        空闲任务(Idle任务)的作用:释放被删除的任务的内存。

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

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

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

        空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。

        注意:如果使用 vTaskDelete()来删除任务,那么必须要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。

        添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。

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

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

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

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

        注意:

        ①使用空闲任务的钩子函数不能导致空闲任务进入阻塞状态、挂起状态。

        ②使用空闲任务的钩子函数,若再使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务一直卡在钩子函数里的话,它就无法释放内存,最后会造成内存耗尽。

7.2 使用空闲任务的钩子函数相关配置 

        使用空闲任务的钩子函数,要配置:

        ①FreeRTOSConfig.h文件中把这个宏定义为1:configUSE_IDLE_HOOK

       ②实现 vApplicationIdleHook函数

8 调度算法

8.1 概念总结

        正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理系统中,任何时间里只能有一个任务处于运行状态。非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。只有处于就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的事件、同步事件。

        ①:时间相关的事件,设置超时时间,在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。

        ②:同步事件:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息(任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等)。

8.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)

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

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

配置项

A

B

C

D

E

configUSE_PREEMPTION

1

1

1

1

0

configUSE_TIME_SLICING

1

1

0

0

x

configIDLE_SHOULD_YIELD

1

0

1

0

x

说明

常用

很少用

很少用

很少用

几乎不用

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

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

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

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

        E:合作调度

8.3 调度demo例程

static void prvSetupHardware( void );

static volatile int flagIdleTaskrun = 0;  // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0;     // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0;     // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0;     // 任务3运行时flagTask3run=1

void Task1( void *pvParameters )
{
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T1 run\r\n");				
	}
}

void Task2( void *pvParameters )
{	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
		
		/* 打印任务的信息 */
		printf("T2 run\r\n");				
	}
}

void Task3( void *pvParameters )
{	
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );		
	
	/* 任务函数的主体都是无限循环 */
	while (1)
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
		
		/* 打印任务的信息 */
		printf("T3 run\r\n");				

		// 如果不休眠的话, 其他任务无法得到执行
		vTaskDelay( xDelay5ms );
	}
}

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

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(Task1, "Task 1", 1000, NULL, 0, NULL);
	xTaskCreate(Task2, "Task 2", 1000, NULL, 0, NULL);
	xTaskCreate(Task3, "Task 3", 1000, NULL, 2, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

①对比抢占与否效果:

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

#define configUSE_PREEMPTION                1
#define configUSE_TIME_SLICING               1
#define configIDLE_SHOULD_YIELD            1

//合作调度

#define configUSE_PREEMPTION                0
#define configUSE_TIME_SLICING               1
#define configIDLE_SHOULD_YIELD            1

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

        合作调度:

对比图可得出:

        可抢占+时间片轮转+空闲任务让步:高优先级任务就绪时,就可以马上执行。

        合作调度:优先级失去意义了,既然不能抢占就只能协商了,任务1一直在运行,其他任务都无法执行。即使任务3的vTaskDelay已经超时、即使它的优先级更高,都没办法执行。

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

//时间片轮转:

#define configUSE_PREEMPTION                1
#define configUSE_TIME_SLICING               1
#define configIDLE_SHOULD_YIELD            1

// 时间片不轮转

#define configUSE_PREEMPTION                1
#define configUSE_TIME_SLICING               0
#define configIDLE_SHOULD_YIELD            1

//时间片轮转:

//时间片不轮转:

对比图可得出:

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

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

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

//空闲任务让步:

#define configUSE_PREEMPTION                1
#define configUSE_TIME_SLICING               1
#define configIDLE_SHOULD_YIELD            1

// 空闲任务不让步

#define configUSE_PREEMPTION                1
#define configUSE_TIME_SLICING               1
#define configIDLE_SHOULD_YIELD            0

//空闲任务让步:

// 空闲任务不让步:

对比图可得出:

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

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

8.4 创建任务流程

①定义TCB(任务块)的指针。

pxTopOfStack:指针变量,保存对应任务的栈顶地址,随着任务运行会变化   

xStateListItem:状态链表节点

xEventListItem:事件链表节点

uxPriority:优先级

pxStack:保存对应任务的栈起始地址,不再变化

pcTaskName:数组,保存对应任务的名字

        给任务分配栈空间,给任务的TCB分配空间,这个空间实际上是从哪分配的呢?

        单片机RAAM区域图:

        这里虽然使用pvPortMalloc申请了内存空间,但是并不是上图的堆中申请的,而是在bss段,也就是全局区。

具体在哪呢?

        元素个数是需要在FreeRTOSConfig.h中定义的。需要注意的是,任务的栈空间不像裸机程序的函数栈空间,通过在startup_xxxx.s启动文件中设定栈空间。

②调用prvInitialiseNewTask函数给TCB(任务块)赋值。

        给成员pxTopOfStack赋值,栈是向下增长,地址从高到低,所以将动态申请到的高地址(按照数组访问的话,数组的高索引也就对应高地址)。

        在这个函数里继续对任务的链表节点进行初始化。

         pvOwner:保存链表节点持有者指针(TCB)。
         pxContainer:保存链表头指针,比如阻塞/就绪链表。

③调用pxPortInitialiseStack函数,对任务的栈空间进行初始化。

执行完,任务栈的布局是这样的:

       ④再回到xTaskCreate(),添加任务到就绪链表(数组)函数。

         prvAddNewTaskToReadyList( pxNewTCB );

9 任务切换

        freertos有两种场景可以触发任务切换,①systick中断触发切换;②在普通程序中使用taskYIELD()触发任务切换(比如vTaskDelay函数中,队列发送/接收的内核函数中),而在中断服务程序中使用portYIELD_FROM_ISR()触发任务切换。

9.1 systick中断触发

        在xTaskIncrementTick()中,主要完成 :

        ①判断节拍计数器xTickCount 是否溢出, 溢出轮换延时阻塞链表。

        ②判断是否有延时阻塞任务超时,取出插入就绪链表。

③判断是否有同优先级任务在就绪链表中需要轮转

9.2 用taskYIELD()触发

#define taskYIELD()                          portYIELD()

#define portYIELD_WITHIN_API    portYIELD

        综上所述:两种场景,最终还是通过向中断控制及状态寄存器ICSR(地址:0xe000ed04)的第28位写入1,触发PendSV(Pendable Service Call)异常,进入服务函数xPortPendSVHandler,从而执行任务切换。

         在Cortex-M处理器中有两个栈指针,一个是主栈指针(Main Stack Pointer,即MSP);另一个是线程栈指针(Processor Stack Pointer,即PSP)。在任何时刻只能使用到其中一个。

        复位后,默认使用MSP,在FreeRTOS中,MSP用于OS内核(比如创建任务队列代码)和异常中断处理(比如pendsv/systick/UART),PSP用于应用任务程序(从task入口函数进入执行的程序)。

        在vTaskDelay()函数中:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
 
	PRESERVE8
 
	mrs r0, psp			          /*【1.1:环境保存】PSP->R0*/
	isb                           /*【1.2:环境保存】指令同步排序,与流水线相关*/
	ldr	r3, =pxCurrentTCB         /*【1.3:环境保存】加载R3=&pxCurrentTCB*/
	ldr	r2, [r3]                 /*【1.4:环境保存】加载R2=pxCurrentTCB */
	stmdb r0!, {r4-r11, r14}     /*【1.5:环境保存】R0用作栈指针,手动入栈R4-R11 R14 
                                    至切换前的任务栈.*/
	str r0, [r2]			   /*【1.6:环境保存】保存栈顶pxCurrentTCB->pxTopOfStack=R0*/ 
	stmdb sp!, {r3, r14}       /*【2.1:任务切换】入主栈r3-r14 */ 
 
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0	          /*【2.2:任务切换】屏蔽部分中断*/
	dsb				          /*【2.3:任务切换】数据同步隔离*/
	isb				          /*【2.4:任务切换】指令同步排序*/
	bl vTaskSwitchContext	   /*【2.5:任务切换】跳转至函数vTaskSwitchContext执行*/
	mov r0, #0                /*【2.6:任务切换】清空R0*/
	msr basepri, r0           /*【2.7:任务切换】取消中断屏蔽*/ 
	ldmia sp!, {r3, r14}      /*【2.8:任务切换】出主栈 R3-R14*/ 
	ldr r1, [r3]              /*【3.1:环境恢复】加载R1=pxCurrentTCB*/ 
	ldr r0, [r1]			   /*【3.2:环境恢复】加载 R0=pxCurrentTCB->pxTopOfStack. */
ldmia r0!, {r4-r11, r14}  /*【3.3:环境恢复】R0用作栈指针,手动从出栈切换后的任务出栈R4-R11 R14*/
	msr psp, r0		       /*【3.4:环境恢复】恢复psp=R0. */
	isb                   /*【3.5:环境恢复】指令同步排序*/
	bx r14                /*【3.6:环境恢复】R14=0XFFFFFFFDL,将切换至用户级线程模式, 使PSP栈                     
                            指针自动恢复R0-R3、R12、R14、R15、xPSR,并且PSP指向了栈底位置*/
	nop
}

        上述过程,可以分为三步,①任务A由运行态切换到了阻塞态,需要保存A的现场到A的内存栈;②然后需找下个要运行的任务,为任务切换做准备;③如果是任务B,则要恢复任务B的现场。

        A和B切换过程如下:

      ① 在A任务正常运行期间,各个任务的情况:

        ②进入xPortPendSVHandler()时刻前,因为要进入到异常,CPU自动保存现场,将部分寄存器数据压入到A任务的内存栈中,使用的是PSP。注意:进入异常服务函数,SP对应就变成MSP了。

       ③ 进入到异常函数,CPU会使用MSP,再手动将部分寄存器数据压入到A任务的内存栈中,mrs r0, psp,也就是接下来通过R0作为栈指针,将R4-R11 R14入栈,再保存栈顶地址到TCB的pxTopOfStack中。

        ④将R3-R14压入主栈,进入中断后r14自动更新保存的是0xfffffffd(在进入中断前,r14保存的是任务A当前执行指令所在函数的返回地址,pc保存的是当前执行指令的下一条指令的地址),保存0xfffffffd到主栈,是为了下面再退出中断前恢复给R14,最后调用bx r14指令使用。但是在任务栈中也保存了一份0xfffffffd,代码里也会恢复给R14,还不清楚为什么,在M3的代码中就没有在任务栈保存0xfffffffd。调用vTaskSwitchContext函数,用来寻找下一个要执行的任务,同时pxCurrentTCB指向任务B的任务块TCB。

       ⑤恢复任务B的现场,取任务B的栈顶指针交给R0,R0=pxCurrentTCB->pxTopOfStack,将R0充当栈指针,手动将B栈中保存的R4-R11 R14出栈赋值给对应寄存器,再将R0赋值给PSP。

        ⑥执行BX R14指令,自动恢复现场,1.因为R14的数值为0xfffffffd,所以SP指针将自动切换到PSP;2.会自动将任务栈里R0-R3、R12、R15(PC)、R14(LR)出栈赋值给对应寄存器。

        比较重要的函数是vTaskSwitchContext(),在vTaskSwitchContext()函数里调用taskSELECT_HIGHEST_PRIORITY_TASK()以获取更高优先级的任务。

        对于freertos的调度器,它有两种方式寻找下一个最高优先级的任务,分别为特殊方式和常用方式,在FreeRTOSConfig.h中可通过宏定义设置。

/* 0:使用常用方式来选择下一个要运行的任务;

1:使用特殊方法来选择下一个要运行的任务 */

#define configUSE_PORT_OPTIMISED_TASK_SELECTION  1

        常用方法:uxTopReadyPriority记录就绪态中最高优先级值,创建任务时会更新值,有任务添加到就绪表时也会更新值。这种方法对任务的数量无限制。

        特殊方法,uxTopReadyPriority每个bit位表示一个优先级,bit0表示优先级0,bit31表示优先级31,使用此方式优先级最大只能是32个。

        __clz( ( uxReadyPriorities ) 是计算uxReadyPriorities 的前导零个数,如:二进制0001 1010 0101 1111的前导零个数为3,可以知道,最高优先级uxTopPriority等于31减去前导零个数。知道最高优先级的优先级,则通过listGET_OWNER_OF_NEXT_ENTRY()对应最高优先级的列表项,将pxCurrentTCB指向对应的控制块。

        listGET_OWNER_OF_NEXT_ENTRY()函数的妙处在于它并不是获取链表的第一个节点的 OWNER,假设当前链表有 N 个节点,当第 N 次调用该函数时,链表头(火车头)的pxIndex 则指向第 N 个节点,即每调用一次,节点遍历指针pxIndex 则会向后移动一次,用于指向下一个节点。那么对于同优先级任务的时间片轮转,就是利用了这个功能。

        比如,优先级3下有两个任务,当系统第一次切换到优先级为3的任务(包含了任务1和任务 2,它们的优先级相同)时,pxIndex指向任务1,任务1得到执行。当时间片周期1ms过后,发生systick中断,在xTaskIncrementTick()中判断同优先级就绪链表中有大于1个任务,就会触发pendsv进行切换,这个时候 pxIndex 指向任务2,任务2得到执行,任务 1和任务 2 轮流执行,享有相同的CPU时间,即所谓的时间片。

9.3 总结

        ①FreeRTOS有两种场景可以触发任务切换:

        一是systick中断触发切换;二是在RTOS函数中使用taskYIELD()触发任务切换(比如vTaskDelay函数中,队列发送/接收的内核函数中)。 两种场景,最终都是触发PendSV(Pendable Service Call)异常,进入服务函数xPortPendSVHandler,从而执行任务切换。

          ②简要描述任务切换过程:

          RTOS的任务切换过程,其核心本质是“保存旧任务的现场,恢复新任务的现场”

         请求调度切换,比如任务的时间片到期轮转、等待的阻塞事件解除(休眠延时时间到了、获取信号量阻塞解除了)。

        触发调度切换,系统触发PendSV异常,切换动作是在异常服务函数中完成。

        保存正在运行任务的现场,在PendSV异常函数中,保存当前运行任务现场,可以理解为保存CPU寄存器的快照到任务栈中(将当前正在运行任务的“上下文”(即CPU核心寄存器的值,如PC程序计数器、LR链接寄存器、通用寄存器R0-R12、PSR状态寄存器等)按照约定顺序压入(保存到)当前任务自己的栈中)。

        根据优先级找到要运行的任务,也就是找到它的TCB(任务块)

        恢复即将运行任务的现场,在PendSV异常函数中,从即将运行的任务栈中,按照与保存时相反的顺序弹出(恢复CPU所有寄存器的值。其中,最后恢复的PC寄存器指向了任务上次被阻塞时即将要执行的那条指令地址)。

        跳转,CPU通过恢复的PC(程序计数器)继续执行新任务的代码。

        ③RAM区域使用情况:

        ④在ARM单片机CPU中有两个栈指针:

        一个是主栈指针(Main Stack Pointer,即MSP);另一个是线程栈指针(Processor Stack Pointer,即PSP)。在任何时刻只能使用到其中一个,可以相互切换。在RTOS中有一个全局数组ucHeap,用于分配任务栈空间内存。   

        复位后,默认使用MSP,在FreeRTOS中,MSP用于OS内核(比如创建任务队列代码)和异常中断处理(比如pendsv/systick/UART),PSP用于应用任务程序(从task入口函数进入执行的程序),在裸机程序中使用MSP。

        通过设置CPU的CONTROL寄存器的bit[1]选择使用哪个栈指针。CONTROL[1]=0选择主栈指针;CONTROL[1]=1选择线程栈指针。

⑤任务控制块(TCB)(在ucHeap数组中)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐