GPIO and the LED matrix

We will now configure and program our LED matrix. It uses 13 GPIO on three different ports.

HAL and peripherals

The embassy_stm32::init() function that you have used earlier returns a value of type Peripherals. This is a large structure which contains every peripheral available on the microcontroller.

❎ Store the peripherals in a variable named p:

    let p = embassy_stm32::init(config);

In this variable, you will find for example a field named PB0 (p.PB0). This field has type embassy_stm32::peripherals::PB0. Each pin will have its own type, which means that you will not use one instead of another by mistake.

HAL and GPIO configuration

A pin is configured through types found in the embassy_stm32::gpio module. For example, you can configure pin PB0 as an output with an initial low state and a very high commuting speed by doing:

   // pin will be of type Output<'_>
   let mut pin = Output::new(p.PB0, Level::Low, Speed::VeryHigh);
   // Set output to high
   pin.set_high();
   // Set output to low
   pin.set_low();

If pin is dropped, it will be automatically deconfigured and set back as an input.

🦀 The lifetime parameter 'a in Output<'a> represents the lifetime of the pin that we have configured as output. In our case, the lifetime is 'static as we work directly with the pins themselves. But sometimes, you get the pin from a structure which has a limited lifetime, and this is reflected in 'a.

Matrix module

❎ Create a public matrix module.

❎ In the matrix module, import embassy_stm32::gpio::* as well as tp_led_matrix::{Color, Image} (from your library) and define the Matrix structure. It is fully given here to avoid a tedious manual copy operation, as well as all the functions you will have to implement on a Matrix:

pub struct Matrix<'a> {
    sb: Output<'a>,
    lat: Output<'a>,
    rst: Output<'a>,
    sck: Output<'a>,
    sda: Output<'a>,
    rows: [Output<'a>; 8],
}

impl Matrix<'_> {
    /// Create a new matrix from the control registers and the individual
    /// unconfigured pins. SB and LAT will be set high by default, while
    /// other pins will be set low. After 100ms, RST will be set high, and
    /// the bank 0 will be initialized by calling `init_bank0()` on the
    /// newly constructed structure.
    /// The pins will be set to very high speed mode.
    #[allow(clippy::too_many_arguments)]   // Necessary to avoid a clippy warning
    pub fn new(
        pa2: PA2,
        pa3: PA3,
        pa4: PA4,
        pa5: PA5,
        pa6: PA6,
        pa7: PA7,
        pa15: PA15, // <Alternate<PushPull, 0>>,
        pb0: PB0,
        pb1: PB1,
        pb2: PB2,
        pc3: PC3,
        pc4: PC4,
        pc5: PC5,
    ) -> Self {
        // Configure the pins, with the correct speed and their initial state
        todo!()
    }

    /// Make a brief high pulse of the SCK pin
    fn pulse_sck(&mut self) {
        todo!()
    }

    /// Make a brief low pulse of the LAT pin
    fn pulse_lat(&mut self) {
        todo!()
    }

    /// Send a byte on SDA starting with the MSB and pulse SCK high after each bit
    fn send_byte(&mut self, pixel: u8) {
        todo!()
    }

    /// Send a full row of bytes in BGR order and pulse LAT low. Gamma correction
    /// must be applied to every pixel before sending them. The previous row must
    /// be deactivated and the new one activated.
    pub fn send_row(&mut self, row: usize, pixels: &[Color]) {
        todo!()
    }

    /// Initialize bank0 by temporarily setting SB to low and sending 144 one bits,
    /// pulsing SCK high after each bit and pulsing LAT low at the end. SB is then
    /// restored to high.
    fn init_bank0(&mut self) {
        todo!()
    }

    /// Display a full image, row by row, as fast as possible.
    pub fn display_image(&mut self, image: &Image) {
        // Do not forget that image.row(n) gives access to the content of row n,
        // and that self.send_row() uses the same format.
        todo!()
    }
}

❎ Implement all those functions.

You can refer to 4SE07 notes for GPIO connections (in French) and the operation of the LED Matrix controller (in French).

Note that you need to maintain the reset signal low for 100ms. How can you do that? Keep reading.

Implementing a delay

Since you do not use an operating system (yet!), you need to do some looping to implement a delay. Fortunately, the embassy-time can be used for this. By cooperating with the embassy-stm32 crate, it will be able to provide you with some timing functionalities:

❎ Add the embassy-time crate as a dependency with feature tick-hz-32_768: this will configure a timer at a 32768Hz frequency, which will give you sub-millisecond precision. You will also have to enable the generic-queue-8 feature since we don't use the full Embassy executor at this stage. Note that embassy-time knows nothing about the microcontroller you use, it needs a timer to run on.

❎ Add the time-driver-any to the embassy-stm32 dependency. This will tell the HAL to make a timer at the disposal of the embassy-time crate.

The Rust embedded working-group has defined common traits to work on embedded systems. One of those traits is the DelayNs in the embedded-hal crate, which is implemented by the embassy_stm32::d::Delay singleton of embassy-time. You can use it as shown below:

❎ Add the embedded-hal dependency.

❎ Import the DelayNS trait in your matrix.rs, as well as the Delay singleton from embassy-time:

use embedded_hal::delay::DelayNs as _;
use embassy_time::Delay;

You can then use the following statement to wait for 100ms:

   Delay.delay_ms(100);

🦀 Note on singletons

Delay is a singleton: this is a type which has only one value. Here, Delay is declared as:

struct Delay;

which means that the type Delay has only one value, which occupies 0 bytes in memory, also called Delay. Here, the Delay type is used to implement the DelayNs trait from the embedded-hal crate:

impl embedded_hal::delay::DelayNs for Delay {
    fn delay_ms(&mut self, ms: u32) { … }
    …
}

You might have noticed that self is not used in delay_ms, but the implementation has to conform to the way the trait has been defined. When you later write Delay.delay_ms(100), you create a new instance (which contains nothing) of the type Delay, on which you mutably call delay_ms(100).

Main program

❎ In your main program, build an image made of a gradient of blue and display it in loop on the matrix. Since it is necessary for the display to go fast, do not forget to run your program in release mode, as we have been doing for a while now. Don't forget that Image values have a .row() method which can be handy here.

Are you seeing a nice gradient? If you do, congratulations, you have programmed your first peripheral in bare board mode with the help of a HAL. 👏

(if not, add traces using defmt)