这里写自定义目录标题
RT-Thread 简单学习笔记
备忘录
- 命令“list_thread”查看线程。
- 使用
MSH_CMD_EXPORT(<函数名称>,<描述、注释 >);
导出到msh命令
线程
- 进程: 独享处理器,处于一个完整的地址空间中,更多的侧重于完成一项(独立的)任务。
- 线程: 侧重于请求、管理、服务等形式,从并发的角度考虑问题。当需要执行一项工作时,先由一个线程传递一个请求给服务线程,当工作完成后再由服务线程给出相应的响应或传递回计算结果。
线程及其功能特点
在 RT-Thread 实时操作系统中,任务采用了线程来实现,线程是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的上下文关系,也描述了这个任务所处的优先等级。优先级相同的线程是时间片参数起的作用。
线程编码形式:
- 无限循环模式:线程中执行while(1)。循环中需要有让出CPU使用权的动作,例如rt-_thread_delay( );不能陷入死循环。
- 顺序执行或有限次循环模式::简单的顺序语句、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;
- 动态线程的创建、删除和启动
- 创建动态线程使用函数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);
- 删除线程使用函数 rt_thread_delete( );
rt_thread_delete(线程对象);
rt_thread_delete(tid1);
- 启动线程使用函数 rt_thread_startup( );
rt_thread_startup(线程对象);
if (thread2 != RT_NULL) rt_thread_startup(thread2); //RT_NULL = 0
- 静态线程的初始化、脱离和启动
- 创建线程使用函数 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);
- 脱离线程使用函数 rt_thread_detach( ) ;
rt_thread_detach(&线程对象);
rt_thread_detach(&thread1);
- 启动线程使用函数 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.信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。