RTThread学习记录8——定时器tick相关,解密delay原理

一、前言

最近研究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);
}
相关推荐
XH华1 小时前
Linux系统第四章:工具的学习
linux·运维·学习
别动我齐刘海1 小时前
“三层同步审计”判定掉帧缺失
c语言·c++·人工智能·深度学习·学习·机器学习·机器人
fanged2 小时前
设备树学习5--读写实操(TODO)
学习
天天爱吃肉82182 小时前
行业笔记|高压连接器瞬态接触退化通用检测方法论
人工智能·笔记·python·功能测试·学习·汽车
Hhy_11072 小时前
【从零开始学数据结构 ⑥】:二叉树之堆——打破规则的优先队列
c语言·开发语言·数据结构·学习·visual studio
weixin_431600442 小时前
为什么 Agent REPL 要上 Ink:好处、用法与内部设计
前端·学习·ai·agent·ai编程
wangjialelele3 小时前
Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)
java·spring boot·笔记·学习·spring·spring cloud·gateway
qeen873 小时前
【数据结构】搜索二叉树的介绍与算法原理解析及实现
数据结构·c++·学习·算法·模板
重庆小透明3 小时前
agent开发学习【第一篇】
学习