第3章
线程的定义与线程切换的实现
本章我们真正开始从0到1写RT-Thread,必须学会创建线程,并重点掌握线程是如何切换的。因为线程的切换是由汇编代码来完成的,所以代码可能比较难懂,但是这里会尽量把代码讲透彻。如果本章内容学不会,后面的内容将无从下手。
在这一章中,我们会创建两个线程,并让这两个线程不断地切换,线程的主体都是让一个变量按照一定的频率翻转,通过KEIL的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图如图3-1所示。
图3-1 线程轮流切换波形图
其实,图3-1中显示的波形图效果并不是真正的多线程系统中线程切换的效果图,这个效果其实可以完全由裸机代码来实现,具体参见代码清单3-1。
代码清单3-1 裸机系统中两个变量轮流翻转
1 /* flag 必须定义成全局变量才能添加到逻辑分析仪里面以观察波形 2 *在逻辑分析仪中要设置为 bit 模式才能看到波形,不能用默认的模拟量 3 */ 4 uint32_t flag1; 5 uint32_t flag2; 6 7 8 /* 软件延时,先不必考虑具体的时间 */ 9 void delay( uint32_t count ) 10 { 11 for (; count! =0; count--); 12 } 13 14 int main(void) 15 { 16 /* 无限循环,顺序执行 */ 17 for (; ; ) { 18 flag1 = 1; 19 delay( 100 ); 20 flag1 = 0; 21 delay( 100 ); 22 23 flag2 = 1; 24 delay( 100 ); 25 flag2 = 0; 26 delay( 100 ); 27 } 28 }
在多线程系统中,两个线程不断切换的效果图应该像图3-2所示那样,即两个变量的波形是完全一样的,就好像CPU在同时做两件事一样,这才是多线程的意义。虽然两者的波形图一样,但是,代码的实现方式是完全不一样的,由原来的顺序执行变成了线程的主动切换,这是根本区别。本章只是开始,我们先掌握好线程是如何切换的,在后面的章节中,会陆续完善功能代码,加入系统调度,实现真正的多线程。
图3-2 多线程系统线程切换波形图
3.1 什么是线程
在裸机系统中,系统的主体就是main()函数里面顺序执行的无限循环,在这个无限循环中,CPU按照顺序完成各种操作。在多线程系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数称为线程。线程的大概形式参见代码清单3-2。
代码清单3-2 多线程系统中线程的形式
1 void thread_entry (void *parg) 2 { 3 /* 线程主体,无限循环且不能返回 */ 4 for (; ; ) { 5 /* 线程主体代码 */ 6 } 7 }
3.2 创建线程
3.2.1 定义线程栈
我们先回想一下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生,那么系统在运行时全局变量放在哪里,子函数调用时局部变量放在哪里,中断发生时函数返回地址是什么?如果只是单纯的裸机编程,则不必考虑这些,但是如果要写一个RTOS,那么我们必须弄清楚这些变量是如何存储的。
在裸机系统中,它们统统存放在栈中。栈是单片机RAM里一段连续的内存空间,栈的大小一般在启动文件或者链接脚本中指定,最后由C库函数__main进行初始化。
但是,在多线程系统中,每个线程都是独立的、互不干扰的,所以要为每个线程都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。
本章我们要实现两个变量按照一定的频率轮流翻转,每个变量对应一个线程,那么就需要定义两个线程栈,具体参见代码清单3-3。在多线程系统中,有多少个线程就需要定义多少个线程栈。
代码清单3-3 定义线程栈
1 ALIGN(RT_ALIGN_SIZE) (2)
2 /* 定义线程栈 */
3 rt_uint8_t rt_flag1_thread_stack[512]; (1)
4 rt_uint8_t rt_flag2_thread_stack[512];
代码清单3-3(1):线程栈其实就是一个预先定义好的全局数据,数据类型为rt_uint8_t,大小我们设置为512。在RT-Thread中,凡是涉及数据类型的地方,RT-Thread都会将标准的C数据类型用typedef重新设置一个类型名,以rt前缀开头。这些经过重定义的数据类型放在rtdef.h(第一次使用rtdef.h时,需要在include文件夹下面新建并添加到工程rtt/source组文件)头文件中,具体参见代码清单3-4。其中,除了rt_uint8_t外,其他数据类型重定义是本章后面内容中需要用到的,这里统一给出,后面将不再赘述。
代码清单3-4 rtdef.h中的数据类型
1 #ifndef __RT_DEF_H__ 2 #define __RT_DEF_H__ 3 4 /* 5 ************************************************************************* 6 * 数据类型 7 ************************************************************************* 8 */ 9 /* RT-Thread 基础数据类型重定义 */ 10 typedef signed char rt_int8_t; 11 typedef signed short rt_int16_t; 12 typedef signed long rt_int32_t; 13 typedef unsigned char rt_uint8_t; 14 typedef unsigned short rt_uint16_t; 15 typedef unsigned long rt_uint32_t; 16 typedef int rt_bool_t; 17 18 /* 32bit CPU */ 19 typedef long rt_base_t; 20 typedef unsigned long rt_ubase_t; 21 22 typedef rt_base_t rt_err_t; 23 typedef rt_uint32_t rt_time_t; 24 typedef rt_uint32_t rt_tick_t; 25 typedef rt_base_t rt_flag_t; 26 typedef rt_ubase_t rt_size_t; 27 typedef rt_ubase_t rt_dev_t; 28 typedef rt_base_t rt_off_t; 29 30 /* 布尔数据类型重定义 */ 31 #define RT_TRUE 1 32 #define RT_FALSE 0 33 34 #ifdef __CC_ARM 35 #define rt_inline static __inline 36 #define ALIGN(n) __attribute__((aligned(n))) 37 38 #elif defined (__IAR_SYSTEMS_ICC__) 39 #define rt_inline static inline 40 #define ALIGN(n) PRAGMA(data_alignment=n) 41 42 #elif defined (__GNUC__) 43 #define rt_inline static __inline 44 #define ALIGN(n) __attribute__((aligned(n))) 45 #else 46 #error not supported tool chain 47 #endif 48 49 50 #define RT_ALIGN(size, align) (((size) + (align)-1) & ~((align)-1)) 51 #define RT_ALIGN_DOWN(size, align) ((size) & ~((align)-1)) 52 53 54 #define RT_NULL (0) 55 56 #endif/* __RT_DEF_H__ */
代码清单3-3(2):设置变量需要多少个字节对齐,对它下面的变量起作用。ALIGN是一个带参宏,在rtdef.h中定义,具体参见代码清单3-4。RT_ALIGN_SIZE是一个在rtconfig.h(第一次使用rtconfig.h时,需要在User文件夹下面新建,然后将其添加到工程user组文件)中定义的宏,默认为4,表示4个字节对齐,具体参见代码清单3-5。
代码清单3-5 RT_ALIGN_SIZE宏定义
1 #ifndef __RTTHREAD_CFG_H__ 2 #define __RTTHREAD_CFG_H__ 3 4 #define RT_ALIGN_SIZE 4 /*多少个字节对齐 */ 5 6 7 #endif/* __RTTHREAD_CFG_H__ */
3.2.2 定义线程函数
线程是一个独立的函数,函数主体无限循环且不能返回。本章我们在main.c中定义的两个线程具体参见代码清单3-6。
代码清单3-6 线程函数
1 /* 软件延时 */ 2 void delay (uint32_t count) 3 { 4 for (; count! =0; count--); 5 } 6 7 /* 线程1 */ 8 void flag1_thread_entry( void *p_arg ) (1) 9 { 10 for ( ; ; ) 11 { 12 flag1 = 1; 13 delay( 100 ); 14 flag1 = 0; 15 delay( 100 ); 16 } 17 } 18 19 /* 线程2 */ 20 void flag2_thread_entry( void *p_arg ) (2) 21 { 22 for ( ; ; ) 23 { 24 flag2 = 1; 25 delay( 100 ); 26 flag2 = 0; 27 delay( 100 ); 28 } 29 }
代码清单3-6(1)、(2):正如所介绍的那样,线程是一个独立的、无限循环且不能返回的函数。
3.2.3 定义线程控制块
在裸机系统中,程序的主体是CPU按照顺序执行的。而在多线程系统中,线程的执行是由系统调度的。系统为了顺利地调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块相当于线程的“身份证”,里面存有线程的所有信息,比如线程的栈指针、线程名称、线程的形参等。有了这个线程控制块,以后系统对线程的全部操作就可以通过这个线程控制块来实现。定义一个线程控制块需要一个新的数据类型,该数据类型在rtdef.h这个头文件中声明,具体声明参见代码清单3-7,使用它可以为每个线程都定义一个线程控制块实体。
代码清单3-7 线程控制块类型声明
1 struct rt_thread (1) 2 { 3 void *sp; /* 线程栈指针 */ 4 void *entry; /* 线程入口地址 */ 5 void *parameter; /* 线程形参 */ 6 void *stack_addr; /* 线程栈起始地址 */ 7 rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */ 8 }; 9 typedef struct rt_thread *rt_thread_t; (2)
代码清单3-7(1):目前线程控制块结构体里面的成员还比较少,以后我们会慢慢向里面添加成员。
代码清单3-7(2):在RT-Thread中,会给新声明的数据结构重新定义一个指针。以后如果要定义线程控制块变量就使用struct rt_thread xxx的形式,定义线程控制块指针就使用rt_thread_t xxx的形式。
在本章中,我们在main.c文件中为两个线程定义线程控制块,参见代码清单3-8。
代码清单3-8 线程控制块定义
1 /* 定义线程控制块 */
2 struct rt_thread rt_flag1_thread;
3 struct rt_thread rt_flag2_thread;
3.2.4 实现线程创建函数
线程的栈、函数实体以及控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由线程初始化函数rt_thread_init()来实现,该函数在thread.c(第一次使用thread.c时需要自行在文件夹rtthread\3.0.3\src中新建并添加到工程的rtt/source组)中定义,在rtthread.h中声明,所有与线程相关的函数都在这个文件中定义。rt_thread_init()函数的实现参见代码清单3-9。
代码清单3-9 rt_thread_init()函数
1 rt_err_t rt_thread_init(struct rt_thread *thread, (1)
2 void (*entry)(void *parameter), (2)
3 void *parameter, (3)
4 void *stack_start, (4)
5 rt_uint32_t stack_size) (5)
6 {
7 rt_list_init(&(thread->tlist)); (6)
8
9 thread->entry = (void *)entry; (7)
10 thread->parameter = parameter; (8)
11
12 thread->stack_addr = stack_start; (9)
13 thread->stack_size = stack_size; (10)
14
15 /* 初始化线程栈,并返回线程栈指针 */ (11)
16 thread->sp =
17 (void *)rt_hw_stack_init( thread->entry,
18 thread->parameter,
19 (void *)((char *)thread->stack_addr + thread->stack_size-4) );
20
21 return RT_EOK; (12)
22 }
rt_thread_init()函数遵循RT-Thread中的函数命名规则,以小写的rt开头,表示这是一个外部函数,可以由用户调用,以_rt开头的函数表示内部函数,只能由RT-Thread内部使用。紧接着是文件名,表示该函数放在哪个文件中,最后是函数功能名称。
代码清单3-9(1):thread是线程控制块指针。
代码清单3-9(2):entry是线程函数名,表示线程的入口。
代码清单3-9(3):parameter是线程形参,用于传递线程参数。
代码清单3-9(4):stack_start用于指向线程栈的起始地址。
代码清单3-9(5):stack_size表示线程栈的大小,单位为字节。
1.实现链表相关函数
代码清单3-9(6):初始化线程链表节点,以后我们要把线程插入各种链表中,就是通过这个节点来实现的,它就好像是线程控制块里的一个钩子,可以把线程控制块挂在各种链表中。在初始化之前,需要在线程控制块中添加一个线程链表节点,具体实现参见代码清单3-10中的加粗部分。
代码清单3-10 在线程控制块中添加线程链表节点
1 struct rt_thread 2 { 3 void *sp; /* 线程栈指针 */ 4 void *entry; /* 线程入口地址 */ 5 void *parameter; /* 线程形参 */ 6 void *stack_addr; /* 线程栈起始地址 */ 7 rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */ 8 9 rt_list_t tlist; /* 线程链表节点 */ (1) 10 };
代码清单3-10(1):线程链表节点tlist的数据类型是rt_list_t,该数据类型在rtdef.h中定义,具体实现参见代码清单3-11。
(1)定义链表节点数据类型
代码清单3-11 定义双向链表节点数据类型rt_list_t
1 struct rt_list_node 2 { 3 struct rt_list_node *next; /* 指向后一个节点 */ 4 struct rt_list_node *prev; /* 指向前一个节点 */ 5 }; 6 typedef struct rt_list_node rt_list_t;
rt_list_t类型的节点中有两个rt_list_t类型的节点指针next和prev,分别用来指向链表中的下一个节点和上一个节点。由rt_list_t类型的节点构成的双向链表示意图如图3-3所示。
图3-3 rt_list_t类型的节点构成的双向链表
现在我们详细讲解一下双向链表的相关操作,这些函数均在rtservice.h中实现,rtservice.h第一次使用时需要自行在rtthread\3.0.3\include文件夹下新建,然后添加到工程的rtt/source组中。
(2)初始化链表节点
rt_list_t类型的节点的初始化,就是将节点里面的next和prev这两个节点指针指向节点本身,具体的代码实现参见代码清单3-12,具体的示意图如图3-4所示。
代码清单3-12 初始化rt_list_t类型的链表节点
1 rt_inline void rt_list_init(rt_list_t *l) 2 { 3 l->next = l->prev = l; 4 }
图3-4 rt_list_t类型的链表节点初始化完成示意图
(3)在双向链表表头后面插入一个节点
在双向链表表头后面插入一个节点,具体代码实现参见代码清单3-13,主要处理分为4步,插入前和插入后的示意图如图3-5所示。
代码清单3-13 在双向链表表头后面插入一个节点
1 /* 在双向链表头部插入一个节点 */ 2 rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n) 3 { 4 l->next->prev = n; /*第①步*/ 5 n->next = l->next; /*第②步*/ 6 7 l->next = n; /*第③步*/ 8 n->prev = l; /*第④步*/ 9 }
图3-5 在双向链表表头后面插入一个节点处理过程示意图
(4)在双向链表表头前面插入一个节点
在双向链表表头前面(也可理解为在双向链表尾部)插入一个节点,具体代码实现参见代码清单3-14,主要处理分为4步,插入前和插入后的示意图如图3-6所示。
代码清单3-14 在双向链表表头前面插入一个节点
1 rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n) 2 { 3 l->prev->next = n; /*第①步*/ 4 n->prev = l->prev; /*第②步*/ 5 6 l->prev = n; /*第③步*/ 7 n->next = l; /*第④步*/ 8 }
图3-6 在双向链表表头前面插入一个节点处理过程示意图
(5)从双向链表中删除一个节点
从双向链表中删除一个节点,具体代码实现参见代码清单3-15,主要处理分为3步,删除前和删除后的示意图如图3-7所示。
代码清单3-15 从双向链表中删除一个节点
1 rt_inline void rt_list_remove(rt_list_t *n) 2 { 3 n->next->prev = n->prev; /*第①步*/ 4 n->prev->next = n->next; /*第②步*/ 5 6 n->next = n->prev = n; /*第③步*/ 7 }
图3-7 从双向链表中删除一个节点
代码清单3-9(7):将线程入口保存到线程控制块的entry成员中。
代码清单3-9(8):将线程入口形参保存到线程控制块的parameter成员中。
代码清单3-9(9):将线程栈起始地址保存到线程控制块的stack_start成员中。
代码清单3-9(10):将线程栈大小保存到线程控制块的stack_size成员中。
代码清单3-9(11):初始化线程栈,并返回线程栈顶指针。
2. rt_hw_stack_init()函数
在前面的代码清单3-9中,rt_hw_stack_init()函数用来初始化线程栈,当线程第一次运行时,加载到CPU寄存器的参数就放在线程栈里面,该函数在cpuport.c中实现,具体实现参见代码清单3-16。第一次使用cpuport.c时需要自行在rtthread\3.0.3\libcpu\arm\cortex-m3(cortex-m4或cortex-m7)文件夹下新建,然后添加到工程的rtt/ports组中。
代码清单3-16 rt_hw_stack_init()函数
1 rt_uint8_t *rt_hw_stack_init(void *tentry, (1) 2 void *parameter, (2) 3 rt_uint8_t *stack_addr) (3) 4 { 5 6 7 struct stack_frame *stack_frame; (4) 8 rt_uint8_t *stk; 9 unsigned long i; 10 11 12 /* 获取栈顶指针 13 调用rt_hw_stack_init()时,传给stack_addr的是(栈顶指针-4)*/ 14 stk = stack_addr + sizeof(rt_uint32_t); (5) 15 16 /* 让stk指针向下8字节对齐 */ 17 stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8); (6) 18 19 /* stk指针继续向下移动sizeof(struct stack_frame)个偏移量 */ 20 stk-= sizeof(struct stack_frame); (7) 21 22 /* 将stk指针强制转化为stack_frame类型后存储到stack_frame中*/ 23 stack_frame = (struct stack_frame *)stk; (8) 24 25 /* 以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame) 26 个内存地址初始化为0xdeadbeef */ 27 for (i = 0; i <sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)(9) 28 { 29 ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef; 30 } 31 32 /* 初始化异常发生时自动保存的寄存器 */ (10) 33 stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */ 34 stack_frame->exception_stack_frame.r1 = 0; /* r1 */ 35 stack_frame->exception_stack_frame.r2 = 0; /* r2 */ 36 stack_frame->exception_stack_frame.r3 = 0; /* r3 */ 37 stack_frame->exception_stack_frame.r12 = 0; /* r12 */ 38 stack_frame->exception_stack_frame.lr = 0; /* lr:暂 时初始化为0 */ 39 stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */ 40 stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */ 41 42 43 /* 返回线程栈指针 */ 44 return stk; (11) 45 }
代码清单3-16(1):线程入口。
代码清单3-16(2):线程形参。
代码清单3-16(3):线程栈顶地址-4,在该函数调用时传进来的是线程栈的栈顶地址-4。
代码清单3-16(4):定义一个struct stack_frame类型的结构体指针stack_frame,该结构体类型在cpuport.c中定义,具体实现参见代码清单3-17。
代码清单3-17 struct stack_frame类型结构体定义
1 struct exception_stack_frame 2 { 3 /* 异常发生时,自动加载到CPU寄存器的内容 */ 4 rt_uint32_t r0; 5 rt_uint32_t r1; 6 rt_uint32_t r2; 7 rt_uint32_t r3; 8 rt_uint32_t r12; 9 rt_uint32_t lr; 10 rt_uint32_t pc; 11 rt_uint32_t psr; 12 }; 13 14 struct stack_frame 15 { 16 /* 异常发生时,需要手动加载到CPU寄存器的内容 */ 17 rt_uint32_t r4; 18 rt_uint32_t r5; 19 rt_uint32_t r6; 20 rt_uint32_t r7; 21 rt_uint32_t r8; 22 rt_uint32_t r9; 23 rt_uint32_t r10; 24 rt_uint32_t r11; 25 26 struct exception_stack_frame exception_stack_frame; 27 };
代码清单3-16(5):获取栈顶指针,将栈顶指针传给指针stk。rt_hw_stack_init()函数在rt_thread_init()函数中调用时,传给形参stack_addr的值是栈顶指针减去4,所以现在加上sizeof(rt_uint32_t)刚好与减掉的4相互抵消,即传递给stk的是栈顶指针。
代码清单3-16(6):让stk指针向下8个字节对齐,确保stk是8字节对齐的地址。在Cortex-M3(Cortex-M4或Cortex-M7)内核的单片机中,因为总线宽度是32位的,通常只要栈保持4字节对齐即可,这里为什么需要8字节?难道是因为有操作是64位的?确实有,那就是浮点运算,所以要8字节对齐,但是目前我们还没有涉及浮点运算,这里只是出于后续兼容浮点运算的考虑。如果栈顶指针是8字节对齐的,在进行向下8字节对齐时,指针不会移动;如果不是8字节对齐的,在进行向下8字节对齐时,就会空出几个字节不会被使用,比如当stk是33时,明显不能被8整除,进行向下8字节对齐就是32,那么就会空出一个字节不使用。
代码清单3-16(7):stk指针继续向下移动sizeof(struct stack_frame)个偏移,即16个字的大小。如果栈顶指针一开始都是8字节对齐的,那么stk现在在线程栈中的指向如图3-8所示。
图3-8 stk指针指向
代码清单3-16(8):将stk指针强制转化为stack_frame类型后存储到指针变量stack_frame中,这时stack_frame在线程栈中的指向如图3-9所示。
图3-9 stack_frame指针指向
代码清单3-16(9):以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame)个内存地址初始化为0xdeadbeef,这时栈空间的内容分布如图3-10所示。
图3-10 栈空间内容分布
代码清单3-16(10):线程第一次运行时,加载到CPU寄存器的环境参数要先初始化。从栈顶开始,初始化的顺序固定,首先是异常发生时自动保存的8个寄存器,即xPSR、r15、r14、r12、r3、r2、r1和r0。其中xPSR寄存器的24位必须是1, r15 PC指针必须存的是线程的入口地址,r0必须是线程形参,剩下的r14、r12、r3、r2和r1初始化为0。
剩下的是8个需要手动加载到CPU寄存器的参数,即r4~r11,默认初始化为0xdeadbeef,如图3-11所示。
图3-11 初始化寄存器环境参数
代码清单3-16(11):返回线程栈指针stk,这时stk指向剩余栈的栈顶。
回到代码清单3-9。
代码清单3-9(12):线程初始化成功,返回错误码RT_EOK。RT-Thread的错误码在rtdef.h中定义,具体实现参见代码清单3-18。
代码清单3-18 错误码宏定义
1 /* 2 ************************************************************************* 3 * 错误码定义 4 ************************************************************************* 5 */ 6 /* RT-Thread 错误码重定义 */ 7 #define RT_EOK 0 /**< There is no error */ 8 #define RT_ERROR 1 /**< A generic error happens */ 9 #define RT_ETIMEOUT 2 /**< Timed out */ 10 #define RT_EFULL 3 /**< The resource is full */ 11 #define RT_EEMPTY 4 /**< The resource is empty */ 12 #define RT_ENOMEM 5 /**< No memory */ 13 #define RT_ENOSYS 6 /**< No system */ 14 #define RT_EBUSY 7 /**< Busy */ 15 #define RT_EIO 8 /**< IO error */ 16 #define RT_EINTR 9 /**< Interrupted system call */ 17 #define RT_EINVAL 10 /**< Invalid argument */
在本章中,我们在main()函数中创建两个fl ag相关的线程,具体实现参见代码清单3-19。
代码清单3-19 初始化线程
1 int main(void) 2 { 3 /* 硬件初始化 */ 4 /* 将硬件相关的初始化放在这里,如果是软件仿真,则没有相关初始化代码 */ 5 6 7 /* 初始化线程 */ 8 rt_thread_init( &rt_flag1_thread, /* 线程控制块 */ 9 flag1_thread_entry, /* 线程入口地址 */ 10 RT_NULL, /* 线程形参 */ 11 &rt_flag1_thread_stack[0], /* 线程栈起始地址 */ 12 sizeof(rt_flag1_thread_stack)); /* 线程栈大小,单位为字节 */ 13 14 /* 初始化线程 */ 15 rt_thread_init( &rt_flag2_thread, /* 线程控制块 */ 16 flag2_thread_entry, /* 线程入口地址 */ 17 RT_NULL, /* 线程形参 */ 18 &rt_flag2_thread_stack[0], /* 线程栈起始地址 */ 19 sizeof(rt_flag2_thread_stack)); /* 线程栈大小,单位为字节 */
3.3 实现就绪列表
3.3.1 定义就绪列表
线程创建好之后,需要把线程添加到就绪列表中,表示线程已经就绪,系统随时可以调度。就绪列表在scheduler.c中定义(第一次使用scheduler.c时,需要在rtthread\3.0.3\src目录下新建,然后添加到工程的rtt/source组中),具体实现参见代码清单3-20。
代码清单3-20 定义就绪列表
1 /* 线程就绪列表 */
2 rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];(1)
图3-12 空的就绪列表
代码清单3-20(1):就绪列表实际上就是一个rt_list_t类型的数组,数组的大小由决定最大线程优先级的宏RT_THREAD_PRIORITY_MAX决定,RT_THREAD_PRIORITY_MAX在rtconfig.h中默认定义为32。数组的下标对应了线程的优先级,同一优先级的线程统一插入就绪列表的同一条链表中。一个空的就绪列表如图3-12所示。
3.3.2 将线程插入就绪列表
线程控制块中有一个tlist成员,数据类型为rt_list_t,我们将线程插入就绪列表里面,就是通过将线程控制块的tlist节点插入就绪列表中来实现的。如果把就绪列表比作晾衣竿,线程比作衣服,那么tlist就是晾衣架,每个线程都自带晾衣架,就是为了把自己挂在不同的链表中。
在本章中,我们在线程创建好之后,紧接着将线程插入就绪列表,具体实现参见代码清单3-21的加粗部分。
代码清单3-21 将线程插入就绪列表
1 /* 初始化线程 */ 2 rt_thread_init( &rt_flag1_thread, /* 线程控制块 */ 3 flag1_thread_entry, /* 线程入口地址 */ 4 RT_NULL, /* 线程形参 */ 5 &rt_flag1_thread_stack[0], /* 线程栈起始地址 */ 6 sizeof(rt_flag1_thread_stack) ); /* 线程栈大小,单位为字节 */ 7 /* 将线程插入就绪列表 */ 8 rt_list_insert_before( &(rt_thread_priority_table[0]), &(rt_fl ag1_thread.tlist) ); 9 10 /* 初始化线程 */ 11 rt_thread_init( &rt_flag2_thread, /* 线程控制块 */ 12 flag2_thread_entry, /* 线程入口地址 */ 13 RT_NULL, /* 线程形参 */ 14 &rt_flag2_thread_stack[0], /* 线程栈起始地址 */ 15 sizeof(rt_flag2_thread_stack) ); /* 线程栈大小,单位为字节 */ 16 /* 将线程插入就绪列表 */ 17 rt_list_insert_before( &(rt_thread_priority_table[1]), &(rt_flag2_thread.tlist) );
图3-13 将线程插入就绪列表示意图
就绪列表的下标对应的是线程的优先级,但是目前我们的线程还不支持优先级,有关支持多优先级的知识点后面会介绍,所以flag1和flag2线程在插入就绪列表时,可以任意选择插入的位置。在代码清单3-21中,我们选择将flag1线程插入就绪列表下标为0的链表中,将flag2线程插入就绪列表下标为1的链表中,如图3-13所示。
3.4 实现调度器
调度器是操作系统的核心,其主要功能是实现线程的切换,即从就绪列表中找到优先级最高的线程,然后执行该线程。从代码上来看,调度器无非是由几个全局变量和一些可以实现线程切换的函数组成的,全部在scheduler.c文件中实现。
3.4.1 调度器初始化
调度器在使用之前必须先初始化,具体实现参见代码清单3-22。
代码清单3-22 调度器初始化函数
1 /* 初始化系统调度器 */ 2 void rt_system_scheduler_init(void) 3 { 4 register rt_base_t offset; (1) 5 6 7 /* 线程就绪列表初始化 */ 8 for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)(2) 9 { 10 rt_list_init(&rt_thread_priority_table[offset]); 11 } 12 13 /* 初始化当前线程控制块指针 */ 14 rt_current_thread = RT_NULL; (3) 15 }
图3-14 空的线程就绪列表
代码清单3-22(1):定义一个局部变量,用C语言关键词register修饰,防止被编译器优化。
代码清单3-22(2):初始化线程就绪列表,初始化完成后,整个就绪列表为空,如图3-14所示。
代码清单3-22(3):初始化当前线程控制块指针为空。rt_current_thread是在scheduler.c中定义的一个struct rt_thread类型的全局指针,用于指向当前正在运行的线程的线程控制块。
在本章中,我们把调度器初始化放在硬件初始化之后,线程创建之前,具体实现参见代码清单3-23中的加粗部分。
代码清单3-23 调度器初始化
1 int main(void) 2 { 3 /* 硬件初始化 */ 4 /* 将硬件相关的初始化放在这里,如果是软件仿真,则没有相关初始化代码 */ 5 6 /* 调度器初始化 */ 7 rt_system_scheduler_init(); 8 9 10 /* 初始化线程 */ 11 rt_thread_init( &rt_flag1_thread, /* 线程控制块 */ 12 flag1_thread_entry, /* 线程入口地址 */ 13 RT_NULL, /* 线程形参 */ 14 &rt_flag1_thread_stack[0], /* 线程栈起始地址 */ 15 sizeof(rt_flag1_thread_stack)); /* 线程栈大小,单位为字节 */ 16 /* 将线程插入就绪列表 */ 17 rt_list_insert_before( &(rt_thread_priority_table[0]), &(rt_flag1_ thread.tlist) ); 18 19 /* 初始化线程 */ 20 rt_thread_init( &rt_flag2_thread, /* 线程控制块 */ 21 flag2_thread_entry, /* 线程入口地址 */ 22 RT_NULL, /* 线程形参 */ 23 &rt_flag2_thread_stack[0], /* 线程栈起始地址 */ 24 sizeof(rt_flag2_thread_stack)); /* 线程栈大小,单位为字节 */ 25 /* 将线程插入就绪列表 */ 26 rt_list_insert_before( &(rt_thread_priority_table[1]), &(rt_flag2_ thread.tlist) ); 27 }
3.4.2 启动调度器
调度器的启动由函数rt_system_scheduler_start()来完成,具体实现参见代码清单3-24。
代码清单3-24 启动调度器函数
1 /* */ 2 void rt_system_scheduler_start(void) 3 { 4 register struct rt_thread *to_thread; 5 6 7 /* 手动指定第一个运行的线程 */ (1) 8 to_thread = rt_list_entry(rt_thread_priority_table[0].next, 9 struct rt_thread, 10 tlist); 11 rt_current_thread = to_thread; (2) 12 13 /* 切换到第一个线程,该函数在context_rvds.s中实现, 14 在rthw.h中声明,用于实现第一次线程切换。 15 当一个汇编函数在C文件中调用时,如果有形参, 16 则执行时会将形参传入CPU寄存器r0*/ 17 rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); (3) 18 }
代码清单3-24(1):调度器在启动时会从就绪列表中取出优先级最高的线程的线程控制块,然后切换到该线程。但是目前我们的线程还不支持优先级,那么就手动指定第一个运行的线程为就绪列表下标为0这条链表中挂着的线程。rt_list_entry是一个已知某结构体中的成员地址,反推出该结构体的首地址的宏,在scheduler.c开头定义,具体实现参见代码清单3-25。
代码清单3-25 rt_list_entry宏定义
1 /* 已知一个结构体中成员的地址,反推出该结构体的首地址 */
2 #define rt_container_of(ptr, type, member) \ (2)
3 ((type *)((char *)(ptr)- (unsigned long)(&((type *)0)->member)))
4
5 #define rt_list_entry(node, type, member) \ (1)
6 rt_container_of(node, type, member)
代码清单3-25(1):node表示一个节点的地址,type表示该节点所在的结构体的类型,member表示该节点在该结构体中的成员的名称。
代码清单3-25(2):rt_container_of()的实现算法如图3-15所示。
图3-15 已知type类型的结构体f_struct中tlist成员的地址为ptr,推算出f_struct的起始地址f_struct_ptr的示意图
由图3-15我们知道了tlist节点的地址ptr,现在要推算出该节点所在的type类型的结构体的起始地址f_struct_ptr。将ptr的值减去图3-15中灰色部分的偏移的大小,即可得到f_struct_ptr的地址,现在的关键是如何计算出灰色部分的偏移大小。这里采取的做法是将0地址强制转换类型为type,即(type *)0,然后通过指针访问结构体成员的方式获取偏移的大小,即(&((type *)0)->member),最后即可算出f_struct_ptr = ptr- (&((type *)0)->member)。
代码清单3-24(2):将获取的第一个要运行的线程控制块指针传到全局变量rt_current_thread中。
3.4.3 第一次线程切换
1. rt_hw_context_switch_to()函数
代码清单3-24(3):第一次切换到新的线程,rt_hw_context_switch_to()函数在context_rvds.s中实现(第一次使用context_rvds.s文件时,需要在rtthread\3.0.3\libcpu\arm\cortex-m3(cortex-m4或者cortex-m7)中新建,然后添加到工程的rtt/ports组中),在rthw.h中声明,用于实现第一次线程切换。当一个汇编函数在C文件中调用时,如果有一个形参,则执行时会将这个形参传入CPU寄存器r0,如果有两个形参,第二个形参则传入r1。rt_hw_context_switch_to()函数的具体实现参见代码清单3-26。context_rvds.s文件中涉及的ARM汇编指令如表3-1所示。
代码清单3-26 rt_hw_context_switch_to()函数
1 ; ************************************************************************ 2 ; 全局变量 (4) 3 ; ************************************************************************ 4 IMPORT rt_thread_switch_interrupt_flag 5 IMPORT rt_interrupt_from_thread 6 IMPORT rt_interrupt_to_thread 7 8 ; ************************************************************************ 9 ; 常量 (5) 10 ; ************************************************************************ 11 ; ------------------------------------------------------------------------ 12;有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual 13;系统控制块外设SCB地址范围:0xE000ED00~0xE000ED3F 14 ; ------------------------------------------------------------------------ 15 SCB_VTOR EQU 0xE000ED08 ; 向量表偏移寄存器 16 NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器 17 NVIC_SYSPRI2 EQU 0xE000ED20 ; 系统优先级寄存器 18 NVIC_PENDSV_PRI EQU 0x00FF0000 ; PendSV 优先级值 (lowest) 19 NVIC_PENDSVSET EQU 0x10000000 ; 触发PendSV exception的值 20 21 ; ************************************************************************ 22 ; 代码产生指令 (1) 23 ; ************************************************************************ 24 25 AREA |.text|, CODE, READONLY, ALIGN=2 26 THUMB 27 REQUIRE8 28 PRESERVE8 29 30 ; /* 31 ; *---------------------------------------------------------------------- 32 ; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to); 33 ; * r0--> to 34 ; * 该函数用于开启第一次线程切换 35 ; *---------------------------------------------------------------------- 36 ; */ 37 38 39 rt_hw_context_switch_to PROC (6) 40 41 ; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件中调用 42 EXPORT rt_hw_context_switch_to (7) 43 44 ; 设置rt_interrupt_to_thread的值 (8) 45 ;将rt_interrupt_to_thread的地址加载到r1 46 LDR r1, =rt_interrupt_to_thread (8)-① 47 ;将r0的值存储到rt_interrupt_to_thread (8)-② 48 STR r0, [r1] 49 50 ; 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换 (9) 51 ;将rt_interrupt_from_thread的地址加载到r1 52 LDR r1, =rt_interrupt_from_thread (9)-① 53 ;配置r0等于0 54 MOV r0, #0x0 (9)-② 55 ;将r0的值存储到rt_interrupt_from_thread 56 STR r0, [r1] (9)-③ 57 58 ; 设置中断标志位rt_thread_switch_interrupt_flag的值为1 (10) 59 ;将rt_thread_switch_interrupt_flag的地址加载到r1 60 LDR r1, =rt_thread_switch_interrupt_flag (10)-① 61 ;配置r0等于1 62 MOV r0, #1 (10)-② 63 ;将r0的值存储到rt_thread_switch_interrupt_flag 64 STR r0, [r1] (10)-③ 65 66 ; 设置 PendSV 异常的优先级 (11) 67 LDR r0, =NVIC_SYSPRI2 68 LDR r1, =NVIC_PENDSV_PRI 69 LDR.W r2, [r0, #0x00] ; 读 70 ORR r1, r1, r2 ; 改 71 STR r1, [r0] ; 写 72 73 ; 触发 PendSV 异常(产生上下文切换) (12) 74 LDR r0, =NVIC_INT_CTRL 75 LDR r1, =NVIC_PENDSVSET 76 STR r1, [r0] 77 78 ; 开中断 79 CPSIE F (13) 80 CPSIE I 81 82 ; 永远不会到达这里 83 ENDP (14) 84 85 ALIGN 4 (3) 86 87 END (2)
表3-1 ARM常用汇编指令讲解
代码清单3-26(1):汇编代码产生指令,当我们新建一个汇编文件写代码时,必须包含类似的指令。AERA表示汇编一个新的数据段或者代码段;.text表示段名,如果段名不是以字母开头,而是以其他符号开头,则需要在段名两边加上“|”; CODE表示伪代码;READONLY表示只读;ALIGN=2表示当前文件指令要22字节对齐;THUMB表示THUMB指令代码;REQUIRE8和PRESERVE8均表示当前文件的栈按照8字节对齐。
代码清单3-26(2):汇编文件结束,每个汇编文件都需要一个END。
代码清单3-26(3):当前文件指令代码要求4字节对齐,不然会有警告。
代码清单3-26(4):使用IMPORT关键字导入一些全局变量,这3个全局变量在cpuport.c中定义,具体实现参见代码清单3-27,每个变量的含义请参见注释。
代码清单3-27 汇编文件导入的3个全局变量定义
1 /* 用于存储上一个线程的栈的sp的指针 */ 2 rt_uint32_t rt_interrupt_from_thread; 3 4 /* 用于存储下一个将要运行的线程的栈的sp的指针 */ 5 rt_uint32_t rt_interrupt_to_thread; 6 7 /* PendSV中断服务函数执行标志 */ 8 rt_uint32_t rt_thread_switch_interrupt_flag;
代码清单3-26(5):定义了一些常量,这些都是内核中的寄存器,之后触发PendSV异常时会用到。有关内核外设寄存器定义可参考官方文档STM32F10xxx Cortex-M3 programming manual——4 Core peripherals, M3/4/7内核均可以参考该文档。
代码清单3-26(6):PROC用于定义子程序,与ENDP成对使用,表示rt_hw_context_switch_to()函数开始。
代码清单3-26(7):使用EXPORT关键字导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件中调用(但也要先在rthw.h中声明)。
代码清单3-26(8):设置rt_interrupt_to_thread的值。
代码清单3-26(8)-①:将rt_interrupt_to_thread的地址加载到r1。
代码清单3-26(8)-②:将r0的值存储到rt_interrupt_to_thread, r0存储的是下一个将要运行的线程的sp的地址,由rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp)调用的时候传到r0。
代码清单3-26(9):设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换。
代码清单3-26(9)-①:将rt_interrupt_from_thread的地址加载到r1。
代码清单3-26(9)-②:配置r0等于0。
代码清单3-26(9)-③:将r0的值存储到rt_interrupt_from_thread。
代码清单3-26(10):设置中断标志位rt_thread_switch_interrupt_flag的值为1,当执行了PendSVC-Handler()时,rt_thread_switch_interrupt_flag的值会被清零。
代码清单3-26(10)-①:将rt_thread_switch_interrupt_flag的地址加载到r1。
代码清单3-26(10)-②:配置r0等于1。
代码清单3-26(10)-③:将r0的值存储到rt_thread_switch_interrupt_flag。
代码清单3-26(11):设置PendSV异常的优先级为最低。
代码清单3-26(12):触发PendSV异常(产生上下文切换)。如果前面关闭了,还要等中断打开时才能执行PendSV中断服务函数。
代码清单3-26(13):开中断。
代码清单3-26(14):rt_hw_context_switch_to()函数运行结束,与PROC成对使用。
2. PendSV_Handler()函数
PendSV_Handler()是真正实现线程上下文切换的函数,具体实现参见代码清单3-28。
代码清单3-28 PendSV_Handler()函数
1 ; /* 2 ; *---------------------------------------------------------------------- 3 ; * void PendSV_Handler(void); 4 ; * r0--> switch from thread stack 5 ; * r1--> switch to thread stack 6 ; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack 7 ; *---------------------------------------------------------------------- 8 ; */ 9 10 PendSV_Handler PROC 11 EXPORT PendSV_Handler 12 13 ; 禁用中断,为了保护上下文切换不被中断 (1) 14 MRS r2, PRIMASK 15 CPSID I 16 17 ; 获取中断标志位,看看是否为0 (2) 18 ; 加载rt_thread_switch_interrupt_flag的地址到r0 19 LDR r0, =rt_thread_switch_interrupt_flag (2)-① 20 ; 加载rt_thread_switch_interrupt_flag的值到r1 21 LDR r1, [r0] (2)-② 22 ; 判断r1是否为0,为0则跳转到pendsv_exit 23 CBZ r1, pendsv_exit (2)-③ 24 25 ; r1不为0则清零 (3) 26 MOV r1, #0x00 27 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清零 28 STR r1, [r0] @@@ 29 ; 判断rt_interrupt_from_thread的值是否为0 (4) 30 ; 加载rt_interrupt_from_thread的地址到r0 31 LDR r0, =rt_interrupt_from_thread (4)-① 32 ; 加载rt_interrupt_from_thread的值到r1 33 LDR r1, [r0] (4)-② 34 ; 判断r1是否为0,为0则跳转到switch_to_thread 35 ; 第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread 36 CBZ r1, switch_to_thread (4)-③ 37 38 ; ========================== 上文保存 ======================== (6) 39 ; 当进入PendSVC-Handler()时,上一个线程运行的环境即 40 ; xPSR, PC(线程入口地址), r14, r12, r3, r2, r1, r0(线程的形参) 41 ; 这些CPU寄存器的值会自动保存到线程的栈中,剩下的r4~r11需要手动保存 42 ; 获取线程栈指针到r1 43 MRS r1, psp (6)-① 44 ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次) 45 STMFD r1! , {r4- r11} (6)-② 46 ; 加载r0地址指向的值到r0,即r0=rt_interrupt_from_thread 47 LDR r0, [r0] (6)-③ 48 ; 将r1的值存储到r0,即更新线程栈sp 49 STR r1, [r0] (6)-④ 50 51 ; ========================== 下文切换 ========================== (5) 52 switch_to_thread 53 ; 加载rt_interrupt_to_thread的地址到r1 ; rt_interrupt_to_thread是一个全局变量,里面保存的是线程栈指针sp的指针 54 LDR r1, =rt_interrupt_to_thread (5)-① 55 ; 加载rt_interrupt_to_thread的值到r1,即sp的指针 56 LDR r1, [r1] (5)-② 57 ; 加载rt_interrupt_to_thread的值到r1,即sp 58 LDR r1, [r1] (5)-③ 59 60 ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11 61 LDMFD r1! , {r4- r11} (5)-④ 62 ;将线程栈指针更新到psp 63 MSR psp, r1 (5)-⑤ 64 65 pendsv_exit 66 ; 恢复中断 67 MSR PRIMASK, r2 (7) 68 69 ; 确保异常返回使用的栈指针是psp,即lr寄存器的位2要为1 70 ORR lr, lr, #0x04 (8) 71 ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器: 72 ; xPSR, PC(线程入口地址), r14, r12, r3, r2, r1, r0(线程的形参) 73 ; 同时psp的值也将更新,即指向线程栈的栈顶 74 BX lr (9) 75 76 ; PendSV_Handler 子程序结束 77 ENDP (10)
代码清单3-28(1):禁用中断,为了保护上下文切换不被中断。
代码清单3-28(2):获取中断标志位rt_thread_switch_interrupt_flag是否为0,如果为0则退出PendSV Handler,如果不为0则继续往下执行。
代码清单3-28(2)-①:加载rt_thread_switch_interrupt_flag的地址到r0。
代码清单3-28(2)-②:加载rt_thread_switch_interrupt_flag的值到r1。
代码清单3-28(2)-③:判断r1是否为0,若为0则跳转到pendsv_exit,退出PendSV_Handler()函数。
代码清单3-28(3):中断标志位rt_thread_switch_interrupt_flag清零。
代码清单3-28(4):判断rt_interrupt_from_thread的值是否为0,如果为0,则表示第一次线程切换,不用做上文保存的工作,直接跳转到switch_to_thread执行下文切换即可;如果不为0,则需要先保存上文,然后再切换到下文。
代码清单3-28(4)-①:加载rt_interrupt_from_thread的地址到r0。
代码清单3-28(4)-②:加载rt_interrupt_from_thread的值到r1。
代码清单3-28(4)-③:判断r1是否为0,若为0则跳转到switch_to_thread,第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread。
代码清单3-28(5):下文切换。下文切换实际上就是把接下来要运行的线程栈中的内容加载到CPU寄存器,更改PC指针和PSP指针,从而实现程序的跳转。
代码清单3-28(5)-①:加载rt_interrupt_to_thread的地址到r1, rt_interrupt_to_thread是一个全局变量,里面保存的是线程栈指针sp的指针。
代码清单3-28(5)-②:加载rt_interrupt_to_thread的值到r1,即sp的指针。
代码清单3-28(5)-③:加载rt_interrupt_to_thread的值到r1,即sp。
代码清单3-28(5)-④:将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11。
代码清单3-28(5)-⑤:将线程栈指针更新到psp。
代码清单3-28(6):rt_interrupt_from_thread的值不为0则表示不是第一次线程切换,需要先保存上文。当进入PendSVC_Handler()时,上一个线程运行的环境即xPSR, PC(线程入口地址), r14, r12, r3, r2, r1, r0(线程的形参),这些CPU寄存器的值会自动保存到线程的栈中,并更新psp的值,剩下的r4~r11需要手动保存。
代码清单3-28(6)-①:获取线程栈指针到r1。
代码清单3-28(6)-②:将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)。
代码清单3-28(6)-③:加载r0地址指向的值到r0,即r0=rt_interrupt_from_thread。
代码清单3-28(6)-④:将r1的值存储到r0,即更新线程栈sp。
代码清单3-28(7):上下文切换完成,恢复中断。
代码清单3-28(8):确保异常返回使用的栈指针是psp,即lr寄存器的位2要为1。
代码清单3-28(9):异常返回,这时接下来将要运行的线程栈中的剩余内容将会自动加载到CPU寄存器:xPSR, PC(线程入口地址), r14, r12, r3, r2, r1, r0(线程的形参)。同时psp的值也将更新,即指向线程栈的栈顶。
代码清单3-28(10):上下文切换完成,恢复中断。
3.4.4 系统调度
系统调度就是在就绪列表中寻找优先级最高的就绪线程,然后执行该线程。但是目前我们还不支持优先级,仅实现两个线程轮流切换,涉及rt_schedule()函数和rt_hw_contex_switch()函数。
1. rt_schedule()函数
系统调度函数rt_schedule()的具体实现参见代码清单3-29。
代码清单3-29 rt_schedule()函数
1 /* 系统调度 */ 2 void rt_schedule(void) 3 { 4 struct rt_thread *to_thread; 5 struct rt_thread *from_thread; 6 7 8 /* 两个线程轮流切换 */ (1) 9 if ( rt_current_thread == rt_list_entry( rt_thread_priority_table[0].next, 10 struct rt_thread, 11 tlist) ) 12 { 13 from_thread = rt_current_thread; 14 to_thread = rt_list_entry( rt_thread_priority_table[1].next, 15 struct rt_thread, 16 tlist); 17 rt_current_thread = to_thread; 18 } 19 else (2) 20 { 21 from_thread = rt_current_thread; 22 to_thread = rt_list_entry( rt_thread_priority_table[0].next, 23 struct rt_thread, 24 tlist); 25 rt_current_thread = to_thread; 26 } 27 28 /* 产生上下文切换 */ 29 rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_ thread->sp); 30 }
代码清单3-29(1):如果当前线程为线程1,则把下一个要运行的线程改为线程2。
代码清单3-29(2):如果当前线程为线程2,则把下一个要运行的线程改为线程1。
2. rt_hw_context_switch()函数
rt_hw_context_switch()函数用于产生上下文切换,在context_rvds.s中实现,在rthw.h中声明。当一个汇编函数在C文件中调用时,如果有两个形参,则执行时会将这两个形参传入CPU寄存器r0、r1。rt_hw_context_switch()函数的具体实现参见代码清单3-30。
代码清单3-30 rt_hw_context_switch()函数
1 ; /* 2 ; *---------------------------------------------------------------------- 3 ; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 4 ; * r0--> from 5 ; * r1--> to 6 ; *---------------------------------------------------------------------- 7 ; */ 8 rt_hw_context_switch PROC 9 EXPORT rt_hw_context_switch 10 11 ; 设置中断标志位rt_thread_switch_interrupt_flag为1(1) 12 ; 加载rt_thread_switch_interrupt_flag的地址到r2 13 LDR r2, =rt_thread_switch_interrupt_flag (1)-① 14 ; 加载rt_thread_switch_interrupt_flag的值到r3 15 LDR r3, [r2] (1)-② 16 ; r3与1比较,相等则执行BEQ指令,否则不执行 17 CMP r3, #1 (1)-③ 18 BEQ _reswitch 19 ; 设置r3的值为1 20 MOV r3, #1 (1)-④ 21 ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1 22 STR r3, [r2] (1)-⑤ 23 24 ; 设置rt_interrupt_from_thread的值 (2) 25 ; 加载rt_interrupt_from_thread的地址到r2 26 LDR r2, =rt_interrupt_from_thread (2)-① 27 ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针 28 STR r0, [r2] (2)-② 29 30 _reswitch 31 ; 设置rt_interrupt_to_thread的值 (3) 32 ; 加载rt_interrupt_from_thread的地址到r2 33 LDR r2, =rt_interrupt_to_thread (3)-① 34 ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针 35 STR r1, [r2] (3)-② 36 37 ; 触发PendSV异常,实现上下文切换 (4) 38 LDR r0, =NVIC_INT_CTRL 39 LDR r1, =NVIC_PENDSVSET 40 STR r1, [r0] 41 ; 子程序返回 42 BX LR (5) 43 ; 子程序结束 44 ENDP (6)
代码清单3-30(1):设置中断标志位rt_thread_switch_interrupt_flag为1。
代码清单3-30(1)-①:加载rt_thread_switch_interrupt_flag的地址到r2。
代码清单3-30(1)-②:加载rt_thread_switch_interrupt_flag的值到r3。
代码清单3-30(1)-③:r3与1比较,相等则执行BEQ指令,否则不执行。
代码清单3-30(1)-④:设置r3的值为1。
代码清单3-30(1)-⑤:将r3的值存储到rt_thread_switch_interrupt_flag,即置1。
代码清单3-30(2):设置rt_interrupt_from_thread的值。
代码清单3-30(2)-①:加载rt_interrupt_from_thread的地址到r2。
代码清单3-30(2)-②:存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针。r0存储的是函数调用rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_thread->sp)时的第一个形参,即上一个线程栈指针sp的指针。
代码清单3-30(3):设置rt_interrupt_to_thread的值。
代码清单3-30(3)-①:加载rt_interrupt_from_thread的地址到r2。
代码清单3-30(3)-②:存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针。r1存储的是函数调用rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_thread->sp)时的第二个形参,即下一个线程栈指针sp的指针。
代码清单3-30(4):触发PendSV异常,在PendSV_Handler()中实现上下文切换。
代码清单3-30(5):子程序返回,返回到调用rt_hw_context_switch_to()函数的地方。
代码清单3-30(6):汇编程序结束。
3.5 main()函数
线程的创建、就绪列表的实现、调度器的实现均已介绍完毕,现在我们把全部的测试代码都放到main.c中,具体参见代码清单3-31。
代码清单3-31 main.c代码
1 /** 2 *********************************************************************** 3 * @file main.c 4 * @author fire 5 * @version V1.0 6 * @date 2018-xx-xx 7 * @brief 《RT-Thread内核实现与应用开发实战指南:基于STM32》例程 8 * 新建RT-Thread工程——软件仿真 9 *********************************************************************** 10 * @attention 11 * 12 * 实验平台:野火STM32系列开发板 13 * 14 * 官网:www.embedfire.com 15 * 论坛:http://www.firebbs.cn 16 * 淘宝:https://fire-stm32.taobao.com 17 * 18 *********************************************************************** 19 */ 20 21 22 /* 23 ************************************************************************* 24 * 包含的头文件 25 ************************************************************************* 26 */ 27 28 #include <rtthread.h> 29 #include "ARMCM3.h" 30 31 32 /* 33 ************************************************************************* 34 * 全局变量 35 ************************************************************************* 36 */ 37 rt_uint8_t flag1; 38 rt_uint8_t flag2; 39 40 extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; 41 42 /* 43 ************************************************************************* 44 * 线程控制块& STACK &线程声明 45 ************************************************************************* 46 */ 47 48 49 /* 定义线程控制块 */ 50 struct rt_thread rt_flag1_thread; 51 struct rt_thread rt_flag2_thread; 52 53 ALIGN(RT_ALIGN_SIZE) 54 /* 定义线程栈 */ 55 rt_uint8_t rt_flag1_thread_stack[512]; 56 rt_uint8_t rt_flag2_thread_stack[512]; 57 58 /* 线程声明 */ 59 void flag1_thread_entry(void *p_arg); 60 void flag2_thread_entry(void *p_arg); 61 62 /* 63 ************************************************************************* 64 * 函数声明 65 ************************************************************************* 66 */ 67 void delay(uint32_t count); 68 69 /************************************************************************ 70 * @brief main()函数 71 * @param 无 72 * @retval 无 73 * 74 * @attention 75 *********************************************************************** 76 */ 77 int main(void) 78 { 79 /* 硬件初始化 */ 80 /* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */ 81 82 /* 调度器初始化 */ 83 rt_system_scheduler_init(); 84 85 86 /* 初始化线程 */ 87 rt_thread_init( &rt_flag1_thread, /* 线程控制块 */ 88 flag1_thread_entry, /* 线程入口地址 */ 89 RT_NULL, /* 线程形参 */ 90 &rt_flag1_thread_stack[0], /* 线程栈起始地址 */ 91 sizeof(rt_flag1_thread_stack)); /* 线程栈大小,单位为字节 */ 92 /* 将线程插入就绪列表 */ 93 rt_list_insert_before( &(rt_thread_priority_table[0]), &(rt_flag1_ thread.tlist) ); 94 95 /* 初始化线程 */ 96 rt_thread_init( &rt_flag2_thread, /* 线程控制块 */ 97 flag2_thread_entry, /* 线程入口地址 */ 98 RT_NULL, /* 线程形参 */ 99 &rt_flag2_thread_stack[0], /* 线程栈起始地址 */ 100 sizeof(rt_flag2_thread_stack)); /* 线程栈大小,单位为字节 */ 101 /* 将线程插入就绪列表 */ 102 rt_list_insert_before( &(rt_thread_priority_table[1]), &(rt_flag2_ thread.tlist) ); 103 104 /* 启动系统调度器 */ 105 rt_system_scheduler_start(); 106 } 107 108 /* 109 ************************************************************************ 110 * 函数实现 111 ************************************************************************ 112 */ 113 /* 软件延时 */ 114 void delay (uint32_t count) 115 { 116 for (; count! =0; count--); 117 } 118 119 /* 线程1 */ 120 void flag1_thread_entry( void *p_arg ) 121 { 122 for ( ; ; ) 123 { 124 flag1 = 1; 125 delay( 100 ); 126 flag1 = 0; 127 delay( 100 ); 128 129 /* 线程切换,这里是手动切换 */ 130 rt_schedule(); (注意) 131 } 132 } 133 134 /* 线程2 */ 135 void flag2_thread_entry( void *p_arg ) 136 { 137 for ( ; ; ) 138 { 139 flag2 = 1; 140 delay( 100 ); 141 flag2 = 0; 142 delay( 100 ); 143 144 /* 线程切换,这里是手动切换 */ 145 rt_schedule(); (注意) 146 } 147 } 148
代码清单3-31中每个局部的代码均已经讲解过,其作用查看代码注释即可。
代码清单3-31(注意):因为目前还不支持优先级,每个线程执行完毕之后都主动调用系统调度函数rt_schedule()来实现线程的切换。
3.6 实验现象
本章代码讲解完毕,接下来是软件调试仿真,具体过程如图3-16~图3-20所示。
图3-16 单击Debug按钮,进入调试界面
图3-17 单击逻辑分析仪按钮,调出逻辑分析仪
图3-18 将要观察的变量添加到逻辑分析仪
图3-19 将变量设置为Bit模式,默认是Analog
图3-20 单击全速运行按钮查看波形,Zoom栏中的In、Out、All按钮可放大和缩小波形
至此,本章讲解完毕。但是,只是把本章的内容看完,然后再仿真看看波形是远远不够的,应该把当前线程控制块指针rt_current_thread、就绪列表rt_thread_priority_table、每个线程的控制块、线程的入口函数和线程的栈这些变量统统添加到观察窗口,然后单步执行程序,看看这些变量是怎么变化的,特别是线程切换时,CPU寄存器、线程栈和PSP是怎样变化的,让机器执行代码的过程在自己的头脑中过一遍。如图3-21所示就是笔者在仿真调试时显现的观察窗口。
图3-21 软件调试仿真时的Watch窗口