Coder Social home page Coder Social logo

port-expander's Introduction

port-expander crates.io page docs.rs page

This is a crate providing a common abstraction for I²C port-expanders. This abstraction is not necessarily the most performant, but it allows using the pins just like direct GPIOs. Because the pin types also implement the embedded-hal digital IO traits, they can also be passed to further drivers downstream (e.g. as a reset or chip-select pin).

Example

// Initialize I2C peripheral from HAL
let i2c = todo!();

// A0: HIGH, A1: LOW, A2: LOW
let mut pca9555 = port_expander::Pca9555::new(i2c, true, false, false);
let pca_pins = pca9555.split();

let io0_0 = pca_pins.io0_0.into_output().unwrap();
let io1_5 = pca_pins.io0_1; // default is input

io0_0.set_high().unwrap();
assert!(io1_5.is_high().unwrap());

Accessing multiple pins at the same time

Sometimes timing constraints mandate that multiple pin accesses (reading or writing) happen at the same time. The write_multiple() and read_multiple() methods are designed for doing this.

Supported Devices

The following list is what port-expander currently supports. If you needs support for an additional device, it should be easy to add. It's best to take a similar existing implementation as inspiration. Contributions welcome!

Non-local sharing

port-expander uses a custom trait for abstracting different kinds of mutexes: PortMutex. This means you can also make the pins shareable across task/thread boundaries, given that you provide an appropriate mutex type:

// Initialize I2C peripheral from HAL
let i2c = todo!();

// A0: HIGH, A1: LOW, A2: LOW
let mut pca9555: port_expander::Pca9555<std::sync::Mutex<_>> =
    port_expander::Pca9555::with_mutex(i2c, true, false, false);
let pca_pins = pca9555.split();

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

port-expander's People

Contributors

dependabot[bot] avatar eivindbergem avatar kelleyk avatar markus-k avatar pixmahestiia avatar rahix avatar sympatron avatar t-moe avatar zandemax avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

port-expander's Issues

Robust port expander initialization

Right now, most port expander implementations assume the device to be in its reset state. They do not check that all registers actually match expected values.

This is not robust because it can break horribly when a device isn't in its reset state for some reason (glitch, bad i2c transfer from other device, bug in our code, etc.).

Thus, let's augment all port expander drivers to properly set all important registers to their reset values during startup:

  • MAX7321
  • PCA9536
  • PCA9538
  • PCA9555
  • PCF8574A
  • PCF8574
  • PCF8575
  • TCA6408A
  • PI4IOE5V6408

Drop `shared-bus` dependency

With embedded-hal 1.0, shared-bus got obsolete. Thus we should drop the remaining dependence on it for its mutex trait and substitute it for something else.

(Follow-up to #19).

using Pin as Peripheral/DerefMut

Hi,

I'm a new Rust developer, trying to use this library to access a pin on a PCA9555 port expander as the RESET pin of an ethernet driver. Even though the Pin struct from this library implements embedded_hal::OutputPin, but it does not implement esp-idf-hal::Peripheral and I don't know how to go forward from here. Is this something that is just not supported by this library? Probably for good reason? Or is there a way to make it work with this library?

In short, I initiatlize my pca9555 and grab the io0_0 pin. This pin is of type

Pin<Output, NullMutex<Driver<I2cDriver>>>

The function I want to pass it to (EthDriver::new_spi) requests the following type:

Option<impl Peripheral<P = impl gpio::OutputPin> + 'd>

The compiler gives me the following errors:

the trait bound `port_expander::Pin<'_, port_expander::mode::Output, shared_bus::mutex::NullMutex<port_expander::dev::pca9555::Driver<I2cDriver<'_>>>>: DerefMut` is not satisfied

For reference, my full (relevant) code look like this:

    let i2cconfig = I2cConfig::new()
        .baudrate(KiloHertz(10).into())
        .sda_enable_pullup(true)
        .scl_enable_pullup(true);

    let i2c_driver_eth = I2cDriver::new(
        peripherals.i2c1,
        peripherals.pins.gpio21,
        peripherals.pins.gpio22,
        &i2cconfig,
    )
    .expect("Failed to create i2c driver");

    let pca9555_eth = port_expander::Pca9555::new(i2c_driver_eth, true, true, true);
    // let boxed_2: Box<port_expander::Pca9555<std::sync::Mutex<_>>> = Box::new(pca9555_2);
    // let never_drop_pca9555_2: &'static mut port_expander::Pca9555<std::sync::Mutex<_>> =
    //     Box::leak(boxed_2);
    // let pca_pins_2 = never_drop_pca9555_2.split();
    let pca_pins_eth = pca9555_eth.split();

    let spidriver = spi::SpiDriver::new(
        peripherals.spi2,
        peripherals.pins.gpio18,       // spi clock
        peripherals.pins.gpio23,       // sdo = mosi = serial data out
        Some(peripherals.pins.gpio19), // sdi = miso = serial data in
        &spi::SpiDriverConfig::new().dma(spi::Dma::Auto(4096)),
    )
    .expect("Failed to make SPI driver");

    let eth_reset_pin = pca_pins_eth
        .io0_0
        .into_output()
        .expect("Failed to grab eth reset pin");

    let ethdriver = eth::EthDriver::new_spi(
        spidriver,
        peripherals.pins.gpio32, // interrupt
        // None as Option<AnyOutputPin>, // cs = chip select
        Some(peripherals.pins.gpio5), // cs = chip select
        // None as Option<AnyOutputPin>, // rst = reset TODO
        Some(eth_reset_pin), // rst = reset,  // !!! THIS LINE PRODUCES THE COMPILER ERROR
        eth::SpiEthChipset::W5500,
        20_u32.MHz().into(),
        None,
        None,
        sysloop.clone(),
    )
    .expect("Failed to make ethernet driver");

The line with Some(eth_reset_pin) produces the following compiler error:

 main.rs   166   9 error    E0277  the trait bound `port_expander::Pin<'_, port_expander::mode::Output, shared_bus::mutex::NullMutex<port_expander::dev::pca9555::Driver<I2cDriver<'_>>>>: DerefMut` is not satisfied
 the following other types implement trait `Peripheral`:
   AnyIOPin
   AnyInputPin
   AnyOutputPin
   Modem
   ADC1
   ADC2
   CAN
   Gpio0
 and 78 others
 required for `port_expander::Pin<'_, port_expander::mode::Output, shared_bus::mutex::NullMutex<port_expander::dev::pca9555::Driver<I2cDriver<'_>>>>` to implement `Peripheral` (lsp)

This is the signature of new_spi:

    pub fn new_spi(
        driver: T,
        int: impl Peripheral<P = impl gpio::InputPin> + 'd,
        cs: Option<impl Peripheral<P = impl gpio::OutputPin> + 'd>,
        rst: Option<impl Peripheral<P = impl gpio::OutputPin> + 'd>,
        chipset: SpiEthChipset,
        baudrate: Hertz,
        mac_addr: Option<&[u8; 6]>,
        phy_addr: Option<u32>,
        sysloop: EspSystemEventLoop,
    ) -> Result<Self, EspError>

I'm kind of stuck here and not sure what to do from here. If I need to provide any more information, please let me know.

Thanks already for any help!

Support for multiple devices on same bus?

I designed an IO expander card with four PCA9555 chips with optional 8-bit ULN2308 buffers. While exploring the options to share i2c bus with several instances of either pca9535 or port-expander drivers, it struck me.

When I have something like 2 to 8 identical chips with consecutive i2c bus addresses also sharing a common _INT line, why on earth should I use something so elaborate like shared-bus, Mutex and all that?

My idea is to make my own copy of this library and add support for using a whole array of identical IO expander chips with consecutive i2c addresses. I guess it could be done backward compatible so that the array thing is created with a different constructor, new_array() perhaps, and with something like num_chips and i2c_addr_offset as parameters.

How does that sound like? I am making it for my own need anyway, but would it be beneficial to push it up to the mainstream version also?

Simple `Pin` type aliases?

Hey! Thanks so much for this crate! I've run into an issue and I'm looking for some guidance.

I'm somewhat new to Rust and very new to embedded so hopefully my question will make sense. Is there a way to type alias expander pins as simply as an MCU pin? For example, the stm32f1xx-hal has simple type aliases for each pin, something like PA0 or PB9.

What I'm trying to solve:

In my project I've got a few basic components: single-pin LED, dual-pin LED and a switch. I've then created assemblies that combine these building blocks to create something like a "button that has a multi-color LED". I wrote them all with the embedded-hal OuputPin or InputPin traits in mind. Here's a simplified version of the Led struct:

pub struct Led<Pin>
where
    Pin: OutputPin,
{
    pin: Pin,
}

impl<Pin> Led<Pin>
where
    Pin: OutputPin,
{
    #[must_use]
    pub fn new(pin: Pin) -> Self {
        Led { pin }
    }

    pub fn on(&mut self) {
        self.pin.set_high().ok();
    }

    pub fn off(&mut self) {
        self.pin.set_low().ok();
    }
}

In order to control these components/assemblies I need to define them in rtic as resources in the Shared resource struct. The catch is, as far as I understand, that the Shared struct doesn't allow lifetimes or generic parameters. Before I added the expander I was just defining Led with the MCU pin within the struct like so:

#[shared]
struct Shared {
    led: Led<stm32f1xx_hal::gpio::PA0>,
}

I would do the same thing with GPIO expander pins but the type for a pin is incredibly complicated and has lifetime and generic parameters. I checked other expander crates and they all seems to have the same sort of complex type signatures. So I'm under the impression that the answer to my question is probably "no".

In any case, I tried using the full GPIO expander pin type definition and just giving it a static lifetime. But when I did that, rtic complained that the type (RefCell) was not thread safe. I know I'm running this single-threaded so I defined a Newtype around the GPIO expander pin type and implemented an unsafe Sync for it. Then I got lifetime issues which I wasn't surprised to see.

Thoughts? Is there something I'm missing? I feel like I'm doing something I shouldn't be doing as this is feeling way difficult. I'm currently stumped and have been considering just using a larger pin variant of the stm32. However I'd really love to get the expander working!

Thanks for taking the time! :D

For the sake of completion here's a fully "working" example that does not compile.

#![no_main]
#![no_std]

use bluepill_rtic_tca9555 as _; // global logger + panicking-behavior + memory layout

use embedded_hal::digital::v2::OutputPin;
use port_expander::Pca9555;
use rtic::app;
use shared_bus::BusManagerSimple;
use stm32f1xx_hal::afio::AfioExt;
use stm32f1xx_hal::flash::FlashExt;
use stm32f1xx_hal::gpio::GpioExt;
use stm32f1xx_hal::i2c::{BlockingI2c, DutyCycle, Mode};
use stm32f1xx_hal::rcc::RccExt;
use systick_monotonic::fugit::{ExtU64, RateExtU32};
use systick_monotonic::Systick;

const CLOCK_HZ: u32 = 36_000_000;
const CRYSTAL_HZ: u32 = 8_000_000;
const TASK_TIMER_HZ: u32 = 1000;

pub struct Led<Pin>
where
    Pin: OutputPin,
{
    pin: Pin,
}

impl<Pin> Led<Pin>
where
    Pin: OutputPin,
{
    #[must_use]
    pub fn new(pin: Pin) -> Self {
        Led { pin }
    }

    pub fn on(&mut self) {
        self.pin.set_high().ok();
    }

    pub fn off(&mut self) {
        self.pin.set_low().ok();
    }
}

#[app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [SPI1])]
mod app {

    use super::*;

    #[monotonic(binds = SysTick, default = true)]
    type MonoTimer = Systick<TASK_TIMER_HZ>;

    #[local]
    struct Local {}

    #[shared]
    struct Shared {
        // LED using pin from the MCU.
        led_mcu: Led<Pin>, // ERROR

        // LEDs usin pins from GPIO expander.
        led_ex1: Led<Pin>, // ERROR
        led_ex2: Led<Pin>, // ERROR
    }

    #[init]
    fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
        // Setup Clocks

        let mut flash = ctx.device.FLASH.constrain();
        let rcc = ctx.device.RCC.constrain();

        let clocks = rcc
            .cfgr
            .use_hse(CRYSTAL_HZ.Hz())
            .sysclk(CLOCK_HZ.Hz())
            .pclk1(CLOCK_HZ.Hz())
            .freeze(&mut flash.acr);

        // Initialize Systick

        let mono = Systick::new(ctx.core.SYST, CLOCK_HZ);

        // Init Expanders

        let mut afio = ctx.device.AFIO.constrain();
        let mut gpio_a = ctx.device.GPIOA.split();
        let mut gpio_b = ctx.device.GPIOB.split();

        let scl = gpio_b.pb8.into_alternate_open_drain(&mut gpio_b.crh);
        let sda = gpio_b.pb9.into_alternate_open_drain(&mut gpio_b.crh);

        let i2c = BlockingI2c::i2c1(
            ctx.device.I2C1,
            (scl, sda),
            &mut afio.mapr,
            Mode::Fast {
                frequency: 100_000.Hz(),
                duty_cycle: DutyCycle::Ratio2to1,
            },
            clocks,
            1000,
            10,
            1000,
            1000,
        );

        let bus = BusManagerSimple::new(i2c);

        let mut ex1 = Pca9555::new(bus.acquire_i2c(), false, false, false);
        let mut ex2 = Pca9555::new(bus.acquire_i2c(), false, false, true);

        let pins_ex1 = ex1.split();
        let pin_ex1_00 = pins_ex1.io0_0.into_output().unwrap();

        let pins_ex2 = ex2.split();
        let pin_ex2_00 = pins_ex2.io0_0.into_output().unwrap();

        let pin_mcu_00 = gpio_a.pa0.into_push_pull_output(&mut gpio_a.crl);

        // Init LEDs

        let led_mcu = Led::new(pin_mcu_00);
        let led_ex1 = Led::new(pin_ex1_00);
        let led_ex2 = Led::new(pin_ex2_00);

        // The type I get for `led_ex1` and `led_ex2`.
        //
        // Unformatted:
        //
        // Led<port_expander::Pin<'_, port_expander::mode::Output, NullMutex<port_expander::dev::pca9555::Driver<I2cProxy<'_, NullMutex<stm32f1xx_hal::i2c::BlockingI2c<I2C1, (stm32f1xx_hal::gpio::Pin<'B', 8, stm32f1xx_hal::gpio::Alternate<stm32f1xx_hal::gpio::OpenDrain>>, stm32f1xx_hal::gpio::Pin<'B', 9, stm32f1xx_hal::gpio::Alternate<stm32f1xx_hal::gpio::OpenDrain>>)>>>>>>>
        //
        //
        // Formatted:
        //
        // Led<
        //     port_expander::Pin<
        //         '_,
        //         port_expander::mode::Output,
        //         NullMutex<
        //             port_expander::dev::pca9555::Driver<
        //                 I2cProxy<
        //                     '_,
        //                     NullMutex<
        //                         stm32f1xx_hal::i2c::BlockingI2c<
        //                             I2C1,
        //                             (
        //                                 stm32f1xx_hal::gpio::Pin<
        //                                     'B',
        //                                     8,
        //                                     stm32f1xx_hal::gpio::Alternate<
        //                                         stm32f1xx_hal::gpio::OpenDrain,
        //                                     >,
        //                                 >,
        //                                 stm32f1xx_hal::gpio::Pin<
        //                                     'B',
        //                                     9,
        //                                     stm32f1xx_hal::gpio::Alternate<
        //                                         stm32f1xx_hal::gpio::OpenDrain,
        //                                     >,
        //                                 >,
        //                             ),
        //                         >,
        //                     >,
        //                 >,
        //             >,
        //         >,
        //     >,
        // >;

        blink::spawn().ok();

        (
            Shared {
                led_mcu,
                led_ex1,
                led_ex2,
            },
            Local {},
            init::Monotonics(mono),
        )
    }

    #[task(shared = [led_mcu, led_ex1, led_ex2], local = [count: u8 = 0])]
    fn blink(ctx: blink::Context) {
        let led_mcu = ctx.shared.led_mcu;
        let led_ex1 = ctx.shared.led_ex1;
        let led_ex2 = ctx.shared.led_ex2;

        (led_mcu, led_ex1, led_ex2).lock(|led_mcu, led_ex1, led_ex2| {
            if *ctx.local.count % 2 == 0 {
                led_mcu.on();
                led_ex1.on();
                led_ex2.on();
            } else {
                led_mcu.off();
                led_ex1.off();
                led_ex2.off();
            }
        });

        *ctx.local.count += 1;

        blink::spawn_at(monotonics::now() + 1.secs()).ok();
    }
}

Migrate to embedded-hal 1.0.0

Hello,
thanks for the crate. ;-) Just recently played with it and it works nicely. I was going to use it, but then for some other reasons I migrated to embedded-hal 1.0.0 and my OutputPin/InputPin trait integration failed.

I started looking into migrating it to 1.0.0. Maybe it's not that hard, but I don't know if I'll be able to do it so I'll leave a bug. 1.0.0 is pretty recent - only 19 days. But alphas started appearing 2 years ago.

edit: Found a guide: https://github.com/rust-embedded/embedded-hal/blob/master/docs/migrating-from-0.2-to-1.0.md

Support port expander interrupts

I also wanted to add a PortDriverInterruptChange trait that would allow the user to wait on one or multiple pins changing state.

My approach:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterruptChangeError<EI, EP> {
    I2cError(EI),
    PinError(EP),
}

pub trait PortDriverInterruptChange: PortDriver {
    /// Wait until at least one pin in `mask` switches
    /// to the state indicated by the corresponding bit in `target_state`.
    ///
    /// If a pin already has the target state, it will not trigger an interrupt,
    /// unless it first switches to the opposite state.
    ///
    /// Returns the bits that switched to the state indicated in `target_state`
    ///
    /// Arguments:
    /// * `int_pin` - The mcu pin to wait for an interrupt by the port expander
    /// * `mask` - The port expander pins to wait for a level change on
    /// * `target_state` - For each port expander pin, the state to wait for.
    /// A 1 in the mask indicates a high level, a 0 indicates a low level.
    async fn wait_for_change_to<P>(
        &mut self,
        int_pin: P,
        mask: u32,
        target_state: u32,
    ) -> Result<u32, InterruptChangeError<Self::Error, P::Error>>
    where
        P: embedded_hal_async::digital::Wait;
}

(API is based on what the PI4IOE5V6408 offers. We could add other traits for edge detection later)

My idea was to make this accessible via:

  • A wait_for_change_to method for each pin
  • A wait_change_multiple method (similar to read_multiple)

Problem: We can not keep the driver locked, while awaiting an async function.
This would require an extension of the shared-bus crate to either allow passing an async closure to the lock method, or have a lock method that returns a guard.

Before I do this, I wanted to ask you, if I'm on the right track here, or whether you have a different solution in mind regarding interrupts.

Originally posted by @t-moe in #17 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.