Skip to content

[RFC] digital::IoPin, pins that can switch between input and output modes at runtime #29

Closed
@japaric

Description

@japaric

Some people have expressed interest in a trait like this for writing generic LCD drivers but there are probably other use cases.

We were discussing this on IRC yesterday and I proposed three different APIs:

Proposals

Result based API

/// A pin that can switch between input and output modes at runtime
pub trait IoPin {
    /// Signals that a method was used in the wrong mode
    type Error;

    /// Configures the pin to operate in input mode
    fn as_input(&mut self);
    /// Configures the pin to operate in output mode
    fn as_output(&mut self);

    /// Sets the pin low
    fn set_low(&mut self) -> Result<(), Self::Error>;
    /// Sets the pin high
    fn set_high(&mut self) -> Result<(), Self::Error>;

    /// Checks if the pin is being driven low
    fn is_low(&self) -> Result<bool, Self::Error>;
    /// Checks if the pin is being driven high
    fn is_high(&self) -> Result<bool, Self::Error>;
}

// this function won't panic; LLVM should be able to opt way the panicking branches
fn example(mut io: impl IoPin) {
    io.as_input();
    if io.is_low().unwrap() {
        /* .. */
    }
    if io.is_high().unwrap() {
        /* .. */
    }

    io.as_output();
    io.set_low().unwrap();
    io.set_high().unwrap();
}

Closure based API

/// A pin that can switch between input and output modes at runtime
pub trait IoPin {
    /// Pin configured in input mode
    type Input: InputPin;

    /// Pin configured in output mode
    type Output: OutputPin;

    /// Puts the pin in input mode and performs the operations in the closure `f`
    fn as_input<R, F>(&mut self, f: F) -> R
    where
        F: FnOnce(&Self::Input) -> R;

    /// Puts the pin in output mode and performs the operations in the closure `f`
    fn as_output<R, F>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Self::Output) -> R;
}

fn example(mut io: impl IoPin) {
    io.as_input(|i| {
        if i.is_low() { /* .. */ }
        if i.is_high() { /* .. */ }
    });

    io.as_output(|o| {
        o.set_low();
        o.set_high();
    });
}

Implicit / Automatic API

/// A pin that can switch between input and output modes at runtime
pub trait IoPin {
    /// Signals that a method was used in the wrong mode
    type Error;

    /// Sets the pin low
    ///
    /// **NOTE** Automatically switches the pin to output mode
    fn set_low(&mut self);
    /// Sets the pin high
    ///
    /// **NOTE** Automatically switches the pin to output mode
    fn set_high(&mut self);

    /// Checks if the pin is being driven low
    ///
    /// **NOTE** Automatically switches the pin to input mode
    /// **NOTE** Takes `&mut self` because needs to modify the configuration register
    fn is_low(&mut self) -> bool;
    /// Checks if the pin is being driven high
    ///
    /// **NOTE** Automatically switches the pin to input mode
    /// **NOTE** Takes `&mut self` because needs to modify the configuration register
    fn is_high(&mut self) -> bool;
}

fn example(mut io: impl IoPin) {
    if io.is_low() {
        /* .. */
    }
    if io.is_high() {
        /* .. */
    }

    io.set_low();
    io.set_high();
}

I have implemented the proposals as branches io-1, io-2, io-3. You can try them out by adding something like this to your Cargo.toml file:

[dependencies]
embedded-hal = "0.1.0"

[replace]
"embedded-hal:0.1.0" = { git = "https://github.com/japaric/embedded-hal", branch = "io-1" }

Implementation concerns

There were some concerns about whether these traits can actually be implemented for all possible scenarios given that switching the mode of any pin on say, GPIOA, usually requires a RMW operation on some control register; thus the pins need exclusive access to the control register when switching modes.

Following the CRL idea of the Brave new IO blog post it seems that implementations would run into this problem:

struct IoPin {
    // pin number
    n: u8,
    crl: CRL,
    // ..
}

let pa0 = IoPin { n: 0, crl, .. };
let pa1 = IoPin { n: 1, crl, .. };
//^ error: use of moved value: `x`

But you can use a RefCell to modify the CRL register in turns:

struct IoPin<'a> {
    // pin number
    n: u8,
    crl: &'a RefCell<CRL>,
    // ..
}

let crl = RefCell::new(crl);
let pa0 = IoPin { n: 0, crl: &crl, .. };
let pa1 = IoPin { n: 1, crl: &crl, .. };

This limits you to a single context of execution though because RefCell is not Sync which means &'_ RefCell is not Send which means IoPin is not Send either.

But you can recover the Send-ness and avoid runtime checks by splitting CRL in independent parts that can be modified using bit banding, if your chip supports that:

let crls = crl.spilt();
let crl0: CRL0 = crls.crl0;
let crl1: CRL1 = crls.crl1;

let pa0 = IoPin0 { crl0, .. };
let pa1 = IoPin0 { crl0, .. };

Thoughts on these two proposals? Do they fit your use case? Do you have a different proposal?

cc @kunerd @therealprof

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions