RISC-V 32架构实践专题八(从零开始写操作系统-协作式多任务的实现)

本文介绍了如何在MCU中实现内存分配器和协作式多任务调度器,包括上下文切换、context结构体的处理、任务创建和调度过程。遇到的问题和解决方法也进行了讨论,如栈空间对齐问题对MCU性能的影响。
摘要由CSDN通过智能技术生成

        实现了内存分配器后,接下来开始实现任务调度器。首先实现一个简单的协作式多任务调度,后续再进行抢占式多任务调度的实现。

        其中协作式多任务是指任务会主动释放CPU资源,而不是完全通过操作系统来进行任务调度。只有当任务主动调用任务切换函数时,当前任务才会交出CPU使用权,让其他任务接管CPU资源开始运行。

使用mscratch寄存器来暂存当前正在执行任务的context上下文内存空间的地址。

当要进行任务调度时,将context进行切换即可实现调度功能。

一、上下文切换

        在进行多任务切换时,操作系统需要保存当前CPU的上下文环境,并且加载下一个任务的上下文环境。而上下文(Context)通常指的是处理器在执行程序时所需的所有状态信息的集合,在具体一点来说就是CPU的各个通用寄存器和一些特殊的状态控制寄存器(如页表管理相关的寄存器等)。

        对于我们所使用的MCU来说,上下文目前主要指的就是32个通用寄存器。所要实现上下文的切换,要完成如下步骤代码的编写:

  1. 实现context结构体,也就是用于保存32个通用寄存器的结构体;
  2. 每个任务对应一个context结构体,所以要实现一个context结构体的加载和保存接口。

1.1 context结构体的实现

// 定义一个上下文结构体,也就是用于保存31个通用寄存器(x0寄存器的值不用保存,因为其恒为0)
typedef struct context
{
    reg_t ra;
    reg_t sp;
    reg_t gp;
    reg_t tp;
    reg_t t0;
    reg_t t1;
    reg_t t2;
    reg_t s0;
    reg_t s1;
    reg_t a0;
    reg_t a1;
    reg_t a2;
    reg_t a3;
    reg_t a4;
    reg_t a5;
    reg_t a6;
    reg_t a7;
    reg_t s2;
    reg_t s3;
    reg_t s4;
    reg_t s5;
    reg_t s6;
    reg_t s7;
    reg_t s8;
    reg_t s9;
    reg_t s10;
    reg_t s11;
    reg_t t3;
    reg_t t4;
    reg_t t5;
    reg_t t6;
} context_t;

        按照x0~x31的顺序,定义一个通用寄存器的结构体集合,便于对MCU当前通用寄存器状态的存储与加载。

1.2 context的存储与加载

        要操作MCU的通用寄存器,那么需要使用汇编代码来完成,如下所示:

# 实现一个store_reg的宏定义,用于保存当前task的context
.macro store_reg base
    sw ra, 0(\base)
    sw sp, 4(\base)
    sw gp, 8(\base)
    sw tp, 12(\base)
    sw t0, 16(\base)
    sw t1, 20(\base)
    sw t2, 24(\base)
    sw s0, 28(\base)
    sw s1, 32(\base)
    sw a0, 36(\base)
    sw a1, 40(\base)
    sw a2, 44(\base)
    sw a3, 48(\base)
    sw a4, 52(\base)
    sw a5, 56(\base)
    sw a6, 60(\base)
    sw a7, 64(\base)
    sw s2, 68(\base)
    sw s3, 72(\base)
    sw s4, 76(\base)
    sw s5, 80(\base)
    sw s6, 84(\base)
    sw s7, 88(\base)
    sw s8, 92(\base)
    sw s9, 96(\base)
    sw s10, 100(\base)
    sw s11, 104(\base)
    sw t3, 108(\base)
    sw t4, 112(\base)
    sw t5, 116(\base)
    # 由于t6的值被用于保存context的地址,则需要在此宏的外面进行保存t6的原始值
.endm

# 实现restore_reg的宏定义,用于加载需要调度task的context
.macro restore_reg base
    lw ra, 0(\base)
    lw sp, 4(\base)
    lw gp, 8(\base)
    lw tp, 12(\base)
    lw t0, 16(\base)
    lw t1, 20(\base)
    lw t2, 24(\base)
    lw s0, 28(\base)
    lw s1, 32(\base)
    # 由于a0被用于存放需调度任务的context,所以最后进行加载
    lw a1, 40(\base)
    lw a2, 44(\base)
    lw a3, 48(\base)
    lw a4, 52(\base)
    lw a5, 56(\base)
    lw a6, 60(\base)
    lw a7, 64(\base)
    lw s2, 68(\base)
    lw s3, 72(\base)
    lw s4, 76(\base)
    lw s5, 80(\base)
    lw s6, 84(\base)
    lw s7, 88(\base)
    lw s8, 92(\base)
    lw s9, 96(\base)
    lw s10, 100(\base)
    lw s11, 104(\base)
    lw t3, 108(\base)
    lw t4, 112(\base)
    lw t5, 116(\base)
    lw t6, 120(\base)
.endm

.global switch_to
.align 4
switch_to:
    # 读取暂存寄存器mscratch中的值到t6寄存器,mscratch寄存器中的值存放的是当前context的地址
    csrrw t6, mscratch, t6
    # 如果t6等于空,则直接跳转到加载下一个任务的上下文
    beqz t6, 1f
    # 调用store_reg宏,用于保存当前上下文
    store_reg t6
    # 由于t6的值保存的是当前context地址,所以需要将t6值还原后再进行保存
    mv t5, t6               # 首先将当前context地址值保存到t5
    csrr t6, mscratch       # 再将t6的值进行还原
    sw t6, 120(t5)          # 最后将t6的值保存到context中

1:
    # 将传入需要调度的任务的context地址
    csrw mscratch, a0
    # 将需要调度的任务的context进行加载
    restore_reg a0
    # 因为a0存放的是context地址,所以需要最后进行加载
    lw a0, 36(a0)

    # 完成调度切换,进行返回(无返回值)
    ret

        上述汇编代码中,注释已经详细解释了其切换功能的实现,这里就不再进行赘述了。需要注意的是,此函数使用mscratch状态控制寄存器来保存当前任务的context地址,然后在进行存储与加载过程中,t6与a0需要最后进行拷贝与赋值操作。

二、协作式多任务的实现

        实现上下文context切换接口后,接下来完成任务的创建以及调度接口就可以进行多任务的运行了。

2.1 任务创建

        对于一个独立的任务来说,它应该具备自己独立的栈空间,所以需要为每个创建的任务分配自己的栈指针。分配完栈空间后,同时将ra更新为自己需要运行的主函数地址,那么在上下文切换完成后,调用ret指令后,就可以跳转到ra寄存器中存放的地址中执行了。

static uint32_t task_sp[TASK_MAX][TASK_SP_SIZE / 4];
static context_t task_context[TASK_MAX];
static uint32_t task_max;


/*********************************************************************
 * @fn      task_create
 *
 * @brief   任务创建函数,创建一个任务.
 *
 * @param   func,传入任务需要执行的主函数
 *
 * @return  任务的id
 */
int task_create(TASK_CUNC func)
{
    uint32_t task_id = task_max;

    if(task_max >= TASK_MAX)
    {
        printf("create task failed!\n");
        return -1;
    }

    // 将栈指针赋值给sp,因为栈方向是自顶向下,使用要使用栈空间的高位地址
    task_context[task_id].sp = (uint32_t)&task_sp[task_id][TASK_SP_SIZE - 1];
    // 将任务主函数地址赋值给ra
    task_context[task_id].ra = (uint32_t)func;

    // 创造的任务总数量记录
    task_max++;

    return task_id;
}

        首先定义需要定义任务的最大数量,这里我们使用宏TASK_MAX来定义了最大的task数量为10;

        然后再定义一个栈空间,提前为最大10个任务分配好栈空间,没有任务的栈空间大小为1KB;

        接着定义一个存储上下文context结构体的数组,数组长度为10,分别对应每一个任务;

        最后再定义一个当前创建的任务的总数量task_max变量。

        创建任务则对context结构体的sp与ra进行赋值即可。

 2.2 任务调度

        任务调度目前采用最简单的轮询任务调度。将每个任务进行排序,根据任务id号进行递增的轮询执行。

        代码实现如下所示:

/*********************************************************************
 * @fn      schedule_task
 *
 * @brief   进行任务调度,调度是顺序执行的,即遵从任务id从小到大顺序执行.
 *
 * @param   none.
 *
 * @return  none.
 */
void schedule_task(void)
{
    uint32_t task_id = 0;
    context_t *context = 0;

    // 使用内嵌汇编语句,读取mscratch的值,计算出当前需要调度的任务id
    asm("csrr %0, mscratch"
        : "=r"(context)
    );

    if(context == 0)
    {
        // 开始第一次调度时,直接运行第一个任务
        switch_to(&task_context[0]);
    }
    else
    {
        // 读取当前任务的id号,并进行加1,得到下一个任务的id;最后完成下一个任务的调度
        task_id = context - task_context;
        task_id = (task_id + 1) % task_max;
        switch_to(&task_context[task_id]);
    }
}

上述代码涉及到了一个c语言内嵌汇编的知识点:

asm [volatile] (

        "汇编指令"

        :"输出操作数列表"

        :"输入操作数列表"

        :"可能影响的寄存器或者存储器"

);

        其中对于"输出操作数列表"、"输入操作数列表"和"可能影响的寄存器或者存储器"都是可选项,当汇编指令不需要输入输出操作数时,可以不用填。(使用%0、%1和%2等来代替通用寄存器)

        但当有多个输入或输出操作数时,每个操作数之间用 , 分隔;并且操作数是有属性的,需要和汇编指令对应上,如m、r、i、f和F:

  • 其中r代表寄存器操作数;
  • m代表内存操作数;
  • i代表整型立即数;
  • f代表浮点寄存器操作数;
  • F代表浮点立即数

三、效果演示

        在主函数中创建两个任务,每个任务都进行循环输出打印log信息,并打开和关闭led灯。代码如下所示:

void taks_fun1(void)
{
	while(1)
	{
		printf("taks_fun1 --- running!\n\r");
		led2_ctrl(1);
		mdelay(1000);
		schedule_task();
	}
}

void taks_fun2(void)
{
	while(1)
	{
		printf("taks_fun2 --- running!\n\r");
		led2_ctrl(0);
		mdelay(1000);
		schedule_task();
	}
}

void mdelay(uint32_t m)
{
	uint32_t i = 0;

	for(i = 0; i < m * 6000; i++)
	{
		;
	}
}

void start_kernel(void)
{
	uint32_t task_id[2] = {0};

	// 设置sysclk系统时钟为96MHz
	clock_hse_96Mhz();
	// 设置usart1,并初始化配置为115200,8,none,1
	usart1_init();

	printf("\r======== hello RVOS ========\n\r");

	// 初始化内存页分配器
	page_init();

	task_id[0] = task_create(taks_fun1);
	printf("create task id: %d!\n\r", task_id[0]);
	task_id[1] = task_create(taks_fun2);
	printf("create task id: %d!\n\r", task_id[1]);

	schedule_task();

	while (1) {} // stop here!
}

可以看到两个任务都在while(1)里循环执行,当调用schedule_task函数后,将切换context上下文,跳到另一个任务开始执行。

四、注意

        在进行调试过程中发现,当我加上任务调度功能后,在初始化page内存分配器时,mcu会跑飞。经反汇编以及nm命令排除,发现使用只读空间存放栈信息时,没有进行地址4字节对齐,从而导致了访问栈信息变量时,mcu跑飞。

        将栈信息进行4字节对齐,即可解决上述发生的问题。

# 声明下面的数据存放在只读代码段中;通过链接文件可知,这些数据将存放在flash中,并只能读取
.section .rodata

# 进行4字节对齐操作
.balign 4

.global	PAGE_SIZE
PAGE_SIZE : .word _page_size

.global	DATA_START
DATA_START : .word _vma_data_start

  • 37
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值