This week, we will look at the task scheduling feature of the RTFM framework. This is a redo of the last project that reads when new data is available on USART and sends the accumulated data through USART every second.

Here are three things we implement:

  1. initialize USART TX, RX, and producer/consumer for a buffer and store them for later use.
  2. When new data arrives on USART, enter an interrupt context to read the data and write it to the buffer.
  3. Every second, see if there are any data available in the buffer and send them through USART.

Instead of using a Timer to trigger an interrupt every second, we will schedule a software task this time.

Hardware

Crates

Code

Full code is available on GitHub

Implementation

1 and 2 in the list above are the same as the last project. In this post, I will only focus on 3, how to schedule software tasks.

This shows an overview of the application.

// import crates, declare buffer queue, etc.

const PERIOD: u32 = 16_000_000;

#[rtfm::app(device = hal::stm32, peripherals = true, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    struct Resources {
        cons: Consumer<'static, U1024>,
        prod: Producer<'static, U1024>,
        tx: hal::serial::Tx<USART3>,
        rx: hal::serial::Rx<USART3>,
    }

    #[init(schedule = [tx_write])]
    fn init(cx: init::Context) -> init::LateResources {
        // Schedule a tx_write task
        // Split bbqueue Producer and Consumer
        // Set up USART, enable interrupt
        // Initialization of late resources
    }

    #[task(binds = USART3, resources = [prod, rx])]
    fn usart3(cx: usart3::Context) {
        // receive data from USART
        // write to the queue
    }

    #[task(schedule = [tx_write], resources = [cons, tx])]
    fn tx_write(cx: tx_write::Context) {
        // read from the queue and write to USART
        // reschedule tx_write
    }

    // This is required for the software task fn tx_write()
    // This can be any interrupt not used by the hardware
    extern "C" {
        fn USART1();
    }
};

There are two tasks here: usart3 and tx_write.

usart3 is a hardware task. We enable RX interrupt in init.

tx_write is the software task we want to focus on.

Schedule a task to run sometime in the future

To schedule a task, we use a Monotonic timer.

Let’s look at the #[app] attribute where we specify the microcontroller’s 32-bit cycle counter as our monotonic timer.

#[rtfm::app(device = hal::stm32, 
	peripherals = true, 
	monotonic = rtfm::cyccnt::CYCCNT)]

Since we set the system clock to 16MHz, clocking 16,000,000 times equals to 1 second. We declare a constant like this.

const PERIOD: u32 = 16_000_000;

Just like we specify which resources to use in the context attribute, we need to pass tasks to the schedule arguments of the context attribute.

Let’s look at init and tx_write where we schedule tasks. See we have schedule argument in the context attribute. We pass our tx_write as an argument.

    #[init(schedule = [tx_write])]
    fn init(cx: init::Context) -> init::LateResources {
	    cx.schedule
	        .tx_write(cx.start + PERIOD.cycles())
	        .unwrap();
		// ...
    }

    #[task(schedule = [tx_write], resources = [cons, tx])]
    fn tx_write(cx: tx_write::Context) {
		cx.schedule
	        .tx_write(cx.scheduled + PERIOD.cycles())
	        .unwrap();
        // ...
    }

In the tasks, we schedule a task by calling Context’s schedule API. In init(), we schedule tx_write to run 16,000,000 cycles (= 1 second) in the future.

cx.schedule.tx_write(cx.start + PERIOD.cycles()).unwrap();

cx.start returns start time of the system, which should be 0. The line above schedules a task to run after the specified cycles (PERIOD: u32 = 16_000_000).

In tx_write, we write to USART and reschedule a task so that it is called again after the specified clock cycles.

cx.schedule.tx_write(cx.scheduled + PERIOD.cycles()).unwrap();

We can access the previously scheduled time through cx.scheduled. We add 16,000,000 cycles to the whatever scheduled time.

Nice. We can now fire a task every second using RTFM framework.

NVIC as a dispatcher

By the way, you may wonder why we have these lines.

    extern "C" {
        fn USART1();
    }

My understanding is that RTFM uses the NVIC to handle scheduling. We just need to specify enough unused extern interrupts so that RTFM can dispatch software tasks.

Since we only have one software task for this project, one is enough here. It doesn’t have to be fn USART1(). It can be TIM2 or whatever interrupt not used by the hardware. If we have two software tasks with different priorities, we need two extern interrupts.