一、前言
最近研究RTThread原理真是又上头又复杂啊,一路研究到这里,记录一下自己对于RTThread里systick和delay实现的一知半解。
二、SysTick 是什么
systick是芯片里的一个硬件定时器,它不是RTThread特有的,是芯片自带的。
它的作用就是产生系统心跳,比如你给它一个值1000,意思就是从1000往下数,数到0时触发一次systick中断,当做节拍心跳用。
在RTThread里,默认配置的是每秒触发1000次systick中断,意思是系统心跳周期就是1ms,每一个tick时长为1ms。
在系统里呢有一个全局变量叫做rt_tick,记录了系统心跳的次数,可以说,rt_tick是所有后面时间计算的基础,所有时间的计算都以它为参照。例如:你要计算线程A运行了多久,计算时就会记录线程A开始运行时rt_tick的值,在记录线程A结束后rt_tick的值,通过这种方式得到结果。很多类似的计算,都是基于rt_tick的。
三、systick中断做什么?
前面我们说了,每次系统心跳一次,就会触发一次systick中断,那这个中断会做什么呢?
首先,当然是给 rt_tick+1了,这样rt_tick才能记录系统心跳。
其次,就是获取当前正在执行的线程的句柄,然后给剩余时间片-1,再检查这个线程的时间片是不是0,如果是0说明线程的执行结束了,要轮换下一个线程了。
最后,就是调用rt_timer_check()函数,负责检测系统里的定时器链表有没有到点的定时器。这个后面会细讲,简单来说就是,一个线程延时挂起之后,线程TCB里专门有一个给线程使用的定时器,此时会被放到这个定时器链表里,这个定时器链表里放的都是所有正在等待的定时器,按照超时时间从小到大排队,这样只要第一个没到点,后面的就没到点。
如果到点了,就把这个定时器链表的第一个线程摘下来,并调用它的超时(timeout)函数,再循环检查下一个。
而这个timeout函数负责把线程重新放回就绪链表末尾,然后重新启用调度器进行调度。
注意:系统里只有一条定时器链表,所有定时器,不管是线程内嵌的延时用的定时器,还是用户创建的软件定时器,最终都挂在这同一条全局软件定时器链表(rt_timer_list)上,由内核用同一个函数 rt_timer_check() 统一检查是否超时。
四、线程延时和唤醒的原理?
1.线程延时
线程延时函数:rt_thread_mdelay()、 rt_thread_delay()这些,其根本原理都是调用的rt_thread_sleep()。
rt_thread_sleep()是以tick为单位的延时函数,比如rt_thread_mdelay,我们输入的是50,意思是我们需要延时50ms,在rt_thread_mdelay这个函数内,就会把50ms转换成对应的tick值,再调用rt_thread_sleep来进行延时。
我们看一下rt_thread_sleep()的代码,其实就是先把当前线程从就绪链表中摘出,然后再挂起线程,并初始化一个定时器给这个线程用来延时,然后把这个定时器放到全局定时器链表里,最后触发调度,刷新一下,这样就算完成线程的延时了。
cs
rt_err_t rt_thread_sleep(rt_tick_t tick)
{
.......
//获取当前线程
thread = rt_thread_self();
........
//关闭全局中断形成临界区保护
level = rt_hw_interrupt_disable();
//在这个函数里,把当前线程从就绪链表上摘下来,然后挂起
err = rt_thread_suspend_with_flag(thread, RT_INTERRUPTIBLE);
if (err == RT_EOK)
{
rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
//给这个线程初始化一个定时器,超时时刻 = 当前 tick + 延时长度
//把超时回调函数绑定成 rt_thread_timeout,然后把这个定时器挂到定时器链表上
rt_timer_start(&(thread->thread_timer));
/* enable interrupt */
rt_hw_interrupt_enable(level);
thread->error = -RT_EINTR;
//触发调度,切换到别的线程,此时该线程已经"睡着了"
rt_schedule();
/* clear error number of this thread to RT_EOK */
if (thread->error == -RT_ETIMEOUT)
thread->error = RT_EOK;
}
else
{
rt_hw_interrupt_enable(level);
}
return err;
}
2.线程唤醒
前面说了,SysTick 是硬件定时器,系统配置好它每 1ms 触发一次中断。
所以线程延时之后,系统并没有闲着,每隔 1ms 就进入 SysTick 中断服务函数。
这个中断函数里调用了 rt_tick_increase(),它先给全局心跳 rt_tick 加一,再检查当前正在运行的线程时间片用没用完(用完了就挪到同优先级链表尾部换人)。
最后最重要的是:调用 rt_timer_check() 去检查那条定时器链表。
定时器链表是按超时时刻从小到大排好队的,所以 rt_timer_check 只需要看链表头第一个:如果第一个的超时时刻比当前 rt_tick 大,说明最早的都没到点,后面的更没到,直接退出;如果第一个到点了,就把它从链表上摘下来,调用它绑定的回调函数------睡着的线程当初挂定时器时绑定的回调是 rt_thread_timeout,参数就是线程自己。
rt_thread_timeout 做的事就是把这个线程从挂起状态摘出来,插回它对应优先级的就绪链表,状态改回就绪。
然后调用 rt_schedule() 触发一次调度。调度器看这个线程优先级够不够高,够就切换回去继续执行,不够就先跑别人,等轮到它再切。
1. SysTick 硬件中断入口
cs
/* 每次 tick 到达,CPU 自动进入这个中断服务函数 */
void SysTick_Handler(void)
{
rt_interrupt_enter(); /* 中断嵌套计数 +1 */
rt_tick_increase(); /* 核心:做三件事 */
rt_interrupt_leave(); /* 中断嵌套计数 -1(为 0 时允许任务切换)*/
}
2. rt_tick_increase ------ 心跳 + 时间片检查 + 定时器检查
cs
void rt_tick_increase(void)
{
struct rt_thread *thread;
/* 第 1 件事:心跳加一 */
++rt_tick;
/* 第 2 件事:当前运行线程的时间片检查 */
thread = rt_thread_self(); /* 取当前正在跑的线程 */
if (thread != RT_NULL)
{
if (--thread->remaining_tick == 0) /* 剩余时间片减到 0 */
{
/* 时间片用完:摘除、恢复满值、插回同优先级链表尾部 */
rt_schedule_remove_thread(thread);
thread->remaining_tick = thread->init_tick;
rt_schedule_insert_thread(thread);
/* 标记需要调度(时间片轮转)*/
rt_schedule();
}
}
/* 第 3 件事:检查定时器链表(唤醒睡着的线程)*/
rt_timer_check();
}
3. rt_timer_check ------ 检查定时器链表
cs
void rt_timer_check(void)
{
struct rt_timer *t;
while (1)
{
/* 取链表头:最早到点的定时器 */
t = rt_list_entry(rt_timer_list.next, struct rt_timer, list);
/* 链表为空,退出 */
if (&t->list == &rt_timer_list)
break;
/* 关键判断:最早的都没到点,后面的更没到,退出 */
if (rt_tick < t->timeout_tick)
break;
/* 到点了:先摘下来 */
rt_list_remove(&t->list);
/* 调用回调函数(对睡眠线程来说,就是 rt_thread_timeout(thread))*/
t->timeout_func(t->parameter);
/* 循环继续检查下一个(可能还有多个同时到点)*/
}
}
4. rt_thread_timeout ------ 唤醒线程(线程当初挂定时器时绑定的回调)
cs
static void rt_thread_timeout(void *parameter)
{
rt_thread_t thread = (rt_thread_t)parameter; /* 参数就是睡着的线程 */
/* 设置错误码:告诉线程这次是超时被唤醒的 */
thread->error = -RT_ETIMEOUT;
/* 检查线程是否还在挂起状态(防止被提前唤醒)*/
if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND)
{
/* ① 从挂起状态/等待链表摘除 */
rt_ipc_list_remove(&thread->tlist);
/* ② 插回对应优先级的就绪链表 */
rt_schedule_insert_thread(thread);
/* ③ 状态改回就绪 */
thread->stat |= RT_THREAD_STAT_READY;
/* ④ 触发调度:让调度器决定要不要立刻切回这个线程 */
rt_schedule();
}
}
5. rt_schedule ------ 调度器决定切不切
cs
void rt_schedule(void)
{
rt_uint32_t highest_priority;
rt_thread_t to_thread;
/* 关中断进入临界区 */
rt_base_t level = rt_hw_interrupt_disable();
/* 用优先级位图找到最高优先级的就绪线程 */
highest_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
/* 取该优先级链表第一个线程 */
to_thread = rt_list_entry(rt_thread_ready_table[highest_priority].next,
struct rt_thread, tlist);
/* 如果要切换的线程不是当前线程,就触发 PendSV */
if (to_thread != rt_current_thread)
{
rt_interrupt_from_thread = rt_current_thread; /* 旧线程 */
rt_interrupt_to_thread = to_thread; /* 新线程 */
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; /* 软件触发PendSV */
}
rt_hw_interrupt_enable(level);
}