I'm going to guess you're not super familiar with Rust yet, in which case good job making it this far with embedded Rust, that's kind of the deep end of the pool. The embedded-hal crate that's at the core of all these crates is a really amazing piece of engineering, it walks a fine line between defining a set of primitives that can be used across all embedded devices while also not being so generic as to be useless or so specific as to exclude certain embedded devices from being supported. A big part of how it accomplishes that is by very carefully using traits and generics. Traits are easiest to work with but they have the downside of potentially introducing dynamic dispatch which has runtime overhead, so static dispatch is preferred. A big part of how you avoid dynamic dispatch is using generics.
For a concrete example, we can look at the Pin struct declared by the rp2040-hal crate. The Pin
struct is generic and includes two parameters, an Id that's an instance of PinId which is itself simply a marker trait that can be applied to each GPIO address, and a Mode that's an instance of the PinMode trait which is a marker trait for the various modes each Pin
can be toggled into. Using these you could for instance have an instance of the Pin
struct declared like so Pin<Gpio0,PushPull>
which would indicate the GPIO0 pin that has been configured into PushPull
mode. The PushPull struct is an instance of the marker trait OutputConfig. Going back to the Pin
struct for a moment, we can see that it provides a generic implementation for OutputPin
which is defined for any Pin
whose mode is an instance of OutputConfig
. Using that OutputPin
marker trait then allows writers of drivers, such as the one for the MAX7219 to write a generic implementation that will work for literally any Pin
that's an instance of OutputPin
.
Now an important point in all of that, is that generics are made concrete at compile time. While you see a declaration like this:
pub struct PinConnectorwhere<DATA, CS, SCK>
DATA: OutputPin,
CS: OutputPin,
SCK: OutputPin,
at compile time that actually ends up looking more like PinConnector<Pin<Gpio3,PushPull>,Pin<Gpio5,PushPull>,Pin<Gpio2,PushPull>>
which is declaring that you're using the GPIO pin 3 for MOSI, pin 5 for CS, and pin 2 as clock as well as statically asserting at compile time that they've all been properly configured into output mode. You would for instance get a compile error if you attempted to pass a pin instance like Pin<Gpio3,PullUp>
because PullUp
is an instance of InputConfig
and therefore that Pin
instance is an instance of InputPin
not an instance of OutputPin
as declared by the bounds on the PinConnector
generics.
Now, that does make reading the docs for all this a little tricky, and requires some getting used to, but it's incredibly powerful once you do understand it. One skill you're going to want to get in the habit of to make the most out of the embedded-hal ecosystem is reading blanket and auto trait implementation, they're really the core of what makes the entire thing function.
To make all of these even more complicated, embedded rust docs are only half the picture, the other half is the docs for the specific hardware devices in question. For instance here is the datasheet for the Max7219. Looking at that I can already see I made a mistake in one of my previous comments. I said the max supported speed was 1mHz, but the datasheet actually indicates it's 10mHz, and indeed when I double check the driver docs I linked previously they do in fact say 10mHz, not 1mHz. Based on the datasheet for the Max7219, I would expect that the DS
parameter on the Spi
device should actually be 16
as that's the size of each serialized packet sent over the SPI bus that it's expecting, however I see that the Max7219 driver crate specifies that the Spi instance should be a Write<u8>
which is only defined for Spi
with a DS
value of 8 or lower. I'm guessing maybe there's some quirk of the Max7219 command set that the driver is working around? Not really sure what's going on there honestly.