精华内容
下载资源
问答
  • pendsv
    2020-06-11 10:45:49

    今天学习freeRTOS的任务切换,了解了一个非常重要的中断,PendSV。

    PendSV的触发方式:往 NVIC 的PendSV 悬起寄存器中写1,也就是往ICSR第28位写1,即可将PendSV异常挂起。若是当前没有高优先级中断产生,那么程序将会进入PendSV handler。

    freeRTOS的任务切换在PendSV的中断函数里面执行。

    更多相关内容
  • 一步步写一步步写STM32 OS【三】【三】PendSV与堆栈操作与堆栈操作假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。但若在产生 SysTick 异常时正在响应一个中断,则 SysTick异常会...
  • RTOS系列文章(2):PendSV功能,为什么需要PendSV RTOS系列文章(3): 为什么将SysTick和PendSV的优先级设置为最低 RTOS系列文章(4): MDK软件仿真 + Debug-(printf)-Viewer使用方法 RTOS系列文章(5):C语言程序...

    RTOS系列(1):基础知识——中断嵌套
    RTOS系列文章(2):PendSV功能,为什么需要PendSV
    RTOS系列文章(3): 为什么将SysTick和PendSV的优先级设置为最低
    RTOS系列文章(4): MDK软件仿真 + Debug-(printf)-Viewer使用方法
    RTOS系列文章(5):C语言程序运行原理分析:汇编、栈、栈帧、进栈、出栈、保存现场、恢复现场、返回
    RTOS系列文章(6):Cortex-M3/4之SP,MSP,PSP,Thread模式、Handler模式、内核态、用户态
    RTOS系列文章(7):CM3/4之LR寄存器、EXC_RETURN深入分析
    RTOS系列文章(8):深入分析中断处理过程
    RTOS系列文章(9):再次分析栈帧、函数调用与中断调用的区别
    RTOS系列文章(10):简单OS示例分析
    RTOS系列文章(11):RTOS启动方式——直接设置CONTROL寄存器、SVC启动、PendSV启动

    前言

    在上一篇文章中,我们分析了启动OS的2种方式,分别为:

    1. 直接设置CONTROL寄存器,这种方式比较简单,但是不够优雅,或者说程序架构不够清晰。
    2. 通过中断+修改EXC_RETURN的方式,间接设置CONTROL寄存器,来实现OS的启动。
      其中方式2是目前RTOS常用的方式,我们以FreeRTOS和uC/OS为例,FreeRTOS是使用SVC来启动第一个任务,即实现OS的启动的。而uC/OS则是通过PendSV来实现启动首个任务,达到启动OS的目的。这两种方式没有好坏之分,只不过在实现理念上,由于FreeRTOS比较年轻,所以基于SVC启动OS的方式,更加安全,比较符合大型OS的设计理念,即OS与应用解耦,应用通过SVC来调用OS的服务。

    为什么要使用SVC、PendSV来启动OS

    这里可能会有人说,上面的提到两种方式中,直接设置CONTROL寄存器太简单粗暴,那能不能直接设置EXC_RETURN的值,然后BL EXC_RETURN是不是更简单呢?我也想过这个问题,虽然没有真正测试过,但是如果直接 BL EXC_RETURN可能会有如下问题:

    1. EXC_RETURN是CPU专门为中断设计的,使中断服务程序可以使用C来编写,关于EXC_RETURN的详细介绍,可以参考前面的文章。所以使用EXC_RETURN机制,就意味着必须要配合着中断使用,而能被程序配置触发的中断,似乎只有Systick 、PendSV和SVC中断。而Systick中断,我们一般只是提供时钟节拍,不会用于任务调度,上下文切换,所以很显然,我们就剩下2种选择:PendSV和SVC,不过这也足够了。
    2. 我们使用中断+EXC_RETURN意味着,我们可以使用CPU专门为中断设计的自动压栈、出栈机制。通过自动出栈机制,实现任务上下文切换后,自动切换到下一个任务,这使得我们的OS启动与任务的调度的代码可以复用,更加简洁。

    使用SVC启动OS分析

    FreeRTOS就是使用SVC中断来启动OS的,废话不多说,直接上FreeRTOS v9.0的代码:
    我们以STM32F103ZET6为例:

    prvStartFirstTask

    __asm void prvStartFirstTask( void )
    {
        PRESERVE8
     
        /* Use the NVIC offset register to locate the stack. */
        ldr r0, =0xE000ED08   //  0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址
        					  //  执行该命令后,R0寄存器内容为0xE000ED08 
     
        ldr r0, [r0]   	      // 将R0中的内容作为地址立即数传给R0,即将0xE000ED08指向的内容作为立即数,
        					 // 传递给R0, 以STM32F103启动文件中, 最初地址放置的__initial_sp, 值为0x08000000
        					  // 执行该命令后,R0寄存器内容为0x08000000,这正是STM32F103的Flash首地址
        				
        ldr r0, [r0]  		 // 将R0中的内容作为地址传递给R0,即将0x08000000指向的内容作为立即数,传递给
        					 // R0, 0x08000000中指向的是MSP的初始值,即初始化栈顶
        					 // 执行该命令后,R0寄存器值内容为0x20005B30
        /* Set the msp back to the start of the stack. */
        msr msp, r0   		//  将 __initial_sp的初始值写入 MSP 中 
        /* Globally enable interrupts. */
        cpsie i				// 开中断
        cpsie f				// 开异常
        dsb
        isb
        /* Call SVC to start the first task. */
        svc 0   			//    调用SVC中断   
        nop
        nop
    }
    /*-----------------------------------------------------------*/
    

    在Cortex-M3 处理器,上电后默认使用MSP,为内核态,所以代码执行到这里的时候MSP已经更新了(因为只要执行C函数,都会使用堆栈),而一旦开启OS,程序将永远不会返回到这里,是一条不归路,所以之前使用的堆栈空间,我们就可以回收了,回收的方式也非常简单,重置MSP即可,将MSP再次指向上电初始位置。 (从这一点也能看出,FreeRTOS在内存使用上,还是比较节俭的)。

    vPortSVCHandler

    __asm void vPortSVCHandler( void )
    {
    	PRESERVE8
    
    	ldr	r3, =pxCurrentTCB	// 1. 获取将要运行的任务TCP指针立即数, 
    	ldr r1, [r3]			// 2. 将R3指向的内容,作为立即数赋值给R1
    	ldr r0, [r1]			// 3. 将R1指向的内容,作为立即数赋值给R0, 经过1,2,3, 后,R0中其实就存放了第一个将要
    							// 启动运行的任务的 堆栈栈顶(The first item in pxCurrentTCB is the task top of stack) 
    							
    	ldmia r0!, {r4-r11}		// Pop the registers that are not automatically saved on exception entry and 
    							// the critical nesting count. 手动出栈,恢复一半现场 
    	msr psp, r0				// 将更新后的任务堆栈地址psp_task 赋值给PSP,用于退出中断时,CPU自动恢复剩余的一半现场 
    	isb
    	mov r0, #0				
    	msr	basepri, r0			// 所有的中断都没有被屏蔽,开所有中断
    	orr r14, #0xd			// !!!这里非常重要,R14即LR,此时LR=0xFFFFFFF9,因为程序运行到这里是从线程模式进入中断,
    							// 而且到目前为止,一直在使用MSP,所以LR被CPU自动设置为0xFFFFFFF9,
    							// orr为[或]操作,相当于LR设置为 0xFFFFFFFD,这显然属于EXC_RETURN的取值范围 
    	bx r14					// 退出中断,由于此时EXC_RETURN为0xFFFFFFFD, 这就相当于告诉CPU 3件事儿:
    							// (1) 中断退出;  (2) 退出后,CPU运行模式为用户态; (3)退出后,使用PSP。
    }
    /*-----------------------------------------------------------*/
    

    执行完vPortSVCHandler,CPU就会感知到是中断退出,此时CPU会从当前任务的堆栈psp开始,使用PSP,自动出栈,恢复另一半现场,也就是xPSR, PC, R14, R12, R3, R2, R1, R0, 其中PC中存放了第一个任务的运行地址,CPU就开始执行第一个任务,至此,完成了启动第一个任务运行,即OS的启动。

    使用SVC启动OS首个任务小结

    在这里插入图片描述
    上述流程虽然与FreeRTOS的有少许的差别,但是基本的流程是一致的。

    使用PendSV启动OS分析

    uC/OS-II是使用PendSV中断来启动OS的,参考uC/OS-II v2.9.3代码:

    OSStart

    void  OSStart (void)
    {
        if (OSRunning == OS_FALSE) {
            OS_SchedNew();                               /* Find highest priority's task priority number   */
            OSPrioCur     = OSPrioHighRdy;
            OSTCBHighRdy  = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run    */
            OSTCBCur      = OSTCBHighRdy;
            OSStartHighRdy();                            /* Execute target specific code to start task     */
        }
    }
    

    先启动一次任务查找,找到最高优先级的就绪任务,然后调用OSStartHighRdy运行该任务。

    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]
        MSR     MSP, R1    
    
        LDR     R0, =OSRunning                                      ; OSRunning = TRUE
        MOVS    R1, #1
        STRB    R1, [R0]
        
        LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
        LDR     R1, =NVIC_PENDSVSET
        STR     R1, [R0]
    
        CPSIE   I                                                   ; Enable interrupts at processor level
    

    这里还包括了一些初始化,比如PSP,设置OSRunning标识,最后触发PendSV中断。

    OS_CPU_PendSVHandler

    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, =OSTCBCur                                       ; OSTCBCur->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, =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, #0x04                                       ; Ensure exception return uses process stack
        CPSIE   I
        BX      LR                                                  ; Exception return will restore remaining context
    
        END
    

    上述中,通过PSP来区分是否为首次运行,如果是首次运行,则直接跳转到OS_CPU_PendSVHandler_nosave,即初次运行,就不需要保存当前任务的一半现场,而是直接启动当前最高优先级就绪任务。
    特别注意, 在最后的两个重要指令:

    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
    

    分别为更新当前任务堆栈地址到PSP,然后对LR进行或运算,此时LR的值为0xFFFFFFF9, 经过与0x04的或运算后,LR值变成了0xFFFFFFFD,然后调用BX LR,这里跟上面分析FreeRTOS的启动流程最后设置EXC_RETURN的机制,本质上是一样的,至此完成了OS的启动。开始运行第一个任务,后面都是一样的,通过Systick Handler来周期更新节拍,判断是否调度,然后通过PendSV进行任务调度,任务上下文切换。

    小结

    启动RTOS的核心是配置CONTROL寄存器,来创造OS依赖的运行环境:用户态 + 使用PSP,而配置CONTROL寄存器一般有2种方式,分别为:

    1. 直接设置CONTROL寄存器,这种方式比较简单,但是不够优雅,或者说程序架构不够清晰。
    2. 通过中断+修改EXC_RETURN的方式,间接设置CONTROL寄存器,来实现OS的启动。
      其中方式2是目前RTOS常用的方式,我们以FreeRTOS和uC/OS为例,FreeRTOS是使用SVC来启动第一个任务,即实现OS的启动的。而uC/OS则是通过PendSV来实现启动首个任务,达到启动OS的目的。这两种方式没有好坏之分,只不过在实现理念上,由于FreeRTOS比较年轻,所以基于SVC启动OS的方式,更加安全,比较符合大型OS的设计理念,即OS与应用解耦,应用通过SVC来调用OS的服务。

    所以,既可以通过PendSV实现OS启动,也可以通过SVC来实现OS启动。但是核心都是使用可编程中断 + 修改EXC_RETURN的方式来设置CONTROL寄存器,这也是主流的启动方式,因为有中断机制的帮助,可以让OS的启动更加合理高效。

    展开全文
  • RTOS系列文章(2):PendSV功能,为什么需要PendSV

    千次阅读 多人点赞 2022-05-15 22:13:45
    大多数嵌入式RTOS在Cortex-M3/M4上的移植都需要PendSV,比如uCOS、RT-Thread、FreeRTOS等,本文就对PendSV的功能作用,以及为什么需要PendSV进行详细的分析。 PendSV是什么? 我们先引用《Cortex-M3权威指南》对...

    背景

    大多数嵌入式RTOS在Cortex-M3/M4上的移植都需要PendSV,比如uCOS、RT-Thread、FreeRTOS等,本文就对PendSV的功能作用,以及为什么需要PendSV进行详细的分析。

    PendSV是什么?

    我们先引用《Cortex-M3权威指南》对PendSV的介绍:

    PendSV(可悬起的系统调用),它是一种CPU系统级别的异常,它可以像普通外设中断一样被悬起,而不会像SVC服务那样,因为没有及时响应处理,而触发Fault。

    个人理解PendSV的英文全称应该是:Pend System Service Call,简称PendSV.
    所以PendSV的最大特点就是,它是系统级别的异常,但它又天生支持【缓期执行】。

    PendSV使用场景

    由于PendSV的特点就是支持【缓期执行】,所以嵌入式OS可以利用它这个特点,进行任务调度过程的上下文切换。而为什么要使用【缓期执行】的特点来进行上下文切换呢?简单的说就是任何嵌入式RTOS,都需要尽量不打断外设中断。
    我们来举例说明,假如一个系统中有2个就绪的任务,上下文切换被切换的场合有:

    1. 执行了一个系统调用SVC。
    2. 系统滴答定时器SYSTICK中断,触发了任务的调度。

    没有外部中断,OS直接切换任务

    假如我们在Systick中断服务程序中,启动上下文切换,流程图如下:
    在这里插入图片描述

    上图中,似乎一切都正常,任务调度很流畅,但现实总是很骨感的,不可能这么简单,假如在产生异常时,CPU正在响应另一个中断ISR1,而SysTick的优先级又大于ISR1,在这种情况下,SysTick就会抢占ISR1,获取CPU使用权,但是在SysTick中不能进行上下文切换,因为这将导致中断ISR1被延迟,这在实时要求的系统中是不能容忍的,但是这个说辞只是为了方便理解,更重要的是:

    在Cortex-M3中,如果OS在某个中断活跃时,抢占了该中断,而且又发生了任务调度,并执行了任务,切换到了线程运行模式,将直接触发Fault异常。

    SysTick优先级高于外部中断,OS抢占IRQ进行任务调度触发Fault

    如下图所示:
    在这里插入图片描述
    为了解决这个问题,早起的OS大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才进行上下文切换,切换期间无法响应中断。
    这个时候,可能会有人想,既然有上面的原因限制,我能不能将SysTick的优先级设置为最低,然后在SysTick中进行上下文切换,然后任务调度呢? 答案是:可以。这一点在Cortex-M3权威指南中没有解释,反而影响了很多读者对于PendSV的理解。按照上面的思路,我们分析一下整体流程:

    Systick中断优先级低与IRQ,任务调度流程 在这里插入图片描述

    在上图中我们可以看到,当OS的Systick中断级别低于外部中断时,确实不会触发Fault,但是这带来了一个问题:

    一般OS在调度任务时,会关闭中断,也就是进入临界区,而OS任务调度是要耗时的,这就会出现一种情况:
    在任务调度期间,如果新的外部IRQ发生,CPU将不能够快速响应处理。

    将SysTick的优先级调低,避免了触发Fault的问题,但是会影响外部中断IRQ的处理速度,那有没有进一步优化的方法呢?答案就是PenSV。因为PendSV有【缓期执行】的特点,所以可以将上图中的OS拆分,分成2段:

    1. 滴答定时器中断,制作业务调度前的判断工作,不做任务切换。
    2. 触发PendSV,PendSV并不会立即执行,因为PendSV的优先级最低,如果此时正好有IRQ请求,那么先响应IRQ,最后等到所有优先级高于PendSV的IRQ都执行完毕,再执行PendSV,进行任务调度。

    Systick、PendSV优先级低,只在PendSV中进行上线文切换,任务调度

    在这里插入图片描述
    上述的流程就是目前常见的嵌入式RTOS的任务调度流程,uC/OS和FreeRTOS都会将Systick和PendSV的优先级设置为最低。
    到这里,可能会有人又有疑问,这样做是不是也有缺点:

    1. SysTick的优先级最低,那如果外部IRQ比较频繁,是不是会导致SysTick经常被挂起,然后滞后,导致Systick的节拍延长,进而导致不准啊?
    2. 因为1的原因,导致任务的执行调度就不够快了?

    答案:确实存在这些问题,但是这些问题影响面已经很小了。我们能不能将SysTick的优先级设置成最高,将PendSV的优先级设置为低,就能完美的解决上述问题,我们不妨分析下这种情况:
    在这里插入图片描述
    这样似乎解决了问题,但是又带来了一个问题,因为SysTick的优先级最高,而且又是周期性的触发,会导致经常抢占外部IRQ,这就会导致外部IRQ响应变慢,这在一些对实时性要求高的,比如按键、断电中断等待,是不能接受的,你肯定不希望你的按键扫描体验卡顿。
    所以,没有十全十美的解决方案,关键是要看我们更关注什么?对于CPU来说,嵌入式OS也是一个程序,跟普通的裸机程序是一样的,无非就是复杂一些,涉及到了手动切换堆栈、PC等高级操作而已,OS的优先级天生就没有外部中断的优先级高。

    小结

    1. PendSV是可悬起系统中断,具有【缓期执行】特征。
    2. PendSV一般被嵌入式OS用于 任务调度的上下文切换。
    3. 使用PendSV时,一般会将PendSV的优先级设置为最低,让外部IRQ先执行,等到没有外部中断后,在执行上下文切换。
    4. 嵌入式实时操作系统的【实时】概念,并不仅仅指应用程序、任务的调度实时,而是指整个系统的实时性高,具备【可剥夺】特点,当有高优先级的中断、任务具备执行条件时,立刻中断当前正在运行的任务,去执行高优先级的IRQ和高优先级任务。
    5. 嵌入式OS一般会将SysTick的优先级也设置为最低,保证外部中断IRQ优先,详细的分析,我们下一篇文章讨论。
    展开全文
  • PendSV源码简析 PendSV中断服务函数 PendSV上下文切换函数 寻找最高优先级函数 SVC异常 SVC源码简析 FreeRTOS多任务启动源码简析 vTaskStartScheduler xPortStartScheduler prvPortStartFirstTask 总结写在前面: ...
    RTOS的任务调度原理和所使用的内核中断、寄存器息息相关
    文中截图大多是《Cortex-M3与Cortex-M4权威指南》翻译版本里面的内容
    需要对内核有一定的了解,本文尽量用简单的描述表达清楚
    虽然是FreeRTOS的记录,但是原理上来说对于其他RTOS也是一样的!
    

    总结写在前面:

    在Cortex-M内核上,FreeRTOS使用Systick定时器作为心跳时钟,一般默认心跳时钟为1ms,进入Systick中断后,内核会进入处理模式进行处理,在Systick中断处理中,系统会在 ReadList 就绪链表从高优先级到低优先找需要执行的任务,进行调度,如果有任务的状态发生了变化,改变了状态链表,就会产生一个pendSV异常,进入pendSV异常,通过改变进程栈指针(PSP)切换到不同的任务。

    对于相同优先级的任务,每隔一个Systick,运行过的任务被自动排放至该优先级链表的尾部(时间片调度)

    用户也可以在线程模式下主动触发PendSV,进行任务切换。

    在FreeRTOS中SVC只使用了一次(M0中没有使用),就是第一次。

    FreeRTOS进入临界区是通过配置BASEPRI寄存器来进行的。

    Systick

    我们已经知道,在Cortex-M系列中 systick是作为FreeRTOS 的心跳时钟,是调度器的核心。

    系统是在Systick中进行上下文切换。

    那么他是如何进行上下文切换的呢,那就得来说说内核的中断管理了,记住一句话

    操作系统的入口是中断(好像是废话,嵌入式所有程序的入口都是中断= =!)

    Systick 源码解析

    Systick 初始化

    systick的初始化在port.c中, vPortSetupTimerInterrupt函数:

    /*
     * Setup the systick timer to generate the tick interrupts at the required
     * frequency.
     */
    __attribute__(( weak )) void vPortSetupTimerInterrupt( void )
    {
    	/* Calculate the constants required to configure the tick interrupt. */
    	#if( configUSE_TICKLESS_IDLE == 1 )
    	{
    		ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
    		xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
    		ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
    	}
    	#endif /* configUSE_TICKLESS_IDLE */
    
    	/* 
    	Stop and clear the SysTick.
    	清0,保证上电后的准确性 
    	*/
    	portNVIC_SYSTICK_CTRL_REG = 0UL;
    	portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
    	/* 
    	Configure SysTick to interrupt at the requested rate.
    	portNVIC_SYSTICK_LOAD_REG  systick装载值
    	portNVIC_SYSTICK_CTRL_REG  systick控制寄存器  配置系统时钟源,开启中断,使能
    	*/
    	portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    	portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
    }
    /*-----------------------------------------------------------*/
    

    Systick 中断服务函数

    每一节拍进入一次Systick 中断,因为Systick 如果调度器返回true,触发pendSV异常:

    /*-----------------------------------------------------------*/
    
    void xPortSysTickHandler( void )
    {
    	/* The SysTick runs at the lowest interrupt priority, so when this interrupt
    	executes all interrupts must be unmasked.  There is therefore no need to
    	save and then restore the interrupt mask value as its value is already
    	known.
    	进入临界区,在上面一篇文章讲过,通过配置BASEPRI寄存器,关闭的中断等级在CubeMX中设置
    	 */
    	portDISABLE_INTERRUPTS();
    	{
    		/* 
    		Increment the RTOS tick. 
    		操作系统调度接口
    		如果调度器返回true,触发pendSV异常
    		*/
    		if( xTaskIncrementTick() != pdFALSE )
    		{
    			/* 
    			A context switch is required.  Context switching is performed in
    			the PendSV interrupt.  Pend the PendSV interrupt. 
    			往中断控制及状态寄存器ICSR(地址:0xE000_ED04)的bit28写1挂起一次PendSV中断
    			触发pendSV
    			*/
    			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    		}
    	}
    	/*
    	开中断,执行pendSV
    	*/
    	portENABLE_INTERRUPTS();
    }
    

    Systick 任务调度

    Systick中断中调用xTaskIncrementTick任务调度如下,源码注释:

    /*----------------------------------------------------------*/
    
    BaseType_t xTaskIncrementTick( void )
    {
    TCB_t * pxTCB;
    TickType_t xItemValue;
    BaseType_t xSwitchRequired = pdFALSE;// 返回值,表示是否进行上下文切换
    
    	/* Called by the portable layer each time a tick interrupt occurs.
    	Increments the tick then checks to see if the new tick value will cause any
    	tasks to be unblocked. */
    	traceTASK_INCREMENT_TICK( xTickCount );
    	/*
    		uxSchedulerSuspended 表示内核调度器是否挂起
    		pdFALSE 表示内核没有挂起
    	*/
    	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    	{
    		/* 
    		Minor optimisation.  The tick count cannot change in this
    		block. 
    		tick计数增加1
    		*/
    		const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
    		/* Increment the RTOS tick, switching the delayed and overflowed
    		delayed lists if it wraps to 0. */
    		xTickCount = xConstTickCount;
    
    		/*
    		判断tick是否溢出越界
    		*/
    		if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
    		{
    			taskSWITCH_DELAYED_LISTS();//如果溢出,要更新延时列表
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    
    		/* See if this tick has made a timeout expire.  Tasks are stored in
    		the	queue in the order of their wake time - meaning once one task
    		has been found whose block time has not expired there is no need to
    		look any further down the list. 
    		当前节拍大于时间片的锁定时间
    		说明有任务需要进行调度了,时间片用完了
    		*/
    		if( xConstTickCount >= xNextTaskUnblockTime )
    		{
    			/*
    				会一直遍历整个任务延时列表,
    				找到时间片最短的任务,进行切换
    			*/
    			for( ;; )
    			{
    				/*
    				判断任务延时列表中,是否为空,
    				也就是说,有没有任务在等待调度
    				*/
    				if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
    				{
    					/* The delayed list is empty.  Set xNextTaskUnblockTime
    					to the maximum possible value so it is extremely
    					unlikely that the
    					if( xTickCount >= xNextTaskUnblockTime ) test will pass
    					next time through. 
    					如果没有任务等待,把时间片赋值为最大值,不再调度
    					*/
    					xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
    					break;
    				}
    				else
    				{
    					/* The delayed list is not empty, get the value of the
    					item at the head of the delayed list.  This is the time
    					at which the task at the head of the delayed list must
    					be removed from the Blocked state. 
    					1、从任务延时列表中,获取第一个任务控制块
    					2、延时列表,插入永远是把时间片最短的任务,放在第一个		
    					3、获取任务控制块的延时时间
    					*/
    					pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
    					xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
    					/*
    					再次判断,这个任务的时间片是否到达
    					*/
    					if( xConstTickCount < xItemValue )
    					{
    						/* It is not time to unblock this item yet, but the
    						item value is the time at which the task at the head
    						of the blocked list must be removed from the Blocked
    						state -	so record the item value in
    						xNextTaskUnblockTime. 
    						没有到达,把此任务的时间片更新为当前系统的时间片
    						*/
    						xNextTaskUnblockTime = xItemValue;
    						/*
    						直接退出,不用调度
    						*/
    						break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
    					}
    					else
    					{
    						mtCOVERAGE_TEST_MARKER();
    					}
    
    					/* 
    					It is time to remove the item from the Blocked state.
    					把任务从延时列表中移除
    					*/
    					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
    
    					/* 
    					Is the task waiting on an event also?  If so remove
    					it from the event list.
    					把任务从事件列表中移除
    					 */
    					if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
    					{
    						( void ) uxListRemove( &( pxTCB->xEventListItem ) );
    					}
    					else
    					{
    						mtCOVERAGE_TEST_MARKER();
    					}
    
    					/* 
    					Place the unblocked task into the appropriate ready
    					list. 
    					把任务添加到就绪列表中
    					*/
    					prvAddTaskToReadyList( pxTCB );
    
    					/* A task being unblocked cannot cause an immediate
    					context switch if preemption is turned off. 
    					抢占式处理
    					*/
    					#if (  configUSE_PREEMPTION == 1 )
    					{
    						/* Preemption is on, but a context switch should
    						only be performed if the unblocked task has a
    						priority that is equal to or higher than the
    						currently executing task. 
    						判断优先级是否大于当前任务
    						大于则进行调度
    						*/
    						if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
    						{
    							xSwitchRequired = pdTRUE;
    						}
    						else
    						{
    							mtCOVERAGE_TEST_MARKER();
    						}
    					}
    					#endif /* configUSE_PREEMPTION */
    				}
    			}
    		}
    
    		/* Tasks of equal priority to the currently running task will share
    		processing time (time slice) if preemption is on, and the application
    		writer has not explicitly turned time slicing off. 
    		时间片处理机制
    		1、获取就绪列表长度
    		2、就绪列表指的是,当前任务优先级的列表
    		3、如果有其他任务在就绪列表中,就开始调度
    		*/
    		#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
    		{	
    			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
    			{
    				xSwitchRequired = pdTRUE;
    			}
    			else
    			{
    				mtCOVERAGE_TEST_MARKER();
    			}
    		}
    		#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
    
    		#if ( configUSE_TICK_HOOK == 1 )
    		{
    			/* Guard against the tick hook being called when the pended tick
    			count is being unwound (when the scheduler is being unlocked). */
    			if( uxPendedTicks == ( UBaseType_t ) 0U )
    			{
    				vApplicationTickHook();
    			}
    			else
    			{
    				mtCOVERAGE_TEST_MARKER();
    			}
    		}
    		#endif /* configUSE_TICK_HOOK */
    	}
    	else //内核调度器挂起了
    	{
    		++uxPendedTicks;//挂起的tick+1
    
    		/* The tick hook gets called at regular intervals, even if the
    		scheduler is locked. */
    		#if ( configUSE_TICK_HOOK == 1 )
    		{
    			vApplicationTickHook();
    		}
    		#endif
    	}
    	/*
    	如果是抢占模式,要开启调度
    	*/
    	#if ( configUSE_PREEMPTION == 1 )
    	{
    		if( xYieldPending != pdFALSE )
    		{
    			xSwitchRequired = pdTRUE;
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    	}
    	#endif /* configUSE_PREEMPTION */
    
    	return xSwitchRequired;//返回调度器状态
    }
    

    Systick优先级分析

    结合后面的中断管理和任务调度相关的内容,需要说明一下Systick优先级的问题。先来看一下简单的任务调度模型。

    在这里插入图片描述

    在上面图示中,可以看到优先级SysTick优先级最高!那么这和我们常听到的SysTick优先级需要设置为最低优先级怎么相互冲突呢?初学者往往在这个问题上感到困惑。

    首先要明白:SysTick是中断,中断优先级和任务优先级没有任何关系,不管中断优先级是多少,中断的优先级永远高于任何线程任务的优先级

    那么在上图中的线程,不管什么线程,SysTick中断来了肯定是需要去执行SysTick中断事件的。

    上图中还有一个IRQ,比SysTick优先级低,这也是可能的,但是实际上我们应用过程中,一般都把SysTick优先级设置为最低,因为不想让SysTick中断打断用户的IRQ中断。

    那么SysTick中断优先级和外设中断优先级是怎么确定的?

    1、SysTick属于内核异常,用SHPRx(x=1.2.3)来设置其优先级;外设中断属于 ISR,用NVIC_IPRx来设置优先级。

    SPRH1-SPRH3是一个32位的寄存器,只能通过字节访问,每8个字段控制着一个内核外设的中断优先级的配置。位7:4这高四位有效,所以可编程为0 ~ 15。如果软件优先级配置相同,那就根据他们在中断向量表里面的位置编号来决定优先级大小,编号越小,优先级越高。

    对于SysTick的配置,系统默认配置为15,(1UL << __NVIC_PRIO_BITS) - 1UL) 在m3、m4中__NVIC_PRIO_BITS为4:

    /*
    core_cm0/3/4.h中关于SysTick_Config
     m3 m4 中是 4 ,4位就是0~15
      #define __NVIC_PRIO_BITS          4U
     m0 中是2, 2位就是 0 ~ 3 
      #endif
      */
    __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
    {
      if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
      {
        return (1UL);                                                   /* Reload value impossible */
      }
    
      SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set reload register */
      NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
      SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
      SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                       SysTick_CTRL_TICKINT_Msk   |
                       SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
      return (0UL);                                                     /* Function successful */
    }
    
    /*
    NVIC_SetPriority对中断分了类,分内核中断和外设中断,
    内核外设中断枚举值小于0,普通外设>=0。其中,SysTick_IRQn = -1。
    */
    __STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
    {
      if ((int32_t)(IRQn) < 0)
      {
        SCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL)-4UL] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
      }
      else
      {
        NVIC->IP[((uint32_t)(int32_t)IRQn)]               = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
      }
    }
    

    2、NVIC的中断优先级分组不仅对片上外设有效,同样对内核的外设也有效。

    systick的优先级15转换成二进制值就是1111,又因为NVIC的优先级分组2,那么前两位的11就是3,3抢占,后两位的11也是3,3子优先级。这样就可以和外设的优先级进行对比。

    如果外设中断的优先级也分成了15,无论怎么分组,SYSTICK优先级高于同优先级的外设(毕竟内核异常优先级高于外设中断,因为中断向量表里面的位置编号内核的靠前更小)。

    3、设置systick优先级的方法NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 15);SCB->SHP[11] = 0x00;设置最高的话可以得到精准延时,但是会频繁打断用户使用的中断程序,不建议。

    内核中断管理

    中断是微处理器外部发送的,通过中断通道送入处理器内部,一般是硬件引起的;
    而异常通常是微处理器内部发生的,大多是软件引起的,比如除法出错异常,特权调用异常。

    Cortex-M的异常类型

    如下图:

    在这里插入图片描述

    Cortex-M的寄存器

    如下图:
    在这里插入图片描述

    这个图主要记住 R13 寄存器,有两个指针:MSP: 主栈指针PSP: 进程栈指针,相关说明如下:

    在这里插入图片描述

    Cortex-M的特殊寄存器

    如下图:

    在这里插入图片描述

    xPSR

    组合程序状态寄存器,该寄存器由三个程序状态寄存器组成

    应用PSR(APSR) : 包含前一条指令执行后的条件标志
    中断PSR(IPSR) : 包含当前ISR的异常编号
    执行PSR(EPSR) : 包含Thumb状态位

    在这里插入图片描述

    PRIMSK

    PRIMSK:中断屏蔽特殊寄存器。

    利用PRIMSK,可以禁止除HardFault 和 NMI外的所有异常。

    在这里插入图片描述

    在这里插入图片描述

    BASEPRI

    利用BASEPRI寄存器来选择屏蔽低于特定优先级的异常或中断。(在上一篇博文中的进入临界区所使用的寄存器就是这个寄存器)

    在这里插入图片描述

    CONTROL

    CONTROL:控制寄存器,部分介绍如下:

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    Cortex-M的工作模式

    Cortex-M有两种工作模式和两种工作状态:

    线程模式(Thread Mode):芯片复位后,进入线程模式,执行用户程序;

    处理模式(Handler Mode):当处理器发生了异常或者中断,则进入处理模式,处理完后返回线程模式。

    Thumb状态: 正常运行时处理器的状态

    调试状态:调试程序时处理器的状态

    在这里插入图片描述

    进入Systick后,发生异常,则进入处理模式进行处理:

    如果是裸机编程,从哪里进去就返回哪里

    但是用了操作系统,该返回哪里呢?

    所以这里就有必要单独讲解下MSP和PSP

    影子栈指针

    在上面的Cortex-M的寄存器图中我们标注过R13寄存器:

    堆栈指针SP。

    在处理模式下,只能使用主堆栈(MSP)。

    在线程模式下,可以使用主堆栈也可以使用进程栈。

    由 CONTROL 寄存器控制,如下:

    在这里插入图片描述在这里插入图片描述

    PendSV和SVC异常

    PendSV异常用于任务切换。

    为了保证操作系统的实时性,除了使用Systick的时间片调度,还得加入pendSV异常加入抢占式调度。

    PendSV(可挂起的系统调用),异常编号为14,可编程。可以写入中断控制和状态寄存器(ICSR)设置挂起位以触发 PendSV异常。它是不精确的。因此,它的挂起状态可以在更高优先级异常处理内设置,且会在高优先级处理完成后执行。

    为什么需要 PendSV异常?

    如下图所示,如果中断请求在Systick异常前产生,则Systick可能会抢占IRQ处理(图中的IRQ优先级小于Systick)。这样执行上下文切换会导致IRQ延时处理,这种行为在任何一种实时操作系统中都是不能容忍的,在CortexM3中如果OS在活跃时尝试切入线程模式,将触发Fault异常。

    在这里插入图片描述

    为了解决上面的问题,使用了 PendSV异常。 PendSV异常会自动延迟上下文切换的请求,直到其他的eISR都完成了处理后才放行。为实现这个机制,需要把 PendSV编程为最低优先级的异常。

    在FreeRTOS中,每一次进入Systick中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂PendSV异常,来缓期执行上下文切换。

    如下:

    在这里插入图片描述

    在Systick中会挂起一个PendSV异常用于上下文切换,每产生一个Systick,系统检测到任务链表变化都会触发一个PendSV如下图:

    在这里插入图片描述

    PendSV业务流程

    中断过程中不但要像一般的C函数调用一样保存(R0-R3,R12,LR,PSR),还要保存中断返回地址(return address)。中断的硬件机制会把EXC_RETURN放进LR,在中断返回时触发中断返回。

    如下图:

    在这里插入图片描述
    如何触发PendSV异常

    触发PendSV异常,向PendSV中断寄存器写1,触发一次PendSV异常。用户可以主动调用portYIELD函数进行任务切换,portYIELD函数如下:

    /* Scheduler utilities. */
    #define portYIELD() 															\
    {																				\
    	/* Set a PendSV to request a context switch. */								\
    	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
    																				\
    	/* Barriers are normally not required but do ensure the code is completely	\
    	within the specified behaviour for the architecture. */						\
    	__asm volatile( "dsb" ::: "memory" );										\
    	__asm volatile( "isb" );													\
    }
    
    

    PendSV源码简析

    PendSV中断服务函数

    我这里使用的是M0 内核中 FreeRTOS的源码xPortPendSVHandler

    void xPortPendSVHandler( void )
    {
    	/* This is a naked function.
    		1.产生PendSV中断,硬件自动保存栈帧到任务A的栈中
    		2.读取当前任务A的栈指针PSP,手动把一些寄存器压栈到当前任务栈。
    		3.把当前任务A栈顶指针保存到任务A的任务控制块中。
    		4.找到下一个任务B的任务控制块。(查找下一个优先级最高的就绪任务)
    		5.把任务B控制块的栈顶指针指向的数据弹出到寄存器中
    		6.更新PSP为任务B的栈顶指针。
    		7.跳出PendSV中断。
    		8.硬件自动弹出任务B栈中的栈帧。
     */
    	__asm volatile
    	(
    	"	.syntax unified						\n" 
    	"	mrs r0, psp							\n"/*将psp值放到r0,此时sp得值为msp*/
    	"										\n"
    	"	ldr	r3, pxCurrentTCBConst			\n" /* Get the location of the current TCB. 获取当前任务控制块,其实就获取任务栈顶 */
    	"	ldr	r2, [r3]						\n"/*将r3寄存器值作为指针取内容存到r2,此时r2保存的为任务控制块首地址*/
    	"										\n"
    	"	subs r0, r0, #32					\n" /* Make space for the remaining low registers. */
    	"	str r0, [r2]						\n" /* Save the new top of stack. */
    	"	stmia r0!, {r4-r7}					\n" /* Store the low registers that are not saved automatically. */
    	" 	mov r4, r8							\n" /* Store the high registers. */
    	" 	mov r5, r9							\n"
    	" 	mov r6, r10							\n"
    	" 	mov r7, r11							\n"
    	" 	stmia r0!, {r4-r7}					\n"
    	"										\n"
    	"	push {r3, r14}						\n"
    	"	cpsid i								\n"
    	"	bl vTaskSwitchContext				\n"/*执行上线文切换*/
    	"	cpsie i								\n"
    	"	pop {r2, r3}						\n" /* lr goes in r3. r2 now holds tcb pointer. */
    	"										\n"
    	"	ldr r1, [r2]						\n"
    	"	ldr r0, [r1]						\n" /* The first item in pxCurrentTCB is the task top of stack. */
    	"	adds r0, r0, #16					\n" /* Move to the high registers. */
    	"	ldmia r0!, {r4-r7}					\n" /* Pop the high registers. */
    	" 	mov r8, r4							\n"
    	" 	mov r9, r5							\n"
    	" 	mov r10, r6							\n"
    	" 	mov r11, r7							\n"
    	"										\n"
    	"	msr psp, r0							\n" /* Remember the new top of stack for the task.记住新的栈顶指针 */
    	"										\n"
    	"	subs r0, r0, #32					\n" /* Go back for the low registers that are not automatically restored. */
    	" 	ldmia r0!, {r4-r7}					\n" /* Pop low registers.  */
    	"										\n"
    	"	bx r3								\n"
    	"										\n"
    	"	.align 4							\n"
    	"pxCurrentTCBConst: .word pxCurrentTCB	  "
    	);
    }
    

    PendSV上下文切换函数

    xPortPendSVHandler中调用的上下文切换vTaskSwitchContext,其核心任务就是找到当前处于就绪态的最高优先级的任务:

    void vTaskSwitchContext( void )
    {
    	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    	{
    		/* The scheduler is currently suspended - do not allow a context
    		switch. 
    		标记调度器状态
    		*/
    		xYieldPending = pdTRUE;
    	}
    	else
    	{
    		xYieldPending = pdFALSE;
    		traceTASK_SWITCHED_OUT();
    
    		#if ( configGENERATE_RUN_TIME_STATS == 1 )
    		{
    		}
    		#endif /* configGENERATE_RUN_TIME_STATS */
    
    		/* 
    		Check for stack overflow, if configured. 
    		检查任务栈是否溢出
    		*/
    		taskCHECK_FOR_STACK_OVERFLOW();
    
    		/* 
    		Select a new task to run using either the generic C or port
    		optimised asm code. 
    		选择优先级最高的任务,把当前的任务控制块进行赋值
    		*/
    		taskSELECT_HIGHEST_PRIORITY_TASK();
    		traceTASK_SWITCHED_IN();
    
    		#if ( configUSE_NEWLIB_REENTRANT == 1 )
    		{
    		}
    		#endif /* configUSE_NEWLIB_REENTRANT */
    	}
    }
    

    寻找最高优先级函数

    上下文切换vTaskSwitchContext中调用了taskSELECT_HIGHEST_PRIORITY_TASK()寻找最高优先级的任务:

    taskSELECT_HIGHEST_PRIORITY_TASK()的硬件方式:

    	/*-----------------------------------------------------------*/
    	/*
    	这里注释解释
    	#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) ucPortCountLeadingZeros( ( uxReadyPriorities ) ) )
    
    	#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );} 
    
    	#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
    	{																							\
    	List_t * const pxConstList = ( pxList );		
    		// pxIndex 为上一个任务索引,下一个要执行的即pxNext											\
    		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\
    		if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\
    		{		
    			// 由于是环形列表,切默认有一个结束项(xListEnd),如果pxIndex刚好为最后一项,则再指向后面一项																				\
    			( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\
    		}																						\
    		( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											\
    	}
    	由于相同优先级的任务可能会存在多个,需要从就绪任务列表中找到位于最前面的任务
    	将其赋值给pxCurrentTCB。
    	*/
    	#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
    	{																								\
    	UBaseType_t uxTopPriority;																		\
    																									\
    		/* Find the highest priority list that contains ready tasks. */								\
    		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
    		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
    		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
    	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
    

    taskSELECT_HIGHEST_PRIORITY_TASK()的通用方式:

    	/*-----------------------------------------------------------*/
    
    	#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
    	{
    	/* uxTopReadyPriority 在每次把任务添加到就绪列表的时候会更新*/ 										\
    	UBaseType_t uxTopPriority = uxTopReadyPriority;														\
    																										\
    		/* Find the highest priority queue that contains ready tasks. 
    		一个优先级一个列表,查看当前最高优先级就绪列表下是否有任务 
    		*/																								\
    		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
    		{																								\
    			configASSERT( uxTopPriority );																\
    			/* 如果当前最高优先级就绪列表没任务就查看下一个优先级列表 */ 									\
    			--uxTopPriority;																			\
    		}																								\
    																										\
    		/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of						\
    		the	same priority get an equal share of the processor time. 
    		 获取下一个优先级最高任务的任务控制块*/															\
    		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
    		uxTopReadyPriority = uxTopPriority;																\
    	} /* taskSELECT_HIGHEST_PRIORITY_TASK */
    

    SVC异常

    SVC(请求管理调用),异常编号为11,可编程。SVC产生的中断必须立即得到相应,否则将触发硬Fault。

    系统调用处理异常,用户与内核进行交互,用户想做一些内核相关功能的时候必须通过SVC异常,让内核处于异常模式,才能调用执行内核的源码。触发SVC异常,会立即执行SVC异常代码。

    下面的启动源码简析中我们可以知道系统在启动调度器函数vTaskStartSchedulerp最后运行到 rvPortStartFirstTask中会调用SVC并启动第一个任务。

    为什么要用SVC启动第一个任务?

    因为使用了OS,任务都交给内核。总不能像裸机调用普通函数一样启动一个任务。

    M4只在上电的触发SVC异常,在SVC异常中启动第一个任务,只上电运行一次,M0上没有。

    在这里插入图片描述

    SVC源码简析

    M0上面没用,特意生成了一个M4的来看看源码vPortSVCHandler

    void vPortSVCHandler( void )
    {
    	__asm volatile (
    					/* 获取当前任务控制块. 
    					任务控制块的第一成员是------任务的栈顶
    					获取到栈顶之后,剩下的事就是出栈工作
    					出栈--------任务的堆栈
    					*/
    					"	ldr	r3, pxCurrentTCBConst2		\n" /* Restore the context. */
    					"	ldr r1, [r3]					\n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
    					"	ldr r0, [r1]					\n" /* The first item in pxCurrentTCB is the task top of stack. */
    					"	ldmia r0!, {r4-r11, r14}		\n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. 出栈内核寄存器 R14其实就是异常返回值
    					表示异常退出后,使用PSP*/
    					"	msr psp, r0						\n" /* Restore the task stack pointer.更新栈指针到PSP */
    					"	isb								\n"
    					"	mov r0, #0 						\n"
    					"	msr	basepri, r0					\n"/* 把basepri赋值为0,打开屏蔽中断 */
    					"	bx r14							\n"
    					"									\n"
    					"	.align 4						\n"
    					"pxCurrentTCBConst2: .word pxCurrentTCB				\n"
    				);
    				/*
    				为什么没有恢复其他寄存器????其他在出栈的时候就会自动恢复(由硬件处理)
    				最终跳转到任务的执行函数里面
    				*/
    }
    

    FreeRTOS多任务启动源码简析

    通过main.c中的main函数调用了osKernelStart();

    osStatus osKernelStart (void)
    {
      vTaskStartScheduler();
      
      return osOK;
    }
    

    vTaskStartScheduler

    创建空闲任务,启动任务调度器vTaskStartScheduler

    void vTaskStartScheduler( void )
    {
    BaseType_t xReturn;
    
    	/* 
    	Add the idle task at the lowest priority. 
    	下面部分根据配置的支持动态任何还是静态任务
    	创建一个优先级最低的空闲任务
    	*/
    	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
    	{
    		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
    		StackType_t *pxIdleTaskStackBuffer = NULL;
    		uint32_t ulIdleTaskStackSize;
    
    		/* The Idle task is created using user provided RAM - obtain the
    		address of the RAM then create the idle task. */
    		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
    		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
    												configIDLE_TASK_NAME,
    												ulIdleTaskStackSize,
    												( void * ) NULL, /*lint !e961.  The cast is not redundant for all compilers. */
    												portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
    												pxIdleTaskStackBuffer,
    												pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
    
    		if( xIdleTaskHandle != NULL )
    		{
    			xReturn = pdPASS;
    		}
    		else
    		{
    			xReturn = pdFAIL;
    		}
    	}
    	#else
    	{
    		/* The Idle task is being created using dynamically allocated RAM. */
    		xReturn = xTaskCreate(	prvIdleTask,
    								configIDLE_TASK_NAME,
    								configMINIMAL_STACK_SIZE,
    								( void * ) NULL,
    								portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
    								&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
    	}
    	#endif /* configSUPPORT_STATIC_ALLOCATION */
    
    	/*
    	如果使能了软件定时器,还会创建一个软件定时器
    	*/
    	#if ( configUSE_TIMERS == 1 )
    	{
    		if( xReturn == pdPASS )
    		{
    			xReturn = xTimerCreateTimerTask();
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    	}
    	#endif /* configUSE_TIMERS */
    
    	if( xReturn == pdPASS )
    	{
    		/* freertos_tasks_c_additions_init() should only be called if the user
    		definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
    		the only macro called by the function. */
    		#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
    		{
    			freertos_tasks_c_additions_init();
    		}
    		#endif
    
    		/* Interrupts are turned off here, to ensure a tick does not occur
    		before or during the call to xPortStartScheduler().  The stacks of
    		the created tasks contain a status word with interrupts switched on
    		so interrupts will automatically get re-enabled when the first task
    		starts to run. 
    		此处关闭中断,以确保不会发生滴答声
            在呼叫 xPortstarts 计划之前或期间 ()。 堆栈
            创建的任务包含打开中断的状态字
            因此,当第一个任务完成时,中断会自动重新启用
            开始运行。
    		*/
    		portDISABLE_INTERRUPTS();
    
    		#if ( configUSE_NEWLIB_REENTRANT == 1 )
    		{
    			/* Switch Newlib's _impure_ptr variable to point to the _reent
    			structure specific to the task that will run first. */
    			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
    		}
    		#endif /* configUSE_NEWLIB_REENTRANT */
    
    		/*
    		下一个任务锁定时间赋值为最大
    		不让时间片进行调度
    		*/
    		xNextTaskUnblockTime = portMAX_DELAY; 
    		xSchedulerRunning = pdTRUE; 				//调度器的运行状态置位,标记开始运行了
    		xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; 	//初始化 系统的节拍值为0
    
    		/* If configGENERATE_RUN_TIME_STATS is defined then the following
    		macro must be defined to configure the timer/counter used to generate
    		the run time counter time base.   NOTE:  If configGENERATE_RUN_TIME_STATS
    		is set to 0 and the following line fails to build then ensure you do not
    		have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
    		FreeRTOSConfig.h file. */
    		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
    
    		traceTASK_SWITCHED_IN();
    
    		/* Setting up the timer tick is hardware specific and thus in the
    		portable interface. 
    		启动调度器
    		*/
    		if( xPortStartScheduler() != pdFALSE )
    		{
    			/* Should not reach here as if the scheduler is running the
    			function will not return. */
    		}
    		else
    		{
    			/* Should only reach here if a task calls xTaskEndScheduler(). */
    		}
    	}
    	else
    	{
    		/* This line will only be reached if the kernel could not be started,
    		because there was not enough FreeRTOS heap to create the idle task
    		or the timer task. 
    		只有在无法启动内核时才能到达此行,
            因为没有足够的自由 BrTAS 堆来创建空闲任务
            或时间器任务。
    		*/
    		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    	}
    
    	/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
    	meaning xIdleTaskHandle is not used anywhere else. */
    	( void ) xIdleTaskHandle;
    }
    

    xPortStartScheduler

    启动调度器xPortStartScheduler

    /*
     * See header file for description.
     */
    BaseType_t xPortStartScheduler( void )
    {
    	/*
    	前面一大段不用深入了解,先不看,我们找到配置 systick pendsv开始 
    	*/
    	/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
    	See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
    	configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
    
    	/* This port can be used on all revisions of the Cortex-M7 core other than
    	the r0p1 parts.  r0p1 parts should use the port from the
    	/source/portable/GCC/ARM_CM7/r0p1 directory. */
    	configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    	configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
    
    	#if( configASSERT_DEFINED == 1 )
    	{
    		volatile uint32_t ulOriginalPriority;
    		volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
    		volatile uint8_t ucMaxPriorityValue;
    
    		/* Determine the maximum priority from which ISR safe FreeRTOS API
    		functions can be called.  ISR safe functions are those that end in
    		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
    		ensure interrupt entry is as fast and simple as possible.
    
    		Save the interrupt priority value that is about to be clobbered. */
    		ulOriginalPriority = *pucFirstUserPriorityRegister;
    
    		/* Determine the number of priority bits available.  First write to all
    		possible bits. */
    		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
    
    		/* Read the value back to see how many bits stuck. */
    		ucMaxPriorityValue = *pucFirstUserPriorityRegister;
    
    		/* Use the same mask on the maximum system call priority. */
    		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
    
    		/* Calculate the maximum acceptable priority group value for the number
    		of bits read back. */
    		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
    		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
    		{
    			ulMaxPRIGROUPValue--;
    			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
    		}
    
    		#ifdef __NVIC_PRIO_BITS
    		{
    			/* Check the CMSIS configuration that defines the number of
    			priority bits matches the number of priority bits actually queried
    			from the hardware. */
    			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
    		}
    		#endif
    
    		#ifdef configPRIO_BITS
    		{
    			/* Check the FreeRTOS configuration that defines the number of
    			priority bits matches the number of priority bits actually queried
    			from the hardware. */
    			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
    		}
    		#endif
    
    		/* Shift the priority group value back to its position within the AIRCR
    		register. */
    		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
    		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
    
    		/* Restore the clobbered interrupt priority register to its original
    		value. */
    		*pucFirstUserPriorityRegister = ulOriginalPriority;
    	}
    	#endif /* conifgASSERT_DEFINED */
    
    	/* 
    	Make PendSV and SysTick the lowest priority interrupts. 
    	配置 systick pendsv为最低的优先级,为了保证系统的实时性
    	*/
    	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
    	/* 
    	Start the timer that generates the tick ISR.  Interrupts are disabled
    	here already.
    	1、初始化systick----》配置为1ms的中断产生时基
    	2、开启systick中断 
    	*/
    	vPortSetupTimerInterrupt();
    
    	/* 
    	Initialise the critical nesting count ready for the first task. 
    	初始化关键嵌套计数,为第一个任务做好准备。
    	临界段?
    	*/
    	uxCriticalNesting = 0;
    
    	/*
    	Ensure the VFP is enabled - it should be anyway. 
    	初始化浮点数寄存器 M4特有,需要初始化
    	*/
    	vPortEnableVFP();
    
    	/* Lazy save always. */
    	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
    
    	/* Start the first task. 启动第一个任务 */
    	prvPortStartFirstTask();
    
    	/* Should never get here as the tasks will now be executing!  Call the task
    	exit error function to prevent compiler warnings about a static function
    	not being called in the case that the application writer overrides this
    	functionality by defining configTASK_RETURN_ADDRESS.  Call
    	vTaskSwitchContext() so link time optimisation does not remove the
    	symbol. */
    	vTaskSwitchContext();
    	prvTaskExitError();
    
    	/* Should not get here! */
    	return 0;
    }
    

    prvPortStartFirstTask

    启动第一个任务prvPortStartFirstTask

    static void prvPortStartFirstTask( void )
    {
    	/* Start the first task.  This also clears the bit that indicates the FPU is
    	in use in case the FPU was used before the scheduler was started - which
    	would otherwise result in the unnecessary leaving of space in the SVC stack
    	for lazy saving of FPU registers. */
    	__asm volatile(
    					/* 
    						0xE000ED08 它是中断向量表的一个地址
    						它存储的是MSP的指针
    						最终获取到MSP的RAM的地址
    					*/
    					" ldr r0, =0xE000ED08 	\n" /* Use the NVIC offset register to locate the stack. */
    					" ldr r0, [r0] 			\n"
    					" ldr r0, [r0] 			\n"
    					/* 
    						Set the msp back to the start of the stack. 
    						重新把MSP的地址,赋值为MSP 
    						为什么需要加这一步,如果我们有在线升级功能
    						使用了我们用户的Bootloder,
    						中断向量表会更新,所以要重新赋值MSP
    					*/
    					" msr msp, r0			\n" /* Set the msp back to the start of the stack. 把MSP的地址,赋值为MSP */
    					" mov r0, #0			\n" /* Clear the bit that indicates the FPU is in use, see comment above. */
    					" msr control, r0		\n"
    					" cpsie i				\n" /* Globally enable interrupts. 开启全局中断 */
    					" cpsie f				\n"
    					" dsb					\n"
    					" isb					\n"
    					" svc 0					\n" /* System call to start first task. 调用SVC */
    					" nop					\n"
    				);
    }
    /*-----------------------------------------------------------*/
    
    展开全文
  • FreeRTOS任务切换——PendSV

    千次阅读 2021-01-12 21:58:42
    文章目录 前言: 一、SVC和PendSV 1.1 异常与中断 1.2 SVC和PendSV 1.2.1 SVC 1.2.1 PendSV 1.2 为什么在PendSV异常中进行任务切换 二、任务切换场景以及时间片调度 2.1 执行系统调用 2.2 SysTick中断 2.2 FreeRTOS...
  • PendSV和SVC一般用于OS,SVC用于产生系统函数的调用请求,从而导致内核的异常处理函数被调用,进而去使用内核的服务。 - 使用`SVC #VAL`指令即可触发SVC异常,进入Handler模式——特权级,其中`#VAL`立即数一般作为...
  • PendSV异常,解析三个问题:怎么触发PendSV异常?何时使用MSP何时切换PSP?PendSV如何实现上下文切换? 1. 触发PendSV异常 在RTOS内核中,任务切换的原理是:手动触发PendSV异常,在PendSV异常服务函数中实现任务...
  • 首先应该明确PendSV和SysTick的优先级应该设置为最低,具体原因参见这一篇博客 PendSV功能,为什么需要PendSV 设置优先级在函数port.c中的xPortStartScheduler()函数中实现的 BaseType_t xPortStartScheduler( void ...
  • 三、任务切换之PendSV异常

    千次阅读 2019-09-06 16:16:07
    文章目录PendSV异常1. 没有PendSV异常的任务切换2. 有PendSV异常的任务切换2.1 系统调用引起的任务切换2.2 systick中断引起任务切换2.3 PendSV异常处理函数2.4 寻找下一个要运行的任务2.5 时间片调度 PendSV异常 &...
  • RTOS系列文章(2):PendSV功能,为什么需要PendSV RTOS系列文章(3): 为什么将SysTick和PendSV的优先级设置为最低 RTOS系列文章(4): MDK软件仿真 + Debug-(printf)-Viewer使用方法 RTOS系列文章(5):C语言程序...
  • 有了Systick中断为什么还要PendSV中断?

    千次阅读 多人点赞 2021-06-10 22:23:55
    然后在SysTick中断中触发PendSV中断,实际的任务切换是在PendSV的中断服务函数中完成的。 2、PendSV的中断服务函数一般用汇编来写。 那为什么不直接在SysTick的中断服务函数里完成任务切换呢? 其中原因如下: 原因...
  • 2.PendSV的触发

    2020-02-22 15:34:39
    PendSV典型使用场合是在上下文切换时(在不同任务之间切换)。 我们先简单的写几段代码实现PendSV的中断触发,当然也会涉及到CM3内核汇编指令,自从开始挑战的那天起,你不如地狱谁入地狱! 如何触发PendSV中断呢?...
  • ARM Contents 线程的核心 创建线程 启动线程 PendSV异常的初始化. PendSV异常触发函数. PendSV异常服务例程. 线程第一次启动函数 切换线程 线程轮询调度测试 定义线程栈 定义线程入口函数 总结与展望 1 线程的核心 ...
  • FreeRTOS系列文章(2):PendSV功能,为什么需要PendSV 前言 在上一篇文章中,我们详细分析了PendSV的功能,也分析了SysTick和PendSV结合,实现OS任务调度,简单的分析了SysTick的优先级。我觉得有必要针对SysTick的...
  • Cortex-M3 PendSV 中断 系统调用 说明

    千次阅读 2019-09-29 14:05:18
    PendSV异常是和系统调用有些类似,cpu 需要手动将往NVIC 的PendSV 悬起寄存器中写1,然后产生中断, 系统调用(SVC)是cortex-M3 CPU执行 SVC指令之后产生中断(arm中一般是SWI 指令),都是软件的方式产生的中断,...
  • PendSV中断服务函数

    千次阅读 2019-09-23 14:44:38
    之前在系统滴答定时器中断服务函数中调用API函数xPortSysTickHandler(),xPortSysTickHandler()函数中通过向中断和状态寄存器的bit28写入1来启动PendSV中断,具体PendSV中断服务函数是 PendSV_Handler,并且任务...
  • 悬起 PendSV 的方法是:手工往 NVIC的 PendSV悬起寄存器中写 1。悬起后,如果优先级不够 高,则将缓期等待执行。 RTOS就是利用了这个特性,来显示 任务切换的。请看下面systick异常中断服务函数的内容 void ...
  • 1.SVC和PENDSV基本概念 SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,...
  • 手写RTOS-PendSV中断

    2019-07-11 19:26:08
    今天这一篇,我们说一下操作系统都要用到的PendSV中断,整个操作系统中,要自己写的的汇编代码不超过20行,全部都在PendSV中断里。 以下是《Cotex-M3权威指南》里对PendSV的描述: 它是可以像普通的中断一样被悬...
  • uCOS-Ⅲ中PendSV任务切换详细分析

    千次阅读 2019-10-24 13:05:56
    当调用OSStartHighRdy()函数,会触发PendSV异常后,就需要编写PendSV异常服务函数,然后在里面进行任务的切换。 PendSV异常服务中主要完成两个工作,一是保存上文,即保存当前正在运行的任务的环境参数;二是切换...
  • 什么是 PendSV

    千次阅读 2019-04-28 16:43:40
    PendSV典型使用场合是上下文切换时(在不同任务之间切换)上下文切换被触发的场合可以是: (1):执行一个系统调用。 比如 OSSched(); // 执行任务调用。 (2):系统滴答定时器(SYSTICK)中断,(轮转...
  • SVC和PendSV

    千次阅读 2019-03-25 10:52:19
    1.SVC SVC(Supervisor Call)指令用于产生一个SVC异常。它是用户模式代码中的主进程,用于创造对特权操作 ...由于PendSV在系统中被设置为最低优先级,因此只有当没有其他异常或者中断在执行时才会被执行。
  • 怎样去理解异常SVC和PendSV

    千次阅读 2020-08-07 10:34:41
    什么是SVC和PendSV SVC(系统服务调用)和 PendSV(可悬挂系统调用)。 它们多用于在操作系统之上的软件开发中。 SVC 用于产生系统函数的调用请求。
  • PendSV异常是?  ucos 任务切换时机?  ucos 如何满足实时性(实现)?  ucos中,systick的优先级? SVC和PendSV SVC(系统服务调用,亦简称系统调用)和PendSV(可悬起系统调用),它们多用于在操作系统之上的...
  • 当调用OSStartHighRdy()函数,会触发PendSV异常后,就需要编写PendSV异常(可以手动触发,也叫软中断)服务函数,然后在里面进行任务的切换。 PendSV异常服务中主要完成两个工作,一是保存上文,即保存当前正在...
  • PendSV_Handler

    千次阅读 2019-10-18 17:29:40
    参考野火的程序: ; OSTCBCurPtr = OSTCBHighRdyPtr; LDR R0, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令 分析:运行后R0=0x20000018=&OSTCBCurPtr ...LDR R1, = OSTCBHighRdyPtr ...
  • 什么是PendSV

    2019-03-16 21:15:42
    一、什么是PendSV PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它。更详细的内容在《Cortex-M3 权威指南》里有介绍,...
  • SysTick和pendSV的中断优先级分别设置为0和15,这样的设置可能会存在一个问题,SysTick优先级太高会抢占用户自己设置的外设的中断。 代码证明: RT-Thread中设置pendSV中断优先级的代码在cpu文件夹下context_rvds.S...
  • UC/OS-III学习——触发PendSV中断系列文章目录前言一、关于PendSV的基础知识二、代码1.c语言2.汇编语言 前言 PendSV典型使用场合是在上下文切换时(在不同任务之间切换)。本文主要介绍触发PendSv中断的两种代码,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,925
精华内容 1,570
关键字:

pendsv