07 Nov 2019

Brighton Rust: Embedded systems, day 3

This week we learned about Real-time For the Masses (RTFM), which gives us tools for concurrent programming on our embedded hardware.

The plan

Last time we had LED blinking via a loop that turned on the LED, kept the CPU busy in a loop for a human-noticeable amount of time, and then turned off the LED. That’s not going to be good enough if we want to do something else while blinking LEDs. For example, we might want to listen out for commands to change the blink rate.

Instead, Tim introduced us to RTFM. We:

  • made sure everyone had caught up with last week;
  • tried out a couple of examples of RTFM apps; and
  • pasted our LED blinky code into an RTFM app framework.

At the end of the evening we were not yet using the concurrency facilities of RTFM. But we had a compiling project in the right form ready to next time.

RTFM examples

We ran a couple of the examples from the cortex-m-rtfm project, and learned a few tricks in doing that.

The smallest example

We started by trying out the smallest example:

$ git clone git@github.com:rtfm-rs/cortex-m-rtfm.git
$ cd cortex-m-rtfm
$ cargo build
$ cargo run --example smallest

I didn’t know about --example. It’s a feature of Cargo that compiles and runs files in an examples folder.

The output isn’t much:

$ cargo run --example smallest
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -gdb tcp::3333 -S -kernel target/thumbv7m-none-eabi/debug/examples/smallest`

It’s running the emulator for this application:

//! examples/smallest.rs

#![no_main]
#![no_std]

use panic_semihosting as _; // panic handler
use rtfm::app;

#[app(device = lm3s6965)]
const APP: () = {};

Notice how RTFM is using a different pattern to represent an application entry point. It’s an annotated constant. In this case it does nothing, hence the output is sitting around doing nothing.

The LM3S6965 device isn’t the hardware we’re using, but we can ignore that for the moment because we’re running in an emulator (and the emulator knows how to handle that hardware).

We learned a few things from running this code:

  • you can terminate the QEMU process by typing Ctrl-a x into the terminal window.

  • we modified the .cargo/config runner line to add -gdb tcp::3333 to enable us to connect to this process using GDB.

  • we also added -S to stop the process at start up, which allows us to connect via GDB and type cont to start execution.

  • we don’t need to run openocd when running the emulator, because that’s already baked into QEMU.

A gdb session might look like this:

$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/examples/smallest
Reading symbols from target/thumbv7m-none-eabi/debug/examples/smallest...
(gdb) target remote :3333
Remote debugging using :3333
Reset ()
    at /Users/richard/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.10/src/lib.rs:503
503	    __pre_init();
(gdb) cont
Continuing.

Notice we provided the location of the binary (via -q) and issued a command to talk to the process over port 3333 (the port we set in the runner inside .cargo/conf).

Init and Idle

The next example we looked at had a little more structure:

//! examples/idle.rs

#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init(_: init::Context) {
        hprintln!("init").unwrap();
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        hprintln!("idle").unwrap();
        debug::exit(debug::EXIT_SUCCESS);
        loop {}
    }
};

It doesn’t do a huge amount either. Here I’m running it without the -S flag:

$ cargo run --example idle
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -gdb tcp::3333 -kernel target/thumbv7m-none-eabi/debug/examples/idle`
init
idle

But what it does show us is an:

  • init entry point, which is the first thing run. It runs without interrupts, and gives us access to peripherals.

  • idle, which is optional and runs after init but has interrupts enabled.

Blinky

We didn’t explore interrupts or other features of RTFM this week. Instead, for today, we just crowbarred our blinky code into the init function.

We did this by taking last week’s code and modifying main.rs. I’ve put the full source code into a repository. It’s the usual story to run it: plug in the hardware, start openocd, then cargo run.

Here I’ll highlight only the start of the code, and the things we did to get this working:

use crate::hal::{prelude::*, stm32};
use stm32f4xx_hal as hal;

use cortex_m_semihosting::hprintln;

#[rtfm::app(device = stm32f4xx_hal::stm32, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        hprintln!("init").unwrap();

        // Cortex-M peripherals
        let cp: cortex_m::Peripherals = cx.core;

        // Device specific peripherals
        let dp: stm32::Peripherals = cx.device;

        // Set up the LED: it's connected to pin PA5 on the microcontroler
        let gpioa = dp.GPIOA.split();
        let mut led = gpioa.pa5.into_push_pull_output();

        // Code continues to use LEDs like last time
        // ...
    }
 }

We changed the rtfm::app annotation to:

  1. specify the specific hardware we’re using; and

  2. signify that we want to access peripherals.

The peripherals = true part means that the Context passed into init can access the devices field. This replaces the calls to take()-ing peripherals we used last week.

We also modified Cogo.toml to add:

cortex-m-rtfm = "0.5.0-beta.1"

You can install a cargo command to automate this:

$ cargo install cargo-edit
$ cargo add cortex-m-rtfm --allow-prerelease

Summary

At the end of the evening we had a working blinky application running inside the RTFM framework.

Next time we’ll probably:

  • figure out how to pass resources from init to idle; and

  • try to make use of RTFM constructs to schedule the blinking, rather than sleeping.

That will open the door to receiving and processing commands concurrently.

Updated 12 Nov 2019: clarity and corrections from Tim.