开启任务调度器
vTaskStartScheduler()
作用: 用于启动任务调度器, 任务调度器启动后, FreeRTOS 便会开始进行任务调度
该函数内部实现, 如下:
-
创建空闲任务
-
如果使能软件定时器, 则创建定时器任务
-
关闭中断, 防止调度器开启之前或过程中, 受中断干扰, 会在运行第一个任务时打开中断
-
初始化全局变量, 并将任务调度器的运行标志设置为已运行
-
初始化任务运行时间统计功能的时基定时器
-
调用函数 xPortStartScheduler()
xPortStartScheduler()
作用: 该函数用于完成启动任务调度器中与硬件架构相关的配置部分, 以及启动第一个任务
该函数内部实现, 如下:
-
检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
-
配置 PendSV 和 SysTick 的中断优先级为最低优先级
-
调用函数 vPortSetupTimerInterrupt()配置 SysTick
-
初始化临界区嵌套计数器为 0
-
调用函数 prvEnableVFP()使能 FPU
-
调用函数 prvStartFirstTask()启动第一个任务
启动第一个任务
想象下应该如何启动第一个任务?
假设我们要启动的第一个任务是任务A, 那么就需要将任务A的寄存器值恢复到CPU寄存器
任务A的寄存器值, 在一开始创建任务时就保存在任务堆栈里边!
注意:
-
中断产生时, 硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0出/入栈; 而R4~R11需要手动出/入栈
-
进入中断后硬件会强制使用MSP指针 , 此时LR(R14) 的值将会被自动被更新为特殊的EXC_RETURN
prvStartFirstTask()
用于初始化启动第一个任务前的环境, 主要是重新设置MSP 指针, 并使能全局中断
1、什么是MSP指针?
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,
MCU 会自动更新 SP 指针, ARM Cortex-M 内核提供了两个栈空间, :
主堆栈指针 (MSP) 它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
进程堆栈指针 (PSP) 用于常规的应用程序代码 (不处于异常服务例程中时) 。
在FreeRTOS中, 中断使用MSP (主堆栈) , 中断以外使用PSP (进程堆栈)
2、为什么是 0xE000ED08?
因为需从 0xE000ED08 获取向量表的偏移, 为啥要获得向量表呢? 因为向量表的第一个是 MSP 指针!
取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址;
在根据向量表存储的地址, 来访问第一个元素, 也就是初始的 MSP
CM3 允许向量表重定位——从其它地址处开始定位各异常向量 这个就是向量表偏移量寄存器, 向量表的起始地址保存的就是主栈指针MSP 的初始值
vPortSVCHandler ()
注意: SVC中断只在启动第一次任务时会调用一次, 以后均不调用
当使能了全局中断, 并且手动触发 SVC 中断后, 就会进入到 SVC 的中断服务函数中
-
通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址, 优先级最高的就绪态任务是系统将要运行的任务 。
-
通过任务的栈顶指针, 将任务栈中的内容出栈到 CPU 寄存器中, 任务栈中的内容在调用任务创建函数的时候, 已初始化, 然后设置 PSP 指针 。
-
通过往 BASEPRI 寄存器中写 0, 允许中断。
-
R14 是链接寄存器 LR, 在 ISR 中 (此刻我们在 SVC 的 ISR 中) , 它记录了异常返回值 EXC_RETURN
而EXC_RETURN 只有 6 个合法的值 (M4、M7) , 如下表所示:
描述 | 使用浮点单元 | 未使用浮点单元 |
---|---|---|
中断返回后进入Hamdler模式, 并使用MSP | 0xFFFFFFE1 | 0xFFFFFFF1 |
中断返回后进入线程模式, 并使用 MSP | 0xFFFFFFE9 | 0xFFFFFFF9 |
中断返回后进入线程模式, 并使用 PSP | 0xFFFFFFED | 0xFFFFFFFD |
出栈/压栈汇编指令详解
- 出栈 (恢复现场) , 方向: 从下往上 (低地址往高地址) : 假设r0地址为0x04汇编指令示例:
ldmia r0!, {r4-r6} /* 任务栈r0地址由低到高, 将r0存储地址里面的内容手动加载到 CPU寄存器r4、r5、r6 */
r0地址(0x04)内容加载到r4, 此时地址r0 = r0+4 = 0x08
r0地址(0x08)内容加载到r5, 此时地址r0 = r0+4 = 0x0C
r0地址(0x0C)内容加载到r6, 此时地址r0 = r0+4 = 0x10
- 压栈 (保存现场) , 方向: 从上往下 (高地址往低地址) : 假设r0地址为0x10汇编指令示例:
stmdb r0!, {r4-r6} } /* r0的存储地址由高到低递减, 将r4、r5、r6里的内容存储到r0的任务栈里面。 */
地址: r0 = r0-4 = 0x0C, 将r6的内容 (寄存器值) 存放到r0所指向地址(0x0C)
地址: r0 = r0-4 = 0x08, 将r5的内容 (寄存器值) 存放到r0所指向地址(0x08)
地址: r0 = r0-4 = 0x04, 将r4的内容 (寄存器值) 存放到r0所指向地址(0x04)
任务切换
任务切换的本质: 就是CPU寄存器的切换。
假设当由任务A切换到任务B时, 主要分为两步:
第一步: 需暂停任务A的执行, 并将此时任务A的寄存器保存到任务堆栈, 这个过程叫做保存现场
第二步: 将任务B的各个寄存器值 (被存于任务堆栈中) 恢复到CPU寄存器, 这个过程叫做恢复现场
对任务A保存现场, 对任务B恢复现场, 这个整体的过程称之为上下文切换
注意: 任务切换的过程在
PendSV中断服务函数
里边完成
PendSV中断是如何触发的?
-
滴答定时器中断调用
-
执行FreeRTOS提供的相关API函数: portYIELD()
本质: 通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断
PendSV的任务切换操作 (出栈, 即恢复现场)
硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0使用PSP压入/出任务堆栈中
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]
- 获取当前运行任务的栈顶地址
即R2保存的栈顶地址, 注意R3等于pxCurrentTCB的地址
stmdb r0!, {r4-r11, r14}
- 压栈, 从上往下压, 将r0的值, 当压栈的起始地址, 开始压栈
如: 先压r14,r0 = r14 (即将r14中的内容放入r0所指的内存地址) ,接着r0 = r0 - 4, 再压r11, r0 = r11 …压栈向下长, 高到低, 此时r0的值为所保存的这些数据的最底部的一个地址, 只要我们按照地址往上找就可以找到这些寄存器所保存的值
str r0, [ r2 ]
- 将r0的值(前面的底部地址), 存到r2地址所指向的内存中 (即栈顶地址指向的内存, pxTopOfStack中)
PendSV的任务切换操作 (入栈, 即保存现场)
硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0使用PSP压入/出任务堆栈中
bl vTaskSwitchContext
- 通过该函数, 获取下一个执行任务的任务控制块, 赋值给pxCurrentTCB
ldr r1, [ r3]
ldr r0, [ r1 ]
- r3前面说过是存放的pxCurrentTCB的地址, 经过上述操作后, 此时该地址指向了当前下一个要运行的任务控制块, 所以r1指向pxCurrentTCB的首成员地址, 即栈顶地址pxTopOfStack , r0就取这个栈顶地址里边的值, 该值为入栈时所保存的寄存器寻址地址
ldmia r0!, {r4-r11, r14}
- 出栈, 以寻址地址开始, 从下往上进行出栈, 将保存在这些地址的值恢复到寄存器里边去
msr psp, r0 /* 更新任务B的栈给PSP */
bx r14
-
将r0更新给psp线程堆栈
-
返回线程模式, 执行新任务