I have recently(!) released port-expander
, a small crate to make
access to I²C port-expanders as easy as possible.
A lot of times, when working with port-expanders, you'll have pins from other
devices connected to them. For those other devices, a driver might already
exist, which expects an embedded_hal::digital::OutputPin
to control said pin.
To make this work, an abstraction is needed which allows "splitting" the
port-expander into several separate "objects", each representing a single pin.
Additionally, this of course means we need some sort of synchronization between
them, to ensure we cannot get any race-conditions between accesses to two
different pins.
port-expander does just that and aims to be a single crate supporting many different I²C port-expander ICs.
API
Here is an example of how it works, shown using the PCA9536 4-bit port-expander:
// Initialize I2C peripheral from HAL
let i2c = todo!();
// A0: HIGH, A1: LOW, A2: LOW
let mut pca9536 = port_expander::Pca9536::new(i2c);
let pca_pins = pca9536.split();
let io0_0 = pca_pins.io0_0.into_output().unwrap();
let io0_1 = pca_pins.io0_1; // default is input
io0_0.set_high().unwrap();
assert!(io0_1.is_high().unwrap());
The magic part is the .split()
method: It returns a Parts
struct which contains one member for each pin:
pub struct Parts<'a, /* ... */> {
pub io0: crate::Pin<'a, crate::mode::Input, /* ... */>,
pub io1: crate::Pin<'a, crate::mode::Input, /* ... */>,
pub io2: crate::Pin<'a, crate::mode::Input, /* ... */>,
pub io3: crate::Pin<'a, crate::mode::Input, /* ... */>,
}
These pins can (should) be moved out of Parts
, configured into their
appropriate mode and can then be used for whatever purpose you need. For
configuration, the into_output()
and into_input()
methods are available:
let input_pin = parts.io0; // default is input
let output_pin = parts.io0.into_output();
// an output can later be configured as an input again
let input_pin2 = output_pin.into_input();
The API provided by each pin closely mirrors what you'd find in an MCU HAL for its GPIO pins:
// Input pins (or quasi-bidirectional)
assert!(input_pin.is_high().unwrap());
assert!(!input_pin.is_low().unwrap());
// Output pins (or quasi-bidirectional)
output_pin.set_high().unwrap();
assert!(output_pin.is_set_high().unwrap());
output_pin.set_low().unwrap();
assert!(output_pin.is_set_low().unwrap());
output_pin.toggle().unwrap();
Additionally, the pins of course implement the relevant traits from embedded-hal so they can be passed to generic drivers.
Device Support
As of writing this post, 4 devices are supported:
The idea of port-expander is to make it super easy to expand this list: Because most devices are very similar, driver implementations can be shared in a lot of places. Contributions are welcome!
Atomic Access to multiple Pins
Sometimes, you need to access multiple pins in a single operation to ensure as
little skew between sampling them. port-expander provides two functions to
help with that: read_multiple()
and write_multiple()
. Each operates
on an array of pins and will perform the operation in a single bus transaction
(to the point where this is supported by the IC). As an example:
// read multiple pins in a single transaction
let values: [bool; 2] = port_expander::read_multiple([&io0, &io1]).unwrap();
// write multiple pins in a single transaction
port_expander::write_multiple(
[&mut io0, &mut io1],
[true, false],
).unwrap();
Synchronization
I mentioned this in the beginning: Due to splitting the driver "apart", we need
to ensure pin accesses are synchronized with each other. To do this,
port-expander leverages the BusMutex
from
shared-bus.
For making the common case easy, the ::new()
constructor always uses
a thread-local mutex (essentially a RefCell
) which has the least possible
overhead. This will work fine for any situation where all "users" of pins are
contained within the same execution context.
For more complex situations where pin "users" are spread across multiple tasks
or threads and where pin accesses can thus race against each other, a "proper"
mutex is needed. You can force a custom mutex type by using the
::with_mutex()
constructor instead:
let pca9536: port_expander::Pca9536<std::sync::Mutex<_>>
= port_expander::Pca9536::with_mutex(i2c);
Interrupts
Some port-expanders provide an interrupt signal which is asserted whenever one of the inputs changes state. port-expander does not handle this signal for you, you should check it separately and then query the input pins.