> 文章列表 > FreeRTOS 任务切换

FreeRTOS 任务切换

FreeRTOS 任务切换

文章目录

  • 一、PendSV 异常
  • 二、FreeRTOS 任务切换场合
    • 1. 执行系统调用 taskYIELD()
    • 2. 系统滴答定时器(SysTick)中断 SysTick_Handler
  • 三、PendSV 中断服务函数 PendSV_Handler()
  • 四、查找下一个要运行的任务 vTaskSwitchContext()
  • 五、FreeRTOS 时间片调度
  • 六、时间片调度实验

RTOS 系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执行顺序,任务切换效率的高低也决定了一款系统的性能,尤其是对于实时操作系统。

一、PendSV 异常

PendSV(可挂起的系统调用)异常对 OS 操作非常重要,其优先级可以通过编程设置。可以通过将中断控制和壮态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。与 SVC 异常不同,它是不精确的,因此它的挂起壮态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。

利用该特性,若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,这对于上下文切换非常有用,也是各种 OS 设计中的关键。

在具有嵌入式 OS 的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行,如下图所示:

FreeRTOS 任务切换
上下文切换被触发的场合可以是:
⚫ 执行一个系统调用
⚫ 系统滴答定时器(SysTick)中断。

在 OS 中,任务调度器决定是否应该执行上下文切换,如上图中任务切换都是由 SysTick中断中执行,每次它都会决定切换到一个不同的任务中。

若中断请求(IRQ)在 SysTick 异常前产生,则 SysTick 异常可能会抢占 IRQ 的处理,在这种情况下,OS 不应该执行上下文切换,否则中断请求 IRQ 处理就会被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。对于 CortexM3 和 Cortex-M4 处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式,若存在活跃中断服务,且 OS 试图返回到线程模式,则将触发用法 fault,如下图所示。
FreeRTOS 任务切换

在一些 OS 设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时可以检查栈帧中的压栈 xPSR 或 NVIC 中的中断活跃壮态寄存器。不过,系统的性能可能会受到影响,特别时当中断源在 SysTick 中断前后持续产生请求时,这样上下文切换可能就没有执行的机会了。

为了解决这个问题,PendSV 异常将上下文切换请求延迟到所有其他 IRQ 处理都已经完成后,此时需要将 PendSV 设置为最低优先级。若 OS 需要执行上下文切换,他会设置 PendSV 的挂起壮态,并在 PendSV 异常内执行上下文切换。如下图所示:

FreeRTOS 任务切换
上图中事件的流水账记录如下:
(1) 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
(2) OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
(3) 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
(4) 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
(5) 发生了一个中断,并且中断服务程序开始执行
(6) 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
(7) OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
(8) 当 SysTick 退出后,回到先前被抢占的 ISR 中, ISR 继续执行
(9) ISR 执行完毕并退出后, PendSV 服务例程开始执行,并且在里面执行上下文切换。
(10) 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。

综上可知,FreeRTOS 系统的任务切换最终都是在 PendSV中断服务函数中完成的,UCOS 也是在 PendSV 中断中完成任务切换的。

二、FreeRTOS 任务切换场合

在(一)中讲解 PendSV 中断的时候提到了上下文(任务)切换被触发的场合:
● 可以执行一个系统调用
● 系统滴答定时器(SysTick)中断。

1. 执行系统调用 taskYIELD()

执行系统调用就是执行 FreeRTOS系统提供的相关API函数,比如任务切换函数 taskYIELD(),FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。函数 taskYIELD()其实就是个宏,在文件 task.h中有如下定义:

#define taskYIELD() portYIELD()

函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:

#define portYIELD() 								\\
{ 													\\portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \\ (1)\\__dsb( portSY_FULL_READ_WRITE ); 				\\__isb( portSY_FULL_READ_WRITE );				\\
}

(1)、通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换了。

中断级的任务切换函数为 portYIELD_FROM_ISR(),定义如下:

#define portEND_SWITCHING_ISR( xSwitchRequired ) \\
if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

可以看出 portYIELD_FROM_ISR()最终也是通过调用函数 portYIELD()来完成任务切换的。

2. 系统滴答定时器(SysTick)中断 SysTick_Handler

FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:

void SysTick_Handler(void)
{ if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行{xPortSysTickHandler();}
}

在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler(),此函数源码如下:

void xPortSysTickHandler( void )
{vPortRaiseBASEPRI(); (1){if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器 xTickCount 的值{portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (2)}}vPortClearBASEPRIFromISR(); (3)
}

(1)、关闭中断
(2)、通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换了。
(3)、打开中断。

三、PendSV 中断服务函数 PendSV_Handler()

PendSV 中断服务函数本应该为 PendSV_Handler(),但是 FreeRTOS 使用#define 重定义了,如下:

#define xPortPendSVHandler PendSV_Handler

函数 xPortPendSVHandler()源码如下:

__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8mrs r0, psp 										(1)isbldr r3, =pxCurrentTCB 								(2)ldr r2, [r3]							 			(3)stmdb r0!, {r4-r11, r14}							(4)str r0, [r2] 										(5)stmdb sp!, {r3,r14} 								(6)mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 		(7)msr basepri, r0 									(8)dsbisbbl vTaskSwitchContext								(9)mov r0, #0 											(10)msr basepri, r0 									(11)ldmia sp!, {r3,r14} 								(12)ldr r1, [r3] (13)ldr r0, [r1] (14)ldmia r0!, {r4-r11} 								(15)msr psp, r0 										(16)isbbx r14												(17)nop
}

(1)、读取进程栈指针,保存在寄存器 R0 里面。

(2)和(3),获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器 R2 里面。

(4)、保存 r4~r11 和 R14 这几个寄存器的值。

(5)、将寄存器 R0 的值写入到寄存器 R2 所保存的地址中去,也就是将新的栈顶保存在任务控制块的第一个字段中。此时的寄存器 R0 保存着最新的堆栈栈顶指针值,所以要将这个最新的栈顶指针写入到当前任务的任务控制块第一个字段,而经过(2)和(3)已经获取到了任务控制块,并将任务控制块的首地址写如到了寄存器 R2 中。

(6)、将寄存器 R3 和 R14 的值临时压栈,寄存器 R3 中保存了当前任务的任务控制块,而接下来要调用函数 vTaskSwitchContext(),为了防止 R3 和 R14 的值被改写,所以这里临时将 R3和 R14 的值先压栈。

(7)和(8)、关闭中断,进入临界区

(9)、调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将
pxCurrentTCB 更新为这个要运行的任务。

(10)和(11)、打开中断,退出临界区。

(12)、刚刚保存的寄存器 R3 和 R14 的值出栈,恢复寄存器 R3 和 R14 的值。注意,经过(12)步,此时 pxCurrentTCB 的值已经改变了,所以读取 R3 所保存的地址处的数据就会发现其值改变了,成为了下一个要运行的任务的任务控制块。

(13)和(14)、获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器 R0 中。

(15)、R4~R11,R14 出栈,也就是即将运行的任务的现场。

(16)、更新进程栈指针 PSP 的值。

(17)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。

四、查找下一个要运行的任务 vTaskSwitchContext()

在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务,缩减后(去掉条件编译)函数源码如下:

void vTaskSwitchContext( void )
{if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) (1){xYieldPending = pdTRUE;}else{xYieldPending = pdFALSE;traceTASK_SWITCHED_OUT();taskCHECK_FOR_STACK_OVERFLOW();taskSELECT_HIGHEST_PRIORITY_TASK(); (2)traceTASK_SWITCHED_IN();}
}

(1)、如果调度器挂起那就不能进行任务切换。

(2)、调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。
taskSELECT_HIGHEST_PRIORITY_TASK()本质上是一个宏,在 tasks.c 中有定义。

FreeRTOS 中查找下一个要运行的任务有两种方法:一个是通用的方法,另外一个就是使用硬件的方法,至于选择哪种方法通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为 1 的时候就使用硬件的方法,否则的话就是使用通用的方法,我们来看一下这两个方法的区别。

①通用方法
顾名思义,就是所有的处理器都可以用的方法,方法如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK() 						 \\
{ 																 \\UBaseType_t uxTopPriority = uxTopReadyPriority;				 \\while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \\ (1){ 														 	 \\configASSERT( uxTopPriority );							 \\--uxTopPriority; 										 \\} 															 \\listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, 					 \\ (2)&( pxReadyTasksLists[ uxTopPriority ] ) );					 \\uxTopReadyPriority = uxTopPriority; 						 \\
} 

(1)、pxReadyTasksLists[]为就绪任务列表数组,一个优先级一个列表,同优先级的就绪任务都挂到相对应的列表中。uxTopReadyPriority 代表处于就绪态的最高优先级值,每次创建任务的时候都会判断新任务的优先级是否大于 uxTopReadyPriority,如果大于的话就将这个新任务的优先级赋值给变量 uxTopReadyPriority。函数 prvAddTaskToReadyList()也会修改这个值,也就是说将某个任务添加到就绪列表中的时候都会用 uxTopReadyPriority 来记录就绪列表中的最高优先级。这里就从这个最高优先级开始判断,看看哪个列表不为空就说明哪个优先级有就绪的任务。函数 listLIST_IS_EMPTY()用于判断某个列表是否为空,uxTopPriority 用来记录这个有就绪任务的优先级。

(2)、已经找到了有就绪任务的优先级了,接下来就是从对应的列表中找出下一个要运行的任务,查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB,这样我们就确定了下一个要运行的任务了。

可以看出通用方法是完全通过 C 语言来实现的,肯定适用于不同的芯片和平台,而且对于任务数量没有限制,但是效率肯定相对于使用硬件方法的要低很多。

2、硬件方法
硬件方法就是使用处理器自带的硬件指令来实现的,比如 Cortex-M 处理器就带有的计算前导 0 个数指令:CLZ,函数如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK()  \\
{ 											\\UBaseType_t uxTopPriority; 				\\portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \\ (1)configASSERT( listCURRENT_LIST_LENGTH( & 			\\( pxReadyTasksLists[ uxTopPriority ] ) )> 0 ); 		\\listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,		    \\ (2)&( pxReadyTasksLists[ uxTopPriority ] ) ); 			\\
} 

(1) 、 通 过 函 数 portGET_HIGHEST_PRIORITY() 获 取 处 于 就 绪 态 的 最 高 优 先 级 ,portGET_HIGHEST_PRIORITY 本质上是个宏,定义如下:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL\\- ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

使用硬件方法的时候 uxTopReadyPriority 就不代表处于就绪态的最高优先级了,而是使用每个 bit 代表一个优先级,bit0 代表优先级 0,bit31 就代表优先级 31,当某个优先级有就绪任务的话就将其对应的 bit 置 1。从这里就可以看出,如果使用硬件方法的话最多只能有 32 个优先级。__clz(uxReadyPriorities)就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高位开始(bit31)到第一个为 1 的 bit,其间 0 的个数,如下例子:
二进制数 1000 0000 0000 0000 的前导零个数就为 0。
二进制数 0000 1001 1111 0001 的前导零个数就是 4。

得到 uxTopReadyPriority 的前导零个数以后在用 31 减去这个前导零个数得到的就是处于就绪态的最高优先级了,比如优先级 30 为此时的处于就绪态的最高优先级,30 的前导零个数为1,那么 31-1=30,得到处于就绪态的最高优先级为 30。

(2)、已经找到了处于就绪态的最高优先级了,接下来就是从对应的列表中找出下一个要运行的任务,查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给pxCurrentTCB,这样我们就确定了下一个要运行的任务了。

可以看出硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级,但是会限制任务的优先级数,比如 STM32 只能有 32 个优先级,不过 32 个优先级已经完全够用了。

FreeRTOS 是支持时间片的,每个优先级可以支持无限多个任务。

五、FreeRTOS 时间片调度

前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级,这些任务的调度是一个值得考虑的问题,不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,至于下一个要运行哪个任务?FreeRTOS 中的这种调度方法就是时间片调度。如下图展示了运行在同一优先级下的执行时间图,在优先级 N 下有 3 个就绪的任务。

FreeRTOS 任务切换
1、任务 3 正在运行。
2、这时一个时钟节拍中断(滴答定时器中断)发生,任务 3 的时间片用完,但是任务 3 还
没有执行完。
3、FreeRTOS 将任务切换到任务 1,任务 1 是优先级 N 下的下一个就绪任务。
4、任务 1 连续运行至时间片用完。
5、任务 3 再次获取到 CPU 使用权,接着运行。
6、任务 3 运行完成,调用任务切换函数 portYIELD()强行进行任务切换放弃剩余的时间片,从而使优先级 N 下的下一个就绪的任务运行。
7、FreeRTOS 切换到任务 1。
8、任务 1 执行完其时间片。

要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期,比如本教程中 configTICK_RATE_HZ 为 1000,那么一个时间片的长度就是 1ms。时间片调度发生在滴答定时器的中断服务函数中,前面讲解滴答定时器中断服务函数的时候说了在中断服务函数 SysTick_Handler()中会调用 FreeRTOS 的 API 函数 xPortSysTickHandler(),而函数 xPortSysTickHandler() 会 引 发 任 务 调 度 , 但 是 这 个 任 务 调 度 是 有 条 件 的 , 函 数xPortSysTickHandler()如下:

void xPortSysTickHandler( void )
{vPortRaiseBASEPRI();{if( xTaskIncrementTick() != pdFALSE ){portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;}}vPortClearBASEPRIFromISR();
}

上述代码中if( xTaskIncrementTick() != pdFALSE )表明只有函数 xTaskIncrementTick()的返回值不为 pdFALSE 的时候就会进行任务调度!查看函数 xTaskIncrementTick()会发现有如下条件编译语句:

BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;TickType_t xItemValue;BaseType_t xSwitchRequired = pdFALSE;if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){/***************************************************************************//***************************此处省去一大堆代码******************************//***************************************************************************/#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) (1){if( listCURRENT_LIST_LENGTH( &( \\pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) (2){xSwitchRequired = pdTRUE; (3)}else{mtCOVERAGE_TEST_MARKER();}}#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )}return xSwitchRequired;
}

(1)、当宏 configUSE_PREEMPTION 和宏 configUSE_PREEMPTION 都为 1 的时候下面的代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为 1,缺一不可!

(2)、判断当前任务所对应的优先级下是否还有其他的任务。

(3)、如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE。

从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数
xTaskIncrementTick() 就 会 返 回 pdTURE , 由 于 函 数 返 回 值 为 pdTURE 因 此 函 数xPortSysTickHandler()就会进行一次任务切换。

六、时间片调度实验

1、实验目的
学习使用 FreeRTOS 的时间片调度。

2、实验设计
本实验设计三个任务:start_task、task1_task 和 task2_task ,其中 task1_task 和 task2_task的任务优先级相同,都为 2,这三个任务的任务功能如下:
start_task:用来创建其他 2 个任务。
task1_task :控制 LED0 灯闪烁,并且通过串口打印 task1_task 的运行次数。
task2_task :控制 LED1 灯闪烁,并且通过串口打印 task2_task 的运行次数。

3、实验程序与分析

● 系统设置
为了观察方便,将系统的时钟节拍频率设置为 20,也就是将宏 configTICK_RATE_HZ 设置为 20:

#define configTICK_RATE_HZ (20) 

这样设置以后滴答定时器的中断周期就是 50ms 了,也就是说时间片值为 50ms,这个时间片还是很大的,不过大一点我们到时候观察的时候方便。

● 任务设置

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级 (1)
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define TASK2_TASK_PRIO 2 //任务优先级 (2)
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数

(1)和(2)、任务 task1_task 和 task2_task 的任务优先级设置为相同的,这里都设置为 2。

● main()函数

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化 LEDLCD_Init(); //初始化 LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 9-1");LCD_ShowString(30,50,200,16,16,"FreeRTOS Round Robin");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )"start_task", //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_0PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度
}

在 main 函数中我们主要完成硬件的初始化,在硬件初始化完成以后创建了任务 start_task并且开启了 FreeRTOS 的任务调度。

● 任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建 TASK1 任务xTaskCreate((TaskFunction_t )task1_task, (const char* )"task1_task", (uint16_t )TASK1_STK_SIZE, (void* )NULL, (UBaseType_t )TASK1_TASK_PRIO, (TaskHandle_t* )&Task1Task_Handler); //创建 TASK2 任务xTaskCreate((TaskFunction_t )task2_task, (const char* )"task2_task", (uint16_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )&Task2Task_Handler); vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}//task1 任务函数
void task1_task(void *pvParameters)
{u8 task1_num=0;while(1){task1_num++; //任务 1 执行次数加 1 注意 task1_num1 加到 255 的时候会清零!!LED0=!LED0;taskENTER_CRITICAL(); //进入临界区printf("任务 1 已经执行:%d 次\\r\\n",task1_num);taskEXIT_CRITICAL(); //退出临界区//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度delay_xms(10); (1)}
}//task2 任务函数
void task2_task(void *pvParameters)
{u8 task2_num=0;while(1){task2_num++; //任务 2 执行次数加 1 注意 task2_num1 加到 255 的时候会清零!!LED1=!LED1;taskENTER_CRITICAL(); //进入临界区printf("任务 2 已经执行:%d 次\\r\\n",task2_num);taskEXIT_CRITICAL(); //退出临界区//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度delay_xms(10); (2)}
}

(1)、调用函数 delay_xms()延时 10ms。在一个时间片内如果任务不主动放弃 CPU 使用权的话那么就会一直运行这一个任务,直到时间片耗尽。在 task1_task 任务中我们通过串口打印字符串的方式提示 task1_task 在运行,但是这个过程对于 CPU 来说执行速度很快,不利于观察,所以这里通过调用函数 delay_xms()来默认任务占用 10ms 的 CPU。函数 delay_xm()不会引起任务调度,这样的话相当于 task1_task 的执行周期>10ms,基本可以看作等于 10ms,因为其他的函数执行速度还是很快的。一个时间片的长度是 50ms,任务执行所需的时间以 10ms 算,理论上在一个时间片内 task1_task 可以执行 5 次,但是事实上很少能执行 5 次,基本上是 4 次。

(2)、同理(1)

4、实验现象
FreeRTOS 任务切换
不管是 task1_task 还是 task2_task 都是连续执行 4,5 次,和前面程序设计的一样,说明在一个时间片内一直在运行一个任务,当时间片用完后就切换到下一个任务运行。

摄像头大全