Pick a scheduling model
Needs first: Organize a larger workspace
Pick a scheduling model when one image runs callbacks of different urgency: a 10 ms control loop beside a 100 ms telemetry publisher, for example, where the control loop must not wait behind the telemetry.
In nano-ros, the unit of scheduling is a sched task: one executor running
on one RTOS thread, with its own priority and spin period, all of them sharing
one middleware session. You name callback groups in code, map each group to a
sched task in system.toml, and give each sched task its priority per RTOS.
The build turns that into the threads each platform has. Where ROS 2 reaches
for a MultiThreadedExecutor and callback groups, nano-ros gives you sched
tasks: an executor dispatches on one thread, and parallelism is a sched task,
not a callback group.
The scheduling theory behind the choice (fixed-priority preemption, rate-monotonic assignment, EDF, priority inversion) belongs to The execution model.
Before you start
- A workspace whose image runs on an RTOS or on Linux.
- The capabilities your platform delivers, from the generated Scheduling wiring matrix: kernel EDF on Zephyr, the sporadic server on NuttX, preemption threshold on ThreadX, core affinity on most.
Steps
-
Name a callback group for each urgency, in the node’s code.
In Rust, call
node.callback_group("ctrl")?before creating the timer or subscription that belongs to it. In C++, use the named form,create_callback_group("ctrl"); an unnamed group runs on the node’s default sched task. -
Map each group to a sched task, on the node’s
[[component]]row:[[component]] pkg = "ctrl_pkg" class = "ctrl_pkg::Control" name = "control_node" group_tasks = { ctrl = "high" } [[component]] pkg = "telem_pkg" class = "telem_pkg::Telem" name = "telem_node" group_tasks = { telem = "low" } -
Declare the sched tasks. A key on
[tasks.<name>]applies on every platform; a key on[tasks.<name>.<rtos>]applies only there:[tasks.high] class = "real_time" spin_period = "1000us" [tasks.high.posix] priority = 80 core = 0 [tasks.high.freertos] priority = 3 [tasks.low] spin_period = "10000us" [tasks.low.posix] priority = 10 [tasks.low.freertos] priority = 2classisreal_time(the default for a priority sched task),best_effortortime_triggered. Priorities are the RTOS’s own numbers, so their direction differs: on FreeRTOS, NuttX and Linux a bigger number is more urgent, and on ThreadX and Zephyr a smaller one is. The table of levels and directions per platform is in the Scheduling wiring matrix. -
Add the platform’s own mechanism where it has one. Scope it to that platform, so it lowers to nothing elsewhere:
[tasks.high.zephyr] priority = 9 deadline = "10000us" # kernel EDF, Zephyr only [tasks.high.nuttx] priority = 99 budget = "5000us" # SCHED_SPORADIC, NuttX only period = "10000us" [tasks.low.threadx] priority = 16 preempt_threshold = 10 # ThreadX only time_slice = "5000us"A
budgeton[tasks.high]itself would switch every platform’s executor to cooperative sporadic gating, which is why these keys go in the platform tables. -
Keep sched tasks below the transport. Each board reserves a priority band for the middleware’s own read and lease threads, and a sched task that outranks them cannot be served by them. The build allocates sched-task priorities from the board’s application pool and refuses a priority that lands on the reserved band. A sched task that really must run above it says so with
above = "transport". -
Choose what a periodic timer does after a stall. When a sched task is preempted past several periods, the timer fires once and counts the missed periods (
TimerOverrunPolicy::Skip, the default, as in rclcpp and Zephyr’sk_timer). Phase is kept, so activations stay on the declared cadence. Replay every missed period instead, for a counter or an accumulator, withexecutor.set_timer_overrun_policy(id, TimerOverrunPolicy::CatchUp). Do not replay a control loop: the replayed activations compute with stale inputs and send a burst of commands. Watchexecutor.timer_overruns(id), or thetimer-overrun-runtimeviolation, rather than the publish rate, which looks nominal underCatchUpduring exactly the stall it should catch. Timer periods are microseconds throughout, including across the C ABI. -
Build. The configure runs the host image once to take its census, and each node logs the sched task it registered on:
nros configure[INFO] Telem::register on a sched task admitting group `telem`
Example
examples/workspaces/realtime-rust/
runs a 10 ms control node on high and a 100 ms telemetry node on low, with
sched-task tables for Linux, FreeRTOS, NuttX, ThreadX and Zephyr in one
system.toml. Its realtime-c and realtime-cpp siblings do the same in C
and C++.
# src/demo_bringup/system.toml (excerpt)
[[component]]
pkg = "ctrl_pkg"
class = "ctrl_pkg::Control"
name = "control_node"
group_tasks = { ctrl = "high" }
[[component]]
pkg = "telem_pkg"
class = "telem_pkg::Telem"
name = "telem_node"
group_tasks = { telem = "low" }
[tasks.high]
class = "real_time"
spin_period = "1000us"
[tasks.high.posix]
priority = 80
core = 0
[tasks.high.freertos]
priority = 3
[tasks.low]
spin_period = "10000us"
[tasks.low.posix]
priority = 10
[tasks.low.freertos]
priority = 2
[image.linux]
board = "linux"
[image.freertos_realtime]
board = "freertos"
panic = "own"
locator = "tcp/10.0.2.2:7891"
nros run demo_bringup:linux
If it doesn’t work
- The build refuses a priority. It lands on the board’s reserved
transport band, or outranks it. Pick one from the application pool the
message names, or state
above = "transport". - A
corepin falls back with a warning on Zephyr. The pin is on the boot sched task, which Zephyr started before nano-ros ran and cannot pin any more. Putcoreon a sched task nano-ros spawns. MultiThreadedExecutordoes not compile. It has no nano-ros form. Move the callbacks that need their own thread to a sched task of their own.
See also
- Analyze timing: check the callbacks a sched task runs for unbounded work.
- Scheduling wiring matrix: what each platform delivers.
- The execution model: how executors, sched tasks, levels and dispatch fit together.