上一节通过一个具体的RT-Thread工程探索了RT-Thread是如何启动的,这节还是以应用为目的学习,通过实现BlinkLED学习RTOS如何编程应用。
1.单片机中的两种系统
在实现BlinkLED之前,先插一段题外话,讲讲单片机中的两种系统及其区别,为实现BlinkLED作以铺垫。
1.1.裸机系统
裸机系统就是我们大多数人所写的程序,直接运行在单片机上,并没有操作系统。
1.1.1.轮询系统
最简单的裸机系统就是一个main函数,单片机重复不断的执行while(1)中的程序,这种模式很像单片机不断的进行查询判断,然后执行相应程序,比如在流水灯程序中查询判断延时时长是否满足设定值,在键盘程序中查询判断按键是否按下,这样的系统称为轮询系统,伪代码如下所示;这种轮询系统很简单,如果项目中只是一些简单的应用,并不需要实时响应用户,那么选用这种系统是再好不过的;
int main(void)
{
/* 初始化相关硬件 */
HardWareInit();
/* 无限循环 */
while(1)
{
/* 处理任务1 */
handle_task1();
/* 处理任务2 */
handle_task2();
/* 处理任务3 */
handle_task3();
......
}
}
这种系统有个致命的缺点——实时响应性差!在处理任务1的时候,任务2中的按键被按下,这个时候就无法响应到这个操作,所以有了第二种裸机系统用来解决这个问题;
1.1.2.前后台系统
这种系统加入了中断服务程序,比如之前所出现的情况——在处理任务1的时候,任务2中的按键被按下,这个时候如果按键可以触发中断,那么CPU就会跑去执行相应的中断服务程序,执行完成后再接着之前的程序开始执行,这样系统的实时响应性就有个进一步的提高,这种系统称为前后台系统,中断服务程序称为前台,main函数中的无限循环称为后台,伪代码如下:
int main(void)
{
/* 初始化相关硬件 */
HardWareInit();
/* 无限循环 */
while(1)
{
/* 处理任务1 */
handle_task1();
/* 处理任务2 */
handle_task2();
/* 处理任务3 */
handle_task3();
......
}
}
//中断服务程序1
void ISR1(void)
{
//中断处理程序
}
这种系统提高了实时响应性能,但是如果在执行中断的时候再发生了一个优先级高的突发事件,发生中断嵌套,CPU转去处理新的中断程序,然后依次返回;
1.2.多线程系统
由上面可以看出,裸机系统的效率并不高,CPU一直在顺序的执行某一段程序,这个时候操作系统的优势就体现出来了,操作系统都是多线程操作系统,虽然只有一个CPU,但是通过操作系统的安排和调度,可以在外接看来并行的执行很多个线程,系统的实时响应处理能力大大提升;
在多线程系统中将整个程序主体分为一个个独立的,无限循环且不能返回的小程序,这一个个小程序就称为线程,每个线程都是独立的,互不干扰的,且具备自身的优先级,由操作系统进行调度和管理,称为多线程系统,伪代码如下:
int main(void)
{
/* 初始化相关硬件 */
HardWareInit();
/* 无限循环 */
while(1)
{
/* 处理任务1 */
handle_task1();
/* 处理任务2 */
handle_task2();
/* 处理任务3 */
handle_task3();
......
}
}
//中断服务程序1
void ISR1(void)
{
/* 中断处理程序 */
}
//一个个独立的线程
void handle_task1()
{
/* 无限循环,不返回 */
while(1)
{
/* 线程实体 */
}
}
在这里有人也许会疑惑,在单片机有限的FLASH和RAM中运行一个操作系统,资源耗费是不是会很大?这个顾虑完全是多余的,如今单片机的内部FLASH和RAM越来越大,如果不够用,很方便外扩,完全可以流畅运行一个操作系统并在此基础上做很多事情~~~
2.操作系统的基础——线程
2.1.什么是线程
(在1.2节中提到)在多线程系统中将整个程序主体分为一个个独立的,无限循环且不能返回的小程序,这一个个小程序就称为线程,每个线程都是独立的,互不干扰的,且具备自身的优先级,由操作系统进行调度和管理;
2.2.线程的组成
RT-Thread中的线程由三部分组成:
- 线程主体(函数)
- 线程控制块
- 线程堆栈
接下来以工程所含例程中的"/package/samples/basic/led/led.c"文件为例进来探索RTT中的线程:
2.2.1.线程主体
static void led_thread_entry(void *parameter)
{
unsigned int count = 0;
rt_hw_led_init();
while (1)
{
/* led1 on */
rt_kprintf("led on, count : %d\r\n", count);
count++;
rt_hw_led_on(0);
/* sleep 0.5 second and switch to other thread */
rt_thread_delay(RT_TICK_PER_SECOND / 2);
/* led1 off */
rt_kprintf("led off\r\n");
rt_hw_led_off(0);
rt_thread_delay(RT_TICK_PER_SECOND / 2);
}
}
这是led线程的主体,可以看到,整个线程的主体是一个无限循环的程序,和我们在裸机下点灯的程序区别不大,只是将其中的具体实现换成了RT-Thread的实现:
int main()
{
/* led初始化(GPIO)*/
led_init();
/* 无限循环 */
while(1)
{
led_on();
delay();
led_off();
delay();
}
}
2.2.2.线程控制块
/* 线程的TCB控制块 */
static struct rt_thread led_thread;
线程控制块就相当于线程的身份证,记录了线程的各个属性,比如线程的名称,线程的栈指针,线程的形参等等,有了这个线程控制块后,系统可以用线程控制块链表对其进行管理,具体的线程控制块结构体声明可以在rtdef.h中看到;
2.2.3.线程堆栈
ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t led_stack[ 512 ];
在裸机程序中,程序的运行过程中有变量的定义,子函数的调用,中断的产生等等,那么,这些变量放在哪里?子函数调用时,子函数的局部变量放在哪里?中断发生时,函数返回地址放在哪里?这些在裸机编程时都不用考虑,但在RTOS编程中这些是程序运行的基本条件,必须要一清二楚!
这些东西都被存放在栈中,即RAM的一段空间内,栈的大小在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。但是在多线程操作系统中,每个线程就是一个独立运行的程序,且不能互相干扰,所以每个线程都要分配独立的栈空间,这个栈空间可以是一个预先定义好的全局数组,比如led线程的堆栈,这样的线程称为静态线程,相对的,如果这个栈空间是动态分配的,该线程称为动态线程,这个在后续会具体说明;
就led线程来看,首先设置下面的变量需要多少个字节对齐,ALIGN是一个带参宏定义,在rtdef.h中可以看到:
#define ALIGN(n) __attribute__((aligned(n)))
ALIGN的参数也是由宏定义指定的,在rtconfig.h中可以看到:
#define RT_ALIGN_SIZE 4
设置好变量需要多少个字节对齐后,定义led线程的线程栈,其实就是一个全局数组,数据类型为rt_uint8_t,数组大小为512;
这里需要注意一点,在RT-Thread中,凡是涉及到数据类型的地方,RT-Thread都会将标准的C数据类型用typedef重新取一个类型名,以"rt"前缀开头。这些重新定义的数据类型放在rtdef.h中,代码如下:
/* RT-Thread basic data type definitions */
typedef signed char rt_int8_t; /**< 8bit integer type */
typedef signed short rt_int16_t; /**< 16bit integer type */
typedef signed long rt_int32_t; /**< 32bit integer type */
typedef unsigned char rt_uint8_t; /**< 8bit unsigned integer type */
typedef unsigned short rt_uint16_t; /**< 16bit unsigned integer type */
typedef unsigned long rt_uint32_t; /**< 32bit unsigned integer type */
typedef int rt_bool_t; /**< boolean type */
/* 32bit CPU */
typedef long rt_base_t; /**< Nbit CPU related date type */
typedef unsigned long rt_ubase_t; /**< Nbit unsigned CPU related data type */
typedef rt_base_t rt_err_t; /**< Type for error number */
typedef rt_uint32_t rt_time_t; /**< Type for time stamp */
typedef rt_uint32_t rt_tick_t; /**< Type for tick count */
typedef rt_base_t rt_flag_t; /**< Type for flags */
typedef rt_ubase_t rt_size_t; /**< Type for size number */
typedef rt_ubase_t rt_dev_t; /**< Type for device */
typedef rt_base_t rt_off_t; /**< Type for offset */
/* boolean type definitions */
#define RT_TRUE 1 /**< boolean true */
#define RT_FALSE 0 /**< boolean fails */
至此,RT-Thread的一个具体的线程就了解完毕,一个线程的三大要素——主体,控制块,堆栈也都作以了解,接下来我们开始最重要的部分,创建一个线程;
2.3.线程的创建
在2.2.3节讲述创建线程堆栈时提到,如果栈空间可以是一个预先定义好的全局数组,比如led线程的堆栈,这样的线程称为静态线程,相对的,如果这个栈空间是动态分配的,该线程称为动态线程**。
接下来通过具体的实例说明:
2.3.1.静态线程的创建
RT-Thread中静态线程的线程堆栈由编译器静态分配,使用rt_thread_init()函数创建,代码如下所示:
int led_sample_init(void)
{
rt_err_t result;
/* init led thread */
result = rt_thread_init(&led_thread,
"led",
led_thread_entry,
RT_NULL,
(rt_uint8_t *)&led_stack[0],
sizeof(led_stack),
20,
5);
if (result == RT_EOK)
{
rt_thread_startup(&led_thread);
}
return 0;
}
在这个函数中调用了rt_thread_init(......)函数初始化led线程,这个函数暂且先不深入,这个函数是我们所用的RTT官方第一个函数,遵循RTT的命名规则,以小写的rt开头,表示这是一个外部函数,可以由yoghurt调用,以_rt开头表示只能由RT-Thread内部使用,紧接着是文件名,表示该函数改放在哪个文件,最后是函数名称;
初始化完了之后,调用 rt_thread_startup() 函数启动线程并将其加入到就绪队列中,并启动调度器,然后由操作系统运行,进行线程切换等等,这个部分是RT-Thread内核的重中之重,到后面再具体深入讲解,这里了解即可。
讲述完了 led_sample_init 这个函数,其功能就是初始化线程,然后启动线程,加入就绪队列,由系统调度,切换,运行,所以我么只需要在用户入口代码中调用这个函数即可:
extern int led_sample_init(void);
int main(void)
{
/* user app entry */
led_sample_init();
return 0;
}
然后编译,进入debug模式,运行,可以从UART#1窗口中观察到现象:
这里我们采用了原有的led例程,从如何创建一个线程,到如何初始化一个线程,最后运行流水灯线程,成功看到UART#1打印出的实验现象,下面我们来自己动手创建两个线程,一个静态线程,一个动态线程,方便对比,为了调用led相关函数,我们直接在led.c中编写;
2.3.2.动态线程的创建
RT-Thread中静态线程的线程堆栈由编译器静态分配,使用 rt_thread_init() 函数创建;而RT-Thread中动态线程的堆栈是由系统动态分配的,使用rt_thread_create() 创建,因为两个线程操作的是统一个led,为了避免冲突,我们只适用串口打印观察现象,代码如下:
修改led1线程的主体代码如下:
static void led1_thread_entry(void *parameter)
{
//rt_hw_led_init();
while (1)
{
/* led1 on */
rt_kprintf("led1 on\r\n");
//rt_hw_led_on(0);
rt_thread_delay(RT_TICK_PER_SECOND / 2);
/* led1 off */
rt_kprintf("led1 off\r\n");
//rt_hw_led_off(0);
rt_thread_delay(RT_TICK_PER_SECOND / 2);
}
}
然后增添led2线程的主体代码:
static void led2_thread_entry(void *parameter)
{
//rt_hw_led_init();
while (1)
{
/* led2 on */
rt_kprintf("led2 on\r\n");
//rt_hw_led_on(0);
rt_thread_delay(RT_TICK_PER_SECOND / 2);
/* led2 off */
rt_kprintf("led2 off\r\n");
//rt_hw_led_off(0);
rt_thread_delay(RT_TICK_PER_SECOND / 2);
}
}
然后新建一个函数创建这两个线程,代码如下:
int blink_led_init(void)
{
rt_err_t result;
/* 动态线程的线程控制块指针 */
rt_thread_t led2_thread;
/* 创建静态线程*/
result = rt_thread_init(&led_thread,
"led",
led1_thread_entry,
RT_NULL,
(rt_uint8_t *)&led_stack[0],
sizeof(led_stack),
20,
5);
if (result == RT_EOK)
{
rt_thread_startup(&led_thread);
}
/* 创建动态线程*/
led2_thread = rt_thread_create("led2",
led2_thread_entry,
RT_NULL,
512,
21,
5);
if (led2_thread != RT_NULL)
{
rt_thread_startup(led2_thread);
}
return 0;
}
和之前一样,在用户入口代码中调用这个函数:
extern int blink_led_init(void);
int main(void)
{
/* user app entry */
blink_led_init();
return 0;
}
编译,进入debug模式,运行,成功观察到现象:
2.3.3.静态线程与动态线程的异同
通过2.3.2中创建了一个静态线程和一个动态线程运行可以看出,两者在运行结果上并无差异,那实际应用中该如何选择呢?
使用静态线程时,必须先定义静态的线程控制块,并且定义好堆栈空间,然后调用 rt_thread_init() 来完成线程的初始化工作。采用这种方式,线程控制块和堆栈占用的内存会放在 RW/ZI 段,这段空间在编译时就已经确定,它不是可以动态分配的,所以不能被释放,而只能使用 rt_thread_detach() 函数将该线程控制块从对象管理器中脱离。
使用动态定义方式 rt_thread_create() 时, RT-Thread 会动态申请线程控制块和堆栈空间。在编译时,编译器是不会感知到这段空间的,只有在程序运行时, RT-Thread 才会从系统堆中申请分配这段内存空间,当不需要使用该线程时,调用 rt_thread_delete() 函数就会将这段申请的内存空间重新释放到内存堆中。
这两种方式各有利弊,静态定义方式会占用 RW/ZI 空间,但是不需要动态分配内存,运行时效率较高,实时性较好。动态方式不会占用额外的 RW/ZI 空间,占用空间小,但是运行时需要动态分配内存,效率没有静态方式高。总的来说,这两种方式就是空间和时间效率的平衡,可以根据实际环境需求选择采用具体的分配方式。