Microbit roulette example code organization

I am happily working my way through the micro:bit version of the discovery book, but sadly (primarily due to time constraints) have only gotten as far as section 5.6 - the LED Roulette challenge. I would like to structure my solution as:

struct Application {
    ...
}
impl Application {
  pub fn new() { ... };
  pub fn run(&self) { ... };
  fn top_row(&self) { ... };
  fn right_side(&self) { ... };
  fn bottom_row(&self) { ... };
  fn left_side(&self) { ... };
}

The first problem I ran into with this approach was in the definition of the member variables in the Application structure. I was planning on moving the variables declared in main():

    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

into the Application structure.

board is pretty easy:

struct Application {
    board: microbit::Board,
};

but timer member variable is more difficult. According to VSCode (aside, is there a better way for me to determine the return type of the Timer::new() function?) , timer is of type microbit::nrf52833_hal::Timer<microbit::nrf52833_pac::TIMER0>. That's a mouthful!

But I can't figure out the proper set of crates and use statements to bring microbit::nrf52833_pac::TIMER0 into scope.

I'll probably just let this go and write the code all within the main() function, but I feel like there should be a way to do this. So, I thought I would ask some friendly experts. (That's where you come in).

Thanks for any tips, advice, and discussion that this topic generates.

--wpd

Starting an answer to my own question... VSCode (with rust-analyzer) tells me that the return type from Board::take().unwrap() is Board, which matches the documentation I am now reading for the microbit-v2 crate.
That documentation points me to the Board structure, which includes the TIMER0 field, of type TIMER0.
Clicking on the link to the TIMER0 type, I see microbit::hal::pac::TIMER0, which I can now use to declare my timer member variable:

struct Application {
    board: microbit::Board,
    timer: microbit::hal::pac::TIMER0,
    display: Display,
}

That's why I ask questions like this in public forums... so I can force myself to continue to read the documentation until I find the answer.

Primarily, reading through this book is an excuse to get me to exercise Rust skills, including reading documentation, asking questions, and writing code with proper punctuation.

Just FYI, microbit::nrf52833_hal::Timer<microbit::nrf52833_pac::TIMER0> would normally be written as

use microbit::nrf52833_hal::Timer;
use microbit::nrf52833_pac::TIMER0;

struct Application {
    // ...
    timer: Timer<TIMER0>,
}

The compiler isn't great at trimming the full path when displaying type names, but there has been work to improve that.

You should also note that somewhat complicated types are pretty common in the Rust embedded world. Debugging embedded software can be pretty tough, and things are often resource constrained. Consequently, people often make use of type-level programming techniques to encode invariants in the type system, so that they can be checked by the compiler. That turns run-time errors into compile-time errors, which can be a life saver.

Moreover, you can often make the type-level approach completely zero overhead. For example, GPIO Pin structs are often zero-sized, so they don't contain any actual data. Instead, the GPIO registers are written as side effects of type transformations in the source code. The result is the exact same thing you would hand write in C, but it's checked by the compiler for correctness.

There's definitely a little bit of a learning curve to type-level programming in Rust, but in my opinion, it's often worth it for embedded projects.

Hello @bradleyharden, thank you for your reply. I understand that complicated types are common in the Rust embedded world. I am hoping to get a better handle on that by playing with the micro:bit. But, at the moment, I feel like I keep running into brick walls, which is usually a sign that I am looking at the problem the wrong way.
I tried your suggestion, and I received the following error when attempting to compile:

error[E0432]: unresolved import `microbit::nrf52833_hal`
  --> src/05-led-roulette/src/main.rs:12:15
   |
12 | use microbit::nrf52833_hal::Timer;
   |               ^^^^^^^^^^^^ could not find `nrf52833_hal` in `microbit`

error[E0432]: unresolved import `microbit::nrf52833_pac`
  --> src/05-led-roulette/src/main.rs:13:15
   |
13 | use microbit::nrf52833_pac::TIMER0;

I don't yet understand the magic in Cargo.toml for switching between the V1 and V2 boards (see https://github.com/rust-embedded/discovery.git). I have a V2 board, and have tweaked my .cargo/config and Cargo.toml files to default to building for my board (just because I'm lazy and don't want to remember to type cargo build --features v2 --target thumbv7em-none-eabihf). But, however the dependencies in Cargo.toml are listed, they don't seem to expose microbit::nrf52833_hal::Timer or microbit::nrf52833_pac::TIMER0.

If I try a different approach with this in my code:

struct Application {
    board: microbit::Board,
    timer: microbit::hal::Timer<microbit::pac::TIMER0>,
}

impl Application {
    pub fn new() -> Application {
        let board = Board::take().unwrap();
        let timer = Timer::new(board.TIMER0);
        Application { board, timer }
    }
}

then I get the following error:

error[E0382]: use of partially moved value: `board`
  --> src/05-led-roulette/src/main.rs:20:23
   |
19 |         let timer = Timer::new(board.TIMER0);
   |                                ------------ value partially moved here
20 |         Application { board, timer }
   |                       ^^^^^ value used here after partial move
   |
   = note: partial move occurs because `board.TIMER0` has type `microbit::nrf52833_pac::TIMER0`, which does not implement the `Copy` trait

This, at least, I understand, although I had never heard of a "partial move" before... but I can see what the compiler is complaining about. I just don't know how to address this.

Which brings me back to the thought that structuring my application in this manner may not be "the embedded Rust way" of doing things.

--wpd

The relevant part for you is here. You can see that the authors re-export (pub use) two different external crates as hal, depending on the v1 or v2 feature flags.

The authors structured the re-exports this way so that you can always refer to microbit::hal and microbit::pac, regardless of which feature you're using.

It looks like the type suggested by VS Code is just wrong though. microbit::nrf52833_hal is not valid. Are you using the "Rust" extension or rust-analyzer? If you don't know about rust-analyzer, you should switch to it. It's much better than RLS (the "Rust" extension). It will eventually become the official Rust language server as well.

Your more recent issue is unresolvable given your current approach. You can't simultaneously store an instance of the Board struct while also taking a field of it out. Taking the TIMER0 field out of the Board invalidates that memory, and Rust won't let you move invalid memory. Once you start to move pieces out of Board, you can never move Board itself again.

Instead, you'll need to take the parts you need from Board and drop the rest. Then you can store those parts somewhere else, like your Application struct.

I can't say yet whether this is a good approach or not, though. I don't have much information about what you're trying to do.

Hello @bradleyharden,
Thank you for the help. You're explanation of what happens when I take the TIMER0 field out of the Board structure. That makes a lot of sense. Between that explanation,and your previous explanation, I was able to complete the example. I am still not thrilled with my approach, but I'm happy enough to continue on.

If anybody is interested (and I don't blame you if you say, "thanks, but I'm not interested"... just stop reading now), here is the approach I was aiming for:

struct Application {
    delay_ms: u16,
    timer: microbit::hal::Timer<microbit::pac::TIMER0>,
    display_pins: DisplayPins,
}
impl Application {
    pub fn run(&mut self) -> ! {
        loop {
            self.spin_once();
        }
    }
    pub fn spin_once(&mut self) {
        loop {
            self.top_row();
            self.right_side();
            self.bottom_row();
            self.left_side();
        }
    }
   ...
}

The part I'm not thrilled about is my implementation of the top_row() function (and its 3 equivalents for the other 3 sides of the square):

    pub fn top_row(&mut self) {
        self.display_pins.row1.set_high().unwrap();

        self.display_pins.col2.set_low().unwrap();
        self.delay();
        self.display_pins.col2.set_high().unwrap();

        self.display_pins.col3.set_low().unwrap();
        self.delay();
        self.display_pins.col3.set_high().unwrap();

        self.display_pins.col4.set_low().unwrap();
        self.delay();
        self.display_pins.col4.set_high().unwrap();

        self.display_pins.col5.set_low().unwrap();
        self.delay();
        self.display_pins.col5.set_high().unwrap();

        self.display_pins.row1.set_low().unwrap();
    }

I would really like to pull the repetitive:

        self.display_pins.col2.set_low().unwrap();
        self.delay();
        self.display_pins.col2.set_high().unwrap();

code block into a separate function, to which I would pass each of the col1, col2, etc... member variables, but I can't figure out how to pass self.display_pins.col* to the function.

As I said, I'm going to let this go for now, and focus on learning what I can learn reading through the rest of the book.

Thank you again for your help. You're the best!

--wpd

@wpd, I think I can help a little bit with your latest problem. The issue relates strongly to the type-level programming I mentioned earlier.

Simplest solution

The fundamental problem is that each pin in your HAL is defined as a completely separate and distinct type. The PX_XX types are defined by a macro, so P0_00 is not related to P0_01 at all. Both take a MODE type parameter to represent the pin mode, but that doesn't tell the compiler anything useful.

Your problem is that you want to pass different pins to a common function like

fn blink(pin: ???, timer: &mut Timer<TIMER0>) {
    pin.set_low().unwrap();
    timer.delay();
    pin.set_high().unwrap();
}

But what should the type of pin be?

With the nrf approach, there's no immediate solution using the HAL types. P0_00 and P0_01 are totally different. To do this, you would have to use the embedded-hal traits directly, i.e.

fn blink<P: OutputPin>(pin: P, timer: &mut Timer<TIMER0>) {
    pin.set_low().unwrap();
    timer.delay();
    pin.set_high().unwrap();
}

This is probably the most straightforward solution to your problem. However, I'll expand a little bit more, in case you're interested.

Our approach in atsamd-hal

In atsamd-hal, we previously used the same macro-based approach, until about a year ago. We eventually transitioned to a different approach that makes better use of type-level programming in Rust.

In atsamd-hal, we have a Pin type that is generic over two type parameters, one for the PinMode (just like the nrf HAL), and a second for the PinId. With this approach, all pins are actually the same type, just with different type parameters. That lets you do cool things, like define the AnyPin trait.

With this approach, we don't need to use a completely generic type P: OutputPin. We can still use HAL types.

fn blink<I: PinId>(
    pin: &mut Pin<I, PushPullOutput>,
    timer: &mut Timer<TIMER0>,
) {
    pin.set_low().unwrap();
    timer.delay();
    pin.set_high().unwrap();
}

For this specific case, it's not much different. But in general, it's more powerful than the macro-based approach.

Value-level approach

That covers the completely type-level approach, but there's a second approach which is used by both the nrf HAL and atsamd-hal. Both of our HALs also define value-level pins.

In atsamd-hal, we call them DynPins. Each DynPin stores a DynPinId and a DynPinMode, which are value-level versions of the type-level alternatives above.

DynPin has no type parameters, so every instance of DynPin is considered the same type by the compiler. That lets you define things like arrays of DynPin, e.g. [DynPin; 5]. That would not be possible with our Pin struct, because each Pin<I, M> has a different type parameter I for its PinId.

The nrf HAL also has a value-level pin type. They call it Pin. But their Pin type is different from our DynPin type. Their version is partially type-level and partially value-level. It stores a u8 for the pin number, but it still takes a MODE type parameter for the pin mode. This is still useful, though, because it lets you create arrays of Pin objects, as long as they are all in the same pin mode, e.g. [Pin<Output<PushPull>>; 5].

It looks like the DisplayPins struct has a degrade method that will convert all of the PX_XX types into Pin types. Rather than store the DisplayPins, you could instead call degrade and store the two Pin arrays. That would let you use an index to access the row or column of interest.

fn blink_column(&mut self, index: usize) {
    self.columns[index].set_high().unwrap();
    self.delay();
    self.columns[index].set_low().unwrap();
}

Note, however, that there is a slight run-time cost to atsamd-hal's DynPin or nrf's Pin. Whereas the type-level structs are completely zero-sized and do not exist at run-time, the value-level versions have a non-zero size and do exist at run-time. It's a pretty minor cost, just a few bytes, but it's something to keep in mind.

Wow @bradleyharden, what a fabulous explanation! The primary reason I picked up the micro:bit was because I saw that it had been added to the Discovery book, and this would give me a chance to get some real experience with the type-level programming concept.
It will take me a little more time to grok your explanation, but I can see where it is coming from and where it is leading to.
Again, thank you so much for your helpful explanations.
If you happen to work for Microchip/Atmel, and know my buddy Matt Wood, please tell him I said hi.

--wpd

Ha, no I don't work for Microchip. It would be great to see direct vendor support for Rust though. I don't see that happening any time soon, unfortunately. But it would be nice to be proven wrong.

Also, I'm sure the Microbit is great for learning. I'm glad to see the Discovery book has been updated.

When I first started learning Rust, in the summer of 2020, the old book was the only one available. I just looked through some of the history, and it looks like the rewrite started around then. Seems like they finally merged it recently. That's good to see.

The nrf and stm32 chips are definitely more popular and widely used in the community. ATSAM chips seem to be a minority. But we're chugging along. The RP2040 seems to be a new favorite as well.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.