ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

四. 任务

四. 任务 学习目标学会创建任务掌握任务是如何切换的。一、任务相关定义1. 什么是任务?根据功能的不同把整个系统分割成一个个独立的且不能返回的函数这个函数就是任务。2. 什么是栈什么是任务栈在裸机系统中当子函数被调用、中断发生全局变量和局部变量函数返回地址都放在哪里实际应用中我们是不需要关心的实际上他们都放在栈里(裸机中叫主栈)那什么是栈呢栈是单片机RAM里一段连续的内存空间栈大小一般在启动文件或者链接脚本里面指定最后由C库函数_main进行初始化。怎么理解栈和RAM的关系我们知道在裸机中单片机里面有一段内存(RAM)主栈只占一部分内存空间主栈是由启动文件进行配置的主栈是用来存放main中的局部变量而main函数之外的全局变量还有其他的东西是放在主栈之外的其他RAM空间。如图辅助理解对于操作系统来说每个任务都是独立的互不干扰的当被调用的时候任务会被保存到栈中所以我们要为每个任务分配一个独立的栈空间这个栈空间通常是一个预先定义好的全局数组也可以是动态内存分配的一段内存空间但他们都属于RAM中。除了裸机上的RAM分配之外我们还会定义一个内存管理器用于管理系统动态分配的一些内存这些系统动态分配的内存可以用来做任务的栈也可以用来做其他的事情我们也可以自己定义静态的数组比如a[1024]a[1024]数组所占用的栈属于全局变量它也可以用来作为任务的栈空间。如图辅助理解二、代码部分1. 本小节我们要实现的是两个变量按一定频率轮流翻转每个变量对应一个任务那么就需要定义两个任务栈见下面代码在多任务系统中有多少个任务就定义多少个任务栈//定义任务栈 #define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE];以上代码需要说明的是任务栈起始就是一个预先定义好的全局数据数据类型为StackType_t大小由TASK1_STACK_SIZE这个宏来定义默认为128单位为字即512字节这也是FreeRTOS推荐的最小任务栈。StackType_t其实就是uint32_t是一个经重定义后的数据类型存在于portmacro.h中(第一次使用需要再include文件夹下新建然后添加到工程freertos/source这个组文件中)。2.任务函数是怎么定义的由上文可知任务是一个独立的函数函数主体无限循环且不能返回。以下代码存在main.c中//定义一个任务函数 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. 为什么要有任务控制块怎么定义的裸机系统中程序主体是CPU顺序执行的而在多任务系统中任务的执行要靠系统调度那么系统是如何调度的呢为此每个任务需要定义一个任务控制块相当于任务的身份证里面存有任务的所有信息如任务的栈指针任务名称任务形参...有了这个任务控制块之后以后对系统任务的全部操作都是可以通过任务控制块来实现。那么定义一个任务控制块需要一个新的数据类型该数据类型在task.c中声明为了tskTCB这个数据类型能在其它地方使用我们把任务控制块的声明放在了FreeRTOS.h中任务控制块的类型声明代码如下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任务节点内置在TCB中通过这个节点可以将任务控制块挂接到各种链表中2任务名称字符串形式长度由宏configMAX_TASK_NAME_LEN决定该宏在FreeRTOSConfig.h中定义默认为16.可以参考FreeRTOS中宏名称的定义方法怎么定义任务控制块如此简单位于main.c中//定义任务控制块 TCB_t Task1TCB; TCB_t Task2TCB;4. 任务创建函数如何实现哪三大函数层层递进哪三大函数分别在哪里创建的1xTaskCreateStatic() 位于task.c -freertos中创建并添加到freertos/source中(2) prvInitialiseNewTask() 位于task.c(3) portInitialiseStack() 位于port.c -freertos/portable/RVDS/ARM-CM4中创建并添加到freertos/source中1). xTaskCreateStatic()/* 静态任务创建函数 */ #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 */对于上面的代码需要解释的是(1)FreeRTOS中任务的创建有两种方法一种是静态创建一种是动态创建。静态创建时任务控制块和栈的内存需要事先定义好是静态的内存当任务删除时内存是不能释放的。动态创建时任务控制块和栈的内存是创建任务时动态分配的当任务删除时内存可以释放。我们这里举例是采用静态创建的方法其中宏configSUPPORT_STATIC_ALLOCATION在FreeRTOSConfig.h中定义配置为1.(2) ​​​​​任务入口也就是任务的函数名称。TaskFunction_t 在projdef.h中的一个重定义的宏第一次使用需要在include中新建然后添加到工程freertos/source这个组文件中实际上他是一个空指针。代码如下#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 */(3)xReturn是一个定义的任务句柄用于指向任务的TCB。它的数据类型是TaskHandle_t在task.h中定义实际上就是一个空指针。见下面代码/* 任务句柄 */ typedef void * TaskHandle_t;在此处我们停留下对比2中的TaskFunction_t 的定义它们两者有无区别这里我们把他们两个的定义摘出来typedef void (*TaskFunction_t)(void *); /* 任务句柄 */ typedef void * TaskHandle_t;这两行代码是FreeRTOS等实时操作系统RTOS中非常核心的类型定义分别用于定义任务函数指针和任务句柄。typedef void (*TaskFunction_t) (void *);是一个函数指针类型的定义它定义了一个名为TaskFunction_t的新类型该类型的变量是一个指针指向一个函数它的返回值是void无返回值参数是void *这是一个通用指针通常用于任务创建时传入的参数在FreeRTOS中每个任务的执行主体必须是一个符合此签名的函数。typedef void * TaskHandle_t;这是一个对象句柄类型的定义他也是定义了一个名为TaskHandle_t的新类型本质上是一个void *(即空指针或通用指针)用于标识和操作一个特定的任务当调用xTaskCreate()创建任务时系统会返回一个TaskHandle_t类型的句柄后续对该任务进行操作如删除、挂起、恢复、获取状态等都需要通过这个句柄来指定具体哪个任务。这里需要注意的是虽然定义为void *但是在FreeRTOS内部实现中它通常指向任务的任务控制块TCB结构体使用void *是为了隐藏内部实现细节。综上TaskFunction_t本质是一个函数指针指向了任务的执行代码作为xTaskCreate()的第一个参数告诉系统“这个任务要运行哪个函数”TaskHandle_t本质是一个数据指针它指向任务的控制信息TCB作为xTaskCreate()的最后一个参数输出或者在vDelete等API中输入用来找到并操作特定任务。这两者配合使用构成了FreeRTOS任务管理的基础TaskFunction_t定义了任务做什么TaskHandle_t定义了任务是谁。(4) 调用了prvInitialiseNewTask()函数用于创建新任务。2). prvInitialiseNewTask()具体代码如下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; } }对于上面的代码需要解释的是1在Cortex-M4内核的单片机中因为总线宽度是32位通常只需要4字节对齐即可为什么还要8字节对齐呢这说明什么说明有些操作是64位的那是什么呢就是浮点运算目前我们没有涉及到这是为了后面兼容浮点运算的考虑。如果栈顶指针是8字节对齐的那么在进行向下8字节对齐的时候指针不会移动如果不是8字节对齐的再向下做8字节对齐的时候就会空出几个字节不会使用比如当栈顶指针(pxTopOfStack)是33它不能整除8AND FFF8 等于32进行向下8字节对齐的是32那么就会空出一个字节不使用。2初始化TCB中的xStateListItem节点也就是初始化该节点所在的链表为空表示节点还没有插入任何链表。3调用pxPortInitialiseStack()初始化任务栈并更新栈顶指针任务第一次运行时的环境参数就存在任务栈中。2). pxPortInitialiseStack()具体代码如下/* 宏定义 */ #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; }对于上面的代码需要解释的是1异常发生时CPU自动从栈中加载到CPU寄存器的内容。包括8个寄存器分别为R0 R1 R2 R3 R12 R14 R15和xPSR的位24且顺序不能变。2任务的返回地址通常任务是不会返回的如果返回了就跳转到prvTaskRxitError该函数是一个无限循环。3返回栈顶指针此时栈顶指针(pxTopOfStack)的指向如下图所示。任务第一次运行时就是从这个栈指针开始手动加载8个字的内容到CPU寄存器R4 R5 R6 R7 R8 R9 R10 R11当退出异常时栈中剩余的8个字的内容会自动加载到CPU寄存器R0 R1 R2 R3 R12 R14 R15 xPSR的位24。此时PC指针就指向了任务入口地址从而成功跳转到第一个任务。5. 就绪列表如何实现怎么定义的怎么初始化怎么把任务插入到就绪列表中为什么要定义一个就绪列表就绪列表是干什么用的这是系统调度前的准备也就是说当任务创建好之后我们把任务添加到就绪表里这就表示任务已经就绪了系统可以随时调度。那就绪列表是怎么定义的呢见下面代码在task.c中定义/* 任务就绪列表 */ List_t pxReadyTasksLists[configMAX_PRIORITIES];这里我们要说明的是就绪列表其实就是一个List_t类型的数组数组的大小由宏configMAX_PRIORITIES决定该宏表示最大优先级它在FreeRTOSConfig.h中定义默认为5最大支持256个优先级。数组的下标对应了任务的优先级同一优先级的任务插入到就绪列表的同一条链表中。一个空的就绪列表见下图来理解定义之后我们就要初始化就绪列表那就绪列表是怎么初始化的呢这里是在prvInitialiseTaskLists()中实现的代码如下在task.c中定义/* 初始化任务相关的列表 */ void prvInitialiseTaskLists(void) { UBaseType_t uxPriority; for(uxPriority (UBaseType_t) 0U; uxPriority (UBaseType_t) configMAX_PRIORITIES; uxPriority) { vListInitialise ((pxReadyTasksLists[uxPriority])); } }初始化之后该怎么理解下面的示意图可以帮助到你其实就是使用链表根节点初始化函数vListInitialise() 将这个List_t类型的数组每个成员也就是就绪列表中的每个成员 初始化好好去体会初始化之后怎么将任务插入到就绪列表中呢这里我们要联想到任务控制块里面有一个xStateListItem成员它的数据类型是ListItem_t通过将该成员插入到就绪列表中 就可实现 将任务插入到就绪列表。实现代码如下在main函数中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的链表中下面的示意图也可以帮助我们理解它6. 调度器如何实现怎么启动调度器任务怎么切换调度器是操作系统的核心其主要功能就是实现任务的切换即从就绪列表中找到优先级高的任务然后去执行该任务。实际上 调度器的实现 无非就是 由几个全局变量和一些可以实现任务切换的函数组成全部在task.c中实现。那么怎么启动调度器呢由vTaskStartScheduler()完成代码如下在task.c中定义这里也是一个层层嵌套的函数。主函数1void vTaskStartScheduler(void) { /* 手动指定第一个运行的任务 1 */ pxCurrentTCB Task1TCB; /* 指定调度器 */ if(xPortStartScheduler() ! pdFALSE) { /* 调度器启动成功则不会返回即不会来到这里 */ } }对于上面的代码需要解释的是1pxCurrentTCB是一个在task.c中定义的TCB_t类型的全局指针用于指向当前正在运行或者将要运行的任务的任务控制块。目前我们还不支持优先级这里就手动指定一个。定义代码如下/* 当前正在运行的任务的任务控制块指针默认初始化为NULL */ TCB_t * volatile pxCurrentTCB NULL;使用volatile来修饰他让编译器从他的源地址来找他的变量而不是从引用的地址来找到他。2调用xPortStartScheduler()函数启动调度器若调度器启动成功不会返回。该函数怎么实现呢见下面代码port.c中定义此处可以联想下函数的定义方法嵌套函数1/* *参考资料《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; }对于上面的代码需要解释的是1不管是什么任务凡是切换任务都是在PendSV中切换的。PenSV是一个可挂起的中断当进行任务切换就会进入PenSV中断中然后将当前任务的栈内容保存放任务栈中并且切换到下一个任务。而SysTick是一个系统的时基驱动了系统的运行。那为什么要把PendSV和SysTick的中断优先级配置为最低SysTick和PenSV都涉及到系统调度系统调度的优先级要低于系统的其他硬件的中断优先级也就是说会优先响应系统中的外部硬件中断所以PendSV和SysTick的中断优先级要配置为最低防止抢占系统响应了其它的外部硬件中断。那怎么配置呢FreeRTOS中直接通过操作它的寄存器来配置的,为什么因为这些Cortex-MC类的东西他是一个内核而不是外设内核基本都是一样的而PendSV和SysTick都是内核中断所以直接操作寄存器就可以了。见下图可以看出这两个寄存器的高4位是可以用的rw,而低4位只读不可写。其中还有一个宏定义#define configKERNEL_INTERRUPT_PRIORITY 255 /高4位有效即等于0xff,或者是15 */2调用函数prvStartFirstTask()启动第一个任务启动成功后不再返回该函数是一个C文件内嵌汇编在port.c中实现。嵌套函数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 }对于上面的代码需要解释的是1在Cortex-M中0xE000ED08是SCB_VTOR寄存器的地址里面存放的是向量表的起始地址即MSP的地址。向量表通常是从内部FLASH的起始地址开始存放那么可知memory:0x00000000处存放的就是MSP的值。也可以通过仿真查看内存的值证实。2使用CPS指令把全局中断打开。为了快速的开关中断Cortex-M内核专门设置了CPS指令它有4种用法CPSID I PRIMASK1 关中断 CPSIE I PRIMASK0 开中断 CPSID F FAULTMASK1 关异常 CPSIE F FAULTMASK0 开异常对于这段代码我们需要知道PRIMASK和FAULTMASK是Cortex-M内核里面三个中断屏蔽寄存器中的两个还有一个BASEPRI有关他们的用法见下图名字功能描述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_HandlervPortSVCHandler()函数才算是真正的启动一个任务不再返回。代码如下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(任务入口地址)R14R12R3R2R1R0(任务的形参) 同时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 PENDSVSETPendSV悬起位 */ #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中调试时显示的相关值R13SP也就是栈指针(Stack Pointer)指向当前栈顶的内存地址用于函数调用、中断处理时保存现场和局部变量。R14LR链接寄存器(Link Register)保存函数调用或中断返回的地址用于从子程序或中断服务程序返回。R15PC程序计数器(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博客描述的更详细。
返回列表