四. 任务
学习目标:
学会创建任务,掌握任务是如何切换的。
一、任务相关定义
- 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中
-
3. 为什么要有任务控制块?怎么定义的?//定义一个任务函数 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--); }- 裸机系统中,程序主体是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中
-
4. 任务创建函数如何实现?(哪三大函数,层层递进)//定义任务控制块 TCB_t Task1TCB; TCB_t Task2TCB;- 哪三大函数?分别在哪里创建的?
- (1)xTaskCreateStatic() 位于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中定义,实际上就是一个空指针。见下面代码:
-
在此处我们停留下,对比(2)中的TaskFunction_t 的定义,它们两者有无区别?/* 任务句柄 */ typedef void * TaskHandle_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,它不能整除8,AND 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中定义)
-
这里也是一个层层嵌套的函数。
-
主函数1:
-
-
-
void vTaskStartScheduler(void) { /* 手动指定第一个运行的任务 (1) */ pxCurrentTCB = &Task1TCB; /* 指定调度器 */ if(xPortStartScheduler() != pdFALSE) { /* 调度器启动成功,则不会返回,即不会来到这里 */ } }对于上面的代码需要解释的是:
-
(1)pxCurrentTCB是一个在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 ;PRIMASK=1 ;关中断 CPSIE I ;PRIMASK=0 ;开中断 CPSID F ;FAULTMASK=1 ;关异常 CPSIE F ;FAULTMASK=0 ;开异常对于这段代码,我们需要知道:
-
PRIMASK和FAULTMASK是Cortex-M内核里面三个中断屏蔽寄存器中的两个,还有一个BASEPRI,有关他们的用法见下图:
-
名字 功能描述 PRIMASK 这是一个只有单一比特的寄存器。置1后,关掉所有异常,只留下NMI和硬FAULT可以响应。缺省值为0,表示没有关中断 FAULTMASK 这也是个只有一个位的寄存器。置1后,只有NMI可以响应,所有其它的异常,甚至是硬FAULT,也统统闭嘴。缺省值为0,表示没有关异常。 BASEPRI 这个寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。若被设成0,则不关闭任何中断,缺省值为0.
-
-
- 这是系统调度前的准备,也就是说,当任务创建好之后,我们把任务添加到就绪表里这就表示任务已经就绪了,系统可以随时调度。
- 为什么要定义一个就绪列表?就绪列表是干什么用的?
-
-
- (1)xTaskCreateStatic() 位于task.c ->freertos中创建并添加到freertos/source中
- 哪三大函数?分别在哪里创建的?
-
- 怎么定义任务控制块?(如此简单)位于main.c中
- (2)任务名称,字符串形式,长度由宏configMAX_TASK_NAME_LEN决定,该宏在FreeRTOSConfig.h中定义,默认为16.(可以参考FreeRTOS中宏名称的定义方法)
- (1)任务节点,内置在TCB中,通过这个节点可以将任务控制块挂接到各种链表中
-
- 任务控制块的类型声明代码如下:
- 那么定义一个任务控制块需要一个新的数据类型,该数据类型在task.c中声明(为了tskTCB这个数据类型能在其它地方使用,我们把任务控制块的声明放在了FreeRTOS.h中)
- 有了这个任务控制块之后,以后对系统任务的全部操作都是可以通过任务控制块来实现。
- 裸机系统中,程序主体是CPU顺序执行的,而在多任务系统中,任务的执行要靠系统调度,那么系统是如何调度的呢?为此每个任务需要定义一个任务控制块,相当于任务的身份证,里面存有任务的所有信息,如任务的栈指针,任务名称,任务形参...
-
-
-
-
-
-
- 1. 本小节我们要实现的是两个变量按一定频率轮流翻转,每个变量对应一个任务,那么就需要定义两个任务栈,见下面代码:(在多任务系统中,有多少个任务就定义多少个任务栈)
-
-
-
(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博客
描述的更详细。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)