kxd.me()

kxd.me()

Adventures in geekdom

06 Oct 2020

Rust on PX-HER0 - Part 2

In my last post I talked about getting Rust running on the PX-HER0 and it went into the minimal code we’d need to run Rust on the PX-HER0. In this post we’re going to take another step in that process!

The Goal

As I mentioned in the Next Steps of the previous post, I had two goals for this post:

  • Turn on an LED when one of the buttons is pressed
  • Use abstractions instead of GPIO pins directly.

It’s likely going to be much shorter than the previous post [it’s not - Ed.], as well as more Rust-focused, but it took me the most amount of time and frustration thus far so I think it’s worth having it out on the internet!

Reading Button Inputs

All the buttons in the PX-HER0 (maybe all buttons in every device?) are connected to the GPIO pins, so reading button inputs is essentially the same as when we were reading whether an LED is on to toggle it.

The first thing to do was figure out where the buttons are on the GPIO pins, which ended up being the same process as last time. I found the line in the GPIO header and determined that the button labeled “1” is on GPIO port C, pin 0, and is a “pull-up” pin. “WTF is a pull-up pin? Is there a pull-down pin?” you may ask. If you want to know the gritty details, I found this post very helpful. For those that want a TL;DR, though, a “pull-down” pin is what you may expect, where the value is considered a 1 when there is current applied to the pin. A “pull-up” pin is the opposite, where the value is considered a 0 when there is current applied to the pin.

Once I knew that, it was a matter of updating the code to include a reference to the GPIO pin C0 and use that in place of checking whether the LED was already on or not:

 1let gpioc = dp.GPIOC.split(&mut rcc);
 2let gpioh = dp.GPIOH.split(&mut rcc);
 3
 4let mut led_usr = gpioh.ph0.into_push_pull_output();
 5// Get a reference to BTN1
 6let btn1 = gpioc.pc0.into_pull_up_input();
 7loop {
 8    while !syst.has_wrapped() {}
 9
10    // Is the button pressed? Also, unwrap is bad. Don't do it.
11    // Since the pin is pull-up, it means the button is pressed if
12    // we DO NOT have current (low).
13    if btn1.is_low().unwrap() {
14        // It's pressed, turn it on!
15        led_usr.set_high();
16    } else {
17        // It's not pressed, turn it off!
18        led_usr.set_low();
19    }
20}

And now we have a way to interact with the outside world! Using GPIO pins everywhere to interact with peripherals is a bit of a pain, though, so I decided to start working on an abstraction that allows me to work with “LED"s and “button"s instead of GPIO pins.

Creating an LED Abstraction

This is where things got pretty tough and I spent a lot of time. It’s been a while since I’ve used Rust, or any language with generics, so my skills were a bit rusty (ha. ha.) and I had to do a bit of playing around to get this to work. I knew I wanted a struct called LED that had an on() and an off() method, as well as an is_on() method and a toggle() method, so I started by creating a new module called led and stuck a struct in there. A quick aside; you may want a quick refresher on the Brave new I/O post I mentioned previously because it’s a huge factor in how the following code works.

The big problem comes in when I want to support more than just one GPIO pin with this LED abstraction. You may have noticed that each GPIO pin in our HAL package is a completely different type from each other, so the User LED’s type is a stm32l0xx_hal::gpio::gpioh::PH0, while the LED for the display’s backlight is a stm32l0xx_hal::gpio::gpiob::PB12 (sneaking a new one in on you). Not only are they not the same type, they’re not even in the same module!

To summarize a lot of mistakes I made and hopefully save you a lot of time, I first tried using the “slightly more generic” version of the type that you get when calling downgrade() on one of the specific pin types. For example, if you have stm32l0xx_hal::gpio::gpiob::PB12 and call downgrade() on it you’ll end up with a stm32l0xx_hal::gpio::gpiob::PB. This is all well and good if every LED you want to access is on the same GPIO port, but the PX-HER0 has LEDs on multiple ports (B and H in the examples I’ve given).

Next, I went down the road of trying to use the traits provided by the HAL crate, specifically stm32l0xx_hal::prelude::OutputPin. This is what eventually led me toward the final solution, but I did a lot of experimenting with different generics and trait bounds before I got this version working. The big problem here is that it only gave me an output, though! Since I wanted to be able to toggle the LED and see if it was turned on I’d also need to be able to use the pin as an input. Thankfully I found stm32l0xx_hal::prelude::StatefulOutputPin, which is not quite InputPin + OutputPin but it seemed like the same idea.

Once I’d found the trait that would give me what I wanted, I needed to find a way to convert all my GPIO instances into LEDs in a way that wasn’t overly complicated and gross. Around this time I was searching online for examples of embedded Rust code and I happened upon one that introduced a trait I wasn’t familiar with (Into) and also had a pattern I thought was pretty elegant. I can’t seem to find the code I saw at the moment, but what I ended up with is very similar in concept to what they had.

The idea they had was to use a combination of the Into trait to allow us to translate one type into another (the GPIO pin type to an LED type), as well as a macro to do the busy work of creating all of the Into implementations for each of the pins we’ll be using.

The Code!

Here’s the full code for the LED abstraction! I’ll go into greater detail about each section after. If you’d like to see everything together, I have a repository with this code, and the rest in those post, available at github.com/aphistic/px-her0-example.

 1// led.rs
 2use hal::prelude::*;
 3
 4use hal::gpio;
 5use hal::gpio::gpiob::PB12;
 6use hal::gpio::gpioh::PH0;
 7
 8// Define some types for our peripherals so we can refer to them
 9// with a useful name instead of just GPIO pins.
10pub type LEDLCD = PB12<gpio::Output<gpio::PushPull>>;
11pub type LEDUSR = PH0<gpio::Output<gpio::PushPull>>;
12
13// Define a macro we can use to create all the Into impls we need
14macro_rules! into_led {
15    ($($pin:ident),+) => {
16        $(
17            impl<PIN: StatefulOutputPin + From<$pin>> Into<Led<PIN>>
18            for $pin {
19                fn into(self) -> Led<PIN> {
20                    Led {
21                        p: self.into(),
22                    }
23                }
24            }
25        )+
26    }
27}
28// Define the Into impls for LEDLCD and LEDUSR
29into_led!(LEDLCD, LEDUSR);
30
31// Define the actual Led struct! This seems deceptively straightforward
32// compared to the other code.
33pub struct Led<PIN: StatefulOutputPin> {
34    p: PIN,
35}
36
37impl<PIN: StatefulOutputPin> Led<PIN> {
38    pub fn on(&mut self) {
39        match self.p.set_high() {
40            _ => {}
41        }
42    }
43
44    pub fn off(&mut self) {
45        match self.p.set_low() {
46            _ => {}
47        }
48    }
49
50    pub fn is_on(&self) -> bool {
51        match self.p.is_set_high() {
52            Ok(v) => v,
53            _ => false,
54        }
55    }
56
57    pub fn toggle(&mut self) {
58        match self.is_on() {
59            true => self.off(),
60            false => self.on(),
61        }
62    }
63}

So, now, what exactly does this all do?!

1use hal::prelude::*;
2
3use hal::gpio;
4use hal::gpio::gpiob::PB12;
5use hal::gpio::gpioh::PH0;

This is a list of your standard Rust use statements. We use PB12 and PH0 directly so it’s shorter when we need to refer to them.

1pub type LEDLCD = PB12<gpio::Output<gpio::PushPull>>;
2pub type LEDUSR = PH0<gpio::Output<gpio::PushPull>>;

These types are defined so we can refer to each pin’s type by what it actually is instead of some GPIO pin. It’s a lot easier to read code that refers to LEDLCD than PB12<gpio::Output<gpio::PushPull>>.

 1macro_rules! into_led {
 2    ($($pin:ident),+) => {
 3        $(
 4            impl<PIN: StatefulOutputPin + From<$pin>> Into<Led<PIN>>
 5            for $pin {
 6                fn into(self) -> Led<PIN> {
 7                    Led {
 8                        p: self.into(),
 9                    }
10                }
11            }
12        )+
13    }
14}
15
16into_led!(LEDLCD, LEDUSR);

This macro is where a lot of the magic (mostly dark magic because macros are so crazy hard to understand) happens. This is where our GPIO types (or rather the LEDLCD and LEDUSR types that refer to them) are actually translated from LEDUSR to Led<LEDUSR>. The bounds we put on the types that can be referred to by PIN say that it needs to not only be a StatefulOutputPin, but also a From<LEDUSR> or From<LEDLCD>. The From<> constraint is required in order for us to use self.into() to convert our GPIO pin into the StatefulOutputPin we want. Then, after we define the macro, we use it to create the impl we want for LEDUSR and LEDLCD.

 1pub struct Led<PIN: StatefulOutputPin> {
 2    p: PIN,
 3}
 4
 5impl<PIN: StatefulOutputPin> Led<PIN> {
 6    pub fn on(&mut self) {
 7        match self.p.set_high() {
 8            _ => {}
 9        }
10    }
11
12    pub fn off(&mut self) {
13        match self.p.set_low() {
14            _ => {}
15        }
16    }
17
18    pub fn is_on(&self) -> bool {
19        match self.p.is_set_high() {
20            Ok(v) => v,
21            _ => false,
22        }
23    }
24
25    pub fn toggle(&mut self) {
26        match self.is_on() {
27            true => self.off(),
28            false => self.on(),
29        }
30    }
31}

Lastly, we have the implementation of the Led struct itself. Compared to the rest of the code so far this is pretty straightforward. We define a struct called Led that can take a StatefulOutput pin, then using the GPIO methods we’re already familiar with we create the nice methods we were looking for!

Using the LED Abstraction

In the same repository I found the macros and Into examples in, they also had something they called the “PAL”. I don’t remember exactly what it stood for but I’m imagining it’s the Peripheral Abstraction Layer, and I liked the idea. It was a struct that held references to each of the peripheral abstractions, so you’d just need a reference to that in order to access one of them. So what I did next is create a pal.rs module with a PAL struct in it:

 1use stm32l0xx_hal::{prelude::*, pac, rcc::Config};
 2
 3use crate::led;
 4
 5pub struct Pal {
 6    pub led_usr: led::Led<led::LEDUSR>,
 7    pub led_lcd: led::Led<led::LEDLCD>,
 8}
 9
10impl Pal {
11    pub fn new() -> Self {
12        let dp = pac::Peripherals::take().unwrap();
13
14        let mut rcc = dp.RCC.freeze(Config::hsi16());
15
16        let gpiob = dp.GPIOB.split(&mut rcc);
17        let gpioh = dp.GPIOH.split(&mut rcc);
18
19        Pal{
20            led_usr: gpioh.ph0.into_push_pull_output().into(),
21            led_lcd: gpiob.pb12.into_push_pull_output().into(),
22        }
23    }
24}

I think this code is pretty straightforward, but there are a few things I’m not happy with in it that I want to call out as things I need to look into later.

1use stm32l0xx_hal::{prelude::*, pac, rcc::Config};
2
3use crate::led;

Here we do the usual use statements to import modules, but I think at some point I want to start using the embedded_hal crate so the code isn’t tied specifically to the STM32L0 processor. I don’t think this will be difficult, but my ultimate goal is to start creating an operating system that I can run on a few embedded devices and the next target is likely going to be a SiFive HiFive1 Rev B that I have because I’m super interested in RISC-V. In order to do that, though, I’ll need abstractions that don’t rely on types tied to a specific processor or architecture.

1pub struct Pal {
2    pub led_usr: led::Led<led::LEDUSR>,
3    pub led_lcd: led::Led<led::LEDLCD>,
4}

I’m not sure that I like the need to specify the generic type here instead of just led::Led, but it’s needed at the moment and it’s not worth spending time on that when there’s so many other, bigger things to do! At least this is the last place where that specificity is needed, since we’ll just reference it through the PAL after this point.

 1impl Pal {
 2    pub fn new() -> Self {
 3        let dp = pac::Peripherals::take().unwrap();
 4
 5        let mut rcc = dp.RCC.freeze(Config::hsi16());
 6
 7        let gpiob = dp.GPIOB.split(&mut rcc);
 8        let gpioh = dp.GPIOH.split(&mut rcc);
 9
10        Pal{
11            led_usr: gpioh.ph0.into_push_pull_output().into(),
12            led_lcd: gpiob.pb12.into_push_pull_output().into(),
13        }
14    }
15}

Lastly we have the PAL implementation itself. There are a couple things I’m not sure about here… The first is that it seems wrong to have the freeze() in here as well as in the main method and I’m not sure how that’s going to interact moving forward. I may need to move everything, including the SYST timer, into the PAL. We’ll find out in the future!

Adding a Button Abstraction!

Before I go too far, I wanted to add an abstraction for buttons as well. The implementation is close enough to the LED abstraction that I didn’t want to include all the code in the post, but it’s available in the repository I linked with the code above. The biggest change is that the only method the Button implementation has is an is_pressed() method. Right now it only does a check for is_low() for pull-up buttons, but I’m sure I’ll need to change it at some point to allow for pull-down as well.

Here’s what the PAL struct and impl look like with the buttons added:

 1pub struct PAL {
 2    pub led_usr: led::Led<led::LEDUSR>,
 3    pub led_lcd: led::Led<led::LEDLCD>,
 4    
 5    pub btn1_left: button::Button<button::BTN1>,
 6    pub btn2_right: button::Button<button::BTN2>,
 7    pub btn3_up: button::Button<button::BTN3>,
 8    pub btn4_down: button::Button<button::BTN4>,
 9    pub btn5_yes: button::Button<button::BTN5>,
10    pub btn6_no: button::Button<button::BTN6>,
11}
12
13impl PAL {
14    pub fn new() -> Self {
15        let dp = pac::Peripherals::take().unwrap();
16
17        let mut rcc = dp.RCC.freeze(Config::hsi16());
18
19        let gpioa = dp.GPIOA.split(&mut rcc);
20        let gpiob = dp.GPIOB.split(&mut rcc);
21        let gpioc = dp.GPIOC.split(&mut rcc);
22        let gpioh = dp.GPIOH.split(&mut rcc);
23
24        PAL{
25            led_usr: gpioh.ph0.into_push_pull_output().into(),
26            led_lcd: gpiob.pb12.into_push_pull_output().into(),
27
28            btn1_left: gpioc.pc0.into_pull_up_input().into(),
29            btn2_right: gpioh.ph1.into_pull_up_input().into(),
30            btn3_up: gpioc.pc13.into_pull_up_input().into(),
31            btn4_down: gpioc.pc12.into_pull_up_input().into(),
32            btn5_yes: gpioa.pa15.into_pull_up_input().into(),
33            btn6_no: gpioc.pc9.into_pull_up_input().into(),
34        }
35    }
36}

Updating Main

Once we’ve updated our main function use the PAL it looks like this:

 1#[entry]
 2fn main() -> ! {
 3    let cp = cortex_m::Peripherals::take().unwrap();
 4    let mut syst = cp.SYST;
 5    syst.set_clock_source(SystClkSource::Core);
 6    syst.set_reload(8_000);
 7    syst.enable_counter();
 8
 9    let mut p = pal::Pal::new();
10
11    loop {
12        while !syst.has_wrapped() {}
13
14        if p.btn1_left.is_pressed() {
15            p.led_usr.on();
16        } else {
17            p.led_usr.off();
18        }
19    }
20}

A lot of it is the same, so there’s not much to dig into in the code. I lowered the number of clock cycles the syst delay waits for as a way to increase the responsiveness of the button press, but the biggest change is that now the code starts to actually make sense when we read it!

One thing I’m not sure about with this code is that the LCD backlight turns on when the code starts running. I initially tried to make it so button 4 would turn on and off the LCD backlight but I wasn’t able to get that working. At this point I’m not sure if it’s the code and the way it interacts with the GPIO pin or if it’s because I’m not initializing the hardware completely or correctly. That’s something I’m going to dig into in the future.

Next Steps

As I was reading up on the difference between JTAG and SWD for my last post I noticed that SWD is supposed to support printing debug info over a debug port, so that seemed interesting. I think being able to send debug logging back will be most beneficial going forward, so I’m planning on working toward getting that working. I’m not sure what it’s going to involve, so the next post may be just as long as the first two! :)