《野火RT-Thread内核实现与应用开发实战》笔记1. RT-Thread仿真工程

1. 重要数据结构介绍
  1. 每个线程拥有一个独有的结构体:

    struct rt_thread {
        void        *sp;            /* 线程栈指针 */
        void        *entry;         /* 线程入口地址 */
        void        *parameter;     /* 线程形参 */
        void        *stack_addr;    /* 线程栈起始地址 */
        rt_uint32_t  stack_size;    /* 线程栈大小,单位为字节 */
        
        rt_list_t    tlist;         /* 线程链表节点 */
    };
    
  2. 有一个线程就绪列表:

    rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
    

    线程就绪列表会通过tlist将每个线程挂载起来。

  3. 异常栈,这些数据会保存在每个线程栈的顶端:

    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. 重要函数介绍
  1. 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异常,开中断。
    }
    
  2. 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(线程的形参)。
    
  3. 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. 详细过程
  1. 初始化后rt_flag1_threadrt_flag2_thread的值如下图所示。

    1. 线程1控制块的栈指针sp为0x200001D8,栈起始地址为0x20000018,加上栈大小512字节,可知栈顶地址应该为0x20000218,而现在sp指向0x200001D8,说明栈空间已被用掉了(0x20000218-0x200001D8=40H)64个字节,这64个字节就是存储的初始psr-r0,r11-r4共16个32bits的寄存器的值。
    2. 线程1控制块的线程入口地址entry为0x00000589,实际上线程1的函数地址为0x00000588,传函数指针进来的时候被自动加1了,不知道是什么原因。
    3. 线程1控制块的列表节点tlist的地址为0x2000042C,其中成员prev的内容为0x20000450,为rt_thread_priority_table[0]的地址,由于该优先级组只添加了线程1一个线程,所以成员next也是指向rt_thread_priority_table[0]。
    4. 线程2控制块的sp为0x200003D8,栈起始地址为0x20000218,栈顶地址为0x20000418,线程入口地址为0x000005AC,列表节点地址为0x20000448,元素prev和next都指向rt_thread_priority_table[1]。
      在这里插入图片描述
  2. 线程1和线程2的栈空间初始值填充如下图所示,r15为线程入口地址,psr-r0的值在进入PendSV_Handler时从CPU的寄存器自动保存下来,在退出PendSV_Handler时自动加载到CPU的寄存器,而r11~R4需要在中断函数中手动同步,初始线程1的sp指针指向0x200001D8,线程2的sp指针指向0x200003D8。
    在这里插入图片描述

  3. 初始化各数据结构后,接下来进入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,程序继续执行,进入到该函数内部。

  4. 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
    

    先介绍三个重要的全局变量:

    1. rt_interrupt_from_thread,存储上一个(现在运行的)线程控制块的栈指针sp的地址
    2. rt_interrupt_to_thread,存储下一个(将要切换的)线程控制块的栈指针sp的地址
    3. 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)中断处理函数。

  5. 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的栈顶。
    在这里插入图片描述

  6. 执行线程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变化情况如下图:
    在这里插入图片描述

  7. 进入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()函数开头定义了两个局部变量,它们将占用一定栈空间。

  8. 进入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中断。

  9. 刚进入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内核寄存器变化如下:
    在这里插入图片描述

  10. 执行线程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变化情况如下图:
    在这里插入图片描述

  11. 进入rt_schedule()函数,切换下一个线程为线程1,进入rt_hw_context_switch()函数,修改变量rt_thread_switch_interrupt_flagrt_interrupt_from_threadrt_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运行,循环往复即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值