FreeRTOS第一个任务的创建和调度详解(SVC异常)
创始人
2024-03-24 18:56:51

在上一篇文章中,我详细分析了FreeRTOS中上下文切换:基于Cortex-M的RTOS上下文切换详解及FreeRTOS实例

但是第一个任务没有上下文,它是怎么运行的呢?

1 创建任务

如果我们没有创建任务的话,系统也有一个空闲任务用来调度,这里不对这个进行分析。

首先,我们知道pxCurrentTCB指向当前运行任务的TCB,所以我们先看看哪里设置了pxCurrentTCB,流程如下

xTaskCreate/* 初始化TCB内容 */prvInitialiseNewTask/* 将TCB加入ReadyList */prvAddNewTaskToReadyList

prvAddNewTaskToReadyList的大概逻辑如下:

if( pxCurrentTCB == NULL )
{pxCurrentTCB = pxNewTCB;...
}
else
{if( xSchedulerRunning == pdFALSE ){if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}...}...
}
prvAddTaskToReadyList( pxNewTCB );

也就是说如果pxCurrentTCB为空,则直接将新创建的任务赋值给pxCurrentTCB,如果不为空且还没有开始任务调度,则判断当前创建任务的优先级是否比pxCurrentTCB中任务的优先级高,若是则更改pxCurrentTCB

  • 若任务已经开始调度,就将任务加入readyList中,交给Systick调度,这不属于本文讨论的范围

2 开始调度

接着就是任务调度了,来看看上电后的第一个任务具体是怎么调度的。

vTaskStartScheduler();/* 创建空闲任务 */xReturn = xTaskCreate(prvIdleTask,...)/* 关闭中断 */portDISABLE_INTERRUPTS();/* 开始任务调度 */xPortStartScheduler();/* 该函数中主要是初始化一些常量并打开PendSV和Systick中断:略 *//* 开始第一个任务 */vPortStartFirstTask();

可以看到最后进入到vPortStartFirstTask函数中:

  • 函数实现的具体内容见注释
vPortStartFirstTask/* 初始化NVIC的VTOR寄存器,来重定位中断向量表 */ldr r0, =0xE000ED08ldr r0, [r0]ldr r0, [r0]/* 中断向量表中的第一个字为MSP的初始值 */msr msp, r0/* 清除CONTROL寄存器,其中第三位FPCA表示FP扩展,将其关闭 */mov r0, #0msr control, r0/* Call SVC to start the first task. *//* 将PRIMASK设置为0,表示关闭NMI和Hardfault异常 */cpsie i/* 将FAULTMASK设置为0,表示关闭NMI异常 */cpsie fdsbisb/* 触发SVC异常程序,其中0在异常处理函数中没用到,随便传一个立即数即可 */svc 0

SVC异常处理函数如下:

vPortSVCHandler:/* Get the location of the current TCB. */ldr	r3, =pxCurrentTCBldr r1, [r3]ldr r0, [r1]/* Pop the core registers. */ldmia r0!, {r4-r11, r14}msr psp, r0isbmov r0, #0msr	basepri, r0bx r14

上面程序的意思就是将pxCurrentTCB的第一个参数,即第一个运行任务的堆栈指针pxTopOfStack加载到r0中,然后将任务中堆栈里的r4-r11r14出栈到系统的r4-r11r14寄存器中,然后把出栈后任务的堆栈地址赋值给psp,最后再开中断(前面调用了portDISABLE_INTERRUPTS()),切换到线程模式运行任务。在该异常处理程序退出时,还将由硬件从psppopr0-r3,r12,LR,PCxPSR到系统对应的寄存器中。这样系统就从第一个任务开始运行了。

问:为什么还要将r14(LR)寄存器出栈?或者说为什么要将它保存在栈中?
在创建任务时,每个任务的LRpxPortInitialiseStack函数初始化为:portINITIAL_EXC_RETURN0xFFFFFFFD,它表示退出异常时进入线程模式并使用PSP堆栈,这是通过最后的bx r14来实现的,它的作用是让硬件知道退出异常时要恢复什么状态。

实际上进入异常时硬件也自动保存了LR,但系统中的第一个任务,也就是第一次进入SVC异常时保存的LRvPortStartFirstTask()的下一跳指令return 0的地址,很明显系统不会执行到return 0。进入异常后,LR表示异常发生之前在使用的堆栈,FreeRTOS进入SVC异常时,它的值为0xFFFFFFF9,表示退出时进入线程模式并使用MSP堆栈(没运行操作系统默认使用MSP),当运行操作系统后,系统将使用PSP(FreeRTOS设置LR0xFFFFFFFD,对应SVC异常程序的ldmia r0!, {r4-r11, r14}中出栈给r14)。

一旦开始运行一个任务之后,每次进入异常硬件保存的LR都是0xFFFFFFFD了,因为FreeRTOS的任务都是在使用PSP堆栈,进入异常前的状态都是一样的。在后续任务的上下文切换的PendSV中断中也有压入r14

xPortPendSVHandler/* 进入时LR=0xFFFFFFFD,它是被SVC异常最后的bl r14修改的 */...stmdb r0!, {r4-r11, r14}...bl vTaskSwitchContext...ldmia r0!, {r4-r11, r14}...

这里将r14压栈再出栈的原因和SVC中的出栈不同,这里是因为后面调用了函数vTaskSwitchContext,会修改LR为其下一条指令的值,所以需要保存r14的值。


最后还有一个问题没有解决:r0-r15xPSR是何时保存到第一个任务的堆栈的呢?或者说每个创建的任务的初始堆栈是怎么设置的呢?不难发现,是在pxPortInitialiseStack中设置的:

xTaskCreateprvInitialiseNewTask/* 假设没打开StackOverflow检测和MPU */pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );

现在来看看pxPortInitialiseStack具体做了什么事:

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{/* Simulate the stack frame as it would be created by a context switchinterrupt. *//* Offset added to account for the way the MCU uses the stack on entry/exitof interrupts, and to ensure alignment. */pxTopOfStack--;*pxTopOfStack = portINITIAL_XPSR;	/* xPSR */pxTopOfStack--;*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) prvTaskExitError;	/* LR *//* Save code space by skipping register initialisation. */pxTopOfStack -= 5;	/* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters;	/* R0 *//* A save method is being used that requires each task to maintain itsown exec return value. */pxTopOfStack--;*pxTopOfStack = portINITIAL_EXC_RETURN;pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4. */return pxTopOfStack;
}

首先来看看任务的堆栈需要将寄存器按什么顺序保存在堆栈中:
在这里插入图片描述

  • 创建任务时,硬件堆栈需要自己初始化

这个函数中就是一个个来初始化这些寄存器的值并写入任务堆栈中,供SVCPendSV进行调度。

  • xPSRportINITIAL_XPSR宏为0x01000000,bit24位为1表示Thumb状态,其它的状态位为0即可
  • PCpxCode就是创建任务时传入的任务函数地址,其中portSTART_ADDRESS_MASK0xFFFFFFFE,根据Cortex-M的规范,PC地址是按字/半字对齐的,所以最低位总是为0。
    • 但使用bxblx跳转时,应该将最低位置1,表示使用Thumb指令
  • LR:硬件的LR设置为prvTaskExitError函数,但任务应该在一个死循环中不该返回,进入这个函数说明程序出错
  • r12~r4:没用到,写为任意值都行,保持默认值即可
  • r0pvParameters即为创建任务时传入的参数,这里可以在任务执行时传给任务
  • r14:前面有提到,设置为portINITIAL_EXC_RETURN(0xFFFFFFFD),表示退出异常时,进入线程模式并使用PSP堆栈

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...