FreeRTOS 第五章的任务的定义和切换

1 任务控制块

区分一下FreeRTOS中的任务,在RTOS中的任务是指拥有TCB,任务栈,以及把任务函数(任务具体的实现函数)这些融合在一起后才叫任务。

首先就是定义任务控制块,在后面的使用中,任务控制块(TCB)是每个任务的身份,在创建任务的时候必须传入,是一个结构体。

ypedef struct tskTaskControlBlock
{
	volatile StackType_t      *pxTopOfStack;        /* 栈顶 */

	ListItem_t			         xStateListItem;    /* 任务节点 */
    
    StackType_t             *pxStack;               /* 任务栈起始地址 */
	                                                /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
} tskTCB;
typedef tskTCB TCB_t;

第一个栈顶是任务栈的栈顶,在静态创建的时候是指向那个预先定义好的一个全局数组的最高位。

第二个是任务节点,此处数据类型不是指针,就是一个节点,之后把任务加入列表就是利用这个节点。

第三个是任务栈的起始地址,如果是静态函数创建,这个指针指向的是定义好的全局数组的首地址。因为在创建静态任务的时候可以发现,传入的参数中的一个是自定义的全局数组的名字,然后把全局数组又转换为指针赋给这个值。

2 静态任务创建(把TCB,任务函数,以及栈空间建立联系)

xTaskCreateStatic

TaskHandle_t xTaskCreateStatic(	TaskFunction_t pxTaskCode,           /* 任务入口 */
					            const char * const pcName,           /* 任务名称,字符串形式 */
					            const uint32_t ulStackDepth,         /* 任务栈大小,单位为字 */
					            void * const pvParameters,           /* 任务形参 */
					            StackType_t * const puxStackBuffer,  /* 任务栈起始地址 */
					            TCB_t * const pxTaskBuffer )         /* 任务控制块指针 */
{

可以见到,在这个函数中,其主要调用了以下这个函数

prvInitialiseNewTask

static void prvInitialiseNewTask( 	TaskFunction_t pxTaskCode,              /* 任务入口 */
									                  const char * const pcName,              /* 任务名称,字符串形式 */
									                  const uint32_t ulStackDepth,            /* 任务栈大小,单位为字 */
								                  	void * const pvParameters,              /* 任务形参 */
						                  			TaskHandle_t * const pxCreatedTask,     /* 任务句柄,等待被改造 */
							                  		TCB_t * pxNewTCB )                       /* 任务控制块指针 */

{

 这个函数实际做了新建任务的事,具体比如获取栈顶的位置,把任务的名字通过FOR循环存入TCB,初始化TCB控制的中的节点,然后把一个空指针(任务句柄)指向控制块,那么在这之后就可以使用这个句柄来代表整个任务,比如删除任务的时候传入这个参数来删除整个任务。

还调用了一个叫做pxPortInitialiseStack 初始化任务栈的函数

这里可能不好理解,实际上是当任务切换的时候,是通过底层的PendSV来实现,属于一个中断,当退出该中断的时候(实际上就已经选好了下一个执行的任务,假设选到了这个),就会把当前任务一些环境变量(寄存器值,PC值)信息从任务栈中pop出来,该初始化的工作实际上就是把这些数据先保存进去,否则当第一次选中了这个任务后,从任务栈中pop数据出来发现是错的,任务就不能执行,所以这一步还保存了任务函数的入口地址。

至此,新任务创建好了

3 实现就绪列表

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

这个并不是列表,它的元素才是列表(根节点),它是一个数组,元素个数是优先级。可以根据任务不同的优先级加入到对应的的数组元素中。就绪列表的初始化就是调用一个for循环,把每个根节点初始化,也就是列表的初始化。

 prvInitialiseTaskLists(),实现就绪列表初始化,可以看出,该函数没有传入参数,而且就绪列表数组只能像上面这样定义,否则,这个初始化函数报错。

4 将任务插入就绪列表 

 vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) )

实际上就是把任务控制块结构体中的节点根据优先级插入就绪数组的不同位置,虽然就绪数组中每个位置都有一个列表(根节点),已经不能加入更多的节点(内容),或者说内存上已经固定,但是每个列表可以在空间上管理列表,里面是各种指针来实现的。

5 启动调度器

vTaskStartScheduler(),这个函数是启动任务调度器,

但是内层调用了xPortStartScheduler,这个有点涉及到硬件底层,所以名字上有个port,端口。

在这个函数中实际上先把NVIC中管哟PendSV和Sys|tick的中断等级弄到最低,后面说为什么,然后在调用

prvStartFirstTask()函数,这个函数是内联汇编。这里需要有点Cortex-M3的知识。还没有进入第一个任务的时候,也就是还在main函数,是运行在线程特权模式,当进入中断的时候需要把寄存器内容保存在主栈msp,因此要先获取主栈指针.然后系统调用SVC,这个系统调用必须马上得到执行,否则出错,而PendSV可以挂起来,因为它中断优先级最低,当没有其它中断的时候执行。

好了先执行vPortSVCHandler(),

当进入这个函数的时候,系统自动把main函数的运行环境的寄存器内容加载到了主栈中(一部分寄存器内容,因为不会返回了,其它的就不保存了)这个函数才是启动系统第一个任务的函数,后面任务切换不会用到了。这个函数首先通过前三行代码获得第一个任务的TCB控制块,并从中取出栈顶指针,然后不断从任务栈把各项数据pop出来。并把这个指针更新到PSP,任务指针。或上0x0d的目的就是为了进入用户线程模式,中断返回的时候会根据r14判断返回的模式(当然,进入中断时候这个寄存器保存的是中断运行前函数运行的环境,从特权线程进入中断就会返回特权线程),因为从主函数进入第一个任务切换的时候是特权线程模式,所以这样做,为了退出进入用户线程。然后退出后会根据psp(而不是MSP,因为退出是进入用户线程模式)把剩下的栈中数据自动出栈,至此,第一个任务运行环境好了。

任务切换函数taskYIELD() 实际上是触发了PendSV中断,开启任务切换。进入该中断的时候已经把上一个任务(主动调用taskYIELD()的任务)的部分寄存器内容压入栈中,然后在该中断函数中,把上个任务的剩下寄存器内容也push进入栈中,

然后调用了一个vTaskSwitchContext 寻找下一个执行的任务,此处抽象一下,反正这个接口实现了寻找下一个任务。然后从下个任务栈中把需要手动加载的寄存器内容提出,再把psp更新成新任务的栈顶,此时退出的时候就不用与上0x0d,相当于从一个用户线程模式进入另外一个用户线程模式。使用的还是psp.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值