@wpd, I think I can help a little bit with your latest problem. The issue relates strongly to the type-level programming I mentioned earlier.
Simplest solution
The fundamental problem is that each pin in your HAL is defined as a completely separate and distinct type. The PX_XX
types are defined by a macro, so P0_00
is not related to P0_01
at all. Both take a MODE
type parameter to represent the pin mode, but that doesn't tell the compiler anything useful.
Your problem is that you want to pass different pins to a common function like
fn blink(pin: ???, timer: &mut Timer<TIMER0>) {
pin.set_low().unwrap();
timer.delay();
pin.set_high().unwrap();
}
But what should the type of pin
be?
With the nrf
approach, there's no immediate solution using the HAL types. P0_00
and P0_01
are totally different. To do this, you would have to use the embedded-hal
traits directly, i.e.
fn blink<P: OutputPin>(pin: P, timer: &mut Timer<TIMER0>) {
pin.set_low().unwrap();
timer.delay();
pin.set_high().unwrap();
}
This is probably the most straightforward solution to your problem. However, I'll expand a little bit more, in case you're interested.
Our approach in atsamd-hal
In atsamd-hal
, we previously used the same macro-based approach, until about a year ago. We eventually transitioned to a different approach that makes better use of type-level programming in Rust.
In atsamd-hal
, we have a Pin
type that is generic over two type parameters, one for the PinMode
(just like the nrf
HAL), and a second for the PinId
. With this approach, all pins are actually the same type, just with different type parameters. That lets you do cool things, like define the AnyPin
trait.
With this approach, we don't need to use a completely generic type P: OutputPin
. We can still use HAL types.
fn blink<I: PinId>(
pin: &mut Pin<I, PushPullOutput>,
timer: &mut Timer<TIMER0>,
) {
pin.set_low().unwrap();
timer.delay();
pin.set_high().unwrap();
}
For this specific case, it's not much different. But in general, it's more powerful than the macro-based approach.
Value-level approach
That covers the completely type-level approach, but there's a second approach which is used by both the nrf
HAL and atsamd-hal
. Both of our HALs also define value-level pins.
In atsamd-hal
, we call them DynPin
s. Each DynPin
stores a DynPinId
and a DynPinMode
, which are value-level versions of the type-level alternatives above.
DynPin
has no type parameters, so every instance of DynPin
is considered the same type by the compiler. That lets you define things like arrays of DynPin
, e.g. [DynPin; 5]
. That would not be possible with our Pin
struct, because each Pin<I, M>
has a different type parameter I
for its PinId
.
The nrf
HAL also has a value-level pin type. They call it Pin
. But their Pin
type is different from our DynPin
type. Their version is partially type-level and partially value-level. It stores a u8
for the pin number, but it still takes a MODE
type parameter for the pin mode. This is still useful, though, because it lets you create arrays of Pin
objects, as long as they are all in the same pin mode, e.g. [Pin<Output<PushPull>>; 5]
.
It looks like the DisplayPins
struct has a degrade
method that will convert all of the PX_XX
types into Pin
types. Rather than store the DisplayPins
, you could instead call degrade
and store the two Pin
arrays. That would let you use an index to access the row or column of interest.
fn blink_column(&mut self, index: usize) {
self.columns[index].set_high().unwrap();
self.delay();
self.columns[index].set_low().unwrap();
}
Note, however, that there is a slight run-time cost to atsamd-hal
's DynPin
or nrf
's Pin
. Whereas the type-level structs are completely zero-sized and do not exist at run-time, the value-level versions have a non-zero size and do exist at run-time. It's a pretty minor cost, just a few bytes, but it's something to keep in mind.