Skip to Content
Course content

182: RTIC Framework Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of RTIC (Real-Time Interrupt-driven Concurrency) like running a professional high-end kitchen. In a home kitchen, you might just do one thing at a time—boil the pasta, then chop the onions. But in a pro kitchen, everything is event-driven. The timer for the soufflé goes off (an interrupt), a waiter yells that a table is ready (another interrupt), and the head chef decides who gets to use the one available sauté pan (resource management).

The magic of a pro kitchen isn't just speed; it's priority. If the fish is burning, the chef doesn't care that the salad is being plated; the fish gets immediate attention. RTIC brings this exact philosophy to your microcontroller. Instead of a giant loop {} where you're constantly polling flags and hoping you didn't miss a button press, RTIC lets you define specific tasks that trigger only when they're needed, with strict priorities to ensure the most critical code always wins.

Mapping the Kitchen to the Code

When you look at an RTIC application, you'll see a structure that differs from a standard main function. Here is how the analogy maps to the framework:

  • The Kitchen Layout (The #[app] macro): This is where you define your entire system—what hardware you're using and how it's wired.
  • The Pantry (Shared Resources): These are variables or peripherals (like a GPIO pin or an I2C bus) that multiple tasks need to access. In RTIC, these are protected so you can't have two tasks fighting over the same "pan" at the same time.
  • The Chef's Private Kit (Local Resources): These are variables that only one specific task can touch. No locking is required because no one else has the key to that drawer.
  • The Order Tickets (Tasks): These are your functions. Some are "hardware tasks" (triggered by a timer or a pin change) and some are "software tasks" (triggered by other parts of your code).

Handling Shared Resources Without the Headache

In standard embedded Rust, sharing a peripheral between the main loop and an interrupt usually involves static mut and a lot of unsafe blocks, or a heavy Mutex. It's clunky. RTIC handles this via a "priority-based locking" mechanism.


#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [EXTI0])]
mod app {
    #[shared]
    struct Shared {
        // This is our "shared pan"
        led_status: bool,
    }

    #[local]
    struct Local {
        // This is the "private kit"
        button: gpio::Pin,
    }

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        // Setup hardware here...
        (Shared { led_status: false }, Local { button }, init::Monotonics())
    }

    // A high-priority hardware task
    #[task(binds = EXTI1, shared = [led_status], priority = 2)]
    fn on_button_press(mut cx: on_button_press::Context) {
        // To access the shared resource, we use a lock
        cx.shared.led_status.lock(|status| {
            *status = !*status;
        });
    }
}

Notice the .lock(|status| { ... }) call. This is where RTIC shines. Instead of a traditional mutex that might block your whole program, RTIC uses the microcontroller's own priority levels. If a task with priority 2 locks a resource, the system temporarily prevents any other task that *also* uses that resource from preempting it. It's an incredibly efficient way to guarantee data safety without the overhead of a full OS.

The Dance of Software and Hardware Tasks

I want to point out a distinction that often trips people up: the difference between binds = ... and software tasks. A hardware task is bound to a specific interrupt vector (like a timer or a GPIO pin). It's the "Order Ticket" that arrives from the outside world.

A software task, however, is something you trigger manually. Imagine the "fish burning" interrupt triggers a hardware task, which then spawns a software task to "alert the manager." You do this using the spawn method. This keeps your interrupt handlers lean—you do the bare minimum in the hardware task and push the heavy lifting to a lower-priority software task so you don't block other critical interrupts.




📋 Practical Task

Build a Priority-Based Emergency Stop System

You are tasked with implementing a safety system for a motor controller using RTIC. The system must handle two different inputs with different priorities.

Requirements:

  • Shared Resource: Create a shared boolean called motor_enabled.
  • High Priority Task: Create a hardware task bound to an EmergencyStop interrupt (priority 3). When triggered, it must immediately set motor_enabled to false.
  • Low Priority Task: Create a hardware task bound to a UserToggle interrupt (priority 1). When triggered, it should toggle the motor_enabled state using a .lock().
  • The Logic: Ensure that if the UserToggle is currently locking the resource, the EmergencyStop can still preempt it (or handle the locking priority correctly) to ensure the motor stops as fast as possible.

Deliverable: Write the RTIC #[app] module structure including the Shared and Local structs, the init function, and the two task handlers described above. You don't need to provide the full HAL initialization code—you can assume the peripherals are passed in via the context.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.