esp_rtos/
lib.rs

1#![cfg_attr(
2    all(docsrs, not(not_really_docsrs)),
3    doc = "<div style='padding:30px;background:#810;color:#fff;text-align:center;'><p>You might want to <a href='https://docs.espressif.com/projects/rust/'>browse the <code>esp-hal</code> documentation on the esp-rs website</a> instead.</p><p>The documentation here on <a href='https://docs.rs'>docs.rs</a> is built for a single chip only (ESP32-C6, in particular), while on the esp-rs website you can select your exact chip from the list of supported devices. Available peripherals and their APIs change depending on the chip.</p></div>\n\n<br/>\n\n"
4)]
5//! An RTOS (Real-Time Operating System) implementation for esp-hal.
6//!
7//! This crate provides the runtime necessary to run `async` code on top of esp-hal,
8//! and implements the necessary capabilities (threads, queues, etc.) required by esp-radio.
9//!
10//! ## Setup
11//!
12//! This crate requires an esp-hal timer to operate, and needs to be started like so:
13//!
14//! ```rust, no_run
15//! use esp_hal::timer::timg::TimerGroup;
16//! let timg0 = TimerGroup::new(peripherals.TIMG0);
17#![doc = esp_hal::before_snippet!()]
18#![cfg_attr(
19    any(riscv, multi_core),
20    doc = "
21
22use esp_hal::interrupt::software::SoftwareInterruptControl;
23let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);"
24)]
25#![cfg_attr(
26    xtensa,
27    doc = "
28
29esp_rtos::start(timg0.timer0);"
30)]
31#![cfg_attr(
32    riscv,
33    doc = "
34
35esp_rtos::start(timg0.timer0, software_interrupt.software_interrupt0);"
36)]
37#![cfg_attr(
38    all(xtensa, multi_core),
39    doc = "
40// Optionally, start the scheduler on the second core
41esp_rtos::start_second_core(
42    software_interrupt.software_interrupt0,
43    software_interrupt.software_interrupt1,
44    || {}, // Second core's main function.
45);
46"
47)]
48#![cfg_attr(
49    all(riscv, multi_core),
50    doc = "
51// Optionally, start the scheduler on the second core
52esp_rtos::start_second_core(
53    software_interrupt.software_interrupt1,
54    || {}, // Second core's main function.
55);
56"
57)]
58#![doc = ""]
59//! // You can now start esp-radio:
60//! // let esp_radio_controller = esp_radio::init().unwrap();
61//! # }
62//! ```
63//! 
64//! To write `async` code, enable the `embassy` feature, and mark the main function with `#[esp_rtos::main]`.
65//! This will create a thread-mode executor on the main thread. Note that, to create async tasks, you will need
66//! the `task` macro from the `embassy-executor` crate. Do NOT enable any of the `arch-*` features on `embassy-executor`.
67//!
68//! ## Additional configuration
69#![doc = ""]
70#![doc = include_str!(concat!(env!("OUT_DIR"), "/esp_rtos_config_table.md"))]
71#![doc = ""]
72//! ## Feature Flags
73#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
74#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
75#![no_std]
76#![cfg_attr(xtensa, feature(asm_experimental_arch))]
77#![cfg_attr(docsrs, feature(doc_cfg))]
78#![deny(missing_docs)]
79
80#[cfg(feature = "alloc")]
81extern crate alloc;
82
83// MUST be the first module
84mod fmt;
85
86#[cfg(feature = "esp-radio")]
87mod esp_radio;
88mod run_queue;
89mod scheduler;
90pub mod semaphore;
91mod task;
92mod timer;
93mod wait_queue;
94
95#[cfg(feature = "embassy")]
96#[cfg_attr(docsrs, doc(cfg(feature = "embassy")))]
97pub mod embassy;
98
99use core::mem::MaybeUninit;
100
101#[cfg(feature = "alloc")]
102pub(crate) use esp_alloc::InternalMemory;
103#[cfg(any(multi_core, riscv))]
104use esp_hal::interrupt::software::SoftwareInterrupt;
105#[cfg(systimer)]
106use esp_hal::timer::systimer::Alarm;
107#[cfg(timergroup)]
108use esp_hal::timer::timg::Timer;
109use esp_hal::{
110    Blocking,
111    system::Cpu,
112    time::{Duration, Instant},
113    timer::{AnyTimer, OneShotTimer, any::Degrade},
114};
115#[cfg(multi_core)]
116use esp_hal::{
117    peripherals::CPU_CTRL,
118    system::{CpuControl, Stack},
119};
120#[cfg(feature = "embassy")]
121#[cfg_attr(docsrs, doc(cfg(feature = "embassy")))]
122pub use macros::rtos_main as main;
123pub(crate) use scheduler::SCHEDULER;
124pub use task::CurrentThreadHandle;
125
126use crate::{task::IdleFn, timer::TimeDriver};
127
128type TimeBase = OneShotTimer<'static, Blocking>;
129
130/// Trace events, emitted via `marker_begin` and `marker_end`
131#[cfg(feature = "rtos-trace")]
132pub enum TraceEvents {
133    /// The scheduler function is running.
134    RunSchedule,
135
136    /// A task has yielded.
137    YieldTask,
138
139    /// The timer tick handler is running.
140    TimerTickHandler,
141
142    /// Process timer queue.
143    ProcessTimerQueue,
144
145    /// Process embassy timer queue.
146    #[cfg(feature = "embassy")]
147    ProcessEmbassyTimerQueue,
148}
149
150// Polyfill the InternalMemory allocator
151#[cfg(all(feature = "alloc", not(feature = "esp-alloc")))]
152mod esp_alloc {
153    use core::{alloc::Layout, ptr::NonNull};
154
155    use allocator_api2::alloc::{AllocError, Allocator};
156
157    unsafe extern "C" {
158        fn malloc_internal(size: usize) -> *mut u8;
159
160        fn free_internal(ptr: *mut u8);
161    }
162
163    /// An allocator that uses internal memory only.
164    pub struct InternalMemory;
165
166    unsafe impl Allocator for InternalMemory {
167        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
168            // We assume malloc returns a 4-byte aligned pointer. We can skip aligning types
169            // that are already aligned to 4 bytes or less.
170            let ptr = if layout.align() <= 4 {
171                unsafe { malloc_internal(layout.size()) }
172            } else {
173                // We allocate extra memory so that we can store the number of prefix bytes in the
174                // bytes before the actual allocation. We will then use this to
175                // restore the pointer to the original allocation.
176
177                // If we can get away with 0 padding bytes, let's do that. In this case, we need
178                // space for the prefix length only.
179                // We assume malloc returns a 4-byte aligned pointer. This means any higher
180                // alignment requirements can be satisfied by at most align - 4
181                // bytes of shift, and we can use the remaining 4 bytes for the prefix length.
182                let extra = layout.align().max(4);
183
184                let allocation = unsafe { malloc_internal(layout.size() + extra) };
185
186                // reserve at least 4 bytes for the prefix
187                let ptr = allocation.wrapping_add(4);
188
189                let align_offset = ptr.align_offset(layout.align());
190
191                let data_ptr = ptr.wrapping_add(align_offset);
192                let prefix_ptr = data_ptr.wrapping_sub(4);
193
194                // Store the amount of padding bytes used for alignment.
195                unsafe { prefix_ptr.cast::<usize>().write(align_offset) };
196
197                data_ptr
198            };
199
200            let ptr = NonNull::new(ptr).ok_or(AllocError)?;
201            Ok(NonNull::slice_from_raw_parts(ptr, layout.size()))
202        }
203
204        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
205            // We assume malloc returns a 4-byte aligned pointer. In that case we don't have to
206            // align, so we don't have a prefix.
207            if layout.align() <= 4 {
208                unsafe { free_internal(ptr.as_ptr()) };
209            } else {
210                // Retrieve the amount of padding bytes used for alignment.
211                let prefix_ptr = ptr.as_ptr().wrapping_sub(4);
212                let prefix_bytes = unsafe { prefix_ptr.cast::<usize>().read() };
213
214                unsafe { free_internal(prefix_ptr.wrapping_sub(prefix_bytes)) };
215            }
216        }
217    }
218}
219
220/// Timers that can be used as time drivers.
221///
222/// This trait is meant to be used only for the [`start`] function.
223pub trait TimerSource: private::Sealed + 'static {
224    /// Returns the timer source.
225    fn timer(self) -> TimeBase;
226}
227
228mod private {
229    pub trait Sealed {}
230}
231
232impl private::Sealed for TimeBase {}
233impl private::Sealed for AnyTimer<'static> {}
234#[cfg(timergroup)]
235impl private::Sealed for Timer<'static> {}
236#[cfg(systimer)]
237impl private::Sealed for Alarm<'static> {}
238
239impl TimerSource for TimeBase {
240    fn timer(self) -> TimeBase {
241        self
242    }
243}
244
245impl TimerSource for AnyTimer<'static> {
246    fn timer(self) -> TimeBase {
247        TimeBase::new(self)
248    }
249}
250
251#[cfg(timergroup)]
252impl TimerSource for Timer<'static> {
253    fn timer(self) -> TimeBase {
254        TimeBase::new(self.degrade())
255    }
256}
257
258#[cfg(systimer)]
259impl TimerSource for Alarm<'static> {
260    fn timer(self) -> TimeBase {
261        TimeBase::new(self.degrade())
262    }
263}
264
265/// Starts the scheduler.
266///
267/// The current context will be converted into the main task, and will be pinned to the first core.
268///
269/// This function is equivalent to [`start_with_idle_hook`], with the default idle hook. The default
270/// idle hook will wait for an interrupt.
271///
272/// For information about the arguments, see [`start_with_idle_hook`].
273pub fn start(timer: impl TimerSource, #[cfg(riscv)] int0: SoftwareInterrupt<'static, 0>) {
274    start_with_idle_hook(
275        timer,
276        #[cfg(riscv)]
277        int0,
278        crate::task::idle_hook,
279    )
280}
281
282/// Starts the scheduler, with a custom idle hook.
283///
284/// The current context will be converted into the main task, and will be pinned to the first core.
285///
286/// The idle hook will be called when no tasks are ready to run. The idle hook's context is not
287/// preserved. If you need to execute a longer process to enter a low-power state, make sure to call
288/// the relevant code in a critical section.
289///
290/// The `timer` argument is a timer source that is used by the scheduler to
291/// schedule internal tasks. The timer source can be any of the following:
292///
293/// - A timg `Timer` instance
294/// - A systimer `Alarm` instance
295/// - An `AnyTimer` instance
296/// - A `OneShotTimer` instance
297#[doc = ""]
298#[cfg_attr(
299    riscv,
300    doc = "The `int0` argument must be `SoftwareInterrupt<0>` which will be used to trigger context switches."
301)]
302#[doc = ""]
303/// For an example, see the [crate-level documentation][self].
304pub fn start_with_idle_hook(
305    timer: impl TimerSource,
306    #[cfg(riscv)] int0: SoftwareInterrupt<'static, 0>,
307    idle_hook: IdleFn,
308) {
309    #[cfg(feature = "rtos-trace")]
310    {
311        rtos_trace::trace::name_marker(TraceEvents::YieldTask as u32, "yield task");
312        rtos_trace::trace::name_marker(TraceEvents::RunSchedule as u32, "run scheduler");
313        rtos_trace::trace::name_marker(TraceEvents::TimerTickHandler as u32, "timer tick handler");
314        rtos_trace::trace::name_marker(
315            TraceEvents::ProcessTimerQueue as u32,
316            "process timer queue",
317        );
318        rtos_trace::trace::name_marker(
319            TraceEvents::ProcessEmbassyTimerQueue as u32,
320            "process embassy timer queue",
321        );
322        rtos_trace::trace::start();
323    }
324
325    trace!("Starting scheduler for the first core");
326    assert_eq!(Cpu::current(), Cpu::ProCpu);
327
328    SCHEDULER.with(move |scheduler| {
329        scheduler.setup(TimeDriver::new(timer.timer()), idle_hook);
330
331        // Allocate the default task.
332
333        unsafe extern "C" {
334            static _stack_start_cpu0: u32;
335            static _stack_end_cpu0: u32;
336            static __stack_chk_guard: u32;
337        }
338        let stack_top = &raw const _stack_start_cpu0;
339        let stack_bottom = (&raw const _stack_end_cpu0).cast::<MaybeUninit<u32>>();
340        let stack_slice = core::ptr::slice_from_raw_parts_mut(
341            stack_bottom.cast_mut(),
342            stack_top as usize - stack_bottom as usize,
343        );
344
345        task::allocate_main_task(
346            scheduler,
347            stack_slice,
348            esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET"),
349            // For compatibility with -Zstack-protector, we read and use the value of
350            // `__stack_chk_guard`.
351            unsafe { (&raw const __stack_chk_guard).read_volatile() },
352        );
353
354        task::setup_multitasking(
355            #[cfg(riscv)]
356            int0,
357        );
358
359        task::yield_task();
360    })
361}
362
363/// Starts the scheduler on the second CPU core.
364///
365/// Note that the scheduler must be started first, before starting the second core.
366///
367/// The supplied stack and function will be used as the main thread of the second core. The thread
368/// will be pinned to the second core.
369///
370/// You can return from the second core's main thread function. This will cause the scheduler to
371/// enter the idle state, but the second core will continue to run interrupt handlers and other
372/// tasks.
373#[cfg(multi_core)]
374pub fn start_second_core<const STACK_SIZE: usize>(
375    cpu_control: CPU_CTRL,
376    #[cfg(xtensa)] int0: SoftwareInterrupt<'static, 0>,
377    int1: SoftwareInterrupt<'static, 1>,
378    stack: &'static mut Stack<STACK_SIZE>,
379    func: impl FnOnce() + Send + 'static,
380) {
381    start_second_core_with_stack_guard_offset::<STACK_SIZE>(
382        cpu_control,
383        #[cfg(xtensa)]
384        int0,
385        int1,
386        stack,
387        None,
388        func,
389    );
390}
391
392/// Starts the scheduler on the second CPU core.
393///
394/// Note that the scheduler must be started first, before starting the second core.
395///
396/// The supplied stack and function will be used as the main thread of the second core. The thread
397/// will be pinned to the second core.
398///
399/// The stack guard offset is used to reserve a portion of the stack for the stack guard, for safety
400/// purposes. Passing `None` will result in the default value configured by the
401/// `ESP_HAL_CONFIG_STACK_GUARD_OFFSET` esp-hal configuration.
402///
403/// You can return from the second core's main thread function. This will cause the scheduler to
404/// enter the idle state, but the second core will continue to run interrupt handlers and other
405/// tasks.
406#[cfg(multi_core)]
407pub fn start_second_core_with_stack_guard_offset<const STACK_SIZE: usize>(
408    cpu_control: CPU_CTRL,
409    #[cfg(xtensa)] int0: SoftwareInterrupt<'static, 0>,
410    int1: SoftwareInterrupt<'static, 1>,
411    stack: &'static mut Stack<STACK_SIZE>,
412    stack_guard_offset: Option<usize>,
413    func: impl FnOnce() + Send + 'static,
414) {
415    trace!("Starting scheduler for the second core");
416
417    #[cfg(xtensa)]
418    task::setup_smp(int0);
419
420    struct SecondCoreStack {
421        stack: *mut [MaybeUninit<u32>],
422    }
423    unsafe impl Send for SecondCoreStack {}
424    let stack_ptrs = SecondCoreStack {
425        stack: core::ptr::slice_from_raw_parts_mut(
426            stack.bottom().cast::<MaybeUninit<u32>>(),
427            STACK_SIZE,
428        ),
429    };
430
431    let stack_guard_offset = stack_guard_offset.unwrap_or(esp_config::esp_config_int!(
432        usize,
433        "ESP_HAL_CONFIG_STACK_GUARD_OFFSET"
434    ));
435
436    let mut cpu_control = CpuControl::new(cpu_control);
437    let guard = cpu_control
438        .start_app_core_with_stack_guard_offset(stack, Some(stack_guard_offset), move || {
439            trace!("Second core running");
440            task::setup_smp(int1);
441            SCHEDULER.with(move |scheduler| {
442                // Make sure the whole struct is captured, not just a !Send field.
443                let ptrs = stack_ptrs;
444                assert!(
445                    scheduler.time_driver.is_some(),
446                    "The scheduler must be started on the first core first."
447                );
448
449                // esp-hal may be configured to use a watchpoint. To work around that, we read the
450                // memory at the stack guard, and we'll use whatever we find as the main task's
451                // stack guard value, instead of writing our own stack guard value.
452                let stack_bottom = ptrs.stack.cast::<u32>();
453                let stack_guard = unsafe { stack_bottom.byte_add(stack_guard_offset) };
454
455                task::allocate_main_task(scheduler, ptrs.stack, stack_guard_offset, unsafe {
456                    stack_guard.read()
457                });
458                task::yield_task();
459                trace!("Second core scheduler initialized");
460            });
461
462            func();
463
464            loop {
465                SCHEDULER.sleep_until(Instant::EPOCH + Duration::MAX);
466            }
467        })
468        .unwrap();
469
470    // Spin until the second core scheduler is initialized
471    let start = Instant::now();
472
473    while start.elapsed() < Duration::from_secs(1) {
474        if SCHEDULER.with(|s| s.per_cpu[1].initialized) {
475            break;
476        }
477        esp_hal::rom::ets_delay_us(1);
478    }
479
480    if !SCHEDULER.with(|s| s.per_cpu[1].initialized) {
481        panic!(
482            "Second core scheduler failed to initialize. \
483            This can happen if its main function overflowed the stack."
484        );
485    }
486
487    core::mem::forget(guard);
488}
489
490const TICK_RATE: u32 = esp_config::esp_config_int!(u32, "ESP_RTOS_CONFIG_TICK_RATE_HZ");
491
492pub(crate) fn now() -> u64 {
493    Instant::now().duration_since_epoch().as_micros()
494}
495
496#[cfg(feature = "embassy")]
497embassy_time_driver::time_driver_impl!(static TIMER_QUEUE: crate::timer::embassy::TimerQueue = crate::timer::embassy::TimerQueue::new());
498
499/// Waits for a condition to be met or a timeout to occur.
500///
501/// This function is meant to simplify implementation of blocking primitives. Upon failure the
502/// `attempt` function should enqueue the task in a wait queue and put the task to sleep.
503fn with_deadline(timeout_us: Option<u32>, attempt: impl Fn(Instant) -> bool) -> bool {
504    let deadline = timeout_us
505        .map(|us| Instant::now() + Duration::from_micros(us as u64))
506        .unwrap_or(Instant::EPOCH + Duration::MAX);
507
508    while !attempt(deadline) {
509        // We are here because the operation failed. We've either timed out, or the operation is
510        // ready to be attempted again. However, any higher priority task can wake up and
511        // preempt us still. Let's just check for the timeout, and try the whole process
512        // again.
513
514        if timeout_us.is_some() && deadline < Instant::now() {
515            // We have a deadline and we've timed out.
516            return false;
517        }
518        // We can block more, so let's attempt the operation again.
519    }
520
521    true
522}