ARM-Cortex-M3任务创建与切换(简易板)
    
    芯片:GD32F103C8T6
    
    编译环境:DMK Keil5
    
    参考资料:
    
    《FreeRTOS 内核实现与应用开发实战—基于STM32》—野火
    
    《Cortex-M3权威指南(中文)》
    
    《STM32F3与F4系列Cortex M4内核编程手册》
   
    
    
    一、任务创建
   
    
    
    1、申请内存空间
   
    
    
    栈内存:
   
    #define TASK1_STACK_SIZE 128
    
    StackType_t Task1Stack[TASK1_STACK_SIZE];
    
    #define TASK2_STACK_SIZE 128
    
    StackType_t Task2Stack[TASK2_STACK_SIZE];
   
    
    
    任务句柄和任务控制块:
   
    typedef void * TaskHandle_t;
    
    typedef struct tskTaskControlBlock
    
    {
    
    
    volatile StackType_t
    
     pxTopOfStack; /
    
    栈顶
    
     /
     
     ListItem_t xStateListItem; /
    
    任务节点 */
    
    StackType_t
    
     pxStack; /
    
    任务栈起始地址
    
     /
     
     char pcTaskName[ configMAX_TASK_NAME_LEN ];/
    
    任务名称,字符串形式 */
    
    } tskTCB;
    
    typedef tskTCB TCB_t;
    
    TaskHandle_t Task1_Handle;
    
    TCB_t Task1TCB;
    
    TaskHandle_t Task2_Handle;
    
    TCB_t Task2TCB;
   
    
    
    2、创建任务函数:
   
    static void LED1_Task(void *parameter)
    
    {
    
    
    while(1)
    
    {
    
    
    gpio_bit_reset(GPIOA,GPIO_PIN_4);//LED off
    
    delay_1ms(1000);
    
    gpio_bit_set(GPIOA,GPIO_PIN_4); //LED on
    
    delay_1ms(1000);
    
    taskYIELD();//任务切换,这里是手动切换
    
    }
    
    }
   
    
    
    3、初始化任务1内存数据
   
    
    
    将栈其实地址与任务控制块关联:
   
Task1TCB.pxStack = ( StackType_t * ) Task2Stack;
    
    
    获取栈顶地址并8字节对齐:
   
    StackType_t *pxTopOfStack;
    
    //获取栈顶地址
    
    pxTopOfStack = Task1TCB.pxStack + ( TASK1_STACK_SIZE – ( uint32_t ) 1 );
    
    //向下做 8 字节对齐
    
    pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
   
    
    
    将任务的名字存储在 TCB 中
   
    
    
    设置 xStateListItem 节点的拥有者
   
listSET_LIST_ITEM_OWNER( &(  Task1TCB.xStateListItem ),  Task1TCB );
    
    
    模拟的异常响应序列,开始自动入栈,即把xPSR, PC, LR, R12, R3‐R0压入堆栈:
   
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR;	/* xPSR(状态字寄存器)的位24置1,其他位为0。Bit24(T: Thumb state bit.),在 CM3 中 T 位必须是 1,所*/
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t )  LED1_Task ) & 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. */
    
    
    将任务控制块添加到就绪列表
   
    vListInsertEnd( &( pxReadyTasksLists
    
     1
    
    ),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
   
    
    
    4、初始化任务2内存数据(同上)
   
    
    
    二、启动调度
   
    
    
    1、配置 PendSV 和 SysTick 的中断优先级为最低
   
    #define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
    
    #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
    
    #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
    
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
   
    
    
    2、开始第一个任务
   
    __asm void prvStartFirstTask( void )
    
    {
    
    
    PRESERVE8//当前栈需按照 8 字节对齐
    
    /* 向量表偏移寄存器0xE000ED08,里面存放的是向量表的起始地址,即 MSP 的地址
    
    —-《STM32F3与F4系列Cortex M4内核编程手册》P212
    
     /
     
     ldr r0, =0xE000ED08
     
     ldr r0, [r0]
     
     ldr r0, [r0]
     
     /
    
    设置主堆栈指针 msp 的值
    
     /
     
     msr msp, r0
     
     /
    
    使能全局中断
    
     /
     
     cpsie i//等同于PRIMASK=0,快速开中断
     
     cpsie f//等同于FAULTMASK=0,快速开异常
     
     dsb//数据同步屏障指令。它的作用是等待所有前面的指令完成后再执行后面的指令。
     
     isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
     
     /
    
    调用 SVC 去启动第一个任务 */
    
    svc 0
    
    nop
    
    nop
    
    }
   
    
    
    3、执行SCV调用
   
    SCV中断中完成了:将Task1TCB栈中的相应内容加载到r4-r11;将psp指向Task1TCB栈;打开所有中断;设置r14 为 0xFFFFFFFD,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态;退出中断到任务1中。
    
    #define vPortSVCHandler SVC_Handler
    
    pxCurrentTCB=Task1TCB;
    
    __asm void vPortSVCHandler(void)
    
    {
    
    
    extern pxCurrentTCB;
    
    PRESERVE8//8 字节对齐
    
    ldr r3, =pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3。
    
     /
     
     ldr r1, [r3] /
    
    加载 pxCurrentTCB 到 r1。
    
     /
     
     ldr r0, [r1] /
    
    任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。
    
     /
     
     ldmia r0!, {r4-r11} /
    
    以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存器 r4~r11,同时 r0 也会跟着自增。
    
     /
     
     msr psp, r0 /
    
    将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp。 */
    
    isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
    
    mov r0, #0//将寄存器 r0清 0。
    
    msr basepri, r0//即打开所有中断。
    
    /*0x0d=1101b
    
    当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示返回 thumb 状态,
    
    bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式。
    
    当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆
    
    栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。
    
    在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。
    
    
     /
     
     orr r14, #0xd
     
     /
    
    
    异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下
    
    内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0
    
    (任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶,
    
    */
    
    bx r14
    
    }
   
    
    
    三、任务切换
   
    
    
    taskYIELD()函数
   
    任务切换通过调用taskYIELD()函数实现,taskYIELD()函数里就是通过触发 PendSV实现。
    
    #define taskYIELD() portYIELD()
    
    //0xE000ED04:中断控制状态寄存器 [28]PENDSVSET 设置挂起pendSV位:1= 设置挂起 pendSV 《CM3技术参考手册》P88
    
    #define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
    
    #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
    
    #define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
    
    #define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
    
    #define portYIELD()
    
    {
    
    /* 触发 PendSV,产生上下文切换.
    
     /
     
     portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
     
     /
    
    __dsb:数据同步屏障,确保在下一条指令开始执行前,所有的存储器访问已经完成。
    
    __isb: 指令同步屏障,清除流水线并且确保在新指令执行时,之前的指令都已经执行完毕。*/
    
    __dsb( portSY_FULL_READ_WRITE );
    
    __isb( portSY_FULL_READ_WRITE );
    
    }
   
    
    
    PendSV中断
   
    PendSV回调函数中实现了:硬件将CPU 寄存器(R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和 xPSR)压入原任务栈中;将r4~r11压入原任务栈中,调用vTaskSwitchContext函数切换pxCurrentTCB指向新任务的控制块;打开中断;将新任务栈中相应数数存入r4-r11;将新任务栈地址存入psp,此时的 r14 等于 0xfffffffd,表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向新任务栈的栈顶;中断结束,硬件将新任务栈中剩下的内容加载到 CPU 寄存器(R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和 xPSR),从而切换到新的任务。
    
    #define xPortPendSVHandler PendSV_Handler
    
    __asm void xPortPendSVHandler( void )
    
    {
    
    
    extern pxCurrentTCB;//声明外部变量 pxCurrentTCB,用于指向当前正在运行或者即将要运行的任务的任务控制块。
    
    extern vTaskSwitchContext;//声明外部函数 vTaskSwitchContext
    
    PRESERVE8
    
    /*当进入 PendSVC Handler 时,上一个任务运行的环境即:
    
    xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
    
    这些 CPU 寄存器的值会自动存储到任务的栈中,并且在返回时自动弹出它们.
    
    剩下的 r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶),
    
    
     /
     
     mrs r0, psp//将 PSP 的值存储到 r0。
     
     isb//指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
     
     ldr r3, =pxCurrentTCB /
    
    Get the location of the current TCB. */
    
    ldr r2, [r3]//即 r2 等于 pxCurrentTCB。
    
    /*由于前面“mrs r0, psp”将 PSP 的值存储到 r0。
    
    以 r0 作为基址(指针先递减,再操作,STMDB 的 DB 表示Decrease Befor),
    
    将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0的值。
    
    
     /
     
     stmdb r0!, {r4-r11}
     
     /
     
      由于前面“r2, [r3]”将r2 等于 pxCurrentTCB。pxCurrentTCB首个元素即为栈顶指针 pxTopOfStack。
      
      r0就存储了pxCurrentTCB的栈顶指针。到此,上下文切换中的上文保存就完成了。
      
      
       /
       
       str r0, [r2] /
      
      Save the new top of stack into the first member of the TCB.
      
       /
       
       /
       
        lr(r14)的作用问题,这个lr一般来说有两个作用:
        
        (1)当使用bl或者blx跳转到子过程的时候,r14保存了返回地址,可以在调用过程结尾恢复。
        
        (2)异常中断发生时,这个异常模式特定的物理R14被设置成该异常模式将要返回的地址。
        
        因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回
        
        地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执
        
        行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使
        
        用的是 PSP 还是 MSP),因此需要入栈保护.
        
        R3 保存的是TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新
        
        
         /
         
         stmdb sp!, {r3, r14}//将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP)
         
         //关中断,进入临界段
         
         mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
         
         msr basepri, r0
         
         dsb
         
         isb
         
         bl vTaskSwitchContext//调用函数 vTaskSwitchContext选择优先级最高的任务,然后更新 pxCurrentTCB。
         
         //退出临界段,开中断,直接往 BASEPRI 写 0。
         
         mov r0, #0
         
         msr basepri, r0
         
         ldmia sp!, {r3, r14}//从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP。
         
         ldr r1, [r3]
         
         ldr r0, [r1] /
        
        加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针。
       
       /
       
       ldmia r0!, {r4-r11} /
      
      将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。
     
     /
     
     msr psp, r0//更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。
     
     isb
     
     /
    
    
    异常发生时,R14 中保存异常返回标志,包括返回后进入任务模
    
    式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd,最
    
    表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈
    
    的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务
    
    的任务栈中剩下的内容加载到 CPU 寄存器:R0(任务形参)、R1、R2、R3、R12、R14
    
    (LR)、R15(PC)和 xPSR,从而切换到新的任务。
    
    */
    
    bx r14
    
    nop
    
    }
   
    void vTaskSwitchContext( void )
    
    {
    
    
    if( pxCurrentTCB == &Task1TCB )
    
    {
    
    
    /* The scheduler is currently suspended – do not allow a context
    
    switch. */
    
    pxCurrentTCB = &Task2TCB;
    
    }
    
    else
    
    {
    
    
    pxCurrentTCB = &Task1TCB;
    
    }
    
    }
   
    
    
    四、main()函数
   
    TaskHandle_t Task1_Handle;
    
    #define TASK1_STACK_SIZE 128
    
    StackType_t Task1Stack[TASK1_STACK_SIZE];
    
    TCB_t Task1TCB;
   
    TaskHandle_t Task2_Handle;
    
    #define TASK2_STACK_SIZE 128
    
    StackType_t Task2Stack[TASK2_STACK_SIZE];
    
    TCB_t Task2TCB;
   
    int main(void)
    
    {
    
    
    BSP_init();
    
    //初始化与任务相关的列表,如就绪列表
    
    prvInitialiseTaskLists();
    
    Task1_Handle=
    
    xTaskCreateStatic((TaskFunction_t)LED1_Task,//任务入口函数
    
    (const char *) “LED1_Task”,//任务名称
    
    (uint16_t) TASK1_STACK_SIZE,//任务栈大小
    
    (void *) NULL,//入口函数参数
    
    (StackType_t
    
     )Task1Stack, /
    
    任务栈起始地址 */
    
    (TCB_t
    
     )&Task1TCB);//任务控制块
     
     /
    
    将任务添加到就绪列表 */
    
    vListInsertEnd( &( pxReadyTasksLists
    
     1
    
    ),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
    
    Task2_Handle=
    
    xTaskCreateStatic((TaskFunction_t)LED2_Task,//任务入口函数
    
    (const char *) “LED2_Task”,//任务名称
    
    (uint16_t) TASK2_STACK_SIZE,//任务栈大小
    
    (void *) NULL,//入口函数参数
    
    (StackType_t
    
     )Task2Stack, /
    
    任务栈起始地址 */
    
    (TCB_t *)&Task2TCB);//任务控制块
    
    vListInsertEnd( &( pxReadyTasksLists
    
     2
    
    ), &( ((TCB_t
    
     )(&Task2TCB))->xStateListItem ) );
     
     /
    
    启动调度器,开始多任务调度,启动成功则不返回 */
    
    vTaskStartScheduler();
    
    while(1)
    
    {
    
    
    gpio_bit_reset(GPIOA,GPIO_PIN_5);//LED off
    
    gpio_bit_reset(GPIOA,GPIO_PIN_4);//LED off
    
    delayms(1000);
    
    gpio_bit_set(GPIOA,GPIO_PIN_5); //LED on
    
    gpio_bit_set(GPIOA,GPIO_PIN_4);//LED off
    
    delayms(2000);
    
    }
    
    }
   
 
