So far, all of my hardware examples have run directly on the microcontroller. To combine the rendering, input and sound features of Quinti-Maze 2022 on the microcontroller I’ve decided to use RTIC, “A concurrency framework for building real-time systems”.

The core of RTIC is the notion of tasks.

Tasks, defined with #[task], are the main mechanism of getting work done in RTIC.

Tasks can

  • Be spawned (now or in the future)
  • Receive messages (message passing)
  • Prioritized allowing preemptive multitasking
  • Optionally bind to a hardware interrupt

RTIC makes a distinction between “software tasks” and “hardware tasks”. Hardware tasks are tasks that are bound to a specific interrupt vector in the MCU while software tasks are not.

Here is the embedded “hello, world”, using RTIC to blink the LED five times a second.

#![no_std]
#![no_main]

#[cfg(feature = "feather_m4")]
use feather_m4 as bsp;

use panic_semihosting as _;
use rtic;

#[rtic::app(device = bsp::pac, peripherals = true, dispatchers = [EVSYS_0])]
mod app {
    use super::*;
    use bsp::{hal, pin_alias};
    use hal::clock::GenericClockController;
    use hal::pac::Peripherals;
    use hal::prelude::*;
    use rtt_target::rtt_init_print;
    use systick_monotonic::*;

    #[local]
    struct Local {
        red_led: bsp::RedLed,
    }

    #[shared]
    struct Shared {}

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

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        rtt_init_print!();
        let mono = Systick::new(cx.core.SYST, 120_000_000);
        let mut peripherals: Peripherals = cx.device;
        let pins = bsp::Pins::new(peripherals.PORT);
        let red_led: bsp::RedLed = pin_alias!(pins.red_led).into();

        let mut _clocks = GenericClockController::with_internal_32kosc(
            peripherals.GCLK,
            &mut peripherals.MCLK,
            &mut peripherals.OSC32KCTRL,
            &mut peripherals.OSCCTRL,
            &mut peripherals.NVMCTRL,
        );
        // Start the blink task
        blink::spawn().unwrap();

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

    #[task(local = [red_led])]
    fn blink(cx: blink::Context) {
        cx.local.red_led.toggle().unwrap();
        blink::spawn_after(100.millis()).ok();
    }
}

RTIC will make it much easier for me to interleave the different responsibilities of the program. For example, while the key scanner is waiting for the GPIO to settle low for scanning a row, the screen can update.

I’ve decided to make the game structure shared between the scanning and rendering tasks. Since the rendering task keeps the game locked for the entire task, it will prevent the scanner from sending a key during rendering. This could cause some lag in key detection, but so far I’m not feeling noticeable lag.

The one thing that RTIC does that I don’t currently know how to do, though, is keep a tick count in order to show elapsed time. The ATSAMD M4 clearly has the capability, but here the lack of specific examples, and my limited appetite to puzzle out exactly how, makes me very happy to use RTIC.