经过上面的操作以后我们就可以启动第一个任务了,函数 prvStartFirstTask()用于启动第一 个任务,这是一个汇编函数,函数源码如下:
__asm void prvStartFirstTask( void )
{
PRESERVE8
ldr r0, =0xE000ED08 ;R0=0XE000ED08 (1)
ldr r0, [r0] ;取 R0 所保存的地址处的值赋给 R0 (2)
ldr r0, [r0] ;获取 MSP 初始值 (3)
msr msp, r0 ;复位 MSP (4)
cpsie I ;使能中断(清除 PRIMASK) (5)
cpsie f ;使能中断(清除 FAULTMASK) (6)
dsb ;数据同步屏障 (7)
isb ;指令同步屏障 (8)
svc 0 ;触发 SVC 中断(异常) (9)
nop
nop
}
(1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000) 开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提 供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器 (VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定 义向量表,比如在 STM32F103 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器, 代码如下: SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00 通过上面一行代码就将向量表开始地址重新定义到了 0X08000000,向量表的起始地址存储 的就是 MSP 初始值。
(2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取寄存器 VTOR 中的值,并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为 0X08000000。这个就是向量表重定义的起始地址
(3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取地址 0X08000000 处存储的数据,并将其保存在 R0 寄存器中(因为MSP的值保存在第一个变量中)。我们知道向量表的起始地址保存的就是主栈指针 MSP 的初始值,这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、 (3)这三步起始就是为了获取 MSP 的初始值而已!
(4)、复位 MSP,R0 中保存了 MSP 的初始值,将其赋值给 MSP 就相当于复位 MSP。
(5)和(6)、使能中断,关于这两个指令的详细内容请参考《权威指南》的“第 4 章 架构” 的第 4.2.3 小节。
(7)和(8)、数据同步和指令同步屏障,这两个指令的详细内容请参考《权威指南》的“第 5 章 指令集”的 5.6.13 小节。
(9)调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于 OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。关于 SVC 的详细内容请参考《权威指 南》的“第 10 章 OS 支持特性”的 10.3 小节。在 FreeRTOS 中仅仅使用 SVC 异常来启动第一 个任务,后面的程序中就再也用不到 SVC 了。
8.2.4 SVC 中断服务函数
在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就 是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler(),但是 FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:
#define xPortPendSVHandler PendSV_Handler 函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1)
ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1 (2)
ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0 (3)
ldmia r0!, {r4-r11, r14} ;出栈 ,R4~R11 和 R14 (4)
msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5)
isb ;指令同步屏障
mov r0, #0 ;R0=0 (6)
msr basepri, r0 ;寄存器 basepri=0,开启中断 (7)
orr r14, #0xd ; (8)
bx r14 (9)
}
(1)、获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。这里先获取这个指针存储的地址
(2)、通过任务控制块的地址获取首成员的地址
(3)、通过首成员的地址获得首成员的值,首成员的值就是创建函数时栈顶的值。这个值就是此时此刻栈顶的地址。
(4)、R4~R11,R14 这些寄存器出栈。这里使用了指令 LDMIA,LDMIA 指令是多加载/存储指令,不过这里使用的是具有回写的多加载/存储访问指令,用法如下: LDMIA Rn! , {reg list} 表示从 Rn 指定的存储器位置读取多个字,地址在每次读取后增加(IA),Rn 在传输完成以后 写回。对于 STM32 来说地址一次增加 4 字节,比如如下代码: LDR R0, =0X800 LDMIA R0!, {R2~R4} 上面两行代码就是将 0X800 地址的数据赋值给寄存器 R2,0X804 地址的数据赋值给寄存 器 R3,0X8008 地址的数据赋值给 R4 寄存器,然后,重点来了!此时 R0 为 800A!通过这一 步我们就从任务堆栈中将 R4~R11 这几个寄存器的值给恢复了。 这里有朋友就要问了,R0~R3,R12,PC,xPSR 这些寄存器怎么没有恢复?这是因为这些寄 存器会在退出中断的时候 MCU 自动出栈(恢复)的,而 R4~R11 需要由用户手动出栈。这个我们 在分析 PendSV 中断服务函数的时候会讲到。到这步以后我们来看一下堆栈的栈顶指针指到哪 里了?如图 8.2.4.5 所示:
从图 8.3.4.5 可以看出恢复 R4~R11 和 R14 以后堆栈的栈顶指针应该指向地址 0X20000EB8, 也就是保存寄存器 R0 值的存储地址。退出中断服务函数以后进程栈指针 PSP 应该从这个地址 开始恢复其他的寄存器值。
(5)、设置进程栈指针 PSP,PSP=R0=0X20000EB8,如图 8.2.4.6 所示
(6)、设置寄存器 R0 为 0。
(7)、设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
(8)、R14 寄存器的值与 0X0D 进行或运算,得到的结果就是 R14 寄存器的新值。表示退出 异常以后 CPU 进入线程模式并且使用进程栈!
(9)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使 用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开 始运行!