1、背景

        本文章主要说明 rtthread 内核线程是如何切换的,初学者刚从裸机开发接触 RTOS 时难免会有些不适应,明白这部分原理之后就会对 RTOS 有更深的理解。在学习内核线程切换原理之前需要有以下基础知识铺垫。本文以 arm 公司的 Cortex-M3 内核为例。

2、基础知识

  • CM3 拥有通用寄存器 R0-R15 以及一些特殊功能寄存器(中断屏蔽寄存器等等)
  • R0-R12 都是通用寄存器,用来临时存储程序运行时产生的数据
  • R13 这个寄存器存储堆栈指针,在 CM3 内核中一共有两个堆栈指针(MSP、PSP),于是 CM3 支持两个堆栈。在启动文件中定义的那个栈空间属于主栈,还有一个在我们创建线程时的栈属于线程栈。这两个栈空间不是同一个空间。

主堆栈指针(MSP),这是默认的堆栈指针,在裸机开发中只是用这一个指针,由 OS 内核、中断服务程序以及所有需要特权访问的应用程序代码使用。

进程堆栈指针(PSP),用于常规的应用程序代码,比如线程。

  • R14 也叫做连接寄存器LR,在调用子程序时存储返回地址
  • R15 也叫做程序计数器 (PC,program counter),因为 CM3 内部使用了指令流水线,PC 中存放的是当前指令的地址+4,也就是下一条指令的地址。
  • 栈空间的定义 : 向下生长的栈。也就是说每次执行一个 push(压栈)命令,栈指针向下减小一个单元,每次执行pop命令,栈指针增加一个单元。如下图所示
  • reactiveredistemplate不进行线程切换 rtthread线程切换_单片机

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 在最低的地址。如图所示,此图出自野火。
  • reactiveredistemplate不进行线程切换 rtthread线程切换_arm_02

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的值吗?

  • 答案是肯定的,因为我们传入的 sp 地址就指向了线程栈地址的偏移16个字处,而内核压栈时,先自动压入 r0、r1、r2、r3、r12、lr、pc、psr 这8个字的空间,按照顺序压,先压psr,然后我们手动压 r4 - r11 ,也是按照顺序压,先压r11。此时这16个字的空间就被填满了,这也是为什么线程栈结构体中的成员变量的顺序不是随便填的(个人理解)。

问题点二:当我进入hard_fault 异常时,我能否获取到当前线程栈指针,从而拿到 pc 指针来判断程序出错的位置?

  • 答案是可以的,rt-thread 已经帮我们重写了 hard_fault 服务程序