Skip to content

User Guide Syntax

Zakaria Madaoui edited this page Aug 9, 2026 · 19 revisions

Syntax Reference

This page describes the RTICX attributes currently supported by the core pass (rticx-core) and the software-tasks pass (rticx-sw-pass). Other officially supported passes may add their own attributes; those will be documented as they are added.

Application-level attributes: #[app(...)]

The #[<distro>::app(...)] attribute is placed on the module that contains your application.

Common arguments

#[<distro>::app(device = rp2040_hal::pac, dispatchers = [DMA_IRQ_0])]
mod my_app { ... }
  • device = path — path to the PAC (Peripheral Access Crate) for the target.
  • cores = N — number of cores (default is 1).
  • dispatchers = [...] — single-core list of interrupt dispatchers for software tasks.
  • dispatchers = [[...], [...]] — per-core dispatcher lists for software tasks on multicore targets.

Shared resources: #[shared]

#[shared]
struct SharedResources {
    alarm: Alarm0,
    led: LedOutPin,
}

Fields of SharedResources are accessed inside tasks via self.shared().name and locked with .lock(|resource| { ... }).

Init: #[init]

#[init]
fn system_init() -> SharedResources {
    // setup code
    SharedResources { alarm, led }
}

The init function runs once at startup and returns the initial shared resources. On multicore targets, use #[init(core = N)].

Idle: #[idle]

#[idle]
struct MyIdleTask;
impl RticIdleTask for MyIdleTask {
    fn init() -> Self { Self }
    fn exec(&mut self) -> ! {
        loop { /* low-power or background work */ }
    }
}

On multicore targets, use #[idle(core = N)].

Hardware tasks: #[task(...)]

#[task(binds = TIMER_IRQ_0, priority = 3, shared = [alarm, led])]
struct MyTask;
impl RticTask for MyTask {
    fn init() -> Self { Self }
    fn exec(&mut self) { /* handler code */ }
}
  • binds = IRQ — interrupt line that triggers this task.
  • priority = N — task priority. Higher values preempt lower values.
  • shared = [...] — list of shared resource fields this task accesses.
  • core = N — on multicore targets, the core on which the interrupt lives.

Software tasks: #[sw_task(...)]

Software tasks are provided by the rticx-sw-pass compilation pass. They are triggered by other tasks via message queues instead of direct interrupts.

#[sw_task(priority = 2, shared = [led], spawn_by = 0, core = 0)]
struct MySwTask;
impl RticSwTask for MySwTask {
    type SpawnInput = u16;
    fn init() -> Self { Self }
    fn exec(&mut self, input: u16) { /* handler code */ }
}
  • priority = N — dispatcher hardware task priority.
  • shared = [...] — shared resources this task accesses.
  • core = N — the core on which this task executes.
  • spawn_by = N — the only core allowed to spawn this task. If omitted, any core may spawn it.

Spawning software tasks

  • MySwTask::spawn(value) — spawn from the same core.
  • MySwTask::spawn_from(core, value) — spawn from another core, when allowed.

Resource locking

Inside a task, shared resources are accessed through the shared proxy and locked to obtain a mutable reference:

self.shared().led.lock(|led| {
    let _ = led.set_high();
});

The lock implementation is target-specific and generated by the distribution backend.

Async software tasks: #[async_task(...)]

Async tasks provide async fn / .await support in RTICX. Each async task has a future polled by an executor loop — which is a hardware-task dispatcher shared by all tasks at the same (core, priority).

Async tasks require the asynctasks distribution feature and the rticx-async crate in your dependencies.

Syntax

#[async_task(priority = 2, shared = [counter])]     // core and spawn_by optional
struct Worker {
    rx: Receiver<'static, u32, 4>,
}

impl RticAsyncTask for Worker {
    type InitArgs = Self;           // late-init via TaskInits
    type SpawnInput = ();           // input type for exec
    fn init(s: Self::InitArgs) -> Self { s }
    async fn exec(&mut self, input: Self::SpawnInput) { ... }
}

Channels

Task-to-task communication uses channels from rticx-async:

use rticx_async::{channel::{Receiver, Sender}, make_channel};

let (tx, rx) = make_channel!(u32, 4);     // 'static lifetimes, no unsafe

The make_channel! macro creates a channel backed by static memory, returning (Sender<'static, T, N>, Receiver<'static, T, N>). Each call site can only execute once (a run-time guard panics on repeated calls). Channels must be split in #[init] and passed into tasks via TaskInits.

Spawning

let _ = Worker::spawn(input);                       // same core → Result<(), Input>
let _ = Worker::spawn_from(core_token, input);      // cross-core (spawn_by)
// Ok(()) on success, Err(input) if the task is already running

Dispatcher requirements

Every async priority group needs a dispatcher IRQ listed in dispatchers = [...] (one per priority level). Dispatchers are hardware tasks bound to the listed IRQs; they poll all tasks in their priority group and self-pend until all futures complete.

Heap management

Future storage uses Pin<Box<dyn Future>>. The distribution manages a hidden global heap (embedded-alloc) — the end user sees no #[global_allocator] or heap configuration.

Other officially supported passes

The following passes are available but may add their own attributes. Detailed syntax for each will be added as the documentation expands:

  • rticx-deadline-pass — converts deadline = D attributes into RTICX priorities.
  • rticx-auto-assign — automatically assigns core = N based on shared resource usage.
  • rticx-async-pass — adds #[async_task] with async/await, channels, and executor dispatching (see Async software tasks above).

See the Supported Distributions page to know which passes are enabled by each distribution.

Next steps

Clone this wiki locally