使用STM32编写一个简单的RTOS:4.时钟管理(二)定时器


参考资料: RTT官网文档
关键字:分析RT-Thread源码、stm32、RTOS、定时器timer。

问题及总结

一、为什么定时器定时不支持超过RT_TICK_MAX / 2(RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2);)?

这个问题其实就跟我们现实中的时钟一样。这个问题分为过了12点(归0)和没过12点。

1)没过12点:
在这里插入图片描述
已知 a + b + c = max;因为定时不超过RT_TICK_MAX / 2,即b < RT_TICK_MAX / 2。
所以a + c 就会大于RT_TICK_MAX / 2,即cur_tick - timeout_tick 在cur_tick小于timeout时,cur_tick - timeout_tick = a + c > RT_TICK_MAX / 2。

2)超过12点:
在这里插入图片描述
因为定时init_tick不超过RT_TICK_MAX / 2,所以a + c < RT_TICK_MAX / 2,
所以b > RT_TICK_MAX / 2,即cur_tick - timeout_tick = b > RT_TICK_MAX / 2。

所以我们才可以用(t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2来判断是否到达超时时间。

二、定时器跳表总结
在这里插入图片描述
加入插入一个400tick的定时器时start的流程。这里就没有话引索的建立了(额,灵魂画手。。)
1:start的第一个for循环,用来遍历各级引索
2:第二个for循环,遍历该级引索链表
345:这行代码的意思:row_head[row_lvl + 1] = row_head[row_lvl] + 1;
总结:这种方法实现跳表比较简洁易懂,不过多少级引索每个定时器就要包含多少个节点,比较浪费空间。目前还不清楚这个建立引索的算法是否高效。

跳表

背景介绍(可以忽略)
有了时钟节拍之后,我们就可以利用它来完成一些和时间相关功能,如定时器。我们只要记录下调用定时时的时钟节拍tick_start,和需要定时多久的时钟节拍tick_count,然后我们在在时钟节拍的处理里面比较当前tick_current, 只要检测到tick_cuurent >= tick_start +tick_count,就可以调用超时处理函数了。操作系统中会广泛的使用定时器,所以我们不可能只维护一个,这个时候就需要用链表把它们串起来,只要我们按超时时间顺序链起来,从头遍历比较就可以实现管理多个定时任务了。
好了,既然链表得顺序排序,那我们插入链表的时候就得顺序插入。即便对于已经按顺序排序的链表,我们也得从头开始一个个遍历,如果链表比较长,而我们要插入的节点在比较靠后的位置上,那么我们就要花费很大时间才能插入这个节点了,有什么办法可以解决这个问题吗?
二分法?可惜链表不能跟数组一样可以随机访问。哈希/HASH?消耗较大。看过RTT文档你应该知道是跳表了。跳表。也许你可能没听说过,大部分数据结构和算法的书籍好像也都不怎么讲关于跳表。但它是个好东西。

跳表
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。

在这里插入图片描述
例如在原有链表上,我们要找39,需要从3一个个遍历到39,需要3->18->39就可以找到39了,
跳表就是利用引索来快速遍历的,时间复杂度为O(log n),相当于二分查找。可以看出跳表是用空间来换时间的。
跳表的难点在于其是动态的,需要动态的建立多级引索,每次插入一个节点都要考虑是否需要重新建立引索。如何建立引索又是一个难题,建立不好,不仅浪费空间,也会降低搜索的时间,极端情况还可能退化到单链表,所以需要考虑如何才能适当的建立引索,才有高效的搜索。

这里推荐一下王争老师的数据结构与算法之美,讲的不错,也不太枯燥。
跳表一节可以试读https://time.geekbang.org/column/article/42896

源码分析

先来看一下定时器的结构体成员组成。

/**
 * timer structure
 */
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);              /**< timeout function */
    void            *parameter;                         /**< timeout function's parameter */

    rt_tick_t        init_tick;                         /**< timer timeout tick */
    rt_tick_t        timeout_tick;                      /**< timeout tick */
};

成员: struct rt_object parent
parent是用来“继承”基本的内核对象的,加入对象容器队列中,形成一条定时器链表。
parent.flag这里是用来记录这个定时器的状态。flag的状态有以下几种:
RT_TIMER_FLAG_DEACTIVATED(无效的),RT_TIMER_FLAG_ACTIVATED(激活的),
RT_TIMER_FLAG_ONE_SHOT(单次的),RT_TIMER_FLAG_PERIODIC(周期性的),
RT_TIMER_FLAG_HARD_TIMER(硬定时), RT_TIMER_FLAG_SOFT_TIMER(软定时)

成员: rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
这是一个链表数组,用来记录跳表的引索,下标是引索的级数。默认是1。

成员: void (*timeout_func)(void *parameter);
超时回调函数,当定时时间到了的时候,将会调用这个超时函数。

成员:void *parameter;
超时函数的参数,类型时void *。

成员:init_tick
是用来记录超时间隔的,在启动定时器后的init_tick时间后,将触发超时。

成员:timeou_tick
用来记录超时时的tick。

了解了定时器的结构体后,现在我们来看一下定时器相关的API。以介绍硬定时器为例。

void rt_system_timer_init(void)
{
    int i;

    for (i = 0; i < sizeof(rt_timer_list) / sizeof(rt_timer_list[0]); i++)
    {
        rt_list_init(rt_timer_list + i);
    }
}

Alt
在rt_system_timer_init中初始化了rt_timer_list,默认只有一个。

接着看一下初始化定时器:

void rt_timer_init(rt_timer_t  timer,
                   const char *name,
                   void (*timeout)(void *parameter),
                   void       *parameter,
                   rt_tick_t   time,
                   rt_uint8_t  flag)
{
    /* timer check */
    RT_ASSERT(timer != RT_NULL);

    /* timer object initialization */
    rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);

    _rt_timer_init(timer, timeout, parameter, time, flag);
}

在这里插入图片描述
第2行代码rt_object_init将timer初始化为内核的定时器对象,并插入定时器对象链表中。
第3行,真正初始化定时器的地方。

static void _rt_timer_init(rt_timer_t timer,
                           void (*timeout)(void *parameter),
                           void      *parameter,
                           rt_tick_t  time,
                           rt_uint8_t flag)
{
    int i;

    /* set flag */
    timer->parent.flag  = flag;	//设置标识位,单次或周期性,硬定时或软定时

    /* set deactivated */
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;	//初始化时先设置为无效定时器。

    timer->timeout_func = timeout;	//设置超时函数
    timer->parameter    = parameter;	//设置超时函数参数

    timer->timeout_tick = 0;	//超时tick初始值为0
    timer->init_tick    = time;	//超时间隔

    /* initialize timer list */
    for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
    {
        rt_list_init(&(timer->row[i]));	//初始化引索链表,i为引索级别
    }
}

初始化定时器后,就可以启动启动器了。
下面就是重点了,在启动中插入链表和建立引索。
这个start函数有点长,这里以硬定时为例。

rt_err_t rt_timer_start(rt_timer_t timer)
{
    unsigned int row_lvl;
    rt_list_t *timer_list;
    register rt_base_t level;
    rt_list_t *row_head[RT_TIMER_SKIP_LIST_LEVEL];
    unsigned int tst_nr;
    static unsigned int random_nr;

 
    /* remove timer from list */
    _rt_timer_remove(timer);	//防止重复插入链表
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;	//将定时器标识为无效

    /*
     * get timeout tick,
     * the max timeout tick shall not great than RT_TICK_MAX/2
     */
    RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2);	//init_tick必须小于RT_TICK_MAX / 2。
    timer->timeout_tick = rt_tick_get() + timer->init_tick;	//计算超时时间
    {
        /* insert timer to system timer list */
        timer_list = rt_timer_list;	//将timer_list指向rt_timer_list
    }

    row_head[0]  = &timer_list[0];	//将row_head[0]指向rt_timer_list

   //遍历各级跳表,这里的level默认为1,这个for就相当于没有。
    for (row_lvl = 0; row_lvl < RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
//遍历当前级别(row_lvl)跳表,第一次没有定时器,不会进入。
//注意这里for的语句1为空,row_head是从上一级跳下来的,而不从第一个节点开始遍历
//如果不是空或者最后一个的则遍历,即当找到不到比他大的时候已指向最后一个
        for (; row_head[row_lvl] != timer_list[row_lvl].prev;
             row_head[row_lvl]  = row_head[row_lvl]->next)
        {
            struct rt_timer *t;
            rt_list_t *p = row_head[row_lvl]->next;	//这里指向的是下一个定时器的row而不是当前的。

            /* fix up the entry pointer */
//通过成员地址获取结构体地址,rt_list_entry比较简单,就不介绍了
            t = rt_list_entry(p, struct rt_timer, row[row_lvl]);	//t指向了p的定时器地址

            /* If we have two timers that timeout at the same time, it's
             * preferred that the timer inserted early get called early.
             * So insert the new timer to the end the the some-timeout timer
             * list.
             */
            if ((t->timeout_tick - timer->timeout_tick) == 0)	//如果超时时间一样。下一轮则插入其后面
            {
                continue;
            }
//如果要插入的超时时间小于遍历的这个定时器的超时时间,即找到位置,则break。
            else if ((t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2)	
            {
                break;
            }
        }
//把row_head[row_lvl + 1]指向了上一级找到的位置的下一个row
//如果不是后面一级,则进入下一级引索继续遍历,这里的+1等于sizeof(row_head[0]),即将row_head[row_lvl +1]指向了该定时器的row[row_lvl+1]。
        if (row_lvl != RT_TIMER_SKIP_LIST_LEVEL - 1)
            row_head[row_lvl + 1] = row_head[row_lvl] + 1;
    }

    /* Interestingly, this super simple timer insert counter works very very
     * well on distributing the list height uniformly. By means of "very very
     * well", I mean it beats the randomness of timer->timeout_tick very easily
     * (actually, the timeout_tick is not random and easy to be attacked). */
    random_nr++;	//这里将插入的定时器次数作为随机值,为了让跳表引索分布均匀
    tst_nr = random_nr;

//将定时器插入到最后一级的跳表链表中,即RT_TIMER_SKIP_LIST_LEVEL - 1级,所有的都会插入最后一级链表中
    rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - 1],
                         &(timer->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));

//建立跳表引索,RT_TIMER_SKIP_LIST_LEVEL应该要大于2的
    for (row_lvl = 2; row_lvl <= RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
//判断低两位是否为0,是则插入当前引索链表中,越上层的引索应该插入的越少。
        if (!(tst_nr & RT_TIMER_SKIP_LIST_MASK))
            rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - row_lvl],
                                 &(timer->row[RT_TIMER_SKIP_LIST_LEVEL - row_lvl]));
        else
            break;
        /* Shift over the bits we have tested. Works well with 1 bit and 2
         * bits. */
//将tst_nr左移2位
        tst_nr >>= (RT_TIMER_SKIP_LIST_MASK + 1) >> 1;
    }
//置为活跃状态
    timer->parent.flag |= RT_TIMER_FLAG_ACTIVATED;

    return -RT_EOK;
}

接着看一下stop函数。

rt_err_t rt_timer_stop(rt_timer_t timer)
{
    register rt_base_t level;

    /* timer check */
    RT_ASSERT(timer != RT_NULL);
//如果不是活跃状态
    if (!(timer->parent.flag & RT_TIMER_FLAG_ACTIVATED))
        return -RT_ERROR;

    RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(timer->parent)));

    /* disable interrupt */
    level = rt_hw_interrupt_disable();
//从链表中移除
    _rt_timer_remove(timer);

    /* enable interrupt */
    rt_hw_interrupt_enable(level);
//修改状态为无效
    /* change stat */
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;

    return RT_EOK;
}

stop中将定时器从链表中移除,并将状态设置为无效状态。

现在看一下是怎么检测定时器是否到达定时时间的。rt_timer_check放在系统节拍的中断上,也就是每个系统节拍都会检查一下是否有定时器到达超时时间。

void rt_timer_check(void)
{
    struct rt_timer *t;
    rt_tick_t current_tick;
    register rt_base_t level;

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check enter\n"));

//获取当前tick
    current_tick = rt_tick_get();

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

//判断定时器最低层(已排序好)的第一个定时器是否超时。
    while (!rt_list_isempty(&rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
    {
        t = rt_list_entry(rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
                          struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);

        /*
         * It supposes that the new tick shall less than the half duration of
         * tick max.
         */
//如果到达超时时间
        if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
        {
            RT_OBJECT_HOOK_CALL(rt_timer_timeout_hook, (t));
//从链表中移除该定时器
            /* remove timer from timer list firstly */
            _rt_timer_remove(t);
//调用超时函数,并把parameter当做参数传入
            /* call timeout function */
            t->timeout_func(t->parameter);

            /* re-get tick */
            current_tick = rt_tick_get();

            RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));
//判断是否是周期性的以及是否还是活跃状态
            if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&
                (t->parent.flag & RT_TIMER_FLAG_ACTIVATED))
            {
                /* start it */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
                rt_timer_start(t);	//周期性的话就重新插入跳表中
            }
            else
            {
                /* stop timer */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
            }
        }
        else
            break;
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check leave\n"));
}

rt_timer_check中找到超时的定时器,并调用超时函数。

有了定时器,现在实现线程的延时/睡眠就轻松多了。线程管理那一篇已经介绍过rt_thread_delay了,
现在不用看代码我们也能知道是怎么实现delay的了。首先delay的时候需要将线程挂起suspend,然后根据要delay的时间,start启动定时器,然后timeout超时函数里则是将线程唤醒resume就绪态。

关于rtt的定时器就分析到这里,总结及问题就放在最前面了。

测试

下面是RTT-MINI的定时器测试,这里没有用跳表,代码见链接。
Alt
在这里插入图片描述
发现了两个bug,thread_exit里面没有调用schedule,导致推出时跑死。schedule_ready_del_thread里面没有判断同优先级下是否还有其它线程。
在这里插入图片描述
在这里插入图片描述
测试代码链接:https://download.csdn.net/download/u012220052/11236208

好的,以下是使用C语言编写STM32定时器程序的基本步骤: 1. 首先,需要在代码中包含适当的头文件,例如stm32f10x.h等。 2. 接下来,需要初始化定时器。这可以通过配置相应的寄存器来完成,如TIM_CR1、TIM_CR2等。在初始化定时器时,需要设置计数器的时钟源、预分频器值、计数模式等。 3. 然后,可以设置定时器的中断,以在计数器溢出时触发中断。可以通过TIM_ITConfig函数来实现。 4. 接下来,需要设置定时器的计数值。这可以通过设置TIM_Period寄存器来完成。计数值越大,定时器的时间周期就越长。 5. 最后,需要启动定时器。可以通过TIM_Cmd函数来启动定时器。 下面是一个简单的例子,演示如何使用C语言编写STM32定时器程序: ``` #include "stm32f10x.h" void TIM2_IRQHandler(void) { // 处理定时器中断 // ... TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } int main(void) { // 初始化定时器 TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStruct.TIM_Prescaler = 7200 - 1; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_InitStruct.TIM_Period = 1000 - 1; TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_InitStruct); // 设置定时器中断 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 启动定时器 TIM_Cmd(TIM2, ENABLE); while (1) { // 主循环代码 // ... } } ``` 这个例子使用STM32定时器2,设置了预分频器值为7200-1,计数模式为向上计数,计数值为1000-1,时钟分频因子为TIM_CKD_DIV1。在定时器中断处理函数中,可以添加需要执行的代码。主循环中的代码将会一直执行,直到程序结束。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值