Drivers

Writing drogue device drivers

Drogue-device contains device drivers for different boards and sensors. 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:

  • A trait that defines the API that drivers implement. This may already exist, either in drogue-device or elsewhere. We encourage re-using existing traits where it makes sense. The traits are located in device/src/traits.

  • A driver that implements an API, either using a HAL or hardware directly. Drivers are located in device/src/drivers/.

  • An actor implementation for the driver. This is useful for peripherals that require shared access by multiple parts of the system, as the actor will ensure its only serving one user at a time. Actors are located in device/src/actors.

Which of the above you decide to implement depends on the driver. A Led may not need an Actor, because it is usually operated only by a single component, whereas it makes sense for a network stack/driver that is used by multiple components. The nice properties of actors is that they compose easily.

Actors does carry with them some overhead of extra code to spawn the actor and holding incoming messages in a queue. Therefore, it sometimes makes sense using a layered approach for drivers: A driver that can be used for the cases where you don’t need the shared access, and an Actor using that driver where you need shared access.

Writing an async trait

Drogue-device supports both sync and async APIs, and both have their uses. Sync traits are covered by the Rust documentation. 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):

#![feature(generic_associated_types)]

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:

#![feature(type_alias_impl_trait)]
#![feature(generic_associated_types)]

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;
            self.value
        }
    }

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

Writing an Actor

An actor must implement the Actor trait, which will allow it to be mounted into the drogue device runtime. An actor providing an atomic counter using the above driver is shown below:

#![feature(type_alias_impl_trait)]
#![feature(generic_associated_types)]
pub struct AtomicCounter {
    counter: MyCounter
}

pub enum CounterMessage {
    Increment,
    Add(u32),
}

impl Actor for AtomicCounter {
    type Message<'m> = CounterMessage;
    type Response = u32;

    type OnMountFuture<'m> = impl Future<Output = ()> + 'm;
    fn on_mount<'m, M>(
        &'m mut self,
        _: Self::Configuration,
        _: Address<'static, Self>,
        inbox: &'m mut M,
    ) -> Self::OnMountFuture<'m, M>
    where
        M: Inbox<'m, Self> + 'm
    {
        async move {
            loop {
                if let Some(m) = inbox.next().await {
                    let value = match m.message() {
                        CounterMessage::Increment => self.counter.increment().await,
                        CounterMessage::Add(value) => self.counter.add(*value).await,
                    }
                    // Set the response value
                    message.set_response(value);
                }
            }
        }
    }
}

Although a very complex way to implement an atomic counter, it gives you an idea of how actors provide exclusive access to a resource.