Rp2040_hal crate impossible to use

I may be missing something, but I'm at a loss to understand why the rp2040_hal crate authors decided to make every GPIO pin have a unique type. My application needs to be able to iterate over an array of pins, and dynamically configure them as input or output. But they can't be put into an array unless they are converted to DynPins, and then they can't be reconfigured. Am I even using the best crate for the job? Grateful for any explanations or hints.

to fully utilize the rust type system for the common case where the configuration of the io pins are statically known at compile time, this is not just for the rp2040, it's an idiom for most rust bare metal target libraries.

that's exactly the intended use cases.

for dynpins, it's just the bank and pin numbers are not tracked by their types, but carried as runtime information, but the functions are still encoded in the types.

any io pin can only be reconfigured to compatible functions. this is true also for dynpins.

rust arrays are homogenous, it cannot hold values of different types. for examle, using dynpins, you can an array of push-pull SIO output pins, (e.g. to drive many LEDs), but you cannot have an array contains both SIO and UART pins. you need other types (possibly a custom defined struct) other than array.

hard to way, it depends on your use cases. the rp2040-hal isn't for everyone. it's totally possible your use case isn't covered by rp2040-hal. and it's also possible the community don't have any suitable ready-to-use libraries, in which case you can write your own peripheral drivers. that's just the nature of a community driven ecosystem.

but even if the rp2040-hal library cannot be used directly, more than often you can create custom wrapper types to model your problem and don't need to implement everything from scratch. in any case, I don't suggest to circumvent the type system (using unsafe, for example).

all in all, I think I don't really understand what you are trying to do exactly. could you please show some examples?

Thanks, @nerditation, for your prompt and comprehensive reply.

I guess, for my current use, I can put (refs to) all GPIO pins, configured as inputs, in one array, then ditto for all pins, as outputs, in another; then select the subsets of each that I want and copy into two more arrays, which can then be iterated over in the main loop.

I did have a look for alternative crates, without much success. Totally agree about avoiding 'unsafe'.

this is exactly the situation the design of the types want to prevent: to have a single pin represented as multiple variables. a pin can be either an input, or an output, as encoded in its type, but NOT BOTH at the same time.

if you want to toggle the input/output direction at runtime, I can think of two approaches:

  • method 1:

    create a sum type (i.e. enum) of input pin and output pin for predefined function and pull type.

    • note this "sum" type is essentially a subset of the fully dynamic pin type with DynPinId x DynFunction x DynPullType

    the code looks something like:

    // for demo purpose, I hard coded the `Function` and`PullType` to arbitrary types
    // you may want to use generics if it better suits your use case
    enum MyPin {
        I(Pin<DynPinId, FunctionSio<SioInput>, PullNone>),
        O(Pin<DynPinId, FunctionSio<SioOutput>, PullUp>)
    }
    

    this approach allow you put every pin into a single array, you just need to check the variant (the match keyword in rust) at runtime before operating each pin. basically you can iterate in two style:

    // style 1: one pass, check input/output variant inside loop
    for pin in all_pins.iter_mut() {
      match pin {
          MyPin::I(ref pin) => {
              // `pin` has input type
          }
          MyPin::O(ref pin) => {
              // `pin` has output type
          }
      }
    }
    // style 2: two pass, each filtered for single type
    for pin in all_pins.iter_mut().filter_map(|pin| pin.as_input()) {
        // `pin` is input pin
    }
    for pin in all_pins.iter_mut().filter_map(|pin| pin.as_output()) {
        // `pin` is ouput pin
    }
    impl MyPin {
        // `as_input()` type signature and implementation, `as_output()` omitted
        fn as_input(&mut self) -> Option<&mut Pin<DynPinId, FunctionSio<SioInput>, PullNone>> {
            match self {
               MyPin::I(pin) -> Some(pin),
               MyPin::O(_) -> None,
            }
        }
    }
    

    to convert between input/output pins, you can do it safely with Option<MyPin>, which add some runtime overhead; or you can do some unsafe trickery with MaybeUninit (or raw pointers), but better choice is to use safe wrappers like heapless::Vec so you don't need to do it yourself:

    // with `Option<MyPin>`
    let mut all_pins = [Some(MyPin::I(...)), Some(MyPin::O(...)), ...];
    // with `MaybeUninit`
    let mut all_pins = [MaybeUninit::new(MyPin::I(...)), MaybeUninit::new(MyPin::O(...)), ...];
    // with `heapless::Vec`
    let mut all_pins = Vec::<MyPin, 8>::new();
    all_pins.push(MyPin::I(...));
    all_pins.push(MyPin::O(...));
    
    impl MyPin {
        // reconfigure into input pin
        fn into_input(self) -> Self {
            match self {
                MyPin::O(pin) -> MyPin::I(pin.into_floating_input()),
                pin -> pin,
            }
        }
    }
    
  • method 2

    put pins of different types into separate arrays:

    when reconfigure a pin, you simply move the pin from one array to another. (instead of arrays, you may want to use the "Vec"-like apis from the arrayvec crate or heapless crate. this example uses heapless::Vec),

    // capacity for the "Vec"
    const CAPACITY: usize = 8; 
    // you can give explicit pin type, but often it can be inferred
    let mut input_pins = Vec::<_, CAPACITY>::new();
    let mut output_pins = Vec::<_. CAPACITY>::new();
    // assume gpio is already initialized, configure them as needed:
    input_pins.push(gpio.gpio22.into_floating_input().into_dyn_pin());
    input_pins.push(gpio.gpio23.into_floating_input().into_dyn_pin());
    output_pins.push(gpio.gpio3.into_push_pull_output().into_pull_type::<PullUp>().into_dyn_pin());
    output_pins.push(gpio.gpio5.into_push_pull_output().into_pull_type::<PullUp>().into_dyn_pin());
    

    at runtime, if you want to reconfigure one pin to different direction, you do it like this:

    // example to convert gpio22 from input to output, first search using pin id
    let idx = input_pins.iter().position(|pin| pin.id().num == 22).unwrap();
    // `swap_remove()` is more efficient, but doesn't preserve original order
    // if order is important, use `remove()`
    let pin = input_pins.swap_remove(idx);
    // put it into the output array. already `DynPinId`, so no need `into_dyn_pin()`
    // use `insert()` if order is important for your application
    output_pins.push(pin.into_push_pull_output().into_pull_type::<PullNone>());
    

of course there's other methods and variants, but in generall, I think the second approach is preferred and recommended, it is much simpler to implement and to understand.

Thanks again for all the suggestions.

impl MyPin {
    // reconfigure into input pin
    fn into_input(self) -> Self {
        match self {
            MyPin::O(pin) => MyPin::I(pin.into_floating_input()),
            pin => pin,
        }
    }
}

This runs into the same problem that I was having:

the trait `ValidFunction<FunctionSio<SioInput>>` is not implemented for `DynPinId`

However, I don't actually need to reconfigure pins, so that is not a problem. I just need to be able to configure each pin once, but dynamically, without knowing in advance which pins will be which. So I also need an "uncommitted pin" enum variant.

The arrayvec crate and heapless::Vec sound very useful, I'll certainly check them out.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.