Experimenting with the New I/O Framework (in Rust) for embedded systems

Published on: 2018-01-31

Home | Archive

For the past one or two days, I was trying to find out more about the working of Jorge Aparicio’s new I/O framework for microcontrollers. I have been able to get a simple example working on my Stellaris Launchpad; read on to find out more!

Motivation

One of the motivations behind using Rust for embedded programming is to exploit the type system to make sure certain kinds of errors are caught at compile time itself. Jorge Aparicio has been doing pioneering work in this area; please do spend some time going through the articles on his blog and you will come away as excited as I am! You should at least read this post on the new I/O framework before going any further!

Ok, now that you have gone through that article, let’s see how you can get a minimal program working on your TI Tiva/Stellaris launchpad board!

Setting up the tools

Click here to find out what you should do to prepare your system for embedded development using Rust.

Embedded development in Rust requires the nightly compiler; you are at the bleeding edge when using nightly and can expect things to break depending on the particular version you are using. I did a rustup update nightly yesterday and discovered that I was unable to compile code which worked perfectly with a previous version. You can install an older version to get things running once again:


rustup default nightly-2017-12-01

rustup component add rust-src

Now, clone this repo: https://github.com/pcein/launchpad-new-io.

Build and flash the code to your launchpad; you should see the RED LED lighting up!


cd launchpad-new-io

make release; make flash

How does it work?

A lot of work in microcontroller programming revolves around digging through datasheets and programming manuals (which may be over a thousand pages long) and finding out the device registers and the magical bit patterns you need to write to these registers to initialize a peripheral and make it do something. Get a single bit wrong and you can spend the next few hours (days …) debugging code rather than working out in the gym or playing with your kids!

ARM processor manufacturers are supposed to provide an SVD specification of their CPU’s. An SVD file is an XML file which describes the peripherals contained in a processor - name of peripheral, the base address associated with its registers, name of each register, its offset, size, reset value, whether you can read/write to it etc.

Here is a small part of an SVD file (for an ARM processor from TI):

<peripheral>
        <name>TIMER_A0</name>
        <version>356.0</version>
        <description>TIMER_A0</description>
        <baseAddress>0x40000000</baseAddress>
        <interrupt>
            <name>TA0_0_IRQ</name>
            <description>TA0_0 Interrupt</description>
            <value>8</value>
        </interrupt>
        <interrupt>
            <name>TA0_N_IRQ</name>
            <description>TA0_N Interrupt</description>
            <value>9</value>
        </interrupt>
        <addressBlock>
            <offset>0x0</offset>
            <size>0x30</size>
            <usage>registers</usage>
        </addressBlock>
        <registers>
            <register>
                  <name>TAxCTL</name>
                  <displayName>CTL</displayName>
                  <description>TimerAx Control Register</description>
                  <addressOffset>0x0</addressOffset>
                  <size>16</size>
                  <access>read-write</access>
                  <resetValue>0x00000000</resetValue>
                  <resetMask>0x0000ffff</resetMask>
                  <fields>
                    <field>
                        <name>TAIFG</name>
                        <description>TimerA interrupt flag</description>
                        <bitOffset>0x0</bitOffset>
                        <bitWidth>0x1</bitWidth>
                        <access>read-write</access>
                        <enumeratedValues>
                            <enumeratedValue>
                                <name>TAIFG_0</name>
                                <description>No interrupt pending</description>
                                <value>0</value>
                            </enumeratedValue>
                            <enumeratedValue>
                                <name>TAIFG_1</name>
                                <description>Interrupt pending</description>
                                <value>1</value>
                            </enumeratedValue>
                        </enumeratedValues>
                    </field>

It is obvious that we are describing a timer peripheral (TIMER_A0) and its associated registers, their memory mapped addresses and reset values, specific fields within each register (for example a one bit wide field called TAIFG), the possible values for these fields (TAIFG_0 and TAIFG_1 in this case).

What if there is a program which takes an SVD file and automatically generates code which will help us access these registers using the peripheral/register/field names specified?

That is what svd2rust does!

If you check the Cargo.toml file in the git repo which you cloned just now, you will see that it has a line:

tm4c123x = "0.6.0"

This is the tm4c123x crate, and it contains a large Rust file which has been auto-generated by svd2rust from the SVD file describing the tm4c123 processor (it looks like TI does not provide SVD files for this processor - instead it provides a dslite file. The tool dslite2svd generates SVD files from dslite files)!

You can see the auto generated Rust file here: https://github.com/m-labs/dslite2svd/blob/master/crates/tm4c123x/src/lib.rs. It is a 6Mb source file!

Let’s see what is inside src/main.rs of our program:

fn main() {
    let p = tm4c123x::Peripherals::take().unwrap();

    let gpiof = p.GPIO_PORTF;
    let sysctl = p.SYSCTL;

    // Enable PORTF
    sysctl.rcgcgpio.modify(|_, w| w.r5().bit(true));
    // Need to wait for sometime after modifying rcgcgpio
    let _t = sysctl.rcgcgpio.read(); 
    
    // Enable digital IO on the specified pin
    gpiof.den.modify(|r, w| unsafe { w.bits(r.bits() | (1 << PIN_RED_LED)) });
    // Make it an output pin
    gpiof.dir.modify(|r, w| unsafe { w.bits(r.bits() | (1 << PIN_RED_LED)) });
    
    // Write a 1 to the pin - RED LED lights up!
    gpiof.data.modify(|r, w| unsafe { w.bits(r.bits() | (1 << PIN_RED_LED))  });
    
    loop {
    }

}

All the functions that you invoke in main have been auto-generated by svd2rust!

Let’s look at some specific functions.

To use the peripherals, you need to call the take function. You can call this only once.

GPIOF and SYSCTL are the two peripherals we are interested in. We use a SYSCTL register called RCGCGPIO to enable each GPIO port. The fifth bit of RCGCGPIO (called R5) needs to be set to enable PORTF.

Look at the line which does this:


sysctl.rcgcgpio.modify(|_, w| w.r5().bit(true));

The modify function accepts a closure with two parameters; the first one is a read-only representation of the bits of the register and the second one is a mutable struct which initially has the same bit pattern. When you invoke:


w.r5().bit(true)

you are modifying this bit pattern: in this case, set the R5 bit to 1. After the closure is invoked, the new bit pattern in w is written to the RCGCGPIO register (note that names like RCGCGPIO, R5 etc are exactly those names used in the processor manual).

Here is what the modify function looks like (taken from the tm4c123x crate):


pub fn modify<F>(&self, f: F)
            where
                for<'w> F: FnOnce(&R, &'w mut W) -> &'w mut W,
            {
                let bits = self.register.get();
                let r = R { bits: bits };
                let mut w = W { bits: bits };
                f(&r, &mut w);
                self.register.set(w.bits);
            }

Let’s now look at another function call:


gpiof.den.modify(|r, w| unsafe { w.bits(r.bits() | (1 << PIN_RED_LED)) });

The digital functionality of each pin of a GPIO port has to be separately enabled. We do this by writing 1 to a bit position corresponding to the pin connected to the RED LED on the launchpad board.

r.bits() gives us the current value of the bits of DEN, it gets modified and written back to the DEN register.

I was wondering for some time as to why w.bits is unsafe. Seems like it is because this function lets you write arbitrary bit patterns to registers - some bits of a register may be reserved for special purposes and changing those bits accidentally can cause problems (Note that bits doesn’t directly write to the register - it is done by the modify function. bits simply enables unsafe behaviour).

[Note: I still feel a bit unsatisfied by this explanation. Would love to get some clarification]

The next two functions behave in similar ways; set the direction of the pin to OUTPUT and then make the pin go HIGH.

Zero cost abstractions!

The tm4c123x crate seems to be using a lot of abstractions (functions called with closures as parameters, multiple levels of indirection involving structures and function calls) for doing something as simple as setting/clearing a bit. This will surely make C programmers unhappy, that is, until they see the actual machine code produced by the compiler in release mode!


 4b6:   f24e 6008       movw    r0, #58888      ; 0xe608
 4ba:   f2c4 000f       movt    r0, #16399      ; 0x400f
 4be:   6801            ldr     r1, [r0, #0]
 4c0:   f041 0120       orr.w   r1, r1, #32
 4c4:   6001            str     r1, [r0, #0]
 4c6:   6800            ldr     r0, [r0, #0]
 4c8:   f245 30fc       movw    r0, #21500      ; 0x53fc
 4cc:   f2c4 0002       movt    r0, #16386      ; 0x4002
 4d0:   f8d0 1120       ldr.w   r1, [r0, #288]  ; 0x120
 4d4:   f041 0102       orr.w   r1, r1, #2
 4d8:   f8c0 1120       str.w   r1, [r0, #288]  ; 0x120
 4dc:   6841            ldr     r1, [r0, #4]
 4de:   f041 0102       orr.w   r1, r1, #2
 4e2:   6041            str     r1, [r0, #4]
 4e4:   6801            ldr     r1, [r0, #0]
 4e6:   f041 0102       orr.w   r1, r1, #2
 4ea:   6001            str     r1, [r0, #0]

The first five lines perform a read from RCGCGPIO, sets the 5th bit of that value and writes it back to RCGCGPIO. The remaining lines also do similar operations, on the DEN, DIR and DATA registers. This is as good as hand-coded assembly; the compiler has broken down all the abstractions to perform the minimal amount of work which gets the job done!

Moving ahead

Let’s say you have written the driver for an accelerometer; won’t it be great if you can use it unmodified with your STM32 Nucleo boards, TI launchpads, BeagleBone, Raspberry Pi, etc?

This is what Jorge Aparicio plans to achieve with his embedded-hal! The embedded-hal is a set of traits, for example:


/// Single digital output pin
pub trait OutputPin {
    /// Is the output pin high?
    fn is_high(&self) -> bool;

    /// Is the output pin low?
    fn is_low(&self) -> bool;

    /// Sets the pin low
    fn set_low(&mut self);

    /// Sets the pin high
    fn set_high(&mut self);
}

If you have an implementation of the embedded-hal for your favourite board, and if you have a peripheral driver on crates.io targeted at the embedded-hal, you will easily be able to get this driver running on your board, and any other for which there is an implementation of the embedded-hal!

Check out a sample implementation of the embedded-hal. Also, a sample driver for an L3GD20 gyroscope which uses the embedded-hal.