STM32 操作系统内核调度原理与实现(3):两个任务之间的切换

注:测试发现,打印浮点数会出现硬件错误

在实现多个任务之间的切换之前,可以先通过实现两个固定任务切换,作为一个过度,本文将介绍在STM32中,如何通过操作内核,来实现两个固定任务之间的切换。

一、嵌入汇编

1. 嵌入汇编的方法

正如前文所提及,实现任务切换中操作内核寄存器需要使用汇编程序,所以我们需要掌握如何在自己使用的IDE中嵌入汇编语句,本文使用的编译器为MDK5默认编译器v5版本:
STM32 操作系统内核调度原理与实现(3):两个任务之间的切换_第1张图片
嵌入汇编的方式十分简单,只需要使用_asm来声明一个函数即可,如:

//程序清单3.1
__asm void BUD_ContextSwitch(stack_reg_t* pstr_cur_task_stack_reg_adrr,stack_reg_t* pstr_next_task_stack_reg_adrr)
{
    STMIA R0!,{R4 - R12};
    STMIA R0!,{R13};
    STMIA R0!,{R14};
    MRS R2,XPSR;
    STMIA R0,{R2};
    
    LDMIA R1!,{R4 - R12};
    LDMIA R1,{R13};
    ADD   R1,#8;
    LDMIA R1,{R0};
    MSR   XPSR,R0;
    SUB   R1,#4;
    LDMIA R1,{PC};
}

程序清单3.1是一个示例函数,其详细含义在后文解释。可以发现,我们操作了寄存器,R0,R1,R4,R12,XPSR 等等。注意,在不同的编译器中,嵌入汇编的方式可能不太一样,所以程序员需要选择符合所用环境的方式。

2. 测试

可以用一段简单的程序,来测试程序的汇编指令正常与否,在 AAPCS 标准中, R0和R1寄存器分别存放函数的第1个参数和第2个参数,固可通过使用汇编(ADD)来改变参数值的方法来验证:

//程序清单3.2
__asm void test_asm_func(int para1,int para2)
{
    ADD   R1,#1                              //将R1的值加1,# 符号表示数值
    ADD   R0,#1                              //将R0的值加1
    
    ADD   R1,#2
    ADD   R0,#2
    
    ADD   R1,#3
    ADD   R0,#3
    
    ADD   R1,#4
    ADD   R0,#4
}

void main(void)
{
···
    test_asm_func(0,1);                     //调用测试函数,并传入参数0和1
···
}

程序清单3.2 不断的累加R0和R1的值,使得我们可以在硬件仿真时,通过观察M3内核寄存器的窗口,了解R0和R1的值的变化,同时,还需要注意,在第一句汇编函数ADD R1,#1没有运行之前,R0和R1的值。
STM32 操作系统内核调度原理与实现(3):两个任务之间的切换_第2张图片
上图表明,当程序停留在69行,即第一句汇编指令即将运行时候,R0和R1的值分别是0和1,这正是函数传入的参数,接着使用单步执行,来观察值的变化:
STM32 操作系统内核调度原理与实现(3):两个任务之间的切换_第3张图片
可以发现,程序运行正常。接下来可以进入正式的程序编程中,这种测试方法通用合适其它的汇编指令。

二、固定任务的切换

掌握了汇编语句的使用和测试以后,就可以进入本文的主题部分——函数的切换。

2.1 添加文件

固定任务的切换代码量并不大,本文创建BudOS.c/h,两个文件来存放所有程序,并将它们存放到BudOS_core文件夹:
STM32 操作系统内核调度原理与实现(3):两个任务之间的切换_第4张图片

2.2 任务堆栈的申请

在M3内核中,PC(R15)寄存器存放值为内核当前运行的程序的地址,所以当程序改变PC值时,程序的流程也被改变,但是,在改变PC指针之前,一定要做好现场的保护,否则程序将会出错。

保护现场,即使保存当前M3内核各个寄存器的值,然后切换到另一个任务中,当切换回来时,将这些寄存器的值恢复即可,所以程序需要在内存中申请一点空间,来存放这些数据,这是每一个任务都需要的,可以使用数组的方式来申请一块内存:

#define TASK_STACK_SIZE (64)                               //堆栈的大小

__align(4) uint32_t task1_stack[TASK_STACK_SIZE];          //任务1的堆栈区
__align(4) uint32_t task2_stack[TASK_STACK_SIZE];

通常,不同的任务的堆栈不太一样,比如一些任务如果申请较大的临时数组,那么它就需要更大的内存。

2.3 任务控制块(TCB)

2.3.1 数据结构

任务控制块是一个数据结构,每一个任务都应该拥有一个任务控制块,以便程序管理各个任务。本文由于任务简单,所以目前任务控制块只有一个结构体——stack_reg,它的作用是保存内核寄存器的值,即是,当程序需要从当前任务切换到另一个任务时候,当前内核寄存器的值将存储到这个结构体中,以确保程序切回后,可正常运行。

//程序清单3.3
//内核寄存器结构体
typedef struct stack_reg
{
    uint32_t ui_R4;
    uint32_t ui_R5;
    uint32_t ui_R6;
    uint32_t ui_R7;
    uint32_t ui_R8;
    uint32_t ui_R9;
    uint32_t ui_R10;
    uint32_t ui_R11;
    uint32_t ui_R12;
    uint32_t ui_R13;
    uint32_t ui_R14;
    uint32_t ui_xPSR;
}stack_reg_t;
//任务控制块结构体
typedef struct bud_tcb
{
    stack_reg_t stack_reg;
}bud_tcb_t;

程序清单3.3中的stack_reg结构中,R0~R3并不需要备份,这是由于在 AAPCS标准 中,这4个寄存器用以存放函数的参数,所以在发生函数调用时,父函数会自动备份。我们将任务控制块,存放在任务栈区的顶部:
STM32 操作系统内核调度原理与实现(3):两个任务之间的切换_第5张图片

2.3.1 获取TCB的地址

你可能感兴趣的:(#,TulipOS)