在GD32VF103实现简单线程切换

目录

本文通过一个最小化的实验,在GD32VF103下使用C和汇编实现两个 线程函数的切换,简单介绍上下文切换的机制。

完整程序下载地址:https://www.jiawei.site/downloads/gd32/gd32_context.zip

背景

RTOS引入了线程的概念,从一个线程切换到另一个线程时,需要 将当前线程的上下文进行保存,以便下次恢复执行时能从中断处 继续运行。恢复将执行线程的上下文时,CPU重新加载上次运行时 的寄存器状态、栈指针以及程序计数器。这个过程即上下文切换, 是操作系统调度机制的核心之一。

每个线程都有独立的栈,用于保存该线程运行期间的局部变量 和上下文信息。当发生上下文切换时,系统会把当前线程的寄存器 内容保存到它的栈中,然后从下一个线程的栈中取出之前保存的 寄存器值,从而恢复该线程的运行现场。

基本思路

本实验的核心目标是在裸机环境下模拟线程的切换过程,从而理解 RTOS 中线程调度和上下文切换的底层机制。裸机工程见本站文章: 从零开始创建GD32VF103 Makefile工程

初始化两个线程,每个线程拥有独立的线程栈,并分别绑定到各自 的线程函数。让定时器产生周期性中断,在定时器中断发生时从一个 线程切换到另一个线程,模拟两个线程轮转调度。

线程初始化

func1func2是两个线程各自对应的线程函数,执行相反的 操作,func1点亮LED,而func2使LED熄灭:

void func1()
{
    while(1){
        if(*(uint32_t *)GPIOA_OCTL & (uint32_t)0b10){
            *(uint32_t *)GPIOA_OCTL &= (uint32_t)0xfffffffd;
        }
    }
}

void func2()
{
    while(1){
        if(!(*(uint32_t *)GPIOA_OCTL & (uint32_t)0b10)){
            *(uint32_t *)GPIOA_OCTL |= (uint32_t)0b10;
        }
    }
}

两个线程都有独立的栈空间:

__attribute__((aligned(4))) uint8_t func1_thread_stack[512] = {0};
__attribute__((aligned(4))) uint8_t func2_thread_stack[512] = {0};

关键的全局变量:

uint32_t func1_sp;
uint32_t func2_sp;
uint32_t main_sp;
uint32_t thread_sp;
uint32_t current_thread;
  • func1_spfunc2_sp用于分别记录两个线程的栈指针;
  • main_sp用于记录主栈指针;
  • thread_sp用于记录线程栈指针;
  • current_thread用于记录当前正在执行的线程。

以下是线程的初始化函数:

uint32_t thread_init(void (*entry)(), uint8_t *stack_addr, uint32_t stack_size)
{
    uint32_t *p = (uint32_t *)(stack_addr + stack_size - 32 * 4);
    p[0] = (uint32_t)entry;
    p[2] = 0x1880;
    return (uint32_t)p;
}

其中:

  • entry是线程的入口函数;
  • stack_addrstack_size用于初始化线程的栈空间;
  • 初始化栈空间时,mepc寄存器在线程栈中对应的位置 写入入口函数的地址,mstatus寄存器对应的位置写入0x1880, 使切换到该线程后启用全局中断。

main函数中初始化两个线程:

func1_sp = thread_init(func1, func1_thread_stack, 512);
func2_sp = thread_init(func2, func2_thread_stack, 512);
current_thread = 1;

切换到第一个线程执行

context_switch_to函数用于切换到第一个线程执行, 其声明如下:

extern void context_switch_to(uint32_t *to);

context_switch_to函数的汇编实现:

    .globl context_switch_to
context_switch_to:
    la t0, main_sp
    sw sp, (t0)

    lw sp, (a0)
context_switch_exit:
    lw a0, 0 * 4(sp)
    csrw mepc, a0

    lw x1, 1 * 4(sp)

    lw a0, 2 * 4(sp)
    csrs mstatus, a0

    lw x3, 3 * 4(sp)
    lw x4, 4 * 4(sp)
    ; ...
    lw x31, 31 * 4(sp)

    addi sp, sp, 32 * 4

    mret

context_switch_to函数的关键操作:

  • 保存主栈指针;
  • 设置线程栈指针;
  • 从线程栈取线程入口函数地址写mepc寄存器;
  • 执行mret指令后,会进入到线程入口函数开始执行。

main函数中,切换到第一个线程:

context_switch_to(&func1_sp);

定时器中断与线程切换

_trap_entry是非向量中断入口,关于GD32VF103的 非向量中断机制,可参考本站文章 GD32VF103中断机制(非向量中断)。 定时器中断配置为非向量处理方式,在中断发生时, 保存当前线程的上下文。当前线程被中断的指令位置 记录在mepc寄存器中,保存mepc寄存器到线程栈中, 将线程栈指针保存到全局变量thread_sp,最后 调用handle_trap继续处理上下文切换。

    .globl _trap_entry
_trap_entry:
    addi sp, sp, -32 * 4

    sw x1, 1 * 4(sp)
    sw x3, 3 * 4(sp)
    sw x4, 4 * 4(sp)
    ; ...
    sw x31, 31 * 4(sp)

    csrr t0, mepc
    sw t0, 0 * 4(sp)

    la t0, thread_sp;
    sw sp, (t0);

    li t0, 0x80
    sw t0, 2 * 4(sp)

    csrr a0, mcause
    call handle_trap
1:
    j 1b

handle_trap在定时器中断时切换到另一个线程,修改 current_thread全局变量重新记录当前线程:

void handle_trap(uint32_t mcause)
{
    if((mcause & 0x80000000) && (mcause & 0xfff) == 7){
        // reset mtime
        *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME + 4) = 0;
        *(volatile uint32_t *)(TIMER_CTRL_ADDR + TIMER_MTIME) = 0;
        if(current_thread == 1){
            current_thread = 2;
            context_switch(&func1_sp, &func2_sp);
        } else{
            current_thread = 1;
            context_switch(&func2_sp, &func1_sp);
        }
    } else{
        asm volatile("nop");
        while(1);
    }
}

context_switch函数的声明如下,参数fromto 分别是源和目标线程的栈指针:

extern void context_switch(uint32_t *from, uint32_t *to);

context_switch的汇编实现:

    .globl context_switch
context_switch:
    la t0, thread_sp
    lw sp, (t0)
    sw sp, (a0)

    lw sp, (a1)
    j context_switch_exit

将在_trap_entry中临时存放在thread_sp中源线程的栈指针 保存到context_switch第一个参数所代表的源线程的栈指针, 从第二个参数取目标线程的栈指针设置sp寄存器,再执行 恢复目标线程上下文的操作。

验证

执行构建并烧写到开发板,复位后可以看到LED亮灭循环。

参考