嵌入式系统和RT-Thread
在裸机系统中,所有的程序基本都是自己写的,所有的操作都是在一个无限的大循环里面实现。现实生活中的很多中小型的电子产品用的都是裸机系统,而且也能够满足需求。但是为什么还要学习RTOS 编程,偏偏还要整个操作系统进来。一是项目需要,随着产品要实现的功能越来越多,单纯的裸机系统已经不能够完美地解决问题,反而会使编程变得更加复杂,如果想降低编程的难度,我们可以考虑引入RTOS 实现多线程管理,这是使用RTOS 的最大优势。二是学习的需要,必须学习更高级的东西,实现更好的职业规划,为将来走向人生巅峰迎娶白富美做准备,而不是一味的在裸机编程上面死磕。作为一个合格的嵌入式软件工程师,学习是永远不能停歇的事,时刻都得为将来准备。书到用时方恨少,我希望机会来临时你不要有这种感觉。
可偏偏在10 几年前,在中国,有一个天赋异禀,倔强不屈的极客,他叫熊谱翔,编写了RT-Thread 初代内核,并联合中国开源社区的极客不断完善,推陈出新,经过10 几年的发展,如今占据国产RTOS 的鳌头,每年递增数十万的开发者,加上如今AI 和物联网等风口,让RT-Thread 有一统江湖之势,从今年完成A 轮数百万美元的融资就可以看出,在未来不出5 年,RT-Thread 将是你学习和做产品的不二之选。
裸机系统和多线程系统
裸机系统
裸机系统通常分成轮询系统和前后台系统
轮询系统
轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断环,顺序地做各种事情。轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int main(void) { HardWareInit();
for (;;) { DoSomething1();
DoSomething2();
DoSomething3(); } }
|
前后台系统
相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台,main 函数里面的无限循环我们称为后台。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| 1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 8 HardWareInit(); 9 10 11 for (;;) { 12 if (flag1) { 13 14 DoSomething1(); 15 } 16 17 if (flag2) { 18 19 DoSomething2(); 20 } 21 22 if (flag3) { 23 24 DoSomething3(); 25 } 26 } 27 } 28 29 void ISR1(void) 30 { 31 32 flag1 = 1; 33
35 DoSomething1(); 36 } 37 38 void ISR2(void) 39 { 40 41 flag2 = 1; 42 43
45 DoSomething2(); 46 } 47 48 void ISR3(void) 49 { 50 51 flag3 = 1; 52 53
55 DoSomething3(); 56 }
|
多线程系统
相比前后台系统,多线程系统的事件响应也是在中断中完成的,但是事件的处理是在线程中完成的。在多线程系统中,线程跟中断一样,也具有优先级,优先级高的线程会被优先执行。当一个紧急的事件在中断被标记之后,如果事件对应的线程的优先级足够高,就会立马得到响应。相比前后台系统,多线程系统的实时性又被提高了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| 1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 8 HardWareInit(); 9 10 11 RTOSInit(); 12 13 14 RTOSStart(); 15 } 16 17 void ISR1(void) 18 { 19 20 flag1 = 1; 21 } 22 23 void ISR2(void) 24 { 25 26 flag2 = 2; 27 } 28 29 void ISR3(void) 30 { 31 32 flag3 = 1; 33 } 34 35 void DoSomething1(void) 36 { 37 38 for (;;) { 39 40 if (flag1) { 41 42 } 43 } 44 } 45 46 void DoSomething2(void) 47 { 48 49 for (;;) { 50 51 if (flag2) { 52 53 } 54 } 55 } 56 57 void DoSomething3(void) 58 { 59 60 for (;;) { 61 62 if (flag3) { 63 64 } 65 } 66 }
|
相比前后台系统中后台顺序执行的程序主体,在多线程系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为线程。每个线程都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流,不用担心每个功能模块之间是否存在干扰。加入了操作系统,我们的编程反而变得简单了。整个系统随之带来的额外开销就是操作系统占据的那一丁点FLASH 和RAM。现如今,单片机的FLASH 和RAM是越来越大,完全足以抵挡RTOS 那点开销。
线程
在裸机系统中,系统的主体就是main 函数里面顺序执行的无限循环,这个无限循环里面CPU 按照顺序完成各种事情。在多线程系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为线程。
1 2 3 4 5 6 7
| 1 void thread_entry (void *parg) 2 { 3 4 for (;;) { 5 6 } 7 }
|
线程栈
在多线程系统中,每个线程都是独立的,互不干扰的,所以要为每个线程都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM 中。在多线程系统中,有多少个线程就需要定义多少个线程栈。
1 2 3
| rt_uint8_t rt_flag1_thread_stack[512]; (1) rt_uint8_t rt_flag2_thread_stack[512];
|
线程控制块
在裸机系统中,程序的主体是CPU 按照顺序执行的。而在多线程系统中,线程的执行是由系统调度的。系统为了顺利的调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块就相当于线程的身份证,里面存有线程的所有信息,比如线程的栈指针,线程名称,线程的形参等。有了这个线程控制块之后,以后系统对线程的全部操作都可以通过这个线程控制块来实现。
1 2 3 4 5 6 7 8 9
| 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)
|
线程创建
线程的栈,线程的函数实体,线程的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由线程初始化函数rt_thread_init()来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 }
|
在main 函数中创建两个flag 相关的线程实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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) );
|
就绪列表
线程创建好之后,我们需要把线程添加到就绪列表里面,表示线程已经就绪,系统随时可以调度。
1 2
| 1 2 rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; (1)
|
线程控制块里面有一个tlist 成员,数据类型为rt_list_t,我们将线程插入到就绪列表里面,就是通过将线程控制块的tlist 这个节点插入到就绪列表中来实现的。如果把就绪列表比作是晾衣杆,线程是衣服,那tlist 就是晾衣架,每个线程都自带晾衣架,就是为了把自己挂在各种不同的链表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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_flag1_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) );
|
调度器
调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。
调度器在使用之前必须先初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 }
|
我们把调度器初始化放在硬件初始化之后,线程创建之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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 }
|
初始化调度器后,我们就可以启动调度器啦,调度器启动由函数rt_system_scheduler_start()来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
17 rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); (3) 18 }
|
临界段
临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在RT-Thread 里面,这个临界段最常出现的就是对全局变量的操作,全局变量就好像是一个枪把子,谁都可以对他开枪,但是我开枪的时候,你就不能开枪,否则就不知道是谁命中了靶子。那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在RTThread,系统调度,最终也是产生PendSV 中断,在PendSV Handler 里面实现线程的切换,所以还是可以归结为中断。既然这样,RT-Thread 对临界段的保护就处理的很干脆了,直接把中断全部关了,NMI FAULT 和硬FAULT 除外。
对象容器
在RT-Thread 中,所有的数据结构都称之为对象。其中线程,信号量,互斥量、事件、邮箱、消息队列、内存堆、内存池、设备和定时器在rtdef.h 中有明显的枚举定义,即为每个对象打上了一个数字标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1 enum rt_object_class_type 2 { 3 RT_Object_Class_Thread = 0, 4 RT_Object_Class_Semaphore, 5 RT_Object_Class_Mutex, 6 RT_Object_Class_Event, 7 RT_Object_Class_MailBox, 8 RT_Object_Class_MessageQueue, 9 RT_Object_Class_MemHeap, 10 RT_Object_Class_MemPool, 11 RT_Object_Class_Device, 12 RT_Object_Class_Timer, 13 RT_Object_Class_Module, 14 RT_Object_Class_Unknown, 15 RT_Object_Class_Static = 0x80 16 };
|
容器
在rtt 中,每当用户创建一个对象,如线程,就会将这个对象放到一个叫做容器的地方,这样做的目的是为了方便管理,管理什么?在RT-Thread 的组件finsh 的使用中,就需要使用到容器,通过扫描容器的内核对象来获取各个内核对象的状态,然后输出调试信息。
从代码上看,容器就是一个数组,是一个全局变量,数据类型为struct_rt_object_information,在object.c 中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| 1 static struct rt_object_information (1) 2 rt_object_container[RT_Object_Info_Unknown] = { (2) 3 (3) 4 { 5 RT_Object_Class_Thread, (3)-① 6 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Thread), (3)-② 7 sizeof(struct rt_thread) (3)-③ 8 }, 9 10 #ifdef RT_USING_SEMAPHORE (4) 11 12 { 13 RT_Object_Class_Semaphore, 14 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Semaphore), 15 sizeof(struct rt_semaphore) 16 }, 17 #endif 18 19 #ifdef RT_USING_MUTEX (5) 20 21 { 22 RT_Object_Class_Mutex, 23 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Mutex), 24 sizeof(struct rt_mutex) 25 }, 26 #endif 27 28 #ifdef RT_USING_EVENT (6) 29 30 { 31 RT_Object_Class_Event, 32 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Event), 33 sizeof(struct rt_event) 34 }, 35 #endif 36 37 #ifdef RT_USING_MAILBOX (7) 38 39 { 40 RT_Object_Class_MailBox, 41 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MailBox), 42 sizeof(struct rt_mailbox) 43 }, 44 #endif 45 46 #ifdef RT_USING_MESSAGEQUEUE (8) 47 48 { 49 RT_Object_Class_MessageQueue, 50 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MessageQueue), 51 sizeof(struct rt_messagequeue) 52 }, 53 #endif 54 55 #ifdef RT_USING_MEMHEAP (9) 56 57 { 58 RT_Object_Class_MemHeap, 59 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MemHeap), 60 sizeof(struct rt_memheap) 61 }, 62 #endif 63 64 #ifdef RT_USING_MEMPOOL (10) 65 66 { 67 RT_Object_Class_MemPool, 68 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MemPool), 69 sizeof(struct rt_mempool) 70 }, 71 #endif 72 73 #ifdef RT_USING_DEVICE (11) 74 75 { 76 RT_Object_Class_Device, 77 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Device), 78 sizeof(struct rt_device) 79 }, 80 #endif 81 82 (12) 83
90 #ifdef RT_USING_MODULE (13) 91 92 { 93 RT_Object_Class_Module, 94 _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Module), 95 sizeof(struct rt_module) 96 }, 97 #endif 98 };
|
对象初始化
每创建一个对象,都需要先将其初始化,主要分成两个部分的工作,首先将对象控制块里面与对象相关的成员初始化,然后将该对象插入到对象容器中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 1
8 void rt_object_init(struct rt_object *object, (1) 9 enum rt_object_class_type type, (2) 10 const char *name) (3) 11 { 12 register rt_base_t temp; 13 struct rt_object_information *information; 14 15 16 information = rt_object_get_information(type); (4) 17 18 19 object->type = type | RT_Object_Class_Static; (5) 20 21 22 rt_strncpy(object->name, name, RT_NAME_MAX); (6) 23 24 25 temp = rt_hw_interrupt_disable(); (7) 26 27 28 rt_list_insert_after(&(information->object_list), &(object->list)); (8) 29 30 31 rt_hw_interrupt_enable(temp); (9) 32 }
|
对象初始化函数在线程初始化函数里面被调用。
空闲线程
线程体内的延时使用的是软件延时,即还是让CPU 空等来达到延时的效果。使用RTOS 的很大优势就是榨干CPU 的性能,永远不能让它闲着,线程如果需要延时也就不能再让CPU 空等来实现延时的效果。RTOS 中的延时叫阻塞延时,即线程需要延时的时候,线程会放弃CPU 的使用权,CPU 可以去干其它的事情,当线程延时时间到,重新获取CPU 使用权,线程继续运行,这样就充分地利用了CPU 的资源,而不是干等着。
当线程需要延时,进入阻塞状态,那CPU 又去干什么事情了?如果没有其它线程可以运行,RTOS 都会为CPU 创建一个空闲线程,这个时候CPU 就运行空闲线程。在RTThread中,空闲线程是系统在初始化的时候创建的优先级最低的线程,空闲线程主体主要是做一些系统内存的清理工作。鉴于空闲线程的这种特性,在实际应用中,当系统进入空闲线程的时候,可在空闲线程中让单片机进入休眠或者低功耗等操作。
空闲线程也是线程,其定义方法和普通线程定义方法一样。
定义空闲线程栈
1 2 3 4 5 6 7
| 1 #include <rtthread.h> 2 #include <rthw.h> 3 4 #define IDLE_THREAD_STACK_SIZE 512 5 6 ALIGN(RT_ALIGN_SIZE) 7 static rt_uint8_t rt_thread_stack[IDLE_THREAD_STACK_SIZE];
|
定义空闲线程的线程控制块
1 2
| 1 struct rt_thread idle;
|
定义空闲线程函数
在RT-Thread 中空闲线程函数主要是做一些系统内存的清理工作,但是为了简单起见,我们实现的空闲线程只是对一个全局变量rt_idletask_ctr 进行计数
1 2 3 4 5 6 7 8 9 10
| 1 rt_ubase_t rt_idletask_ctr = 0; 2 3 void rt_thread_idle_entry(void *parameter) 4 { 5 parameter = parameter; 6 while (1) 7 { 8 rt_idletask_ctr ++; 9 } 10 }
|
空闲线程初始化
当定义好空闲线程的栈,线程控制块和函数主体之后,我们需要空闲线程初始化函数将这三者联系在一起,这样空闲线程才能够被系统调度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 1 void rt_thread_idle_init(void) 2 { 3 4 (1) 5 rt_thread_init(&idle, 6 "idle", 7 rt_thread_idle_entry, 8 RT_NULL, 9 &rt_thread_stack[0], 10 sizeof(rt_thread_stack)); 11 12 (2) 13 rt_list_insert_before( &(rt_thread_priority_table[RT_THREAD_PRIORITY_MAX-1]), 14 &(idle.tlist) ); 15 }
|
时间片
在RT-Thread 中,当同一个优先级下有两个或两个以上线程的时候,线程支持时间片功能,即我们可以指定线程持续运行一次的时间,单位为tick。假如有两个线程分别为线程2 和线程3,他们的优先级都为3,线程2 的时间片为2,线程3 的时间片为3。当执行到优先级为3 的线程时,会先执行线程2,直到线程2 的时间片耗完,然后再执行线程3。
本次博客主要记录rt-thread的内核学习,下一篇记录应用。