文章目录
在实时操作系统中,上下文切换指的是从一个任务切换到另一个任务,与此同时需要保存任务的上下文,以确保在下一次被切换的任务重新调度时能恢复原有的任务状态。
本文将从硬件上来探讨在ARM Cortex-M的MCU上的上下文切换是如何进行的,然后分析FreeRTOS中是如何实现上下文切换的。
1 Cortex-M MCU特性
在了解上下文切换之前,肯定需要先了解MCU的架构才能知道如何实现上下文切换的功能,这里以Cortex-M为例进行分析。
1.1 操作模式
当MCU运行于异常处理回调函数,如ISR中时,MCU处于Handler Mode
;其它时候MCU都处于Thread Mode
。
系统内核可以在特权/非特权模式下运行,有一些特定的指令和操作只能在特权模式下运行。比如在非特权模式下,用户无法访问NVIC寄存器,而在Handler Mode
下,内核总是处于特权模式;而在Thread Mode
下,程序可以在特权/非特权模式下切换。只有当程序处于Handler Mode
时,才能将Thread Mode
中的非特权模式切换为特权模式。
1.2 寄存器
1.2.1 核心寄存器
AAPCS
(ARM Architecture Procedure Calling Standard
)定义的寄存器如下:
寄存器 | 别名 | 描述 |
---|---|---|
r15 | PC | Program Counter(Current Instruction) |
r14 | LR | Link Register(Return Address) |
r13 | SP | Stack Pointer |
r12 | IP | Intra-Procedure-call scratch register |
r11 | v8 | Variable-register 8 |
r10 | v7 | Variable-register 7 |
r9 | v6,SB,TR | Variable-register 6 or Platform Register |
r8~r4 | v5~v1 | Variable-register 5 ~ Variable-register 1 |
r3~r0 | a4~a1 | Argument/scratch register 4 ~ Argument/scratch register 1 |
- 大部分的编译器都能识别上面的别名,比如下面两个语句都可以将r0寄存器置0:
mov r0, #0
和mov a1, #0
- 在
AAPCS specification
中提到对于有固定作用的寄存器需要使用大小描述,比如程序计数器是PC
而不是pc
(1) r12
(Intra-Procedure-call Scratch Register)
该寄存器有32位,对于汇编中的跳转指令bl
,它并不能跳转到整个程序的地址空间,因为指令中的前几位是对指令进行编码的,所以无法跳转到整个32位的地址。所以如果要跳转到一个在地址空间中很大的地址,就需要传递一个由编译器产生的shim
函数,r12
是唯一的一个可以用在这个传递过程中的寄存器且计算机不需要保存原来的状态。
(2)r9
(Platform Register)
在绝大多数应用中,r9
也同样在一个函数运行的过程中作为一个Variable-register
,但是在下面两种情况下,r9
寄存器需要在函数调用的过程中被保存。
①r9
用作thread register
:该寄存器保存当前线程局部存储的上下文指针。
②r9
用作static base
:通常情况下,当编译代码时,代码依赖于它运行的位置。但是,对于某些应用程序,我们可能希望能够从任意位置运行代码。比如,我们想把一个函数从flash加载到RAM中执行。这就需要编写位置无关代码,在这个过程中就需要查找全局和静态数据的地址,这些地址都存储在一个全局偏移表中(Global Offset Table
),而这个表的基地址就保存在r9
寄存器中。
- 使用这个特性需要在编译过程中添加编译选项
-fpic
和-msingle-pic-base
1.2.2 浮点寄存器(Floating Point registers)
在Cortex M4、Cortex M7和Cortex M33中支持添加一个可选的浮点扩展单元来支持MCU的浮点运算。Cortex-M设备可以实现两种浮点扩展FPv4-SP
和FPv5
,这里不对这两种扩展进行详细描述,但这两种扩展都为FPU操作提供相同的寄存器集。
- 使能不同的浮点扩展,需要在编译时添加相关的浮点编译选项:GCC浮点编译选项
他们可以通过两种方式被寻址:
- 作为32位(单字)寄存器(s0 - s31)
- 作为16位(双字)寄存器(d0 - d16)
- 其中
s(x)
和s(x+1)
组成d(x/2)
寄存器,如s2
和s3
组成了d1
寄存器
当使能FP扩展时,还会用到一个特殊的寄存器FPSCR
,它允许配置和控制浮点操作的选项。默认情况下,即使MCU实现了FP扩展,当设备复位时,该功能是禁用的。要启用它,必须往0xE000ED88
地址处的协处理器访问控制寄存器(CPACR
)的相关位进行配置。
其中,CP10
和CP11
用于控制使能浮点运算。这两个字段都是2位,定义如下:
bit1 | bit0 | 描述 |
---|---|---|
0 | 0 | FPU Disabled (default). Any access generates a UsageFault. |
0 | 1 | Privileged access only. Any unprivileged access generates a UsageFault. |
1 | 0 | Reserved |
1 | 1 | Privileged and unprivileged access allowed. |
其中CP10
和CP11
字段的内容必须相同:
1.2.3 特殊寄存器(Special Registers)
特殊寄存器的访问需要使用汇编指令MSR
来写,MRS
来读。这里不对特殊据存器进行深入讨论,ARMv7 Architecture Reference Manual
中有对特殊寄存器的相关描述:
有一些关于读写特殊寄存器的Privilege
需要注意:
(1)MRS(手册B5.2.2)
- 如果非特权模式的代码尝试读取任何堆栈指针、优先级掩码或IPSR寄存器,都将返回0。
(2)MSR(手册5.2.3)
- 处理器会忽略非特权线程模式下对所有的堆栈指针的写操作,包括
EPSR
、IPSR
、masks
(即CONTROL
)寄存器。如果在特权线程模式下向CONTROL
寄存器的nPRIV
位写1,处理器会切换到非特权线程模式执行,并忽略之后对Special Register
的写入操作。
对于上下文切换,最重要的特殊寄存器之一就是CONTROL
寄存器。在ARMv8-M
架构中又对此寄存器的功能进行了增加,以ARMv8-M
架构中的CONTROL
寄存器进行说明:
SFPA
:浮点安全选项,需要打开ARMv8-M Security Extension
选项FPCA
:指示浮点上下文是否处于激活状态SPSEL
:控制正在使用的堆栈指针nPriv
:控制线程模式是作为特权还是非特权操作。1为非特权模式,0为特权模式
1.3 上下文堆栈指针
Cortex-M架构实现了两个栈,分别是Main Stack
(在MSP
寄存器中查看)和Process Stack
(在PSP
寄存器中查看)。每次系统复位后,默认是采用MSP
,它的初始值通过中断向量表中的第一个word
来设置。访问r13
(SP
)寄存器将返回当前使能的堆栈指针。
在Handler Mode
下,总是使用MSP
;在Thread Mode
下,堆栈指针可以为上述二种之一:
- 将
CONTROL
寄存器中的SPSEL
位置1,上下文堆栈将从MSP
切换为PSP
- 如果发生
exception
错误,相关返回值EXC_RETURN
将保存在LR
寄存器中
1.4 上下文状态堆栈
根据AAPCS
,在被调用函数返回之前,需要将特定的寄存器集(r4-r8
, r10
,r11
,SP
,用作v6
的r9
)恢复到调用函数前的原始值,这些寄存器由被调函数保存,称为callee-saved register
,也就是说如果被调函数中有使用到这些寄存器,则需要在函数入口处保存,在函数出口处恢复。
对于函数的调用者,即硬件上在进入异常程序之前,它会自动保存r0-r3
,r12
,LR
(r14
),PC
(r15
)和xPSR
寄存器(按顺序从后往前压,即先压xPSR
),这些寄存器称为caller-saved register
。
xPSR
保存了最近使用汇编指令保存的状态,xPSR
将APSR
、IPSR
和EPSR
组合在一个32位寄存器中- 除此之外,如果还用到了
FPU
,则caller
将保存s0-s15
,而callee
需要保存s16-s31
AAPCS
还强制要求:堆栈必须双字对齐。对于“公共接口”,也就是说,当调用一个函数时,它的堆栈指针总是8字节对齐的。考虑到所有这些因素,又为了保证中断中执行的程序ABI
(Application Binary Interface
)兼容,ARM架构需要对齐堆栈并保存调用函数负责保存的寄存器状态。
- 如果系统在使用
PSP
的线程模式下发生异常,数据将被压入PSP
上;如果系统已经在处理另一个异常服务,并且被一个优先级更高的异常抢占,则数据将被压入MSP
。
ARMv7 Architecture Reference Manual
中描述了在异常处理的入口处自动保存上下文状态后堆栈的变化:
可以看到,如果原来的SP
是四字节对齐的,将会保留一个字以满足字节对齐。
- 在异常处理入口处,ARM硬件使用堆栈的
xPSR
的第9位来指示是否添加了4个字节的填充以使堆栈对齐到8字节边界上。
1.4.1 浮点扩展和上下文状态堆栈
当浮点(FP
)扩展使能时,AAPCS
规定子例程调用必须保存s16-s31
,而不需要保存s0-s15
。此外,与PSR
寄存器类似,FPU
的状态需要被保存,因此FPSCR
也需要被存储。这意味着需要在进入异常时多压入17个寄存器(68字节)。对于某些工程来说,这可能会影响性能或内存问题,所以ARM就允许我们通过位于0xE000EF34
的FPCCR
(Floating Point Context Control Register
)寄存器配置上下文如何保存。
对于上下文状态的堆栈信息,我们主要介绍以下几个字段:
ASPEN
:默认为1,此时任何浮点指令的执行都会将CONTROL
寄存器中的FPCA
为置1LSPEN
:默认为1,此时会使能Lazy Context Save。这意味着在进入异常时,堆栈将为调用者保存的浮点寄存器(s0-s15
和FPSCR
)保留空间。但默认情况下数据实际上不会被压栈,当且仅当在异常中执行浮点指令时才会压入这些寄存器。这意味着只要在中断不使用FPU
,就可以不用将68字节的浮点状态压入堆栈。
当FPU
使能的时候(CONTROL
中的FPCA
为1),扩展帧将会由硬件保存:
- 如果启用了
FPU
但没有执行浮点指令或禁用了CONTROL
寄存器中的ASPEN
,则只保存基本帧。
1.4.2 异常返回
最后,为了让硬件知道退出异常时要恢复什么状态,需要将一个称为EXC_RETURN
的特殊值加载到LR
寄存器中。
在ARMv7 Architecture Reference Manual
的B1.5.6 Exception entry behavior
中,给出了下面的伪代码,它描述了当前正在使用的堆栈帧(扩展的/基本的),以及在异常发生之前正在使用的堆栈指针是什么。
当从异常返回时,EXC_RETURN
的可能值如下:
EXC_RETURN | Return To | Return Stack |
---|---|---|
0xFFFFFFF1 | Handler Mode | MSP |
0xFFFFFFF9 | Thread Mode | MSP |
0xFFFFFFFD | Thread Mode | PSP |
0xFFFFFFE1 | Handler Mode(FPU Extended Frame) | MSP |
0xFFFFFFE9 | Thread Mode(FPU Extended Frame) | MSP |
0xFFFFFFED | Thread Mode(FPU Extended Frame) | PSP |
2 FreeRTOS的上下文切换
当RTOS调度程序决定运行一个与当前运行的任务不同的任务时,它将触发上下文切换。从一个任务切换到另一个任务时,需要以某种方式保留当前任务的状态,包括任务的执行状态(例如在互斥锁上阻塞,休眠等)和硬件寄存器的值等信息。接下来,将以FreeRTOS(v10.2.0)为例,来说明在Cortex-M内核的设备中是如何进行上下文切换的。
2.1 上下文切换代码分析
FreeRTOS调度器通过利用内置的SysTick
和PendSV
中断工作。SysTick
被配置为定期触发,每次触发时,(xTaskIncrementTick
函数)都会检查是否需要上下文切换。
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked. There is therefore no need to
save and then restore the interrupt mask value as its value is already
known. */
portDISABLE_INTERRUPTS();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
除此之外,上下文切换也会在任务放弃占用CPU时发生(portYIELD
)。这两种上下文切换的方式最终都会触发PendSV
异常。
现在来看一下PendSV
异常处理程序的汇编代码:
// portasm.S
xPortPendSVHandler:
mrs r0, psp
isb
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r2, [r3]
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
/* Save the new top of stack into the first member of the TCB. */
str r0, [r2]
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3}
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [r3]
ldr r0, [r1]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
/* Is the task using the FPU context? If so, pop the high vfp registers
too. */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0
isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
#endif
#endif
bx r14
现在来分析一下上面的代码:
mrs r0, psp
isb
首先将psp
(该栈在之前的异常入口用到)保存到r0
中,然后紧跟着一个isb
(Instruction Synchronization Barrier
),这是为了刷新指令流水线以保证后面的指令被重新获取。
ldr r3, pxCurrentTCBConst
ldr r2, [r3]
然后将task.c
中定义的pxCurrentTCB
变量赋值给r3
,再将pxCurrentTCB
的值加载到r2
中。实际上pxCurrentTCB
为一个TCB_t *
的指针,它保存了当前正在运行的任务的TCB的地址,所以将这个TCB结构体的地址赋值给r2
。
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
这组指令检查FPU是否激活。之前有提到,在进入异常时,LR寄存器会保存EXC_RETURN
,从EXC_RETURN
的值来看,第5位为0时表示FPU使能。下面对于上面的指令进行分析:
①tst
(test):在寄存器和立即数之间进行与操作,与操作的结果会保存在PSR
寄存器中,如果结果为0,该寄存器的零标志位将会置位
②it
(if then):根据EPSR
中的IT
标志位有条件地执行进一步的指令,It eq
就是判断最后一次比较的结果是零
③vstmdbeq r0!, {s16-s31}
:将被调用者的浮点寄存器保存到r0
(前面赋值为psp
)中
在我的例子中,没有使能FPU,故结果不是零,所以这条指令应该被跳过。
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
/* Save the new top of stack into the first member of the TCB. */
str r0, [r2]
接着使用stmdb
(Store Multiple Decrement Before stores multiple registers
)指令将所有callee
可能用到的寄存器压入psp
,感叹号表示r0
等于最终压栈之后的栈顶地址,stmdb
的压栈顺序为后面的参数先压,即先压r14
,再压r11
…最后将r0
的值写入pxCurrentTCB
指针的第一个字中(栈顶地址)。
pxCurrentTCB
的类型为tskTaskControlBlock
,其第一个字段为volatile StackType_t *pxTopOfStack
,其为指针,故大小为一个字- 进入异常后,硬件会自动将
PSR
、PC
、LR
、r12
、r3~r0
按顺序压栈,即保存进入PendSV
异常之前的寄存器状态 r14
即LR
寄存器,稍后调用的C函数vTaskSwitchContext
会修改LR为它的下一条指令的地址,调用完需要恢复,后面的bx r14
会用到,故需要保存
现在我们已经保存了原始任务的所有寄存器状态,现在是时候将上下文切换到一个新任务了:
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
可以看到最后调用C函数vTaskSwitchContext
来决定下一个执行的任务,所以前面的代码就是为调用这个函数做准备。首先将r0
和r3
这两个参数寄存器保存到正在使用的堆栈中(MSP
或PSP
),然后设置basepri
寄存器,该寄存器可以让优先级比设置值(这里是configMAX_SYSCALL_INTERRUPT_PRIORITY
)低的中断被屏蔽,以保证vTaskSwitchContext
不会被其它中断打断。
- 实际上调用
vTaskSwitchContext
后不止是r0
和r3
的值会改变,r1
和r2
等寄存器都有可能会因为vTaskSwitchContext
的调用而改变。但是后面的汇编代码只用到了r0
和r3
,故这里保存这两个寄存器。- 实际上保存
r3
是因为它的值为pxCurrentTCB
的地址,后面要用到。而对于r0
来说,指向的是前一个任务的PSP
,在后面的代码中也没有用到。所以这里实际上不压r0
(后面的代码也不用出栈r0
)也不会有错误。
- 实际上保存
- 如果没有屏蔽其它中断,且在中断中有调用FreeRTOS的
*_FromISR()
函数,会修改原有的上下文数据结构,程序就可能会崩溃。
当修改basepri
寄存器降低有效执行级别时,需要执行isb
指令以让新的优先级对未来的指令可见,否则还是有一定概率被嵌套。dsb
指令在这并不是显式必须的。
dsb
:等待存储器访问操作执行完毕isb
:刷新流水线的指令,保证都执行完毕
最后,进入到C函数vTaskSwitchContext
中,当这个函数返回时,pxCurrentTCB
将指向切换后的新任务。
/*
* THIS FUNCTION MUST NOT BE USED FROM APPLICATION CODE. IT IS ONLY
* INTENDED FOR USE WHEN IMPLEMENTING A PORT OF THE SCHEDULER AND IS
* AN INTERFACE WHICH IS FOR THE EXCLUSIVE USE OF THE SCHEDULER.
*
* Sets the pointer to the current TCB to the TCB of the highest priority task
* that is ready to run.
*/
portDONT_DISCARD void vTaskSwitchContext( void ) PRIVILEGED_FUNCTION;
从vTaskSwitchContext
返回时,将basepri
寄存器重置为0来恢复所有中断,并从堆栈中pop出参数寄存器到r0
和r3
来恢复函数调用之前的初始值。
mov r0,#0
msr basepri, r0
ldmia sp!, {r0, r3}
msr
不需要同步指令,因为当执行优先级增加时,ARM内核实际上会为你处理这个问题
现在,是时候执行新的任务了,之前我们保存了原来人物的堆栈到pxTopOfStack
中,现在需要切换为新任务的状态。我们首先需要加载pxCurrentTCB
指向的新任务的TCB
的内容。
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [r3]
ldr r0, [r1]
r0
现在保存了一个新任务堆栈顶部的指针。首先,我们使用ldmia
(Load Multiple Increment After
)指令将callee
保存的核心寄存器出栈,然后我们要检查LR
(r14
)寄存器的内容来判断是否之前有将FPU的寄存器压栈,若有则要出栈。现在r0
指向程序堆栈的位置,此时的堆栈和我们进入异常入口处的时候一样。
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
/* Is the task using the FPU context? If so, pop the high vfp registers
too. */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
最后,我们需要更改psp
堆栈的位置为当前任务的堆栈指针的位置,并用我们刚刚用ldmia
指令从新任务的任务栈中恢复的EXC_RETURN
值填充到LR
(r14
)寄存器。前面1.4.2 异常返回
中有介绍,这将告诉硬件如何返回线程模式并恢复上下文状态:
msr psp, r0
isb
bx r14
bx r14
后,即整个函数执行完毕,硬件自动将寄存器r0-r3
,r12
,LR
(r14
),PC
(r15
)和xPSR
出栈,此时优先级最高任务的PC
会出栈到系统的PC
中并开始执行。
2.2 上下文切换总结
最后从FreeRTOS的角度来总结一下整个FreeRTOS上下文切换的过程,FreeRTOS的TCB_t
中有几个关于堆栈的变量:
pxStack
:任务栈的基地址pxEndOfStack
:任务栈的结束地址pxTopOfStack
:任务栈指针的位置
在Cortex-M中,堆栈是向下生长的,所以栈是从pxEndOfStack
往下存数据的。而对于pxEndOfStack
来说,在任务运行的过程中它并不会修改,理论上它是和SP
同步的,但是不可能系统每次操作这个堆栈就更新到pxTopOfStack
中,所以仅仅是在每次任务切换的时候更新这个值。所以再来回顾一下前面的汇编代码(删除部分):
mrs r0, psp
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11, r14}
str r0, [r2]
首先将在Thread mode
下使用的PSP
加载进来,即当前任务使用的堆栈指针所在的位置。进入PendSV
异常处理程序时,硬件已经把r0-r3
,r12
,LR
,PC
和xPSR
压入PSP
中了,还需要保存r4~r11
这些寄存器(再次保存r14
即LR
是为了后续调用C函数返回使用),所以继续在PSP
的基础上压栈,这样就保存了当前任务的所有寄存器的值。然后把压栈后的PSP
值保存到当前任务的pxTopOfStack
中。
当前任务的上下文保存完了,就可以进行上下文切换了:
stmdb sp!, {r0, r3}
bl vTaskSwitchContext
因为调用vTaskSwitchContext
后我们还将用到r0
和r3
寄存器,所以这里暂时用一下当前任务的堆栈来保存这两个寄存器。而在vTaskSwitchContext
函数中,如果去掉那些统计和溢出检测的代码,实际上就两行:
taskSELECT_HIGHEST_PRIORITY_TASK();
/* 该函数实际上用于Segger JLink调试数据的发送(如果使能的话),可忽略 */
traceTASK_SWITCHED_IN();
其中taskSELECT_HIGHEST_PRIORITY_TASK
就是在pxReadyTasksLists
中从任务优先级最高往最低寻找就绪的任务的TCB,然后赋值给pxCurrentTCB
。
pxReadyTasksLists
中任务加入的时机:Systick
中断中判断vTaskDelay
超时的任务、信号量/事件位等释放完后正在等待这些信号量/事件的任务等
好了现在系统将要运行的下一个任务已经保存在pxCurrentTCB
中了,再来看看后面的汇编:
ldmia sp!, {r0, r3}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
bx r14
首先将前面保存的r0
和r3
出栈,其中r3
即pxCurrentTCB
的地址,然后将该地址里面的*pxCurrentTCB
的内容(前四字节)加载到r1
中,即r1 = &(*pxTopOfStack)
。然后再把*pxTopOfStack
,即待运行任务的堆栈指针加载到r0
中,再将待运行任务的上下文r4~r11
和r14
从栈顶指针r0
出栈到系统对应的寄存器中,再将出栈后的堆栈指针pxTopOfStack
(r0
)保存到psp
中。最后调用bx r14
切换回Thread mode
,退出PendSV
异常。最后别忘了,由caller
保存的r0-r3
,r12
,LR
,PC
和xPSR
也将由硬件出栈,硬件并不知道我们已经修改psp
到新的任务中了,它就是从psp
位置处依次将这些寄存器出栈,所以新任务堆栈中的r0-r3
,r12
,LR
,PC
和xPSR
将出栈到这些寄存器中,最后任务切换成功。
- 所以前面进入异常时硬件的压栈和
stmdb r0!, {r4-r11, r14}
压的是原任务的上下文;而ldmia r0!, {r4-r11, r14}
的出栈和退出异常时出的是新任务的上下文
2.3 任务调度
现在还有一个问题,如果PendSV
异常被触发,但系统刚刚启动,没有当前运行的任务,会发生什么?
有几种不同的策略,但在创建新任务时,RTOS将遵循的一个常见的模式是初始化任务堆栈,使其看起来像已被调度器上下文切换过了一样。然后通过使用SVC
指令触发SVC
异常来启动调度程序本身。这种启动线程的方式与将上下文切换到线程几乎相同。
SVC
异常仅在系统刚上电后触发一次,用于运行第一个任务
第一个任务创建的具体流程和分析,参考:FreeRTOS第一个任务的创建和调度详解(SVC异常)
查看初始化代码,还会发现一些配置的设置,例如:
- 配置任务是在特权还是非特权模式下运行
FP
扩展配置,如是否使能、使用什么上下文堆栈方式
对于本例中,FPU
的配置如下:
vPortEnableVFP:
/* The FPU enable bits are in the CPACR. */
ldr.w r0, =0xE000ED88
ldr r1, [r0]
/* Enable CP10 and CP11 coprocessors, then save back. */
orr r1, r1, #( 0xf << 20 )
str r1, [r0]
bx r14
BaseType_t xPortStartScheduler( void )
{
...
/* Ensure the VFP is enabled - it should be anyway. */
vPortEnableVFP();
/* Lazy save always. */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
...
}
开启任务调度需要调用vTaskStartScheduler
,如果想要深入了解任务调度的逻辑,建议详细理解port.c
中的pxPortInitialiseStack
、xPortStartScheduler
和vPortSVCHandler
函数。
版权声明:本文为CSDN博主「tilblackout」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tilblackout/article/details/128135347