嵌入式操作系统学习(2)任务启动和任务切换

2023-05-16

1.前后台系统与多任务系统

熟悉嵌入式开发的同学都知道,一般没有操作系统的程序都是在main函数有一个死循环来完成相关任务,一些紧急的操作放在中断里来完成,通常称作前后台系统,如下图所示:
这里写图片描述
对于业务逻辑简单的程序,这么做没什么不好的。但是代码复杂后,很多个中断包含嵌套中断会使复杂性急剧膨胀,中断间的交互将会变得十分困难,可维护性差,增加一个新功能对代码的改动较大,如果中断函数执行时间太长,同级中断将会受到影响。所以为了减少复杂性,通过在中断里置标志位,在主循环里查询,但是这样查询又是按照顺序来的,后面的任务实时性将会降低,而且如果每个任务的执行周期不一样,又会额外增加很多工作量。另外,这种系统的应用层代码和硬件代码的耦合性较高也带来了移植的困难。

实时操作系统(RTOS)正是为了解决前后台系统应对复杂应用的不足,通过芯片内核的软件中断来实现多任务系统。多任务系统如下图所示
这里写图片描述
每个模块的任务保持独立,任务可以自行设置执行周期而不受其他任务的影响。高优先级的任务能够抢占低优先级的任务,保证了系统的实时性,当任务优先级相同的时候还可以按照时间片的方式运行,看起来就像在同时运行多个任务一样。任务由系统调度器统一管理,通过信号量、消息、事件标志等机制使任务和任务、任务和中断的交互变得更加容易,但又保持了各模块的独立性,总之引入实时操作系统使程序处理复杂逻辑业务变得更加容易,很好地隔离了应用层和硬件驱动层,极大地提高了系统的可扩展性。

接下来就来介绍FreeRTOS和µC/OS-III是如何管理任务运行的,主要从任务启动和任务切换2个方面来分析。

2.FreeRTOS的任务启动和切换

2.1堆栈初始化

每一个任务都会有一个任务控制块(TCB),TCB是一个非常复杂的结构体,包含了大量任务的重要信息如任务堆栈、任务运行状态、任务优先级等等,它与任务的调度息息相关,这里我们不细讲。

通过调用xTaskCreate()函数来创建一个任务,来初始化TCB相关信息,这里我们需要知道TCB的第一个变量为堆栈的栈顶指针,在任务启动和切换时会用到,新建任务时会初始化堆栈,要注意堆栈的偏移地址与Cortex-M3的内核寄存器存在对应关系,之后在任务启动后会与Cortex-M3的线程堆栈指针挂接在一起,任务切换后堆栈的内容会pop到内核寄存器,CPU从而开始执行任务代码。

//更新任务控制块的栈顶指针
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
//初始化堆栈
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    /* Simulate the stack frame as it would be created by a context switch
    interrupt. */
    pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
    *pxTopOfStack = portINITIAL_XPSR;   /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;    /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;   /* LR */
    pxTopOfStack -= 5;  /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters;   /* R0 */
    pxTopOfStack -= 8;  /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

入栈时堆栈地址从高地址到低地址增长,xPSR、PC、LR、R12、R3~R0这8个寄存器会由CPU自动入栈,入栈顺序如下图所示:
这里写图片描述
其他寄存器R11~R4需要手工入栈

最后在FreeRTOS里通过全局变量pxCurrentTCB指向当前优先级最高的第一个就绪任务控制块,任务切换时只需改变pxCurrentTCB的指向即可

2.2任务启动

通过调用vTaskStartScheduler()来启动任务,此时会创建一个空闲任务

    /* The Idle task is being created using dynamically allocated RAM. */
        xReturn = xTaskCreate( prvIdleTask, "IDLE", configMINIMAL_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

之后根据不同的硬件平台会调用xPortStartScheduler()来启动任务,此时会先设置系统定时器周期,再调用vPortStartFirstTask()来启动第一个任务,这是一个汇编函数

vPortStartFirstTask
    /* Use the NVIC offset register to locate the stack. */
    ldr r0, =0xE000ED08 ;中断向量表的第一个地址
    ldr r0, [r0]        ;获取第一个中断地址
    ldr r0, [r0]        ;获取栈顶地址
    /* Set the msp back to the start of the stack. */
    msr msp, r0
    /* Call SVC to start the first task, ensuring interrupts are enabled. */
    cpsie i             ;开中断
    cpsie f             ;开异常
    dsb                 ;数据隔离
    isb                 ;指令隔离,保证前面的指令先执行完
    svc 0               ;跳转到SVC中断

    END

进到这里之后,以后不会再返回主循环了,所以代码先复位主堆栈指针,然后跳转到SVC中断

vPortSVCHandler:
    /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB
    ldr r1, [r3]
    ldr r0, [r1] ;获取栈顶地址
    /* Pop the core registers. */
    ldmia r0!, {r4-r11} ;手工将寄存器r4-r11出栈
    /*让线程堆栈指针PSP指向pxCurrentTCB->pxTopOfStack
    此后上面提到的8个寄存器就会由M3内核自动出栈*/
    msr psp, r0   
    isb
    mov r0, #0
    msr basepri, r0  ;不屏蔽任何中断
    orr r14, r14, #13
    bx r14     ;跳转到任务代码,任务地址将会从PSP自动出栈到PC指针

上面有一句话是orr r14, r14, #13表示返回线程模式,使用的是线程堆栈PSP,没有这一句则会使用主堆栈MSP,r14即为LR寄存器,在调用子函数时会保存返回地址,而在进入中断时只有3个合法值,这个值是CPU自动设置的,意义如下

  • 0xFFFFFFF1
    表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入Handler模式,使用MSP堆栈,(相当于从中断返回到另一个中断)。

  • 0xFFFFFFF9
    表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入线程模式,使用MSP堆栈(这种用于不使用PSP只使用MSP堆栈的情况)。

  • 0xFFFFFFFD
    表示中断返回时从PSP堆栈恢复寄存器值,中断返回后进入线程模式,使用PSP堆栈(这是常见的,OS处理完中断后返回用户程序)。

这里需要返回任务代码,并使用PSP堆栈,所以使用0xFFFFFFFD作为返回值

2.3任务切换

任务切换和任务启动的代码其实是类似的,只不过多了2个步骤:

  • 在当前任务堆栈中,手动将R4~R11寄存器入栈
  • 调用vTaskSwitchContext寻找当前最高优先级任务TCB,并保存在pxCurrentTCB

切换操作在xPortPendSVHandler中断里进行

xPortPendSVHandler:
    mrs r0, psp
    isb
    ldr r3, =pxCurrentTCB           /* Get the location of the current TCB. */
    ldr r2, [r3]

    stmdb r0!, {r4-r11}             /* Save the remaining registers. */
    str r0, [r2]                    /* Save the new top of stack into the first member of the TCB. */

    stmdb sp!, {r3, r14}
    /*关中断,防止全局变量pxCurrentTCB被外部修改*/
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp!, {r3, r14}

    ldr r1, [r3]
    ldr r0, [r1]                    /* The first item in pxCurrentTCB is the task top of stack. */
    ldmia r0!, {r4-r11}             /* Pop the registers. */
    /*将新的堆栈挂接到PSP,PC等8个寄存器会自动出栈,跳转到新任务代码执行任务*/
    msr psp, r0
    isb
    bx r14

在系统心跳或发送信号量时都会触发一次请求任务切换,即进入xPortPendSVHandler中断,都会调用portYIELD,这个函数直接修改寄存器的相应bit来进入中断

/* Scheduler utilities. */
#define portYIELD()                                         \
{                                                           \
    /* Set a PendSV to request a context switch. */         \
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;         \
    __DSB();                                                \
    __ISB();                                                \
}

3.µC/OS-III的任务启动和切换

同样的µC/OS-III也有一个任务TCB,TCB的第一个成员就是任务堆栈指针,新建任务也要初始化任务堆栈

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                                            */
    *--p_stk = (CPU_STK)OS_TaskReturn;                      /* R14 (LR)                                               */
    *--p_stk = (CPU_STK)0x12121212u;                        /* R12                                                    */
    *--p_stk = (CPU_STK)0x03030303u;                        /* R3                                                     */
    *--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);
}

在启动第一个任务前会调用OSStart()启动调度器,在这里会调用 OSStartHighRdy()来启动第一个任务,这个函数是用汇编写的

OSStartHighRdy
    LDR     R0, =NVIC_SYSPRI14                                  ; Set the PendSV exception priority
    LDR     R1, =NVIC_PENDSV_PRI     ;设置最低优先级中断
    STRB    R1, [R0]

    MOVS    R0, #0                                              ; Set the PSP to 0 for initial context switch call
    MSR     PSP, R0

    LDR     R0, =OS_CPU_ExceptStkBase                           ; Initialize the MSP to the OS_CPU_ExceptStkBase
    LDR     R1, [R0]                 ;设置主堆栈指针为OS_CPU_ExceptStkBase
    MSR     MSP, R1    

    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET     ;跳转到OS_CPU_PendSVHandr中断
    STR     R1, [R0]

    CPSIE   I                                                   ; Enable interrupts at processor level

OSStartHang
    B       OSStartHang                                         ; Should never get here

由代码知道,首先设置OS_CPU_PendSVHandr中断为最低优先级,并初始PSP指针为0,表示这是第一次中断,不同于FreeRTOS,µC/OS-III并没有使用SVC中断,第一次启动任务和任务切换使用同一个中断。之后把主堆栈指针设为指定值,而FreeRTOS中是设为复位值。接着就进入OS_CPU_PendSVHandr切换任务,代码如下:

OS_CPU_PendSVHandler
    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, =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
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, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;
    LDR     R1, =OSTCBHighRdyPtr
    LDR     R2, [R1]
    STR     R2, [R0]

    LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
    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, #0x04                                       ; Ensure exception return uses process stack
    CPSIE   I
    BX      LR                                                  ; Exception return will restore remaining context

    END

如果PSP为0则表示是第一次启动任务,则直接进入OS_CPU_PendSVHandler_nosave切换任务,否则需要事先把当前的寄存器R4~R11手工压入当前任务堆栈,再进行切换。

在切换前会调用OSTaskSwHook对原来的任务做一些统计工作,之后就会更新OSPrioCur为新任务优先级,OSTCBCurPtr为新任务TCB,最后将新任务堆栈出栈到R4-R11寄存器,剩余的寄存器在绑定PSP后由M3内核自动出栈。

µC/OS-III中通过调用OS_TASK_SW() 或OSIntCtxSw()触发OS_CPU_PendSVHandler来切换任务,在此前已经获取最高优先级的就绪任务,所以在切换时直接把OSTCBCurPtr更新为OSTCBHighRdyPtr即可

void  OSSched (void)
{
    ......

    CPU_INT_DIS();
    //此处获取最高优先级就绪任务
    OSPrioHighRdy   = OS_PrioGetHighest();                  /* Find the highest priority ready                        */
    OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
    if (OSTCBHighRdyPtr == OSTCBCurPtr) {                   /* Current task is still highest priority task?           */
        CPU_INT_EN();                                       /* Yes ... no need to context switch                      */
        return;
    }
    ......
    //此处触发切换中断
    OS_TASK_SW();                                           /* Perform a task level context switch                    */
    CPU_INT_EN();
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式操作系统学习(2)任务启动和任务切换 的相关文章

随机推荐

  • JAVA-- 带你重温函数式接口、使用Functional Interface最佳实践

    1 概念及分类 函数式接口 Functional Interface 就是一个有且仅有一个抽象方法 但是可以有多个非抽象方法的接口 并且这类接口使用了 FunctionalInterface进行注解 在jdk8中 引入了一个新的包java
  • 如何基于LLMs使用LangChain构建强大的差异化应用--LangChain之初体验

    近年来 语言模型 LMs 特别是LLMs已经成为最令人兴奋和最有影响力的技术之一 它们可以为各种目的生成自然语言文本 例如回答问题 撰写摘要 创建故事等等 然而 仅使用LMs还不足以构建真正强大且与众不同的应用程序 您还需要 将LMs连接到
  • 连接局域网下centos服务器和mysql数据库慢优化方法

    连接局域网下centos服务器和mysql数据库慢优化方法 Xshell连接centos比较慢优化 通过xshell连接服务器需要服务端安装openssl xff0c openssl配置项UseDNS 默认情况是开启状态的 xff0c Us
  • 云技术概念

    云计算概念是由Google提出的 xff0c 这是一个美丽的网络应用模式 狭义云计算是指IT基础设施的交付和使用模式 xff0c 指通过网络以按需 易扩展的方式获得所需的资源 xff1b 广义云计算是指服务的交付和使用模式 xff0c 指通
  • 通过IP地址和子网掩码与运算计算相关地址

    通过IP地址和子网掩码与运算计算相关地址 知道ip地址和子网掩码后可以算出 xff1a 1 网络地址 2 广播地址 3 地址范围 4 本网有几台主机 例1 xff1a 下面例子IP地址为192 168 100 5 子网掩码是255 255
  • 关于VSCode的三个配置文件

    关于VSCode的三个配置文件 若要使用VSCode来开发C 43 43 程序 xff0c 则应该了解以下三种配置文件 xff0c 分别为 xff1a tasks json xff1a 编译器相关的配置文件 比如 xff0c 设置编译指令
  • 用Prime95来做linux下CPU压力测试

    Prime95是用来做linux下CPU压力测试的 由GIMPS Great Internet Mersenne Prime Search xff09 所提供 主要是透过运算找出梅森质数 质数 xff08 Prime number xff0
  • ubuntu修改启动项等待时间、修改启动项顺序、更改启动内核

    目录 ubuntu修改启动项等待时间 修改启动项顺序 更改系统内核版本 ubuntu修改启动项等待时间 步骤 sudo vi etc default grub找GRUB TIMEOUT 61 10 那一行 xff0c 把10改为需要的时间即
  • 好电影推荐

    今天在网上看到一篇给大家推荐电影的帖子 xff0c 里面搜集了一些欧美电影 xff0c 有悬疑片 剧情片 动作片 恐怖片 xff0c 包括经典的 以及一些流传不是很广的 xff0c 在这里收藏一下与大家一起分享 xff1a 1 不请自来 典
  • CodeBlock无编辑器问题解决

    对于新手小白来说 xff0c 在使用CodeBlock做C语言或者C 43 43 方面的编写时 xff0c CodeBlock会出现如下情况 xff1a Can 39 t find compiler executable in your c
  • argparse—使用命令行输入,给变量赋值

    import argparse if name 61 61 39 main 39 parser 61 argparse ArgumentParser parser add argument 39 bbb 39 type 61 int def
  • 树莓派keras、TensorFlow环境安装记录

    1 安装arm版的TensorFlow xff0c 方法参考 xff1a https github com samjabrahams tensorflow on raspberry pi 2 安装keras sudo apt get ins
  • inception 论文阅读笔记

    inception V1 提出inception unit xff0c 采用多个size的卷积核对输入进行卷积 采用1x1 3x3 5x5的卷积核不是必须的 xff0c 而是为了方便 可以理解为不同大小的卷积核可以提取到图像中不同大小的特征
  • 树莓派3 raspbian系统 红外遥控器 lirc库使用笔记

    目的 xff1a 接入红外接受硬件 xff0c 使用红外遥控器控制树莓派 硬件图 xff1a 稍后补 接受口GPIO 61 18 首先安装lirc库 sudo apt get update sudo apt get install lirc
  • 树莓派设置静态IP

    vi etc dhcpcd conf 使用 vi 编辑文件 xff0c 增加下列配置项 指定接口 eth0 或者wlan0 interface wlan0 指定静态IP xff0c 24表示子网掩码为 255 255 255 0 stati
  • BGP→→

    BGP 4 提供了一套新的机制以支持无类域间路由 这些机制包括支持网络前缀的通告 取消 BGP 网络中 类 的概念 BGP 4 也引入机制支持路由聚合 xff0c 包括 AS 路径的集合 特点 BGP属于外部或域间路由协议 BGP的主要目标
  • omnet++,veins,sumo使用多应用层实现车辆和行人的模拟

    1 完成网络的搭建 xff1a 搭建一个简单的十字路口场景 xff1a 1 xff09 定义结点 xff1a 新建intersection node xml 内容如下 xff1a NOTE1 xff1a 结点属性type 61 34 tra
  • 我的2011--衣带渐宽终不悔,为伊消得人憔悴

    古今之成大事业 大学问者 xff0c 必经过三种之境界 xff1a 34 昨夜西风凋碧树 独上高楼 xff0c 望尽天涯路 34 此第一境也 34 衣带渐宽终不悔 xff0c 为伊消得人憔悴 34 此第二境也 34 众里寻他千百度 xff0
  • Java架构师面试必备题(含答案)

    第一题 xff1a 一条sql执行过长的时间 xff0c 你如何优化 xff0c 从哪些方面 xff1f 答 xff1a 1 查看sql是否涉及多表的联表或者子查询 xff0c 如果有 xff0c 看是否能进行业务拆分 xff0c 相关字段
  • 嵌入式操作系统学习(2)任务启动和任务切换

    1 前后台系统与多任务系统 熟悉嵌入式开发的同学都知道 xff0c 一般没有操作系统的程序都是在main函数有一个死循环来完成相关任务 xff0c 一些紧急的操作放在中断里来完成 xff0c 通常称作前后台系统 xff0c 如下图所示 xf