基于Cortex-M的RTOS上下文切换详解及FreeRTOS实例


    在实时操作系统中,上下文切换指的是从一个任务切换到另一个任务,与此同时需要保存任务的上下文,以确保在下一次被切换的任务重新调度时能恢复原有的任务状态。
    本文将从硬件上来探讨在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)定义的寄存器如下:

寄存器别名描述
r15PCProgram Counter(Current Instruction)
r14LRLink Register(Return Address)
r13SPStack Pointer
r12IPIntra-Procedure-call scratch register
r11v8Variable-register 8
r10v7Variable-register 7
r9v6,SB,TRVariable-register 6 or Platform Register
r8~r4v5~v1Variable-register 5 ~ Variable-register 1
r3~r0a4~a1Argument/scratch register 4 ~ Argument/scratch register 1
  • 大部分的编译器都能识别上面的别名,比如下面两个语句都可以将r0寄存器置0:mov r0, #0mov 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-SPFPv5,这里不对这两种扩展进行详细描述,但这两种扩展都为FPU操作提供相同的寄存器集。

  • 使能不同的浮点扩展,需要在编译时添加相关的浮点编译选项:GCC浮点编译选项

他们可以通过两种方式被寻址:

  1. 作为32位(单字)寄存器(s0 - s31)
  2. 作为16位(双字)寄存器(d0 - d16)
  • 其中s(x)s(x+1)组成d(x/2)寄存器,如s2s3组成了d1寄存器

当使能FP扩展时,还会用到一个特殊的寄存器FPSCR,它允许配置和控制浮点操作的选项。默认情况下,即使MCU实现了FP扩展,当设备复位时,该功能是禁用的。要启用它,必须往0xE000ED88地址处的协处理器访问控制寄存器(CPACR)的相关位进行配置。
在这里插入图片描述
其中,CP10CP11用于控制使能浮点运算。这两个字段都是2位,定义如下:

bit1bit0描述
00FPU Disabled (default). Any access generates a UsageFault.
01Privileged access only. Any unprivileged access generates a UsageFault.
10Reserved
11Privileged and unprivileged access allowed.

其中CP10CP11字段的内容必须相同:
在这里插入图片描述

1.2.3 特殊寄存器(Special Registers)

特殊寄存器的访问需要使用汇编指令MSR来写,MRS来读。这里不对特殊据存器进行深入讨论,ARMv7 Architecture Reference Manual中有对特殊寄存器的相关描述:
Special Register
有一些关于读写特殊寄存器的Privilege需要注意:
(1)MRS(手册B5.2.2)
在这里插入图片描述

  • 如果非特权模式的代码尝试读取任何堆栈指针、优先级掩码或IPSR寄存器,都将返回0。

(2)MSR(手册5.2.3)
在这里插入图片描述

  • 处理器会忽略非特权线程模式下对所有的堆栈指针的写操作,包括EPSRIPSRmasks(即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,用作v6r9)恢复到调用函数前的原始值,这些寄存器由被调函数保存,称为callee-saved register,也就是说如果被调函数中有使用到这些寄存器,则需要在函数入口处保存,在函数出口处恢复。
对于函数的调用者,即硬件上在进入异常程序之前,它会自动保存r0-r3,r12,LR(r14),PC(r15)和xPSR寄存器(按顺序从后往前压,即先压xPSR),这些寄存器称为caller-saved register

  • xPSR保存了最近使用汇编指令保存的状态,xPSRAPSRIPSREPSR组合在一个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就允许我们通过位于0xE000EF34FPCCR(Floating Point Context Control Register)寄存器配置上下文如何保存。
在这里插入图片描述
对于上下文状态的堆栈信息,我们主要介绍以下几个字段:

  • ASPEN:默认为1,此时任何浮点指令的执行都会将CONTROL寄存器中的FPCA为置1
  • LSPEN:默认为1,此时会使能Lazy Context Save。这意味着在进入异常时,堆栈将为调用者保存的浮点寄存器(s0-s15FPSCR)保留空间。但默认情况下数据实际上不会被压栈,当且仅当在异常中执行浮点指令时才会压入这些寄存器。这意味着只要在中断不使用FPU,就可以不用将68字节的浮点状态压入堆栈。

FPU使能的时候(CONTROL中的FPCA为1),扩展帧将会由硬件保存:
在这里插入图片描述

  • 如果启用了FPU但没有执行浮点指令或禁用了CONTROL寄存器中的ASPEN,则只保存基本帧。

1.4.2 异常返回

最后,为了让硬件知道退出异常时要恢复什么状态,需要将一个称为EXC_RETURN的特殊值加载到LR寄存器中。
ARMv7 Architecture Reference ManualB1.5.6 Exception entry behavior中,给出了下面的伪代码,它描述了当前正在使用的堆栈帧(扩展的/基本的),以及在异常发生之前正在使用的堆栈指针是什么。
在这里插入图片描述
当从异常返回时,EXC_RETURN的可能值如下:

EXC_RETURNReturn ToReturn Stack
0xFFFFFFF1Handler ModeMSP
0xFFFFFFF9Thread ModeMSP
0xFFFFFFFDThread ModePSP
0xFFFFFFE1Handler Mode(FPU Extended Frame)MSP
0xFFFFFFE9Thread Mode(FPU Extended Frame)MSP
0xFFFFFFEDThread Mode(FPU Extended Frame)PSP

2 FreeRTOS的上下文切换

当RTOS调度程序决定运行一个与当前运行的任务不同的任务时,它将触发上下文切换。从一个任务切换到另一个任务时,需要以某种方式保留当前任务的状态,包括任务的执行状态(例如在互斥锁上阻塞,休眠等)和硬件寄存器的值等信息。接下来,将以FreeRTOS(v10.2.0)为例,来说明在Cortex-M内核的设备中是如何进行上下文切换的。

2.1 上下文切换代码分析

FreeRTOS调度器通过利用内置的SysTickPendSV中断工作。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,其为指针,故大小为一个字
  • 进入异常后,硬件会自动将PSRPCLRr12r3~r0按顺序压栈,即保存进入PendSV异常之前的寄存器状态
  • r14LR寄存器,稍后调用的C函数vTaskSwitchContext会修改LR为它的下一条指令的地址,调用完需要恢复,后面的bx r14会用到,故需要保存

现在我们已经保存了原始任务的所有寄存器状态,现在是时候将上下文切换到一个新任务了:

stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext

可以看到最后调用C函数vTaskSwitchContext来决定下一个执行的任务,所以前面的代码就是为调用这个函数做准备。首先将r0r3这两个参数寄存器保存到正在使用的堆栈中(MSPPSP),然后设置basepri寄存器,该寄存器可以让优先级比设置值(这里是configMAX_SYSCALL_INTERRUPT_PRIORITY)低的中断被屏蔽,以保证vTaskSwitchContext 不会被其它中断打断。

  • 实际上调用vTaskSwitchContext后不止是r0r3的值会改变,r1r2等寄存器都有可能会因为vTaskSwitchContext的调用而改变。但是后面的汇编代码只用到了r0r3,故这里保存这两个寄存器。
    • 实际上保存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出参数寄存器到r0r3来恢复函数调用之前的初始值。

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,PCxPSR压入PSP中了,还需要保存r4~r11这些寄存器(再次保存r14LR是为了后续调用C函数返回使用),所以继续在PSP的基础上压栈,这样就保存了当前任务的所有寄存器的值。然后把压栈后的PSP值保存到当前任务的pxTopOfStack中。

当前任务的上下文保存完了,就可以进行上下文切换了:

stmdb sp!, {r0, r3}
bl vTaskSwitchContext

因为调用vTaskSwitchContext后我们还将用到r0r3寄存器,所以这里暂时用一下当前任务的堆栈来保存这两个寄存器。而在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

首先将前面保存的r0r3出栈,其中r3pxCurrentTCB的地址,然后将该地址里面的*pxCurrentTCB的内容(前四字节)加载到r1中,即r1 = &(*pxTopOfStack)。然后再把*pxTopOfStack,即待运行任务的堆栈指针加载到r0中,再将待运行任务的上下文r4~r11r14从栈顶指针r0出栈到系统对应的寄存器中,再将出栈后的堆栈指针pxTopOfStack(r0)保存到psp中。最后调用bx r14切换回Thread mode,退出PendSV异常。最后别忘了,由caller保存的r0-r3,r12,LR,PCxPSR也将由硬件出栈,硬件并不知道我们已经修改psp到新的任务中了,它就是从psp位置处依次将这些寄存器出栈,所以新任务堆栈中的r0-r3,r12,LR,PCxPSR将出栈到这些寄存器中,最后任务切换成功。

  • 所以前面进入异常时硬件的压栈和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中的pxPortInitialiseStackxPortStartSchedulervPortSVCHandler函数。


版权声明:本文为CSDN博主「tilblackout」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tilblackout/article/details/128135347

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tilblackout

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值