Icarus - Application Firmware Setup

Since the initial bring up of the board I’ve tested the PWM and IMU. Time to setup the actual framework for building the application firmware.

Ultimately the controller will have to do the following:

Everything needs to happen in a timely manner so the system response is consistent and predicable.

This is where usage of a Real Time Operation System (RTOS) comes in.

RTIC

RTIC stands for “Real time interrupt driven concurrency”, and is a framework for building real time systems.

The key features I like about RTIC are:

RTIC application setup is done with several derive macros. Combined with board support package I’ve already created for Icarus the setup is quiet clean.

#![no_std]
#![no_main]

use panic_halt as _;

#[rtic::app(device = icarus::hal::pac, peripherals = true, dispatchers = [EXTI3, EXTI4])]
mod app {
    use icarus::{
        prelude::*,
        cortex_m,
    };

    use systick_monotonic::*;

    #[monotonic(binds = SysTick, default = true)]
    type IcarusMono = Systick<100>;

    #[shared]
    struct Shared {}

    #[local]
    struct Local {
        stat1: PinStat1,
    }

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        let systick = cx.core.SYST;
        let mono = Systick::new(systick, 8_000_000);

        // Initialize hardware
        let hw = Icarus::new(cx.device).unwrap();

        // LED indicators
        let stat1 = hw.stat1;

        (
            Shared {},
            Local{
                stat1,
            },
            init::Monotonics(mono)
        )
    }

    ///
    /// Spawn tasks to handle incoming data and system state
    ///
    #[idle]
    fn idle(cx: idle::Context) -> ! {
        loop {
            cortex_m::asm::nop();
        }
    }

    ///
    /// Show activity using the status LEDs
    ///
    #[task(local = [stat1])]
    fn status_task(cx: status_task::Context) {
        cx.local.stat1.toggle().unwrap();
        status_task::spawn_after(500.millis()).unwrap();
    }
}

This sets up a RTIC application using the PAC from the icarus board support package. What is cool is I could swap the MCU type for something completely different (but still ARM based) and everything would still work.

This application is setup simply to toggle the STAT1 LED at 500ms intervals.

Hardware tasks can be bound to interrupt handlers.

defmt

I figured setting up logging sooner rather then later would be better as it can be a significant help with debugging. defmt is an efficient logging framework for embedded Rust applications. Though its typical examples have it logging through a debugger probe. I’d like it to log over a serial port (This will probably be over USB in the future).

defmt-bbq is a relatively new library for forwarding defmt frames into a global buffer. It provides a consumer that can be used to get the data from the defmt encoder.

#[rtic::app(device = icarus::hal::pac, peripherals = true, dispatchers = [EXTI3, EXTI4])]
mod app {
    // ...

    use icarus::{
        prelude::*,
        hal::{
            block,
            Toggle,
        },
        types::{PinStat1, Serial2},
    };

    #[local]
    struct Local {
        logger: defmt_bbq::Consumer,

        stat1: PinStat1,

        serial2: Serial2,
    }

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        // Setup defmt logging
        let logger = defmt_bbq::init().unwrap();

        let systick = cx.core.SYST;
        let mono = Systick::new(systick, 8_000_000);

        // Initialize hardware
        let hw = Icarus::new(cx.device).unwrap();

        // LED indicators
        let stat1 = hw.stat1;

        // Serial 2 used as logger port
        let serial2 = hw.usart2;

        // Spawn tasks
        status_task::spawn().unwrap();

        (
            Shared {},
            Local{
                logger,
                stat1,
                serial2,
            },
            init::Monotonics(mono)
        )
    }

    ///
    /// Spawn tasks to handle incoming data and system state
    ///
    #[idle(local = [logger, serial2])]
    fn idle(cx: idle::Context) -> ! {
        loop {
            // Write logs to serial port 2
            if let Ok(grant) = cx.local.logger.read() {
                for byte in grant.buf() {
                    block!(cx.local.serial2.write(*byte)).unwrap();
                }

                let glen = grant.len();
                grant.release(glen);
            }
        }
    }

    ///
    /// Show activity using the status LEDs
    ///
    #[task(local = [stat1])]
    fn status_task(cx: status_task::Context) {
        defmt::println!("hello!");
        cx.local.stat1.toggle().unwrap();
        status_task::spawn_after(500.millis()).unwrap();
    }
}

What makes defmt efficient is that it actually does the formatting of a log message on the host system. The format strings are store in the ELF file and need to be loaded by a program and used to decode the serial stream.

The code is used for this is almost an exact copy of https://github.com/knurling-rs/defmt/blob/main/print/src/main.rs with the difference being the incoming data is from the serial port.

    let mut buf: Vec<u8> = vec![0; READ_BUF_SIZE];
    let mut stream_decoder = table.new_stream_decoder();

    loop {
        // Check if the user attempted to exit the program
        let exit = rx.try_recv();
        if exit.is_ok() {
            break;
        }

        match ser.read(buf.as_mut_slice()) {
            Ok(n) => {
                stream_decoder.received(&buf[..n]);

                match stream_decoder.decode() {
                    Ok(frame) => forward_to_logger(&frame, location_info(&locs, &frame, &current_dir)),
                    Err(DecodeError::UnexpectedEof) => break,
                    Err(DecodeError::Malformed) => match table.encoding().can_recover() {
                        false => return Err(DecodeError::Malformed.into()),
                        true => {
                            eprintln!("Frame Malformed, recoverable");
                            continue;
                        }
                    }
                }
            },
            Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (),
            Err(e) => bail!("{:?}", e),
        }
    }

The log command requires the path to the elf file to be specified:

icarus-cli --port <port> log ./target/thumbv7em-none-eabihf/release/icarus-app

Though this could be found automatically, or built directly into the binary.

Next steps: