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

相关文章

IDEA如何切换数据库版本mysql5或mysql8

《IDEA如何切换数据库版本mysql5或mysql8》本文介绍了如何将IntelliJIDEA从MySQL5切换到MySQL8的详细步骤,包括下载MySQL8、安装、配置、停止旧服务、启动新服务以及... 目录问题描述解决方案第一步第二步第三步第四步第五步总结问题描述最近想开发一个新应用,想使用mysq

Python Invoke自动化任务库的使用

《PythonInvoke自动化任务库的使用》Invoke是一个强大的Python库,用于编写自动化脚本,本文就来介绍一下PythonInvoke自动化任务库的使用,具有一定的参考价值,感兴趣的可以... 目录什么是 Invoke?如何安装 Invoke?Invoke 基础1. 运行测试2. 构建文档3.

解决Cron定时任务中Pytest脚本无法发送邮件的问题

《解决Cron定时任务中Pytest脚本无法发送邮件的问题》文章探讨解决在Cron定时任务中运行Pytest脚本时邮件发送失败的问题,先优化环境变量,再检查Pytest邮件配置,接着配置文件确保SMT... 目录引言1. 环境变量优化:确保Cron任务可以正确执行解决方案:1.1. 创建一个脚本1.2. 修

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

Spring Boot实现多数据源连接和切换的解决方案

《SpringBoot实现多数据源连接和切换的解决方案》文章介绍了在SpringBoot中实现多数据源连接和切换的几种方案,并详细描述了一个使用AbstractRoutingDataSource的实... 目录前言一、多数据源配置与切换方案二、实现步骤总结前言在 Spring Boot 中实现多数据源连接

MyBatis 切换不同的类型数据库方案

下属案例例当前结合SpringBoot 配置进行讲解。 背景: 实现一个工程里面在部署阶段支持切换不同类型数据库支持。 方案一 数据源配置 关键代码(是什么数据库,该怎么配就怎么配) spring:datasource:name: test# 使用druid数据源type: com.alibaba.druid.pool.DruidDataSource# @需要修改 数据库连接及驱动u

解决Office Word不能切换中文输入

我们在使用WORD的时可能会经常碰到WORD中无法输入中文的情况。因为,虽然我们安装了搜狗输入法,但是到我们在WORD中使用搜狗的输入法的切换中英文的按键的时候会发现根本没有效果,无法将输入法切换成中文的。下面我就介绍一下如何在WORD中把搜狗输入法切换到中文。