-
Notifications
You must be signed in to change notification settings - Fork 8
Distributor Guide Writing Distributions
This page explains how to create a new RTICX distribution for a target that is not already covered by the reference distributions.
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 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.
A distribution consists of two crates:
-
The library crate — users depend on this. It re-exports the proc macro and exposes an
exportmodule with runtime helpers. -
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
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. -
generate_interrupt_free_fn— the global critical-section function.
Task priorities flowing through the framework are logical values: 0 is
the idle/disabled priority and larger values mean higher urgency. They are
not hardware values. Your post_init implementation must convert each
logical priority to the target's hardware encoding before programming the
interrupt controller.
post_init receives the used interrupts and their logical priorities via
app_analysis.used_irqs, a list of UsedIrq structs with name and
priority fields. For each entry, convert priority and then configure the
controller (set the NVIC/CLIC/SLIC priority and unmask the interrupt):
-
Cortex-M / ARM — hardware priority numbering is reversed: hardware value
0is the highest urgency. Convert with a helper such ascortex_logical2hw(seerticx-cortex-m/src/export.rs), which maps logical priorityNto(1 << nvic_prio_bits) - N, shifted into the high bits. Also validate thatNfits within the device's priority bits. - RISC-V — the hardware priority equals the logical value directly: write the logical value as-is to the threshold/priority register (no reversal), while still clamping/validating against the target's supported range.
Apply the same conversion anywhere else a priority is handed to hardware, e.g.
in wrap_task_execution and generate_resource_proxy_lock_impl — the ceiling
used for SRP locking must use the same hardware encoding as the interrupt
controller.
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)
}If your distribution uses software/async tasks, implement SwPassBackend/AsyncPassBackend:
impl SwPassBackend for MySwBackend {
...
}
impl AsyncPassBackend for MySwBackend {
...
}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 { ... }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.
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.
[dependencies]
rticx-core = { path = "../rticx-core" }
rticx-sw-pass = { path = "../rticx-sw-pass" }
rticx-async-pass = { path = "../rticx-async-pass" }
[features]
swtasks = []
async = []The software-tasks pass and the async-tasks pass are not designed to coexist in a single build: both use the dispatchers attribute to generate their own dispatcher tasks, so running both over the same application would produce conflicting code. Any distribution that offers both passes must therefore make the two features mutually exclusive.
Cargo has no built-in mutually-exclusive feature mechanism, so enforce it with a
compile_error! guard in both the library crate and the macro crate:
#[cfg(all(feature = "swtasks", feature = "async"))]
compile_error!(
"the `swtasks` and `async` features are mutually exclusive; enable at most one"
);The reference distributions (rticx-cortex-m, rticx-riscv, rticx-rp2040) all
enforce this in their src/lib.rs.
#[proc_macro_attribute]
pub fn app(args: TokenStream, input: TokenStream) -> TokenStream {
let mut builder = RticMacroBuilder::new(MyBackend);
if cfg!(feature = "swtasks") {
builder.bind_pre_core_pass(rticx_sw_pass::SoftwarePass::new(MySwBackend));
}
if cfg!(feature = "autoassign"){
builder.bind_pre_core_pass(rticx_auto_assign::AutoAssignPass);
}
builder.build_rtic_macro(args, input)
}- 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.
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'srunning/pendingflags and the slot-pointer indirection. -
Critical sections via
critical-section— the channel queues, wait queues, waker registration, and themake_channel!one-shot guard.
These are correct only if the distribution configures the target support.
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",
]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.
Use the rticx-expand cargo tool to
inspect the code your distribution generates, and to debug your compilation passes.
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.
- Writing Compilation Passes — if you need a new pass for your distribution.