Expand description
§PiFace Digital driver
A driver for the PiFace Digital I/O expander for the Raspberry Pi which is accessed over an SPI bus.
§Example usage
See also the contents of the ${CARGO_MANIFEST_DIR}/examples
folder for more extensive examples.
use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, SpiBus, SpiMode};
// Create an instance of the driver for the device with the hardware address
// (A1, A0) of 0b00 on SPI bus 0 clocked at 100kHz. The address bits are set using
// `JP1` and `JP2` on the PiFace Digital board.
let mut pfd = PiFaceDigital::new(
HardwareAddress::new(0).expect("Invalid hardware address"),
SpiBus::Spi0,
ChipSelect::Cs0,
100_000,
SpiMode::Mode0,
).expect("Failed to create PiFace Digital");
pfd.init().expect("Failed to initialise PiFace Digital");
// Take ownership of the output pin on bit 4 of the device.
let pin = pfd
.get_output_pin(4)
.expect("Failed to get Pin");
// Set the pin to logic-level low.
pin.write(Level::Low).expect("Bad pin write");
§Features
The crate implements the following features.
§mockspi
The crate is compiled with all code that accesses the real Raspberry Pi hardware mocked out so that the code will compile and run successfully on non-Raspberry Pi hardware:
- The PiFaceDigital code to access the GPIO (used for handling interrupts from the MCP23S17) is entirely removed.
- The MCP23S17 code from the
rppal_mcp23s17
crate uses a mock SPI that provides for very simple setting of test data in the MCP23S17’s registers and checking that the expected reads and writes have been undertaken.
§Building
You are likely to want to cross-compile this code for your target Raspberry Pi. The
project includes a Makefile.toml
for use with
cargo-make
which has tasks that use the
cross
cross-compilation environment:
- rpi - build the debug build for the target Raspberry Pi.
- rpi-release - build the release build for the target Raspberry Pi.
- rpi-test - test target code under
qemu
emulation.
In your project you’re likely to use a similar cross-compilation environment and invoke your build like:
cross build --target arm-unknown-linux-gnueabihf # First generation Raspberry Pi.
cross build --target armv7-unknown-linux-gnueabihf # Later Raspberry Pi versions.
When testing the underlying rppal-mcp23s17
crate will compile in a trivial mock SPI that allows you to write unit tests that will
run in the host environment without requiring target hardware. Similarly, this crate
will compile in an accessor function TODO
that your unit tests can access the mock SPI
to set up test data and to check that the I/O expander was configured as expected.
§Concurrency Warning
Note that the rppal_mcp23s17
contained in the PiFaceDigital
is
!Send
so that the device can only be used within the
context of a single thread. However, there is nothing to stop separate instances on
separate threads accessing the same MCP23S17 device. However, when it comes to the
PiFace Digital itself, it needs to take ownership of the Raspberry PI’s GPIO-25
pin which is used as the interrupt input. As it currently stands that has the effect
of enforcing the existence of just one PiFace Digital device on the system because
attempts to create a second device will fail with a “GPIO device busy” error.
Further work is necessary to allow a single process to share the interrupts; sharing between processes is likely always going to be impossible with this user-space architecture for the interrupts.
§Acknowledgements
This library has taken a lot of inspiration and guidance from the design of the PiFace Digital I/O Python library.
This library has followed some of the API design patterns used in the RPPAL crate.
Thanks!
§Extended example
This example is available in ${CARGO_MANIFEST_DIR}/examples/blink-fast-slow.rs
.
// Blink an LED, controlling the flash-rate with buttons.
//
// This example illustrates:
//
// - OutputPin to flash the LED.
// - InputPin to detect buttons.
// - Polling for interrupts across multiple InputPins.
// - Use of a timeout for the interrupt polling.
//
// USAGE:
//
// The four buttons control the operation:
//
// 1) Flash faster
// 2) Flash slower
// 3) Quit the program
// 4) [Not used]
use anyhow::Result;
use log::{error, info};
use rppal_pfd::{
ChipSelect, HardwareAddress, InterruptMode, Level, PiFaceDigital, SpiBus, SpiMode,
};
use std::{cmp::max, ptr, time::Duration};
fn main() -> Result<()> {
env_logger::init();
info!("Blink started!");
println!("Use the push-buttons to control the blink rate:\n");
println!(" Button 1: Faster");
println!(" Button 2: Slower");
println!(" Button 3: Quit\n");
let mut pfd = PiFaceDigital::new(
HardwareAddress::new(0).unwrap(),
SpiBus::Spi0,
ChipSelect::Cs0,
100_000,
SpiMode::Mode0,
)?;
pfd.init()?;
let mut faster_button = pfd.get_pull_up_input_pin(0)?;
let mut slower_button = pfd.get_pull_up_input_pin(1)?;
let mut quit_button = pfd.get_pull_up_input_pin(2)?;
let led = pfd.get_output_pin_low(2)?;
// Generate interrupts on both edges as this simplifies the logic
// to avoid perpetual re-interrupts whilst the button is pressed.
faster_button.set_interrupt(InterruptMode::BothEdges)?;
slower_button.set_interrupt(InterruptMode::BothEdges)?;
quit_button.set_interrupt(InterruptMode::BothEdges)?;
let mut period = 1000;
let mut led_state = Level::High;
let mut quit = false;
while !quit {
led.write(led_state)?;
match pfd.poll_interrupts(
&[&faster_button, &slower_button, &quit_button],
false,
Some(Duration::from_millis(period / 2)),
) {
// At least one (most probably exactly one) button interrupted so action it.
Ok(Some(interrupts)) => {
for (pin, level) in interrupts {
match pin {
p if ptr::eq(p, &faster_button) && level == Level::Low => {
period = max(period / 2, 125);
println!("Going faster: {} Hz", 1000.0 / period as f32);
}
p if ptr::eq(p, &slower_button) && level == Level::Low => {
period *= 2;
println!("Going slower: {} Hz", 1000.0 / period as f32);
}
p if ptr::eq(p, &quit_button) && level == Level::Low => {
quit = true;
}
p => {
info!(
"Ignoring button: {} going {}",
p.get_pin_number() + 1,
level
);
}
}
}
}
// Poll timed out, so invert the state of the LED
Ok(None) => {
led_state = !led_state;
}
// Oops!
Err(e) => {
error!("Poll failed unexpectedly with {e}");
return Err(e.into());
}
}
}
// Quit, leaving the LED off.
led.set_low()?;
println!("\nBlinking is done!\n");
Ok(())
}
Structs§
- The hardware address of the device - two bits.
- An input pin.
- Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. A pin on a GPIO port configured for output. - Represents an instance of the PiFace Digital I/O expander for the Raspberry Pi.
- Internal state of the PiFace Digital card.
Enums§
- Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. WhichChip Select
line to use on the SPI bus. - Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. Interrupt input trigger modes that an InputPin supports. - Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. Pin logic levels. - Errors that operation of the PiFace Digital can raise.
- Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. SPI buses. - Re-export of
rppal_mcp23s17
crate APIs which we use on this crate’s APIs. SPI modes indicating the clock polarity and phase.
Type Aliases§
- Convenient alias for
Result<_>
types can havePiFaceDigitalError
s.