STM32:rtthread_"rt_timer"定时器

发布时间 2023-06-19 13:35:13作者: caesura_k

1 定时器

  轮询系统和前后台系统中的延时为直接阻塞延时,让函数一直等着直到延时够了再继续执行;

  大概rtthread觉得直接阻塞延时效率不够高,逻辑不够优美;所以它给每个thread都配置了一个rt_timer类型的thread_timer定时器;

  所有定时器由定时器链表统一管理,通过对thread_timer定时器统一管理多个线程的延时,效率高,逻辑优美;

  1.1 定时器结构体

//rtdef.h
#define RT_TIMER_FLAG_DEACTIVATED       0x0     /* 定时器没有激活 */
#define RT_TIMER_FLAG_ACTIVATED         0x1     /* 定时器已经激活 */
#define RT_TIMER_FLAG_ONE_SHOT          0x0     /* 单次定时 */
#define RT_TIMER_FLAG_PERIODIC          0x2     /* 周期定时 */
#define RT_TIMER_FLAG_HARD_TIMER        0x0     /* 硬件定时器,定时器回调函数在 tick isr中调用 */
#define RT_TIMER_FLAG_SOFT_TIMER        0x4     /* 软件定时器,定时器回调函数在定时器线程中调用 */
struct rt_object{
    char       name[RT_NAME_MAX];                       
    rt_uint8_t type;                                    
    rt_uint8_t flag;                            //上面define
    rt_list_t  list;                                    
};
struct rt_timer
    struct rt_object parent;                         //还搞了个继承
    rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];         //挂载到rt_timer_list链表
    void (*timeout_func)(void *parameter);           //超时函数地址,将线程重新挂载到优先级表、组
    void            *parameter;                      //所在thread的结构体地址 
    rt_tick_t        init_tick;                      //延时时间
    rt_tick_t        timeout_tick;                   //延时时间 + rt_tick全局变量数值
};
typedef struct rt_timer *rt_timer_t;

    1.1.1 定时器链表

      定时器开始:将所在延时线程的优先级从优先级组和表中移除,然后把自己挂载到定时器链表上;

      定时器关闭:将所在延时线程的优先级挂回优先级组和表中,然后把自己从定时器链表上移除;

//timer.c
static rt_list_t rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL];

  1.2 定时器初始化

    定时器结构体也属于线程结构体的一部分,所以对线程结构体进行初始化的时候,也初始化了定时器结构体;

    每个线程结构体都有一个自己的定时器结构体,在对thread结构体初始化的时候也初始化了定时器控制块;

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)
{
    rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);//初始化对象变量,并将其节点list插入对应容器中;
    _rt_timer_init(timer, timeout, parameter, time, flag);          //初始化剩下部分的定时器参数
}

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;
    timer->parent.flag  = flag;
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
    timer->timeout_func = timeout;
    timer->parameter    = parameter;
    timer->timeout_tick = 0;
    timer->init_tick    = time;
    //定时器节点初始化成自身,用来在需要延时的时候将自己挂载到定时器链表上;
    for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
    {
        rt_list_init(&(timer->row[i]));
    }
}

   1.3 定时器启动

    定时器在线程结构体初始化的时候也一并初始化了,那么定时器又是在什么时候启动的呢?定时器是在线程函数需要延时的时候启动的;

    当线程需要延时的时候,就会将 &thread->tlist 从优先级表和优先级组中移除,把自己suspend挂起之后线程不会通过调度而被执行;修改 thread->stat;

    设置timer->timeout_tick数值,然后将 &timer->row[0]插入定时器链表rt_timer_list[0]之后,执行rt_schedule()调度;

    因为线程把自己从优先级表和组中悬起了,所以rt_schedule()调度会去执行其他线程的内容;直到通过超时函数重新挂回优先级表和组后才可以调度执行;

//当线程需要延时的时候调用;作用是把线程优先级挂起,开启定时器
//挂起的意思是将节点从所在链表中移除,并且将节点初始化为自身地址;
rt_err_t rt_thread_sleep(rt_tick_t tick)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    temp = rt_hw_interrupt_disable();
	
    thread = rt_current_thread;
    rt_thread_suspend(thread);                   //把thread的优先级表和组都挂起;这里面还有个rt_timer_stop(),把定时器链表也挂起了;
    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();                               //当前线程优先级在前面被挂起了,调度执行其他函数去;

    return RT_EOK;
}
//static   rt_list_t   rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL]; 定时器链表;
//这个if else蛮新颖的,乍一看匪夷所思,多看几遍构思很巧;
//如果前面的数都要小于,那么找到一个大于的,然后插入到这个大于的数前面就可以了;
rt_err_t rt_timer_start(rt_timer_t timer)
{
    unsigned int row_lvl = 0;
    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;

    //前面rt_thread_suspend中设置过了,这里又设置了一遍;
    level = rt_hw_interrupt_disable();
    _rt_timer_remove(timer);
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
    rt_hw_interrupt_enable(level);

    //rt_thread规定 timeout tick < (RT_TICK_MAX/2);这样的话比较的时候大于部分就作为负数的掩码使用;
    timer->timeout_tick = rt_tick_get() + timer->init_tick;

    level = rt_hw_interrupt_disable();
    timer_list = rt_timer_list;
    row_head[0]  = &timer_list[0];
    //这个最外层的for循环放在这里并没有实质性作用,它应该只是野火没删掉而已;
    for (row_lvl = 0; row_lvl < RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
        //第一次插入定时器时,自身地址就是.prev地址,不执行该for循环;timer_list[row_lvl].prev为链表尾巴地址;
        //第二次插入定时器开始,才满足下面for循环条件,执行for循环;
        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;//第一个插入的定时器节点地址;

            //找出 定时器链表中第一个链表节点所在的定时器结构体首地址;
			//#define rt_list_entry(ptr, type, member)    (type *) { (char *)ptr - (unsigned long)[&(type *0)->member] }
            t = rt_list_entry(p, struct rt_timer, row[row_lvl]);     
            
            //相等的话也继续,直到下一个定时器是大于当前定时器的,然后插入到它前面的后面;
            if ((t->timeout_tick - timer->timeout_tick) == 0)
                continue;
            //下面判断等价于(定时器t->timeout_tick > 待插入timer->timeout_tick)
            //row_head[row_lvl]->next的时间大于timer->timeout_tick,那么row_head[row_lvl]节点的时间小于timer->timeout_tick
            //break之后,把timer插入到row_head[row_lvl]之后;
            else if ((t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2)
                break;
            //如果timer->timeout_tick比较大,前面的数都符合小于但是后面的数不一定符合全部大于,这里的问题在于负数掩码是极大值;
        }
        //始终不执行,也不知道干啥用,不管了;
        if (row_lvl != RT_TIMER_SKIP_LIST_LEVEL - 1)
           row_head[row_lvl + 1] = row_head[row_lvl] + 1;            
    }

    random_nr++;               //静态变量,用于记录启动了多少定时器;
    tst_nr = random_nr;

    rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - 1], &(timer->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
    
    //这个for不会执行,为什么不会执行的代码还要放着,想删掉又担心后面用得到;
    for (row_lvl = 2; row_lvl <= RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
    {
        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;
        tst_nr >>= (RT_TIMER_SKIP_LIST_MASK + 1) >> 1;
    }

    timer->parent.flag |= RT_TIMER_FLAG_ACTIVATED;
    rt_hw_interrupt_enable(level);

    return -RT_EOK;
}

  1.4 定时器扫描

//因为定时器链表是按延时时间升序排列的,所以第一个定时器时间没到,后面的定时器也没到;
void rt_timer_check(void)
{
    struct rt_timer *t;
    rt_tick_t current_tick;
    register rt_base_t level;
    current_tick = rt_tick_get();

    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]);          
        if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
		{
            _rt_timer_remove(t);
            t->timeout_func(t->parameter);      //这个是调用超时函数的方式,第一次见;

            current_tick = rt_tick_get();       //这个函数是多余的,应该可以注释掉;没测试过就先放着;

            if ( (t->parent.flag & RT_TIMER_FLAG_PERIODIC) && (t->parent.flag & RT_TIMER_FLAG_ACTIVATED) ){
				//如果线程定时器是周期循环,则清除状态位之后、重新启动定时器;
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
                rt_timer_start(t);
            }
            else
				//如果线程定时器只是定时一次,则清除状态位之后、不重新启动定时器;
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
        }
        else
            break; //如果第一个定时器时间没到,那就退出扫描;
    }
    rt_hw_interrupt_enable(level);
}

    1.4.1 超时函数

      struct rt_timer中存放了超时函数的地址,以及将所在struct rt_thread的首地址作为参数传递给超时函数;

      当定时器超时之后,就会调用超时函数,在超时函数中将定时器所在线程重新挂回优先级组和优先级表中;

void rt_thread_timeout(void *parameter)
{
    struct rt_thread *thread;
    thread = (struct rt_thread *)parameter;

    thread->error = -RT_ETIMEOUT;         //负号的功能类似return -1;
    rt_list_remove(&(thread->tlist));     //插入优先级表之前把自身节点初始化,感觉没必要,保留意见;
    rt_schedule_insert_thread(thread);    //将函数重新插入优先级表和优先级组,stat改成ready;
    rt_schedule();                        //找出优先级组中年优先级最高的线程,然后切换到该线程;
}

2 systick中断函数

//在main函数中调用SysTick_Config()函数使能systick定时器;系统晶振25MHz,RT_TICK_PER_SECOND分频100;tick定时10ms;
    SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND );

//在main.c中重写systick中断函数,虽然建议中断函数放在stm32f10x_it.c中写,但实际上功能少的时候大家都放在main.c中写;
void SysTick_Handler(void){
    rt_interrupt_enter();
    rt_tick_increase();
    rt_interrupt_leave();
}

//clock.c
void rt_tick_increase(void)
{
    ++ rt_tick;//systick的全局变量;
    rt_timer_check();
}
SysTick_Config( )
 /***core_cm3.h 
    systick不仅在core_cm3.h中提供了使用函数,还在misc.c中提供了使用函数;
    正常外设不仅需要使能自身的中断,还要使能nvic中断才能进入中断函数;
    systick只要使能自身中断就可以使用中断函数了,但是呢它又配置了nvic优先级。。。
    总之这个内核的SysTick_Config函数搭配SystemCoreClock宏还挺好用的,以后要是用到了就用它了;
***/
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                               
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}

3 时间片

  线程优先级表数组有32个对象,每个对象依次排序可区分32个优先级;

  每个优先级都是一个链表的head节点,相同优先级的线程存储在相同优先级链表下;

  rtos中高优先级的线程可以将低优先级的线程悬起,然后让芯片执行高优先级的线程;那么对于优先级相同的线程rtos又该如何调度执行呢?

  当线程的优先级相同时,就为每个相同优先级的线程配置一个时间片,每个线程都依次执行时间片长度的时间;为了确保效率需要合理安排每个线程的时间片长度;

  相同优先级下,一个线程执行完自己的时间片之后,就将处理器交由下一个线程执行完下一个线程的时间片;

  目的是确保相同优先级下的线程都能够合理均匀地分配到处理器资源;

  3.1 定义

//在rt_thread结构体中定义的时间片参数;
struct rt_thread
{   //...
    rt_ubase_t  init_tick;            /* 初始时间片 */
    rt_ubase_t  remaining_tick;       /* 剩余时间片 */ 
    //...   
};
typedef struct rt_thread *rt_thread_t;

  3.2 初始化

//在rt_thread_init()中线程初始化的时候,对线程的两个时间片参数初始化;
rt_err_t rt_thread_init(/*...其他参数*/ ,rt_uint32_t tick)
{
    /*...其他参数初始化*/
    thread->init_tick      = tick;
    thread->remaining_tick = tick;
}

  3.3 使用函数

//添加时间片功能,只需要在定时器的基础上添加部分时间片函数代码就可以了,内容也不多,主要就两个函数;
//main.c下的中断函数,不用修改;
void SysTick_Handler(void){
    rt_interrupt_enter();
    rt_tick_increase();
    rt_interrupt_leave();
}

void rt_tick_increase(void)
{
	++ rt_tick;
	
    struct rt_thread *thread;    
    thread = rt_thread_self();
    -- thread->remaining_tick;    //数的自减减可以这么写;
    //当前线程的时间片用完之后,就让出处理器,调度高优先级的线程运行;
    if (thread->remaining_tick == 0)
    {
        thread->remaining_tick = thread->init_tick;
        rt_thread_yield();
    }
	
    rt_timer_check();
}

/**
 * 该函数将让当前线程让出处理器,调度器选择最高优先级的线程运行。当前让出处理器之后,
 * 当前线程还是在就绪态。
 */
rt_err_t rt_thread_yield(void)
{
    register rt_base_t level;
    struct rt_thread *thread;

    level = rt_hw_interrupt_disable();
    thread = rt_current_thread;
    
    //tlist.next和tlist.prev里放的是相同优先级下链表的地址;
    if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_READY && thread->tlist.next != thread->tlist.prev)
    {
        rt_list_remove(&(thread->tlist));
        rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]), &(thread->tlist));
        rt_schedule();
		
		rt_hw_interrupt_enable(level);
        return RT_EOK;
    }
	
    rt_hw_interrupt_enable(level);
    return RT_EOK;
}

4 小结

  主要在于rt_timer_start函数的逻辑,写的蛮好;

  如果结构体成员是函数指针,那么它是怎么通过结构体成员调用函数指针的?

   那个systick_config()函数搭配SystemCoreClock使用,systick使能中断函数不需要配置nvic中断,但是又需要配置nvic中断优先级;