1. 什么是线程?
线程就是系统所执行的一个一个独立的且无法返回的最小程序。
我们通常会把系统分割成一个又一个线程来执行。
比如说,一边读取室温,一边在屏幕上显示。把读取室温程序作为一个线程,在屏幕上显示程序作为一个线程,两者以极快的速度来回切换执行,看上去就像是同时执行一样。
线程对象的主要属性
- **线程栈指针:**由于线程会被系统切换来切换去,所以需要一段内存保存线程内部参数
- 线程栈大小
- **函数入口:**线程是运行函数的载体,被切换时函数入口会被放到线程栈中
2. RT-Thread 内核对线程的管理
线程首先需要被初始化(本次实现是静态初始化),然后会被挂到就绪列表里。调度器从就绪列表中取出线程执行。
遇到事件发生时,线程会被换下,切换为另一个线程执行。在这个过程中,需要保存线程的现场,即把 ARM Cortex3 的 16 个寄存器的值全部塞到线程栈中。然后加载另一个线程的现场,即把另一个线程的线程栈中保存的寄存器值拿出来给 CPU。
问题:如果第一次执行线程,该如何加载现场?
- 需要在初始化时设置线程栈,即设置各个寄存器的默认值
- 硬件会帮助我们完成加载现场,硬件中有专门保存栈指针的寄存器 - PSP,指明 PSP 的数值后,硬件会自动帮助我们提取栈中的值,按照固定次序放到 CPU 寄存器中。还有一部分寄存器需要我们手动装载。
3. 线程编程
目标:
实现线程的定义与两个线程间的切换。
方法:
** ==两个线程初始化后,放入到就绪列表。然后调度器(已经初始化)从就绪列表中取出第一个线程执行,然后第一个线程隔一段时间后就绪,切换到第二个线程执行,第二个线程隔一段时间就绪再去执行第一个线程,如此来回交替。==**
实验:
在线程1 和 线程 2中各自使标志位置1,退出时置0,观察两个标志位的波形
3.1 程序架构与文件架构
源函数文件:
-
thread.c : 定义线程初始化函数
- cpuport.c : 定义了线程栈操作函数
-
secheduler.c : 定义调度器操作函数与就绪列表
- rtservice.h : 定义了就绪列表操作函数
-
context_rvds.s : 汇编,线程栈切换函数
头文件:
- rtdef.h : 各种数据类型以及对象的定义
- rtconfig.h : 可能需要用户配置的宏
- thread.h : 包含了线程与调度器操作函数的声明
- rthw.h : 声明汇编函数(线程切换函数)
3.2 函数实现
程序的函数主要包括
3.2.1 线程对象定义(rtdef.h 文件中)以及初始化(thread.c)
线程对象的定义
/* 控制块定义 */
struct rt_thread
{
void *sp; /* 线程栈指针 */
void *entry; /* 线程入口地址 */
void *parameter; /* 线程形参 */
void *stack_addr; /* 线程栈起始地址 */
rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */
rt_list_t tlist; /* 线程链表节点 */
};
线程对象初始化
主要是线程栈的初始化!!
#include <rtthread.h>
extern rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr);
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;
}
线程栈初始化 ==rt_uint8_t *rt_hw_stack_init(void *tentry,void *parameter,rt_uint8_t *stack_addr)==
主要是初始化线程栈,以后第一次调用线程的时候要用。线程栈中保存了入口函数地址,所以 CPU 才可以找到入口函数。
返回值是栈指针 SP
#include <rtthread.h>
/* 用于存储上一个线程的栈的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;
struct exception_stack_frame
{
/* 异常发生时,自动加载到CPU寄存器的内容 */
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
{
/* 异常发生时,需要手动加载到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;
};
/*
* rt_hw_stack_init()函数用来初始化线程栈,当线程第一次运行时,加载到CPU寄存器的参数就放在线程栈里面
*/
rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
/* 获取栈顶指针调用rt_hw_stack_init()时,传给stack_addr的是(栈顶指针-4) (5)*/
stk = stack_addr + sizeof(rt_uint32_t);
/* 让stk指针向下8字节对齐 (6)*/
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
/* stk指针继续向下移动sizeof(struct stack_frame)个偏移量 (7) */
stk-= sizeof(struct stack_frame);
/* 将stk指针强制转化为stack_frame类型后存储在stack_frame中, (8)*/
/* 从此之后stack_frame就指向了用户提供栈的栈顶 (8)*/
stack_frame = (struct stack_frame *)stk;
/* 以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame)个内存地址初始化为0xdeadbeef (9)*/
for (i = 0; i <sizeof(struct stack_frame) /sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
/* 初始化异常发生时自动保存的寄存器(10) */
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
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;
stack_frame->exception_stack_frame.pc = (unsigned long)tentry;/* 入口指针, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
return stk;
}
3.2.2 就绪列表
初始化完线程函数后,需要将其插入就绪列表中,就绪列表是存放链表节点的数组。节点中存放了 next 和 prev 指针
struct rt_list_node
{
struct rt_list_node *next; /* 指向后一个节点 */
struct rt_list_node *prev; /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t;
/* 控制块定义 */
struct rt_thread
{
void *sp; /* 线程栈指针 */
void *entry; /* 线程入口地址 */
void *parameter; /* 线程形参 */
void *stack_addr; /* 线程栈起始地址 */
rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */
rt_list_t tlist; /* 线程链表节点 */
};
就绪列表定义在 scheduler.c 中
/* 定义就绪列表 */
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
初始化完成后,插入就绪列表中:
rt_thread_init( &rt_flag1_thread, /* 线程控制块 */
flag1_thread_entry, /* 线程入口 */
RT_NULL, /* 线程形参地址 */
&rt_flag1_thread_stack[0], /* 线程栈起始地址 */
sizeof(rt_flag1_thread_stack) ); /* 线程栈大小,单位为字节 */
/* 将线程1插入就绪列表 */
rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
3.2.3 调度器初始化以及启动调度器
程序三大步骤:
- 调度器初始化以及线程初始化
- 将线程插入就绪列表
- 启动调度器,从就绪列表中取出线程对象执行
调度器函数主要分为:
-
调度器初始化 - **==void rt_system_scheduler_init(void)==**
-
启动调度器 - **==void rt_system_scheduler_start(void)==**
- 切换到第一个线程(汇编实现) - **==rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);==**
-
系统调度函数 - 在各自的线程中执行仅仅实现两个线程轮流切换 - ==void rt_schedule(void)==
- 产生上下文切换(汇编实现)- **==rt_hw_context_switch((rt_uint32_t)&from_thread->sp,(rt_uint32_t)&to_thread->sp);==**
/* 调度器初始化 */
rt_system_scheduler_init();
/* 线程初始化 */
rt_thread_init( &rt_flag2_thread, /* 线程控制块 */
flag2_thread_entry, /* 线程入口*/
RT_NULL, /* 线程形参地址 */
&rt_flag2_thread_stack[0], /* 线程栈起始地址 */
sizeof(rt_flag2_thread_stack) ); /* 线程栈大小,单位为字节 */
/* 将线程2插入就绪列表 */
rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );
/* 启动系统调度器 */
rt_system_scheduler_start();
- 调度器初始化
即让各链表节点自指:
/* 线程就绪列表初始化 */
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;
}
调度器启动
调度器从就绪列表中取出对象,但是就绪列表中存放的是链表。
问题来了,链表对象只有前后节点,如何通过链表节点来访问一个线程对象的其他属性?
==-- ==**==已知一个结构体里面的成员的地址,反推出该结构体的首地址!!!==**
/* 已知一个结构体里面的成员的地址,反推出该结构体的首地址 */
#define rt_container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
#define rt_list_entry(node, type, member) \
rt_container_of(node, type, member)
- 启动调度器函数 - **==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);
}
3.2.4 线程上下文切换
线程切换函数统一在 context_rvds.s 中实现
;*************************************************************************
; 全局变量
;*************************************************************************
IMPORT rt_thread_switch_interrupt_flag
IMPORT rt_interrupt_from_thread
IMPORT rt_interrupt_to_thread
;*************************************************************************
; 常量
;*************************************************************************
;-------------------------------------------------------------------------
;有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
;系统控制块外设SCB地址范围:0xE000ED00-0xE000ED3F
;-------------------------------------------------------------------------
;给数字常量取一个符号名,相当于 C 语言中的 define
SCB_VTOR EQU 0xE000ED08 ; 向量表偏移寄存器
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器
NVIC_SYSPRI2 EQU 0xE000ED20 ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI EQU 0x00FF0000 ; PendSV 优先级值 (lowest)
NVIC_PENDSVSET EQU 0x10000000 ; 触发PendSV exception的值
;*************************************************************************
; 代码产生指令
;*************************************************************************
;汇编一个新的代码段或者数据段
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
REQUIRE8
PRESERVE8
;/*
; *-----------------------------------------------------------------------
; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; *-----------------------------------------------------------------------
; */
;rt_hw_context_switch_to函数,调度器启动后需要调用第一个函数的入口
rt_hw_context_switch_to PROC
; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件调用
EXPORT rt_hw_context_switch_to
; ------------------------------------------------------------------------------------------
; 设置rt_interrupt_to_thread的值,即设置下一个将要运行的线程的栈地址
; --------------------------------------------------------------------------------------------
;一开始r0储存了函数的第一个形参 r0 = (rt_uint32_t)&to_thread->sp ,存储了要运行程序的栈指针
LDR r1, =rt_interrupt_to_thread ;将rt_interrupt_to_thread的地址加载到r1,以后修改r1就是修改rt_interrupt_to_thread
;==> rt_interrupt_to_thread = (rt_uint32_t)&to_thread->sp
STR r0, [r1] ;将r0的值存储到rt_interrupt_to_thread
; ------------------------------------------------------------------------------------------
; 设置rt_interrupt_from_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
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 ; 装载PenSV优先级寄存器
LDR.W r2, [r0,#0x00] ; 读 r2保存了NVIC_SYSPRI2的值
ORR r1,r1,r2 ; 改
STR r1, [r0] ; 写
; 触发 PendSV 异常 (产生上下文切换),手动开启中断,程序会调转到PendSV_Handler执行
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
; 开中断
CPSIE F
CPSIE I
; 永远不会到达这里
ENDP
;/*
; *-----------------------------------------------------------------------
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; *-----------------------------------------------------------------------
; */
;rt_hw_context_switch_interrupt
;EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch
; 设置中断标志位rt_thread_switch_interrupt_flag为1
LDR r2, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag的地址到r2
LDR r3, [r2] ; 加载rt_thread_switch_interrupt_flag的值到r3
CMP r3, #1 ; r3与1比较,相等则执行BEQ指令,否则不执行
BEQ _reswitch
MOV r3, #1 ; 设置r3的值为1
STR r3, [r2] ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1
; 设置rt_interrupt_from_thread的值
LDR r2, =rt_interrupt_from_thread ; 加载rt_interrupt_from_thread的地址到r2
STR r0, [r2] ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针
_reswitch
; 设置rt_interrupt_to_thread的值
LDR r2, =rt_interrupt_to_thread ; 加载rt_interrupt_from_thread的地址到r2
STR r1, [r2] ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针
; 触发PendSV异常,实现上下文切换,向PenSV状态寄存器写1后,就可以产生终端
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
; 子程序返回
BX LR
; 子程序结束
ENDP
;/*
; *-----------------------------------------------------------------------
; * 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
; 失能中断,为了保护上下文切换不被中断
MRS r2, PRIMASK
CPSID I
; 获取中断标志位,看看是否为0
LDR r0, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag的地址到r0
LDR r1, [r0] ; 加载rt_thread_switch_interrupt_flag的值到r1
CBZ r1, pendsv_exit ; 判断r1是否为0,为0则跳转到pendsv_exit
; r1不为0则清0
MOV r1, #0x00
STR r1, [r0] ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0
; 判断rt_interrupt_from_thread的值是否为0
LDR r0, =rt_interrupt_from_thread ; 加载rt_interrupt_from_thread的地址到r0
LDR r1, [r0] ; 加载rt_interrupt_from_thread的值到r1
CBZ r1, switch_to_thread ; 判断r1是否为0,为0则跳转到switch_to_thread
; 第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread
; ========================== 上文保存 ==============================
; 当进入PendSVC Handler时,上一个线程运行的环境即:
; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
; 这些CPU寄存器的值会自动保存到线程的栈中,剩下的r4~r11需要手动保存
MRS r1, psp ; 获取线程栈指针到r1
STMFD r1!, {r4 - r11} ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)
LDR r0, [r0] ; 加载r0指向值到r0,即r0=rt_interrupt_from_thread
STR r1, [r0] ; 将r1的值存储到r0,即更新线程栈sp
; ========================== 下文切换 ==============================
switch_to_thread
LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread的地址到r1
; rt_interrupt_to_thread是一个全局变量,里面存的是线程栈指针SP的指针
LDR r1, [r1] ; 加载rt_interrupt_to_thread的值到r1,即sp指针的指针
LDR r1, [r1] ; 加载rt_interrupt_to_thread的值到r1,即sp
LDMFD r1!, {r4 - r11} ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
MSR psp, r1 ;将线程栈指针更新到PSP
pendsv_exit
; 恢复中断
MSR PRIMASK, r2
ORR lr, lr, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
BX lr ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。
; PendSV_Handler 子程序结束
ENDP
ALIGN 4
END
3.3 实验
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于