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! :)