-
Notifications
You must be signed in to change notification settings - Fork 8
Distributor Guide Writing Distributions
This page explains how to create a new RTIC 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.
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
#[rtic::app]proc macro and implements the backend traits.
Example layout:
my-rtic/
├── Cargo.toml
├── src/
│ └── lib.rs # re-exports app macro and export module
└── my-rtic-macro/
├── Cargo.toml
└── src/
└── lib.rs # proc macro + backend impl
The macro crate implements rtic_core::CorePassBackend. This is the bulk of the target-specific work. Refer to the method table in Architecture for the full interface.
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. -
task_attrs— attributes injected onto task interrupt handlers. -
default_task_priority— fallback task priority. -
generate_interrupt_free_fn— the global critical-section function.
use proc_macro::TokenStream;
use rtic_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 tasks, implement SwPassBackend:
impl SwPassBackend for MySwBackend {
fn generate_local_pend_fn(&self, empty_body_fn: ItemFn) -> ItemFn {
// Fill the local NVIC set-pending function
}
fn generate_cross_pend_fn(&self, empty_body_fn: ItemFn) -> Option<ItemFn> {
// Fill the cross-core pending function, or None for single-core
}
}The library crate re-exports the macro and provides the export module:
pub use my_rtic_macro::app;
pub mod export {
// Re-export target runtime helpers, e.g.:
// pub use cortex_m::peripheral::NVIC;
// pub use rtic_sw_pass::export::*;
}Users write:
use my_rtic::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 rtic-core is always required and is not feature-gated.
[dependencies]
rtic-core = { path = "../rtic-core" }
rtic-sw-pass = { path = "../rtic-sw-pass", optional = true, features = ["proc-macro"] }
rtic-auto-assign = { path = "../rtic-auto-assign", optional = true }
[features]
swtasks = ["rtic-sw-pass"]
autoassign = ["rtic-auto-assign"]#[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(rtic_sw_pass::SoftwarePass::new(MySwBackend));
#[cfg(feature = "autoassign")]
builder.bind_pre_core_pass(rtic_auto_assign::AutoAssignPass);
builder.build_rtic_macro(args, input)
}The user-facing library crate should mirror the macro crate features so users can enable them from their own Cargo.toml:
[dependencies]
rtic-macro = { path = "my-rtic-macro" }
[features]
swtasks = ["rtic-macro/swtasks"]
autoassign = ["rtic-macro/autoassign"]A pass is considered optional if the core RTIC syntax (#[app], #[init], #[idle], #[shared], #[task]) works correctly without it. Mandatory passes can be registered unconditionally.
- For single-core targets, implement only the core backend and ignore cross-core features.
- For multicore targets, you need to handle core entry points, cross-core dispatch, and shared memory. See Multibin and Multipac for multi-binary systems.
Use the debug_expand feature of rtic-core to write the expanded macro output to examples/{binary_name}_expanded.rs:
[features]
debug_expand = ["rtic-core/debug_expand"]Study the existing reference distributions for concrete examples:
-
rp2040-rtic— dual-core Cortex-M0+ with software tasks. -
stm32-renode-rtic— multi-binary multicore build. -
rtic-hippo— single-core RISC-V with threshold-based locking. -
atalanta-rtic— single-core RISC-V.
- Multibin and Multipac — multi-binary and multi-PAC support.
- Writing Compilation Passes — if you need a new pass for your distribution.