3 线程与线程切换

0.前言

本节是基础中的基础,是后续所有学习的基础,务必搞清楚,弄明白。

学习本节需要一点点前置知识:

1.线程

1.1 提出问题

什么是线程?怎么切换线程?怎么保存线程?

韦大佬的深刻提问:

  • 线程是函数吗?函数需要保存吗?
    • 函数在Flash上,不会被破坏,无需保存
  • 函数里用到了全局变量,全局变量需要保存吗?
    • 全局变量在内存上,还能保存到哪里去?全局变量无需保存
  • 函数里用到了局部变量,局部变量需要保存吗?
    • 局部变量在栈里,也是在内存里,只要避免栈不被破坏即可,局部变量无需保存
  • 运算的中间值需要保存吗?中间值保存在哪里?
    • 所有的运算都在CPU中进行,运算的结果也保存在CPU寄存器里,
    • 如果运算过程中有中断来临,cpu会跳过去执行中断,执行完中断后再返回继续执行,凭什么能返回?
    • 因为中断前的环境参数(cpu寄存器的值)被保存在栈中,
  • 函数运行了哪里?需要保存吗?
    • 需要保存(不然怎么调用),它也是一个CPU寄存器,名为"PC",指示当前CPU运行的位置

综上:

  • 中断来临时什么需要保存?
    • CPU所有寄存器需要保存
  • 保存在哪里?
    • 保存在线程的栈里
  • 怎么理解CPU寄存器、怎么理解栈?
    • 当前CPU寄存器中的值代表当前运行环境,CPU寄存器=运行环境,
    • 栈就是内存RAM中的一片空间,存放函数运行过程中的局部变量

有了上述的基本概念,就可以解答这三个问题:

  • 什么叫线程?

    • 运行中的函数、被暂停运行的函数:函数代码+函数运行过程中在栈中的局部变量+运行过程中瞬间的CPU寄存器值
  • 怎么保存线程?

    • 把暂停瞬间的CPU寄存器值,保存进线程自身的栈里。
  • 怎么切换线程?

    • 切换之前先保存本线程,再把要切换的线程栈中存放的CPU寄存器值加载到CPU中。

要实现:

  • 线程的创建

  • 线程的保存

  • 线程的切换

  • 调度器的实现–程序自动切换线程

OK接下来就是要具体实现这些功能,火哥已经替我们把所有的东西都准备好了,自己去走一遍吧!

强烈建议大家拿着这份工程看这本节(进入Code下载Template工程):

链接:https://pan.baidu.com/s/1eciQGN3QdUKOPckF2p6-og?pwd=6666
提取码:6666

野火书籍配套的程序,注释之详细,简直“令人发指”:

同时我也已经打开所有的.c .h文件,关键部分已经打上断点,本节最后直接进入debug即可一步步观察底层做了什么!

1.2 创建线程

  • 先创建几个文件
    • rtdef.h 数据类型重定义、编译器相关宏定义
    • rtconfig.h裁剪系统功能
/*数据类型重定义 rtdef.h 第一次使用需要在include文件夹下面新建-->然后添加到工程rtt/source这个组文件*/
#ifndef __RT_DEF_H__
#define __RT_DEF_H__

/*
 *          数据类型
 */
/* RT-Thread 基础数据类型重定义*/
typedef signed   char                   rt_int8_t;
typedef signed   short                  rt_int16_t;
typedef signed   long                   rt_int32_t;
typedef unsigned char                   rt_uint8_t;
typedef unsigned short                  rt_uint16_t;
typedef unsigned long                   rt_uint32_t;
typedef int                             rt_bool_t;

/* 32bit CPU*/
typedef long                            rt_base_t;
typedef unsigned long                   rt_ubase_t;
typedef rt_base_t                       rt_err_t;
typedef rt_uint32_t                     rt_time_t;
typedef rt_uint32_t                     rt_tick_t;
typedef rt_base_t                       rt_flag_t;
typedef rt_ubase_t                      rt_size_t;
typedef rt_ubase_t                      rt_dev_t;
typedef rt_base_t                       rt_off_t;

/* 布尔数据类型重定义*/
#define RT_TRUE                         1
#define RT_FALSE                        0

/* RT-Thread 错误码重定义 */
#define RT_EOK                          0               /**< There is no error */
#define RT_ERROR                        1               /**< A generic error happens */
#define RT_ETIMEOUT                     2               /**< Timed out */
#define RT_EFULL                        3               /**< The resource is full */
#define RT_EEMPTY                       4               /**< The resource is empty */
#define RT_ENOMEM                       5               /**< No memory */
#define RT_ENOSYS                       6               /**< No system */
#define RT_EBUSY                        7               /**< Busy */
#define RT_EIO                          8               /**< IO error */
#define RT_EINTR                        9               /**< Interrupted system call */
#define RT_EINVAL                       10              /**< Invalid argument */


/*********  关于内联函数+内存对齐  根据不同的编译工具链 有一些不同 ***********/
// 使用keil mdk集成工具编译时,会自动定义宏__CC_ARM
#ifdef __CC_ARM
        #define rt_inline                   static __inline
        #define ALIGN(n)                    __attribute__((aligned(n)))
// 使用IAR集成工具编译
#elif defined (__IAR_SYSTEMS_ICC__)
    #define rt_inline                   static inline
        #define ALIGN(n)                    PRAGMA(data_alignment=n)
// 使用GNU (gcc)来编译工程
#elif defined (__GNUC__)
    #define rt_inline                   static __inline
    #define ALIGN(n)                    __attribute__((aligned(n)))
#else
    #error not supported tool chain
#endif

#define RT_ALIGN(size, align)           (((size) + (align) - 1) & ~((align) - 1))
#define RT_ALIGN_DOWN(size, align)      ((size) & ~((align) - 1))

#define RT_NULL                         (0)


// 双向链表节点数据类型
struct rt_list_node
{
	struct rt_list_node *next;
	struct rt_list_node *prev;
};
typedef struct rt_list_node rt_list_t;


#endif /* __RT_DEF_H__*/
/*rtconfig.h第一次使用需要在User文件夹下面新建然后添加到工程user这个组文件 */
//该文件用于裁剪系统功能

#ifndef __RTTHREAD_CFG_H__
#define __RTTHREAD_CFG_H__

#define RT_THREAD_PRIORITY_MAX  32     /* 最大优先级 */
#define RT_ALIGN_SIZE           4      /* 多少个字节对齐 */

#endif /* __RTTHREAD_CFG_H__ */

1.2.1 定义线程栈

在一个裸机系统中,有全局变量,有子函数调用,有中断发生。

那么系统在运行的时候,全局变量放在哪里?子函数调用时,局部变量放在哪里?中断发生时,函数返回地址放在哪里?

如果只是单纯的裸机编程,它们放哪里我们不用管,但是如果要写一个RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。

在裸机系统 中,他们统统放在一个叫栈的地方栈是单片机RAM里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。

在多线程系统中,每个线程都是独立的,互不干扰的,所以要为每个线程都分配独立的栈空间。

这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。

定义线程栈:

// 设置变量需要多少个字节对齐,对在它下面的变量起作用
// 4字节对齐
ALIGN(RT_ALIGN_SIZE)  
rt_uint8_t rt_flag1_thread_stack[512];
rt_uint8_t rt_flag2_thread_stack[512];

1.2.2 定义线程控制块

  • 线程控制块类型声明(在rtdef.h中)
// 线程结构体
struct rt_thread
{
    void        *sp;              /* 线程栈指针    指向栈顶 */
    void        *entry;           /* 线程入口地址 */
    void        *parameter;       /* 线程形参 */
    void        *stack_addr;      /* 线程起始地址  指向栈底 */
    rt_uint32_t stack_size;       /* 线程栈大小,单位为字节 */

    rt_list_t   tlist;            /* 线程双向链表节点,像一个钩子,通过它线程把自己挂载到各个链表上*/
};
// 重定义别名
typedef struct rt_thread *rt_thread_t;

在裸机系统中,程序的主体是CPU按照顺序执行的。在多线程系统中,线程的执行是由系统调度的。

而系统为了顺利的调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块就相当于线程的身份证,里面存有线程的所有信息,比如线程的栈指针,线程名称,线程的形参等。(面向对象设计的过程)

有了这个线程控制块之后,以后系统对线程的全部操作都可以通过这个线程控制块来实现。

  • 定义两个线程控制块

    • /* 定义线程控制块 */
      struct rt_thread rt_flag1_thread;
      struct rt_thread rt_flag2_thread;
      

1.2.3 定义线程函数

线程是一个独立的函数,函数主体无限循环且不能返回

/* 线程1  void* 用于线程传参*/
void flag1_thread_entry( void *p_arg )
{
    for( ;; )
    {
        flag1 = 1;
        delay( 100 );
        flag1 = 0;
        delay( 100 );

        /* 线程切换,这里是手动切换 */
        rt_schedule();
    }
}
/* 线程2 */
void flag2_thread_entry( void *p_arg )
{
    for( ;; )
    {
        flag2 = 1;
        delay( 100 );
        flag2 = 0;
        delay( 100 );

        /* 线程切换,这里是手动切换 */
        rt_schedule();
    }
}

1.2.4 实现线程创建函数

线程的栈,线程的函数实体,线程的控制块 这三者最终需要联系在一起才能由系统进行统一调度

/*线程初始化函数,将线程的栈,线程的函数实体,线程的控制块联系起来*/
rt_err_t  rt_thread_init(struct rt_thread * thread,			// 线程控制块
							void (*entry)(void *parameter), // 线程入口函数
							void *parameter,				// 线程入口函数参数
						    void *stack_start,				// 线程栈起始地址
							rt_uint32_t stack_size)		    // 线程栈大小				
{
	// 初始化线程链表->自己指向自己
	rt_list_init(&(thread->tlist));	
	// 线程入口函数(函数名字就是一个地址)
	thread->entry=(void *)entry;
	// 线程函数参数
	thread->parameter=parameter;
	// 线程栈参数
	thread->stack_addr=stack_start;
	thread->stack_size=stack_size;
	// 初始化线程栈,并返回线程栈顶指针
	thread->sp=(void *)rt_hw_stack_init(thread->entry,
										thread->parameter,
										(void *)((char *)thread->stack_addr + thread->stack_size - 4));
	return RT_EOK;
}
1.2.4.1 初始化线程栈

- 进入到最精彩的部分,线程栈的初始化,这部分代码根据芯片架构的不同而不同,是系统移植的核心部分,把它定义在cpuport.c中。

/* 线程栈初始化*/
// 当线程第一次运行的时候,加载到CPU寄存器的参数,就放在线程栈里面 
rt_uint8_t *rt_hw_stack_init(void       *tentry,	 //   线程函数入口地址               
                             void       *parameter,  //   线程形参             
                             rt_uint8_t *stack_addr) //   线程栈顶地址-4         
{
	// 栈帧结构 指针
	struct stack_frame *stack_frame;
	// 线程栈指针 单位:字节	
	rt_uint8_t         *stk;
	// 遍历局部变量 
	unsigned long       i;
	
	// 1.获取栈顶指针 
	// rt_hw_stack_init 在被调用的时候,传给stack_addr的是(栈顶指针)-4,所以要加回来
	stk = stack_addr+ sizeof(rt_uint32_t);
	// 2.stk指针向下8字节对齐,通常栈保持4字节对齐就行(32位),这里后续兼容浮点运行(64位操作)
	stk	= (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk,8);
	// 3.stk指针继续向下移动sizeof(struct stack_frame)个偏移
	stk -= sizeof(struct stack_frame); 
	// 4.将stk指针强制转化为stack_frame类型后存到stack_frame
	stack_frame = (struct stack_frame *)stk;
	// 5.以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame)块内存->初始化为0xdeadbeef 
	// stack_frame存放了栈顶地址,可以采用数组的操作的形式
	for(i=0; i<sizeof(struct stack_frame)/sizeof(rt_uint32_t);i++)
	{
		((rt_uint32_t *)stack_frame)[i]=0xdeadbeef;
	}
	// 6.初始化异常发生时自动保存的寄存器 r0-r3、r12、r14、r15、xPSR
    /* r0 : 存放函数的形参,这是调用标准,c语言向汇编传参 */
	stack_frame->exception_stack_frame.r0=(unsigned long)parameter; 
    stack_frame->exception_stack_frame.r1= 0;
	stack_frame->exception_stack_frame.r2=0;
	stack_frame->exception_stack_frame.r3=0;
	stack_frame->exception_stack_frame.r12=0;
	stack_frame->exception_stack_frame.lr=0;
    /*pc: 存放现场保护后,接下来跳转执行的代码->线程函数,强制转化为一个32位数*/
	stack_frame->exception_stack_frame.pc=(unsigned long)tentry; 
    /*psr: 第24位必须为1 0000 0001 0000 0000 0000 0000 0000 0000  指令集*/
	stack_frame->exception_stack_frame.psr=0x01000000L; 
	/* 返回线程栈指针 */
	return stk;
}
  • 栈帧结构体,在栈中一个统一的结构,他指代ARM的内部CPU寄存器

    • 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;
      };
      

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2.5初始化线程并将线程插入到就绪列表

线程创建好之后,系统要调度,要使用这个线程,但是那么多线程,调度器怎么找到他们?

答案:从就绪列表里面找,把所有准备好的线程通过链表串在这个就绪列表中,调度器从这里找。

就绪列表在(调度器)scheduler.c中定义

  • 定义就绪列表数组

    • /* 线程就绪列表*/
      /**************数组下标代表优先级,线程按照优先级插入各自的链表中****************/
      rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; 
      // 指向当前运行线程
      struct rt_thread *rt_current_thread;
      
  • 初始化线程并插入就绪列表

    • /* 初始化线程 */
      rt_thread_init(&rt_flag1_thread,                 /* 线程控制块 */
                      flag1_thread_entry,               /* 线程入口地址 */
                      RT_NULL,                          /* 线程形参 */
                      &rt_flag1_thread_stack[0],        /* 线程栈起始地址 */
                      sizeof(rt_flag1_thread_stack) );  /* 线程栈大小,单位为字节 */
      /* 将线程插入到就绪列表 */
      rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
      
      /* 初始化线程 */
      rt_thread_init(&rt_flag2_thread,                 /* 线程控制块 */
                      flag2_thread_entry,               /* 线程入口地址 */
                      RT_NULL,                          /* 线程形参 */
                      &rt_flag2_thread_stack[0],        /* 线程栈起始地址 */
                      sizeof(rt_flag2_thread_stack) );  /* 线程栈大小,单位为字节 */
      /* 将线程插入到就绪列表 */
      rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
      

2.线程切换与调度器实现

关于调度器的功能在scheduler.c文件中实现。

系统调度的整个流程:

1.初始化调度器,初始化就绪链表,使链表头指向自己,此时就绪链表为空,当前线程控制块为空,无线程运行;

2.启动调度器,调度器按照优先级从高到低查看就绪链表是否为空,然后加载最高优先级就绪链表中的的第一个线程控制块运行;

3.与当前运行线程控制块比较,产生线程切换

Tip:当前没有支持多优先级,所以需要手动指定线程和手动切换线程。

2.1 实现调度器

调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。

从代码上来看,调度器也就是由几个全局变量和一些可以实现线程切换的函数组成

  • 定义线程调度所需变量

    • /* 用于存储上一个线程的栈的sp的指针*/
      rt_uint32_t rt_interrupt_from_thread;
      
      /* 用于存储下一个将要运行的线程的栈的sp的指针*/
      rt_uint32_t rt_interrupt_to_thread;
      
      /* PendSV中断服务函数执行标志*/
      rt_uint32_t rt_thread_switch_interrupt_flag;
      
  • 调度器初始化

/* 初始化系统调度器 */
void rt_system_scheduler_init(void)
{
    register rt_base_t offset;                                    
    /* 线程就绪列表初始化 使得就绪表中的每个链表指向自身 */
    for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++) 
    {
            rt_list_init(&rt_thread_priority_table[offset]);
    }
    /* 初始化当前线程控制块指针 */
    rt_current_thread = RT_NULL;//                                       
}
  • 启动调度器
/* 启动系统调度器 */
void rt_system_scheduler_start(void)
{
    /*将要切换的线程*/
    register struct rt_thread *to_thread;

    /* 手动指定第一个运行的线程,是一个宏定义,参考导航界面文章合集c语言宏定义*/                                    
    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);      // 注意这是一个二级指针 结构体指针成员sp的地址     
}

2.2 第一次线程切换

需要在汇编中实现线程切换了,需要少许汇编的概念,加油~

;*************************************************************************
;   全局变量,        使用IMPORT关键字导入一些全局变量,这三个全局变量在cpuport.c中定义
;*************************************************************************
    IMPORT rt_thread_switch_interrupt_flag
    IMPORT rt_interrupt_from_thread
    IMPORT rt_interrupt_to_thread
;*************************************************************************
;                                 常量
;*************************************************************************
;-------------------------------------------------------------------------
;有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
;还有一本中文Cortex M3与M4权威指南,讲的很详细 P205
;系统控制块外设SCB地址范围:0xE000ED00-0xE000ED3F
;-------------------------------------------------------------------------
NVIC_INT_CTRL   EQU     0xE000ED04     ; 中断控制状态寄存器
SCB_VTOR        EQU     0xE000ED08     ; 向量表偏移寄存器
NVIC_SYSPRI2    EQU     0xE000ED20     ; 系统优先级寄存器
NVIC_PENDSV_PRI EQU     0x00FF0000     ; PendSV 优先级值 (lowest)
NVIC_PENDSVSET  EQU     0x10000000     ; 触发PendSV exception的值

;*************************************************************************
;  代码产生指令,段名:.text;CODE:代码;READONLY:只读;ALIGN=2:四字节对齐;THUMB指令代码;当前文件的栈按照8字节对齐
;*************************************************************************
    AREA |.text|, CODE, READONLY, ALIGN=2
    THUMB
    REQUIRE8
    PRESERVE8        
    
; *-----------------------------------------------------------------------
; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to   第一次线程切换r0传入将要切换的->线程栈顶指针的地址;
; * 该函数用于开启第一次线程切换
; *-----------------------------------------------------------------------		
rt_hw_context_switch_to    PROC 
	; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件调用
	EXPORT rt_hw_context_switch_to
		
    ; 设置rt_interrupt_to_thread的值
    LDR     r1, =rt_interrupt_to_thread             ;将变量rt_interrupt_to_thread的地址加载到r1
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_to_thread

    ; 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换,之前没有线程
    LDR     r1, =rt_interrupt_from_thread           ;将rt_interrupt_from_thread的地址加载到r1
    MOV     r0, #0x0                                ;配置r0等于0
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_from_thread

    ; 设置中断标志位rt_thread_switch_interrupt_flag的值为1
    ; 当执行PendSVC Handler时,rt_thread_switch_interrupt_flag的值会被清0。
    LDR     r1, =rt_thread_switch_interrupt_flag    ;将rt_thread_switch_interrupt_flag的地址加载到r1
    MOV     r0, #1                                  ;配置r0等于1
    STR     r0, [r1]                                ;将r0的值存储到rt_thread_switch_interrupt_flag

    ; 设置 PendSV 异常的优先级为最低。
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]       ; 读
    ORR     r1,r1,r2             ; 改 按位或
    STR     r1, [r0]             ; 写

    ; 触发 PendSV 异常 (产生上下文切换),如果前面关了,还要等中断打开才能去执行PendSV中断服务函数。
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 开中断
    CPSIE   F
    CPSIE   I

    ; 永远不会到达这里
    ENDP
  • PendSV_Handler()函数是真正实现线程上下文切换的地方

    • ; *-----------------------------------------------------------------------
      ; * void PendSV_Handler(void);
      ; * 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
          ; 1.失能中断,为了保护上下文切换不被中断                   
          MRS     r2, PRIMASK
          CPSID   I
      
          ; 2.获取中断标志位,看看是否为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 
          
          ; 3.清除中断标志位加载rt_thread_switch_interrupt_flag的值到r1
          ; r1不为0则清0                                               
          MOV     r1, #0x00
          ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0
          STR     r1, [r0]
          
          ; 4.判断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                                     
          
      
      	; 5.------------------上文保存 rt_interrupt_from_thread---------------------------------------
          ; 获取线程栈指针到r1,此时psp指针指向硬件自动保存的cpu参数的位置,接下来保存r4-r11
          MRS     r1, psp                                                 
          ; 连续压栈:将CPU寄存器r4~r11的值存储到r1指向的地址,r1不断减小(高->低),始终指向栈顶
          STMFD   r1!, {r4 - r11}                                       
          ; 加载r0指向值->到r0,即r0=rt_interrupt_from_thread,[r0]:线程栈顶sp地址
          LDR     r0, [r0]                                               
          ; 更新线程栈顶sp
          STR     r1, [r0]                                                
      
      	; 6.-----------------下文切换 rt_interrupt_to_thread-------------------------------------------
      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   --->需要手动恢复CPU环境
          LDMFD   r1!, {r4 - r11}                                          
          ;将线程栈指针更新到PSP									--->中断退出的时候会自动从栈里恢复其余CPU寄存器	
          MSR     psp, r1                                                  
      
      ;退出Pendsv中断
      pendsv_exit
          ; 恢复中断
          MSR     PRIMASK, r2                                              
      
          ; 确保异常返回使用的栈指针是PSP,即LR寄存器的位2要为1  按位或置1:0010 
          ORR     lr, lr, #0x04                                            
          ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
          ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
          ; 同时PSP的值也将更新,即指向线程栈的栈顶					--->退出中断,同时会触发硬件恢复CPU寄存器 
          BX      lr                                         		           
      
      ; PendSV_Handler 子程序结束
          ENDP                                                             
      

2.3 系统调度

  • rt_schedule()实现调度函数

    • /* 系统调度 */
      void rt_schedule(void)
      {
          struct rt_thread *to_thread;
          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);
      }
      
  • rt_hw_contex_switch()实现上下文切换函数

    • 
      ; *----------------------------------------------------------------------
      ; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
      ; * r0 --> from
      ; * r1 --> to
      ; * 就是栈顶sp地址在交换
      ; *----------------------------------------------------------------------
      ; */
      rt_hw_context_switch    PROC
          EXPORT rt_hw_context_switch
      
          ; ------------1.设置中断标志位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]                                           
      
          ; ------------2.设置rt_interrupt_from_thread的值<---形参1:R0-------------------------------         
          ; 加载rt_interrupt_from_thread的地址到r2
          LDR     r2, =rt_interrupt_from_thread                    
          ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针
          STR     r0, [r2]                                          
      
      _reswitch
          ; ------------3.设置rt_interrupt_to_thread的值<---形参2:R1 ------------------------------          
          ; 加载rt_interrupt_from_thread的地址到r2
          LDR     r2, =rt_interrupt_to_thread                        
          ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针
          STR     r1, [r2]                                          
      
          ; ------------4.触发PendSV异常,在PendSV中断中中实现上下文切换------------------------------      
          LDR     r0, =NVIC_INT_CTRL
          LDR     r1, =NVIC_PENDSVSET
          STR     r1, [r0]		;将PENDSV中断配置装入中断控制寄存器
          ; 子程序返回
          BX      LR                                                
      ; 子程序结束
      ENDP                                                      
      

3.main函数

测试代码

#include <rtthread.h>
#include "ARMCM3.h"


rt_uint8_t flag1;
rt_uint8_t flag2;


extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];


/* 定义线程控制块 */
struct rt_thread rt_flag1_thread;
struct rt_thread rt_flag2_thread;

ALIGN(RT_ALIGN_SIZE)
/* 定义线程栈 */
rt_uint8_t rt_flag1_thread_stack[512];
rt_uint8_t rt_flag2_thread_stack[512];

/* 线程声明 */
void flag1_thread_entry(void *p_arg);
void flag2_thread_entry(void *p_arg);

void delay(uint32_t count);

int main(void)
{
    /* 硬件初始化 */
    /* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */

    /* 调度器初始化 */
    rt_system_scheduler_init();


    /* 初始化线程 */
    rt_thread_init( &rt_flag1_thread,                 /* 线程控制块 */
                    flag1_thread_entry,               /* 线程入口地址 */
                    RT_NULL,                          /* 线程形参 */
                    &rt_flag1_thread_stack[0],        /* 线程栈起始地址 */
                    sizeof(rt_flag1_thread_stack) );  /* 线程栈大小,单位为字节 */
    /* 将线程插入到就绪列表 */
    rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );

    /* 初始化线程 */
    rt_thread_init( &rt_flag2_thread,                 /* 线程控制块 */
                    flag2_thread_entry,               /* 线程入口地址 */
                    RT_NULL,                          /* 线程形参 */
                    &rt_flag2_thread_stack[0],        /* 线程栈起始地址 */
                    sizeof(rt_flag2_thread_stack) );  /* 线程栈大小,单位为字节 */
    /* 将线程插入到就绪列表 */
    rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );

    /* 启动系统调度器 */
    rt_system_scheduler_start();
}



/* 软件延时 */
void delay (uint32_t count)
{
    for(; count!=0; count--);
}

/* 线程1 */
void flag1_thread_entry( void *p_arg )
{
    for( ;; )
    {
        flag1 = 1;
        delay( 100 );
        flag1 = 0;
        delay( 100 );

        /* 线程切换,这里是手动切换 */
        rt_schedule();//                                
    }
}

/* 线程2 */
void flag2_thread_entry( void *p_arg )
{
    for( ;; )
    {
        flag2 = 1;
        delay( 100 );
        flag2 = 0;
        delay( 100 );

        /* 线程切换,这里是手动切换 */
        rt_schedule();//                                
    }
}

Step 1 进入debug模式:

在这里插入图片描述

Step 2 把main函数中的待观察变量加入到逻辑分析仪,改变显示状态位bit:
在这里插入图片描述
在这里插入图片描述

Step 3:全速运行(先失能所有断点),观察现象:
在这里插入图片描述

4.全细节调试

观察重要变量在栈中的分布:

首先贴一张图,程序的分布:

在这里插入图片描述

- 去各个.c文件里面找到重要的全局变量,加入到观察窗口里面,可以发现,变量确实在0x2000 0000这个地方开始占据内存,

请添加图片描述

flag1、flag2为rt_uint8_t类型,占据1个字节;

rt_flag1_thread和rt_flag2_thread为struct
rt_thread类型,占据28个字节即0x1C个字节;

rt_flag1_thread_stack和rt_flag2_thread_stack为rt_uint8_t类型的数组,[512]占据512即0x200个字节;

ok,结合红色标记的地址栏发现正好符合。

  • 打开几个内存窗口,观察一下

从内存起始地址开始观察
请添加图片描述

从线程1的栈地址开始观察

在这里插入图片描述

从线程2的栈地址开始观察

在这里插入图片描述

- 观察线程的创建过程(主要是线程栈),打上断点,逐步运行:

接合堆栈调用和内存图验证程序执行过程:

请添加图片描述

  • 观察上文切换
    将环境参数压入栈中保存:STMFD r1! , {r4 - r11}
    请添加图片描述
    执行STMFD之后,可以清楚地看到内存栈的变化:

请添加图片描述

  • 观察下文切换:
  • 从线程栈中加载环境参数:LDMFD r1!, {r4 - r11} 请添加图片描述

请添加图片描述
请添加图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值