学习目标:

        学会创建任务,掌握任务是如何切换的。

一、任务相关定义

  1.       1. 什么是任务?
    1.         根据功能的不同,把整个系统分割成一个个独立的不能返回函数,这个函数就是任务。

      1.       2. 什么是栈?什么是任务栈?

        1.         在裸机系统中,当子函数被调用、中断发生,全局变量和局部变量,函数返回地址都放在哪里?实际应用中我们是不需要关心的,实际上他们都放在栈里(裸机中叫主栈),那什么是栈呢?栈是单片机RAM里一段连续的内存空间,栈大小一般在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。

                  怎么理解栈和RAM的关系?

                  我们知道,在裸机中,单片机里面有一段内存(RAM),主栈只占一部分内存空间,主栈是由启动文件进行配置的,主栈是用来存放main中的局部变量,而main函数之外的全局变量还有其他的东西是放在主栈之外的其他RAM空间。如图辅助理解:                                                                                                     对于操作系统来说,每个任务都是独立的,互不干扰的,当被调用的时候,任务会被保存到栈中,所以我们要为每个任务分配一个独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态内存分配的一段内存空间,但他们都属于RAM中。除了裸机上的RAM分配之外,我们还会定义一个内存管理器,用于管理系统动态分配的一些内存,这些系统动态分配的内存可以用来做任务的栈,也可以用来做其他的事情,我们也可以自己定义静态的数组,比如a[1024],a[1024]数组所占用的栈属于全局变量,它也可以用来作为任务的栈空间。如图辅助理解:                   

          二、代码部分
          1.      1. 本小节我们要实现的是两个变量按一定频率轮流翻转,每个变量对应一个任务,那么就需要定义两个任务栈,见下面代码:(在多任务系统中,有多少个任务就定义多少个任务栈)   
            1.      
              //定义任务栈
              #define TASK1_STACK_SIZE                    128
              StackType_t Task1Stack[TASK1_STACK_SIZE];
              
              #define TASK2_STACK_SIZE                    128
              StackType_t Task2Stack[TASK2_STACK_SIZE];

                      以上代码需要说明的是:

              1. 任务栈起始就是一个预先定义好的全局数据,数据类型为StackType_t,大小由TASK1_STACK_SIZE这个宏来定义,默认为128,单位为字,即512字节,这也是FreeRTOS推荐的最小任务栈。StackType_t(其实就是uint32_t)是一个经重定义后的数据类型,存在于portmacro.h中(第一次使用需要再include文件夹下新建然后添加到工程freertos/source这个组文件中)。

                1.         2.任务函数是怎么定义的?

                  1.         由上文可知,任务是一个独立的函数,函数主体无限循环且不能返回。

                    1.         以下代码存在main.c中

                      1. //定义一个任务函数
                        void Task1_Entry(void *p_arg)
                        {
                        	for(;;)
                        	{
                        		flag1 = 1;
                        		delay(100);
                        		flag1 = 0;
                        		delay(100);
                        	}
                        }
                        
                        //定义一个任务函数
                        void Task2_Entry(void *p_arg)
                        {
                        	for(;;)
                        	{
                        		flag2 = 1;
                        		delay(100);
                        		flag2 = 0;
                        		delay(100);
                        	}
                        }
                        
                        //定义一个软件延时函数
                        void delay(uint32_t count)
                        {
                        	for(;count != 0;count--);
                        }
                        ​​​        3. 为什么要有任务控制块?怎么定义的?
                        1.         裸机系统中,程序主体是CPU顺序执行的,而在多任务系统中,任务的执行要靠系统调度,那么系统是如何调度的呢?为此每个任务需要定义一个任务控制块,相当于任务的身份证,里面存有任务的所有信息,如任务的栈指针,任务名称,任务形参...
                          1.         有了这个任务控制块之后,以后对系统任务的全部操作都是可以通过任务控制块来实现。
                            1.         那么定义一个任务控制块需要一个新的数据类型,该数据类型在task.c中声明(为了tskTCB这个数据类型能在其它地方使用,我们把任务控制块的声明放在了FreeRTOS.h中)
                              1.         任务控制块的类型声明代码如下:
                                1. typedef struct tskTaskControlBlock
                                  {
                                  	volatile StackType_t *pxTopOfStack; /* 栈顶指针 */  
                                  	ListItem_t xStateListItem;          /* 任务节点 (1) */    
                                  	StackType_t *pxStack;               /* 任务栈起始地址 */
                                  	char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称,字符串类型 (2) */  
                                  }tskTCB;
                                  typedef tskTCB TCB_t; /* 数据类型重定义*/
                                          对于以上代码需要解释的是:
                                  1.         (1)任务节点,内置在TCB中,通过这个节点可以将任务控制块挂接到各种链表中
                                    1.         (2)任务名称,字符串形式,长度由宏configMAX_TASK_NAME_LEN决定,该宏在FreeRTOSConfig.h中定义,默认为16.(可以参考FreeRTOS中宏名称的定义方法)
                                      1.         怎么定义任务控制块?(如此简单)位于main.c中        
                                        1. //定义任务控制块
                                          TCB_t Task1TCB;
                                          TCB_t Task2TCB;
                                                  4. 任务创建函数如何实现?(哪三大函数,层层递进)
                                          1.         哪三大函数?分别在哪里创建的?
                                            1.         (1)xTaskCreateStatic()    位于task.c ->freertos中创建并添加到freertos/source中
                                              1.           (2)   prvInitialiseNewTask() 位于task.c
                                              2.           (3)   portInitialiseStack() 位于port.c ->freertos/portable/RVDS/ARM-CM4中创建并添加到freertos/source中
                                                1.         1). xTaskCreateStatic()
                                                2. /* 静态任务创建函数 */
                                                  #if (configSUPPORT_STATIC_ALLOCATION == 1)     //是否支持静态内存 =1表示支持 (1)
                                                  
                                                  TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,        /* 任务入口 (2) */   //TaskFunction_t在projdefs.h声明,并且已经在#include "FreeRTOS.h"中声明引用
                                                                                  const char * const pcName,        /* 任务名称,字符串形式 */
                                                  								const uint32_t ulStackDepth,      /* 任务栈大小,单位为字 */
                                                  								void * const pvParameters,        /* 任务形参 */
                                                  								StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
                                                  								TCB_t * const pxTaskBuffer )        /* 任务控制块指针 */
                                                  {
                                                  	TCB_t *pxNewTCB;//定义一个任务的控制块,其实就是一个新的任务控制块指针
                                                  	TaskHandle_t xReturn;//(3)
                                                  	
                                                  	if((pxTaskBuffer != NULL) && (puxStackBuffer != NULL))
                                                  	{
                                                  		pxNewTCB = (TCB_t *) pxTaskBuffer;//任务控制块的指针赋给一个新的任务控制块
                                                  	    pxNewTCB->pxStack = (StackType_t *) puxStackBuffer;//把任务栈的起始地址赋给任务控制块的任务栈地址
                                                  	
                                                  	    /* 创建新的任务 */
                                                  		//调用初始化新任务的函数来进行初始化
                                                  	    prvInitialiseNewTask( pxTaskCode,      /* 任务入口 */
                                                  	                          pcName,          /* 任务名称,字符串形式 */
                                                  	                          ulStackDepth,    /* 任务栈大小,单位为字 */
                                                  	                          pvParameters,    /* 任务形参 */ 
                                                                                &xReturn,         /* 任务句柄 */
                                                  	                          pxNewTCB);       /* 任务栈起始地址 */
                                                  	}//(4)
                                                  	else
                                                  	{
                                                  		xReturn = NULL;
                                                  	}
                                                  	/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
                                                  	return xReturn;
                                                  }
                                                  #endif /* configSUPPORT_STATIC_ALLOCATION */
                                                  对于上面的代码需要解释的是:
                                                3. (1)FreeRTOS中,任务的创建有两种方法,一种是静态创建,一种是动态创建。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,当任务删除时,内存是不能释放的。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,当任务删除时,内存可以释放。我们这里举例是采用静态创建的方法,其中宏configSUPPORT_STATIC_ALLOCATION在FreeRTOSConfig.h中定义,配置为1.
                                                  (2)  ​​​​​任务入口,也就是任务的函数名称。TaskFunction_t 在projdef.h中的一个重定义的宏(第一次使用需要在include中新建然后添加到工程freertos/source这个组文件中),实际上他是一个空指针。代码如下:
                                                4. #ifndef PROJDEFS_H
                                                  #define PROJDEFS_H
                                                  
                                                  typedef void (*TaskFunction_t)(void *);
                                                  
                                                  #define pdFALSE ((BaseType_t) 0)
                                                  #define pdTRUE ((BaseType_t) 1)
                                                  #define pdPASS (pdTRUE)
                                                  #define pdFAIL (pdFALSE)
                                                  
                                                  #endif /* PROJDEFS_H */
                                                5. (3)xReturn是一个定义的任务句柄,用于指向任务的TCB。它的数据类型是TaskHandle_t,在task.h中定义,实际上就是一个空指针。见下面代码:
                                                6. /* 任务句柄 */
                                                  typedef void * TaskHandle_t;
                                                     在此处我们停留下,对比(2)中的TaskFunction_t 的定义,它们两者有无区别?
                                                7. 这里我们把他们两个的定义摘出来

                                                8. typedef void (*TaskFunction_t)(void *);
                                                  
                                                  /* 任务句柄 */
                                                  typedef void * TaskHandle_t;

                                                  这两行代码是FreeRTOS等实时操作系统(RTOS)中非常核心的类型定义,分别用于定义任务函数指针和任务句柄。

                                                9. typedef void (*TaskFunction_t) (void *);

                                                10. 是一个函数指针类型的定义;它定义了一个名为TaskFunction_t的新类型,该类型的变量是一个指针,指向一个函数;它的返回值是void(无返回值);参数是void *,这是一个通用指针,通常用于任务创建时传入的参数;在FreeRTOS中,每个任务的执行主体必须是一个符合此签名的函数。

                                                11. typedef void * TaskHandle_t;

                                                12. 这是一个对象句柄类型的定义,;他也是定义了一个名为TaskHandle_t的新类型,本质上是一个void *(即空指针或通用指针);用于标识和操作一个特定的任务,当调用xTaskCreate()创建任务时,系统会返回一个TaskHandle_t类型的句柄,后续对该任务进行操作(如删除、挂起、恢复、获取状态等)都需要通过这个句柄来指定具体哪个任务。

                                                13. 这里需要注意的是,虽然定义为void *,但是在FreeRTOS内部实现中,它通常指向任务的任务控制块(TCB)结构体,使用void *是为了隐藏内部实现细节。

                                                14. 综上,TaskFunction_t本质是一个函数指针,指向了任务的执行代码,作为xTaskCreate()的第一个参数,告诉系统“这个任务要运行哪个函数”;TaskHandle_t本质是一个数据指针,它指向任务的控制信息(TCB),作为xTaskCreate()的最后一个参数输出,或者在vDelete等API中输入,用来找到并操作特定任务。

                                                15. 这两者配合使用构成了FreeRTOS任务管理的基础,TaskFunction_t定义了任务做什么,TaskHandle_t定义了任务是谁。

                                                16. (4) 调用了prvInitialiseNewTask()函数,用于创建新任务。

                                                  1.         2). prvInitialiseNewTask(),具体代码如下:

                                                  2. static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,      /* 任务入口 */
                                                    	                      const char * pcName,                /* 任务名称,字符串形式 */
                                                    	                      const uint32_t ulStackDepth,        /* 任务栈大小,单位为字 */
                                                    	                      void * const pvParameters,          /* 任务形参 */
                                                    	                      TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
                                                    	                      TCB_t *pxNewTCB)                  /* 任务控制块指针 */
                                                    {
                                                    	StackType_t *pxTopOfStack;//定义一个栈顶指针
                                                    	UBaseType_t x;
                                                    	
                                                    	/* 获取栈顶地址 */
                                                    	pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t) 1);
                                                    	/* 向下做8字节对齐 (1)*/
                                                    	pxTopOfStack = (StackType_t *) (((uint32_t) pxTopOfStack) & (~((uint32_t) 0x0007)));
                                                    	/* 将任务的名字存储到TCB中 */
                                                    	for(x = (UBaseType_t) 0; x < (UBaseType_t) configMAX_TASK_NAME_LEN; x++)
                                                    	{
                                                    		pxNewTCB->pcTaskName[x] = pcName[x];
                                                    		if(pcName[x] == 0x00)
                                                    		{
                                                    			break;
                                                    		}
                                                    	}
                                                    	/* 任务的名字不能超过configMAX_TASK_NAME_LEN 并以'\0'结尾 */
                                                    	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1] = '\0';
                                                    	/* 初始化TCB中的xStateListItem节点 (2) */
                                                    	vListInitialiseItem(&(pxNewTCB->xStateListItem));
                                                    	/* 设置xStateListItem节点的拥有者 也就是拥有这个节点本身的TCB */
                                                       	listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
                                                    	
                                                    	/* 初始化任务栈 (3) */
                                                    	pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
                                                    	
                                                    	/* 让任务句柄指向任务控制块 */
                                                    	if((void *) pxCreatedTask != NULL)
                                                    	{
                                                    		*pxCreatedTask = (TaskHandle_t) pxNewTCB;
                                                    	}
                                                    }

                                                    对于上面的代码需要解释的是:

                                                  3. (1)在Cortex-M4内核的单片机中,因为总线宽度是32位,通常只需要4字节对齐即可,为什么还要8字节对齐呢?这说明什么,说明有些操作是64位的,那是什么呢?就是浮点运算,(目前我们没有涉及到,这是为了后面兼容浮点运算的考虑)。如果栈顶指针是8字节对齐的,那么在进行向下8字节对齐的时候,指针不会移动,如果不是8字节对齐的,再向下做8字节对齐的时候,就会空出几个字节,不会使用,比如当栈顶指针(pxTopOfStack)是33,它不能整除8,AND FFF8 等于32,进行向下8字节对齐的是32,那么就会空出一个字节不使用。

                                                  4. (2)初始化TCB中的xStateListItem节点,也就是初始化该节点所在的链表为空,表示节点还没有插入任何链表。

                                                  5. (3)调用pxPortInitialiseStack()初始化任务栈,并更新栈顶指针,任务第一次运行时的环境参数就存在任务栈中。

                                                    1.         2). pxPortInitialiseStack(),具体代码如下:

                                                    2. /* 宏定义 */
                                                      #define portINITAL_XPSR           (0x01000000)
                                                      #define portSTART_ADDRESS_MASK    ((StackType_t) 0xfffffffeul)
                                                      
                                                      /* 任务栈初始化函数 */
                                                      static void prvTaskExitError(void)
                                                      {
                                                      	/* 函数停在这里 */
                                                      	for(;;);
                                                      }
                                                      
                                                      //任务栈初始化函数
                                                      StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters)
                                                      {
                                                      	/* 异常发生时,自动加载到CPU寄存器的内容 (1)*/
                                                      	pxTopOfStack--;
                                                      	*pxTopOfStack = portINITAL_XPSR;                                 /* xPSR的bit24必须置1 */
                                                      	pxTopOfStack--;
                                                      	*pxTopOfStack = ((StackType_t) pxCode) & portSTART_ADDRESS_MASK; /* PC,即任务入口函数 */
                                                      	pxTopOfStack--;
                                                      	*pxTopOfStack = (StackType_t) prvTaskExitError;                  /* LR,函数返回地址 (2)*/
                                                      	pxTopOfStack -= 5; /* R12,R3,R2 and R1默认初始化为0 */
                                                      	*pxTopOfStack = (StackType_t) pvParameters;                      /* R0,任务形参 */
                                                      	
                                                      	/* 异常发生时,手动加载到CPU寄存器的内容 */
                                                      	pxTopOfStack -= 8; /* R11,R10,R9,R8,R7,R6,R5 and R4默认初始化为0 */
                                                      	
                                                      	/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 (3)*/
                                                      	return pxTopOfStack;
                                                      }

                                                      对于上面的代码需要解释的是:

                                                    3. (1)异常发生时,CPU自动从栈中加载到CPU寄存器的内容。包括8个寄存器,分别为R0 R1 R2 R3 R12 R14 R15和xPSR的位24,且顺序不能变。

                                                    4. (2)任务的返回地址,通常任务是不会返回的,如果返回了就跳转到prvTaskRxitError,该函数是一个无限循环。

                                                    5. (3)返回栈顶指针,此时栈顶指针(pxTopOfStack)的指向如下图所示。任务第一次运行时,就是从这个栈指针开始手动加载8个字的内容到CPU寄存器,R4 R5 R6 R7 R8 R9 R10 R11,当退出异常时,栈中剩余的8个字的内容会自动加载到CPU寄存器,R0 R1 R2 R3 R12 R14 R15 xPSR的位24。此时PC指针就指向了任务入口地址,从而成功跳转到第一个任务。

                                                    6.         5. 就绪列表如何实现?(怎么定义的?怎么初始化?怎么把任务插入到就绪列表中?)
                                                      1.         为什么要定义一个就绪列表?就绪列表是干什么用的?
                                                        1.         这是系统调度前的准备,也就是说,当任务创建好之后,我们把任务添加到就绪表里这就表示任务已经就绪了,系统可以随时调度。
                                                          1.         那就绪列表是怎么定义的呢?见下面代码(在task.c中定义):
                                                          2. /* 任务就绪列表 */
                                                            List_t pxReadyTasksLists[configMAX_PRIORITIES];

                                                            这里我们要说明的是,就绪列表其实就是一个List_t类型的数组,数组的大小由宏configMAX_PRIORITIES决定,该宏表示最大优先级,它在FreeRTOSConfig.h中定义,默认为5,最大支持256个优先级。数组的下标对应了任务的优先级,同一优先级的任务插入到就绪列表的同一条链表中。一个空的就绪列表见下图来理解:

                                                            1.         定义之后我们就要初始化就绪列表,那就绪列表是怎么初始化的呢?这里是在prvInitialiseTaskLists()中实现的,代码如下(在task.c中定义):
                                                            2. /* 初始化任务相关的列表 */
                                                              void prvInitialiseTaskLists(void)
                                                              {
                                                              	UBaseType_t uxPriority;
                                                              	for(uxPriority = (UBaseType_t) 0U; uxPriority < (UBaseType_t) configMAX_PRIORITIES; uxPriority++)
                                                              	{
                                                              		vListInitialise (&(pxReadyTasksLists[uxPriority]));
                                                              	}
                                                              }

                                                              初始化之后该怎么理解,下面的示意图可以帮助到你:

                                                            3. 其实就是使用链表根节点初始化函数vListInitialise() 将这个List_t类型的数组每个成员(也就是就绪列表中的每个成员) 初始化(好好去体会)

                                                            4.         初始化之后,怎么将任务插入到就绪列表中呢?

                                                              1.         这里我们要联想到任务控制块里面有一个xStateListItem成员,它的数据类型是ListItem_t,通过将该成员插入到就绪列表中 就可实现 将任务插入到就绪列表。实现代码如下(在main函数中):

                                                              2. 	prvInitialiseTaskLists();
                                                                    Task1_Handle = xTaskCreateStatic(Task1_Entry,
                                                                									  "Task1_Entry",
                                                                									  TASK1_STACK_SIZE,
                                                                									  NULL,
                                                                									  Task1Stack,
                                                                									  &Task1TCB);
                                                                	
                                                                	/* 核心函数是 vListInser ,将任务控制块的列表项插入到就绪列表中 */
                                                                	vListInser(&pxReadyTasksLists[1], &Task1TCB.xStateListItem);
                                                                	Task2_Handle = xTaskCreateStatic(Task2_Entry,
                                                                									  "Task2_Entry",
                                                                									  TASK2_STACK_SIZE,
                                                                									  NULL,
                                                                									  Task2Stack,
                                                                									  &Task2TCB);
                                                                	vListInser(&pxReadyTasksLists[2], &Task2TCB.xStateListItem);

                                                                在上面代码中我们实现的是,将Task1任务插入到就绪列表下标为1的链表中,将Task2任务插入到就绪列表下标为2的链表中,下面的示意图也可以帮助我们理解它:

                                                              3.         6. 调度器如何实现?(怎么启动调度器?任务怎么切换?)

                                                                1.         调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表中找到优先级高的任务,然后去执行该任务。实际上 调度器的实现 无非就是 由几个全局变量和一些可以实现任务切换的函数组成,全部在task.c中实现。

                                                                  1.         那么怎么启动调度器呢?(由vTaskStartScheduler()完成)代码如下:(在task.c中定义)

                                                                    1.         这里也是一个层层嵌套的函数。

                                                                      1.         主函数1:

                                                                  2. void vTaskStartScheduler(void)
                                                                    {
                                                                    	/* 手动指定第一个运行的任务 (1) */
                                                                    	pxCurrentTCB = &Task1TCB;
                                                                    	
                                                                    	/* 指定调度器 */
                                                                    	if(xPortStartScheduler() != pdFALSE)
                                                                    	{
                                                                    		/* 调度器启动成功,则不会返回,即不会来到这里 */
                                                                    	}
                                                                    }

                                                                    对于上面的代码需要解释的是:

                                                                  3. (1)pxCurrentTCB是一个在task.c中定义的TCB_t类型的全局指针,用于指向当前正在运行或者将要运行的任务的任务控制块。目前我们还不支持优先级,这里就手动指定一个。定义代码如下:

                                                                  4. /* 当前正在运行的任务的任务控制块指针,默认初始化为NULL */
                                                                    TCB_t * volatile pxCurrentTCB = NULL;

                                                                    使用volatile来修饰他,让编译器从他的源地址来找他的变量,而不是从引用的地址来找到他。

                                                                  5. (2)调用xPortStartScheduler()函数,启动调度器,若调度器启动成功,不会返回。该函数怎么实现呢?见下面代码:(port.c中定义,此处可以联想下函数的定义方法)

                                                                    1. 嵌套函数1:

                                                                  6. /* 
                                                                     *参考资料《STM32F10xxx Cortex_M3 programing manual》4.4.3,百度搜索“PM0056”即可找到这个文档
                                                                     *在Cortex-M中,内核外设SCB中的SHPR3寄存器用于设置Systick和PendSV的异常优先级
                                                                     *System handler priority register 3(SCB_SHPR3) SCB_SHPR3:0xE000 ED20
                                                                     *Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
                                                                     *Bits 23:16 PRI_14[7:0]: Priority of system handler 14,PendSV
                                                                     */
                                                                    #define portNVIC_SYSPRI2_REG (*((volatile uint32_t *) 0xe000ed20))
                                                                    #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY) << 16UL)
                                                                    #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY) << 24UL)
                                                                    
                                                                    /* 调度器启动函数 */
                                                                    BaseType_t xPortStartScheduler(void)
                                                                    {
                                                                    	/* 配置PendSV和SysTick的中断优先级为最低 (1) */
                                                                    	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
                                                                    	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
                                                                    	
                                                                    	/* 启动第一个任务,不再返回 */
                                                                    	prvStartFirstTask();
                                                                    	
                                                                    	/* 不应该运行到这里 */
                                                                    	return 0;
                                                                    }

                                                                    对于上面的代码需要解释的是:

                                                                  7. (1)不管是什么任务,凡是切换任务都是在PendSV中切换的。PenSV是一个可挂起的中断,当进行任务切换就会进入PenSV中断中,然后将当前任务的栈内容保存放任务栈中,并且切换到下一个任务。而SysTick是一个系统的时基,驱动了系统的运行。那为什么要把PendSV和SysTick的中断优先级配置为最低?SysTick和PenSV都涉及到系统调度,系统调度的优先级要低于系统的其他硬件的中断优先级,也就是说会优先响应系统中的外部硬件中断,所以PendSV和SysTick的中断优先级要配置为最低,防止抢占系统响应了其它的外部硬件中断。那怎么配置呢?FreeRTOS中直接通过操作它的寄存器来配置的,为什么?因为这些Cortex-MC类的东西他是一个内核而不是外设,内核基本都是一样的,而PendSV和SysTick都是内核中断,所以直接操作寄存器就可以了。见下图:

                                                              4. 可以看出,这两个寄存器的高4位是可以用的,rw,而低4位只读不可写。其中还有一个宏定义:

                                                              5. #define configKERNEL_INTERRUPT_PRIORITY 255 /高4位有效,即等于0xff,或者是15 */
                                                              6. (2)调用函数prvStartFirstTask()启动第一个任务,启动成功后,不再返回,该函数是一个C文件,内嵌汇编,在port.c中实现。

                                                                1. 嵌套函数2:

                                                                2. /* 
                                                                   *参考资料《STM32F10xxx Cortex_M3 programing manual》4.4.3,百度搜索“PM0056”即可找到这个文档
                                                                   *在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F
                                                                   *0xE000ED08为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
                                                                  */
                                                                  __asm void prvStartFirstTask(void)
                                                                  {
                                                                  	//以下全是汇编语言
                                                                  	PRESERVE8 /* 8字节对齐 */
                                                                  	
                                                                  	/*在Cortex-M中,0xE000ED08为SCB外设中SCB_VTOR这个寄存器的地址,
                                                                  	  里面存放的是向量表的起始地址,即MSP的地址 (1) */
                                                                  	ldr r0, =0xE000ED08  //将立即数0xE000ED08加载到寄存器R0
                                                                  	ldr r0, [r0] //将地址0xE000ED08指向的内容加载到寄存器R0,此时R0就等于SCB_VTOR的值,等于0x00000000 ,也就是memory的起始地址
                                                                  	ldr r0, [r0] //将0x00000000这个地址指向的内容加载到r0,此时R0等于0x200008DB
                                                                  	
                                                                  	/* 设置主堆栈指针msp的值 */
                                                                  	msr msp,r0 //将R0的值存储在MSP,此时MSP等于0x200008DB,这是主栈的栈顶指针。其实这一步有点多余,因为当系统启动的时候,执行完Reset_Handler的时候,向量表已经初始化完毕,MSP的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针
                                                                  	
                                                                  	/* 使能全局中断 (2) */
                                                                  	cpsie i  //开中断
                                                                  	cpsie f  //开异常
                                                                  	dsb
                                                                  	isb
                                                                  	
                                                                  	/* 调用SVC区启动第一个任务 (3) */
                                                                  	svc 0
                                                                  	nop
                                                                  	nop
                                                                  }

                                                                  对于上面的代码需要解释的是:

                                                                3. (1)在Cortex-M中,0xE000ED08是SCB_VTOR寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址。向量表通常是从内部FLASH的起始地址开始存放,那么可知memory:0x00000000处存放的就是MSP的值。也可以通过仿真查看内存的值证实。

                                                                4. (2)使用CPS指令把全局中断打开。为了快速的开关中断,Cortex-M内核专门设置了CPS指令,它有4种用法:

                                                                5. CPSID I ;PRIMASK=1 ;关中断
                                                                  CPSIE I ;PRIMASK=0 ;开中断
                                                                  CPSID F ;FAULTMASK=1 ;关异常
                                                                  CPSIE F ;FAULTMASK=0 ;开异常

                                                                  对于这段代码,我们需要知道:

                                                                6. PRIMASK和FAULTMASK是Cortex-M内核里面三个中断屏蔽寄存器中的两个,还有一个BASEPRI,有关他们的用法见下图:

                                                                7. 名字 功能描述
                                                                  PRIMASK 这是一个只有单一比特的寄存器。置1后,关掉所有异常,只留下NMI和硬FAULT可以响应。缺省值为0,表示没有关中断
                                                                  FAULTMASK 这也是个只有一个位的寄存器。置1后,只有NMI可以响应,所有其它的异常,甚至是硬FAULT,也统统闭嘴。缺省值为0,表示没有关异常。
                                                                  BASEPRI 这个寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。若被设成0,则不关闭任何中断,缺省值为0.

(3)产生系统调用,服务号0表示SVC中断,接下来将会执行SVC中断服务函数。

SVC中断服务函数:

在FreeRTOS中,SVC中断服务函数的名字是vPortSVCHandler(),但是在启动文件中SVC的中断服务函数名字是SVC_Handler,为了能顺利相应,有两个方法:一是改启动文件中的名称,而是改vPortSVCHandler()。这里我们采用第二种方法,怎么解决呢?我们可以定义一个宏,放在FreeRTOSConfig.h文件中,这里顺便也把PendSV和SysTick的中断服务函数名也同理改下,如下:

#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define xPortSVCHandler SVC_Handler

    vPortSVCHandler()函数才算是真正的启动一个任务,不再返回。代码如下:(port.c中定义)

__asm void vPortSVCHandler(void)
{
	extern pxCurrentTCB;
	
	PRESERVE8
	
	ldr r3,=pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
	ldr r1,[r3]  /* 加载pxCurrentTCB到R1 */
	ldr r0,[r1]  /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
	ldmia r0!,{r4-r11}  /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
	msr psp,r0   /* 将r0的值,即任务的栈指针更新到psp */
	isb
	mov r0,#0         /* 恢复R0的值,设置r0的值为0 */
	msr basepri,r0    /* 设置basepri寄存器的值为0,即所有的中断没有被屏蔽 */
	orr r14,#0xd      /* (1)当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
	                      使得硬件在退出时使用进程栈指针PSP完成出栈操作并返回后进入线程模式,返回Thumb状态 */
	bx r14            /* 异常返回,这个时候栈中剩下的内容将自动加载到CPU寄存器:
	                     xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                     同时PSP的值也将更新,即指向任务栈的栈顶 */
}

对于上面的代码我们会有很多疑问。

什么是MSP?什么是PSP?简单来说,MSP是主栈指针,PSP是进程栈指针。在使用操作系统的时候,我们可以简单的将这两个指针理解为,当在线程中运行的时候,使用的就是PSP指针,也就是说线程的那些栈它都是用的PSP指针,而假如发生了一些异常,使用的就是MSP指针。简言之,PSP在线程中,MSP在异常中

(1)假如R14的值是0xfffffffx     假如x的值1101,也就是D,那么它的位1是0,位1为0表示使用的是PSP指针,位1表示使用MSP指针;位2表示使用特权模式(0)还是用户模式(1)

上面的内容学完之后,最后一步,任务是怎么切换的呢?

        什么是任务切换?怎么理解?

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务,但是目前我们还没有学到支持优先级的部分,这里不支持优先级,而是仅仅实现了两个任务的轮流切换。

        任务切换靠任务切换函数taskYIELD()来实现。它的实现理解起来也挺简单,其实就是将PendSV的悬起位置1,当没有其它中断运行的时候响应PendSV中断,去执行我们写好的PendSV中断服务函数,在里面实现任务切换。

在上文中,我们已经使用宏定义将xPortPendSVHandler()定义为PendSV_Handler

        对于PendSV很多人都不太理解,这里我也把手册里的内容摘出来了,见上图:我们看一下PendSV的悬起位,bit28,什么是悬起位呢?比如说在一个任务中把它置1,它就会触发一个PendSV中断,假如在中断中把它置1,因为PendSV的优先级是最低的,中断中置1没用,没法立即响应它的中断,因此他会被悬起。我们看下bit28的说明:

        只有写1就改变它的状态为挂起状态。

在实现xPortPendSVHandler之前我们先实现一下taskYIELD(),也就是PortYIELD()。

这里我们插入一个知识点,与硬件接口相关的函数都是port.c porable.h portmacro.h中。

        PortYIELD()在portmacro.h中实现,见下面代码:

/* 中断控制状态寄存器 
 * Bit 28 PENDSVSET:PendSV悬起位 
 */
#define portNVIC_INT_CTRL_REG    (*((volatile uint32_t *) 0xe000ed04))
#define portNVIC_PENDSVSET_BIT    (1UL << 28UL)
#define portSY_FULL_READ_WRITE    (15)

#define portYIELD() \
{                                                   \
	/* 触发PendSV,产生上下文切换 */                 \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
	__dsb(portSY_FULL_READ_WRITE);                  \
	__isb(portSY_FULL_READ_WRITE);                  \
}

而后在task.h中将taskYIELD()定义为PortYIELD(),然后在任务中调用taskYIELD()

xPortPendSVHandler在哪里实现呢?

__asm void xPortPendSVHandler(void)
{
	extern pxCurrentTCB;//声明一个外部变量,也就是指向当前任务控制块的指针
	extern vTaskSwitchContext;//声明一个外部函数,用于选择要运行哪一个任务的
	
	PRESERVE8 //8字节对齐
	/* 当进入PendSV Handler时,上一个任务运行的环境即:
	   xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	   这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
	/* 获取任务栈指针到r0 */
	mrs r0,psp
	isb
	
	ldr r3,=pxCurrentTCB    /* 加载pxCurrentTCB的地址到r3 */
	ldr r2, [r3]            /* 加载pxCurrentTCB到r2 */
	
	stmdb r0!,{r4-r11}      /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
	str r0,[r2]             /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
	
	stmdb sp!,{r3,r14}      /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
	                           调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
	                           R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
	mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */
	msr basepri,r0
	dsb
	isb
	bl vTaskSwitchContext   /* 调用函数vTaskSwitchContext,寻找新的任务执行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
	mov r0,#0               /* 退出临界段 */
	msr basepri,r0
	ldmia sp!,{r3,r14}      /* 恢复r3和r14*/
	
	ldr r1,[r3]
	ldr r0,[r1]             /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0 */
	ldmia r0!,{r4-r11}      /* 出栈 */
	msr psp,r0
	isb
	bx r14                  /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式,
	                           使用PSP堆栈指针还是MSP堆栈指针,当调用bx r14指令后,硬件会知道要从异常返回,
	                           然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
	                           当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行 */
	nop
	
}

这里是三个步骤:上文保存;下文切换;中间调用一个寻找任务优先级的函数,也就是寻找要运行的任务。

怎么实现vTaskSwitchContext  呢?

void vTaskSwitchContext(void)
{
	/* 两个任务轮流切换 */
	if(pxCurrentTCB == &Task1TCB)
	{
		pxCurrentTCB = &Task2TCB;
	}
	else
	{
		pxCurrentTCB = &Task1TCB;
	}
}

上面的内容如果您已经理解了,那么看到这里就可以了。


下面的内容是一些理论性的内容或者是定义吧。

1,主要理解ARM的核心寄存器(SP,LR,PC,xPSR)以及如何在KEIL中理解它

R13 R14 R15 xPSR是ARM Cortex-M系列处理器的核心寄存器,上面的截图是我们在KEIL中调试时显示的相关值:

R13(SP):也就是栈指针(Stack Pointer),指向当前栈顶的内存地址,用于函数调用、中断处理时保存现场和局部变量。

R14(LR):链接寄存器(Link Register),保存函数调用或中断返回的地址,用于从子程序或中断服务程序返回。

R15(PC):程序计数器(Program Counter),保存下一条要执行指令的内存地址,决定CPU执行代码的位置。

xPSR:程序状态寄存器(Program Status Register),包含CPU当前的状态信息,如条件标志(N、Z、C、V)、中断屏蔽位、当前运行模式等。

上图是一个典型的ARM-Cortex-M系列MCU的寄存器窗口截图,下面我们逐行分析下它的意义:

关于这些内容的详细理解,我们可以参考这位大佬的内容:

简单理解:ARM 核心寄存器(SP、LR、PC、xPSR)以及如何看这些值在keil软件MDK来调试代码_keil中显示cortex-m4的寄存器列表中的states-CSDN博客

描述的更详细。

Logo

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

更多推荐