Crate rppal_pfd

source ·
Expand description

§PiFace Digital driver

Crates.io Crates.io Crates.io GitHub Workflow Status

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. Which Chip 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§