0.前言
本节是基础中的基础,是后续所有学习的基础,务必搞清楚,弄明白。
学习本节需要一点点前置知识:
- 基本的ARM架构知识(CPU的的结构与工作流程)
- 简单的汇编认识(几个指令、几个概念就可以看懂涉及的汇编代码)
- 以上两个快速看这个:4.ARM架构和汇编简明教程_哔哩哔哩_bilibili
- C语言双向链表、结构体、函数指针
- 函数的本质(了解一个函数在运行过程中的方方面面):5.简单的C函数反汇编码分析_哔哩哔哩_bilibili
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}