I’m trying to run an LED matrix display (with a Max7219 controller) from a raspberry pi pico using rust. There is a max7219-crate that I used. But i am unsure about how to prepare the pins I want to use. Can I Use any of the pins? Do I have to set them to push-pull-output?

  • orclev@lemmy.world
    link
    fedilink
    English
    arrow-up
    5
    ·
    edit-2
    11 months ago

    Looks like that crate uses the standard embedded-hal abstractions. There are two functions you want to look at on the MAX7219 struct, from_pins and from_spi depending on whether you’re using hardware SPI or bit bashing. If you notice the signature for from_pins specifies they must be OutputPin instances.

    If you don’t already have a HAL crate for the pico you should first grab that and familiarize yourself with the process of initializing the hardware and the HAL.

    Edit: for reference: https://crates.io/crates/rp-pico Check the examples from that crate for how to setup pins/SPI.

    • exocortex@discuss.tchncs.deOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      11 months ago

      Hey! Thank you very much! It’s very cool to see that there’s still people around here :-)

      Yes, i am using the rp-pico-hal. I guess I have to somehow activate the spi-funcrtonality first then, right?

      I found an example where someone uses a Max7219-attached 8x8 Matrix with a Pico in rust. But in this example the MAX7219-crate isn’t used. Instead some SPI-structs/functionality is written directly in the main. This example managed to write stuff on my LED-matrix.

      I will read more into the stuff you mentioned. Thanks a lot!

      • orclev@lemmy.world
        link
        fedilink
        English
        arrow-up
        3
        ·
        edit-2
        11 months ago

        If you follow that link I provided there’s an example in there of exactly how to initialize SPI. The example uses it to control a MMC, but most of the setup (except the speed) should be essentially identical for you.

        Here’s a direct link to the relevant example: SPI SD Card example

        Also the example code on the crate itself is a pretty good place to start. It’s using bit bashing rather than hardware SPI, but if you follow everything after the from_pins call and use most of the stuff from the SPI SD card example but init the MAX7219 using from_spi instead you should be up an running in no time.

      • exocortex@discuss.tchncs.deOP
        link
        fedilink
        English
        arrow-up
        1
        ·
        11 months ago

        I realize that I mixed two things together. Or misunderstood what spi is. I thought using the pins to communicate to another chip (the max7219) is the same as “spi”. But it isn’t.

        The sd-card example is a little too big for me to understand right now. I see that apparently I can use the max7219 via pins or via spi. Does this mean, that SPI is preferrable? ( im imagine with the pins I have to set the pin high and low manually, whereas with the spi i can use more abstract commands like “send out this byte”.

        • orclev@lemmy.world
          link
          fedilink
          English
          arrow-up
          3
          ·
          edit-2
          11 months ago

          OK, so SPI is a bus protocol, although when you’re talking that low level that’s almost overselling it. With SPI you have a clock signal on one pin, a device select signal on another pin, and then two unidirectional signal pins, MOSI (master out, slave in) and MISO/SOMI (master in, slave out, this pin is often unused in SPI if the device being talked to is write only). The big advantage to this setup is that you can read and write from devices at the same time since the read and write pins are separate. The other tradeoff with SPI is that adding additional devices requires one extra pin per device (for the device select signal, usually identified as CS). The way you route signals to a particular device is by driving the CS pin low for the device you want to communicate with. So you need 4 pins for 1 device, 5 pins for 2 devices, 6 pins for 3, etc.

          It’s is entirely possible to implement SPI communication using software, you just manually flip the CLK and MOSI pins to send your messages (and read from the MISO pin). That’s usually referred to as bit bashing. Most microcontrollers though also include special hardware for managing SPI for you where you can set a desired clock speed and write/read from buffers and the controller will handle flipping the CLK and MOSI pins for you and filling the buffers.

          The other major bus protocol you’ll typically run into for embedded devices is I2C (or IIC). I2C only uses 2 or 3 pins, a clock pin, a command pin, and sometimes a data pin. I2C operates by assigning each device on the bus a unique address, and as part of each message sent on the bus the target devices address is included. The downside to this design is that reading and writing is often significantly slower than what’s possible with SPI, but the upside is that you can have a nearly unlimited number of devices all driven by just 2 or 3 pins. Just like with SPI many microcontrollers will include hardware specifically for managing I2C communications, but you can also just bit bash your messages across the bus as well.

          No matter how you decide to configure things with the MAX7219 it’s using SPI, it’s just the difference of whether you’re “manually” implementing SPI where there’s code running on the main CPU that’s sending the messages, or if the actual sending is being handled by some dedicated SPI hardware and the main CPU is just writing to buffers. The library itself is handling the SPI communications when you pass it the pins, so you don’t need to actually implement SPI yourself, the library handles it for you, but it is going to end up being noticeably slower than if you initialize the SPI module on the rp-pico and let its dedicate SPI hardware manage it instead.

          Looking at the SPI example there’s this chunk of code:

              let clocks = hal::clocks::init_clocks_and_plls(
                  rp_pico::XOSC_CRYSTAL_FREQ,
                  pac.XOSC,
                  pac.CLOCKS,
                  pac.PLL_SYS,
                  pac.PLL_USB,
                  &mut pac.RESETS,
                  &mut watchdog,
              )
              .ok()
              .unwrap();
          

          That’s initializing the system clocks in the rp-pico. You should be doing this no matter what and this doesn’t really have anything to do with SPI except in that SPI uses the system clocks.

          Then there’s this chunk here:

              let _spi_sclk = pins.gpio2.into_mode::();
              let _spi_mosi = pins.gpio3.into_mode::();
              let _spi_miso = pins.gpio4.into_mode::();
              let spi_cs = pins.gpio5.into_push_pull_output();
          

          That block is configuring a few pins to be usable with the hardware SPI driver and configuring one final pin as a CS pin.

          Edit: N.B. this chunk is getting mangled by lemmy for some reason, it keeps removing the turbofish operators. You can follow this link to see what I’m talking about. Additionally you can reference this chart to see what pins on the rp-pico support which hardware functions. GPIO 0 through 7 and 16 to 19 all support the SPI0 hardware driver, and GPIO 8 through 15 support SPI1.

          Lastly there’s this chunk:

              let spi = spi::Spi::<_, _, 8>::new(pac.SPI0);
              let spi = spi.init(
                  &mut pac.RESETS,
                  clocks.peripheral_clock.freq(),
                  400.kHz(), // card initialization happens at low baud rate
                  &embedded_hal::spi::MODE_0,
              );
          

          That’s using the first hardware SPI unit (SPI0) and initializing it. It’s setting it to run at 400kHz and in Mode 0 (SPI loosely defines a couple different ways communication can be implemented, usually referred to as mode 0 through 4). The clock speed and mode that need to be configured will vary based on the device. For the MAX7219 it looks like it wants to run at a max of 1mHz and mode 0.

          Once you have the MAX7219 struct returned from the from_spi_cs function you can basically just ignore SPI entirely and just use the functions on the MAX7219 driver to send messages to the display.

          • exocortex@discuss.tchncs.deOP
            link
            fedilink
            English
            arrow-up
            3
            ·
            11 months ago

            Hey! Thank you very much! I’ve actually managed to get the SPI to work with the max7219. I initialized it with <,,16> as template args. It only compiled when i changed the 16 to 8. (it was 16 in another example). It took me a long time to figure out, because i couldn’t find documentation about what this parameter means - it’s simply called “DS” which i interpreted as “Data Size” but I could be wrong.

            I guess the main difficulty in the HAL-Code is reading the heavily generic types used.

            i don’t want to get on your nerves, so don’t answer if it’s annoying, I will find some answers eventually. But I think I will continue to ask questions here, maybe some other people will see it and frequent this community more often.

            I’ve got some weird issues now that it isn’t behaving like i would expect, but that may have to do with power or something else. Maybe some transmission errors? Could a lower SPI-frequency reduce errors in transmission… (my example uses 1MHz, yours 400kHz).

            • orclev@lemmy.world
              link
              fedilink
              English
              arrow-up
              2
              ·
              edit-2
              11 months ago

              Edit: OK, so Lemmy keeps stripping all the angle brackets out of my comments which makes posting any code that uses generics really hard/impossible. To work around that I’m just going to link to a gist of this post the way it’s supposed to look.

              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 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: OutputPin,
                  CS: OutputPin,
                  SCK: OutputPin,
              

              at compile time that actually ends up looking more like PinConnector,Pin,Pin> 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 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 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.

              • exocortex@discuss.tchncs.deOP
                link
                fedilink
                English
                arrow-up
                2
                ·
                edit-2
                11 months ago

                Hey! Thank you very much! This is an incredibly well made, probably labor-intensive and (nice!) comment! (and yeah a few code-pieces seem to disappear, but i think i understand the original meaning.

                that cleared a lot up to be honest. I have been using rust for a while now, but i think all the more advanced features that i didn’t really have to deep-dive into before are now used all at once in the embedded context. it’s all very dense to read when only looking into the source code (or the docs). But your explanations helped tremendously (i will read them again tomorrow though.

                It’s really fascinating what rust makes possible here. I haven’t really programmed too much in c++ in the embedded context, but i guess i would have to basically rewrite a lot of software if i want to use it on a different device, right?

                Regarding the 8 or 16 values of the DS-values, i am not quite sure myself. I’ve found two examples where a Max7219-chip is used together with a raspberry Pi pico with Rust. One implemented the max7219-struct itself and didn’t use the max7219-crate and used the value 16 for DS… This example works on my setup.

                The other example is using the max7219 and it needs DS=8 otherwise it doesn’t compile. It kinda works, but there seems to be some errors when i use it: if I use write_raw to set all the pixels on the display certain values seem to change the display’s state. at a certain point it changes its intensity and changes into all-pixels-on-mode suddenly. This shouldn’t happen if i only use wrote_raw.

                But with your explanations i might understand a little more of the stuff that i used in the code. Thank you very much!

                • orclev@lemmy.world
                  link
                  fedilink
                  English
                  arrow-up
                  1
                  ·
                  edit-2
                  11 months ago

                  Honestly I’m suspecting that the driver crate is just broken, and that it is supposed to be using a value of 16 for the DS parameter. The trait constraint the from_spi function should have applied should be Write with a u16 generic, not u8 which would then allow you to use 16 as the DS parameter when initializing the Spi instance. If I had a Max7219 chip at hand I would try modifying the driver crate to verify if that’s the case, but I don’t unfortunately. Maybe open an issue on the driver repo describing the behavior you’re seeing (and maybe link him back to this thread) to see what he thinks?

                  As for the case with C++ code it is often more device specific, but it can also cheat a certain amount. Rust is all about safety, it doesn’t let you make a bunch of mistakes that are possible in C++. The upshot of that is that when you get a piece of Rust code to compile, it’s more often than not correct. That’s somewhat on the skill of the person writing the libraries though, you can certainly write code that can be used wrong, but a good author can often define their APIs in such a way that it’s impossible to use it incorrectly. As in the example above, the Spi instance is being constrained to a DS of 8 due to the way the Max7219 crate is defined, it’s impossible to accidentally use a DS of 16 with it, it just happens that it seems like that constraint is wrong in this case.

                  C++ in contrast lets you take shortcuts. For instance you can define a bunch of constants and use ifdefs to conditionally set them at compile time. For example you can see this random driver I found using a google search that it defines the Max7219 class as taking a PinName class/struct/enum (not sure which honestly) which I’m sure is defined elsewhere as the raw pin identifier constant exposed by the underlying hardware. That driver for instance does not enforce that the pin has been configured into the proper PushPull mode prior to it being passed to the driver, it’s on you as the user of the library to make sure everything has been properly setup before hand. It’s “easier” in that everything is basic, but it’s also error prone as it doesn’t double check your work, you’ll just get a crash at runtime.

                  C/C++ is very low level, barely higher than assembly. If you’re armed with the datasheets for everything you can probably make it work, but you need to be very sure you’re getting all the details right. Rust on the other hand tries to force you to use things correctly. Ideally you should have just been able to grab the Max7219 crate, and just use it and everything would work. The fact it isn’t suggests there’s a possible bug in the crate, rather than that you’re just using it wrong, as it really should be impossible to use it wrong.

  • Kalcifer@lemmy.world
    link
    fedilink
    English
    arrow-up
    2
    ·
    edit-2
    11 months ago

    Yes, this community is stil alive.

    I don’t really have an exact answer to your question, as I don’t write Rust, and I’ve never used a Raspberry Pi for electronics, but I have some educated input:

    There is a max7219-crate that used. But i am unsure about how to prepare the pins want to use.

    Presumably, the crate just exposes pre-defined config objects that you call in your code.

    Can Use any of the pins?

    From what I recall, no, you can’t use any of the pins on the Raspberry Pi as generic IO (all this information that you are looking for is in the datasheets of the devices that you are using) – some pins are dedicated for power, etc.

    Do have to set them to push-pull-output?

    Generally speaking, yes, if you want to use a generic I/O as an output, then you must configure it as such.