Skip to main content

oxigdal_embedded/
realtime.rs

1//! Real-time scheduling and deadline management
2//!
3//! Provides utilities for real-time constrained operations in embedded systems
4
5use crate::error::{EmbeddedError, Result};
6use crate::target;
7use core::sync::atomic::{AtomicU64, Ordering};
8
9/// Real-time priority levels
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11#[repr(u8)]
12pub enum Priority {
13    /// Idle priority (lowest)
14    Idle = 0,
15    /// Low priority
16    Low = 1,
17    /// Normal priority
18    Normal = 2,
19    /// High priority
20    High = 3,
21    /// Critical priority (highest)
22    Critical = 4,
23}
24
25impl Priority {
26    /// Get priority from u8
27    pub const fn from_u8(value: u8) -> Option<Self> {
28        match value {
29            0 => Some(Self::Idle),
30            1 => Some(Self::Low),
31            2 => Some(Self::Normal),
32            3 => Some(Self::High),
33            4 => Some(Self::Critical),
34            _ => None,
35        }
36    }
37}
38
39/// Deadline specification
40#[derive(Debug, Clone, Copy)]
41pub struct Deadline {
42    /// Deadline time in microseconds
43    pub time_us: u64,
44    /// Is this a hard deadline (must be met)?
45    pub is_hard: bool,
46}
47
48impl Deadline {
49    /// Create a new soft deadline
50    pub const fn soft(time_us: u64) -> Self {
51        Self {
52            time_us,
53            is_hard: false,
54        }
55    }
56
57    /// Create a new hard deadline
58    pub const fn hard(time_us: u64) -> Self {
59        Self {
60            time_us,
61            is_hard: true,
62        }
63    }
64
65    /// Check if deadline is expired
66    pub fn is_expired(&self, current_us: u64) -> bool {
67        current_us >= self.time_us
68    }
69
70    /// Get remaining time in microseconds
71    pub fn remaining_us(&self, current_us: u64) -> u64 {
72        self.time_us.saturating_sub(current_us)
73    }
74}
75
76/// Real-time scheduler
77pub struct RealtimeScheduler {
78    start_cycles: AtomicU64,
79    cycles_per_us: u64,
80}
81
82impl RealtimeScheduler {
83    /// Create a new real-time scheduler
84    ///
85    /// # Arguments
86    ///
87    /// * `cpu_freq_mhz` - CPU frequency in MHz
88    pub const fn new(cpu_freq_mhz: u64) -> Self {
89        Self {
90            start_cycles: AtomicU64::new(0),
91            cycles_per_us: cpu_freq_mhz,
92        }
93    }
94
95    /// Initialize the scheduler (record start time)
96    pub fn init(&self) {
97        if let Some(cycles) = target::cycle_count() {
98            self.start_cycles.store(cycles, Ordering::Relaxed);
99        }
100    }
101
102    /// Get elapsed time in microseconds since init
103    pub fn elapsed_us(&self) -> u64 {
104        match target::cycle_count() {
105            Some(current) => {
106                let start = self.start_cycles.load(Ordering::Relaxed);
107                let elapsed_cycles = current.saturating_sub(start);
108                elapsed_cycles / self.cycles_per_us
109            }
110            None => 0,
111        }
112    }
113
114    /// Execute a function with a deadline
115    ///
116    /// # Errors
117    ///
118    /// Returns `DeadlineMissed` if the deadline is exceeded
119    pub fn execute_with_deadline<F, T>(&self, deadline: Deadline, f: F) -> Result<T>
120    where
121        F: FnOnce() -> T,
122    {
123        let start_us = self.elapsed_us();
124        let result = f();
125        let end_us = self.elapsed_us();
126
127        let elapsed = end_us.saturating_sub(start_us);
128
129        if deadline.is_hard && elapsed > deadline.time_us {
130            return Err(EmbeddedError::DeadlineMissed {
131                actual_us: elapsed,
132                deadline_us: deadline.time_us,
133            });
134        }
135
136        Ok(result)
137    }
138
139    /// Check if deadline can be met
140    pub fn can_meet_deadline(&self, deadline: &Deadline) -> bool {
141        let current_us = self.elapsed_us();
142        !deadline.is_expired(current_us)
143    }
144
145    /// Get time until deadline
146    pub fn time_until_deadline(&self, deadline: &Deadline) -> u64 {
147        let current_us = self.elapsed_us();
148        deadline.remaining_us(current_us)
149    }
150}
151
152/// Periodic task specification
153///
154/// A task may carry a `run` function pointer that is invoked by
155/// [`RateMonotonicScheduler::schedule`] when the task becomes ready. Tasks
156/// created without a runner (via [`PeriodicTask::new`]) only advance their
157/// period bookkeeping when scheduled; use [`PeriodicTask::with_runner`] or
158/// [`RateMonotonicScheduler::schedule_with`] to run real work.
159#[derive(Debug, Clone)]
160pub struct PeriodicTask {
161    /// Period in microseconds
162    pub period_us: u64,
163    /// Execution time budget in microseconds
164    pub budget_us: u64,
165    /// Priority
166    pub priority: Priority,
167    /// Last execution time (None if never executed)
168    last_exec_us: Option<u64>,
169    /// Optional work executed when the task is ready.
170    ///
171    /// A plain `fn()` pointer keeps `PeriodicTask` usable in `no_std` targets
172    /// without an allocator. Stateful tasks should use
173    /// [`RateMonotonicScheduler::schedule_with`] instead.
174    run: Option<fn()>,
175}
176
177impl PeriodicTask {
178    /// Create a new periodic task without an attached runner
179    ///
180    /// When scheduled, such a task only advances its period bookkeeping and
181    /// records a zero-duration execution. Attach work with
182    /// [`PeriodicTask::with_runner`] or drive execution through
183    /// [`RateMonotonicScheduler::schedule_with`].
184    pub const fn new(period_us: u64, budget_us: u64, priority: Priority) -> Self {
185        Self {
186            period_us,
187            budget_us,
188            priority,
189            last_exec_us: None,
190            run: None,
191        }
192    }
193
194    /// Create a new periodic task with an attached runner function
195    ///
196    /// The `run` function is executed by [`RateMonotonicScheduler::schedule`]
197    /// each time the task becomes ready, and its wall-clock duration is
198    /// recorded in the task statistics (including deadline-miss detection
199    /// against `budget_us`).
200    pub const fn with_runner(
201        period_us: u64,
202        budget_us: u64,
203        priority: Priority,
204        run: fn(),
205    ) -> Self {
206        Self {
207            period_us,
208            budget_us,
209            priority,
210            last_exec_us: None,
211            run: Some(run),
212        }
213    }
214
215    /// Get the attached runner, if any
216    pub fn runner(&self) -> Option<fn()> {
217        self.run
218    }
219
220    /// Check if task is ready to execute
221    pub fn is_ready(&self, current_us: u64) -> bool {
222        match self.last_exec_us {
223            // First execution: task is always ready
224            None => true,
225            // Subsequent executions: check if period has elapsed
226            Some(last) => current_us.saturating_sub(last) >= self.period_us,
227        }
228    }
229
230    /// Mark task as executed
231    pub fn mark_executed(&mut self, current_us: u64) {
232        self.last_exec_us = Some(current_us);
233    }
234
235    /// Get deadline for next execution
236    pub fn next_deadline(&self) -> Deadline {
237        let last = self.last_exec_us.unwrap_or(0);
238        Deadline::hard(last + self.period_us + self.budget_us)
239    }
240}
241
242/// Task timing statistics
243#[derive(Debug, Clone, Copy, Default)]
244pub struct TaskStats {
245    /// Total executions
246    pub executions: u64,
247    /// Minimum execution time (microseconds)
248    pub min_exec_us: u64,
249    /// Maximum execution time (microseconds)
250    pub max_exec_us: u64,
251    /// Total execution time (microseconds)
252    pub total_exec_us: u64,
253    /// Number of deadline misses
254    pub deadline_misses: u64,
255}
256
257impl TaskStats {
258    /// Create new task statistics
259    pub const fn new() -> Self {
260        Self {
261            executions: 0,
262            min_exec_us: u64::MAX,
263            max_exec_us: 0,
264            total_exec_us: 0,
265            deadline_misses: 0,
266        }
267    }
268
269    /// Record an execution
270    pub fn record_execution(&mut self, exec_us: u64, missed_deadline: bool) {
271        self.executions = self.executions.saturating_add(1);
272        self.total_exec_us = self.total_exec_us.saturating_add(exec_us);
273
274        if exec_us < self.min_exec_us {
275            self.min_exec_us = exec_us;
276        }
277
278        if exec_us > self.max_exec_us {
279            self.max_exec_us = exec_us;
280        }
281
282        if missed_deadline {
283            self.deadline_misses = self.deadline_misses.saturating_add(1);
284        }
285    }
286
287    /// Get average execution time
288    pub fn avg_exec_us(&self) -> u64 {
289        self.total_exec_us.checked_div(self.executions).unwrap_or(0)
290    }
291
292    /// Get deadline miss rate
293    pub fn miss_rate(&self) -> f32 {
294        if self.executions == 0 {
295            0.0
296        } else {
297            self.deadline_misses as f32 / self.executions as f32
298        }
299    }
300}
301
302/// Rate monotonic scheduler
303///
304/// Tasks are assigned priorities based on their periods (shorter period = higher priority)
305pub struct RateMonotonicScheduler<const MAX_TASKS: usize> {
306    tasks: heapless::Vec<PeriodicTask, MAX_TASKS>,
307    stats: heapless::Vec<TaskStats, MAX_TASKS>,
308    scheduler: RealtimeScheduler,
309}
310
311impl<const MAX_TASKS: usize> RateMonotonicScheduler<MAX_TASKS> {
312    /// Create a new rate monotonic scheduler
313    pub const fn new(cpu_freq_mhz: u64) -> Self {
314        Self {
315            tasks: heapless::Vec::new(),
316            stats: heapless::Vec::new(),
317            scheduler: RealtimeScheduler::new(cpu_freq_mhz),
318        }
319    }
320
321    /// Initialize the scheduler
322    pub fn init(&mut self) {
323        self.scheduler.init();
324    }
325
326    /// Add a periodic task
327    ///
328    /// # Errors
329    ///
330    /// Returns error if maximum tasks reached
331    pub fn add_task(&mut self, task: PeriodicTask) -> Result<()> {
332        self.tasks
333            .push(task)
334            .map_err(|_| EmbeddedError::BufferTooSmall {
335                required: 1,
336                available: 0,
337            })?;
338
339        self.stats
340            .push(TaskStats::new())
341            .map_err(|_| EmbeddedError::BufferTooSmall {
342                required: 1,
343                available: 0,
344            })?;
345
346        // Sort tasks by period (rate monotonic scheduling)
347        self.sort_tasks();
348
349        Ok(())
350    }
351
352    /// Sort tasks by period (shortest period first)
353    fn sort_tasks(&mut self) {
354        let len = self.tasks.len();
355
356        for i in 0..len {
357            for j in (i + 1)..len {
358                if self.tasks[j].period_us < self.tasks[i].period_us {
359                    self.tasks.swap(i, j);
360                    self.stats.swap(i, j);
361                }
362            }
363        }
364    }
365
366    /// Schedule and execute ready tasks
367    ///
368    /// For each ready task the attached runner (see
369    /// [`PeriodicTask::with_runner`]) is invoked and its wall-clock duration is
370    /// measured against the task budget. Tasks without a runner advance their
371    /// period bookkeeping and record a zero-duration execution. Returns the
372    /// number of tasks that became ready this tick.
373    pub fn schedule(&mut self) -> Result<usize> {
374        let current_us = self.scheduler.elapsed_us();
375        let mut executed: usize = 0;
376
377        for i in 0..self.tasks.len() {
378            if !self.tasks[i].is_ready(current_us) {
379                continue;
380            }
381
382            let start_us = self.scheduler.elapsed_us();
383            if let Some(run) = self.tasks[i].run {
384                // Actually execute the task body.
385                run();
386            }
387            let end_us = self.scheduler.elapsed_us();
388            let exec_us = end_us.saturating_sub(start_us);
389
390            let missed = exec_us > self.tasks[i].budget_us;
391            self.stats[i].record_execution(exec_us, missed);
392
393            self.tasks[i].mark_executed(current_us);
394            executed = executed.saturating_add(1);
395        }
396
397        Ok(executed)
398    }
399
400    /// Schedule ready tasks, executing arbitrary (possibly stateful) work
401    ///
402    /// For each ready task the `dispatch` closure is invoked with the task's
403    /// index; its wall-clock duration is measured against the task budget and
404    /// recorded in the task statistics. This is the preferred entry point for
405    /// tasks that need to capture state, since [`PeriodicTask`] can only store a
406    /// bare `fn()` runner.
407    ///
408    /// Returns the number of tasks that became ready this tick.
409    pub fn schedule_with<F>(&mut self, mut dispatch: F) -> Result<usize>
410    where
411        F: FnMut(usize),
412    {
413        let current_us = self.scheduler.elapsed_us();
414        let mut executed: usize = 0;
415
416        for i in 0..self.tasks.len() {
417            if !self.tasks[i].is_ready(current_us) {
418                continue;
419            }
420
421            let start_us = self.scheduler.elapsed_us();
422            dispatch(i);
423            let end_us = self.scheduler.elapsed_us();
424            let exec_us = end_us.saturating_sub(start_us);
425
426            let missed = exec_us > self.tasks[i].budget_us;
427            self.stats[i].record_execution(exec_us, missed);
428
429            self.tasks[i].mark_executed(current_us);
430            executed = executed.saturating_add(1);
431        }
432
433        Ok(executed)
434    }
435
436    /// Get statistics for a task
437    pub fn get_stats(&self, task_index: usize) -> Option<&TaskStats> {
438        self.stats.get(task_index)
439    }
440
441    /// Get number of tasks
442    pub fn task_count(&self) -> usize {
443        self.tasks.len()
444    }
445}
446
447/// Watchdog timer for deadline monitoring
448pub struct Watchdog {
449    timeout_us: u64,
450    last_feed_us: AtomicU64,
451}
452
453impl Watchdog {
454    /// Create a new watchdog with timeout
455    pub const fn new(timeout_us: u64) -> Self {
456        Self {
457            timeout_us,
458            last_feed_us: AtomicU64::new(0),
459        }
460    }
461
462    /// Feed the watchdog (reset timer)
463    pub fn feed(&self, current_us: u64) {
464        self.last_feed_us.store(current_us, Ordering::Release);
465    }
466
467    /// Check if watchdog has expired
468    pub fn is_expired(&self, current_us: u64) -> bool {
469        let last_feed = self.last_feed_us.load(Ordering::Acquire);
470        current_us.saturating_sub(last_feed) >= self.timeout_us
471    }
472
473    /// Get time until expiry
474    pub fn time_until_expiry(&self, current_us: u64) -> u64 {
475        let last_feed = self.last_feed_us.load(Ordering::Acquire);
476        let elapsed = current_us.saturating_sub(last_feed);
477        self.timeout_us.saturating_sub(elapsed)
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn test_priority_ordering() {
487        assert!(Priority::Idle < Priority::Low);
488        assert!(Priority::Low < Priority::Normal);
489        assert!(Priority::Normal < Priority::High);
490        assert!(Priority::High < Priority::Critical);
491    }
492
493    #[test]
494    fn test_deadline() {
495        let deadline = Deadline::hard(1000);
496        assert!(!deadline.is_expired(500));
497        assert!(deadline.is_expired(1000));
498        assert_eq!(deadline.remaining_us(500), 500);
499    }
500
501    #[test]
502    fn test_periodic_task() {
503        let mut task = PeriodicTask::new(1000, 100, Priority::Normal);
504        assert!(task.is_ready(0));
505        task.mark_executed(0);
506        assert!(!task.is_ready(500));
507        assert!(task.is_ready(1000));
508    }
509
510    #[test]
511    fn test_task_stats() {
512        let mut stats = TaskStats::new();
513        stats.record_execution(100, false);
514        stats.record_execution(200, false);
515        stats.record_execution(150, true);
516
517        assert_eq!(stats.executions, 3);
518        assert_eq!(stats.min_exec_us, 100);
519        assert_eq!(stats.max_exec_us, 200);
520        assert_eq!(stats.avg_exec_us(), 150);
521        assert_eq!(stats.deadline_misses, 1);
522    }
523
524    #[test]
525    fn test_schedule_runs_attached_runner() {
526        use core::sync::atomic::AtomicUsize;
527
528        static RUN_COUNT: AtomicUsize = AtomicUsize::new(0);
529        fn body() {
530            RUN_COUNT.fetch_add(1, Ordering::Relaxed);
531        }
532
533        let mut scheduler = RateMonotonicScheduler::<4>::new(1);
534        scheduler.init();
535        scheduler
536            .add_task(PeriodicTask::with_runner(1000, 100, Priority::Normal, body))
537            .expect("add_task failed");
538
539        let executed = scheduler.schedule().expect("schedule failed");
540        assert_eq!(executed, 1, "ready task should be scheduled");
541        assert_eq!(
542            RUN_COUNT.load(Ordering::Relaxed),
543            1,
544            "attached runner must actually execute"
545        );
546
547        let stats = scheduler.get_stats(0).expect("stats should exist");
548        assert_eq!(stats.executions, 1);
549    }
550
551    #[test]
552    fn test_schedule_with_closure_executes() {
553        let mut scheduler = RateMonotonicScheduler::<4>::new(1);
554        scheduler.init();
555        scheduler
556            .add_task(PeriodicTask::new(1000, 100, Priority::Normal))
557            .expect("add_task failed");
558
559        let mut counter = 0usize;
560        let executed = scheduler
561            .schedule_with(|_idx| {
562                counter += 1;
563            })
564            .expect("schedule_with failed");
565
566        assert_eq!(executed, 1);
567        assert_eq!(counter, 1, "dispatch closure must run for the ready task");
568    }
569
570    #[test]
571    fn test_watchdog() {
572        let watchdog = Watchdog::new(1000);
573        watchdog.feed(0);
574
575        assert!(!watchdog.is_expired(500));
576        assert!(watchdog.is_expired(1000));
577        assert_eq!(watchdog.time_until_expiry(500), 500);
578    }
579}