UCOS任务切换

2024-03-31 06:38
文章标签 切换 任务 ucos

本文主要是介绍UCOS任务切换,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

任务切换

堆栈初始化

由于抢占式任务的需要,每个任务需要有自己的任务堆栈,在任务初始化函数OSTaskCreate中,通过传递p_stk_base来指出创建的那部分数组空间,最终在OSTaskStkInit中初始化该数组所储存的最初的数据:通用寄存器的值。由于通用寄存器的内容涉及到程序运行的各个过程,相关内容参考寄存器,此外,由于任务堆栈是从大地址到小的增长方式,所以寄存器的值实际上是放置在任务堆栈的末尾,然后从末尾一点点增加其他的数据的,这一点不要混淆。

CPU_STK  *OSTaskStkInit (OS_TASK_PTR    p_task,void          *p_arg,CPU_STK       *p_stk_base,CPU_STK       *p_stk_limit,CPU_STK_SIZE   stk_size,OS_OPT         opt)
{CPU_STK  *p_stk;/*数组地址由小到大增长,即数组末端的地址最大*/(void)opt;                                              /* Prevent compiler warning                               */p_stk = &p_stk_base[stk_size];                          /* Load stack pointer 获取任务堆栈的栈顶(堆栈中的最大地址)  *//* Registers stacked as if auto-saved on exception        */*--p_stk = (CPU_STK)0x01000000u;                        /* xPSR,程序状态寄存器,包含条件,中断等白标志位           */*--p_stk = (CPU_STK)p_task;                             /* Entry Point,R15寄存器实际作用是指向下一个运行的代码位置,所以此出指向任务的实际代码位置,类似于函数指针*/*--p_stk = (CPU_STK)OS_TaskReturn;                      /* R14 (LR)  是链接寄存器,用来储存当前函数结束时范返回的位置*/*--p_stk = (CPU_STK)0x12121212u;                        /* R12 是内部调用暂时寄存器                                */*--p_stk = (CPU_STK)0x03030303u;                        /* R3到R0作为传入的参数寄存器                              */*--p_stk = (CPU_STK)0x02020202u;                        /* R2                                                     */*--p_stk = (CPU_STK)p_stk_limit;                        /* R1                                                     */*--p_stk = (CPU_STK)p_arg;                              /* R0 : argument                                          *//* Remaining registers saved on process stack             */*--p_stk = (CPU_STK)0x11111111u;                        /* R11                                                    */*--p_stk = (CPU_STK)0x10101010u;                        /* R10                                                    */*--p_stk = (CPU_STK)0x09090909u;                        /* R9                                                     */*--p_stk = (CPU_STK)0x08080808u;                        /* R8                                                     */*--p_stk = (CPU_STK)0x07070707u;                        /* R7                                                     */*--p_stk = (CPU_STK)0x06060606u;                        /* R6                                                     */*--p_stk = (CPU_STK)0x05050505u;                        /* R5                                                     */*--p_stk = (CPU_STK)0x04040404u;                        /* R4                                                     */return (p_stk);
}

寄存器在进入中断的过程中xPSR,R0-R3,R12,R14,R15会由中断自行保存,是最先被保存的,所以放在最上面。而后的部分要代码手动保存,主要是R4-R11。
参考下图
任务堆栈模型

PendSV_Handler

当系统调用OS_TASK_SW()或者OSIntCtxSw()时,都将执行NVIC_INT_CTRL = NVIC_PENDSVSET,其功能是触发一次PendSV,对于PendSV,在分析其内容前先了解下为什么使用PendSv而不是其他的中断异常。
首先,PendSv是Cortex-m中优先级最低的中断异常,这意味着任何其他的中断异常都可以抢占它,同时在进入PendSV处理函数后就关闭了中断CPSID I参考,也就是在响应PendSv的过程中不会有其他中断去抢占任务切换的过程,对于PendSv的分析,在不同的抢占式系统中不一样,但可以总结如下,系统存在一个最高优先级操作SVC,在运行该操作时无法被抢占,而后,当tick_task任务运行到需要进行任务切换时,系统将挂起一个pendsv,当所有的异常都处理结束时才会进行pendsv_handle,保证了每次任务切换的过程能被及时执行。
其示例如下:
pendSV

流程:

  1. 任务A呼叫SVC来请求任务切换(例如,等到某些工作完成);
  2. OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常;
  3. 当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换;
  4. 当PendSV执行完毕后,将返回到任务B,同时进入线程模式;
  5. 发生了一个中断,并且中断服务程序已开始执行;
  6. 在ISR执行过程中,发生SysTick异常,并且抢占了该ISR;
  7. OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备;
  8. 当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行;
  9. ISR执行完毕并退出后,PendSV服务程序开始执行,并且在里面执行上下文切换;
  10. 当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。

来源

在ucos中,在每次退出最后一个嵌套层或者主动执行OSSched()时,会最终触发OSIntCtxSw/OS_TASK_SW,两者都将触发PendSv。PendSV_Handler处理cpu寄存器的代码如下,主要是保存了cpu寄存器中与当前的任务相关的数据并切换到新任务的环境的过程,分析过程涉及汇编(ARM),以下假设任务A,B。

  1. 初始化触发PendSV
    从头开始分析,在第一次进入PendSV_Handler前,系统是通过OSStartHighRdy进行最后的环境设置后触发了PendSV,其中包括初始化PSP寄存器为0的操作。在这以后的任务切换(触发PendSv异常)都是任务和中断的自发行为。

    OSStartHighRdyLDR     R0, =NVIC_SYSPRI4                                  ; Set the PendSV exception priorityLDR     R1, =NVIC_PENDSV_PRISTRB    R1, [R0]MOVS    R0, #0                                              ; Set the PSP to 0 for initial context switch call,初始化PSP寄存器为0MSR     PSP, R0LDR     R0, =OS_CPU_ExceptStkBase                           ; Initialize the MSP to the OS_CPU_ExceptStkBaseLDR     R1, [R0]MSR     MSP, R1    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)LDR     R1, =NVIC_PENDSVSET                                 ;触发一次PenSv,这也是最初的PensvSTR     R1, [R0]CPSIE   I                                                   ; Enable interrupts at processor levelOSStartHangB       OSStartHang                                         ; Should never get here
    
  2. 初入PendSv
    PendSV_Handler的完整过程如下,需要提醒:汇编在没有使用跳出当前汇编Bx Lr或者End指令等其他跳出或结束指令时会继续不停地运行后面的代码。,另外PSP,MSP都作为SP的物理寄存器,当系统进入不同的状态时会进行切换。

    PendSV_HandlerCPSID   I                                                   ; Prevent interruption during context switchMRS     R0, PSP                                             ; PSP is process stack pointer,将PSP的值保存在R0中,然后判断R0是否为0来查看CBZ     R0, PendSVHandler_nosave                            ; Skip register save the first time,判断psp是不是0,零则跳转后面的代码(第一次任务切换时psp为0)SUBS    R0, R0, #0x20                                       ; Save remaining regs r4-11 on process stackSTM     R0, {R4-R11}LDR     R1, =OSTCBCurPtr                                    ; OSTCBCurPtr->OSTCBStkPtr = SP;LDR     R1, [R1]STR     R0, [R1]                                            ; R0 is SP of process being switched out; At this point, entire context of process has been saved
    PendSVHandler_nosavePUSH    {R14}                                               ; Save LR exc_return value,暂存R14的值,后面对OSTaskSwHook()的操作会影响到R14寄存器LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();将OSTaskSwHook函数地址保存到R0BLX     R0													;POP     {R14}												;恢复R14的值LDR     R0, =OSPrioCur                                      ; OSPrioCur   = OSPrioHighRdy;LDR     R1, =OSPrioHighRdy                                  LDRB    R2, [R1]                                            STRB    R2, [R0]                                            ; LDR     R0, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;R0=&OSTCBCurPtrLDR     R1, =OSTCBHighRdyPtr                                ;R1=&OSTCBHighRdyPtr;LDR     R2, [R1]                                            ;R2=*R1=OSTCBHighRdyPtr;STR     R2, [R0]											;此时R2的值还是OSTCBHighRdyPtr的值,OSTCBHighRdyPtr是指向新tcb的指针,即此时R2存着指向新TCB的值,TCB结构的第一个对象CPU_STK *StkPtr,就是堆栈指针,那么R2=OSTCBHighRdyPtr=&StkPtr;   LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;R0=*R2=*&StkPtr=StkPtr,StkPtr为指向任务堆栈栈顶的指针,即SPLDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack,通过获取的任务堆栈的栈顶取出的值按顺序赋值给R4-R11寄存器,也就实现了非自动保存非自动恢复类寄存器的数据更新ADDS    R0, R0, #0x20                                       ;由于R4-R11相应的堆栈数据已经从任务堆栈中写入对应的寄存器,需要将栈顶SP指向存放R0寄存器的位置MSR     PSP, R0                                             ; Load PSP with new process SP,将刚获取的栈顶SP设置为新的任务堆栈PSP,这样当跳出中断时,系统会根据PSP恢复R0-R3,R12-R15,xPSR。读者需要明白,这类寄存器的恢复和保存在进入异常和出异常的过程中只要提供正确的堆栈指针就能够实现。ORR     LR, LR, #0x04                                       ; Ensure exception return uses process stackCPSIE   IBX      LR                                                  ; Exception return will restore remaining contextEND
    

    第一次进入PendSv,pendsv进入后的第一步就是判断psp是否为0,通过CBZ R0, PendSVHandler_nosave判断,由于OSStartHighRdy的初始化工作,PSP的值为0,此时将跳转到PendSVHandler_nosave继续执行。
    具体汇编和注释在上面的PendSVHandler_nosave中,阅读PendSVHandler_nosave后你会发现,除了进行必要的指针更新(OSPrioCur、OSTCBCurPtr)之外,就是实现恢复寄存器值的功能,通过上面的代码,会发现只恢复了R4-R11的寄存器值,那么其他寄存器呢,这里需要展开一个CM3的知识点

    在发生中断异常前,Cortex-M3都会自动保存一半的处理器上下文,并在从中断返回时恢复相同的上下文。即在进入中断前,系统会使用当前的堆栈指针sp(psp)保存必要的寄存器(PSR,PC,LR(R14),R12,R3,R2,R1,R0),然后在进入中断后,系统默认会改用handler下的MSP堆栈指针。在MSP下,通过获取堆栈里面的堆栈值,间接地修改堆栈环境,更新堆栈指针SP(PSP),最后在退出中断异常时,系统会切换回PSP指针。所以,在PendSVC的ISR中,我们为了保存环境,需要手动保存R4-R11,并更新堆栈指针(PSP)。对于PSP和MSP的内容,参考CM3基础>MSP&PSP;

    回到初入PendSv,当第一次进入PendSv时,由于任务堆栈对应的任务实际上还没有运行,r0-r3的参数,r15的pc,r14的返回寄存器和r12,都以初始化任务堆栈OSTaskStkInit时的值为原始值,同时R4-R11也象征性地保存,会发现OSTaskStkInit中对寄存器初始化的值有部分其实是随便写的😂,当这个寄存器被使用时,实际上其内容会被刷新,OSTaskStkInit里除了赋值了重要寄存器(如R15)相应的函数地址等重要参数的值外,其他的寄存器值只是便于调试设置的(如R12=0x12121212)。

  3. 任务切换
    假设任务A要抢占任务B时,其经过的过程如下。
    外部源触发任务切换,运行PendSV_Handler。
    系统在进入PendSV_Handler之前,已经通过PSP(B)保存能够自动保存的寄存器(R0-R3,R12…),此时PSP(B)刚好指向R0的位置(注意不是等于,只是指向),然后将堆栈指针切换为MSP,同之前一样,判断psp指针是否为0,由于不是初始状态,PSP保存着B任务的栈顶不为0,此时代码继续执行,如下注释内容

    PendSV_HandlerCPSID   I                                                   ; Prevent interruption during context switchMRS     R0, PSP                                             ; 将PSP(B)的值保存在R0中,然后判断R0是否为0来查看,此时R0=PSP(B)CBZ     R0, PendSVHandler_nosave                            ; 判断PSP(B)是否为0,由于初始化结束,PSP(B)不为0,固不跳转PendSVHandler_nosaveSUBS    R0, R0, #0x20                                       ;在上面,已知R0=PSP(B),通过R0计算需要储存R4-R11时PSP(B)的值,即自减0x20,然后从该值开始开始,STM     R0, {R4-R11}                                        ;将R4-R11的值保存在B的任务堆栈(这部分不理解请对照OSTaskStkInit里面初始化任务堆栈的顺序)。;每个寄存器占用4个字节,r4-r11共8个寄存器,即4*8=32(dec)个偏移量,转16进制即0x20。这样就把任务B的相关寄存器保存好。还差更新PSP(B)的值。LDR     R1, =OSTCBCurPtr                                    ; R1=&OSTCBCurPtrLDR     R1, [R1]                                            ; R1=*&OSTCBCurPtr=OSTCBCurPtrSTR     R0, [R1]                                            ; *OSTCBCurPtr=OSTCBStkPtr=R0,OSTCBStkPtr在此时还是指向B任务堆栈栈顶;解析:当B任务的所有寄存器保存好之后,还需要更新B任务堆栈指针PSP(B),即更新栈顶,更新的PSP(B)保存在哪里呢?实际上就保存在B任务的TCB块里面。下次切换回B任务时直接取就是了。
    PendSVHandler_nosave...                                                                 
    

    以上就完成了对B任务相关寄存器的保存,后面就是切换到A任务中,这和上面初始化进入第一个堆栈的过程是一样的,都是使用PendSVHandler_nosave。

参考
一步步写STM32 OS【三】PendSV与堆栈操作
Cortex-M3 SVC与PendSV
cortex-M3 的SVC、PendSV异常,与操作系统(ucos实时系统)
从零开始写一个操作系统(三) —— 任务切换器
uCOS在任务切换时做了什么以及任务切换汇编代码分析

这篇关于UCOS任务切换的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/863666

相关文章

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

如何使用Python实现一个简单的window任务管理器

《如何使用Python实现一个简单的window任务管理器》这篇文章主要为大家详细介绍了如何使用Python实现一个简单的window任务管理器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 任务管理器效果图完整代码import tkinter as tkfrom tkinter i

Spring Boot 集成 Quartz 使用Cron 表达式实现定时任务

《SpringBoot集成Quartz使用Cron表达式实现定时任务》本文介绍了如何在SpringBoot项目中集成Quartz并使用Cron表达式进行任务调度,通过添加Quartz依赖、创... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启

Java使用多线程处理未知任务数的方案介绍

《Java使用多线程处理未知任务数的方案介绍》这篇文章主要为大家详细介绍了Java如何使用多线程实现处理未知任务数,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 知道任务个数,你可以定义好线程数规则,生成线程数去跑代码说明:1.虚拟线程池:使用 Executors.newVir

JDK多版本共存并自由切换的操作指南(本文为JDK8和JDK17)

《JDK多版本共存并自由切换的操作指南(本文为JDK8和JDK17)》本文介绍了如何在Windows系统上配置多版本JDK(以JDK8和JDK17为例),并通过图文结合的方式给大家讲解了详细步骤,具有... 目录第一步 下载安装JDK第二步 配置环境变量第三步 切换JDK版本并验证可能遇到的问题前提:公司常

nvm如何切换与管理node版本

《nvm如何切换与管理node版本》:本文主要介绍nvm如何切换与管理node版本问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录nvm切换与管理node版本nvm安装nvm常用命令总结nvm切换与管理node版本nvm适用于多项目同时开发,然后项目适配no

Spring Boot中定时任务Cron表达式的终极指南最佳实践记录

《SpringBoot中定时任务Cron表达式的终极指南最佳实践记录》本文详细介绍了SpringBoot中定时任务的实现方法,特别是Cron表达式的使用技巧和高级用法,从基础语法到复杂场景,从快速启... 目录一、Cron表达式基础1.1 Cron表达式结构1.2 核心语法规则二、Spring Boot中定