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