-
Notifications
You must be signed in to change notification settings - Fork 8
User Guide Syntax
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.
The #[<distro>::app(...)] attribute is placed on the module that contains your application.
#[<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 is1). -
dispatchers = [...]— single-core list of interrupt dispatchers for software tasks. -
dispatchers = [[...], [...]]— per-core dispatcher lists for software tasks on multicore targets.
#[shared]
struct SharedResources {
alarm: Alarm0,
led: LedOutPin,
}Fields of SharedResources are accessed inside tasks via self.shared().name and locked with .lock(|resource| { ... }).
#[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]
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)].
#[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 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.
-
MySwTask::spawn(value)— spawn from the same core. -
MySwTask::spawn_from(core, value)— spawn from another core, when allowed.
Spawning from #[init] always returns Err(input). Use #[post_init] to spawn initial tasks.
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.
RTICX supports single-binary multicore: all cores share the same address space and execute from a single firmware image. Tasks are assigned to cores, and cross-core communication uses software/async task spawning.
#[<distro>::app(device = rp2040_hal::pac, cores = 2, dispatchers = [[DMA_IRQ_0], [DMA_IRQ_1]])]
mod my_app { ... }-
cores = 2— declares the number of cores. Default is1. -
dispatchers = [[...], [...]]— per-core dispatcher lists. The outer array indexes cores; inner lists are dispatcher IRQs for that core's software/async tasks. -
device = [path0, path1]— per-core PAC paths (when they differ). A single path is replicated across all cores.
Every task must specify which core it runs on:
| Task type | Attribute |
|---|---|
#[init] |
#[init(core = 0)] |
#[idle] |
#[idle(core = 0)] |
#[task(...)] |
#[task(binds = IRQ, core = 0, ...)] |
#[sw_task(...)] |
#[sw_task(priority = N, core = 0, ...)] |
#[async_task(...)] |
#[async_task(priority = N, core = 0, ...)] |
The core = N on a hardware task specifies which core the interrupt line belongs to. On software/async tasks, it specifies which core's dispatcher manages the task.
#[shared(core = 0)]
struct SharedCore0 {
uart_tx: UartTx,
}
#[shared(core = 1)]
struct SharedCore1 {
dma_buf: DmaBuffer,
}Each core gets its own shared resource group. Only tasks on that core may access those resources. SRP ceilings are computed independently per core.
Cross-core tasks are software/async tasks where one core produces (spawns) and another core consumes (executes):
#[sw_task(priority = 2, core = 0, spawn_by = 1)]
struct Core0Task;
impl RticSwTask for Core0Task {
type SpawnInput = u32;
fn init() -> Self { Self }
fn exec(&mut self, input: u32) { /* runs on core 0 */ }
}-
core = 0— this task executes on core 0. -
spawn_by = 1— only core 1 may spawn this task.
When spawn_by == core (or is omitted), the task is core-local and uses Task::spawn(input). When spawn_by != core, the task is cross-core and uses Task::spawn_from(core_token, input).
// From a task on core 1, spawn a task on core 0:
Core0Task::spawn_from(Self::current_core(), 42)?;Self::current_core() returns a zero-sized token (__rticx__internal__Core1) that proves at compile time which core is calling spawn_from. The token is type-checked against the task's spawn_by constraint — attempting to call spawn_from from the wrong core is a compile error.
A priority level (on a given core) can be used for either core-local tasks or cross-core tasks, but never both. This is because each dispatcher queue has exactly one producer and one consumer:
| Task kind | Producer | Consumer | Same core? |
|---|---|---|---|
Core-local (spawn_by == core) |
Core N | Core N (dispatcher) | Yes |
Cross-core (spawn_by != core) |
Core M | Core N (dispatcher) | No |
If a priority level mixed core-local and cross-core tasks, the dispatcher queue would have multiple producers on different cores, breaking the single-producer guarantee of the SPSC queue.
For the reasoning behind these constraints — SPSC queue ordering, how cross-core interrupts are delivered (e.g. SIO FIFO on RP2040), and async waker cross-core mechanics — see the Architecture and Multicore Architecture & Design Decisions pages.
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 priority or (core, priority) pair in multicore case.
Async tasks require the async distribution feature and the rticx-async crate in your dependencies.
rticx-asyncuses atomics and critical sections. On targets without native atomics the distribution enables theatomic-critical-sectionfallback, and multicore targets must use a spinlock-based critical-section backend (e.g. the RP2040 hardware spinlocks). See Multicore Architecture & Design Decisions.
#[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) { ... }
}
Task-to-task communication uses channels from rticx-async:
use rticx_async::{channel::{Receiver, Sender}, make_channel};
let (tx, rx) = make_channel!(u32, 4); 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 created and split in #[init] and passed into tasks via TaskInits.
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
// Err(input) if called from #[init] (tasks not yet initialized)Spawning from #[init] always returns Err(input). Use #[post_init] to spawn initial tasks.
Setting priority = 0 assigns the task to the idle executor — a framework-generated #[idle] task that busy-polls all priority-0 futures. Priority-0 tasks do not require a dispatcher IRQ.
#[async_task(priority = 0)]
struct Background;
impl RticAsyncTask for Background {
type InitArgs = ();
type SpawnInput = ();
fn init(_: Self::InitArgs) -> Self { Self }
async fn exec(&mut self, _input: ()) {
loop {
do_background_work().await;
}
}
}- No dispatcher needed: priority-0 tasks are polled by the idle loop, not by an interrupt-driven dispatcher.
-
No custom
#[idle]: defining priority-0 tasks and a custom#[idle]simultaneously is a compile error. Use#[post_init]to spawn initial tasks instead. -
No cross-core priority 0:
spawn_by != coreis disallowed for priority-0 tasks.
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.
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— convertsdeadline = Dattributes into RTICX priorities. -
rticx-auto-assign— automatically assignscore = Nbased 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.
- Supported Distributions — feature flags per distribution.
- Distributor Guide — how these attributes are processed internally.