FreeRTOS入门知识(初识RTOS任务调度)(三)

文章目录



摘要

持续更新中

一、浅识FreeRTOS快速入门课程

1.2、FerrRTOS 与裸机区别

1.2.7 任务状态

时间片
c 复制代码
int main( void )
{
		
#ifdef DEBUG
  debug();
#endif
	prvSetupHardware();
	printf("Hello, world!\r\n");
	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
	xHandleTask3 = xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
	/* Start the scheduler. */
	vTaskStartScheduler();
	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

在实际运行中,是以滴答定时器为该系统的时间基准,并且利用调度器进行相关执行。

在上述例子子,最先执行的是任务3,然后是任务1、任务2、任务3,切换的原因是按照调度器的思路执行,什么时候切换是按照时钟中断进行的。并且还需要判断是否切换任务,并且需要记住的是FreeRTOS每个任务运行的最基本时间是1ms。关于如何使用滴答定时器,在前面文章也有详细分析如何配置。

FreeRTOS的时间片长度通常是由系统时钟节拍决定的,也就是我们常说的Tick,在默认情况下,一个时间片就是1个Tick,也就是1ms。这个地方我觉得应该是规定,这是因为在FreeRTOS中这个时间好像是约定俗成的,就是在我们配置的时候要配置为1ms。(configTICK_RATE_HZ=1000 时,时间片为 ​1ms

时间片调度机制(同优先级任务)​,

当多个任务优先级相同,并且启用了时间片调度,那么当一个任务运行满一个时间片的时候(1ms),即时该任务没有执行完,也会被强制切换,至于这是为什么?这是FreeRTOS的本身机制,就是要这样,就是要和裸机的执行逻辑要求区别才有意义,以一种新的架构去执行不同任务。

例如:两个优先级相同的任务 Task1 和 Task2 会轮流执行各 1ms,无论任务是否完成逻辑。

触发任务切换的其他条件:

即使时间片未耗尽,任务也可能被切换:

  • 主动放弃 CPU ​:

    任务调用 taskYIELD()vTaskDelay() 等函数时,会立即触发切换(即使时间片未用完)。

  • 高优先级任务就绪 ​:

    若有更高优先级任务进入就绪态,当前任务会立即被抢占,无论是否用完时间片 。

  • 任务进入阻塞状态 ​:

    若任务因等待信号量、队列或延时操作(如 vTaskDelay())进入阻塞态,会立刻切换至其他任务。

时间片未耗尽时不被切换的场景:

在该批次优先级下仅有一个任务,也就是说没有任务可以切换,那么不就是只能就继续执行。

没有使用时间片调度。

在FreeRTOS中

认时间片是固定的1个系统节拍,仅支持同优先级任务轮转,并且无法自定义单个任务的时间片,也就说一个时间片只能是1个系统节拍。

ThreadX

支持在创建任务时显式指定时间片长度。例如,可为关键任务分配更长的时间片(如5ms),提升其单次执行时长。

注意这两个的区别:

FreeRTOS只能统一调整所有同优先级任务的时间片长度 。FreeRTOS ​不允许为不同任务分配独立的时间片长度

但是ThreadX可单独设置单个任务时间片。

任务的状态分析

运行就是runing状态,一个时刻只能运行一个任务, 那么其他任务就需要给一个状态,这个非运行状态还需要进行一点细分,这是因为不运行状态,有可能是一直不能运行,还有可能是准备好了,所以还需要进行细分:

ready:已经准备好了,随时运行,属于是准备好了就绪状态。随时运行,但是还没有轮到我。

blocked:阻塞,就是卡住了。以例子为讲解,就是任务等到某些事情发生才能继续执行,这个等待某些事情可以是一些标志位等,有点像任务暂停?但是又跟暂停不一样,这个是只要条件到了,就可以再次触发回到就绪状态,就是随时可以执行。阻塞态(Blocked):被动等待事件 任务因等待外部事件时间条件 而主动进入阻塞态,等待信号量、消息队列、事件标志(同步事件),调用延时函数(如 vTaskDelay())等待时间到期(定时事件)。在等待事件的过程中,这个任务就处于阻塞状态(Blocked)。

阻塞状态是事件驱动型:任务因等待外部条件暂停,内核自动监控并恢复,适用于需要同步或定时的场景。

suspend:挂起,就是先不执行。这个是属于暂停状态,主动休息状态,或者是被命令休息。就是我不干了。挂起态(Suspended):主动暂停任务 , 任务被移入挂起列表,完全脱离调度器管理,内核不在检查其状态。只能通过vTaskResume()xTaskResumeFromISR() 手动恢复,且恢复后直接进入就绪状态。场景:日志记录、调试时临时冻结任务。

挂起态是人工干预 ​:任务被显式暂停且不受内核管理,需手动恢复,适合长期暂停或调试。 是主动休息或者是被动休息。

通过这个状态跳转图其实可以看到很多东西,能理解很多FreeRTOS的开发思路。能明白为什么会调用API,以及如何调用API。

在任务处于Runing状态的时候,我们可以操作将部分任务给挂起,从这个状态图中,只要能连接到挂起状态的状态都可以被挂起,甚至我可以让我自己暂停。

同理处于挂起状态的任务,必须要主动进行回复,也就是我可以在某些任务中嵌套恢复的API vTaskResume(),这是因为要想让挂起的任务动作,只能通过这个API恢复,只有这一条路。

并且想要让任务执行Runing,必须要先处于Ready状态,否则都是无稽之谈。

而处于阻塞状态的 任务必须依靠事件进行跳转 到Ready,还有就是每一个状态,都是通过链表进行管理的,至于什么事链表,在之前文章已经详细分析。
数据结构---链表结构体、指针深入理解(三)_void inserhead(node *node,elemtype value){ node *p-CSDN博客

换句话说,我们在开发过程中,可以随便的在不同任务中调用相关控制函数,从而实现了复杂性。复杂的原因就是这里,并且任务之间可以控制任务就是因为使用了每一个任务的句柄。

c 复制代码
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask3;

如果没有这个句柄,外部的任务根本无法控制。

那这个地方其实就又引入一个新的问题,句柄是全局变量,那么频繁的使用这些全局句柄是否会带来影响?或者说应该怎么优化?

这个地方跟裸机开发的思维还是一样的, 1、使用static关键字,模块化编程思维。此外还有一些FreeRTOS中特有的一些思路如:

1、替代全局句柄的通信机制,​任务通知(Task Notifications)​​ 使用任务通知代替句柄操作,无需全局句柄即可唤醒或传递数据。

2、句柄存储优化,将句柄存入专用结构体,结合互斥锁保护。

3、动态查询替代静态句柄,​按任务名查询句柄​。

  • 避免滥用全局句柄​:优先使用任务通知、事件组等内核机制替代直接句柄操作。

  • 必要时的保护 ​:若必须全局使用,需通过互斥锁(xSemaphoreCreateMutex())或临界区(taskENTER_CRITICAL())保护。

关于这些内容,后续会详细分析。一定要深入的理解FreeRTOS,才能在开发过程中解决很多问题,遇到Bug才不会慌张,不然你无法定位到Bug,只能在应用层解决一些问题,并不能解决深入的问题。 都是以结果为导向的解决问题。

任务嵌套理解
c 复制代码
void Task1Function(void * param)
{
	TickType_t tStart = xTaskGetTickCount();
	TickType_t t;
	int flag = 0; 
	
	while (1)
	{
		t = xTaskGetTickCount();	
		task1flagrun = 1;
		task2flagrun = 0;
		task3flagrun = 0;
		printf("1");

		if(!flag && (t > tStart + 10)){
			vTaskSuspend(xHandleTask3);
			flag = 1;  /* 标志位 */
		}

		if(t > tStart + 20){
			vTaskResume(xHandleTask3);
		}
	}
}

void Task2Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 1;
		task3flagrun = 0;
		printf("2");
		vTaskDelay(10);   /* 阻塞状态。 */
	}
}

void Task3Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 0; 
		task3flagrun = 1;
		printf("3");
	}
}

通过此次实验可以明显看出,Task1可以灵活处理Task3,可以让他挂起也可以让他恢复。同时自己可以控制自己让自己处于阻塞,例如使用延时函数,但是延时函数结束,阻塞状态就结束。那么是否可以让别人控制是否阻塞?

肯定可以,但是目前还没有掌握这种思路,因为:任务 A 控制任务 B 的阻塞状态主要通过任务间通信与同步机制实现,接下里理解了这种机制,自然也就明白了如何实现控制。理论上来说,任何任务都可以控制任何任务的任何状态。

通过这个实验,只是对任务的相互控制有一个简单的认识。

真正复杂的是在业务逻辑之间的嵌套,以及如何设计实现任务之间的循环嵌套。

FreeRTOS中延时函数分析(用于阻塞?不拘泥于阻塞)
c 复制代码
void vTask1( void *pvParameters )
{
	const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
	TickType_t xLastWakeTime;
	int i;
	
	/* 获得当前的Tick Count */
	xLastWakeTime = xTaskGetTickCount();
			
	for( ;; )
	{
		flag = 1;
		
		/* 故意加入多个循环,让程序运行时间长一点 */
		for (i = 0; i <5; i++)
			printf( "Task 1 is running\r\n" );

#if 1		
		vTaskDelay(xDelay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif		
	}
}

事件驱动型和中断驱动型在裸机中也是存在的,但是在RTOS中,只不过多了几个枷锁,或者说执行的更加规范,

任务的执行一定是按照优先级和调度策略的。这是逻辑中不一样的,裸机中事件来了就能直接驱动。在FreeRTOS中,​中断和事件的到来确实是唤醒任务的契机,并且唤醒只是让他处于Ready状态。但它们不能直接决定任务是否立即执行 。任务的实际执行由调度器根据优先级和调度策略综合决策。

以两个任务再次理解FreeRTOS内核。

c 复制代码
void vTask1( void *pvParameters )
{
	const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
	TickType_t xLastWakeTime;
	int i;	
	/* 获得当前的Tick Count */
	xLastWakeTime = xTaskGetTickCount();			
	for( ;; )
	{
		flag = 1;		
		/* 故意加入多个循环,让程序运行时间长一点 */
		for (i = 0; i <5; i++)
			printf( "Task 1 is running\r\n" );
#if 1		
		vTaskDelay(xDelay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif		
	}
}

void vTask2( void *pvParameters )
{
	for( ;; )
	{
		flag = 0;
		printf( "Task 2 is running\r\n" );
	}
}

int main( void )
{
	prvSetupHardware();
	/* Task1的优先级更高, Task1先执行 */
	xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
	/* 启动调度器 */
	vTaskStartScheduler();
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

在上述两个任务创建函数中,我们可以明确的看出,这两个任务函数都是死循环,这也是FreeRTOS中的核心,区别于裸机开发。并且FreeRTOS调度器是无法处理任务"正常返回"的情况,因为任务的上下文(栈和TCB)会处于未定义状态。那怎么实现任务轮询呐?依靠的是调度器,这不正是我们使用FreeRTOS的原因,正是因为这个原因,我们才借助于系统开发。

如果我们必须终止任务,那么也可以就需要显示删除自身,例如初始化任务,避免死循环,一直初始化。

删除自身:

c 复制代码
 vTaskDelete(NULL);  // 删除当前任务,传递 NULL 表示删除自身

但是还需要明白vTask1中延时函数:

vTaskDelay(xDelay50ms);

vTaskDelayUntil(&xLastWakeTime, xDelay50ms);

c 复制代码
void vTask1( void *pvParameters )
{
	const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
	TickType_t xLastWakeTime;
	int i;	
	/* 获得当前的Tick Count */
	xLastWakeTime = xTaskGetTickCount();			
	for( ;; )
	{
		flag = 1;		
		/* 故意加入多个循环,让程序运行时间长一点 */
		for (i = 0; i <5; i++)
			printf( "Task 1 is running\r\n" );
#if 1		
		vTaskDelay(xDelay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif		
	}
}

在任务1中,执行到延时函数vTaskDelay(xDelay50ms);任务右执行状态,被调整为挂起,从就绪列表给移除,避免被调度器选中执行,并且加入延时列表,这是因为内核需要计算唤醒时间,并将任务按唤醒时间排序插入延时列表(xDelayedTaskListpxOverflowDelayedTaskList)。恢复调度与切换​,调用 xTaskResumeAll() 恢复调度器,并触发任务切换(portYIELD_WITHIN_API()),让出 CPU 给其他就绪任务。并且该转换状态是已经实现了,不需要开发者在关注。

阻塞态 → 就绪态

延时到期后,内核将任务从延时列表移除,并插入就绪列表,状态自动变为就绪(Ready)。

就绪态 → 运行态

调度器根据优先级自动分配 CPU:若任务优先级最高,则立即抢占当前任务;否则等待调度点(如时间片结束)。

任务是不会退出,但是会在调度器中切换,该栈空间还是会存在的。函数调用栈被完整保留在任务私有堆栈中,等待唤醒后恢复。相当于任务只是暂停。

并且恢复以后唤醒后任务vTaskDelay 的下一行代码继续执行 ​(即 for(;;) 循环的末尾 }),而非重新开始函数或退出循环。

并且vTaskDelay(xDelay20ms);内部会对该任务函数进行上述描述的处理。

vTaskDelay(xDelay20ms);延时函数的作用是:

从进入该延时函数到退出该延时函数是固定的,

也就是说只要在本次任务执行完成,进入该延时函数,那么就必须要固定的绝对的时间才能退出来,也就是固定时间阻塞。

vTaskDelayUntil(&xLastWakeTime, xDelay20ms);延时函数的作用是:

如果我想让该任务周期性执行,固定时间执行。那么就需要使用上述的延时函数。

并且只要在任务1执行范围内,我们调用该函数vTaskDelayUntil(&xLastWakeTime, xDelay20ms);就可以,因为这个延时函数只是固定了终点的时间和上一次的终点时间,也就是说只要在这两个时间点之间执行就行,相当于是告诉调度器,我上一次是什么时候,而我下一次应该是什么时候。固定间隔执行。

通过上述的逻辑分析仪也能看出来时间。

并且这个函数vTaskDelayUntil(&xLastWakeTime, xDelay20ms);记录的是每次的唤醒时间,因此每次都是这一次唤醒时间和下一次的唤醒时间都是20ms。

从这三个图片中可以明显看出,不管Task1的执行时间是多少,任务1就是每隔20ms时间执行一次。并且一定是这一次的唤醒时间和下一次的唤醒时间。

vTaskDelayUntil(&xLastWakeTime, xDelay20ms); xLastWakeTime会自动更新的。

并且还有一些隐藏的细节:

就是在任务1执行完成以后,关于任务2和任务3的启动顺序是不一样的。

目前尝试分析并没有分析出来什么原因。暂时先放在这里。

**==优先级任务相同的情况下,是交替执行。

1.2.8 空闲任务和钩子函数

在创建任务是有返回值的,如果创建成功就会返回dpPASS

失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)

当我们直接创建两个任务的时候,一个是高优先级,一个是低优先级,那么如果没有其他附加条件,高优先级会一直执行,而低优先级不会执行。这是因为FreeRTOS中,任务调度式基于优先级的抢占式调度,高优先的任务总是优先获取CPU使用权,低优先级任务的执行依赖于高优先级任务主动释放CPU资源,所以如果高优先级的不主动释放CPU资源,那么低优先级的任务是无法执行的。

低优先级任务的三种核心执行场景:

1、高优先级任务进入阻塞态(Blocked State)​

当高优先级任务调用阻塞式函数(如 vTaskDelay()xQueueReceive() 等)时,会主动让出 CPU 使用权,进入阻塞态等待事件(如延时结束、信号量释放)。此时调度器会从就绪态任务中选择最高优先级的任务执行。若当前无更高优先级任务就绪,低优先级任务即可获得 CPU 时间

复制代码
存在一种情况,低优先级任务内部创建了一个高优先级任务,然后高优先级任务内部又没有阻塞,那么从此以后就再也不会执行低优先级任务了。

2、高优先级任务主动挂起(Suspended)或删除自身

若高优先级任务调用 vTaskSuspend() 挂起自身或 vTaskDelete() 删除自身,其不再参与调度。调度器会从剩余就绪任务中选择最高优先级任务(可能是低优先级任务)运行。相当于内核不在追踪该高优先级任务,内核直接放弃这个任务了,所以低优先级任务就有机会了。

3、所有高优先级任务均未就绪时(空闲窗口)​

当所有用户任务均阻塞或挂起时,FreeRTOS 会运行优先级为 0 的空闲任务​(Idle Task)。此时若低优先级任务处于就绪态(如延时结束),调度器会将其从就绪态切换至运行态。

  • 1.无阻塞则无执行

    • 若高优先级任务始终未阻塞(如死循环中无延时或同步操作),低优先级任务永远无法执行。这是抢占式调度的核心特性。
  • 2.​状态转换与调度触发

    • 低优先级任务需处于就绪态(Ready)才能被调度。若其因等待事件而阻塞,需等待条件满足(如延时结束)后重回就绪链表,才可能被选中。
  • 3.​中断的影响

    • 中断服务程序(ISR)可抢占任何任务(包括高优先级任务)。若中断中释放了信号量或消息,可能唤醒阻塞的低优先级任务,但该任务仍需等待高优先级任务释放 CPU 后才能运行。
场景 触发条件 调度行为
高优先级任务阻塞 调用 vTaskDelay()、等待队列/信号量等 调度器选择就绪链表中最高优先级任务(可能是低优先级)运行
高优先级任务挂起或删除 调用 vTaskSuspend()vTaskDelete() 低优先级任务作为当前最高优先级就绪任务被选中
所有高优先级任务未就绪 系统进入空闲状态 空闲任务运行,低优先级任务若就绪则可能被调度
c 复制代码
void Task2Function(void * param)
{
	while (1)
	{
		task1flagrun = 0;
		task2flagrun = 1;
		taskidleflagrun = 0;
		printf("2");
		vTaskDelay(2);
		vTaskDelete(NULL);
	}
}

对于该任务自杀以后,需要空闲任务去释放内存,不然是没有任务会进行清理的,就会导致内存用完,报错。

但是在这种情况下:

c 复制代码
void Task1Function(void * param)
{
	TaskHandle_t xHandleTask2;
	BaseType_t xReturn;
	
	while (1)
	{
		task1flagrun = 1;
		task2flagrun = 0;
		taskidleflagrun = 0;
		printf("1");
		xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
		if(xReturn != pdPASS)
			printf("xTaskCreate err\r\n");
		vTaskDelete(xHandleTask2);
			
	}
}

删除以后,直接就会清除内存。为什么呐?

我们知道如果任务2没有阻塞,那么任务1再也不会被执行。但是我们在创建任务2的时候设置了阻塞。

那这就不得不带来一个疑问,空闲任务到底是什么时候有机会执行内存清空的?

FreeRTOS的SysTick中断(通常1ms一次)会强制触发任务调度器检查任务状态。即使Task1未主动阻塞(如无vTaskDelay),​SysTick中断仍会暂停Task1的执行,使调度器有机会切换到空闲任务(Idle Task)。 存疑?

在创建空闲任务以后,注意他的优先级是0,要给他运行的机会,不然永远就不会被运行。

里面不能包含死循环,不然就不能干其他事情了,就不会清空内存了。

所以说这一点还是要注意的。

1.2.9 任务调度算法

configUSE_PREEMPTION 实现是否是抢占式调度

关于是否抢占式调度式可以配置的,一种是抢占式调度,另外一种就是非抢占式调度。抢占式很简单,就是高优先级的执行,然后自己主动阻塞释放CPU资源,给低优先级的任务执行空间。而非抢占式就是很简单,只要我不释放资源,谁都抢不走我。这种也被称为合作调度模式,这样的写法就是每个任务都要配置延时函数,主动的释放相关资源。理解起来还是简单的。在实际应用中,一般都是使用抢占式调度。

configUSE_TIME_SLICING 可抢占的前提下,同优先级的任务是否轮流执行

轮流执行,就是很简单,你执行一次,我执行一次,大家轮流执行。

不轮流执行,就是我要一直执行,除非我主动释放。

可以看出,当最高优先级的任务到来以后,肯定是执行最高优先级的任务,但是高优先级任务执行完成以后,这些同优先级的任务除了空闲任务会主动释放资源,其他的任务都是不会释放任务,那么就会一直霸占CPU资源。

configIDLE_SHOULD_YIELD 空闲任务是否让步于用户任务

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

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

空闲任务如果礼让别人就是相当于自己主动触发一次调度任务函数。

不礼让的波形:

礼让的波形:

可以明显看出两者的区别。

如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。



专栏介绍

《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》



文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】

本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:

署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。

相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。

相关推荐
-森屿安年-13 分钟前
C语言学习笔记——文件
c语言·笔记·学习
夜斗小神社43 分钟前
【LeetCode 热题 100】(六)矩阵
算法·leetcode·矩阵
小六学编程44 分钟前
C语言库中的字符函数
c语言
机器视觉知识推荐、就业指导1 小时前
STM32 外设驱动模块四:光敏电阻(LDR) 模块
stm32·单片机·嵌入式硬件
天地一流殇2 小时前
SimBA算法实现过程
深度学习·算法·对抗攻击·黑盒
Hello_Embed2 小时前
STM32HAL 快速入门(三):从 HAL 函数到寄存器操作 —— 理解 HAL 库的本质
c语言·stm32·单片机·嵌入式硬件·学习
2501_924730612 小时前
智慧城管复杂人流场景下识别准确率↑32%:陌讯多模态感知引擎实战解析
大数据·人工智能·算法·计算机视觉·目标跟踪·视觉检测·边缘计算
weixin_307779132 小时前
C++实现MATLAB矩阵计算程序
开发语言·c++·算法·matlab·矩阵
Kingfar_13 小时前
智能移动终端导航APP用户体验研究案例分享
人工智能·算法·人机交互·ux·用户界面·用户体验