All Articles

Music Visualization VI: RTFM

To continue from the last post, we need to hook up our LED driver to the actual chip. This post won’t have any new pictures and will be quite code heavy. You have been warned.

The code from this blog post is available here, although it will always be slightly ahead of the actual posts so that I can plan for the next steps :)

Let’s actually hook up our LED matrix driver to the RTFM (RealTime For the Masses) library. For the initial driver glue, we can leverage the excellent embedded-hal types to still keep our code device independent.

$ cargo add embedded-hal

Let’s first implement the traits that we defined last time. This should be pretty straight-forward. Regarding the LayerSelector trait, we can just map that to 4 GPIO pins like so:

pub struct GpioLayerSelector<E, A0, A1, A2, A3>
where
    A0: embedded_hal::digital::v2::OutputPin<Error = E>,
    A1: embedded_hal::digital::v2::OutputPin<Error = E>,
    A2: embedded_hal::digital::v2::OutputPin<Error = E>,
    A3: embedded_hal::digital::v2::OutputPin<Error = E>,
{
    layer_address0: A0,
    layer_address1: A1,
    layer_address2: A2,
    layer_address3: A3,
}

Each pin is responsible for one bit of the layer address. We also require that all pins use the same error type to make error handling a bit easier (and for most of the devices this would run on, the error type would be infallible anyway). The reason for storing each pin in a separate field instead of e.g. using an array type is that in practice, each pin will have a different type, and storing the pins with dynamic dispatch (i.e. Box<dyn OutputPin>) would prevent necessary code inlining which we will need for the cube to perform well.

It’s easy to implement the LayerSelector trait for this type. Simply check each bit of the layer address and toggle each GPIO output pin accordingly:

impl<E, A0, A1, A2, A3> LayerSelector for GpioLayerSelector<E, A0, A1, A2, A3>
where
    A0: embedded_hal::digital::v2::OutputPin<Error = E>,
    A1: embedded_hal::digital::v2::OutputPin<Error = E>,
    A2: embedded_hal::digital::v2::OutputPin<Error = E>,
    A3: embedded_hal::digital::v2::OutputPin<Error = E>,
{
    type Error = E;

    #[inline]
    fn select_layer(&mut self, layer: usize) -> Result<(), Self::Error> {
        if layer & 0b0001 != 0 {
            self.layer_address0.set_high()?;
        } else {
            self.layer_address0.set_low()?;
        }

        if layer & 0b0010 != 0 {
            self.layer_address1.set_high()?;
        } else {
            self.layer_address1.set_low()?;
        }

        if layer & 0b0100 != 0 {
            self.layer_address2.set_high()?;
        } else {
            self.layer_address2.set_low()?;
        }

        if layer & 0b1000 != 0 {
            self.layer_address3.set_high()?;
        } else {
            self.layer_address3.set_low()?;
        }

        Ok(())
    }
}

It would be great for embedded-hal to offer a set(bool) method for output pins, but alas that doesn’t exist.

For sending data to the shift registers, we need a slightly more involved type.

pub struct GpioDataBus<E, DA, DB, DC, CLK, STK, OE>
where
    DA: embedded_hal::digital::v2::OutputPin<Error = E>,
    DB: embedded_hal::digital::v2::OutputPin<Error = E>,
    DC: embedded_hal::digital::v2::OutputPin<Error = E>,
    CLK: embedded_hal::digital::v2::OutputPin<Error = E>,
    STK: embedded_hal::digital::v2::OutputPin<Error = E>,
    OE: embedded_hal::digital::v2::OutputPin<Error = E>,
{
    data_a: DA,
    data_b: DB,
    data_c: DC,
    clk: CLK,
    stk: STK,
    oe: OE,
}

We have the data pins for register A, B and C, a shared clock pin, and the Set/Output Enable pins. The latter two are easy to implement, except that OE is inverted:

    #[inline]
    fn set_stk(&mut self, stk: bool) -> Result<(), Self::Error> {
        if stk {
            self.stk.set_high()
        } else {
            self.stk.set_low()
        }
    }

    #[inline]
    fn set_oe(&mut self, oe: bool) -> Result<(), Self::Error> {
        if oe {
            self.oe.set_low()
        } else {
            self.oe.set_high()
        }
    }

Sending the actual data is a tad more complicated. The data is read by the shift registers on the rising edge of the clock. Additionally, it takes some time for the GPIO values to settle. So, we need to set the clock low, update the values, wait for at least three CPU clock cycles, and then set the clock high:

    #[inline]
    fn send_data(&mut self, data_a: bool, data_b: bool, data_c: bool) -> Result<(), Self::Error> {
        self.clk.set_low()?;

        if data_a {
            self.data_a.set_high()?;
        } else {
            self.data_a.set_low()?;
        }
        if data_b {
            self.data_b.set_high()?;
        } else {
            self.data_b.set_low()?;
        }
        if data_c {
            self.data_c.set_high()?;
        } else {
            self.data_c.set_low()?;
        }

        // wait for gpio values to settle
        cortex_m::asm::nop();
        cortex_m::asm::nop();
        cortex_m::asm::nop();

        self.clk.set_high()?;
        Ok(())
    }

Now we can use this to wire everything up in main.rs. We will need a massive type to describe the LedDisplay. You can see that here, we pick specific pin assignments, like C11, D0, C10 and C12 for the layer select pins. These pins happen to match how my PCB from the kit is wired.

use stm32f4xx_hal::gpio;

type LedOutputPinState = gpio::Output<gpio::PushPull>;

type LedDisplay = led::display::LedDisplay<
    core::convert::Infallible,
    led::display::GpioLayerSelector<
        core::convert::Infallible,
        gpio::gpioc::PC11<LedOutputPinState>,
        gpio::gpiod::PD0<LedOutputPinState>,
        gpio::gpioc::PC10<LedOutputPinState>,
        gpio::gpioc::PC12<LedOutputPinState>,
    >,
    led::display::GpioDataBus<
        core::convert::Infallible,
        gpio::gpioc::PC8<LedOutputPinState>,
        gpio::gpioe::PE2<LedOutputPinState>,
        gpio::gpioe::PE3<LedOutputPinState>,
        gpio::gpioc::PC7<LedOutputPinState>,
        gpio::gpioc::PC6<LedOutputPinState>,
        gpio::gpiod::PD15<LedOutputPinState>,
    >,
>;

Let’s for consistency add some types for the LED matrix, and a hardware timer which will control the redrawing of the cube.

type LedMatrix = led::matrix::LedMatrix;
type LedTimer = stm32f4xx_hal::timer::Timer<stm32f4xx_hal::stm32::TIM4>;

Now for the slightly magical RTFM library. We will use the latest Git version, which at the time of writing is the unreleased 0.5 version of the library.

$ cargo add cortex-m-rtfm --git https://github.com/rtfm-rs/cortex-m-rtfm.git

This gives us access to some procedural macros for generating an interrupt-based fully real-time set of tasks. We declare resources, and the tasks that will use them, and RTFM ensures at compile time that the right tasks have access to the right resources, through the use of minimal critical sections (which use dynamic priority adjustments in the NVIC of the processor). You can read more about this in the official RTFM book.

We for now need two tasks: one to initialize all the resources when the chip starts/is reset, and one that triggers periodically to update the cube.

These will be called init and update_led_display:

#[rtfm::app(device = stm32f4xx_hal::stm32)]
const APP: () = {
    struct Resources {
        // LED-related resources
        led_display: LedDisplay,
        led_matrix: LedMatrix,
        led_timer: LedTimer,
    }

    #[init]
    fn init(_cx: init::Context) -> init::LateResources {
        init_impl()
    }

    #[task(binds = TIM4, resources = [led_display, led_matrix, led_timer])]
    fn update_led_display(cx: update_led_display::Context) {
        update_led_display_impl(
            cx.resources.led_timer,
            cx.resources.led_display,
            cx.resources.led_matrix,
        )
    }
}

In our init task, we will first get access to all of the device peripherals, and set the right CPU clock speeds.

fn init_impl() -> init::LateResources {
    use stm32f4xx_hal::time::U32Ext;

    let device = stm32f4xx_hal::stm32::Peripherals::take().unwrap();

    let clocks = stm32f4xx_hal::rcc::RccExt::constrain(device.RCC)
        .cfgr
        .sysclk(168.mhz())
        .hclk(168.mhz())
        .use_hse(25.mhz())
        .freeze();

Then, let’s configure all of the GPIO pins! First, we enable the GPIO ports A, C, D and E:

    let gpioa = gpio::GpioExt::split(device.GPIOA);
    let gpioc = gpio::GpioExt::split(device.GPIOC);
    let gpiod = gpio::GpioExt::split(device.GPIOD);
    let gpioe = gpio::GpioExt::split(device.GPIOE);

To configure all of the pins as output pins, there is unfortunately no common trait that they implement, so we need to resort to a local macro.

    // we need to use a macro because the pins don't implement a common trait
    macro_rules! output_pin {
        ($pin:expr) => {
            $pin.into_push_pull_output()
                .set_speed(gpio::Speed::VeryHigh)
        };
    };

Now we can instantiate a layer selector that uses our desired pins.

    let layer_address0 = output_pin!(gpioc.pc11);
    let layer_address1 = output_pin!(gpiod.pd0);
    let layer_address2 = output_pin!(gpioc.pc10);
    let layer_address3 = output_pin!(gpioc.pc12);

    let layer_selector = led::display::GpioLayerSelector::new(
        layer_address0,
        layer_address1,
        layer_address2,
        layer_address3,
    );

… and the data bus.

    let data_a = output_pin!(gpioc.pc8);
    let data_b = output_pin!(gpioe.pe2);
    let data_c = output_pin!(gpioe.pe3);
    let data_clk = output_pin!(gpioc.pc7);
    let data_stk = output_pin!(gpioc.pc6);
    let data_oe = output_pin!(gpiod.pd15);

    let data_bus =
        led::display::GpioDataBus::new(data_a, data_b, data_c, data_clk, data_stk, data_oe);

Finally, we can instantiate the LED display and the LED matrix using these configured abstractions.

    let led_display = led::display::LedDisplay::new(layer_selector, data_bus);

    let led_matrix = led::matrix::LedMatrix::new();

Now, how do we schedule the update_led_display task? Through setting up a timer with interrupts! This turns out to be quite simple:

    let mut led_timer = stm32f4xx_hal::timer::Timer::tim4(device.TIM4, 100_000.hz(), clocks);
    led_timer.listen(stm32f4xx_hal::timer::Event::TimeOut);

We want to trigger the timer at a super high frequency to make sure it starts as soon as possible. We will quickly adjust the speed of the timer shortly after to dynamically meet our needs. But first, let’s hand over all of the things we just created to RTFM to manage:

    init::LateResources {
        led_display,
        led_matrix,
        led_timer,
    }
}

For the actual update_led_display task, it turns out to be quite straight-forward. First, since it will be called from an interrupt, we need to clear the interrupt to make sure it doesn’t immediately get re-triggered.

fn update_led_display_impl(
    led_timer: &mut LedTimer,
    led_display: &mut LedDisplay,
    led_matrix: &mut LedMatrix,
) {
    use embedded_hal::timer::CountDown;
    use stm32f4xx_hal::time::U32Ext;

    led_timer.clear_interrupt(stm32f4xx_hal::timer::Event::TimeOut);

Then, we update the actual LED display by passing in the LED matrix.

    let update_freq_hz = led_display.update(&led_matrix).unwrap();

If the display requests a new refresh rate, we re-configure the timer accordingly:

    if let Some(update_freq_hz) = update_freq_hz {
        led_timer.start(update_freq_hz.hz());
    }
}

And that’s it!! To actually produce the image from before, all we have to do is fill the LED matrix with some data. Here’s the code for that in the init task:

    for z in 0..led::matrix::CUBE_SIZE {
        for y in 0..led::matrix::CUBE_SIZE {
            for x in 0..led::matrix::CUBE_SIZE {
                led_matrix[led::matrix::Coord::xyz(x, y, z)] = led::matrix::Color::rgb(
                    (x * 255 / led::matrix::CUBE_SIZE) as u8,
                    (y * 255 / led::matrix::CUBE_SIZE) as u8,
                    (z * 255 / led::matrix::CUBE_SIZE) as u8,
                );
            }
        }
    }

Next time, we will implement a simple FFT-based algorithm to show a music spectrum on the cube.

rainbow cube again