RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
关于RT-Thread定时器的使用可以参考定时器源码浅析及其应用
1 线程内置定时器的初始化流程
- 线程控制块
/* 线程控制块 */
struct rt_thread
{
/* 内核对象基本信息 */
char name[RT_NAME_MAX]; /**< 线程名称 */
rt_uint8_t type; /**< 对象类型 */
rt_uint8_t flags; /**< 线程标志位 */
...
struct rt_timer thread_timer; /**< 内置线程定时器 */
...
};
可以看到每个线程对象都有一个内置定时器对象thread->thread_timer
。
- 内置定时器的初始化
使用rt_thread_create
或rt_thread_init
函数初始化线程时会调用_thread_init
函数,然后该函数内部会对线程内置定时器进行初始化:
static rt_err_t _thread_init(struct rt_thread *thread,
const char *name,
void (*entry)(void *parameter),
void *parameter,
void *stack_start,
rt_uint32_t stack_size,
rt_uint8_t priority,
rt_uint32_t tick)
{
...
/* 初始化线程定时器 */
rt_timer_init(&(thread->thread_timer),
thread->name,
rt_thread_timeout,
thread,
0,
RT_TIMER_FLAG_ONE_SHOT);
...
}
将线程内置定时器初始化为单次触发硬件定时器(timer->parent.flag
没有或|上RT_TIMER_FLAG_SOFT_TIMER
的话,就为硬件定时器,因为初值为0),其超时回调函数为rt_thread_timeout
。
最终会在滴答定时器Systick中断里,调用硬件定时器扫描函数rt_timer_check
扫描硬件定时器列表,当有超时事件发生时就会调用对应的超时函数,因为是单次触发,所以只会触发一次定时器事件,触发后会将该定时器设为非激活态且不会再次启动。
问:动态创建的线程,其内置定时器初始化为什么可以使用静态方法?
答:线程对象内存虽然是动态申请的,但是定时器对象是一个静态的对象struct rt_timer
,是一个定时器结构体变量,而不是一个指向该结构体的指针,所以必须使用静态方法初始化。
- 内置定时器超时回调函数
void rt_thread_timeout(void *parameter)
{
struct rt_thread *thread;
register rt_base_t temp;
thread = (struct rt_thread *)parameter;
/* 参数检查 */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND);
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
/* 关中断 */
temp = rt_hw_interrupt_disable();
/* 设置超时错误标志 */
thread->error = -RT_ETIMEOUT;
/* 将线程从挂起列表中移除 */
rt_list_remove(&(thread->tlist));
/* 插入到就绪列表以便于调度 */
rt_schedule_insert_thread(thread);
/* 开中断 */
rt_hw_interrupt_enable(temp);
/* 发起线程调度 */
rt_schedule();
}
所有线程的内置定时器超时回调函数均为rt_thread_timeout
,当某线程内置定时器启动且到达设定的超时时间后,就会调用该函数,将线程的错误码设为超时错误-RT_ETIMEOUT
,然后将线程从挂起列表中移除,插入到就绪列表中,最后发起一次线程调度。
- 线程内置定时器的脱离
当线程被删除或脱离时,函数内部会调用rt_timer_detach(&(thread->thread_timer))
,将线程内置定时器从系统定时器列表中脱离(因为是静态对象)。
2 线程内置定时器的应用
- 线程主动调用延时(睡眠)函数,将自身延时阻塞挂起。
线程主动调用以下函数时,会让线程从运行状态切换到挂起状态,并设置相应的阻塞延时时间,该延时时间即为先内置定时器的超时时间。
rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);
这三个延时函数其实最终都调用的是rt_thread_sleep
:
rt_err_t rt_thread_sleep(rt_tick_t tick)
{
register rt_base_t temp;
struct rt_thread *thread;
/* 获取当前线程句柄 */
thread = rt_thread_self();
RT_ASSERT(thread != RT_NULL);
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
/* 关中断 */
temp = rt_hw_interrupt_disable();
/* 挂起当前线程 */
rt_thread_suspend(thread);
/* 重置线程内置计时器的超时时间并启动它 */
rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
rt_timer_start(&(thread->thread_timer));
/* 开中断 */
rt_hw_interrupt_enable(temp);
rt_schedule(); //> (1)
/* 清除该线程的错误码为RT EOK */
if (thread->error == -RT_ETIMEOUT)
thread->error = RT_EOK;
return RT_EOK;
}
利用中断锁保护临界段,然后将当前线程挂起,再用rt_timer_control
函数修改定时器的超时时间为当前设定的延时时间,然后启动定时器,最后在(1)处发起任务调度。当到达定时器超时时间时,会调用其超时回调函数,将当前线程插入到就绪队列中,并发起一次线程调度。如果当前线程优先级最高,程序会回到(1)处继续执行,因为在超时回调函数中将线程错误码设为了-RT_ETIMEOUT
,但这属于正确调用,所以最后将错误码改为RT_EOK
。
- IPC通信的超时等待机制
使用RT-Thead的IPC通信机制实现多线程间的同步、通信时,当线程利用某通信机制获取某项资源失败而被阻塞时,就会根据设定的等待时间挂起等待该资源,如信号量的获取:
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
register rt_base_t temp;
struct rt_thread *thread;
/* 关中断 */
temp = rt_hw_interrupt_disable();
if (sem->value > 0)
{
/* 有可用信号量 */
sem->value --;
/* 开中断 */
rt_hw_interrupt_enable(temp);
}
else
{
/* 不等待,返回超时错误 */
if (time == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else
{
/* 当前上下文检查 */
RT_DEBUG_IN_THREAD_CONTEXT;
/* 信号量不可用,挂起当前线程 */
/* 获取当前线程 */
thread = rt_thread_self();
/* 设置线程错误码 */
thread->error = RT_EOK;
/* 挂起线程 */
_ipc_list_suspend(&(sem->parent.suspend_thread),
thread,
sem->parent.parent.flag);
/* 有等待时间则开始计时 */
if (time > 0)
{
/* 设置线程超时时间,并启动定时器 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
/* 开中断 */
rt_hw_interrupt_enable(temp);
/* 发起线程调度 */
rt_schedule(); //> (1)
if (thread->error != RT_EOK)
{
return thread->error;
}
}
}
return RT_EOK;
}
当线程获取信号量失败后,如果设置了等待时间,那么会将该线程挂起阻塞,然后将等待时间重新赋值为定时器的超时时间并启动它,最后发起一次线程调度。以下两种情况会唤醒恢复线程,回到(1)处继续执行调度语句后的代码:
- 未到达超时时间,但当前资源变为可用:此时应该有其他执行单元进行了释放该信号量的操作,若此线程是信号量等待队列第一位,就会唤醒它(将线程从挂起列表移除并插入到就绪列表中),然后执行线程调度,若该线程是就绪队列中优先级最高的话,就会回到(1)处继续执行。此时线程错误码
thread->error
值就是调度前设置的RT_EOK
,所以最后会返回RT_EOK
。 - 到达超时时间,资源依然不可用:此时就会执行线程内置定时器超时回调函数了,将线程错误码
thread->error
设为-RT_ETIMEOUT
,然后将线程唤醒,执行线程调度,同样如果该线程是当前就绪队列中优先级最高的话,会回到(1)处继续执行。但此时错误码就是-RT_ETIMEOUT
了,所以函数最后会返回-RT_ETIMEOUT
。
注意:第一种情况,线程内置定时器并未停止,还是需要等超时时间到了,由硬件定时器扫描函数将其设为非激活态。
以上为本人目前所了解的应用,如还有其他应用,欢迎大家私信或在评论区给出。
END