Zephyr Ticker 介绍

Ticker 是zephyr bluetooth controller中的一个核心模块,负责仲裁Link Layer中各个procedure对RF资源的申请,用定义好的Rule来保证这些procedure都能被童叟无欺的执行到。
但实际上这个module是个完全独立的module,跟蓝牙的行为没有什么关系,也可以拿来做他用。
在Ticker里有三大角色:

  • Node:是Ticker的调度单元,里面的关键信息是到期时间(ticks_to_expire)及申请占用RF的时间(ticks_slot)。
  • User:是Node的用户,总共有4个,从字面意思看是跟优先级相关,但实际上没有优先级的概念。

    1
    2
    3
    4
    #define TICKER_USER_ID_LLL           MAYFLY_CALL_ID_0 
    #define TICKER_USER_ID_ULL_HIGH MAYFLY_CALL_ID_1
    #define TICKER_USER_ID_ULL_LOW  MAYFLY_CALL_ID_2  
    #define TICKER_USER_ID_THREAD   MAYFLY_CALL_ID_PROGRAM
  • User Operation:User希望要做的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define TICKER_USER_OP_TYPE_NONE         0
    #define TICKER_USER_OP_TYPE_IDLE_GET     1
    #define TICKER_USER_OP_TYPE_SLOT_GET     2
    #define TICKER_USER_OP_TYPE_PRIORITY_SET 3
    #define TICKER_USER_OP_TYPE_START        4
    #define TICKER_USER_OP_TYPE_UPDATE       5
    #define TICKER_USER_OP_TYPE_YIELD_ABS    6
    #define TICKER_USER_OP_TYPE_STOP         7
    #define TICKER_USER_OP_TYPE_STOP_ABS     8

这三者的关系,我们用下面的图形来举例:

  • 带箭头的蓝色横线是一个时间线,展现已经处于scheduled状态的所有节点的先后关系,这些节点等着他们的expiry时间到后,就可以执行他们的expiry callback函数,完成一个轮回。
  • ticks_current是所有还未到期的节点的一绝对参考时间,第一个节点的expiry时间expiry1是相对于ticks_current,后面每个节点的expiry时间都是相对于前一个节点的expiry时间。
  • 地标所指处为当前时间,可见当前的节点都还未过期。
  • 蓝色Node #4 为待插入的节点,看起来他的slot有跟节点 #2 冲突。虽然#4 的ticks_slot有跟#2 的expiry时间冲突,但还是会插入成功。在#4 的expiry时间到期的时候才会做冲突处理,确定#2和#4 到底谁优先。

在理解Ticker所有时间相关的变量的时候,一定要注意该时间是绝对时间还是相对时间,如果是相对时间,是相对于谁,只要搞清楚这个,基本上能理解整个流程。

ticker

  • 所有Node放在一个大数组里,由两个指针ticker_id_head 和insert_head来管理他们之间的关系。
    • ticker_id_head 把所有可以被scheduled的Node(橘色)都串起来,这个list可以叫做scheduled list,第一个节点的expiry时间会送到RTC timer里,timer到期后就开始执行这个节点的callback。
    • insert_head把所有待定的要进入sheduled list的节点(蓝色)串起来。这些节点会被 ticker_job_list_insert() 逐个插入到scheduled list,交由thread_id_head统领。

两个关键函数 相互配合完成调度

  • ticker_job() 的目的是把所有希望得到调度的节点都欢送到Scheduled list里,它的操作对象有三个:
    • ticker_job_list_manage() 处理所有User的所有的OP中的Update 和 Stop 操作.
    • ticker_job_worker_bh() 处理Scheduled list中所有的过期节点,这些节点可能还需要再进入Scheduled list中, ticker_work()不会判定已经expiry 的节点是否要再次进入Scheduled list中,是交由该函数处理。
    • ticker_job_list_insert()函数处理所有User的所有的operation中的START操作以及 Insert_head中的节点,这些节点是由前两步的操作带入的。
    • 如果ticker_id_head有变,就重新把新的expiry时间设置到RTC。
flowchart TD
    A[start] -->B(ticker_job_list_manage)
    B --> C{flag_elapsed}
    C -->|Yes| D[ticker_job_worker_bh]
    D --> E[ticker_job_list_insert]
    C --> |No| E
    E --> F{ticker_id_head changes?}
    F --> |Yes| G[ticker_job_compare_update]
    F --> |No| H
    G --> H[Schedule job_worker]
    H --> I[End]
  • ticker_worker() 目的是执行ticker_id_head 指向的节点的expiry callback,至于该Node要不要再次进入scheduled List,这里不管。在Node 插入schedule List的时候并不会针对ticks_slot做冲突检测,这样的话必须在这个函数里执行的时候做冲突检测,会影响执行效能。
    graph TD
    A[Start] --> B(Calculate ticks_elapsed)
    B --> C{NextNode?}
    C -->|Yes| D{Expiry?}
    C -->|No| Job
    D -->|Yes| State{state == 0}
    D -->|No| Job
    State -->|Yes| Collide[ticker_resolve_collision]
    State -->|No| C
    Collide -->IsWin[Win?]
    IsWin -->|Yes| Callback[timeout_callback]
    IsWin -->|No| Skip_check[Skip?]
    Skip_check -->|Yes|C
    Skip_check -->|No|SetAsShallowExpiry
    SetAsShallowExpiry -->Callback
    Callback --> Job
    Job[Schedule a ticker_job]
    Job --> End
    End[End]

    为了解决在执行callback的时候才做冲突出来带来的效率问题,有了另外一个版本(
    CONFIG_BT_TICKER_LOW_LAT)的ticker_job_list_insert() 函数,该)函数会将insert_head里所有的节点尝试加入ticker_id_head,在加入的过程中,会有一些冲突处理机制如下图:
  • 特别注意Node #5 是没有slot的,它不占用RF,即使它的expiry时间被 #7 overlap到,但它跟#7并不冲突。

ticker_job_enqueue() 中的冲突处理:

job_enqueue

Ticker中各个Compile option的作用:

  • CONFIG_BT_TICKER_EXT:多了一个 ticks_slot_window,Adv对expiry的时间不需要严格精确,那么就把它的expiry时间前后稍微挪动下,如果能避免冲突,那它就能得到更早的执行。
  • CONFIG_BT_TICKER_SLOT_AGNOSTIC:每个node都没有ticks_slot,完全把ticker拿来当timer来用,可以用一个RTC的HW timer来模拟多个SW timer;
  • CONFIG_BT_TICKER_LOW_LAT:在Node进入schedule list时做collision 处理,而不是在expiry的时候。

Ticker里还没有解决的问题

  1. 假如某种原因,ticker_worker() 被耽误了,导致一次timeout,有好几个node的callback都被执行到了,那这些callback都要吃RF,他们之间的冲突又是怎么解决?