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
xDynFunction
xDynPullType
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 someunsafe
trickery withMaybeUninit
(or raw pointers), but better choice is to use safe wrappers likeheapless::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, } } }
- note this "sum" type is essentially a subset of the fully dynamic pin type with
-
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 orheapless
crate. this example usesheapless::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.