Skip to content

Instantly share code, notes, and snippets.

@orclev

orclev/post.md Secret

Last active August 7, 2023 00:23
Show Gist options
  • Save orclev/03103f3b44c6e2355b6a53244f1b1b0b to your computer and use it in GitHub Desktop.
Save orclev/03103f3b44c6e2355b6a53244f1b1b0b to your computer and use it in GitHub Desktop.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment