1、背景
????????本文章主要说明 rtthread 内核线程是如何切换的,初学者刚从裸机开发接触 RTOS 时难免会有些不适应,明白这部分原理之后就会对 RTOS 有更深的理解。在学习内核线程切换原理之前需要有以下基础知识铺垫。本文以 arm 公司的 Cortex-M3 内核为例。
2、基础知识
-
R13 这个寄存器存储堆栈指针,在 CM3 内核中一共有两个堆栈指针(MSP、PSP),于是 CM3 支持两个堆栈。在启动文件中定义的那个栈空间属于主栈,还有一个在我们创建线程时的栈属于线程栈。这两个栈空间不是同一个空间。
主堆栈指针(MSP),这是默认的堆栈指针,在裸机开发中只是用这一个指针,由 OS 内核、中断服务程序以及所有需要特权访问的应用程序代码使用。
进程堆栈指针(PSP),用于常规的应用程序代码,比如线程。
-
R14 也叫做连接寄存器LR,在调用子程序时存储返回地址 -
R15 也叫做程序计数器 (PC,program counter),因为 CM3 内部使用了指令流水线,PC 中存放的是当前指令的地址+4,也就是下一条指令的地址。 -
栈空间的定义 : 向下生长的栈。也就是说每次执行一个 push(压栈)命令,栈指针向下减小一个单元,每次执行pop命令,栈指针增加一个单元。如下图所示
3、代码分析
3.1 内核寄存器结构体定义
struct exception_stack_frame
?{
? ? ?rt_uint32_t r0;
? ? ?rt_uint32_t r1;
? ? ?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
?{
? ? ?/* r4 ~ r11 register */
? ? ?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;
?};
?struct exception_info
?{
? ? ?rt_uint32_t exc_return;
? ? ?struct stack_frame stack_frame;
?};
3.2 初始化线程栈
rt_uint8_t *rt_hw_stack_init(void ? ? ? *tentry, ?//线程函数入口地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? void ? ? ? *parameter,//线程函数参数
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? rt_uint8_t *stack_addr,//栈地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? void ? ? ? *texit)//线程退出时的函数地址
?{
? ? ?struct stack_frame *stack_frame;
? ? ?rt_uint8_t ? ? ? ? *stk;
? ? ?unsigned long ? ? ? i;
??
? ? ?stk ?= stack_addr + sizeof(rt_uint32_t);//栈地址 + 4 个字节
? ? ?stk ?= (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);//向下8个字节对齐
? ? ?stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节)
??
? ? ?stack_frame = (struct stack_frame *)stk;//强制转换为 struct stack_frame 类型
??
? ? ?/* init all register */
? ? ?for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
? ? {
? ? ? ? ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;//初始化这16个字的空间为 0xdeadbeef
? ? }
? /* 初始化高8个字的内存空间 */
? ? ?stack_frame->exception_stack_frame.r0 ?= (unsigned long)parameter; /* r0 : argument */
? ? ?stack_frame->exception_stack_frame.r1 ?= 0; ? ? ? ? ? ? ? ? ? ? ? ?/* r1 */
? ? ?stack_frame->exception_stack_frame.r2 ?= 0; ? ? ? ? ? ? ? ? ? ? ? ?/* r2 */
? ? ?stack_frame->exception_stack_frame.r3 ?= 0; ? ? ? ? ? ? ? ? ? ? ? ?/* r3 */
? ? ?stack_frame->exception_stack_frame.r12 = 0; ? ? ? ? ? ? ? ? ? ? ? ?/* r12 */
? ? ?stack_frame->exception_stack_frame.lr ?= (unsigned long)texit; ? ? /* lr */
? ? ?stack_frame->exception_stack_frame.pc ?= (unsigned long)tentry; ? ?/* entry point, pc */
? ? ?stack_frame->exception_stack_frame.psr = 0x01000000L; ? ? ? ? ? ? ?/* PSR */
??
?#if USE_FPU
? ? ?stack_frame->flag = 0;
?#endif /* USE_FPU */
??
? ? ?/* return task's current stack address */
? ? ?return stk;
?}
-
stack_addr 这个参数为当前线程栈的结束地址,也就是最高的地址。为什么是最高地址?原因是上面说过的栈空间的定义。 -
struct stack_frame 这个结构体的定义可不是胡乱定义的,里面是有顺序要求的。 -
stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节) 为何偏移这么多字节,因为这16个字的空间的每个地址要按照结构体成员变量的地址去存放,即 psr 要放到这个栈的最高地址,r4 在最低的地址。如图所示,此图出自野火。
3.3 执行线程切换
????????阅读这段代码之前得知道,cm3 内核执行中断或异常时,r0、r1、r2、r3、r12、lr、pc、psr,这些寄存器是自动压栈的。
?rt_hw_context_switch ? ?PROC
? ? ?EXPORT rt_hw_context_switch ;导出函数,此操作能够让C侧代码调用,C侧的第一个参数为当前线程栈sp的指针,第二个
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?;为将要执行的线程栈 sp 的指针
? ? ?; set rt_thread_switch_interrupt_flag to 1
? ? ?LDR ? ? r2, =rt_thread_switch_interrupt_flag;中断标志位 L2 = &rt_thread_switch_interrupt_flag
? ? ?LDR ? ? r3, [r2];r3 = *r2也就是 r3 = rt_thread_switch_interrupt_flag
? ? ?CMP ? ? r3, #1 ;判断rt_thread_switch_interrupt_flag 与 1是否相等
? ? ?BEQ ? ? _reswitch ;相等跳转 _reswitch,当第2次执行线程切换时,rt_thread_switch_interrupt_flag被pendsv置0
? ? ?;既然是第二次,所以当前线程具有上文所以要把sp存到rt_interrupt_from_thread,直接跳转_reswitch
? ? ?;表示的是第一次切换线程,因为没有上文,所以直接跳到 _reswitch
? ? ?MOV ? ? r3, #1 ;不等则置1
? ? ?STR ? ? r3, [r2] ;rt_thread_switch_interrupt_flag = 1
??
? ? ?LDR ? ? r2, =rt_interrupt_from_thread ? ; set rt_interrupt_from_thread
? ? ?STR ? ? r0, [r2] ? ? ? ? ? ? ? ? ? ? ? ?;rt_interrupt_from_thread = r0,&sp,当前线程sp的地址
??
?_reswitch
? ? ?LDR ? ? r2, =rt_interrupt_to_thread ? ? ; set rt_interrupt_to_thread
? ? ?STR ? ? r1, [r2] ? ? ? ? ? ? ? ? ? ? ? ?;rt_interrupt_to_thread = r1,&sp,将要只要的线程的sp的地址
? ;触发 pendsv 中断,线程切换的核心
? ? ?LDR ? ? r0, =NVIC_INT_CTRL ? ? ? ? ? ? ?; trigger the PendSV exception (causes context switch)
? ? ?LDR ? ? r1, =NVIC_PENDSVSET
? ? ?STR ? ? r1, [r0]
? ? ?BX ? ? ?LR
? ? ?ENDP
??
?; r0 --> switch from thread stack
?; r1 --> switch to thread stack
?; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
?PendSV_Handler ? PROC
? ? ?EXPORT PendSV_Handler
??
? ? ?; 关闭所有中断以保护这一过程不被打断
? ? ?MRS ? ? r2, PRIMASK
? ? ?CPSID ? I
??
? ? ?; rt_thread_switch_interrupt_flag 为 1时才继续接下来的操作,为0则跳转 pendsv_exit
? ? ?LDR ? ? r0, =rt_thread_switch_interrupt_flag
? ? ?LDR ? ? r1, [r0]
? ? ?CBZ ? ? r1, pendsv_exit ? ? ? ? ; pendsv already handled
??
? ? ?; 清楚中断标志位
? ? ?MOV ? ? r1, #0x00
? ? ?STR ? ? r1, [r0]
? ;判断 rt_interrupt_from_thread 是否为0,即是否是第一次切换线程,是0则跳转至switch_to_thread
? ? ?LDR ? ? r0, =rt_interrupt_from_thread
? ? ?LDR ? ? r1, [r0]
? ? ?CBZ ? ? r1, switch_to_thread ? ?; skip register save at the first time
??
? ? ?MRS ? ? r1, psp ? ? ? ? ? ? ? ? ; 获取当前线程栈指针到r1中
? ? ?STMFD ? r1!, {r4 - r11} ? ? ? ? ; 将r4 - r11寄存器中的值压入当前栈空间中
? ? ?LDR ? ? r0, [r0]
? ? ?STR ? ? r1, [r0] ? ? ? ? ? ? ? ?; 把当前线程栈指针记录到 rt_interrupt_from_thread 中,即当前栈指针 sp 中
??
?switch_to_thread
? ? ?LDR ? ? r1, =rt_interrupt_to_thread;获取将要执行的栈的sp的地址
? ? ?LDR ? ? r1, [r1]
? ? ?LDR ? ? r1, [r1] ? ? ? ? ? ? ? ?
??
? ? ?LDMFD ? r1!, {r4 - r11} ? ? ? ? ; 从将要执行的栈中弹出这个线程中的寄存器r4-r11
? ? ?MSR ? ? psp, r1 ? ? ? ? ? ? ? ? ; 并把要执行的线程的栈指针给到 psp
??
?pendsv_exit
? ? ?; 恢复中断
? ? ?MSR ? ? PRIMASK, r2
? ;由于cm3 内核发生中断时,堆栈指针使用的是msp,因此退出中断时,确保使用psp指针,实际操作就是对,lr寄存的位3进行置1就控制 ? ; 退出中断后使用psp中断
? ? ?ORR ? ? lr, lr, #0x04
? ? ?BX ? ? ?lr ;退出中断时使用psp指针
? ? ?ENDP
-
通过解读 pendsv 中断代码我们知道,在进入 pendsv 中断前,r0、r1、r2、r3、r12、lr、pc、psr 这些寄存器已经自动压入了当前栈中。 -
当 pendsv 中断退出时,新的将要执行的线程的中断上下文(r0、r1、r2、r3、r12、lr、pc、ps)会自动的从这个线程栈中弹出,程序计数器 PC 就得到了这个将要执行的线程的pc值,这个线程中用到的其他寄存器的值也从这个新的线程栈中得到了(一部分手动pop,一部分自动pop)。
问题点一:我可以通过这个线程栈指针访问到R0~R15的值吗?
问题点二:当我进入hard_fault 异常时,我能否获取到当前线程栈指针,从而拿到 pc 指针来判断程序出错的位置?
|