Cortext-M7

基本原理

对于Cortext-M7系列的芯片, FreeRTOS底层使用了Cortext-M7如下三个异常和中断来实现底层任务调度和时钟滴答实现

  • SVCall
    A Supervisor Call (SVC) is an exception that is triggered by the SVC instruction. In an OS environment, applications can use SVC instructions to access OS kernel functions and device drivers.

  • PendSV
    PendSV is an interrupt-driven request for system-level service. In an OS environment, use PendSV for context switching when no other exception is active.

  • SysTick
    A SysTick exception is an exception the system timer generates when it reaches zero. Software can also generate a SysTick exception. In an OS environment, the processor can use this exception as system tick.

其中SVCall可以通过SVC指令实现触发异常的操作。

在这里插入图片描述

PendSV和Sys Tick则是通过ICSR寄存器的bit[28:25]实现异常的触发和清除。

在这里插入图片描述

Bits Name Type Function
[28] PENDSVSET RW PendSV set-pending bit.
Write:
0: No effect.
1: Changes PendSV exception state to pending.
Read:
0: PendSV exception is not pending.
1: PendSV exception is pending.
Writing 1 to this bit is the only way to set the PendSV exception state to pending.
[27] PENDSVCLR WO PendSV clear-pending bit.
Write:
0: No effect.
1: Removes the pending state from the PendSV exception.
[26] PENDSTSET RW SysTick exception set-pending bit.
Write:
0: No effect.
1: Changes SysTick exception state to pending.
Read:
0: SysTick exception is not pending.
1: SysTick exception is pending.
[25] PENDSTCLR WO SysTick exception clear-pending bit.
Write:
0: No effect.
1: Removes the pending state from the SysTick exception.
This bit is WO. On a register read its value is Unknown.

代码实现

vPortSVCHandler
void vPortSVCHandler( void )
{
    __asm volatile (
        "   ldr r3, pxCurrentTCBConst2          \n"
        "   ldr r1, [r3]                        \n"
        "   ldr r0, [r1]                        \n"
        "   ldmia r0!, {r4-r11, r14}            \n"
        "   msr psp, r0                         \n"
        "   isb                                 \n"
        "   mov r0, #0                          \n"
        "   msr basepri, r0                     \n"
        "   bx r14                              \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB \n"
        );
}

svc指令执行之后,会触发SVCall异常, 此接口就是这个异常的处理函数。这段代码的功能是恢复上下文,在FreeRTOS中主要用于第一次任务的调度,在prvPortStartFirstTask中通过svc指令触发, 主要指令的作用如下:

  • ldr r3, pxCurrentTCBConst2:加载pxCurrentTCB的值到r3寄存器中,为什么指令里面的pxCurrentTCBConst2会指向pxCurrentTCB,这是因为后面的pxCurrentTCBConst2: .word pxCurrentTCB定义了一个指向pxCurrentTCB变量的指针常量,前面的.align 4确保后续数据按 4 字节对齐。
  • ldr r1, [r3]:从 r3 指向的地址加载pxCurrentTCB的值到 r1(r1 现在指向当前任务控制块)。
  • ldr r0, [r1]:从当前任务控制块中获取栈指针(任务栈顶地址,参见后面的struct tskTaskControlBlock结构体,可以看到结构体中的第一个元素就是pxTopOfStack)。
  • ldmia r0!, {r4-r11, r14}:从栈中恢复寄存器 r4 到 r11 以及 lr (r14) 的值,同时自动更新栈指针 r0
  • msr psp, r0:将更新后的栈指针保存到进程栈指针 (PSP)
  • isb:同步指令流,确保前面的指令执行完毕
  • mov r0, #0msr basepri, r0:清除中断屏蔽,允许所有中断
  • bx r14:跳回异常发生前的位置,恢复任务执行
struct tskTaskControlBlock 数据结构
/*
 * Task control block.  A task control block (TCB) is allocated for each task,
 * and stores task state information, including a pointer to the task's context
 * (the task's run time environment, including register values)
 */
typedef struct tskTaskControlBlock
{
    volatile StackType_t * pxTopOfStack; 
	......
    ListItem_t xStateListItem;
    ListItem_t xEventListItem;
    UBaseType_t uxPriority;
    StackType_t * pxStack;
	......
} tskTCB;
LDMIA命令

这条命令的作用是从指定的Rn寄存器指向的内存中加载多个值到寄存器列表中(reglist),IA表明每次加载内存中的值到一个寄存器中后,下一个要加载的内存地址递增。

下面是LDM和STM系列指令的描述。

在这里插入图片描述

vPortSVCHandler中等价于POP指令从栈上恢复现场

在这里插入图片描述

BASEPRI Register

这个寄存器的作用是屏蔽优先级低于或等于此寄存器设置的优先级的异常。异常的优先级的值越大代表优先级越低。

在这里插入图片描述

xPortPendSVHandler
void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
		/*Section1:保存当前任务上下文*/
        "   mrs r0, psp                         \n" /*将进程栈指针(PSP)的值加载到r0*/
        "   isb                                 \n" /*指令同步屏障,确保指令执行顺序*/
        "                                       \n"
        "   ldr r3, pxCurrentTCBConst           \n" /*获取当前任务控制块(TCB)的地址*/
        "   ldr r2, [r3]                        \n" /*r2指向当前TCB*/
        "                                       \n"
        "   tst r14, #0x10                      \n" /*检查EXC_RETURN的bit4,判断是否使用FPU*/
        "   it eq                               \n" /*如果使用FPU(结果为0),执行下一条指令*/
        "   vstmdbeq r0!, {s16-s31}             \n" /*保存FPU的s16-s31寄存器到栈*/
        "                                       \n"
        "   stmdb r0!, {r4-r11, r14}            \n" /*保存核心寄存器r4-r11和lr(r14)到栈*/
        "   str r0, [r2]                        \n" /*将更新后的栈指针保存到TCB的第一个成员*/
        "                                       \n"
		/*Section2:切换到新任务*/
        "   stmdb sp!, {r0, r3}                 \n" /*保存r0和r3到主栈(MSP)*/
        "   mov r0, %0                          \n" /*将configMAX_SYSCALL_INTERRUPT_PRIORITY加载到r0*/
        "   cpsid i                             \n" /*关闭中断(针对Cortex-M7的 Errata 837070补丁)*/
        "   msr basepri, r0                     \n" /*设置BASEPRI寄存器,屏蔽低于特定优先级的中断*/
        "   dsb                                 \n" /*数据同步屏障*/
        "   isb                                 \n" /*指令同步屏障*/
        "   cpsie i                             \n" /*重新开启中断,因为在前面关闭了中断(Errata 837070补丁)*/
        "   bl vTaskSwitchContext               \n" /*调用函数选择下一个要运行的任务*/
        "   mov r0, #0                          \n" /*清除BASEPRI的值*/
        "   msr basepri, r0                     \n" /*允许所有中断*/
        "   ldmia sp!, {r0, r3}                 \n" /*恢复r0和r3的值*/
        "                                       \n"
		/*Section3:恢复新任务上下文*/
        "   ldr r1, [r3]                        \n" /*获取新任务的TCB*/
        "   ldr r0, [r1]                        \n" /*获取新任务的栈指针*/
        "                                       \n"
        "   ldmia r0!, {r4-r11, r14}            \n" /*从栈中恢复r4-r11和lr寄存器*/
        "                                       \n"
        "   tst r14, #0x10                      \n" /*检查是否需要恢复FPU寄存器*/
        "   it eq                               \n" /**如果使用FPU(结果为0),执行下一条指令*/
        "   vldmiaeq r0!, {s16-s31}             \n" /*恢复FPU的s16-s31寄存器*/
        "                                       \n"
        "   msr psp, r0                         \n" /*更新进程栈指针(PSP)*/
        "   isb                                 \n" /*指令同步屏障*/
        "                                       \n"
		/*Section4:异常返回*/
        "                                       \n"
        "   bx r14                              \n" /*返回到新任务执行*/
        "                                       \n"
        "   .align 4                            \n" /*4字节对齐*/
        "pxCurrentTCBConst: .word pxCurrentTCB  \n" /*定义指向当前TCB的常量*/
 		/*GCC扩展写法,将configMAX_SYSCALL_INTERRUPT_PRIORITY传递到此函数中,用于配置BASEPRI*/
        ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

xPortPendSVHandler是PendSV异常处理函数,主要功能是执行任务切换操作。此接口的实现主要分为4个部分(保存当前任务上下文、切换到新的任务、恢复新任务的上下文、异常返回),每条指令的作用已在上面的注释中说明。

保存当前任务上下文

这部分实现的功能简洁地描述为,从PSP寄存器中拿到当前运行的任务的栈指针,然后把FPU(如果使用FPU,保存s16-s31寄存器)的关键寄存器和核心寄存器(r4-r11和r14)保存到当前运行的任务的栈上,并把当前任务的栈指针保存在当前任务的struct tskTaskControlBlock数据结构的第一个成员pxTopOfStack中。

切换到新的任务

首先执行stmdb sp!, {r0, r3}保存r0和r3到主栈MSP中, 这是因为r0指向了当前任务的栈指针, r3指向了当前任务的struct tskTaskControlBlock数据结构,在后续的代码逻辑中,这两个值是会被覆盖的,所以需要保存,并在这部分代码的最后从MSP中恢复r0和r3的值。

其次 mov r0, %0这条指令中的%0占位符就是xPortPendSVHandler接口的最后的 ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )中的configMAX_SYSCALL_INTERRUPT_PRIORITY这个立即数,这是GCC的扩展写法。这个配置主要是用于配置BASEPRI寄存器,屏蔽所有异常优先级的数值大于或者等于configMAX_SYSCALL_INTERRUPT_PRIORITY的异常(数值越大,实际优先级越低)。

最后,会调用vTaskSwitchContext选择下一个要运行的任务,在此之前会设置BASEPRI寄存器,屏蔽优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断。在vTaskSwitchContext中会切换到合适的任务,并修改pxCurrentTCB指向新的需要运行的任务。vTaskSwitchContext函数返回之后,会允许所有的中断,然后从MSP中恢复r0和r3寄存器的值。

恢复新任务的上下文

这部分的功能实现和vPortSVCHandler接口实现的功能相似。

从r3指向的pxCurrentTCB中获取新的任务的struct tskTaskControlBlock数据结构,并从这个数据结构的一个成员pxTopOfStack中拿到这个新任务的栈指针,然后从栈上恢复FPU(如果使用FPU,恢复s16-s31寄存器)的关键寄存器和核心寄存器(r4-r11和r14)。最后更新PSP进程指针为新的任务的栈指针。

异常返回

这一步简单,执行bx r14指令,返回到新的任务中。

xPortSysTickHandler
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();
    traceISR_ENTER();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            traceISR_EXIT_TO_SCHEDULER();

            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
        else
        {
            traceISR_EXIT();
        }
    }
    portENABLE_INTERRUPTS();
}

此接口会调用xTaskIncrementTick函数(暂不深入了解此函数的实现),去增加RTOS的tick, 然后通过portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT触发PENDSV异常,从而在xPortPendSVHandler中完成任务的切换。

此接口的入口和出口的位置会调用portDISABLE_INTERRUPTS()portENABLE_INTERRUPTS()关闭和开启中断。

vPortEnableVFP
static void vPortEnableVFP( void )
{
    __asm volatile
    (
        "   ldr.w r0, =0xE000ED88       \n" /* The FPU enable bits are in the CPACR. */
        "   ldr r1, [r0]                \n"
        "                               \n"
        "   orr r1, r1, #( 0xf << 20 )  \n" /* Enable CP10 and CP11 coprocessors, then save back. */
        "   str r1, [r0]                \n"
        "   bx r14                      \n"
        "   .ltorg                      \n"
    );
}

此接口的主要功能是启用 Cortex-M 处理器中的浮点运算单元(FPU) ,允许任务使用浮点指令进行运算。在默认情况下,FPU 可能处于禁用状态,如果需要启用的话,可以调用此接口实现目的。

CPACR Register(0xE000ED88)

这个寄存器是Cortex-M7 floating-point system registers中的一个用于控制协处理器的访问权限的寄存器。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

prvPortStartFirstTask
static void prvPortStartFirstTask( void )
{
    /* Start the first task.  This also clears the bit that indicates the FPU is
     * in use in case the FPU was used before the scheduler was started - which
     * would otherwise result in the unnecessary leaving of space in the SVC stack
     * for lazy saving of FPU registers. */
    __asm volatile (
        " ldr r0, =0xE000ED08   \n" /*NVIC向量表偏移寄存器地址*/
        " ldr r0, [r0]          \n" /*读取向量表偏移寄存器的值(向量表起始地址*/
        " ldr r0, [r0]          \n" /*从向量表首地址读取初始MSP值(向量表第0项是栈顶)*/
        " msr msp, r0           \n" /*将读取到的初始栈值写入MSP寄存器*/
        " mov r0, #0            \n"
        " msr control, r0       \n" /*清除控制寄存器,切换到线程模式并使用MSP*/
        " cpsie i               \n" /*启用IRQ中断*/
        " cpsie f               \n" /*启用FIQ中断*/
        " dsb                   \n" /*数据同步屏障,确保中断启用生效*/
        " isb                   \n" /*指令同步屏障,刷新流水线*/
        " svc 0                 \n" /*执行SVC指令,触发系统服务调用异常*/
        " nop                   \n"
        " .ltorg                \n" /*定义literal pool存放前面ldr指令中的立即数(0xE000ED08)*/
        );
}
VTOR Register(0xE000ED08)

VTOR寄存器表示向量表基址相对于内存地址(0x00000000)的偏移量。

在这里插入图片描述

在这里插入图片描述

为什么在上述的代码之中,会从向量表首地址读取初始MSP值,就是因为Cortex-M7规定了向量表的首地址存储的是Initial SP Value在这里插入图片描述

Literal Pools

简而言之,Literal Pools是汇编器会在每个Section的结尾处放置一个文字池(Literal Pools),在这个Pools中存放的是一些常量数据。受限于LDR指令可以寻址的范围有限,假如Section太大的话,在Section某位的Literal Pools就无法访问到了,因此我们可以在合适的位置主动调用LTORG指令定义一个Literal Pools。调用LTORG的位置一般在在无条件分支指令或者子例程末尾的返回指令之后,因为这样的话,处理器才不会把Literal Pools中的内容当作指令去执行。

例如ldr r0, =0xE000ED08,此处的常量0xE000ED08就会存放在LDTORG定义Literal的位置。下面就是prvPortStartFirstTask接口的反汇编代码,可以看到在末尾的0x240062f8位置存放的就是这个常量。

240062d4 <prvPortStartFirstTask>:
{
    __asm volatile (
240062d4:	4808      	ldr	r0, [pc, #32]	@ (240062f8 <prvPortStartFirstTask+0x24>)
240062d6:	6800      	ldr	r0, [r0, #0]
240062d8:	6800      	ldr	r0, [r0, #0]
240062da:	f380 8808 	msr	MSP, r0
240062de:	f04f 0000 	mov.w	r0, #0
240062e2:	f380 8814 	msr	CONTROL, r0
240062e6:	b662      	cpsie	i
240062e8:	b661      	cpsie	f
240062ea:	f3bf 8f4f 	dsb	sy
240062ee:	f3bf 8f6f 	isb	sy
240062f2:	df00      	svc	0
240062f4:	bf00      	nop
240062f6:	0000      	.short	0x0000
240062f8:	e000ed08 	.word	0xe000ed08
240062fc:	00000000 	.word	0x00000000

参考《ARM Compiler armasm User Guide》中的4.7章节

4.7 Literal pools
The assembler uses literal pools to store some constant data in code sections. You can use the LTORG directive to ensure a literal pool is within range.

The assembler places a literal pool at the end of each section. The end of a section is defined either by the END directive at the end of the assembly or by the AREA directive at the start of the following section.The END directive at the end of an included file does not signal the end of a section.

In large sections the default literal pool can be out of range of one or more LDR instructions. The offset from the PC to the constant must be:

  • Less than 4KB in ARM or Thumb code when the 32-bit LDR instruction is available, but can be in either direction.
  • Forward and less than 1KB when only the 16-bit Thumb LDR instruction is available.

When an LDR Rd,=const pseudo-instruction requires the immediate value to be placed in a literal pool, the assembler:

  • Checks if the value is available and addressable in any previous literal pools. If so, it addresses the existing constant.
  • Attempts to place the value in the next literal pool if it is not already available.

If the next literal pool is out of range, the assembler generates an error message. In this case you must use the LTORG directive to place an additional literal pool in the code. Place the LTORG directive after the failed LDR pseudo-instruction, and within the valid range for an LDR instruction.

You must place literal pools where the processor does not attempt to execute them as instructions. Place them after unconditional branch instructions, or after the return instruction at the end of a subroutine.

Example

        AREA    Loadcon, CODE, READONLY
        ENTRY                  ; Mark first instruction to execute
start
        BL      func1          ; Branch to first subroutine
        BL      func2          ; Branch to second subroutine

stop
        MOV     r0, #0x18      ; angel_SWIreason_ReportException
        LDR     r1, =0x20026   ; ADP_Stopped_ApplicationExit
        SVC     #0x123456      ; ARM semihosting (formerly SWI)

func1
        LDR     r0, =42        ; => MOV R0, #42
        LDR     r1, =0x55555555; => LDR R1, [PC, #offset to
                               ; Literal Pool 1]
        LDR     r2, =0xFFFFFFFF; => MVN R2, #0; Literal Pool 1 contains
                               ; literal Ox55555555
        BX      lr
func2
        LDR     r3, =0x55555555; => LDR R3, [PC, #offset to
                               ; Literal Pool 1]
        ; LDR r4, =0x66666666  ; If this is uncommented it
                               ; fails, because Literal Pool 2
                               ; is out of reach
        BX      lr
LargeTable
        SPACE   4200           ; Starting at the current location,
                               ; clears a 4200 byte area of memory
                               ; to zero
        LTORG                  ; Literal Pool 2 is inserted here,
                               ; but is out of range of the LDR
                               ; pseudo-instruction that needs it
        END
LTORG

这个指令的作用在Literal Pools章节已经描述,主要是为了告诉汇编器立马汇编当前代码的Literal Pools。

参考《ARM Compiler armasm User Guide》中的15.50章节

15.50 LTORG
The LTORG directive instructs the assembler to assemble the current literal pool immediately.
Syntax
LTORG
Usage
The assembler assembles the current literal pool at the end of every code section. The end of a code
section is determined by the AREA directive at the beginning of the following section, or the end of the
assembly.

These default literal pools can sometimes be out of range of some LDR, VLDR, and WLDR pseudo-instructions. Use LTORG to ensure that a literal pool is assembled within range.

Large programs can require several literal pools. Place LTORG directives after unconditional branches or subroutine return instructions so that the processor does not attempt to execute the constants as instructions.
The assembler word-aligns data in literal pools.

Example

        AREA    Example, CODE, READONLY
start   BL      func1
func1
        ;code
        LDR     r1,=0x55555555 ; => LDR R1, [pc, #offset to Literal Pool 1]
        ;code
        MOV     pc,lr          ; end function
        LTORG                  ; Literal Pool 1 contains literal &55555555.
data    SPACE   4200           ; Clears 4200 bytes of memory starting at
                               ; current location.
        END                    ; Default literal pool is empty.

Logo

「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!

更多推荐