embassy_time_driver::Driver
在 embassy 中,计时器的核心抽象是 Driver 接口:
#![allow(unused)] fn main() { pub trait Driver: Send + Sync + 'static { fn now(&self) -> u64; unsafe fn allocate_alarm(&self) -> Option<AlarmHandle>; fn set_alarm_callback( &self, alarm: AlarmHandle, callback: fn(_: *mut ()), ctx: *mut () ); fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) -> bool; } }
结合 embassy 一些实现代码,我发现 Driver
trait 的类型大多有以下行为:
now
函数提供 tick 级别的、自开机以来的时间(不一定作为字段存储在类型上,因为可以直接读取状态寄存器来获取时间)allocate_alarm
函数提供 alarm 分配逻辑:- alarm 应该有自己的数据结构(比如 AlarmState),存储来自执行器设置的回调函数和执行器实例
- alarm 的功能:适时执行回调函数
- 分配 alarm 的数量通常是有上限的,该数量尤其是编译时已知的,比如
[AlarmState; N]
,其中索引为 AlarmHandle - AlarmHandle 实际上是一个整数编号的包装器类型,所以分配一个 alarm,就是分配一个索引/编号(当然,也可能包含一些设置 AlarmState 的逻辑)
set_alarm_callback
函数其实就是对第 n 个 alarm 在运行时进行初始化,通常由执行器设置set_alarm
函数通过告知第 n 个 alram,以及一个给定的 tick 时刻来安排执行回调函数- 当这个给定的时刻处于将来,那么它返回 true,以非同步方式安排回调函数执行,这意味着不要在 set_alarm 函数内直接运行回调函数!
- 当这个给定的时刻已经过去,那么它返回 false,无需安排回调函数执行
embassy-executor
开启integrated-timers
feature 之后,一旦 set_alarm 返回 true,那么结束一轮 poll;如果 set_alarm 一直返回 false,那么将这轮 poll 会一直持续下去
这个 回调函数 目前仅仅是调用 __pender
函数,相应的代码如下:
#![allow(unused)] fn main() { // 每个编译目标提供: // * embassy 在 arch* feature 上提供默认的 __pender // * 如果自己提供,不要开启 embassy 任何 arch 开头的 feature // // context 实际上是 raw::Executor::new(context) 中的参数,由自己构造, // 比如在我写的 embassy-usage 中为一个类似唤醒的结构 #[export_name = "__pender"] fn __pender(context: *mut ()) { ... } impl SyncExecutor { // Executor 的内部结构 #[cfg(feature = "integrated-timers")] fn alarm_callback(ctx: *mut ()) { let this: &Self = unsafe { &*(ctx as *const Self) }; this.pender.pend(); // 最终调用 __pender } pub(crate) unsafe fn poll(&'static self) { #[cfg(feature = "integrated-timers")] // 由执行器注册回调函数 (__pender) embassy_time_driver::set_alarm_callback(self.alarm, Self::alarm_callback, self as *const _ as *mut ()); ... loop { ... #[cfg(feature = "integrated-timers")] { // If this is already in the past, set_alarm might return false // In that case do another poll loop iteration. let next_expiration = self.timer_queue.next_expiration(); if embassy_time_driver::set_alarm(self.alarm, next_expiration) { // 当 set_alarm 返回 true,结束这轮 poll,安排执行 __pender break; } } } ... } } }
now
#![allow(unused)] fn main() { fn now(&self) -> u64 }
当前的时间戳,用 tick 计算;必须保证:
- 单调增加:调用的结果总是比之前调用的结果更大或者相等(时间不能回退)
- 不能溢出:可以运行足够长的时间
allocate_alarm
#![allow(unused)] fn main() { unsafe fn allocate_alarm(&self) -> Option<AlarmHandle> }
试图分配一个“警报器” (alarm, 用来在某个时间到了调用回调函数)。
alarm 应该携带一个回调函数和上下文指针:
- 初始化 alarm 的时候,没有 callback,ctx 为 null
- executor 会通过
set_alarm_callback
,给这个 alram 设置回调和上下文。 safty: 在设置回调之前触发 alarm,是 UB 行为。
如果 alarm 用完了,那么返回 None。
set_alarm_callback
#![allow(unused)] fn main() { fn set_alarm_callback( &self, alarm: AlarmHandle, callback: fn(_: *mut ()), ctx: *mut () ) }
给一个 alarm 设置回调函数和上下文。
这个回调函数:
- 应在触发 alarm 时调用
- 参数是 ctx
- 可能从任何上下文调用(中断、或者 thread mode)
set_alarm
#![allow(unused)] fn main() { fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) -> bool }
如果当前时间戳达到这个给定的时间戳,应调用回调函数。
返回值
- true 表示设置/触发 alarm,应尽快安排 异步 调用 alarm 的回调函数:此函数绝不能同步调用回调函数;当前时间 <= 给定的时间
- 如果给定的时间马上就要发生,并且设置 alarm 后,这个时间滑入过去,情况有些复杂
- false 表示不设置 alarm:当前时间 > 给定时间
当调用回调函数时,必须保证 now() 返回的时间 >= 给定时间。
每个 AlarmHandle 一次只能设置一个 alarm:如果之前设置了 alarm,后面设置的 alarm 应该覆盖之前设置的 alarm。
实现例子
实现 __pender
函数的例子:embassy-executor/src/arch 。
实现 Driver trait 的例子: