-
Notifications
You must be signed in to change notification settings - Fork 8
Distributor Guide Architecture
This page describes the modular architecture of RTICX and how the core framework, compilation passes, and distributions interact in addition to how multicore is supported.
┌─────────────────────────────────────────────┐
│ User application │
│ #[<distro>::app(...)] │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Distribution │
│ - RticMacroBuilder │
│ - CorePassBackend impl │
│ - Selected passes │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Compilation passes │
│ - RticPass implementations │
│ - Pure syntax-to-syntax transformations │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ rticx-core │
│ - Parse #[<distro>::app] │
│ - Run SRP analysis │
│ - Generate code via CorePassBackend │
└─────────────────────────────────────────────┘
rticx-core is the bottom layer. It is responsible for:
- Parsing the
#[<distro>::app]attribute and the annotated module into an AST (App). - Running resource-ceiling analysis under the Stack Resource Policy (SRP).
- Generating the final Rust code for tasks, resources,
init,idle, and interrupt dispatchers.
To keep rticx-core target-agnostic, the actual hardware-specific code generation is delegated to a backend trait.
CorePassBackend is the interface a distribution implements to provide hardware-specific details:
| Method | Purpose |
|---|---|
post_init(...) |
Code inserted after init and task initialization. |
generate_resource_proxy_lock_impl(...) |
Body of the lock function for shared resources. |
generate_global_definitions(...) |
Extra constants, imports, or helpers at global scope. |
wrap_task_execution(...) |
Wrap the task exec call inside an interrupt handler. |
entry_name(core) |
Name of the entry function for each core. |
populate_idle_loop() |
Custom body for the default idle loop. |
generate_interrupt_free_fn(...) |
Implement the global critical-section function. |
pre_codegen_validation(...) |
Target-specific validation before codegen. |
entry_attrs() |
Attributes injected onto entry points. |
task_attrs(...) |
Attributes injected onto task interrupt handlers. |
A compilation pass is a crate that implements RticPass:
pub trait RticPass {
fn subscribe(&mut self, info_bus: InfoBus);
fn run_pass(
&self,
args: TokenStream2,
app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)>;
fn pass_name(&self) -> &str;
fn main_injection(&self, point: &MainInjectionPoint) -> Option<TokenStream2> {
None
}
}Passes are pure syntax-to-syntax transformations. They receive the macro arguments and the annotated module, transform them, and return the updated pair. Passes are registered with RticMacroBuilder: they run before the core pass parses the module.
Run before the module is parsed by rticx-core. They are used to transform high-level syntax into the core RTICX syntax that the core pass understands. For example, the software-tasks pass converts #[sw_task] and spawn calls into hardware tasks and dispatcher queues.
Every distribution builds its macro by constructing an RticMacroBuilder:
let mut builder = RticMacroBuilder::new(my_backend);
builder.bind_pre_core_pass(SoftwarePass::new(my_sw_backend));
builder.bind_pre_core_pass(AutoAssignPass);
let tokens = builder.build_rtic_macro(args, input);The pipeline order inside build_rtic_macro is:
- Subscribe the core backend to the
InfoBus. - Subscribe and run each pre-core pass in insertion order. Each pass may strip the
#[app]arguments it consumes from theargsit returns (e.g.dispatchers), and may publish entries to theInfoBus. - Parse the module with
App::parse(args, app_mod); leftover#[app]arguments produce warnings in the generated code. - Publish
rticx_core::Appto theInfoBus. - Run SRP analysis; publish
rticx_core::Analysis. - Call
CorePassBackend::pre_codegen_validation. - Collect
main_injectiontokens from every pass for eachMainInjectionPoint. - Run code generation via
rticx_core::CodeGen.
TBA
TBA
TBA
RTICX supports multicore targets while preserving SRP guarantees through the following constraints:
-
Each core owns and manages its own set of shared resources (
#[shared(core = N)]). This preserves SRP by ensuring resource ceilings are computed per-core. -
Software and async tasks are the primary method of inter-core communication. One core can spawn a software/async task on another core and pass arguments to it. These tasks are known as
cross-coretasks. -
Cross-core tasks are constrained further to preserve SRP. A software task dispatcher can manage either core-local or cross-core tasks but not both — a priority level is reserved exclusively for one or the other. The user must specify which core a cross-core task runs on (
core = N) and which core is allowed to spawn it (spawn_by = M). The reason for these two restrictions: each dispatcher queue has exactly one producer and one consumer. For core-local tasks the same core is both; for cross-core tasks one core is the producer and the other is the consumer. -
Pending an interrupt on a remote core is implementation-specific and handled by the distribution that enables multicore support.
-
In some distributions, asynchronous channels (
asyncfeature) can also be used as another means of cross-core communication if the distribution implements additional bindings (described at a later stage). -
RTICX is a single-binary multicore framework: all cores share the same address space and execute from a single firmware image. Multi-binary scenarios (separate firmware per core) fall outside RTICX's syntax scope. For those use cases, users create independent single-core or single-binary multicore RTICX projects — one per core/or per set of homogenous cores — and the distribution can optionally provide IPC primitives or syntax extensions to bridge them. RTICX's modular design makes this possible without changes to the core framework.
The core compilation pass generates per-core entry points, interrupt handlers, and shared resource proxies. The backend decides:
- How each core is started (e.g., RP2040 starts core 1 from
post_init). - How cross-core tasks are dispatched — for example, RP2040 uses the SIO inter-core FIFO for cross-core interrupt delivery.
- Optionally, how the async runtime's waker pends the executor interrupt (for cross-core wake scenarios — see
generate_wake_pend_fn).
More details are discussed in Multicore Architecture and Design Decisions.