Skip to content

Distributor Guide Writing Distributions

Zakaria Madaoui edited this page Aug 13, 2026 · 15 revisions

Writing Distributions

This page explains how to create a new RTICX distribution for a target that is not already covered by the reference distributions.

Important: new distributions live out-of-tree

This repository only maintains the core framework and a small set of reference distributions. New hardware distributions should be developed in their own crates and repositories. They are not merged into the core project.

Start from a reference distribution

Start by copy-pasting one of the reference distributions — rticx-cortex-m for single-core Cortex-M targets or rticx-riscv for RISC-V targets — as a working starting point for the crate structure and backend traits.

Distribution structure

A distribution consists of two crates:

  1. The library crate — users depend on this. It re-exports the proc macro and exposes an export module with runtime helpers.
  2. The macro crate — defines the actual #[<distro>::app] proc macro and implements the backend traits.

Example layout:

my-rticx/
├── Cargo.toml
├── src/
│   └── lib.rs          # re-exports app macro and export module
└── my-rticx-macro/
    ├── Cargo.toml
    └── src/
        └── lib.rs      # proc macro + backend impl

Implementing CorePassBackend

The macro crate implements rticx_core::CorePassBackend. This is the bulk of the target-specific work.

At minimum, you must implement:

  • generate_resource_proxy_lock_impl — how shared resources are locked.
  • generate_global_definitions — any global constants or helper functions.
  • wrap_task_execution — how a task body is wrapped in an interrupt handler.
  • post_init — code after initialization.
  • entry_name, entry_attrs — entry point naming and attributes.
  • default_task_priority — fallback task priority.
  • generate_interrupt_free_fn — the global critical-section function.

Assembling the macro with RticMacroBuilder

use proc_macro::TokenStream;
use rticx_core::RticMacroBuilder;

#[proc_macro_attribute]
pub fn app(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut builder = RticMacroBuilder::new(MyBackend);
    builder.bind_pre_core_pass(SoftwarePass::new(MySwBackend));
    builder.bind_pre_core_pass(AutoAssignPass);
    builder.build_rtic_macro(args, input)
}

Optional: implementing pass backends

If your distribution uses software/async tasks, implement SwPassBackend/AsyncPassBackend:

impl SwPassBackend for MySwBackend {
   ...
}

impl AsyncPassBackend for MySwBackend {
   ...
}

The library crate

The library crate re-exports the macro and provides the export module:

pub use my_rticx_macro::app;

pub mod export {
    // Re-export target runtime helpers, e.g.:
    // pub use cortex_m::peripheral::NVIC;
    // pub use rticx_sw_pass::export::*;
}

Users write:

use my_rticx::app;

#[app(device = ...)]
mod my_app { ... }

Feature gating compilation passes

You should expose a compilation pass as a Cargo feature when its syntax is optional for the distribution. This lets users opt into syntax extensions and keeps compile times, dependency trees, and generated code small when those extensions are not needed.

When to feature-gate a pass

Feature-gating is appropriate when:

  • The pass adds new syntax that not every application uses (e.g., #[sw_task], spawn, deadline = ...).
  • Omitting the pass significantly reduces compilation time, dependencies, or generated code.
  • The distribution works correctly for a meaningful subset of applications without the pass.

Do not feature-gate a pass when:

  • It is part of the distribution's core programming model and every application is expected to use it.
  • It is required for the backend to produce correct code for the target.

The core compilation pass provided by rticx-core is always required and is not feature-gated.

Example Cargo.toml in the macro crate

[dependencies]
rticx-core = { path = "../rticx-core" }
rticx-sw-pass = { path = "../rticx-sw-pass", optional = true, features = ["proc-macro"] }
rticx-auto-assign = { path = "../rticx-auto-assign", optional = true }

[features]
swtasks = ["rticx-sw-pass"]
autoassign = ["rticx-auto-assign"]

Example conditional registration in the proc macro

#[proc_macro_attribute]
pub fn app(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut builder = RticMacroBuilder::new(MyBackend);

    #[cfg(feature = "swtasks")]
    builder.bind_pre_core_pass(rticx_sw_pass::SoftwarePass::new(MySwBackend));

    #[cfg(feature = "autoassign")]
    builder.bind_pre_core_pass(rticx_auto_assign::AutoAssignPass);

    builder.build_rtic_macro(args, input)
}

Single-core vs multicore

  • For single-core targets, implement only the core backend and ignore cross-core features.
  • For multicore targets, you need to handle core entry points and cross-core dispatch.

Async tasks: atomics and critical sections

If your distribution enables the rticx-async-pass (#[async_task]), the generated code depends on rticx-async, which uses two synchronization primitives:

  • Atomics via portable-atomic — the executor slot's running/pending flags and the slot-pointer indirection.
  • Critical sections via critical-section — the channel queues, wait queues, waker registration, and the make_channel! one-shot guard.

These are correct only if the distribution configures the target support.

Targets without native atomics

On targets without native atomic instructions (Cortex-M0/M0+ / ARMv6-M, RISC-V without the "A" extension), enable the rticx-async feature atomic-critical-section so portable-atomic emulates atomics inside a critical section:

[features]
async = [
    "dep:rticx-async",
    "rticx-async/atomic-critical-section",
]

Critical-section backend

The critical-section crate needs exactly one backend linked in. It is the same backend your generate_interrupt_free_fn uses:

  • Single-core: interrupt-disable is sufficient (e.g. cortex-m/critical-section-single-core, riscv/critical-section-single-hart).
  • Multicore: interrupt-disable is not sufficient. You must provide a multicore-aware backend — e.g. enable rp2040-hal/critical-section-impl, which implements the backend with the RP2040 hardware spinlocks:
[features]
async = [
    "dep:rticx-async",
    "rticx-async/atomic-critical-section",
    "rp2040-hal/critical-section-impl",
]

See rticx-rp2040/Cargo.toml for the reference configuration.

Validation and debugging

Use the debug_expand feature of rticx-core to write the expanded macro output to examples/{binary_name}_expanded.rs:

[features]
debug_expand = ["rticx-core/debug_expand"]

Reference distributions

Study the existing reference distributions for concrete examples:

  • rticx-cortex-m — Cortex-M single core distribution (ARMv6M/ARMv7M).
  • rticx-riscv — RISCV single core distribution (SLIC/ESP32C3/ESP32C6).
  • rticx-rp2040 — dual-core Cortex-M0+ with software tasks.

Next steps

Clone this wiki locally