FreeRTOS任务调度(06)

开启任务调度器

vTaskStartScheduler()

作用: 用于启动任务调度器, 任务调度器启动后, FreeRTOS 便会开始进行任务调度

该函数内部实现, 如下:

  1. 创建空闲任务

  2. 如果使能软件定时器, 则创建定时器任务

  3. 关闭中断, 防止调度器开启之前或过程中, 受中断干扰, 会在运行第一个任务时打开中断

  4. 初始化全局变量, 并将任务调度器的运行标志设置为已运行

  5. 初始化任务运行时间统计功能的时基定时器

  6. 调用函数 xPortStartScheduler()

xPortStartScheduler()

作用: 该函数用于完成启动任务调度器中与硬件架构相关的配置部分, 以及启动第一个任务

该函数内部实现, 如下:

  1. 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误

  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级

  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick

  4. 初始化临界区嵌套计数器为 0

  5. 调用函数 prvEnableVFP()使能 FPU

  6. 调用函数 prvStartFirstTask()启动第一个任务

启动第一个任务

想象下应该如何启动第一个任务?

假设我们要启动的第一个任务是任务A, 那么就需要将任务A的寄存器值恢复到CPU寄存器

任务A的寄存器值, 在一开始创建任务时就保存在任务堆栈里边!

注意:

  1. 中断产生时, 硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0出/入栈; 而R4~R11需要手动出/入栈

  2. 进入中断后硬件会强制使用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 的中断服务函数中

  1. 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址, 优先级最高的就绪态任务是系统将要运行的任务 。

  2. 通过任务的栈顶指针, 将任务栈中的内容出栈到 CPU 寄存器中, 任务栈中的内容在调用任务创建函数的时候, 已初始化, 然后设置 PSP 指针 。

  3. 通过往 BASEPRI 寄存器中写 0, 允许中断。

  4. R14 是链接寄存器 LR, 在 ISR 中 (此刻我们在 SVC 的 ISR 中) , 它记录了异常返回值 EXC_RETURN

而EXC_RETURN 只有 6 个合法的值 (M4、M7) , 如下表所示:

描述使用浮点单元未使用浮点单元
中断返回后进入Hamdler模式, 并使用MSP0xFFFFFFE10xFFFFFFF1
中断返回后进入线程模式, 并使用 MSP0xFFFFFFE90xFFFFFFF9
中断返回后进入线程模式, 并使用 PSP0xFFFFFFED0xFFFFFFFD

出栈/压栈汇编指令详解

  1. 出栈 (恢复现场) , 方向: 从下往上 (低地址往高地址) : 假设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

  1. 压栈 (保存现场) , 方向: 从上往下 (高地址往低地址) : 假设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中断是如何触发的?

  1. 滴答定时器中断调用

  2. 执行FreeRTOS提供的相关API函数: portYIELD()

本质: 通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断

在这里插入图片描述

PendSV的任务切换操作 (出栈, 即恢复现场)

硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0使用PSP压入/出任务堆栈中

ldr r3, =pxCurrentTCB 
ldr r2, [ r3 ] 
  1. 获取当前运行任务的栈顶地址

即R2保存的栈顶地址, 注意R3等于pxCurrentTCB的地址

stmdb r0!, {r4-r11, r14}
  1. 压栈, 从上往下压, 将r0的值, 当压栈的起始地址, 开始压栈

如: 先压r14,r0 = r14 (即将r14中的内容放入r0所指的内存地址) ,接着r0 = r0 - 4, 再压r11, r0 = r11 …压栈向下长, 高到低, 此时r0的值为所保存的这些数据的最底部的一个地址, 只要我们按照地址往上找就可以找到这些寄存器所保存的值

str r0, [ r2 ] 
  1. 将r0的值(前面的底部地址), 存到r2地址所指向的内存中 (即栈顶地址指向的内存, pxTopOfStack中)

在这里插入图片描述

PendSV的任务切换操作 (入栈, 即保存现场)

硬件自动将xPSR, PC(R15), LR(R14), R12, R3-R0使用PSP压入/出任务堆栈中

 bl vTaskSwitchContext
  1. 通过该函数, 获取下一个执行任务的任务控制块, 赋值给pxCurrentTCB
ldr r1, [ r3]
ldr r0, [ r1 ] 
  1. r3前面说过是存放的pxCurrentTCB的地址, 经过上述操作后, 此时该地址指向了当前下一个要运行的任务控制块, 所以r1指向pxCurrentTCB的首成员地址, 即栈顶地址pxTopOfStack , r0就取这个栈顶地址里边的值, 该值为入栈时所保存的寄存器寻址地址
ldmia r0!, {r4-r11, r14}
  1. 出栈, 以寻址地址开始, 从下往上进行出栈, 将保存在这些地址的值恢复到寄存器里边去
msr psp, r0     /* 更新任务B的栈给PSP */
bx r14 
  1. 将r0更新给psp线程堆栈

  2. 返回线程模式, 执行新任务
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值