Rust’s Real Time For the Masses (RTFM): Between bare metal and Real Time OS

In this post, I will talk about my first experiment with Real Time For the Masses (RTFM) Framework . My understanding is that RTFM finds itself between bare metal and Real Time OS. Bare metal’s small footprint is great for a resource constrained platform. But managing resources and tasks can be very difficult. Real Time OS provides rich features. However, it comes with so much overhead. RTFM provides an ability to easily schedule tasks and guarantees safe access to shared resources without much runtime overhead.

Here are RTFM’s features listed in the doc .

  • Tasksas the unit of concurrency. Tasks can be event triggered (fired in response to asynchronous stimuli) or spawned by the application on demand.

  • Message passingbetween tasks. Specifically, messages can be passed to software tasks at spawn time.

  • A timer queue. Software tasks can be scheduled to run at some time in the future. This feature can be used to implement periodic tasks.

  • Support for prioritization of tasks and, thus, preemptive multitasking .

  • Efficient and data race free memory sharingthrough fine grained *priority based* critical sections.

  • Deadlock free executionguaranteed at compile time. This is an stronger guarantee than what’s provided by the standard Mutex abstraction.

  • Minimal scheduling overhead. The task scheduler has minimal software footprint; the hardware does the bulk of the scheduling.

  • Highly efficient memory usage: All the tasks share a single call stack and there’s no hard dependency on a dynamic memory allocator.

Timer interrupt with RTFM

This week’s program toggles an LED when triggered by timer interrupts.

Hardware

Crates

Code

Full code is available on GitHub

Implementation

This experiment is basically a redo of my timer interrupt experiment from a few weeks ago . Let’s look at how I did it without RTFM.

Without RTFM

static LED: Mutex<RefCell<Option<PB7<Output<PushPull>>>>> = Mutex::new(RefCell::new(None));
static TIMER_TIM2: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> = Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    let dp = stm32::Peripherals::take().unwrap();
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.sysclk(48.mhz()).freeze();

    // Set up the LED
    let gpiob = dp.GPIOB.split();
    let led = gpiob.pb7.into_push_pull_output();

    // Set up the timer
    let mut timer = Timer::tim2(dp.TIM2, 5.hz(), clocks);
    timer.listen(Event::TimeOut);
	
    // Move shared resources to Mutex
    free(|cs| {
        TIMER_TIM2.borrow(cs).replace(Some(timer));
        LED.borrow(cs).replace(Some(led));
    });

    // Enable interrupt
    stm32::NVIC::unpend(stm32::Interrupt::TIM2);
    unsafe {
        stm32::NVIC::unmask(stm32::Interrupt::TIM2);
    }

    loop {}
}

#[interrupt]
fn TIM2() {
    free(|cs| {
        if let Some(ref mut tim2) = TIMER_TIM2.borrow(cs).borrow_mut().deref_mut() {
            tim2.clear_interrupt(Event::TimeOut);
        }
        if let Some(ref mut led) = LED.borrow(cs).borrow_mut().deref_mut() {
            led.toggle().unwrap();
        }
    });
}

The application does these:

#[interrupt]

Now, let’s do the same with RTFM. Implementation with RTFM looks like this.

With RTFM

#[rtfm::app(device = hal::stm32, peripherals = true)]
const APP: () = {
    struct Resources {
        led: PB7<Output<PushPull>>,
        timer: Timer<stm32::TIM2>,
    }

    #[init]
    fn init(cx: init::Context) -> init::LateResources {
        let rcc = cx.device.RCC.constrain();
        let clocks = rcc.cfgr.freeze();

        // Set up the LED
        let gpiob = cx.device.GPIOB.split();
        let led = gpiob.pb7.into_push_pull_output();

        // Set up the timer
        let mut timer = Timer::tim2(cx.device.TIM2, 5.hz(), clocks);
        timer.listen(Event::TimeOut);

        // Initialization of late resources
        init::LateResources { led, timer }
    }

    #[task(binds = TIM2, resources = [timer, led])]
    fn tim2(cx: tim2::Context) {
        cx.resources.timer.clear_interrupt(Event::TimeOut);
        cx.resources.led.toggle().unwrap();
    }
};

This looks very different from what we have seen so far. To start, there is no #[entry] attribute. This may be confusing. We are supposed to initialize resources, enable interrupts, and infinitely loop in main like this.

#[entry]
fn main() -> ! {
	// configure pins, set up interrupt, wrap shared resouces with Mutex, etc.
    loop {}
}

Also, we can’t find NVIC anywhere either. Below is how we normally enable a timer interrupt without RTFM.

stm32::NVIC::unpend(Interrupt::TIM2);
unsafe {
    stm32::NVIC::unmask(Interrupt::TIM2);
}

So, where are our #[entry] and functions to enable interrupts?

RTFM framework uses macros. When we use attributes like #[init] and #[task] , they expand and generate main and all that.

Code generation happens in the background and we don’t need to see the expanded code. We can just build an app and flash it to a device. But I think it is a good idea to see what exactly RTFM is doing first. At least for me, seeing the expanded code helped me understand what was going on.

Expanded Code

#[allow(non_snake_case)]
fn init(cx: init::Context) -> init::LateResources {
    let rcc = cx.device.RCC.constrain();
    let clocks = rcc.cfgr.freeze();
    let gpiob = cx.device.GPIOB.split();
    let led = gpiob.pb7.into_push_pull_output();
    let mut timer = Timer::tim2(cx.device.TIM2, 5.hz(), clocks);
    timer.listen(Event::TimeOut);
    init::LateResources { led, timer }
}
#[allow(non_snake_case)]
fn tim2(cx: tim2::Context) {
    use rtfm::Mutex as _;
    cx.resources.timer.clear_interrupt(Event::TimeOut);
    cx.resources.led.toggle().unwrap();
}
#[doc = r" Resources initialized at runtime"]
#[allow(non_snake_case)]
pub struct initLateResources {
    pub led: PB7<Output<PushPull>>,
    pub timer: Timer<stm32::TIM2>,
}
#[allow(non_snake_case)]
#[doc = "Initialization function"]
pub mod init {
    #[doc(inline)]
    pub use super::initLateResources as LateResources;
    #[doc = r" Execution context"]
    pub struct Context {
        #[doc = r" Core (Cortex-M) peripherals"]
        pub core: rtfm::export::Peripherals,
        #[doc = r" Device peripherals"]
        pub device: hal::stm32::Peripherals,
    }
    impl Context {
        #[inline(always)]
        pub unsafe fn new(core: rtfm::export::Peripherals) -> Self {
            Context {
                device: hal::stm32::Peripherals::steal(),
                core,
            }
        }
    }
}
#[allow(non_snake_case)]
#[doc = "Resources `tim2` has access to"]
pub struct tim2Resources<'a> {
    pub timer: &'a mut Timer<stm32::TIM2>,
    pub led: &'a mut PB7<Output<PushPull>>,
}
#[allow(non_snake_case)]
#[doc = "Hardware task"]
pub mod tim2 {
    #[doc(inline)]
    pub use super::tim2Resources as Resources;
    #[doc = r" Execution context"]
    pub struct Context<'a> {
        #[doc = r" Resources this task has access to"]
        pub resources: Resources<'a>,
    }
    impl<'a> Context<'a> {
        #[inline(always)]
        pub unsafe fn new(priority: &'a rtfm::export::Priority) -> Self {
            Context {
                resources: Resources::new(priority),
            }
        }
    }
}
#[doc = r" Implementation details"]
const APP: () = {
    #[doc = r" Always include the device crate which contains the vector table"]
    use hal::stm32 as _;
    #[cfg(core = "1")]
    compile_error!("specified 1 core but tried to compile for more than 1 core");
    #[allow(non_upper_case_globals)]
    #[link_section = ".uninit.rtfm0"]
    static mut timer: core::mem::MaybeUninit<Timer<stm32::TIM2>> = core::mem::MaybeUninit::uninit();
    #[allow(non_upper_case_globals)]
    #[link_section = ".uninit.rtfm1"]
    static mut led: core::mem::MaybeUninit<PB7<Output<PushPull>>> =
        core::mem::MaybeUninit::uninit();
    #[allow(non_snake_case)]
    #[no_mangle]
    unsafe fn TIM2() {
        const PRIORITY: u8 = 1u8;
        rtfm::export::run(PRIORITY, || {
            crate::tim2(tim2::Context::new(&rtfm::export::Priority::new(PRIORITY)))
        });
    }
    impl<'a> tim2Resources<'a> {
        #[inline(always)]
        unsafe fn new(priority: &'a rtfm::export::Priority) -> Self {
            tim2Resources {
                timer: &mut *timer.as_mut_ptr(),
                led: &mut *led.as_mut_ptr(),
            }
        }
    }
    #[no_mangle]
    unsafe extern "C" fn main() -> ! {
        rtfm::export::assert_send::<PB7<Output<PushPull>>>();
        rtfm::export::assert_send::<Timer<stm32::TIM2>>();
        rtfm::export::interrupt::disable();
        let mut core: rtfm::export::Peripherals = core::mem::transmute(());
        let _ = [(); ((1 << hal::stm32::NVIC_PRIO_BITS) - 1u8 as usize)];
        core.NVIC.set_priority(
            hal::stm32::Interrupt::TIM2,
            rtfm::export::logical2hw(1u8, hal::stm32::NVIC_PRIO_BITS),
        );
        rtfm::export::NVIC::unmask(hal::stm32::Interrupt::TIM2);
        core.SCB.scr.modify(|r| r | 1 << 1);
        let late = init(init::Context::new(core.into()));
        led.as_mut_ptr().write(late.led);
        timer.as_mut_ptr().write(late.timer);
        rtfm::export::interrupt::enable();
        loop {
            rtfm::export::wfi()
        }
    }
};

I find it very interesting to read the expanded code line by line. I can see fn main() at the bottom of the expanded code. In main , interrupt is enabled with NVIC and it infinitely waits for interrupts.

Although I don’t understand everything, it seems to me that resources are nicely managed with mod . I guess RTFM makes it a bit easier to handle resources and tasks.

Attributes

This is how our RTFM application is constructed.

#[rtfm::app(device = hal::stm32, peripherals = true)]
const APP: () = {

    struct Resources {
        led: PB7<Output<PushPull>>,
        timer: Timer<stm32::TIM2>,
    }

    #[init]
    fn (cx: init::Context) -> init::LateResources  {
        // omitted
    }
	
    // not used for this week's experiment
    // #[idle]
    // fn idle(c: idle::Context) -> ! {}

    #[task(binds = TIM2, resources = [timer, led])]
    fn tim2(c: tim2::Context) {
        // omitted
    }
};

Let’s look at the four attributes: app , init , idle and task .

1. app

All RTFM applications start with app .

#[rtfm::app(device = hal::stm32, peripherals = true)]
const App(): = {
    //
}

device argument is mandatory. We want to specify a path to our PAC crate here. In my case, it is stm32f4xx-hal ’s stm32 .

periperals = true makes it possible to access PAC’s core and device modules. For example, let rcc = cx.device.RCC.constrain();

2. init

app expects initialization functions in #[init] . This is the first thing that runs in a RTFM application. Interrupts are always disabled in this. We initialize resources in here.

3. idle

We don’t use this for this week’s experiment. But when we have this task, it runs after init with interrupt enabled.

4. task

#[task] attribute makes it possible to declare interrupt handler. Use binds to attach a handler to a specific interrupt.

#[task(binds = UART0)]
fn uart0(_: uart0::Context) {
    //
}

It is possible to set priority using priority argument. A higher priority task preempts a lower priority task. If not specified, the priority is set to 1. ( idle task has the lowest priority, 0.)

#[task(binds = UART0, priority = 1)]
fn uart0(_: uart0::Context) {
    //
}

#[task(binds = UART1, priority = 2)]
fn uart1(_: uart1::Context) {
    //
}

See the RTFM doc for more info .

Shared Resources

Before #[init] , we have Resources . These are late resources we want to initialize at runtime.

struct Resources {
    led: PB7<Output<PushPull>>,
    timer: Timer<stm32::TIM2>,
}

These are like Mutexes we used in a previous experiment.

static LED: Mutex<RefCell<Option<PB7<Output<PushPull>>>>> 
    = Mutex::new(RefCell::new(None));
    
static TIMER_TIM2: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> 
    = Mutex::new(RefCell::new(None));

Instead of Mutex, RTFM uses LateResources . We declare struct Resources for all the resources we want to share across different contexts. These resources must be initialized in init and returned.

struct Resources {
    led: PB7<Output<PushPull>>,
    timer: Timer<stm32::TIM2>,
}

#[init]
fn init(cx: init::Context) -> init::LateResources {
    let rcc = cx.device.RCC.constrain();
    let clocks = rcc.cfgr.freeze();

    // Set up the LED
    let gpiob = cx.device.GPIOB.split();
    let led = gpiob.pb7.into_push_pull_output();

    // Set up the timer
    let mut timer = Timer::tim2(cx.device.TIM2, 5.hz(), clocks);
    timer.listen(Event::TimeOut);

    // Initialization of late resources
    init::LateResources { led, timer }
}

(* If we don’t have late resources, we don’t need to return anything.)

The resources can be safely accessed in tasks. Let’s look at our TIM2 interrupt handler.

#[task(binds = TIM2, resources = [timer, led])]
fn tim2(cx: tim2::Context) {
    cx.resources.timer.clear_interrupt(Event::TimeOut);
    cx.resources.led.toggle().unwrap();
}

resources = [timer, led]) defines which resources we want to use in this context. We can access them by following a path from context to resources: cx.resources.timer , cx.resources.led .

In this interrupt handler, we, using the shared resources, clear the interrupt flag and toggle the LED just like non RTFM version.

That’s it. Our first experiment with RTFM. I am liking this framework. I will explore more in the future projects.

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章