
Deterministic Scheduling for SDR
R4W Development Team
(Aida, Joe Mooney, Claude
Code)
December 2025
SDR systems have strict timing requirements:
| Protocol | Timing Constraint |
|---|---|
| Frequency hopping | Hop within microseconds |
| TDMA slots | Slot boundaries exact |
| TX/RX turnaround | Hardware limits |
| Preamble detection | Symbol-accurate |
Miss a deadline = lost data or interference.
R4W provides two scheduling systems:
| Scheduler | Use Case | Time Model |
|---|---|---|
| TickScheduler | Simulation, testing | Virtual ticks |
| RealTimeScheduler | Production systems | Wall clock |
Same event API, different timing backends.
┌─────────┐ ┌─────────┐
│ Idle │◄───────────────────►│ Hopping │
└────┬────┘ └─────────┘
│
┌────┴────┐
▼ ▼
┌───────────┐ ┌───────────┐
│Transmitting│ │ Receiving │
└─────┬─────┘ └─────┬─────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│TxTurnaround│────►│RxTurnaround│
└───────────┘ └───────────┘
| State | Can TX? | Can RX? | Description |
|---|---|---|---|
| Idle | Yes | Yes | Ready |
| Transmitting | No | No | TX active |
| TxTurnaround | No | After delay | TX→RX switch |
| Receiving | No | No | RX active |
| RxTurnaround | After delay | No | RX→TX switch |
| Hopping | No | No | Changing frequency |
| Error | No | No | Fault state |
use r4w_core::rt_scheduler::{
RealTimeScheduler, ClockSource
};
use std::time::Duration;
let scheduler = RealTimeScheduler::builder()
.clock_source(ClockSource::System)
.tx_rx_turnaround(Duration::from_micros(100))
.rx_tx_turnaround(Duration::from_micros(100))
.deadline_tolerance(Duration::from_micros(500))
.build()?;use r4w_core::rt_scheduler::{ScheduledEvent, EventAction};
// Single event
let event = ScheduledEvent::new(
scheduler.now_ns() + 10_000_000, // 10ms from now
EventAction::StartTx,
);
scheduler.schedule(event);
// Atomic batch
scheduler.schedule_batch(vec![
ScheduledEvent::new(100_000_000, EventAction::StartTx),
ScheduledEvent::new(110_000_000, EventAction::StopTx),
]);| Action | Description |
|---|---|
StartTx |
Begin transmission |
StopTx |
End transmission |
StartRx |
Begin reception |
StopRx |
End reception |
SetFrequency(f) |
Change center frequency |
SetGain(g) |
Adjust gain |
Custom(fn) |
User-defined action |
Events fire only when preconditions met:
let event = ScheduledEvent::new(
scheduler.now_ns() + 1_000_000,
EventAction::StartTx,
)
.with_guard(|state| state.can_transmit())
.with_priority(0); // Highest priorityGuards prevent invalid state transitions.
| Source | Precision | Use Case |
|---|---|---|
System |
~1us | Development |
Hpet |
~100ns | Linux high-precision |
Tsc |
~10ns | x86 cycle counter |
Gps |
~50ns | Multi-device sync |
Ptp |
~100ns | IEEE 1588 networks |
HardwareDevice |
Varies | SDR device clock |
// Non-blocking: process all due events
let results = scheduler.process();
for result in results {
match result {
Ok(event) => println!("Executed: {:?}", event.action),
Err(e) => eprintln!("Failed: {}", e),
}
}
// Or run continuously in dedicated thread
std::thread::spawn(move || {
scheduler.run().unwrap();
});Schedule transmission with automatic stop:
Schedule receive window:
let (start_id, stop_id) = scheduler.schedule_rx_window(
scheduler.now_ns() + 1_000_000, // Start in 1ms
Duration::from_millis(50), // Window duration
);Automatic state transitions.
let hop_sequence = vec![915.0e6, 916.0e6, 917.0e6, 918.0e6];
let dwell_time = Duration::from_millis(10);
for (i, &freq) in hop_sequence.iter().enumerate() {
let hop_time = scheduler.now_ns()
+ (i as u64) * dwell_time.as_nanos() as u64;
scheduler.schedule(ScheduledEvent::new(
hop_time,
EventAction::SetFrequency(freq),
).with_priority(0)); // High priority
}let slot_duration = Duration::from_millis(5);
let my_slot = 3; // Assigned slot number
loop {
let frame_start = scheduler.next_frame_start();
let my_slot_time = frame_start
+ my_slot * slot_duration.as_nanos() as u64;
// TX only in assigned slot
scheduler.schedule_tx_burst(
my_slot_time,
slot_duration - GUARD_TIME,
);
}For development and testing:
| Feature | TickScheduler | RealTimeScheduler |
|---|---|---|
| Time unit | Virtual ticks | Nanoseconds |
| Determinism | Perfect | OS-dependent |
| Debugging | Step through | Real-time |
| Testing | Reproducible | Hardware-dependent |
| Performance | Fast | Actual speed |
| Metric | Target | Actual |
|---|---|---|
| FFT p99 latency | < 100 us | 18 us |
| BPSK roundtrip p99 | < 100 us | 20 us |
| FHSS hop timing p99 | < 500 us | 80-118 us |
| Page faults (RT mode) | 0 | 0 |
| Hot-path allocations | 0 | 0 |
Lock-free data structures:
Prevent page faults:
use r4w_core::rt::LockedBuffer;
// Allocate and lock memory
let buffer = LockedBuffer::<IQSample>::new(4096)?;
// Guaranteed no page faults
for sample in buffer.iter() {
process_sample(sample);
}Uses mlockall() on Linux.
Typical SDR processing chain:
| Stage | Latency |
|---|---|
| ADC/DAC | 1-10 us |
| Driver transfer | 10-100 us |
| DSP processing | 10-50 us |
| Scheduler overhead | 1-5 us |
| Total | 22-165 us |
Budget: Know your deadlines!
let stats = scheduler.get_stats();
println!("Events scheduled: {}", stats.total_scheduled);
println!("Events executed: {}", stats.total_executed);
println!("Missed deadlines: {}", stats.missed_deadlines);
println!("Max latency: {} ns", stats.max_latency_ns);
println!("Avg latency: {} ns", stats.avg_latency_ns);Complete FHSS system:
let mut scheduler = RealTimeScheduler::builder()
.clock_source(ClockSource::Gps)
.build()?;
// Generate hop sequence
for (i, freq) in hop_sequence.iter().enumerate() {
scheduler.schedule(ScheduledEvent::new(
start + i * dwell,
EventAction::SetFrequency(*freq),
));
// TX burst each hop
scheduler.schedule_tx_burst(
start + i * dwell + SETTLE_TIME,
Duration::from_micros(800),
);
}
scheduler.run()?;| Component | Purpose |
|---|---|
| RealTimeScheduler | Wall-clock event scheduling |
| TickScheduler | Simulation/testing |
| State Machine | TX/RX coordination |
| Clock Sources | GPS/PTP/system time |
| RT Primitives | Lock-free, zero-alloc |
| Metrics | Latency tracking |
Deterministic timing for SDR.
R4W - Real-Time Systems
github.com/joemooney/r4w
Docs: docs/REALTIME_SCHEDULER_GUIDE.md