RT-Thread 简单学习笔记

RT-Thread 简单学习笔记

备忘录

  • 命令“list_thread”查看线程。
  • 使用 MSH_CMD_EXPORT(<函数名称>,<描述、注释 >); 导出到msh命令

线程

  1. 进程: 独享处理器,处于一个完整的地址空间中,更多的侧重于完成一项(独立的)任务。
  2. 线程: 侧重于请求、管理、服务等形式,从并发的角度考虑问题。当需要执行一项工作时,先由一个线程传递一个请求给服务线程,当工作完成后再由服务线程给出相应的响应或传递回计算结果。

线程及其功能特点

在 RT-Thread 实时操作系统中,任务采用了线程来实现,线程是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的上下文关系,也描述了这个任务所处的优先等级。优先级相同的线程是时间片参数起的作用。
线程编码形式:

  1. 无限循环模式:线程中执行while(1)。循环中需要有让出CPU使用权的动作,例如rt-_thread_delay( );不能陷入死循环。
  2. 顺序执行或有限次循环模式::简单的顺序语句、do whlie() 和for() 循环等,此类线程不会永久循环,是一定会被执行完毕的。执行完毕后,线程将被系统自动删除。

线程工作机制
线程的三种环境:
1 . 中断服务环境:非线程环境,不能挂起当前线程。
2. 空闲线程环境:最低优先级的线程,死循环无法被挂起。可用钩子函数自定义功能。
3. 普通线程环境:普通线程中不能陷入死循环操作,必须要有让出CPU使用权的动作。

线程状态图

在这里插入图片描述

线程相关总结

设置静态线程和动态线程的对象,并设置静态线程运行时用到的栈。

/*  静态线程1 的对象和运行时用到的栈 */  
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[THREAD_STACK_SIZE]; 

/*  动态线程2 的对象 */  
static rt_thread_t thread2 = RT_NULL;
  • 动态线程的创建、删除和启动
  1. 创建动态线程使用函数rt_thread_create( );
    rt_thread_create(“线程名称”,线程进入函数,线程进入函数参数,线程栈大小,优先级,时间片大小);
/*  动态线程2 的对象 */  
static rt_thread_t thread2 = RT_NULL;

thread2 = rt_thread_create( "thread2", thread2_entry, RT_NULL, 
			THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); 
  1. 删除线程使用函数 rt_thread_delete( );
    rt_thread_delete(线程对象);
rt_thread_delete(tid1);
  1. 启动线程使用函数 rt_thread_startup( );
    rt_thread_startup(线程对象);
if (thread2 != RT_NULL) rt_thread_startup(thread2); //RT_NULL = 0
  • 静态线程的初始化、脱离和启动
  1. 创建线程使用函数 rt_thread_init( ) ;
    rt_thread_init(&线程对象,“线程名称”,线程进入函数,线程进入函数参数,&线程栈,线程栈大小,优先级,时间片大小);
	rt_err_t result; 

	/*  初始化线程1 */ 
	/*  线程的入口是thread1_entry ,参数是RT_NULL 
	 *  线程栈是thread1_stack 栈空间是512 ,  
	 *  优先级是25 ,时间片是5个OS Tick 
	 */  
	result = rt_thread_init(&thread1, "thread1", 
			thread1_entry, RT_NULL, 
			&thread1_stack[0], sizeof(thread1_stack), 
			THREAD_PRIORITY, THREAD_TIMESLICE); 
  1. 脱离线程使用函数 rt_thread_detach( ) ;
    rt_thread_detach(&线程对象);
rt_thread_detach(&thread1);
  1. 启动线程使用函数 rt_thread_startup( );
    rt_thread_startup(&线程对象 );
if (result == RT_EOK) rt_thread_startup(&thread1); //#define RT_EOK        0  /**< There is no error */

线程相关应用

下面是关于线程的实际运用。

1.不同线程相同进入函数

对不同的线程设置不同的入口参数,在线程进入函数中读取不同的入口参数,从而区分运行“不同”的线程函数。这样使用的好处是入口函数可以重用。

示例代码:

static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

/* 线程入口 */
static void thread_entry(void* parameter)
{
    rt_uint32_t count = 0;
    rt_uint32_t no = (rt_uint32_t) parameter; /* 获得线程的入口参数 */

    while (1)
    {
        /* 打印线程计数值输出 */
        rt_kprintf("thread%d count: %d\n", no, count ++);
        /* 休眠10个OS Tick */
        rt_thread_delay(10);
    }
}

void test_thread_06(void)
{
	tid1 = rt_thread_create("thread1",
		thread_entry, (void*)1, /* 线程入口是thread_entry, 入口参数是1 */
		THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
	if (tid1 != RT_NULL)
		rt_thread_startup(tid1);
	else
		return ;

	tid2 = rt_thread_create("thread2",
		thread_entry, (void*)2, /* 线程入口是thread_entry, 入口参数是2 */
		THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
	if (tid2 != RT_NULL)
		rt_thread_startup(tid2);
	else
		return ;
}

2.线程让出

线程让出函数:rt_thread_yield(void) 此函数会让出当前线程函数的优先权,调度器会选取下一优先级的线程函数运行,让出完毕后原线程函数仍处于就绪状态。

  • This function will let current thread yield processor, and scheduler will choose a highest thread to run. After yield processor, the current thread is still in READY state.
/* 线程1入口 */
static void thread1_entry(void* parameter)
{
    rt_uint32_t count = 0;
    int i=0;

    for(i = 0 ; i < 10 ; i++)
    {
        /* 执行yield后应该切换到thread2执行*/
        rt_thread_yield();
        /* 打印线程1的输出*/
        rt_kprintf("thread1: count = %d\n", count ++);
    }
}
/* 线程2入口 */
 static void thread2_entry(void* parameter)
 {
        rt_uint32_t count = 0;
    int i=0;

    for(i = 0 ; i < 10 ; i++)
    {
        /* 打印线程2的输出*/
        rt_kprintf("thread2: count = %d\n", count ++);
        /* 执行yield后应该切换到thread1执行*/
        rt_thread_yield();
    }
}

3.线程优先级抢占

优先级高的线程若设置了rt_thread_delay( );函数,则在延时过程中会被其他线程抢占优先级。
在下例中,线程1的优先级为24,线程2的优先级为25,线程1先执行,但随后调用了rt_thread_delay(3000);函数,被线程2抢占了优先级,因此线程2先打印了2行数据,当延时结束后,线程1重新获得优先级,并继续打印。

 /* 线程1入口*/ 
static void thread1_entry(void* parameter) 
{
  rt_uint32_t count ; 
 for(count = 0;count<4;count++) 
 { 
        rt_thread_delay(RT_TICK_PER_SECOND*3);  //被抢占优先级
        rt_kprintf("count = %d\n", count); 
 } 
} 

 /* 线程2入口*/
static void thread2_entry(void* parameter) 
{ 
    rt_tick_t tick; 
    rt_uint32_t i; 

    for(i=0; i<10 ; ++i) 
    { 
        tick = rt_tick_get(); 
        rt_thread_delay(RT_TICK_PER_SECOND);
        rt_kprintf("tick = %d\n",tick++); 
    } 
} 

4.线程挂起

高优先级线程可以将低优先级线程挂起(暂停),也可以将本线程挂起。使用函数rt_thread_suspend(<线程名>);

使用命令list_thread可以查看线程

/* 线程 1 入口 */

static void thread1_entry(void* parameter)
{
	rt_uint32_t count = 0;
	while (1)
	{
	/* 线程 1 采用低优先级运行,一直打印计数值 */
	rt_kprintf("thread count: %d\n", count ++);
	rt_thread_delay(RT_TICK_PER_SECOND);
	}
}

/* 线程 2 入口 */

static void thread2_entry(void* parameter)
{
	rt_thread_delay(RT_TICK_PER_SECOND*2);
	/* 挂起线程 1 */
	rt_thread_suspend(tid1);
}

5.线程的恢复

当一个线程被挂起后,另外一个线程可以将其唤醒,使用函数rt_thread_resume(<线程名>)

线程一代码中挂起自身,然后使用rt_schedule( );主动执行线程调度。

rt_kprintf("thread1 suspend\n"); /* 挂起自身 */
rt_thread_suspend(tid1);
rt_schedule();
/* 主动执行线程调度 */
rt_kprintf("thread1 resumed\n");

主动执行调度后,优先级较低的线程二代码中唤醒了优先级较高的线程一
rt_thread_resume(tid1);

static void thread2_entry(void* parameter)
{
rt_thread_delay(RT_TICK_PER_SECOND*5);
/* 唤醒线程 1 */
rt_thread_resume(tid1);
rt_thread_delay(10);
}

6.线程睡眠

以下两个函数能够让当前线程延时一段时间,同时释放出占用(相当于挂起线程)。
rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
这两个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定了线程的休眠时间(单位是 OS Tick 时钟节拍)。
使用示例:


rt_thread_delay(10);
rt_thread_delay(RT_TICK_PER_SECOND*5);

7.线程控制

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

rt_thread_control( ) 命令使用方法:

/**
 * This function will control thread behaviors according to control command. 根据命令做出动作。
 * @param thread the specified thread to be controlled 控制对象的函数名
 * @param cmd the control command, which includes  
 *  RT_THREAD_CTRL_CHANGE_PRIORITY for changing priority level of thread;  更换优先级
 *  RT_THREAD_CTRL_STARTUP for starting a thread;		启动线程
 *  RT_THREAD_CTRL_CLOSE for delete a thread; 删除线程
 *  RT_THREAD_CTRL_BIND_CPU for bind the thread to a CPU.    绑定线程至CPU???
 * @param arg the argument of control command  参数
 *
 * @return RT_EOK  标志位
 */

使用示例: rt_thread_control(tid, RT_THREAD_CTRL_CHANGE_PRIORITY, &prio);

8.相同优先级线程问题

当两个优先级相同的线程同时开启时,在定义线程时被赋予过时间片,两个优先级相同的线程轮流执行,当自己的时间片到时,交出控制权给相同优先级的其他线程轮流执行。

9.空闲线程和钩子

正常情况下CPU执行完所有线程,没有任务在处理时,CPU会进入空闲线程。空闲线程的优先级最低,采用静态线程创建方式创建。 创建方式如下:

rt_thread_init(&idle,"tidle",

rt_thread_idle_entry,

RT_NULL,

&rt_thread_stack[0],

sizeof(rt_thread_stack),

RT_THREAD_PRIORITY_MAX - 1,

32);

学到这里的时候发现:在启动空闲线程后再启动其他优先级的函数,系统会自动调度到优先级高的线程执行,在执行完毕后进入空闲线程等待。

定时器

使用定时器需要打开编译开关 ,即在文件rtconfig.h 文件中 , 定义宏 RT_USING_TIMER_SOFT。

定时器也分为动态定时器和静态定时器,静态定时器需要初始化,动态定时器需要创建。空闲线程的优先级最低(最大值-1),它的进入函数称为钩子函数,如果需要使用钩子函数,则可能需要修改空闲线程的堆栈大小。

RT-Thread的软件定时器有两种类型:

  • 单次触发定时器:仅触发一次定时器时间,然后定时器停止。
  • 周期触发定时器:周期触发事件。

定时器的结构体定义如下:

struct rt_timer
{
    struct rt_object parent;                            /**< inherit from rt_object */

    rt_list_t        row[RT_TIMER_SKIP_LIST_LEVEL];		/* 定时器列表算法用到的队列 */

    void (*timeout_func)(void *parameter);              /*定时器超时进入函数*/
    void            *parameter;                         /*定时器超时进入函数的参数*/

    rt_tick_t        init_tick;                         /**< 初始超时调用节拍数 */
    rt_tick_t        timeout_tick;                      /**< 实际超时时的节拍数*/
};
typedef struct rt_timer *rt_timer_t;

1.动态定时器

动态定时器,由r t_timer_create( )创建

static rt_timer_t timer1;//创建的定时器结构体 名为timer

void test_timer_01(void)
{
	/* 创建定时器1 */
	timer1 = rt_timer_create("timer1",  /* 定时器名字是 timer1 */
						timeout1, /* 超时时回调的处理函数 */
						RT_NULL,  /* 超时函数的入口参数 */
						100,       /* 定时长度,以OS Tick为单位,即100个OS Tick */
						RT_TIMER_FLAG_PERIODIC); /* 周期性定时器 */
						//若需要定义为单次定时器则:
						//RT_TIMER_FLAG_ONE_SHOT /* 单次定时器 */

	/* 启动定时器 */
	if (timer1 != RT_NULL) rt_timer_start(timer1);
}

/* 定时器1超时函数 */
static void timeout1(void* parameter)
{
    rt_kprintf("periodic timer is timeout\n");
}

2.静态定时器

静态定时器与动态定时器的功能类似,只是代码中有些许差别。

static struct rt_timer timer1;
void test_timer_02(void)
{
    /* 初始化定时器 */
    rt_timer_init(&timer1, "timer1",  /* 定时器名字是 timer1 */
                    timeout1, /* 超时时回调的处理函数 */
                    RT_NULL, /* 超时函数的入口参数 */
                    100, /* 定时长度,以OS Tick为单位,即100个OS Tick */
                    RT_TIMER_FLAG_PERIODIC); /* 周期性定时器 */
    /* 启动定时器 */
    rt_timer_start(&timer1);
}

/* 定时器1超时函数 */
static void timeout1(void* parameter)
{
    rt_kprintf("periodic timer is timeout\n");
}

3.定时器控制接口

定时器在使用过程中,可以修改其参数。
使用函数rt_timer_control(rt_timer_t timer, int cmd, void *arg)
rt_timer_control(<定时器名>,<命令名>,<可能用到的参数>)

**
 * This function will get or set some options of the timer
 *
 * @param timer the timer to be get or set
 * @param cmd the control command
 * 		RT_TIMER_CTRL_GET_TIME // 获得当前定时器定时时长
 * 		RT_TIMER_CTRL_SET_TIME// 设定定时器定时时长
 * 		RT_TIMER_CTRL_SET_ONESHOT// 切换为单触发定时器
 * 		RT_TIMER_CTRL_SET_PERIODIC// 切换为周期触发定时器
 * @param arg the argument
 *
 * @return RT_EOK
 */

4.如何使用定时器

任务间同步与通信

中断与临界区的保护

1.线程抢占导致临界区问题

临界区指的是公共资源的代码区,他同时只允许一个线程访问,独占CPU。临界区的资源称为临界资源。
在多个线程使用同一个共享变量时,如果没有使用中断锁对共享变量的读写做保护,那么很可能得不到我们想要的结果,为了解决这种问题,需要引入线程间通信机制,这就是所谓的 IPC 机制(Inter-Process Communication),IPC的方式主要有:信号量、互斥量、时间、邮箱和消息队列。


/*定义共享变量*/
int share_var;
/*使用中断锁时的定义变量*/
rt_uint32_t level;

static void thread1_entry(void* parameter)
{
    int i;
    share_var = 0;
    /* 使用中断锁关闭中断 */
    level = rt_hw_interrupt_disable();
    rt_kprintf("share_var = %d\n", share_var);
    for(i=0; i<100000; i++)
    {
        share_var ++;
    }
    rt_kprintf("\r\nshare_var = %d\n", share_var);
    rt_hw_interrupt_enable(level);
}

static void thread2_entry(void* parameter)
{
    /*延时修改为1000后,就不会打断线程1的share_var累加*/
    rt_thread_delay(1);
    share_var ++;
}

如上述代码所示,线程2的优先级高于线程1,在线程1的执行过程中,对共享变量share_var进行累计,为了避免线程2中对共享变量的影响,线程1中加入中断锁rt_hw_interrupt_disable(); rt_hw_interrupt_enable(level); 来避免对share_var产生影响。

同样的,进入临界区还可以使用rt_enter_critical()rt_exit_critical()进入临界区。
rt_hw_interrupt_disable();关闭中断,CPU不再进行调度。
rt_enter_critical() 对调度器上锁,系统仍然能响应外部中断。
使用时应该注意:临界区的代码不要过多地占用CPU的时间,否则会占用时间,RTT的实时操作系统的意义也不存在了。

2.线程同步

类似于上一小节的问题,当多个任务需要访问共享的数据时,需要处理好数据的时效性,例如:传感器线程写入数据至共享数据,另外一个发送线程将共享数据周期发送出去,如何做到两个线程间隔执行不干扰,保持共享数据的一致性呢?

使用中断锁进行线程间同步

中断关闭的时候,当前线程的任务不会被其他时间打断,相当于只执行当前线程内的任务,并且当前线程不会被抢占,除非该任务主动放弃控制权。使用时需要注意中断关闭时间不能持续很长

level = rt_hw_interrupt_disable();//关闭中断
a = a + value; //保证其他时间不会对a的值产生影响。
rt_hw_interruput_enable(level)//打开中断

另外一种方法是使用信号锁,关于信号量的概念后面再讲。具体代码如下

/* 获得信号量锁*/ 
rt_sem_take(sem_lock, RT_WAITING_FOREVER);
a = a + value;
/* 释放信号量锁*/
 rt_sem_release(sem_lock);
使用调度锁

使用调度锁的方式和上一节的方法一样。

void rt_enter_critical(void); /* 进入临界区*/ 
a = a + value;
void rt_exit_critical(void); /* 离开临界区*/

3.信号量

信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值