从0写RT-Thread内核之线程定义及切换的实现具体可以分为以下六步来实现
一:分别定义线程栈、线程函数、线程控制块;
ALIGN(RT_ALIGN_SIZE)//设置4字节对齐
/* 定义线程栈 */
rt_uint8_t rt_flag1_thread_stack[512];
rt_uint8_t rt_flag2_thread_stack[512];
/* 线程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();//此函数在下面介绍}
}
/* 此结构体在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;/*
*************************************************************************
* 双向链表结构体
*************************************************************************
*/
struct rt_list_node
{struct rt_list_node *next; /* 指向后一个节点 */struct rt_list_node *prev; /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t; /* 在main.c定义线程控制块 */
struct rt_thread rt_flag1_thread;
struct rt_thread rt_flag2_thread;
二:用线程初始化函数rt_thread_init(函数体如下图)调用rt_list_init函数初始化rt_list_t类型的节点,其实就是将节点里面的next和prev这两个节点指针指向节点本身,然后把上面第一步定义的线程栈、线程函数、线程控制块这三部分联系起来,实际上就是初始化线程控制块,由该函数的函数体我们知道它还调用了rt_hw_stack_init初始化了线程的栈。
/* 初始化链表节点(在rtserver.h中定义) */
rt_inline void rt_list_init(rt_list_t *l)
{l->next = l->prev = l;
}
三:定义就绪列表并把线程插入到就绪列表中(线程控制块里有一个tlist成员,数据类型为rt_list_t,我们将线程插入就绪列表里面,就是通过将线程控制块的tlist这个节点插入到就绪列表中来实现的。如果把就绪列表比作晾衣杆,线程是衣服,那tlist就是晾衣架,每个线程都自带晾衣架,就是为了把自己挂在各种不同的链表中。)
/* 定义就绪列表 */
extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];就绪列表的下标对应的是线程的优先级,但是目前我们的线程还不支持优先级,有关支持多优先级的知识点我们后面会讲到,所以flag1和flag2线程在插入到就绪列表的时候,可以随便选择插入的位置。我们这里将flag1线程插入到就绪列表下表为0的链表中,flag2线程插入到就绪列表下标为1的链表中。/* 将线程1插入到就绪列表 */
rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );/* 将线程2插入到就绪列表 */
rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );
四:调度器的初始化(初始化就绪列表里面各个不同优先级的链表,其实就是将节点里面的next和prev这两个节点指针指向节点本身)
/* 初始化系统调度器 */
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;/* 初始化线程休眠列表,当线程创建好没有启动之前会被放入到这个列表 */rt_list_init(&rt_thread_defunct);
}
五:使用rt_system_scheduler_start()启动调度器,手动指定第一个运行的线程并切换到该线程中去(利用rt_hw_context_switch_to函数切换线程,该函数用于第一次线程的切换),该函数只是开启中断并设置中断标志位,真正实现线程切换是在中断服务函数里。
/* 启动系统调度器 */
void rt_system_scheduler_start(void)
{register struct rt_thread *to_thread;/* 手动指定第一个运行的线程 *//* rt_list_entry是一个已知一个结构体里面成员的地址,反推出该结构体的首地址的宏 *//* 此处是通过struct rt_thread里面tlist成员获取该结构体首地址,即获取该线程控制块的首地址 */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);
}
六:定义一个void rt_schedule(void)函数,并在其中调用rt_hw_context_switch()函数(该函数实现新老线程的切换,和第五点中的rt_hw_context_switch_to()一样,都只是开启中断并设置中断标志位,真正的线程切换在中断服务函数中实现)
/* 系统调度 */
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); }
中断服务函数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 PROCEXPORT PendSV_Handler; 失能中断,为了保护上下文切换不被中断MRS r2, PRIMASKCPSID I; 获取中断标志位,看看是否为0LDR r0, =rt_thread_switch_interrupt_flag ; 加载rt_thread_switch_interrupt_flag的地址到r0LDR r1, [r0] ; 加载rt_thread_switch_interrupt_flag的值到r1CBZ r1, pendsv_exit ; 判断r1是否为0,为0则跳转到pendsv_exit; r1不为0则清0MOV r1, #0x00STR r1, [r0] ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0; 判断rt_interrupt_from_thread的值是否为0LDR r0, =rt_interrupt_from_thread ; 加载rt_interrupt_from_thread的地址到r0LDR r1, [r0] ; 加载rt_interrupt_from_thread的值到r1CBZ 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 ; 获取线程栈指针到r1STMFD r1!, {r4 - r11} ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)LDR r0, [r0] ; 加载r0指向值到r0,即r0=rt_interrupt_from_threadSTR r1, [r0] ; 将r1的值存储到r0,即更新线程栈sp; ========================== 下文切换 ==============================
switch_to_threadLDR 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,即spLDMFD r1!, {r4 - r11} ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11MSR psp, r1 ;将线程栈指针更新到PSPpendsv_exit; 恢复中断MSR PRIMASK, r2ORR lr, lr, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1BX lr ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参); 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。; PendSV_Handler 子程序结束ENDP ALIGN 4END
通过上面六个步骤,我们已经完成了线程切换的各个函数,接下来的是我们main()函数,先初始化调度器,然后分别初始化线程1和2并把两者分别插入到就绪列表中的0号和1号链表,然后启动系统调度器,启动系统调度器之后就会进行线程的第一次切换,切换到线程flag1的函数中去,在该函数的最后又会切换到线程flag2中去,然后线程flag2又会切换会线程flag1,一直这样来回切换。
/************************************************************************* @brief main函数* @param 无* @retval 无** @attention*********************************************************************** */
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();
}
最后我们通过软件仿真的效果如下图,可以看到flag1和flag2两个变量波形的占空比为1/4,刚好符合我们两个线程函数来回切换的效果。
最后声明一下,我这里只是对学习的知识点进行总结,本文章的大多数知识来自于野火公司出版的《RT-Thread 内核实现与应用开发实战—基于STM32》,这本书非常不错,有志学习RT-Thread物联网操作系统的人可以考虑一下。