1. 重要数据结构介绍
-
每个线程拥有一个独有的结构体:
struct rt_thread { void *sp; /* 线程栈指针 */ void *entry; /* 线程入口地址 */ void *parameter; /* 线程形参 */ void *stack_addr; /* 线程栈起始地址 */ rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */ rt_list_t tlist; /* 线程链表节点 */ };
-
有一个线程就绪列表:
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
线程就绪列表会通过
tlist
将每个线程挂载起来。 -
异常栈,这些数据会保存在每个线程栈的顶端:
struct exception_stack_frame { /* 异常发生时,自动加载到CPU寄存器的内容 */ rt_uint32_t r0; rt_uint32_t r10; rt_uint32_t r2; rt_uint32_t r3; rt_uint32_t r12; rt_uint32_t lr; rt_uint32_t pc; rt_uint32_t psr; }; struct stack_frame { /* 异常发生时,需手动加载到CPU寄存器的内容 */ rt_uint32_t r4; rt_uint32_t r5; rt_uint32_t r6; rt_uint32_t r7; rt_uint32_t r8; rt_uint32_t r9; rt_uint32_t r10; rt_uint32_t r11; struct exception_stack_frame exception_stack_frame; };
2. 重要函数介绍
-
void rt_hw_context_switch_to (rt_uint32_t to)
void rt_hw_context_switch_to (rt_uint32_t to) { // 1. 根据调用时传入的形参设置rt_interrupt_to_thread的值。 // 2. 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换。 // 3. 设置rt_thread_switch_interrupt_flag的值为1。 // 4. 设置PendSV异常优先级,触发PendSV异常,开中断。 }
-
void PendSV_Handler(void)
// 0. 进入中断时,PSP指针向下移动,CPU的xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)会保存到上一个线程的栈中,然后CPU的SP选择为MSP。 void PendSV_Handler(void) { // 1. 禁能中断,保护上下文切换不被打断。 // 2. 获取rt_thread_switch_interrupt_flag的值,如果为0就跳转到pendsv_exit;否则清零rt_thread_switch_interrupt_flag,继续往下执行。 // 3. 获取rt_interrupt_from_thread的值,如果为0就跳转到switch_to_thread;否则继续往下执行。 // 4. 获取PSP的值,将CPU中R4~R11的值保存到上一个线程的栈中。 // 5. switch_to_thread: 根据rt_interrupt_to_thread的值,将下一个线程的sp指针赋值R1,将下一个线程的栈中的r4~r11加载到CPU的R4~R11,将偏移后的位置更新到PSP。 // 6. pendsv_exit: 恢复中断,设置CPU的SP选择PSP,异常返回。 } // 7. 退出中断时,PSP指针向上移动,下一个线程的栈中剩下的内容会自动加载到CPU的xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)。
-
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to)
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to) { // 1. 根据调用时传入的形参设置rt_interrupt_from_thread和rt_interrupt_to_thread的值。 // 2. 设置rt_thread_switch_interrupt_flag的值为1。 // 3. 触发PendSV中断。 }
3. 详细过程
-
初始化后
rt_flag1_thread
和rt_flag2_thread
的值如下图所示。- 线程1控制块的栈指针sp为0x200001D8,栈起始地址为0x20000018,加上栈大小512字节,可知栈顶地址应该为0x20000218,而现在sp指向0x200001D8,说明栈空间已被用掉了(0x20000218-0x200001D8=40H)64个字节,这64个字节就是存储的初始psr-r0,r11-r4共16个32bits的寄存器的值。
- 线程1控制块的线程入口地址entry为0x00000589,实际上线程1的函数地址为0x00000588,传函数指针进来的时候被自动加1了,不知道是什么原因。
- 线程1控制块的列表节点tlist的地址为0x2000042C,其中成员prev的内容为0x20000450,为rt_thread_priority_table[0]的地址,由于该优先级组只添加了线程1一个线程,所以成员next也是指向rt_thread_priority_table[0]。
- 线程2控制块的sp为0x200003D8,栈起始地址为0x20000218,栈顶地址为0x20000418,线程入口地址为0x000005AC,列表节点地址为0x20000448,元素prev和next都指向rt_thread_priority_table[1]。
-
线程1和线程2的栈空间初始值填充如下图所示,r15为线程入口地址,psr-r0的值在进入
PendSV_Handler
时从CPU的寄存器自动保存下来,在退出PendSV_Handler
时自动加载到CPU的寄存器,而r11~R4需要在中断函数中手动同步,初始线程1的sp指针指向0x200001D8,线程2的sp指针指向0x200003D8。
-
初始化各数据结构后,接下来进入
void rt_system_scheduler_start (void)
函数,该函数实体如下:/* 启动系统调度器 */ void rt_system_scheduler_start (void) { register struct rt_thread *to_thread; /* 手动指定第一个运行的线程 */ to_thread = rt_list_entry(rt_thread_priority_table[0].next, struct rt_thread, tlist); rt_current_thread = to_thread; /* 切换到第一个线程,该函数在context_rvds.S中实现, 在rthw.h声明,用于实现第一次线程切换。 当一个汇编函数在C文件中调用的时候,如果有形参, 则执行的时候会将形参传入到CPU寄存器r0。 */ rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); }
启动第一次任务调度,也就是第一次切换线程,这里手动指定第一个运行的线程为线程1,to_thread就是获得的线程1的控制块的首地址(0x20000418),然后执行
void rt_hw_context_switch_to(rt_uint32_t to)
函数,传入线程1控制块的栈指针sp的地址,该函数用汇编语言编写,在进入函数的时候,形参将会赋值给CPU寄存器R0,程序继续执行,进入到该函数内部。 -
void rt_hw_context_switch_to(rt_uint32_t to)
函数实体如下:rt_hw_context_switch_to PROC ; 导出 rt_hw_context_switch_to,让其具有全局属性,可以在C文件调用 EXPORT rt_hw_context_switch_to ; 设置 rt_interrupt_to_thread 的值为下一个线程的栈指针SP的指针 ; 将 rt_interrupt_to_thread 的地址加载到r1 LDR r1, =rt_interrupt_to_thread ; 将 r0 的值存储到 rt_interrupt_to_thread STR r0, [r1] ; 设置 rt_interrupt_from_thread 的值为0,表示启动第一次线程切换 ; 将 rt_interrupt_from_thread 的地址加载到r1 LDR r1, =rt_interrupt_from_thread ; 配置 r0 等于0 MOV r0, #0x0 ; 将 r0 的值存储到 rt_interrupt_from_thread STR r0, [r1] ; 设置中断标志位 rt_thread_switch_interrupt_flag 的值为1 ; 将 rt_thread_switch_interrupt_flag 的地址加载到r1 LDR r1, =rt_thread_switch_interrupt_flag ; 配置 r0 等于1 MOV r0, #0x1 ; 将 r0 的值存储到 rt_thread_switch_interrupt_flag STR r0, [r1] ; 设置 PendSV 异常的优先级 LDR r0, =NVIC_SYSPRI2 LDR r1, =NVIC_PENDSV_PRI LDR.W r2, [r0, #0x00] ; 读 ORR r1, r1, r2 ; 改 STR r1, [r0] ; 写 ; 触发 PendSV 异常(产生上下文切换) LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] ; 开中断 CPSIE F CPSIE I ; 永远不会到达这里 ENDP
先介绍三个重要的全局变量:
rt_interrupt_from_thread
,存储上一个(现在运行的)线程控制块的栈指针sp的地址rt_interrupt_to_thread
,存储下一个(将要切换的)线程控制块的栈指针sp的地址rt_thread_switch_interrupt_flag
,中断标志位
rt_hw_context_switch_to
函数的目的就是将变量rt_interrupt_to_thread
赋值为线程1的线程控制块的栈指针sp的地址(0x20000418),将变量rt_interrupt_from_thread
赋值为0,将变量rt_thread_switch_interrupt_flag
赋值为1,最后设置PendSV异常中断优先级,开启并触发中断。该函数执行完成后将会进入
void PendSV_Handler(void)
中断处理函数。 -
void PendSV_Handler(void)
函数实体如下:PendSV_Handler PROC EXPORT PendSV_Handler ; 禁能中断,为了保护上下文切换不被中断 MRS r2, PRIMASK CPSID I ; 获取中断标志位,看看是否为0 ; 加载rt_thread_switch_interrupt_flag的地址到r0 LDR r0, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag的值到r1 LDR r1, [r0] ; 判断r1是否为0,为0则跳转到pendsv_exit CBZ r1, pendsv_exit ; r1不为0则清0 MOV r1, #0x00 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0 STR r1, [r0] ; 判断rt_interrupt_from_thread的值是否为0 ; 加载rt_interrupt_from_thread的地址到r0 LDR r0, =rt_interrupt_from_thread ; 加载rt_interrupt_from_thread的值到r1 LDR r1, [r0] ; 判断r1是否为0,为0则跳转到switch_to_thread ; 第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread CBZ r1, switch_to_thread ;--------------------------------- 上文保存 ------------------------------------ ; 当进入PendSVC_Handler时,上一个线程运行的环境即: ; xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参) ; 这些CPU寄存器的值会自动保存到线程的栈中,剩下的r4~r11需要手动保存 ; 获取线程栈指针到r1 MRS r1, psp ; 将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次) STMFD r1!, {r4 - r11} ; 加载r0指向值到r0,即r0=rt_interrupt_from_thread LDR r0, [r0] ; 将r1的值存储到r0,即更新线程栈sp STR r1, [r0] ;--------------------------------- 下文切换 ------------------------------------ switch_to_thread ; 加载rt_interrupt_to_thread的地址到r1 ; rt_interrupt_to_thread是一个全局变量,里面存的是线程栈指针SP的指针 LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread的值到r1,即sp指针的指针 LDR r1, [r1] ; 加载rt_interrupt_to_thread的值到r1,即sp LDR r1, [r1] ; 将线程栈指针r1(先操作后递增)指向的内容加载到CPU寄存器r4~r11 LDMFD r1!, {r4 - r11} ; 将线程栈指针更新到PSP MSR psp, r1 pendsv_exit ; 恢复中断 MSR PRIMASK, r2 ; 确保异常返回使用的栈指针是PSP,即LR寄存器的位2要为1 ORR lr, lr, #0x04 ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器: ; xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参) ; 同时PSP的值也将更新,即指向线程栈的栈顶 BX lr ; PendSV_Handler子程序结束 ENDP
第一次进入该函数不会执行上文保存部分,因为
rt_interrupt_from_thread
的值为0,直接跳转到switch_to_thread
下文切换部分执行,该部分主要目的是将线程1栈中的r4-r11加载到CPU的R4-R11寄存器,因为rt_interrupt_to_thread
存储的是线程1的栈指针sp的地址(0x20000418),根据这个地址可以取出sp的值(0x200001D8),将这个值赋给R1,R1向上偏移32个字节(一个数据占4个字节,一共要加载8个数据)可将r4-r11加载到CPU寄存器,最后将R1的值赋给PSP(CPU有MSP和PSP,可选择作为CPU的SP指针),此时PSP的值为0x200001D8+32=0x200001F8,CPU内核寄存器截图如下:
最后选择PSP作为CPU的SP指针(R13),因为要保证该中断函数退出后就进入线程1执行,而此时CPU的R15寄存器的值还不是线程1的入口地址(0x00000588),中断函数退出时,会根据CPU的SP指针自动加载栈中内容到xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
,这样就可以把线程1栈中剩下的内容更新到CPU寄存器中了,并且实现线程1的切换。该中断函数退出后,CPU内核寄存器的值更新如下图所示,PSP指针已经指向0x20000218,即线程1的栈顶。
-
执行线程1,线程1实体如下:
void flag1_thread_entry (void *p_arg) { for (; ;){ flag1 = 1; delay(100); flag1 = 0; delay(100); /* 线程切换,这里是手动切换 */ rt_schedule(); } }
执行到
rt_schedule()
函数时,flag1和线程2中的flag2变化情况如下图:
-
进入
rt_schedule()
函数,该函数实体如下:void rt_schedule (void) { register struct rt_thread *to_thread; register struct rt_thread *from_thread; /* 两个线程轮流切换 */ if (rt_current_thread == rt_list_entry(rt_thread_priority_table[0].next, struct rt_thread, tlist)) { from_thread = rt_current_thread; to_thread = rt_list_entry(rt_thread_priority_table[1].next, struct rt_thread, tlist); rt_current_thread = to_thread; } else { from_thread = rt_current_thread; to_thread = rt_list_entry(rt_thread_priority_table[0].next, struct rt_thread, tlist); rt_current_thread = to_thread; } /* 产生上下文切换 */ rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_thread->sp); }
该函数的目的是让线程1和线程2轮流切换,如果当前运行的是线程1,那么接下来要切换到线程2;如果当前运行的是线程2,那么接下来要切换到线程1。显然现在运行的是线程1,那么接下来要切换到线程2,因此执行到
rt_hw_context_switch
函数时,from_thread
应该指向线程1控制块,to_thread
应该指向线程2控制块。另外还需注意的时,执行到
rt_hw_context_switch
函数时,PSP指针会从0x20000218移动到0x20000208,原因是rt_schedule()
函数开头定义了两个局部变量,它们将占用一定栈空间。 -
进入
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to)
函数,该函数实体如下:rt_hw_context_switch PROC EXPORT rt_hw_context_switch ; 设置中断标志位rt_thread_switch_interrupt_flag为1 ; 加载rt_thread_switch_interrupt_flag的地址到r2 LDR r2, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag的值到r3 LDR r3, [r2] ; r3与1比较,相等则执行BEQ指令,否则不执行 CMP r3, #1 BEQ _reswitch ; 设置r3的值为1 MOV r3, #1 ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1 STR r3, [r2] ; 设置rt_interrupt_from_thread的值 ; 加载rt_interrupt_from_thread的地址到r2 LDR r2, =rt_interrupt_from_thread ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针(SP)的指针 STR r0, [r2] _reswitch ; 设置rt_interrupt_to_thread的值 ; 加载rt_interrupt_to_thread的地址到r2 LDR r2, =rt_interrupt_to_thread ; 存储r1的值到rt_interrupt_to_thread,即下一个线程栈指针(SP)的指针 STR r1, [r2] ; 触发PendSV异常,实现上下文切换 LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] ; 子程序返回 BX LR ; 子程序结束 ENDP
该函数有两个形参from和to,进入该函数时,调用时传入的线程1栈指针sp的地址将被赋值给CPU寄存器R0,线程2栈指针sp的地址将被赋值给R1,接下来设置中断标志位
rt_thread_switch_interrupt_flag
为1,表示即将触发PendSV中断,然后将R0的值赋给全局变量rt_interrupt_from_thread
,将R1的值赋给全局变量rt_interrupt_to_thread
,最后触发PendSV中断。 -
刚进入
PendSV_Handler()
函数时,CPU内核寄存器变化如下图所示。CPU的xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
会自动保存到线程1的栈中,PSP会向下移动32个字节(从0x20000208到0x200001E8),同时CPU的R13(SP)切换为MSP。
此时线程栈中内容更新如下图:
程序继续向下执行,在上文保存部分会把CPU寄存器R4-R11的值保存到PSP指向的位置,并且更新最新的偏移位置到线程1的栈指针sp(0x200001C8),这时线程栈中内容更新如下:
程序继续向下执行,在下文切换部分会把线程2的栈指针sp(0x200003D8)指向的数据r4-r11加载到CPU的R4-R11寄存器,然后同样把最新偏移地址赋值给PSP,中断函数退出时,会自动把线程2的栈剩下的数据加载到CPU的xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
寄存器,同时PSP指针偏移到地址0x20000418处。中断函数退出后,CPU内核寄存器变化如下:
-
执行线程2,线程2实体如下:
void flag2_thread_entry (void *p_arg) { for (; ;){ flag2 = 1; delay(100); flag2 = 0; delay(100); /* 线程切换,这里是手动切换 */ rt_schedule(); } }
执行到
rt_schedule()
函数时,flag1和线程2中的flag2变化情况如下图:
-
进入
rt_schedule()
函数,切换下一个线程为线程1,进入rt_hw_context_switch()
函数,修改变量rt_thread_switch_interrupt_flag
,rt_interrupt_from_thread
,rt_interrupt_to_thread
的值,触发中断,进入PendSV_Handler()
函数,CPU的xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
会自动保存到线程2的栈中,PSP会向下移动32个字节(从0x20000408到0x200003E8),同时CPU的R13(SP)切换为MSP,此时线程栈中内容更新如下图:
接下来手动保存R4~R11寄存器的值到线程2的栈中,同时更新线程2的sp指针,保存结束后线程栈中内容更新如下:
接下来取出线程1的sp指针,将线程1栈中的r4-r11更新到CPU的R4-R11,PSP指针更新为0x200001E8,退出中断时自动将线程1栈中剩下的数据更新到CPU寄存器中,退出中断后进入线程1运行,循环往复即可。