2 unstable releases
0.2.0 | Jan 2, 2025 |
---|---|
0.1.0 | Jan 11, 2024 |
#1456 in Embedded development
37,813 downloads per month
Used in 110 crates
(16 directly)
42KB
581 lines
embassy-time-driver
This crate contains the driver trait necessary for adding embassy-time
support
for a new hardware platform.
If you want to use embassy-time
with already made drivers, you should depend on the main embassy-time
crate, not on this crate.
If you are writing a driver, you should depend only on this crate, not on the main embassy-time
crate.
This will allow your driver to continue working for newer embassy-time
major versions, without needing an update,
if the driver trait has not had breaking changes.
How it works
embassy-time
is backed by a global "time driver" specified at build time.
Only one driver can be active in a program.
All methods and structs transparently call into the active driver. This makes it
possible for libraries to use embassy-time
in a driver-agnostic way without
requiring generic parameters.
lib.rs
:
Implementing a driver
- Define a struct
MyDriver
- Implement
Driver
for it - Register it as the global driver with
time_driver_impl
.
If your driver has a single set tick rate, enable the corresponding tick-hz-*
feature,
which will prevent users from needing to configure it themselves (or selecting an incorrect configuration).
If your driver supports a small number of set tick rates, expose your own cargo features and have each one
enable the corresponding embassy-time-driver/tick-*
.
Otherwise, don’t enable any tick-hz-*
feature to let the user configure the tick rate themselves by
enabling a feature on embassy-time
.
Example
use core::task::Waker;
use embassy_time_driver::Driver;
struct MyDriver{} // not public!
impl Driver for MyDriver {
fn now(&self) -> u64 {
todo!()
}
fn schedule_wake(&self, at: u64, waker: &Waker) {
todo!()
}
}
embassy_time_driver::time_driver_impl!(static DRIVER: MyDriver = MyDriver{});
Implementing the timer queue
The simplest (but suboptimal) way to implement a timer queue is to define a single queue in the
time driver. Declare a field protected by an appropriate mutex (e.g. critical_section::Mutex
).
Then, you'll need to adapt the schedule_wake
method to use this queue.
Note that if you are using multiple queues, you will need to ensure that a single timer queue item is only ever enqueued into a single queue at a time.
use core::cell::RefCell;
use core::task::Waker;
use critical_section::{CriticalSection, Mutex};
use embassy_time_queue_utils::Queue;
use embassy_time_driver::Driver;
struct MyDriver {
queue: Mutex<RefCell<Queue>>,
}
impl MyDriver {
fn set_alarm(&self, cs: &CriticalSection, at: u64) -> bool {
todo!()
}
}
impl Driver for MyDriver {
fn now(&self) -> u64 { todo!() }
fn schedule_wake(&self, at: u64, waker: &Waker) {
critical_section::with(|cs| {
let mut queue = self.queue.borrow(cs).borrow_mut();
if queue.schedule_wake(at, waker) {
let mut next = queue.next_expiration(self.now());
while !self.set_alarm(&cs, next) {
next = queue.next_expiration(self.now());
}
}
});
}
}
Linkage details
Instead of the usual "trait + generic params" approach, calls from embassy to the driver are done via extern
functions.
embassy
internally defines the driver function as extern "Rust" { fn _embassy_time_now() -> u64; }
and calls it.
The driver crate defines the function as #[no_mangle] fn _embassy_time_now() -> u64
. The linker will resolve the
calls from the embassy
crate to call into the driver crate.
If there is none or multiple drivers in the crate tree, linking will fail.
This method has a few key advantages for something as foundational as timekeeping:
- The time driver is available everywhere easily, without having to thread the implementation through generic parameters. This is especially helpful for libraries.
- It means comparing
Instant
s will always make sense: if there were multiple drivers active, one could compare anInstant
from driver A to anInstant
from driver B, which would yield incorrect results.
Feature flags
Dependencies
~175KB