Description
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?