ucos-ii嵌入式操作系统任务调度(二)----任务切换瞬间cpu做了什么以及任务任务切换函数OS_TASK_SW

先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题,二维码如下:
在这里插入图片描述

一 概述

在上一篇文章《ucos-ii嵌入式操作系统任务调度(一)----任务调度的过程及实现原理》中,我们分析了任务调度过程中的步骤,但是在最后结尾处,我们留了一个小疑问:任务是如何实现切换的呢?在切换的瞬间cpu发生了什么?汇编函数OSCtxSw实现原理又是什么?
在分析这个疑问之前,先看如下几个问题:

  1. CPU的栈是什么?
  2. CPU在执行代码,调用函数的过程中,如何传递函数参数?如何正确返回被调用函数的?
  3. CPU发生中断/异常时,如何响应中断/异常,响应完之后,又是如何跳转回发生异常/中断的地方,继续执行代码的
    先看第一个问题
    栈:首先从物理意义上来看,它就是位于芯片内部的一端RAM内存(**当然这个说法不一定准确,我理解的是外部sdram是不是也可以作为栈区域,不一定是芯片内部的RAM内存,在我的印象中,会在linker file指定栈地址,欢迎各位大佬不吝赐教 **)。其次从软件代码操作的角度来看,它是仅在表头进行插入和删除操作的线性表,也就说对于码农来说,我们只能在一端读取/写入栈。根据芯片架构的不同,又分为两种栈,栈空间地址向上增长和栈空间地址向下增长。
    这个栈是sp指针实时指向的栈。
    再看第二个问题
    如何实现函数调用时,参数传递?
    针对ARM平台,其指令集规定,当参数不超过4个时,我们使用cpu内部的R0~R3来传递参数,如果函数参数超过4个,比如说对于printf函数,我们使用栈来传递参数。
    再看第三个问题
    cpu发生中断、异常时,会先进行现场保护,把当前cpu各个寄存器的值保存在栈中(R0~R15寄存器,当然就包括了sp指针,pc指针),保存完之后,就会调转到中断,异常处执行,执行完成之后,就会从栈中恢复当前的状态,并继续运行。
    上面三个问题,在这里也是粗浅的做了一些说明,在此就不展开来详细说明,读者可以自行查阅资料

二 过程分析

有了上面的基础认识之后,我们现在来看一下,ucos任务切换的瞬间,发生了什么。
首先任务切换,是通过调用宏OS_TASK_SW(),即汇编函数OSCtxSw实现的,OSCtxSw函数如下:

OSCtxSw
    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

该汇编函数的目的,就是将寄存器NVIC_INT_CTRL设置为NVIC_PENDSVSET,查阅cortex-m系列内核手册可知,就是使能了PendSV异常,也就是说在这里软件触发了一次异常。
既然这个时候,软件触发了一次异常,根据我们上面的知识,可以知道,在响应异常处理时,cpu会自动先进行现场保存,也就是会将当前cpu各个寄存器的值写入到栈中(这个栈是cpu指针真正指向的栈,也就是我们在linker file中指定的内存区域),保存完之后,就会响应异常处理了,异常处理函数如下:

PendSV_Handler
    CPSID   I                                                   ; Prevent interruption during context switch
    MRS     R0, PSP                                             ; PSP is process stack pointer
    CBZ     R0, OS_CPU_PendSVHandler_nosave                     ; Skip register save the first time

    SUBS    R0, R0, #0x20                                       ; Save remaining regs r4-11 on process stack
    STM     R0, {R4-R11}

    LDR     R1, =OSTCBCur                                       ; OSTCBCur->OSTCBStkPtr = SP;
    LDR     R1, [R1]
    STR     R0, [R1]                                            ; R0 is SP of process being switched out

逐句分析

CPSID   I          ;关闭中断,防止切换期间被打扰,在切换期间有可能产生新的中断,更新堆栈中的信息
MRS     R0, PSP    ;将cpu的堆栈指针psp送给R0,此时R0也指向栈顶
CBZ     R0, OS_CPU_PendSVHandler_nosave ;如果 R0 = 0,跳到OS_CPU_PendSVHandler_nosave,即不保存上下文;如果R0 != 0,则继续往下执行;为什呢?因为首次调用时,是没有上文的,我们只需要切换到下文即可
;保存上文 (如果是第一次切换,则不执行下面的语句,直接执行OS_CPU_PendSVHandler_nosave)
SUBS    R0, R0, #0x20   ;因为寄存器是32位的,4字节对齐,自动压栈的寄存器有8个,所以偏移为8*0x04=0x20
STM     R0, {R4-R11}   ;除去自动压栈的寄存器外,需手动将R4-R11压栈
 LDR     R1, =OSTCBCur
 LDR     R1, [R1] 
 STR     R0, [R1] 
 ;这里为什么是三行?ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令,先让R1=OSTCBCur(注意这里R1存放的是一个地址),然后将这个地址指向的值赋值给R1,最后再将R1中的值付给R0,其实最终目的就是将R0寄存器值,写入到当前用户分配的堆栈中去

接着就是切换到下文了,如下:

OS_CPU_PendSVHandler_nosave
    PUSH    {R14}                                               ; Save LR exc_return value
    LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();
    BLX     R0
    POP     {R14}

    LDR     R0, =OSPrioCur                                      ; OSPrioCur = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    LDR     R0, =OSTCBCur                                       ; OSTCBCur  = OSTCBHighRdy;
    LDR     R1, =OSTCBHighRdy
    LDR     R2, [R1]
    STR     R2, [R0]

    LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
    LDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; Load PSP with new process SP
    ORR     LR, LR, #0xF4                                       ; Ensure exception return uses process stack
    CPSIE   I
    BX      LR                                                  ; Exception return will restore remaining context

同样,逐句分析

PUSH    {R14}                          ; LR压栈,下面要调用C函数,调用C函数时,将用来保存返回的函数地址
LDR     R0, =OSTaskSwHook              ; 调用OSTaskSwHook();
BLX     R0                             ; 调用OSTaskSwHook()结束并返回
POP     {R14}                          ; 恢复 LR 寄存器,与前面对应
LDR     R0, =OSPrioCur                 ; 置OSPrioCur = OSPrioHighRdy;
LDR     R1, =OSPrioHighRdy
LDRB    R2, [R1]
STRB    R2, [R0]
LDR     R0, =OSTCBCur                  ; 置OSTCBCur  = OSTCBHighRdy;
LDR     R1, =OSTCBHighRdy
LDR     R2, [R1]
STR     R2, [R0]
LDR     R0, [R2]                       ; R0中的值为新任务的SP; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM     R0, {R4-R11}                   ; 堆栈中的值手动弹出到寄存器:R4-R11
ADDS    R0, R0, #0x20
 MSR     PSP, R0                        ; PSP = 新任务SP,将SP的指针指向堆栈的顶端
ORR     LR, LR, #0x04                  ; 确保异常返回后使用PSP
CPSIE   I                              ; 重新打开中断,与开始关闭中断对应
BX      LR                             ; 退出异常,从PSP自动弹出xPSR,PC,LR,R0-R3,进入新任务运行
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值