Expand description
Driver library for the Maxim MAX7301 serial I/O expander.
The MAX7301 is a device that provides either 20 or 28 GPIO pins, which are software-configurable as push-pull output, floating input, or input with weak pull-up. The state of each pin can be read and written through an SPI serial bus.
This driver is intended to work on embedded platforms using any implementation of the
embedded-hal
trait library. It communicates with the expander via any SPI and GPIO device
implementing the respective traits, and permits creation of new GPIO devices corresponding to
the I/O pins on the device, which themselves implement the HAL traits.
§Construction
To set up the driver:
- Use your platform’s
embedded-hal
implementation to obtain the necessary I/Os where your MAX7301 is connected. For the SPI version (currently the only supported version), you will need an SPI master device, and one GPIO push-pull output pin device for chip select. - Construct an
ExpanderInterface
— theSpiInterface
for MAX7301 — which will take ownership of the I/O devices you just obtained. - Construct an
Expander
, which will take ownership of theExpanderInterface
, and which will provide a builder API to configure the device.
let spi = /* construct something implementing embedded_hal::spi::blocking::{Write,Transfer} */
let cs = /* construct something implementing embedded_hal::digital::v2::OutputPin */
let ei = max7301::SpiInterface::new(spi, cs);
let mut expander = max7301::Expander::new(ei);
§Device configuration
See Expander::configure
and [expander::Configurator
].
The configure
method will produce a builder that you can use to set up the chip’s
configuration registers:
expander
.configure()
.ports(4..=31, max7301::PortMode::InputPullup)
.port(7, max7301::PortMode::Output)
.shutdown(false)
.commit()?;
§Raw mode
See Expander
.
Now with a configured device, you may use it in raw mode, accessing the device’s ports directly:
let four_thru_twelve: u8 = expander.read_ports(4)?;
expander.write_port(7, false)?;
§HAL modes
To compose the driver with other embedded-hal
drivers that are compatible with
embedded_hal::digital::v2::{InputPin,OutputPin}
, you can convert the Expander
into an I/O
adapter that will produce ownable PortPin
instances for each GPIO port on the expander.
PortPin
implments the InputPin
and OutputPin
traits from embedded-hal
, allowing
composition with other drivers that need GPIO pins.
You can choose one of two interfaces to construct PortPin
s:
- The immediate mode interface, where calling the GPIO trait methods on any
PortPin
immediately generates a bus transaction to the expander in order to perform the operation on that pin; or - The transactional interface, which allows you to control the bus traffic separately from
the activity on the
PortPin
s, enabling batching of reads and writes to amortize and/or reduce bus traffic and latency.
§Immediate mode
See Expander::into_immediate
and ImmediateIO
.
expander.configure().ports(4..=6, max7301::PortMode::Output).commit()?;
let imm_io = expander.into_immediate::<max7301::DefaultMutex<_>>();
let red_lamp = imm_io.port_pin(4);
let yellow_lamp = imm_io.port_pin(5);
let green_lamp = imm_io.port_pin(6);
let mut traffic_light = MyTrafficLight::new(red_lamp, yellow_lamp, green_lamp);
traffic_light.change_to_red();
In this example, each time MyTrafficLight
interacts with an OutputPin
trait method, the
driver will immediately trigger a bus transaction to set the appropriate level on the
expander’s corresponding output pin. Likewise, if an InputPin
trait method is called, the
driver will perform a bus transaction to read the current state from the expander pin.
§Transactional mode
See Expander::into_transactional
and TransactionalIO
.
expander
.configure()
.ports(4..=6, max7301::PortMode::Output)
.port(7, max7301::PortMode::InputFloating)
.commit()?;
let txn_io = expander.into_transactional::<max7301::DefaultMutex<_>>();
let red_lamp = txn_io.port_pin(4);
let yellow_lamp = txn_io.port_pin(5);
let green_lamp = txn_io.port_pin(6);
let sensor_tripped = txn_io.port_pin(7);
let mut traffic_light =
MyFancyTrafficLight::new(red_lamp, yellow_lamp, green_lamp, sensor_tripped);
txn_io.refresh()?;
traffic_light.change_if_tripped();
txn_io.write_back(max7301::Strategy::Exact)?;
In this example, the transactional API adds two extra methods on the I/O adapter: refresh()
and write_back()
.
- The
refresh
method instructs the driver to perform bus operations to load the current state of every port for which aPortPin
exists into its internal write-back cache. - As
MyTrafficLight
interacts with thePortPin
trait methods, they read and mutate the state of the ports in the write-back cache, without interacting with the hardware. - Finally,
write_back
is called to instruct the driver to perform bus operations to write any modified port states back to the expander. The single argument selects the strategy that will be used to batch the write operations into fewer write cycles on the bus (seeexpander::transactional::Strategy
for a description).
§Choosing a mode
- Transactional mode is excellent for applications which use the MAX7301 to obtain a large
number of independent GPIOs, for example HMI applications like keypads, switches, encoders,
sensors, LEDs, and indicators. In such a case, the states of the GPIOs are often read or
updated in a procedure that can be bracketed by
refresh
andwrite_back
, since the order in which the states are read from or written out to the hardware is not important. - Transactional mode is not appropriate, and immediate mode should be used instead, for drivers
or applications that use the generated GPIOs in a “bit-banged” manner, where pins must
transition states with particular timings or orderings with respect to each other. In
transactional mode, the write-back cache delays the hardware transitions until the next
write_back
call, erasing any sequencing or timing and breaking these use cases.
§Mutual exclusion
The HAL adapters require you to provide a mutual exclusion primitive to arbitrate access to the
hardware from multiple PortPin
s. For now, the adapters are parameterized over a type
implementing the IOMutex
trait, which is a concept borrowed from
shared-bus
, and which will hopefully be superseded by a
standard embedded-hal
trait.
In a std
environment you may enable the std
Cargo feature, and mutex::DefaultMutex<T>
will be a type alias to std::sync::Mutex<T>
with a provided impl of IOMutex
. Similarly, for
Cortex-M environments using the cortex-m
crate, enabling the cortexm
Cargo feature will
alias mutex::DefaultMutex<T>
to cortex_m::interrupt::Mutex<core::cell::RefCell<T>>
with a
provided IOMutex
impl. This arrangement should allow you to just specify DefaultMutex
as in
the examples, and have the correct thing happen based on the build environment.
Re-exports§
pub use config::PortMode;
pub use expander::immediate::ImmediateIO;
pub use expander::pin::ExpanderIO;
pub use expander::pin::PortPin;
pub use expander::transactional::Strategy;
pub use expander::transactional::TransactionalIO;
pub use expander::Expander;
pub use interface::spi::SpiInterface;
pub use interface::ExpanderInterface;
pub use mutex::DefaultMutex;
pub use mutex::IOMutex;
Modules§
- config
- Abstractions used to configure the MAX7301 hardware.
- expander
- The port expander device API. This provides the
Expander
type which is a direct abstraction of the MAX7301. It allows direct use of all operations available on the device. - interface
- This module provide shims for the
embedded-hal
hardware correspoding to the MAX7301’s supported electrical/bus interfaces. It is a shim betweenembedded-hal
implementations and the expander’s registers. - mutex
- Provides mutual exclusion for various environments.
- registers
- The register addresses within the MAX7301.