FreeRTOS任务调度过程vTaskStartScheduler()&任务设计和划分

以下学习资料均来自某高校的刘老师。仔细阅读笔记会发现受益匪浅!!

书接上回

任务优先级一样,调用滴答定时器的延时函数, 就会发生时间片的轮转的调度,让相同优先级的几个任务轮流运行, 每个任务运行一个时间片, 任务在时间片运行完之后,操作系统自动切换到下一个任务运行。例如:

cs 复制代码
//ÈÎÎñÓÅÏȼ¶
#define LED0_TASK_PRIO		3
//ÈÎÎñ¶ÑÕ>>´óС	
#define LED0_STK_SIZE 		50  
//ÈÎÎñ¾ä±ú
TaskHandle_t LED0Task_Handler;
//ÈÎÎñº¯Êý
void led0_task(void *pvParameters);

//ÈÎÎñÓÅÏȼ¶
#define LED1_TASK_PRIO		3
//ÈÎÎñ¶ÑÕ>>´óС	
#define LED1_STK_SIZE 		50  
//ÈÎÎñ¾ä±ú
TaskHandle_t LED1Task_Handler;
//ÈÎÎñº¯Êý
void led1_task(void *pvParameters);

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//ÉèÖÃϵͳÖжÏÓÅÏȼ¶·Ö×é4	 
	delay_init();	    				//ÑÓʱº¯Êý³õʼ>>¯	  
	uart_init(115200);					//³õʼ>>¯´®¿Ú
	LED_Init();		  					//³õʼ>>¯LED
	 
	//´´½¨LED0ÈÎÎñ
   xTaskCreate((TaskFunction_t )led0_task,   //ÈÎÎñº¯Êý  	
                (const char*    )"led0_task", //ÈÎÎñÃû³Æ  	
                (uint16_t       )LED0_STK_SIZE,//ÈÎÎñ¶ÑÕ>>´óС 
                (void*          )NULL,				//´<<µÝ¸øÈÎÎñº¯ÊýµÄ²ÎÊý
                (UBaseType_t    )LED0_TASK_PRIO,//ÈÎÎñÓÅÏȼ¶	
                (TaskHandle_t*  )&LED0Task_Handler); //ÈÎÎñ¾ä±ú   
    //´´½¨LED1ÈÎÎñ
    xTaskCreate((TaskFunction_t )led1_task,     
                (const char*    )"led1_task",   
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )LED1_TASK_PRIO,
                (TaskHandle_t*  )&LED1Task_Handler);         
    vTaskStartScheduler();          //¿ªÆôÈÎÎñµ÷¶È
}

//LED0ÈÎÎñº¯Êý 
void led0_task(void *pvParameters)
{
    while(1)
    {
        LED0=~LED0;	//
        //vTaskDelay(500);		//freertosϵͳµÈ´ýº¯Êý
		delay_xms(500);
    }
}   

//LED1ÈÎÎñº¯Êý
void led1_task(void *pvParameters)
{
    while(1)
    {
        LED1=0;
    //    vTaskDelay(200);
			delay_xms(500);	//
        LED1=1;
     //   vTaskDelay(800);
			delay_xms(500);
    }
}

任务控制块

FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示, 这个结构体叫做任务控制块:TCB_t,在使用函数xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。此结构体在文件 tasks.c 中有定义,typedef struct tskTaskControlBlock 保存进程信息用的,每个进程有一个。

cs 复制代码
/*
 * Task control block.  A task control block (TCB) is allocated for each task,
 * and stores task state information, including a pointer to the task's context
 * (the task's run time environment, including register values)
 */
typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;	/*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

	#if ( portUSING_MPU_WRAPPERS == 1 )
		xMPU_SETTINGS	xMPUSettings;		/*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
	#endif

	ListItem_t			xStateListItem;	/*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
	ListItem_t			xEventListItem;		/*< Used to reference a task from an event list. */
	UBaseType_t			uxPriority;			/*< The priority of the task.  0 is the lowest priority. */
	StackType_t			*pxStack;			/*< Points to the start of the stack. */
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

	#if ( portSTACK_GROWTH > 0 )
		StackType_t		*pxEndOfStack;		/*< Points to the end of the stack on architectures where the stack grows up from low memory. */
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t		uxCriticalNesting;	/*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;		/*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
		UBaseType_t		uxTaskNumber;		/*< Stores a number specifically for use by third party trace code. */
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t		uxBasePriority;		/*< The priority last assigned to the task - used by the priority inheritance mechanism. */
		UBaseType_t		uxMutexesHeld;
	#endif

	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
		TaskHookFunction_t pxTaskTag;
	#endif

	#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
		void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
	#endif

	#if( configGENERATE_RUN_TIME_STATS == 1 )
		uint32_t		ulRunTimeCounter;	/*< Stores the amount of time the task has spent in the Running state. */
	#endif

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
		/* Allocate a Newlib reent structure that is specific to this task.
		Note Newlib support has been included by popular demand, but is not
		used by the FreeRTOS maintainers themselves.  FreeRTOS is not
		responsible for resulting newlib operation.  User must be familiar with
		newlib and must provide system-wide implementations of the necessary
		stubs. Be warned that (at the time of writing) the current newlib design
		implements a system-wide malloc() that must be provided with locks. */
		struct	_reent xNewLib_reent;
	#endif

	#if( configUSE_TASK_NOTIFICATIONS == 1 )
		volatile uint32_t ulNotifiedValue;
		volatile uint8_t ucNotifyState;
	#endif

	/* See the comments above the definition of
	tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
	#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
		uint8_t	ucStaticallyAllocated; 		/*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
	#endif

	#if( INCLUDE_xTaskAbortDelay == 1 )
		uint8_t ucDelayAborted;
	#endif

} tskTCB;

对上述函数一一解释

指针pxTopOfStack必须位于结构体的第一项, 指向当前堆栈的栈顶, 对于向下增长的堆栈, pxTopOfStack总是指向最后一个入栈的项目。 如果使用MPU, xMPUSettings必须位于结构体的第二项, 用于MPU设置。

接下来是状态列表项xStateListItem事件列表项xEventListItem , 列表被FreeRTOS调度器 使用, 用于跟踪任务, 处于就绪、 挂 起、 延时的任务,都会被挂接到各自的列表中。 调度器就是通过把任务TCB中的状态列表项xStateListItem和事件列表项xEventListItem挂接到不同的列表中来实现上述过程的。 在 task.c中, 定义了一些静态列表变量, 其中有就绪、 阻塞、挂起列表, 例如当某个任务处于就绪态时, 调度器就将这个任务TCB的xStateListItem列表项挂接到就绪列表。 事件列表项也与之类似, 当队列满的情况下, 任务因入队操作而阻塞时, 就会将事件列表项挂接到队列的等待入队列表上。

uxPriority用于保存任务的优先级, 0为最低优先级。 任务创建时, 指定的任务优先级就被保存到该变量中。

指针pxStack指向堆栈的起始位置, 任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量。 pxTopOfStack指向当前堆栈栈顶, 随着进栈出栈, pxTopOfStack指向的位置是会变化的; pxStack指向当前堆栈的起始位置, 一经分配后, 堆栈起始位置就固定了, 不会被改变了。那么为什么需要pxStack变量呢, 这是因为随着任务的运行, 堆栈可能会溢出, 在堆栈向下增长的系统中,这个变量可用于检查堆栈是否溢出; 如果在堆栈向上增长的系统中, 要想确定堆栈是否溢出, 还需要另外一个变量pxEndOfStack来辅助诊断是否堆栈溢出。

字符数组pcTaskName保存任务的描述或名字,在任务创建时,由参数指定。名字的长度由宏configMAX_TASK_NAME_LEN( 位于FreeRTOSConfig.h中) 指定,包含字符串结束标志。

变量uxCriticalNesting用于保存临界区嵌套深度, 初始值为0。 接下来两个变量用于可视化追踪, 仅当宏configUSE_TRACE_FACILITY( 位于FreeRTOS Config.h中) 为1时有效。

任务堆栈

FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中, 等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场, 恢复现场以后任务就会接着从上次中断的地方开始运行。

创建任务的时候需要给任务指定堆栈, 如果使用的函数 xTaskCreate()创建任务(动态方法 )的话那么任务堆栈就会由函数 xTaskCreate()自动创建。 如果使用函数 **xTaskCreateStatic()创建任务(静态方法)**的话就需要程序员自行定义任务堆栈, 然后堆栈首地址作为函数的参数puxStackBuffer 传递给函数。

任务创建

main里面调用xTaskCreateStatic创建了任务, 观察可知这个函数其实改变的是Task1TCB任务控制块, 这个任务控制块诞生之初, 就没有进行过初始化。 调用任务创建函数目的就是初始化任务控制块。

cs 复制代码
Task1_Handle = xTaskCreateStatic(
    (TaskFunction_t)Task1_Entry,      // 任务函数指针
    (char *)"Task1",                 // 任务名称(用于调试)
    (uint32_t)TASK1_STACK_SIZE,      // 堆栈深度(以字为单位,非字节)
    (void *)NULL,                    // 传递给任务的参数
    (UBaseType_t)tskIDLE_PRIORITY,   // 任务优先级(你代码中省略了此参数)
    (StackType_t *)Task1Stack,       // 预分配的堆栈数组
    (TCB_t *)&Task1TCB               // 预分配的任务控制块
);

任务实现

  1. 任务函数本质也是函数, 所以肯定有任务名什么的, 不过这里我们要注意:任务函数的返回类型一定要为 void 类型, 也就是无返回值, 而且任务的参数也是 void 指针类型的! 任务 函数名可以根据实际情况定义。
  2. 任务的具体执行过程是一个大循环, for(; ; )就代表一个循环, 作用和while(1)一样。
  3. 循环里面就是真正的任务代码了, 此任务具体要干的活就在这里实现!
  4. FreeRTOS 的延时函数, 此处不一定要用延时函数, 其他只要能让FreeRTOS 发生任务切换的 API 函数都可以, 比如请求信号量、 队列等, 甚至直接调用任务调度器。只不过最常用的就是 FreeRTOS 的延时函数。
  5. 任务函数一般不允许跳出循环, 如果一定要跳出循环的话在跳出循环以后一定要调用函数 vTaskDelete(NULL)删除此任务!

任务调度过程

调度器是在main函数中初始化, 同时初始化后会启动第一个任务函数; 任务上下文切换实际也是借用了一个特殊的中断PendSV, 此中断可以挂起, 当有任务切换请求后, 需要在没有更高优先级中断执行时才会执行PendSV。 PendSV主要就是保存上文, 切换到下文

关键概念:PendSV 中断

  • 作用:专门用于 任务切换 的中断,优先级通常最低(确保不阻塞其他中断)。
  • 执行逻辑:
    • 进入中断后,保存当前任务上下文(寄存器)。
    • 从就绪链表中选最高优先级任务,恢复其上下文。
    • 实现任务切换的 "无缝衔接"。

下图展现了FreeRTOS 任务调度的 "启动 - 触发 - 执行" 全流程

FreeRTOS滴答定时器

定义与作用

滴答定时器本质上是一个硬件定时器,在多数常见的微控制器平台上,如 STM32 通常使用系统滴答定时器(SysTick) 。它为 FreeRTOS 提供了一个基本的时间基准,以固定的频率产生中断,系统根据这些中断来维护时间节拍(tick),进而实现任务的调度、延时操作、超时检测等功能。比如,通过滴答定时器产生的中断,FreeRTOS 可以知道何时该切换到高优先级的就绪任务,或者判断一个任务的延时时间是否已到。

工作原理

  1. 初始化配置 :在 FreeRTOS 初始化过程中,会对滴答定时器进行配置,设置其时钟源和分频系数,以确定定时器的计数频率。例如,在 STM32 上,如果系统时钟频率为72MHz,配置滴答定时器的分频系数,使其计数频率为1000Hz,也就意味着每 1 毫秒产生一次中断 。
  2. 中断产生 :定时器按照设定的频率进行计数,当计数值达到预设值时,就会产生中断信号。在 FreeRTOS 中,每次滴答定时器中断发生时,会调用一个特定的中断服务函数(在不同平台上函数名可能不同,比如在 STM32 上是SysTick_Handler)。
  3. 系统处理 :中断服务函数会通知 FreeRTOS 内核有一个时间节拍到来。内核会更新系统的时间节拍计数器(通常是xTickCount变量),并检查是否有任务的延时时间已到,或者是否有高优先级的任务进入了就绪状态。如果有任务需要被唤醒或者有更高优先级的任务可以运行,调度器会根据任务的优先级等信息,决定是否进行任务切换。

配置方法

在 FreeRTOS 中,滴答定时器相关的配置主要通过FreeRTOSConfig.h文件中的宏定义来实现:

  1. configTICK_RATE_HZ:用于设置滴答定时器的频率(单位为 Hz),即每秒产生的时间节拍数。比如configTICK_RATE_HZ设置为1000,表示每 1 毫秒产生一个时间节拍。
  2. 对于硬件定时器的具体配置,通常是在 FreeRTOS 的移植层代码(如port.cportmacro.h)中完成,不同的微控制器平台移植代码会有所差异。以 STM32 为例,在port.c中会有类似SysTick_Config(SystemCoreClock / configTICK_RATE_HZ)的代码,用于配置 SysTick 定时器产生中断的频率。

与系统其他部分的交互

  1. 任务调度:滴答定时器中断是触发任务调度的重要时机之一。当滴答定时器中断发生时,调度器会检查任务的状态,判断是否需要进行任务切换,确保高优先级的就绪任务能够及时获得 CPU 资源。
  2. 延时函数 :FreeRTOS 的延时函数,如vTaskDelayvTaskDelayUntil,都是基于滴答定时器实现的。vTaskDelay函数会使任务进入阻塞状态,经过指定的时间节拍数后才会被唤醒;vTaskDelayUntil函数则用于实现周期性任务,通过记录上一次唤醒时间和设定的周期,结合滴答定时器的时间节拍来保证任务以固定的周期执行。
  3. 队列和信号量:在操作队列和信号量时,如果设置了等待超时时间,也是根据滴答定时器的时间节拍来进行计时的,判断等待时间是否已到,从而决定是否返回相应的错误码。

FreeRTOS 使用裸机自带的滴答定时器中断, 使用其主频或者外部频率作为时钟基准。 由于定时器的功能作为FreeRTOS的核心, 所以正常情况下必须是一个一直运行着的中断, 那么就意味着FreeRTOS庞大的代码量也必须与此中断相互配合, 保证实时性和可靠性, 因此滴答定时器的中断时间必然是不能太短的, 否则将大大影响实时性。

cs 复制代码
void delay_init()
{
    u32 reload;
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择内部时钟 HCLK
    fac_us=SystemCoreClock/1000000; //不论是否使用OS,fac_us都需要使用
    reload=SystemCoreClock/1000000; //每秒钟的计数次数 单位为M
    reload*=1000000/configTICK_RATE_HZ;//根据configTICK_RATE_HZ设定溢出时间
    //reload为24位寄存器,最大值:16777216,在72M下,约合0.233s左右
    fac_ms=1000/configTICK_RATE_HZ; //代表OS可以延时的最少单位
    SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启SYSTICK中断
    SysTick->LOAD=reload; //每1/configTICK_RATE_HZ秒中断一次
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
}

任务切换

任务设计

在基于实时操作系统的应用程序设计中, 任务设计是整个应用程序的基础, 其它软件设计工作都是围绕任务设计来展开。

任务的分类

1、单次执行类任务

2、周期执行类任务

3、事件触发执行类任务

任务的划分目标

在对一个具体的嵌入式应用系统进行任务划分时, 可以有不同的任务划分方案。 为了选择最佳划分方案, 就必须知道任务划分的目标。

任务的划分方法

1、 设备依赖性原则 : 通信、 采集、 控制类任务都对设备具有不同程度的依赖性, 外部设备的特点将导致任务的属性也不同,⽐如通信任务⼀般外部设备的运行速率比主控芯片较低, 所以任务的执行周期和运行时间需要调整。 输⼊输出设备的速度差别是任务" 并发运行的基础" 。 所以通常将不同输⼊输出设备划分为不同的任务独⽴运行。

2、 关键任务 : 关键性,一个系统中必有其关键功能,可以是一个或多个, 对于关键性任务划分的原则是使其功能独⽴,优先级较⾼,通过信号量或则消息与其他任务进⾏通信,简化关键任务的体积, 尽可能的与其他任务剥离。

3、 紧迫任务 : 紧迫性: 是指⼀些具有较⾼的实时性要求的任务, 严格地执⾏周期。 ⼤多数紧迫任务都由异步事件触发, 这些异步事件⼀般能够引发某种中断, 所以将任务安排在ISR中较为合适。

4、数据处理任务: 通常⼀个系统必定会有⼤量的数据计算,这种数据计算通常会耗费⼤量的CPU时间,所以处理不当将会严重影响其他任务的实时性,比如如果⼀个任务具有较⾼的优先级, 而且任务重含有大量的数据计算模块,将会长时间的占有CPU,严重影响其他任务的运行。

5、 功能聚合任务: 功能密切的任务封装为一个任务, 节省通信时间,功能密切一般分为数据关系密切和时序关系密切。

6、 同等触发任务: 触发条件相同的任务划分为一个任务。

任务的优先级安排原则

相关推荐
iCxhust1 小时前
Prj10--8088单板机C语言8259测试(1)
c语言·开发语言
知识噬元兽3 小时前
【工具使用】STM32CubeMX-FreeRTOS操作系统-信号标志、互斥锁、信号量篇
stm32·单片机·嵌入式硬件
крон4 小时前
【Auto.js例程】华为备忘录导出到其他手机
开发语言·javascript·智能手机
Flag- L4 小时前
STM32标准库-TIM定时器
stm32·单片机·嵌入式硬件
zh_xuan4 小时前
c++ 单例模式
开发语言·c++·单例模式
老胖闲聊5 小时前
Python Copilot【代码辅助工具】 简介
开发语言·python·copilot
Blossom.1185 小时前
使用Python和Scikit-Learn实现机器学习模型调优
开发语言·人工智能·python·深度学习·目标检测·机器学习·scikit-learn
2301_775602385 小时前
STM32什么是寄存器
stm32·单片机·嵌入式硬件
曹勖之5 小时前
基于ROS2,撰写python脚本,根据给定的舵-桨动力学模型实现动力学更新
开发语言·python·机器人·ros2
豆沙沙包?6 小时前
2025年- H77-Lc185--45.跳跃游戏II(贪心)--Java版
java·开发语言·游戏