Writing async drivers

Async Drivers follow a common set of patterns that makes it easier to write new drivers. Device drivers can be written in different ways, but the common approach is to implement one or more of the following:

  • Create a new Rust crate that implements the driver.

  • Implement traits from embedded-hal-async if relevant.

Writing an async trait

Unfortunately, the support in Rust for writing async traits is limited, but it is possible using features from Rust nightly.

An async trait can be specified by enabling a feature named generic associcated types (GAT):

pub trait Counter {
    type IncrementFuture<'m>: Future<Output = u32> where Self: 'm;
    fn increment<'m>(&'m mut self) -> Self::IncrementFuture<'m>;

    type AddFuture<'m>: Future<Output = u32> where Self: 'm;
    fn add<'m>(&'m mut self, value: u32) -> Self::AddFuture<'m>;

An implementor of the above trait will be able to write async code with some boilerplate, and application code can rely on the trait.

Writing an async driver

A driver is an implementation of the trait that applications use. With the trait defined in the previous section, lets take a look at what a driver would look like:


pub struct MyCounter {
    value: u32,

impl Counter for MyCounter {
    type IncrementFuture<'m> = impl Future<Output = u32> + 'm;
    fn increment<'m>(&'m mut self) -> Self::IncrementFuture<'m> {
        async move {
            self.value += 1;

    type AddFuture<'m> = impl Future<Output = u32> + 'm;
    fn add<'m>(&'m mut self, value: u32) -> Self::AddFuture<'m> {
        async move {
            self.value += value;

You’ll notice that we’ve used yet another nightly feature that allows specifying the IncrementFuture associated type using the impl Future<…​> syntax.

At the expense of needing to define the associated type and a somewhat awkward return value, the driver can write blocks of async code to implement the trait.