【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】

  笔者在嵌入式领域深耕6年,对嵌入式项目构建,BLDC电机控制,产品上位机开发以及产品量产和产品售后维护有多年工作经验。经验分享,从0到1, 让我带你从实际工作的角度走进嵌入式成长之路。

  原创不易欢迎大家关注我的微信公众号嵌入式工程师成长之路扫下面二维码
                     在这里插入图片描述
所有文章总目录:【电子工程师 qt工程师】

原创视频总目录:【电子工程师 qt工程师】

1. 任务的初始化

【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第1张图片
【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第2张图片注意:

  • 程序入口地址会被加载到PC寄存器,CPU设计决定。
  • 函数的函数会被加载到R0寄存器,编译器决定。
    【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第3张图片【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第4张图片
    【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第5张图片

  系统运行后,默认使用的是MSP(主栈),当第一次切换任务,也就是从main函数切换到第一个初始任务时,触发PendSV异常,并设置PSP为0;当进入到PendSV异常处理函数时,会先判断PSP是否为0,如果为0,表示此时是第一次切换任务,直接跳转到PendSVHander_nosave中运行,恢复初始任务,然后返回时设置返回值为0x04,表示之后使用PSP。

  任务运行起来之后再发生任务切换,此时PSP不为0,就直接往下运行,硬件会自动将某些寄存器保存在PSP中,另外一些不会自动保存的需要自己保存在任务自己的栈中。

1.main.c

#include "tinyOS.h"

// 当前任务:记录当前是哪个任务正在运行
tTask * currentTask;

// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
tTask * nextTask;

// 所有任务的指针数组:简单起见,只使用两个任务
tTask * taskTable[2];

void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack)
{
    // 为了简化代码,tinyOS无论是在启动时切换至第一个任务,还是在运行过程中在不同间任务切换
    // 所执行的操作都是先保存当前任务的运行环境参数(CPU寄存器值)到堆栈中(如果已经运行运行起来的话),然后再
    // 取出从下一个任务的堆栈中取出之前的运行环境参数,然后恢复到CPU寄存器
    // 对于切换至之前从没有运行过的任务,我们为它配置一个“虚假的”保存现场,然后使用该现场恢复。

    // 注意以下两点:
    // 1、不需要用到的寄存器,直接填了寄存器号,方便在IDE调试时查看效果;
    // 2、顺序不能变,要结合PendSV_Handler以及CPU对异常的处理流程来理解
    *(--stack) = (unsigned long)(1<<24);                // XPSR, 设置了Thumb模式,恢复到Thumb状态而非ARM状态运行
    *(--stack) = (unsigned long)entry;                  // 程序的入口地址
    *(--stack) = (unsigned long)0x14;                   // R14(LR), 任务不会通过return xxx结束自己,所以未用
    *(--stack) = (unsigned long)0x12;                   // R12, 未用
    *(--stack) = (unsigned long)0x3;                    // R3, 未用
    *(--stack) = (unsigned long)0x2;                    // R2, 未用
    *(--stack) = (unsigned long)0x1;                    // R1, 未用
    *(--stack) = (unsigned long)param;                  // R0 = param, 传给任务的入口函数
    *(--stack) = (unsigned long)0x11;                   // R11, 未用
    *(--stack) = (unsigned long)0x10;                   // R10, 未用
    *(--stack) = (unsigned long)0x9;                    // R9, 未用
    *(--stack) = (unsigned long)0x8;                    // R8, 未用
    *(--stack) = (unsigned long)0x7;                    // R7, 未用
    *(--stack) = (unsigned long)0x6;                    // R6, 未用
    *(--stack) = (unsigned long)0x5;                    // R5, 未用
    *(--stack) = (unsigned long)0x4;                    // R4, 未用

    task->stack = stack;                                // 保存最终的值
}

void tTaskSched () 
{    
    // 这里的算法很简单。
    // 一共有两个任务。选择另一个任务,然后切换过去
    if (currentTask == taskTable[0]) 
    {
        nextTask = taskTable[1];
    }
    else 
    {
        nextTask = taskTable[0];
    }
    
    tTaskSwitch();
}

void delay (int count) 
{
    while (--count > 0);
}

int task1Flag;
void task1Entry (void * param) 
{
    for (;;) 
    {
        task1Flag = 1;
        delay(100);
        task1Flag = 0;
        delay(100);
        tTaskSched();
    }
}

int task2Flag;
void task2Entry (void * param) 
{
    for (;;) 
    {
        task2Flag = 1;
        delay(100);
        task2Flag = 0;
        delay(100);
        tTaskSched();
    }
}

// 任务1和任务2的任务结构,以及用于堆栈空间
tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];     
tTaskStack task2Env[1024];

int main () 
{
    // 初始化任务1和任务2结构,传递运行的起始地址,想要给任意参数,以及运行堆栈空间
    tTaskInit(&tTask1, task1Entry, (void *)0x11111111, &task1Env[1024]);
    tTaskInit(&tTask2, task2Entry, (void *)0x22222222, &task2Env[1024]);
    
    // 接着,将任务加入到任务表中
    taskTable[0] = &tTask1;
    taskTable[1] = &tTask2;
    
    // 我们期望先运行tTask1, 也就是void task1Entry (void * param) 
    nextTask = taskTable[0];

    // 切换到nextTask, 这个函数永远不会返回
    tTaskRunFirst();
    return 0;
}


2.switch.c

#include "tinyOS.h"
#include "ARMCM3.h"

// 在任务切换中,主要依赖了PendSV进行切换。PendSV其中的一个很重要的作用便是用于支持RTOS的任务切换。
// 实现方法为:
// 1、首先将PendSV的中断优先配置为最低。这样只有在其它所有中断完成后,才会触发该中断;
//    实现方法为:向NVIC_SYSPRI2写NVIC_PENDSV_PRI
// 2、在需要中断切换时,设置挂起位为1,手动触发。这样,当没有其它中断发生时,将会引发PendSV中断。
//    实现方法为:向NVIC_INT_CTRL写NVIC_PENDSVSET
// 3、在PendSV中,执行任务切换操作。
#define NVIC_INT_CTRL       0xE000ED04      // 中断控制及状态寄存器
#define NVIC_PENDSVSET      0x10000000      // 触发软件中断的值
#define NVIC_SYSPRI2        0xE000ED22      // 系统优先级寄存器
#define NVIC_PENDSV_PRI     0x000000FF      // 配置优先级

#define MEM32(addr)         *(volatile unsigned long *)(addr)
#define MEM8(addr)          *(volatile unsigned char *)(addr)

// 下面的代码中,用到了C文件嵌入ARM汇编
// 基本语法为:__asm 返回值 函数名(参数声明) {....}, 更具体的用法见Keil编译器手册,此处不再详注。
__asm void PendSV_Handler ()
{   
		//注意:此时的上下文使用的是MSP堆栈
    IMPORT  currentTask               // 使用import导入C文件中声明的全局变量
    IMPORT  nextTask                  // 类似于在C文文件中使用extern int variable
																			
									  // PSP保留栈的地址
    MRS     R0, PSP                   // MRS加载PSP的值到R0,获取当前任务的堆栈指针,每次在PendSV异常退出时,都会将当前任务的堆栈
    								  //地址赋值给PSP(当前任务的堆栈在PendSV_Handler函数的倒数第三句汇编代码中有赋值给PSP)
									  // 即,PSP只有一个,但每次PendSV异常退出时,都会保存当前任务堆栈到PSP中。
									  
	CBZ     R0, PendSVHandler_nosave  // 判断PSP是否为0,如果是0表示是由tTaskRunFirst触发,直接跳转到PendSVHandler_nosave;
    STMDB   R0!, {R4-R11}             //  由tTaskSched函数切换时,PSP肯定不会0
																			
    LDR     R1, =currentTask          
    LDR     R1, [R1]                  
    STR     R0, [R1]                  // 更新一下,上一个任务的栈顶,R0的值是栈顶

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行
    LDR     R0, =currentTask          // currentTask和nextTask是一个用来存放tTask地址的变量
    LDR     R1, =nextTask             
    LDR     R2, [R1]  				  // nextTask指针解引用,将值读取到R2
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 
    LDR     R0, [R2]                  // 从currentTask中加载stack(这句话有点难理解,得仔细琢磨)
    LDMIA   R0!, {R4-R11}             // 将任务栈中的值依次恢复到{R4至R11}。为什么只恢复了这么点,
									  // 因为其余的寄存器在进入PendSV时被自动保存到了MSP,退出异常时自动恢复。

    MSR     PSP, R0                   // 最后把栈顶指针恢复到PSP  
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}  

//在启动tinyOS时,调用该函数,将切换至第一个任务运行
void tTaskRunFirst()
{
    // 这里设置了一个标记,PSP = 0, 用于与tTaskSwitch()区分,用于在PEND_SV
    // 中判断当前切换是tinyOS启动时切换至第1个任务,还是多任务已经跑起来后执行的切换
    __set_PSP(0);

    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级
    
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

    // 可以看到,这个函数是没有返回
    // 这是因为,一旦触发PendSV后,将会在PendSV后立即进行任务切换,切换至第1个任务运行
    // 此后,tinyOS将负责管理所有任务的运行,永远不会返回到该函数运行
}
    
void tTaskSwitch() 
{
    // 和tTaskRunFirst, 这个函数会在某个任务中调用,然后触发PendSV切换至其它任务
    // 之后的某个时候,将会再次切换到该任务运行,此时,开始运行该行代码, 返回到
    // tTaskSwitch调用处继续往下运行
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;  // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

3.zpowerOS.h

// 标准头文件,里面包含了常用的类型定义,如uint32_t
#include 

// Cortex-M的堆栈单元类型:堆栈单元的大小为32位,所以使用uint32_t
typedef uint32_t tTaskStack;

// 任务结构:包含了一个任务的所有信息
typedef struct _tTask {
	// 任务所用堆栈的当前堆栈指针。每个任务都有他自己的堆栈,用于在运行过程中存储临时变量等一些环境参数
	// 在tinyOS运行该任务前,会从stack指向的位置处,会读取堆栈中的环境参数恢复到CPU寄存器中,然后开始运行
	// 在切换至其它任务时,会将当前CPU寄存器值保存到堆栈中,等待下一次运行该任务时再恢复。
	// stack保存了最后保存环境参数的地址位置,用于后续恢复
    tTaskStack * stack;
}tTask;

// 当前任务:记录当前是哪个任务正在运行
extern tTask * currentTask;

// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
extern tTask * nextTask;

/**********************************************************************************************************
** Function name        :   tTaskRunFirst
** Descriptions         :   在启动tinyOS时,调用该函数,将切换至第一个任务运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskRunFirst (void); 

/**********************************************************************************************************
** Function name        :   tTaskSwitch
** Descriptions         :   进行一次任务切换,tinyOS会预先配置好currentTask和nextTask, 然后调用该函数,切换至
**                          nextTask运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskSwitch (void);
#endif

4.任务切换的本质

第一步:
系统一上电,程序先运行的时main函数,此时用的是MSP,当调用tTaskRunFirst函数(试图切换到任务1)后,进入PendSV异常处理函数,此时依次发生以下动作:

(1) currentTask = nextTask;用汇编对指针进行赋值。
(2) 把currentTask栈里面的数据(这些数据在初始化任务时已经准备好)恢复到CPU寄存器中。
(3) 把currentTask栈的栈顶地址赋值给PSP,这句代码非常重要,以后每次进入PendSV异常函数都需要从PSP中获得当前任务的栈顶,然后将CPU数据保存到栈中,这里的栈地址为什么要从PSP中获得,就是因为在每次退出异常函数时,都会将栈顶地址赋值给PSP。
(4) 设置异常返回值,表示以后都是用PSP(如果在任务内部调用了函数,这里使用的是堆栈就是任务自己的堆栈,堆栈地址已经在切换任务时赋值给了PSP)。
(5) 执行BX LR指令,真正进行切换的指令。

第二步:
从任务1切到任务2:运行任务1时,发生PendSV任务切换异常,此时进入PendSV异常处理函数,因为PSP不为0,所以会依次执行汇编指令;先把PSP中任务1的栈地址取出,并将当前CPU的寄存器的值保存到栈中。然后再执行上面第一步的动作。

5.仿真结果

两个任务函数都被调用到了。
【专题1:电子工程师】 之 【28.基于STM32从0到1写操作系统 - 【7.任务切换(重点)】】_第6张图片

5.把代码移植到STM32F103ZET6芯片上

代码网盘地址 提取码:qxqg

你可能感兴趣的:(专题1:电子工程师)