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_PROGRAMUser 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所有时间相关的变量的时候,一定要注意该时间是绝对时间还是相对时间,如果是相对时间,是相对于谁,只要搞清楚这个,基本上能理解整个流程。
- 所有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() 中的冲突处理:
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里还没有解决的问题:
- 假如某种原因,ticker_worker() 被耽误了,导致一次timeout,有好几个node的callback都被执行到了,那这些callback都要吃RF,他们之间的冲突又是怎么解决?