Supporting the new Embedded HAL

Recently, @japaric posted a about a new approach to an Embedded HAL in Rust. This is something that's been kicked around on #rust-embedded for a while, but it was great to see it get to a point it could be pushed out to the wider world.

I run the Cambridge Rust Meetup and the post coincided with our next Hack-n-Learn evening. The purpose of these evenings is to work through problems together and I decided it would be fun to try and implement the Embedded HAL on my LM4F120 chip crate, and then fix the Stellaris Launchpad examples to use the new HAL. To engage some of the attendees new to Rust, I thought it would help if I did it live on the big projector and talked through the changes as I made them.

Well, despite a quick 10 minute break for pizza (thanks Cambridge Consultants) I managed to get the changes completed to the UART driver in under two hours. Here's what I had to do.

The old UART driver in the LM4F120 crate used my earlier Embedded Serial HAL. That looked like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
pub trait MutNonBlockingTx {
    type Error;

    fn putc_try(&mut self, ch: u8) -> Result<Option<u8>, Self::Error>;

    fn puts_try<I: ?Sized>(&mut self, data: &I) -> Result<usize, (usize, Self::Error)>
        where I: AsRef<[u8]>
    {
        let mut count = 0;
        for octet in data.as_ref() {
            // If we get an error, return it (with the number of bytes sent),
            // else if we get None, we timed out so abort.
            if self.putc_try(*octet).map_err(|e| (count, e))?.is_none() {
                break;
            }
            count += 1;
        }
        Ok(count)
    }
}

pub trait MutNonBlockingRx {
    type Error;

    fn getc_try(&mut self) -> Result<Option<u8>, Self::Error>;

    fn gets_try<I: ?Sized>(&mut self, buffer: &mut I) -> Result<usize, (usize, Self::Error)>
        where I: AsMut<[u8]>
    {
        let mut count: usize = 0;
        for space in buffer.as_mut() {
            *space = match self.getc_try() {
                Err(e) => return Err((count, e)),
                Ok(None) => return Ok(count),
                Ok(Some(ch)) => ch,
            };
            count += 1;
        }
        Ok(count)
    }
}

The new HAL looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use nb;

pub trait Read<Word> {
    type Error;

    fn read(&mut self) -> nb::Result<Word, Self::Error>;
}

pub trait Write<Word> {
    type Error;

    fn write(&mut self, word: Word) -> nb::Result<(), Self::Error>;

    fn flush(&mut self) -> nb::Result<(), Self::Error>;
}

The first two differences here are that I tried to express both blocking vs non-blocking behaviour through two different Traits and that I had both mutable (&mut self) and immutable version (&self). I greatly prefer @japaric's approach to making the Traits (and hence the drivers) fundamentally non-blocking and leaving the choice of how to block (spinning, futures, generators, etc) to the caller, but I can see problems in the future with those who wish to share immutable references to drivers. Maybe there's other ways around that (it's not something I needed, but others commented on the old HAL that it was necessary).

The third difference is that the old HAL had trait methods which would attempt to read or write an entire buffer, stopping as soon as the underlying single octet read/write failed. These are useful if your UART has a multi-octet FIFO, but in my case it only has a single register, so they're not that useful. Perhaps it's something we could look at adding in future, with appropriate macro wrappers that track now many octets from the buffer have been read/written and do the appropriate thing (spin, etc) until the buffer is complete. I'd probably need to work through a concrete example with a FIFO UART before thinking about how to make it generic enough to re-use.

The final difference is a subtle change in the return type. The old HAL made use of Result<Option<u8>, Error>, where Ok(None) indicates that the API would block, while the new HAL uses Result<u8, Error>, where Would Block is indicated by a special case of error. I think these are broadly equivalent, it just depends on whether you consider the 'Would Block' case to be an error, or a success. I'm happy to embrace the new approach in the name of consistency.

So, how does the code look, once the changes have been made? Well I think it's fairly readable, but see for yourself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
impl ReadHal<u8> for Uart {
    type Error = !;

    fn read(&mut self) -> nb::Result<u8, Self::Error> {
        if self.reg.fr.read().rxfe().bit() {
            return Err(nb::Error::WouldBlock);
        }
        Ok(self.reg.dr.read().data().bits())
    }

}

impl WriteHal<u8> for Uart {
    type Error = !;

    fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> {
        if self.reg.fr.read().txff().bit() {
            return Err(nb::Error::WouldBlock);
        }
        self.reg.dr.write(|w| unsafe { w.data().bits(word) });
        Ok(())
    }

    fn flush(&mut self) -> nb::Result<(), Self::Error> {
        if self.reg.fr.read().txff().bit() {
            return Err(nb::Error::WouldBlock);
        }
        Ok(())
    }
}

You can see here that even though WouldBlock considered an error, spelling it out is actually perhaps a little clearer than using None would have been.

What next for me? Well there's plenty more Traits in the HAL to implement. I'll probably look at the GPIO pins next. I'm also interested in writing I2C and SPI drivers for the LM4F120, and maybe hooking up some little sensors, or maybe a Microchip MCP23S17 I/O expander or something. One of the downsides of the Stellaris Launchpad is it doesn't have an awful lot on-board (except the LEDs and the buttons), but hey, it is cheap! I'm also interested in porting the Raspberry Pi SenseHat drivers to use the HAL, so that they can be used with examples of the chips that don't happen to be on a SenseHat (or indeed a Linux device using /dev/i2cX).

In the words of Neil Buchanan, why not try it yourself?

Comments

Popular posts from this blog

Embedded Rust in 2018

I decided to make an 1980's Home Computer in Rust - Part 1

Advent of Code