• 任务及任务切换


    1.概述

    这一块的内容有点晦涩,我也是是搞了很久才搞明白的。所以,从自己搞明白到我决定讲明白也经过了漫长的过程。基于以上的原因我踌躇了很久,这部分的内容讲清楚也是一件具有挑战性的事情。

    2.任务

    什么是任务?理解这个问题对理解bcos任务切换的原理至关重要。对于一个嵌入式操作系统的使用者理解任务的重点在于任务处理函数,因为任务处理函数是实现任务功能的主体。然而,他们往往忽略了在任务创建之初的他们定义的那个用于任务栈的数组。对于bcos来说,每一个任务都有一片独立的内存空间作为任务执行的栈,对于操作系统开发者而言任务的栈几乎是一个任务的全部。

    讲到这里几乎所有读者都还是云里雾里的,对于大多数初学者或者已经工作了好几年的开发者而言,特别是那些更加专注于应用开发的嵌入式开发者,栈在他们的印象中一直都是一个比较模糊的概念。其实,我也是去年(2021年)才在工作中对这个概念逐渐清晰,然而我已经从事C语言编程开发工作3年了,如果从2015年开始算起的话已经有六年之久。过去我印象中的栈只是C语言程序中的一篇内存空间,只知道函数的局部变量会保存在这一片空间中,很长一段时间甚至傻傻的分不清此栈和数据结构课程中学习的栈的区别。既然讲清楚任务切换的原理无法绕开栈这个概念,那我就顺着我的理解过程逐步讲解。

    3.从计算机的体系结构讲起

    如果有的读者研究过《ARM Cortex-M3权威指南》这本书应该对栈有过深入的了解,我就是从研究这本书开始的。到此,决定从头讲起。话说ARM核内大约有十几二十个寄存器,他们都是32位的寄存器所以STM32是32位的单片机。这些寄存器分别是他们:
    在这里插入图片描述

    当然还有几个特殊功能寄存器这里图片中没有给出,这里先关注这15个寄存器就好了。我可以说CPU就是靠着这15个寄存器就实现了软件代码的加载和执行。
    R0-R12:通用寄存器
    R0-R12都是32位的通用寄存器,用于数据操作。下面有一段汇编代码就很好的体现了通用寄存器的作用。我们看到无论是操作数还是函数地址都要先加载到通用寄存器中才会进一步去使用和执行相应的指令。

    ; Reset handler routine
    Reset_Handler   PROC
                    EXPORT  Reset_Handler             [WEAK]
                    IMPORT  __main
    
                    LDR     R0, = SystemInit_ExtMemCtl ; initialize external memory controller
                    BLX     R0
    
                    LDR     R1, = __initial_sp        ; restore original stack pointer
                    MSR     MSP, R1                   
    
                    LDR     R0, =__main
                    BX      R0
                    ENDP
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Banked R13: 两个堆栈指针
    Cortex‐M3 拥有两个堆栈指针,然而它们是 banked,因此任一时刻只能使用其中的一个。
    · 主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程),当然一般情况下不适用操作系统的情况下大多数的单片机系统都只使用这一个主堆栈指针就够了。
    · 进程堆栈指针(PSP):由用户的应用程序代码使用。一般操作系统会使用PSP实现多线程或多任务的功能。bcos就是使用该堆栈指针实现任务的切换从而实现多任务的功能。

    R14:连接寄存器
    当CPU调用用一个子函数时(子程序),R14用于保存子函数的返回地址。下面的例子很好的展示了R14的作用:
    在这里插入图片描述

    我们看到上图的程序马上要执行一个叫“SystemInit()”的子程序,它的返回地址是0x08003DF8,当我再向下执行一步后R14应该会保存这个返回地址,下图是我执行一步后的截图:
    在这里插入图片描述

    有的读者可能发现和我们的预期有些不同,此时的R14的值是0x08003DF9不是我们期望的0x08003DF8,由于程序指令都是四字节对其的,这里R14的最后两位有其他特殊的用途,这个后续再做说明。

    R15:程序计数寄存器
    指向当前的程序地址。如果修改它的值,就能改变程序的执行流。

    4.单片机上电是如何执行起来的?

    我提出这个问题相比有些读者会笑话。但是,想必百分之六十的读者不甚清楚其中的细节,只有真正掌握了一点汇编的开发者才对这个问题有所了解,从始至终清楚的人已经是嵌入式领域的高级人才了。

    通过前面几小节的内容我们已经清楚了CPU核内的几个寄存器的主要作用,下面我们随意打开一个STM32的工程,想一下我们的程序是从哪个文件里的程序开始执行的?想必很多人都是知道的,就是startup.s。有的读者看过一些分析startup.s的博客但终于还是云里雾里不得其解。我们会发现每个启动文件中都有这么一段程序:

    ; Vector Table Mapped to Address 0 at Reset
                    AREA    RESET, DATA, READONLY
                    EXPORT  __Vectors
                    EXPORT  __Vectors_End
                    EXPORT  __Vectors_Size
    
    __Vectors       DCD     __initial_spTop           ; Top of Stack
                    DCD     Reset_Handler             ; Reset Handler
                    DCD     NMI_Handler               ; NMI Handler
                    DCD     HardFault_Handler         ; Hard Fault Handler
                    DCD     MemManage_Handler         ; MPU Fault Handler
                    DCD     BusFault_Handler          ; Bus Fault Handler
                    DCD     UsageFault_Handler        ; Usage Fault Handler
                    DCD     0                         ; Reserved
                    DCD     0                         ; Reserved
                    DCD     0                         ; Reserved
                    DCD     0                         ; Reserved
                    DCD     SVC_Handler               ; SVCall Handler
                    DCD     DebugMon_Handler          ; Debug Monitor Handler
                    DCD     0                         ; Reserved
                    DCD     OS_CPU_PendSVHandler            ; PendSV Handler
                    DCD     SysTick_Handler           ; SysTick Handler
    
                    ; External Interrupts
                    DCD     WWDG_IRQHandler           ; Window Watchdog
                    DCD     PVD_IRQHandler            ; PVD through EXTI Line detect
                    DCD     TAMPER_IRQHandler         ; Tamper
                    DCD     RTC_IRQHandler            ; RTC
                    DCD     FLASH_IRQHandler          ; Flash
                    DCD     RCC_IRQHandler            ; RCC
                    DCD     EXTI0_IRQHandler          ; EXTI Line 0
                    DCD     EXTI1_IRQHandler          ; EXTI Line 1
                    DCD     EXTI2_IRQHandler          ; EXTI Line 2
                    DCD     EXTI3_IRQHandler          ; EXTI Line 3
                    DCD     EXTI4_IRQHandler          ; EXTI Line 4
                    DCD     DMA1_Channel1_IRQHandler  ; DMA1 Channel 1
                    DCD     DMA1_Channel2_IRQHandler  ; DMA1 Channel 2
                    DCD     DMA1_Channel3_IRQHandler  ; DMA1 Channel 3
                    DCD     DMA1_Channel4_IRQHandler  ; DMA1 Channel 4
                    DCD     DMA1_Channel5_IRQHandler  ; DMA1 Channel 5
                    DCD     DMA1_Channel6_IRQHandler  ; DMA1 Channel 6
                    DCD     DMA1_Channel7_IRQHandler  ; DMA1 Channel 7
                    DCD     ADC1_2_IRQHandler         ; ADC1 & ADC2
                    DCD     USB_HP_CAN1_TX_IRQHandler  ; USB High Priority or CAN1 TX
                    DCD     USB_LP_CAN1_RX0_IRQHandler ; USB Low  Priority or CAN1 RX0
                    DCD     CAN1_RX1_IRQHandler       ; CAN1 RX1
                    DCD     CAN1_SCE_IRQHandler       ; CAN1 SCE
                    DCD     EXTI9_5_IRQHandler        ; EXTI Line 9..5
                    DCD     TIM1_BRK_IRQHandler       ; TIM1 Break
                    DCD     TIM1_UP_IRQHandler        ; TIM1 Update
                    DCD     TIM1_TRG_COM_IRQHandler   ; TIM1 Trigger and Commutation
                    DCD     TIM1_CC_IRQHandler        ; TIM1 Capture Compare
                    DCD     TIM2_IRQHandler           ; TIM2
                    DCD     TIM3_IRQHandler           ; TIM3
                    DCD     TIM4_IRQHandler           ; TIM4
                    DCD     I2C1_EV_IRQHandler        ; I2C1 Event
                    DCD     I2C1_ER_IRQHandler        ; I2C1 Error
                    DCD     I2C2_EV_IRQHandler        ; I2C2 Event
                    DCD     I2C2_ER_IRQHandler        ; I2C2 Error
                    DCD     SPI1_IRQHandler           ; SPI1
                    DCD     SPI2_IRQHandler           ; SPI2
                    DCD     USART1_IRQHandler         ; USART1
                    DCD     USART2_IRQHandler         ; USART2
                    DCD     USART3_IRQHandler         ; USART3
                    DCD     EXTI15_10_IRQHandler      ; EXTI Line 15..10
                    DCD     RTCAlarm_IRQHandler       ; RTC Alarm through EXTI Line
                    DCD     USBWakeUp_IRQHandler      ; USB Wakeup from suspend
                    DCD     TIM8_BRK_IRQHandler       ; TIM8 Break
                    DCD     TIM8_UP_IRQHandler        ; TIM8 Update
                    DCD     TIM8_TRG_COM_IRQHandler   ; TIM8 Trigger and Commutation
                    DCD     TIM8_CC_IRQHandler        ; TIM8 Capture Compare
                    DCD     ADC3_IRQHandler           ; ADC3
                    DCD     FSMC_IRQHandler           ; FSMC
                    DCD     SDIO_IRQHandler           ; SDIO
                    DCD     TIM5_IRQHandler           ; TIM5
                    DCD     SPI3_IRQHandler           ; SPI3
                    DCD     UART4_IRQHandler          ; UART4
                    DCD     UART5_IRQHandler          ; UART5
                    DCD     TIM6_IRQHandler           ; TIM6
                    DCD     TIM7_IRQHandler           ; TIM7
                    DCD     DMA2_Channel1_IRQHandler  ; DMA2 Channel1
                    DCD     DMA2_Channel2_IRQHandler  ; DMA2 Channel2
                    DCD     DMA2_Channel3_IRQHandler  ; DMA2 Channel3
                    DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
    __Vectors_End
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

    此时,有些读者可能就要说了,这不就是中断向量表吗?有什么神奇的?我已经很熟悉了。其实不然,我们更应该关注的是这段程序开头的部分,我专门对它进行了截图,并将重点圈画出来。
    在这里插入图片描述

    任我们如何观察,我们都会发现每一STM32的程序都只有一个程序段标有RESET,这表明CPU复位后从该段开始执行。另外,我们又发现中断向量表的第二个赋值的是Reset_Handler,复位后CPU会固定从这个地方找到程序的入口函数然后开始执行。知道了这个原理,下面这段程序想必应该可以执行起来, 感兴趣的读者可以尝试执行一下:

    ; Vector Table Mapped to Address 0 at Reset
                    AREA    RESET, DATA, READONLY
                    EXPORT  __Vectors
                    EXPORT  __Vectors_End
    
    __Vectors       DCD     0           
                    DCD     test                         
    __Vectors_End
                    AREA    |.text|, CODE, READONLY
    test            PROC
                    EXPORT  test            [WEAK]
                    
                    MOV     R0, #1
                    MOV     R1, #3
                    ADD     R2, R1, R0
    				
    		B	test
    
                    ENDP
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    下面我们继续研究启动文件,我们看看Reset_Handler都做了那些事情:

    ; Reset handler routine
    Reset_Handler   PROC
                    EXPORT  Reset_Handler             [WEAK]
                    IMPORT  __main
    
                    LDR     R0, = SystemInit_ExtMemCtl ; initialize external memory controller
                    BLX     R0
    
                    LDR     R1, = __initial_sp        ; restore original stack pointer
                    MSR     MSP, R1                    
    
                    LDR     R0, =__main
                    BX      R0
                    ENDP
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    我们发现在Reset_Handler里对系统进行了初始化,然后对MSP指针进行了初始化,最后调用了__main并跳转到main函数。这里多说一句,__main的主要作用就是对程序的全局变量进行初始化,后续动态执行程序的内容会重点介绍。

    5.栈

    栈作为一个计算机术语其有两方面的含义,1.一种先进后出的数据结构;2.一片用来保存cpu调用程序段时的寄存器中的数据的内存空间,其在存取的过程中遵循先进后出的原则。
    栈就两种操作:

    • push:压栈,向栈内加入数据
    • pop :出栈

    注:有的人喜欢将栈与堆连起来叫做程序堆栈,其实在C语言中栈是栈,堆是堆,它们本身就是两个东西千万不要混为一谈。

    • 相同点:都是一片内存区域;
    • 不同点:
    1. 由编译器分配,存放函数的参数值,局部变量,寄存器组(不同的单片机/处理器各有不同)、函数调用参数传递、中断异常产生时须保存处理器状态的寄存器值等;
    2. 由程序员分配释放,对于C而言,malloc、realloc/free进行分配/释放;

    到此,我们大概了解了栈在cpu执行过程中的作用。前面讲过,“栈几乎是操作系统中任务的全部”到此读者可能还没有一个较为深入的理解,下面给出bcos系统中PendSV中断处理函数的源码,我们详细的分析一下就可以理解这句话的含义了。为了便于读者理解,我将我的分析注释在代码的上面,这种方式可以方便读者对照源码做出理解。

    /*
     * 这个结构体定义了bcos的任务控制块,将栈指针定义成结构体的第一个变量是为了在任务切换时任务控制块结构体变量的地址就是栈指针的地址,
     * 这样更方便汇编堆栈指针的赋值操作
     */
    typedef struct bcos_tcb_s {
        /* Pointer to current top of stack */
        BC_OS_STK       *OSTCBStkPtr;
        /* 链表头 */ 
        struct list_head list;
        /* 优先级 */
        uint8_t          priority;
        /* 延时时间戳 */   
        BC_OS_TICK       delay_stamp;
    	/* 任务栈顶指针 */
    	BC_OS_STK		 *OSTCBStkTopPtr;
    } bcos_tcb_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    OS_CPU_PendSVHandler\
    	PROC
        EXPORT	OS_CPU_PendSVHandler 			; 对PendSV中断处理函数进行申明
        EXTERN 	OSTCBCur					   ; 对当前任务控制块变量进行申明
        EXTERN 	OSTCBHighRdy				   ; 对当前最高优先级任务控制块变量进行申明
    
        CPSID 	I
        MRS   	R0, PSP							; 获取PSP栈指针寄存器中的值保存到R0
        CBZ   	R0, OS_CPU_PendSVHandler_nosave	  ; 如果R0中的值为0,则跳转到OS_CPU_PendSVHandler_nosave执行,这时上电初始化时第一次做任务调度时
    										  ; 需要做一个特殊的处理
        SUBS  	R0, R0, #0x20					; 在触发PendSV中断时,CPU会将R0 - R3,LR,SP,PC等寄存器压入栈中,但R4-R11需要程序员手动压栈											   (PUSH)
        STM   	R0,{R4-R11}					    ; 至于为什么要将栈指针值减去0x20,这个后面解释
    
        LDR   	R1, =OSTCBCur
        LDR   	R1, [R1]
        STR   	R0, [R1]
    
    OS_CPU_PendSVHandler_nosave
        LDR     R0, =OSTCBCur                   ; 下面的四行代码就相当于C语言的OSTCBCur  = OSTCBHighRdy;操作,从而实现任务的切换
        LDR     R1, =OSTCBHighRdy
        LDR     R2, [R1]
        STR     R2, [R0]
    
        LDR     R0, [R2]                        ; R0 保存的是新任务的SP指针; SP = OSTCBHighRdy->OSTCBStkPtr;
        LDM     R0, {R4-R11}                    ; 将 r4-11 进行手动出栈(POP)
        ADDS    R0, R0, #0x20
        MSR     PSP, R0                         ; 给 PSP赋值新任务的SP,到这里真正完成任务的切换
        ORR     LR, LR, #0x04                   ; Ensure exception return uses process stack
        CPSIE   I
        BX      LR                              ; Exception return will restore remaining context
    
    	ENDP
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    从上面的代码可以看出,bcos任务的切换其实就是对CPU PSP的操作,通过切换PSP指向的栈空间实现任务的切换。每个任务的栈其实保存了各自任务的函数和代码调用的状态。

    img

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    上面的五张图片清晰的展示了任务切换过程中对任务栈的操作过程及任务栈状态的变化过程,读者可以结合代码和注释并对照这五张图片进行理解bcos操作系统任务切换的完整过程。

    6.总结

    到此,任务及任务切换的原理就讲的差不多了,现在我们来总结一下。其实,对于用户来讲可能更加关注的是如何利用操作系统来实现其复杂的业务逻辑,操作系统本身只是为更好的实现多任务的功能服务的。但是,开发者了解操作系统原理本身会对功能的实现提供帮助。

    其实本节内容其实解释了以下几个问题:

    • 什么是任务?任务的本质是什么?
    • 单片机是如何从复位运行起来程序的?
    • 什么是栈?栈在操作系统或程序中扮演的是什么角色?
    • 操作系统如何实现任务的切换?任务的切换其实是栈指针的切换。

    读者如果能够对以上问题有一个比较深刻的认识,那么将对嵌入式操作系统的原理的认识将有一个质的改变。

  • 相关阅读:
    关于Zotero的主要插件功能说明
    图像分割方法一:阈值分割
    UIButton的类型
    记录一次服务器CPU负载高,利用率正常的处理方法
    C语⾔内存函数
    cuda安装失败原因汇总
    学习Oracle数据库的新建表的基本操作(二)
    关于 java 的动态绑定机制
    RabbitMQ-03(实战 、RabbitMQ 的六种消息模式)
    13、用户web层服务(一)
  • 原文地址:https://blog.csdn.net/BLUCEJIE/article/details/125956054