在GD32VF103实现简单线程切换
本文通过一个最小化的实验,在GD32VF103下使用C和汇编实现两个 线程函数的切换,简单介绍上下文切换的机制。
完整程序下载地址:https://www.jiawei.site/downloads/gd32/gd32_context.zip
背景
RTOS引入了线程的概念,从一个线程切换到另一个线程时,需要 将当前线程的上下文进行保存,以便下次恢复执行时能从中断处 继续运行。恢复将执行线程的上下文时,CPU重新加载上次运行时 的寄存器状态、栈指针以及程序计数器。这个过程即上下文切换, 是操作系统调度机制的核心之一。
每个线程都有独立的栈,用于保存该线程运行期间的局部变量 和上下文信息。当发生上下文切换时,系统会把当前线程的寄存器 内容保存到它的栈中,然后从下一个线程的栈中取出之前保存的 寄存器值,从而恢复该线程的运行现场。
基本思路
本实验的核心目标是在裸机环境下模拟线程的切换过程,从而理解 RTOS 中线程调度和上下文切换的底层机制。裸机工程见本站文章: 从零开始创建GD32VF103 Makefile工程。
初始化两个线程,每个线程拥有独立的线程栈,并分别绑定到各自 的线程函数。让定时器产生周期性中断,在定时器中断发生时从一个 线程切换到另一个线程,模拟两个线程轮转调度。
线程初始化
func1和func2是两个线程各自对应的线程函数,执行相反的
操作,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_sp和func2_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_addr和stack_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函数的声明如下,参数from和to
分别是源和目标线程的栈指针:
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亮灭循环。