Zephyr學習(四)系統時鐘

每個支持多進程(線程)的系統都會有一個滴答時鐘(系統時鐘),這個時鐘就比如系統的「心臟」,線程的休眠(延時)和時間片輪轉調度都須要用到它。html

Cortex-M系列的內核都有一個systick時鐘,這個時鐘就是設計用來支持操做系統的,是一個24位的自動重裝載向下計數器,中斷入口就位於中斷向量表裏面,定義在zephyr-zephyr-v1.13.0\arch\arm\core\cortex_m\vector_table.S:node

1 SECTION_SUBSEC_FUNC(exc_vector_table,_vector_table_section,_vector_table)
2 
3     /*
4      * setting the _very_ early boot on the main stack allows to use memset
5      * on the interrupt stack when CONFIG_INIT_STACKS is enabled before
6      * switching to the interrupt stack for the rest of the early boot
7      */
8     .word _main_stack + CONFIG_MAIN_STACK_SIZE
9 
10    .word __reset
11    .word __nmi
12
13    .word __hard_fault
14    .word __mpu_fault
15    .word __bus_fault
16    .word __usage_fault
17    .word __reserved
18    .word __reserved
19    .word __reserved
20    .word __reserved
21    .word __svc
22    .word __debug_monitor
23
24    .word __reserved
25    .word __pendsv
26#if defined(CONFIG_CORTEX_M_SYSTICK)
27    .word _timer_int_handler
28#else
29    .word __reserved
30#endif

第27行,_timer_int_handler()就是systick時鐘的中斷入口函數。app

那麼問題來了,前面的啓動過程隨筆裏並無分析到systick時鐘是什麼時候被初始化的,事實上systick也是經過設備宏定義的方式進行初始化的,定義在zephyr-zephyr-v1.13.0\drivers\timer\sys_clock_init.c:函數

SYS_DEVICE_DEFINE("sys_clock", _sys_clock_driver_init, sys_clock_device_ctrl,
        PRE_KERNEL_2, CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);

可知,系統時鐘屬於PRE_KERNEL_2類設備,同一類設備也是有分優先級的,優先級高的先初始化,初始化函數爲_sys_clock_driver_init(),定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:ui

1 int _sys_clock_driver_init(struct device *device)
2 {
3     /* enable counter, interrupt and set clock src to system clock */
4     u32_t ctrl = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk |
5             SysTick_CTRL_CLKSOURCE_Msk;
6 
7     ARG_UNUSED(device);
8 
9     /*
10     * Determine the reload value to achieve the configured tick rate.
11     */
12
13    /* systick supports 24-bit H/W counter */
14    __ASSERT(sys_clock_hw_cycles_per_tick <= (1 << 24),
15         "sys_clock_hw_cycles_per_tick too large");
16    sysTickReloadSet(sys_clock_hw_cycles_per_tick - 1);
17
18    NVIC_SetPriority(SysTick_IRQn, _IRQ_PRIO_OFFSET);
19
20    SysTick->CTRL = ctrl;
21
22    SysTick->VAL = 0; /* triggers immediate reload of count */
23
24    return 0;
25}

系統時鐘不必定要使用systick,像Nordic的SOC用的是硬件RTC做爲系統時鐘的,只是不過systick是一個通用的時鐘。this

第16行,參數sys_clock_hw_cycles_per_tick的含義是多少個systick時鐘計數產生一箇中斷,這裏CPU時鐘爲72MHz(systick時鐘源來自CPU),系統時鐘中斷週期爲10ms(100Hz,1秒產生100箇中斷),因此sys_clock_hw_cycles_per_tick = 72000000 / 100 = 720000。sysTickReloadSet()函數定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:atom

1  static ALWAYS_INLINE void sysTickReloadSet(
2      u32_t count /* count from which timer is to count down */
3      )
4  {
5      /*
6       * Write the reload value and clear the current value in preparation
7       * for enabling the timer.
8       * The countflag in the control/status register is also cleared by
9       * this operation.
10      */
11     SysTick->LOAD = count;
12     SysTick->VAL = 0; /* also clears the countflag */
13 }

第11行,設置重裝載寄存器。spa

第12行,將計數值置0,在使能systick後就會立刻觸發中斷。操作系統

回到_sys_clock_driver_init()函數,第18行,設置systick的中斷優先級,這裏_IRQ_PRIO_OFFSET的值爲1,所以systick的中斷優先級就爲1。線程

第20行,使能systick。

第22行,立刻觸發systick中斷,並自動重裝計數值。

接下來看systick中斷執行函數_timer_int_handler(),定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:

1  void _timer_int_handler(void *unused)
2  {
3      ARG_UNUSED(unused);
4  
5      sys_trace_isr_enter();
6  
7      /* accumulate total counter value */
8      clock_accumulated_count += sys_clock_hw_cycles_per_tick;
9  
10     /*
11      * one more tick has occurred -- don't need to do anything special since
12      * timer is already configured to interrupt on the following tick
13      */
14     _sys_clock_tick_announce();
15 
16     extern void _ExcExit(void);
17     _ExcExit();
18 }

第8行,累加系統啓動後經歷了多少個時鐘計數,注意這裏不是累加系統ticks的個數,由於累加時鐘計數會更加精確。

第14行,調用_sys_clock_tick_announce()函數,定義在zephyr-zephyr-v1.13.0\include\drivers\ system_timer.h:

#define _sys_clock_tick_announce() \
        _nano_sys_clock_tick_announce(_sys_idle_elapsed_ticks)

在沒有使能TICKLESS_KERNEL配置的狀況下參數_sys_idle_elapsed_ticks的值爲1,實際上調用的是_nano_sys_clock_tick_announce()函數,定義在zephyr-zephyr-v1.13.0\kernel\ sys_clock.c:

1 void _nano_sys_clock_tick_announce(s32_t ticks)
2 {
3     unsigned int  key;
4 
5     K_DEBUG("ticks: %d\n", ticks);
6 
7     /* 64-bit value, ensure atomic access with irq lock */
8     key = irq_lock();
9     _sys_clock_tick_count += ticks;
10    irq_unlock(key);
11
12    handle_timeouts(ticks);
13
14    /* time slicing is basically handled like just yet another timeout */
15    handle_time_slicing(ticks);
16}

第9行,累加系統啓動後所經歷的ticks個數。

在分析第12行的handle_timeouts()函數以前,先說一下線程加入到超時隊列的過程。線程經過調用k_sleep()等函數後,系統會將該線程加入到超時隊列裏,而後調度其餘線程。k_sleep()對應的實現函數爲_impl_k_sleep(),定義在zephyr-zephyr-v1.13.0\kernel\ sched.c:

1 void _impl_k_sleep(s32_t duration)
2 {
3     /* volatile to guarantee that irq_lock() is executed after ticks is
4      * populated
5      */
6     volatile s32_t ticks;
7     unsigned int key;
8 
9     __ASSERT(!_is_in_isr(), "");
10    __ASSERT(duration != K_FOREVER, "");
11
12    K_DEBUG("thread %p for %d ns\n", _current, duration);
13
14    /* wait of 0 ms is treated as a 'yield' */
15    if (duration == 0) {
16        k_yield();
17        return;
18    }
19
20    ticks = _TICK_ALIGN + _ms_to_ticks(duration);
21    key = irq_lock();
22
23    _remove_thread_from_ready_q(_current);
24    _add_thread_timeout(_current, NULL, ticks);
25
26    _Swap(key);
27}

第15行,若是傳進來的時參數爲0,則直接調用k_yield()函數,切換到其餘線程,具體實現的話在下一篇隨筆裏再分析。

第20行,_TICK_ALIGN的值爲1,即將睡眠時間以tick爲單位補齊。

第23行,調用_remove_thread_from_ready_q()函數,定義在zephyr-zephyr-v1.13.0\kernel\ sched.c:

1  void _remove_thread_from_ready_q(struct k_thread *thread)
2  {
3      LOCKED(&sched_lock) {
4          if (_is_thread_queued(thread)) {
5              _priq_run_remove(&_kernel.ready_q.runq, thread);
6              _mark_thread_as_not_queued(thread);
7              update_cache(thread == _current);
8          }
9      }
10 }

第4行,線程可以運行,那它的狀態必須是已經_THREAD_QUEUED了的。

第5行,將線程從運行隊列移除,那麼線程就不會參與線程調度了。

第6行,設置線程狀態不爲_THREAD_QUEUED。

第7行,調用update_cache()函數,在上一篇隨筆已經分析過了,這裏再也不重複。

回到_impl_k_sleep()函數,第24行,調用_add_thread_timeout()函數,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:

1 static inline void _add_thread_timeout(struct k_thread *thread,
2                        _wait_q_t *wait_q,
3                        s32_t timeout_in_ticks)
4 {
5     _add_timeout(thread, &thread->base.timeout, wait_q, timeout_in_ticks);
6 }

實際上調用的是_add_timeout()函數,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:

1 static inline void _add_timeout(struct k_thread *thread,
2                 struct _timeout *timeout,
3                 _wait_q_t *wait_q,
4                 s32_t timeout_in_ticks)
5 {
6     __ASSERT(timeout_in_ticks >= 0, "");
7 
8     timeout->delta_ticks_from_prev = timeout_in_ticks;
9     timeout->thread = thread;
10    timeout->wait_q = (sys_dlist_t *)wait_q;
11
12    K_DEBUG("before adding timeout %p\n", timeout);
13
14    /* If timer is submitted to expire ASAP with
15     * timeout_in_ticks (duration) as zero value,
16     * then handle timeout immedately without going
17     * through timeout queue.
18     */
19    if (!timeout_in_ticks) {
20        _handle_one_expired_timeout(timeout);
21        return;
22    }
23
24    s32_t *delta = &timeout->delta_ticks_from_prev;
25    struct _timeout *in_q;
26
27    SYS_DLIST_FOR_EACH_CONTAINER(&_timeout_q, in_q, node) {
28        if (*delta <= in_q->delta_ticks_from_prev) {
29            in_q->delta_ticks_from_prev -= *delta;
30            sys_dlist_insert_before(&_timeout_q, &in_q->node,
31                        &timeout->node);
32            goto inserted;
33        }
34
35        *delta -= in_q->delta_ticks_from_prev;
36    }
37
38    sys_dlist_append(&_timeout_q, &timeout->node);
39
40inserted:
41    K_DEBUG("after adding timeout %p\n", timeout);
42}

第19行,很明顯timeout_in_ticks的值不爲0。

第27~38行, 按delta_ticks_from_prev的值由小到大插入到_timeout_q超時隊列裏。由此可知,超時隊列裏存放的是與前一個線程的時間的差值,而不是絕對值。

回到_impl_k_sleep()函數,第26行,調用_Swap()函數,把線程切換出去,這在下一篇隨筆再分析。

好了,有了這些基礎以後,如今回到_nano_sys_clock_tick_announce()函數,第12行,調用handle_timeouts()函數,定義在zephyr-zephyr-v1.13.0\kernel\ sys_clock.c:

1  static inline void handle_timeouts(s32_t ticks)
2  {
3      sys_dlist_t expired;
4      unsigned int key;
5  
6      /* init before locking interrupts */
7      sys_dlist_init(&expired);
8  
9      key = irq_lock();
10 
11     sys_dnode_t *next = sys_dlist_peek_head(&_timeout_q);
12     struct _timeout *timeout = (struct _timeout *)next;
13 
14     K_DEBUG("head: %p, delta: %d\n",
15         timeout, timeout ? timeout->delta_ticks_from_prev : -2112);
16 
17     if (!next) {
18         irq_unlock(key);
19         return;
20     }
21 
22     /*
23      * Dequeue all expired timeouts from _timeout_q, relieving irq lock
24      * pressure between each of them, allowing handling of higher priority
25      * interrupts. We know that no new timeout will be prepended in front
26      * of a timeout which delta is 0, since timeouts of 0 ticks are
27      * prohibited.
28      */
29 
30     while (next) {
31 
32         /*
33          * In the case where ticks number is greater than the first
34          * timeout delta of the list, the lag produced by this initial
35          * difference must also be applied to others timeouts in list
36          * until it was entirely consumed.
37          */
38 
39         s32_t tmp = timeout->delta_ticks_from_prev;
40 
41         if (timeout->delta_ticks_from_prev < ticks) {
42             timeout->delta_ticks_from_prev = 0;
43         } else {
44             timeout->delta_ticks_from_prev -= ticks;
45         }
46 
47         ticks -= tmp;
48 
49         next = sys_dlist_peek_next(&_timeout_q, next);
50 
51         if (timeout->delta_ticks_from_prev == 0) {
52             sys_dnode_t *node = &timeout->node;
53 
54             sys_dlist_remove(node);
55 
56             /*
57              * Reverse the order that that were queued in the
58              * timeout_q: timeouts expiring on the same ticks are
59              * queued in the reverse order, time-wise, that they are
60              * added to shorten the amount of time with interrupts
61              * locked while walking the timeout_q. By reversing the
62              * order _again_ when building the expired queue, they
63              * end up being processed in the same order they were
64              * added, time-wise.
65              */
66 
67             sys_dlist_prepend(&expired, node);
68 
69             timeout->delta_ticks_from_prev = _EXPIRED;
70 
71         } else if (ticks <= 0) {
72             break;
73         }
74 
75         irq_unlock(key);
76         key = irq_lock();
77 
78         timeout = (struct _timeout *)next;
79     }
80 
81     irq_unlock(key);
82 
83     _handle_expired_timeouts(&expired);
84 }

代碼有點多,可是原理比較簡單。

第7行,初始化一個超時雙向鏈表,用於後面存放已經超時(到期)的線程。

第11行,取出超時隊列的頭節點。

第17行,即若是超時隊列爲空(沒有超時任務要處理),則直接返回。

第30行,遍歷超時隊列。

第41行,若是取出的線程剩餘的超時時間小於ticks(這裏是1),則說面線程到期了,第42行將線程的超時時間置爲0。不然,第44行,將超時時間減ticks。

第47行,剩下的ticks個數,其值可能爲負數。

第49行,取出下一個節點。

第51行,若是當前線程的超時時間已經到了,則if條件成立。

第54行,將當前線程從超時隊列移除。

第67行,將當前線程加入到臨時隊列裏,後面會統一處理這個隊列裏的線程。

第69行,將當前線程的超時時間置爲_EXPIRED。

如此循環,直到ticks用完(其值小於等於0),而後跳出循環,調用83行的_handle_expired_timeouts()函數,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:

1  static inline void _handle_expired_timeouts(sys_dlist_t *expired)
2  {
3      struct _timeout *timeout;
4  
5      SYS_DLIST_FOR_EACH_CONTAINER(expired, timeout, node) {
6          _handle_one_expired_timeout(timeout);
7      }
8  }

即遍歷臨時隊列,每次調用_handle_one_expired_timeout()函數,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:

1  static inline void _handle_one_expired_timeout(struct _timeout *timeout)
2  {
3      struct k_thread *thread = timeout->thread;
4      unsigned int key = irq_lock();
5  
6      timeout->delta_ticks_from_prev = _INACTIVE;
7  
8      K_DEBUG("timeout %p\n", timeout);
9      if (thread) {
10         _unpend_thread_timing_out(thread, timeout);
11         _mark_thread_as_started(thread);
12         _ready_thread(thread);
13         irq_unlock(key);
14     } else {
15         irq_unlock(key);
16         if (timeout->func) {
17             timeout->func(timeout);
18         }
19     }
20 }

第6行,將超時時間置爲_INACTIVE。

超時的方式有兩種,一是線程調用k_sleep()等函數後將本身掛起致使的超時,二是線程調用軟件定時器k_timer_start()函數致使的超時,線程自己不會掛起,只是開啓了一個定時器。因此就有了第9行和第14行兩種不一樣路徑。

先看第一種方式,第10行,調用_unpend_thread_timing_out()函數,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:

1  static inline void _unpend_thread_timing_out(struct k_thread *thread,
2                           struct _timeout *timeout_obj)
3  {
4      if (timeout_obj->wait_q) {
5          _unpend_thread_no_timeout(thread);
6          thread->base.timeout.wait_q = NULL;
7      }
8  }

第5行,調用_unpend_thread_no_timeout()函數,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:

1  void _unpend_thread_no_timeout(struct k_thread *thread)
2  {
3      LOCKED(&sched_lock) {
4          _priq_wait_remove(&pended_on(thread)->waitq, thread);
5          _mark_thread_as_not_pending(thread);
6      }
7  }

第4行,實際上調用的是_priq_dumb_remove()函數,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:

void _priq_dumb_remove(sys_dlist_t *pq, struct k_thread *thread)
{
    __ASSERT_NO_MSG(!_is_idle(thread));

    sys_dlist_remove(&thread->base.qnode_dlist);
}

將線程從隊列移除。

回到_unpend_thread_no_timeout()函數,第5行,將線程狀態設置爲不是_THREAD_PENDING。

回到_handle_one_expired_timeout()函數,第11~12行這兩個函數在上一篇隨筆裏已經分析過了。第16~17行,若是定時器超時函數不爲空,則調用定時器超時函數。

至此,handle_timeouts()函數分析完了。

回到_nano_sys_clock_tick_announce()函數,第15行,調用handle_time_slicing()函數,定義在zephyr-zephyr-v1.13.0\kernel\sys_clock.c:

1 static void handle_time_slicing(s32_t ticks)
2 {
3     if (!_is_thread_time_slicing(_current)) {
4         return;
5     }
6 
7     _time_slice_elapsed += ticks;
8     if (_time_slice_elapsed >= _time_slice_duration) {
9 
10        unsigned int key;
11
12        _time_slice_elapsed = 0;
13
14        key = irq_lock();
15        _move_thread_to_end_of_prio_q(_current);
16        irq_unlock(key);
17    }
18}

第3行,調用_is_thread_time_slicing()函數,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:

1  int _is_thread_time_slicing(struct k_thread *thread)
2  {
3      int ret = 0;
4  
5      /* Should fix API.  Doesn't make sense for non-running threads
6       * to call this
7       */
8      __ASSERT_NO_MSG(thread == _current);
9  
10     if (_time_slice_duration <= 0 || !_is_preempt(thread) ||
11         _is_prio_higher(thread->base.prio, _time_slice_prio_ceiling)) {
12         return 0;
13     }
14 
15 
16     LOCKED(&sched_lock) {
17         struct k_thread *next = _priq_run_best(&_kernel.ready_q.runq);
18 
19         if (next) {
20             ret = thread->base.prio == next->base.prio;
21         }
22     }
23 
24     return ret;
25 }

第10~13行,_time_slice_duration的值在系統啓動時就設置了。_is_preempt()函數:

static inline int _is_preempt(struct k_thread *thread)
{
    /* explanation in kernel_struct.h */
    return thread->base.preempt <= _PREEMPT_THRESHOLD;
}

_PREEMPT_THRESHOLD的值爲127。即若是線程的優先級小於128則_is_preempt()返回1。

_is_prio_higher()比較當前線程的優先級是否高於_time_slice_prio_ceiling的值(也是在系統啓動時就設置了),若是這三個條件有一個成立了,則不會處理時間片相關的內容。

第17行,調用_priq_run_best()函數取出運行隊列的頭節點,即優先級最高的線程。只有運行隊列的頭節點的優先級與當前線程的優先級相等纔會繼續往下處理。

回到handle_time_slicing()函數,第7行,累加ticks個數。

第8行,若是累加的ticks個數大於等於配置的時間片數,則if條件成立。

第12行,將累加的ticks個數清0。

第15行,調用_move_thread_to_end_of_prio_q()函數,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:

1  void _move_thread_to_end_of_prio_q(struct k_thread *thread)
2  {
3      LOCKED(&sched_lock) {
4          _priq_run_remove(&_kernel.ready_q.runq, thread);
5          _priq_run_add(&_kernel.ready_q.runq, thread);
6          _mark_thread_as_queued(thread);
7          update_cache(0);
8      }
9  }

第4~7行,這幾個函數前面都已經分析過了。

到這裏就能夠知道,要使用時間片輪轉的調度方式,須要如下設置:

1.配置時間片大小(大於0)和優先級;

2.全部建立的線程的優先級要相同,而且優先級要比1中的優先級高;

 

仔細思考會發現目前這種超時處理機制對延時(休眠)的時間是不許確的,所以這種機制老是以tick爲單位進行延時(休眠),也即時間只能精確到tick。那有沒有其餘方法能夠準確延時(休眠)呢?確定是有的,就是須要打開TICKLESS_KERNEL配置,其原理就是不以tick(假如10ms)爲固定時間進行定時,而是每次根據須要延時(休眠)的最小時間進行定時,這樣就能實現精確的延時(休眠),zephyr是支持這種精肯定時方式的,感興趣的能夠去研究研究。

相關文章
相關標籤/搜索