FreeRTOS实时操作系统的任务创建与任务切换
目录
- 任务控制块数据结构
- 任务创建函数
- 定义就绪表
- 就绪表初始化
- 启动调度器
- 任务切换
任务控制块数据结构
任务控制块数据结构在task.c
声明
typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; //栈顶指针 ListItem_t xStateListItem; //任务节点 StackType_t * pxStack; //任务栈起始地址 char pcTaskName[configMAX_TASK_NAME_LEN];//任务名称 }tskTCB;
任务创建函数
下面是用静态方式创建任务的函数
static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,/*任务函数*/ const char * const 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字节对齐 pxTopOfStack = (StackType_t*) ( (uint32_t)pxTopOfStack & (~(uint32_t)0x0007) ); //存储名字 for(x=(UBaseType_t)0;x<(UBaseType_t)configMAX_TASK_NAME_LEN;x++) { pxNewTCB->pcTaskName[x] = pcName[x]; if(pcName[x]=='\0') break; } pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN-1] = '\0'; //初始化TCB中的xStateListItem列表项 vListInitialiseItem(& (pxNewTCB->xStateListItem)); //设置xStateListItem列表项的拥有者即为传入的TCB listSET_LIST_ITEM_OWNER( &(pxNewTCB->xStateListItem), pxNewTCB ); //初始化任务栈,并返回更新的栈顶指针 pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack,pxTaskCode,pvParameters); //返回任务句柄 if((void*)pxCreatedTask !=NULL ) { *pxCreatedTask = (TaskHandle_t) pxNewTCB; } }
下面看pxPortInitialiseStack
是怎么初始化任务栈的
因为是向下生长的满栈,所以是--
操作
#define portINITIAL_XPSR (0x01000000) #define portSTART_ADDRESS_MASK ((StackType_t)0xfffffffeUL) StackType_t * pxPortInitialiseStack(StackType_t* pxTopOfStack, TaskFunction_t pxCode, void*pvParameters) { //-------------设置内核会自动加载的寄存器,且顺序不能变 pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR;//xPSR bit24 pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;//R15 PC pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError;//R14 LR pxTopOfStack -= 5; //R12 R3 R2 R1 *pxTopOfStack = ( StackType_t ) pvParameters;//R0 //--------------下面8个需要手动保存 pxTopOfStack-=8; //返回更新后的栈顶指针 return pxTopOfStack; }
如下图
定义就绪表
在task.c
中,就绪表就是列表类型的数组,元素个数是configMAX_PRIORITIES
,就绪表把同一优先级的任务插入到同一优先级的链表中,数组下标表示优先级,如下图
#define configMAX_PRIORITIES 5 List_t pxReadyTasksLists[configMAX_PRIORITIES];
就绪表初始化
就绪表初始化就是把数组每个元素(即列表)初始化(即调用vListInitialise),结果如下
void prvInitialiseTaskLists(void) { UBaseType_t uxPriority; for(uxPriority = (UBaseType_t)0 ; uxPriority < (UBaseType_t) configMAX_PRIORITIES; uxPriority ++) { vListInitialise(&(pxReadyTasksLists[uxPriority])); } }
启动调度器
调用关系,这里手动指定第一个运行的任务
vTaskStartScheduler->xPortStartScheduler->prvStartFirstTask
void vTaskStartScheduler(void) { //手动指定一个和要运行的任务 pxCurrentTCB = &Task1TCB; //启动调度器 if(xPortStartScheduler()!=pdFALSE) { //启动成功不会跑到这里 } }
//stm32 用4bit表示优先级,所以最低优先级则是15 #define configKERNEL_INTERRUPT_PRIORITY 15 #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中断优先级设置为最低 portNVIC_SYSPRI2_REG |=portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |=portNVIC_SYSTICK_PRI; //启动第一个任务,该函数不会返回 prvStartFirstTask(); //不会运行到这里 return 0; }
在CM3中,0xE000ED08存放的是SCB_VTOR寄存器地址,SCB_VTOR是向量表的起始地址,即MSP的地址
__asm void prvStartFirstTask(void) { PRESERVE8 ldr r0, =0xE000ED08 //r0=0xE000ED08,相当于r0=&SCB_VTOR ldr r0, [r0] //r0=*r0,即r0=*((uint32_t*)0xE000ED08),即r0=0x0 ldr r0, [r0] //r0=*r0,即r0=*((uint32_t*)0x0),即r0=msp指针值 msr msp, r0 //msp=r0,msp指针获得msp地址 cpsie i //开中断 cpsie f //开中断 dsb isb svc 0 //触发SVC系统调用,此后会进入SVCHandler nop //下面这2个nop不会执行 nop }
__asm void vPortSVCHandler(void) { extern pxCurrentTCB; PRESERVE8 //r3=&pxCurrentTCB,注意pxCurrentTCB本身就是一个指针,是指向一个任务的TCB指针 ldr r3, =pxCurrentTCB //r1=*r3,即r1=pxCurrentTCB ldr r1, [r3] //r0=*r1,此时r1=pxCurrentTCB, //又结构体第一个成员(栈顶指针)的地址和结构体地址数值上是相同的, //所以r0=*(&(pxCurrentTCB.pxTopOfStack)),即r0=pxCurrentTCB.pxTopOfStack, //即r0此时指向当前任务空闲栈顶位置 ldr r0, [r1] //以r0为开始把r4-r11依次保存到当前任务的psp栈 ldmia r0!,{r4-r11} //更新当前任务psp栈指针 msr psp,r0 isb mov r0, #0 msr basepri,r0 //开中断 //此时r14是0xFFFF_FFF9,表示返回后进入Thread mode,使用MSP,返回Thmub状态, //这里其实是把bit2置1,要求返回后使用PSP orr r14,#0x0d //返回,这里会自动加载初始化任务栈填写的xPSR、R15、R14、R12、R3-R0值到内核对应寄存器, //所以返回后就直接到pxCurrentTCB指向的任务 bx r14 }
任务切换
这里手动触发任务切换,其实就是向中断控制及状态寄存器ICSR(地址0xE000_ED04)PENDSVSET位(bit28)写1悬起pendsv中断,在pendsv_handler中寻找下一个要运行的任务并做任务切换
#define taskYIELD() portYIELD()
#define portNVIC_INT_CTRL_REG (*((volatile uint32_t*)0xe000ed04)) #define portNVIC_PENDSVSET_BIT (1<<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); \ }
__asm void xPortPendSVHandler(void) { //进入中断前会自动保存xPSR、R15、R14、R12、R3-R0到当前任务P栈中,此时任务栈顶指针指向要保存R11的位置 extern pxCurrentTCB; extern vTaskSwitchContext;//这个函数是用来寻找下一个要运行的任务 PRESERVE8 mrs r0,psp //当前任务栈psp指针存入r0, isb ldr r3,=pxCurrentTCB //r3=&pxCurrentTCB ldr r2,[r3] //r2=*r3, 即r2=pxCurrentTCB //以r0开始递减的手动保存r4-r11 stmdb r0!,{r4-r11} //r0=*r2,此时r2=pxCurrentTCB, //又结构体第一个成员(栈顶指针)的地址和结构体地址数值上是相同的, //所以r0=*(&(pxCurrentTCB.pxTopOfStack)),即r0=pxCurrentTCB.pxTopOfStack, //即r0此时指向当前任务空闲栈顶位置 str r0,[r2] //到这里上文就保存完成 //将r3、r14压入栈保存起来(此时sp使用的是msp) //r14要保存是因为等下要调用函数,避免r14被覆盖无法从pendsv中断正常返回 //r3要保存是因为r3保存的是当前正在运行任务控制块的二级指针&pxCurrentTCB,后面要通过r3来操作pxCurrentTCB来切到下文 stmdb sp!,{r3,r14} //关中断,阈值是configMAX_SYSCALL_INTERRUPT_PRIORITY mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri,r0 dsb isb //调用vTaskSwitchContext,功能是找到优先级最高的任务,然后让pxCurrentTCB = &优先级最高任务TCB,我们这里手动指定 bl vTaskSwitchContext //开中断 mov r0,#0 msr basepri,r0 //从msp中恢复r3、r14 ldmia sp!,{r3,r14} //r1=*r3,此时r3=&pxCurrentTCB //即r1=pxCurrentTCB ldr r1,[r3] //r0=*r1则r0=pxCurrentTCB.pxTopOfStack 理由前面讲过 ldr r0,[r1] //这里把pxCurrentTCB保存的需要手动加载的值加载到内核的r4-r11 ldmia r0!,{r4-r11} //更新加载了r4-r11的任务栈到psp msr psp,r0 isb //返回,这里会以psp自动加载保存了xPSR、R15、R14、R12、R3-R0值到内核的xPSR、R15、R14、R12、R3-R0寄存器,所以返回的是pxCurrentTCB任务 bx r14 nop }
这里为了简单手动指定TCB
extern TCB_t Task1TCB; extern TCB_t Task2TCB; void vTaskSwitchContext(void) { if(pxCurrentTCB == &Task1TCB) { pxCurrentTCB = &Task2TCB; } else { pxCurrentTCB = &Task1TCB; } }
至此任务切换原理讲完
main.c
extern List_t pxReadyTasksLists[configMAX_PRIORITIES]; portCHAR flag1; portCHAR flag2; TaskHandle_t Task1_Handle; StackType_t Task1Stack[128]; TCB_t Task1TCB; TaskHandle_t Task2_Handle; StackType_t Task2Stack[128]; TCB_t Task2TCB; void delay(uint32_t x) { for(;x!=0;x--); } void Task1_Fntry(void *arg) { while(1) { flag1=1; delay(100); flag1=0; delay(100); taskYIELD();//手动触发切换任务 } } void Task2_Fntry(void *arg) { while(1) { flag2=1; delay(100); flag2=0; delay(100); taskYIELD();//手动触发切换任务 } } int main(void) { //初始化就绪列表 prvInitialiseTaskLists(); //创建任务1 Task1_Handle = xTaskCreateStatic(Task1_Fntry,"task1",128,NULL,Task1Stack,&Task1TCB); //将任务添加到就绪列表 vListInsertEnd(&pxReadyTasksLists[1],&((&Task1TCB)->xStateListItem)); //创建任务2 Task2_Handle = xTaskCreateStatic(Task2_Fntry,"task2",128,NULL,Task2Stack,&Task2TCB); //将任务添加到就绪列表 vListInsertEnd(&pxReadyTasksLists[2],&((&Task2TCB)->xStateListItem)); //启动调度器 vTaskStartScheduler(); for(;;); }
以上就是FreeRTOS实时操作系统的任务创建与任务切换的详细内容,更多关于FreeRTOS任务创建与切换的资料请关注我们其它相关文章!
赞 (0)