Skip to main content

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//! # struct FakeHeap;
18//! # unsafe impl core::alloc::GlobalAlloc for FakeHeap {
19//! #     unsafe fn alloc(&self, _: core::alloc::Layout) -> *mut u8 {
20//! #         unimplemented!()
21//! #     }
22//! #     unsafe fn dealloc(&self, _: *mut u8, _: core::alloc::Layout) {
23//! #         unimplemented!()
24//! #     }
25//! # }
26//! # #[global_allocator]
27//! # static ALLOCATOR: FakeHeap = FakeHeap;
28//! use esp_hal::timer::timg::TimerGroup;
29//! let timg0 = TimerGroup::new(peripherals.TIMG0);
30//!
31//! esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0);
32#![cfg_attr(
33    multi_core,
34    doc = "
35// Optionally, start the scheduler on the second core
36use static_cell::ConstStaticCell;
37use esp_hal::system::Stack;
38
39static STACK: ConstStaticCell<Stack<8192>> = ConstStaticCell::new(Stack::new());
40esp_rtos::start_second_core(
41    peripherals.CPU_CTRL,
42    peripherals.FROM_CPU_INTR1,
43    STACK.take(),
44    || {}, // Second core's main function.
45);
46"
47)]
48#![doc = ""]
49//! // You can now start esp-radio:
50//! // let esp_radio_controller = esp_radio::init().unwrap();
51#![doc = esp_hal::after_snippet!()]
52//! ```
53//! 
54//! To write `async` code, enable the `embassy` feature, and make the main function `async`.
55//! This will create a thread-mode executor on the main thread. Note that, to create async tasks, you will need
56//! the `task` macro from the `embassy-executor` crate. Do NOT enable any of the `arch-*` features on `embassy-executor`.
57#![cfg_attr(
58    multi_core,
59    doc = r"
60The scheduler can also run on the second core alone, which keeps the first core free for bare-metal
61code. See [`start_on_second_core_only`] for that configuration and its restrictions.
62"
63)]
64#![cfg_attr(
65    sleep_light_sleep,
66    doc = r"
67## Automatic Light Sleep (experimental)
68
69When enabled, the CPU will automatically enter light sleep mode when there are no tasks to run,
70and wake up when a task is ready to run. This can help reduce power consumption.
71
72Because waking up from automatic light sleep can increase latency, the minimum expected idle time
73can be configured using `ESP_RTOS_CONFIG_LIGHT_SLEEP_MIN_US`.
74
75To enable automatic light sleep, call the [`sleep::configure`] function and pass the
76idle hook from the returned [`Sleep`](sleep::Sleep) object to [`start_with_idle_hook`].
77
78To prevent the CPU from entering light sleep, take a [`WakeLock`]. Drop the lock when you are done
79with the critical code.
80"
81)]
82#![cfg_attr(
83    all(sleep_light_sleep, multi_core),
84    doc = r"
85
86⚠️ If you are using bare-metal code on the second core (i.e. the second core is
87not managed by the RTOS), make sure to take a [`WakeLock`], otherwise the automatic
88light sleep may cause unexpected behavior.
89"
90)]
91#![cfg_attr(
92    sleep_light_sleep,
93    doc = r"
94### Example
95
96```rust,no_run
97#![no_std]
98# #[panic_handler]
99# fn panic(_: &core::panic::PanicInfo) -> ! {
100#     loop {}
101# }
102# struct FakeHeap;
103# unsafe impl core::alloc::GlobalAlloc for FakeHeap {
104#     unsafe fn alloc(&self, _: core::alloc::Layout) -> *mut u8 {
105#         unimplemented!()
106#     }
107#     unsafe fn dealloc(&self, _: *mut u8, _: core::alloc::Layout) {
108#         unimplemented!()
109#     }
110# }
111# #[global_allocator]
112# static ALLOCATOR: FakeHeap = FakeHeap;
113
114use esp_hal::timer::timg::TimerGroup;
115
116# fn main() {
117let p = esp_hal::init(esp_hal::Config::default());
118
119let timg0 = TimerGroup::new(p.TIMG0);
120
121let sleep = esp_rtos::sleep::configure(p.LPWR);
122
123esp_rtos::start_with_idle_hook(
124    timg0.timer0,
125    p.FROM_CPU_INTR0,
126    sleep.light_sleep_hook,
127);
128# }
129```
130
131[`WakeLock`]: esp_hal::rtc_cntl::WakeLock
132"
133)]
134//! ## Additional configuration
135#![doc = ""]
136#![doc = include_str!(concat!(env!("OUT_DIR"), "/esp_rtos_config_table.md"))]
137#![doc = ""]
138//! ## Feature Flags
139#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
140#![doc(html_logo_url = "https://docs.espressif.com/projects/rust/esp-rs-grey-bg.svg")]
141#![no_std]
142#![cfg_attr(xtensa, feature(asm_experimental_arch))]
143#![cfg_attr(docsrs, feature(doc_cfg))]
144#![deny(missing_docs)]
145
146#[cfg(feature = "alloc")]
147extern crate alloc;
148
149// MUST be the first module
150mod fmt;
151
152#[cfg(feature = "esp-radio")]
153mod esp_radio;
154mod run_queue;
155mod scheduler;
156#[cfg(sleep_light_sleep)]
157pub mod sleep;
158mod syscall;
159mod task;
160mod timer;
161// TODO: these esp-radio gates will need to be cleaned up once we re-introduce IPC objects.
162#[cfg(feature = "esp-radio")]
163mod wait_queue;
164
165#[cfg(feature = "embassy")]
166#[cfg_attr(docsrs, doc(cfg(feature = "embassy")))]
167pub mod embassy;
168
169use core::mem::MaybeUninit;
170
171#[cfg(feature = "alloc")]
172pub(crate) use esp_alloc::InternalMemory;
173#[cfg(systimer_driver_supported)]
174use esp_hal::timer::systimer::Alarm;
175#[cfg(timergroup_driver_supported)]
176use esp_hal::timer::timg::Timer;
177use esp_hal::{
178    Blocking,
179    peripherals::FROM_CPU_INTR0,
180    system::Cpu,
181    time::Instant,
182    timer::{AnyTimer, OneShotTimer, any::Degrade},
183};
184#[cfg(multi_core)]
185use esp_hal::{
186    peripherals::{CPU_CTRL, FROM_CPU_INTR1},
187    system::{CpuControl, Stack},
188    time::Duration,
189};
190#[cfg(feature = "embassy")]
191#[cfg_attr(docsrs, doc(cfg(feature = "embassy")))]
192pub use macros::main;
193#[cfg(multi_core)]
194use scheduler::ActiveCores;
195pub(crate) use scheduler::SCHEDULER;
196pub use task::CurrentThreadHandle;
197
198use crate::{task::IdleFn, timer::TimeDriver};
199
200type TimeBase = OneShotTimer<'static, Blocking>;
201
202/// Trace events, emitted via `marker_begin` and `marker_end`
203#[cfg(feature = "rtos-trace")]
204pub enum TraceEvents {
205    /// The scheduler function is running.
206    RunSchedule,
207
208    /// A task has yielded.
209    YieldTask,
210
211    /// The timer tick handler is running.
212    TimerTickHandler,
213
214    /// Process timer queue.
215    ProcessTimerQueue,
216
217    /// Process embassy timer queue.
218    #[cfg(feature = "embassy")]
219    ProcessEmbassyTimerQueue,
220}
221
222// Polyfill the InternalMemory allocator
223#[cfg(all(feature = "alloc", not(feature = "esp-alloc")))]
224mod esp_alloc {
225    use core::{alloc::Layout, ptr::NonNull};
226
227    use allocator_api2::alloc::{AllocError, Allocator};
228
229    unsafe extern "C" {
230        fn malloc_internal(size: usize) -> *mut u8;
231
232        fn free_internal(ptr: *mut u8);
233    }
234
235    /// An allocator that uses internal memory only.
236    pub struct InternalMemory;
237
238    unsafe impl Allocator for InternalMemory {
239        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
240            // We assume malloc returns a 4-byte aligned pointer. We can skip aligning types
241            // that are already aligned to 4 bytes or less.
242            let ptr = if layout.align() <= 4 {
243                unsafe { malloc_internal(layout.size()) }
244            } else {
245                // We allocate extra memory so that we can store the number of prefix bytes in the
246                // bytes before the actual allocation. We will then use this to
247                // restore the pointer to the original allocation.
248
249                // If we can get away with 0 padding bytes, let's do that. In this case, we need
250                // space for the prefix length only.
251                // We assume malloc returns a 4-byte aligned pointer. This means any higher
252                // alignment requirements can be satisfied by at most align - 4
253                // bytes of shift, and we can use the remaining 4 bytes for the prefix length.
254                let extra = layout.align().max(4);
255
256                let allocation = unsafe { malloc_internal(layout.size() + extra) };
257
258                if allocation.is_null() {
259                    return Err(AllocError);
260                }
261
262                // reserve at least 4 bytes for the prefix
263                let ptr = allocation.wrapping_add(4);
264
265                let align_offset = ptr.align_offset(layout.align());
266
267                let data_ptr = ptr.wrapping_add(align_offset);
268                let prefix_ptr = data_ptr.wrapping_sub(4);
269
270                // Store the amount of padding bytes used for alignment.
271                unsafe { prefix_ptr.cast::<usize>().write(align_offset) };
272
273                data_ptr
274            };
275
276            let ptr = NonNull::new(ptr).ok_or(AllocError)?;
277            Ok(NonNull::slice_from_raw_parts(ptr, layout.size()))
278        }
279
280        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
281            // We assume malloc returns a 4-byte aligned pointer. In that case we don't have to
282            // align, so we don't have a prefix.
283            if layout.align() <= 4 {
284                unsafe { free_internal(ptr.as_ptr()) };
285            } else {
286                // Retrieve the amount of padding bytes used for alignment.
287                let prefix_ptr = ptr.as_ptr().wrapping_sub(4);
288                let prefix_bytes = unsafe { prefix_ptr.cast::<usize>().read() };
289
290                unsafe { free_internal(prefix_ptr.wrapping_sub(prefix_bytes)) };
291            }
292        }
293    }
294}
295
296/// Timers that can be used as time drivers.
297///
298/// This trait is meant to be used only for the [`start`] function.
299pub trait TimerSource: private::Sealed + 'static {
300    /// Returns the timer source.
301    fn timer(self) -> TimeBase;
302}
303
304mod private {
305    pub trait Sealed {}
306}
307
308impl private::Sealed for TimeBase {}
309impl private::Sealed for AnyTimer<'static> {}
310#[cfg(timergroup_driver_supported)]
311impl private::Sealed for Timer<'static> {}
312#[cfg(systimer_driver_supported)]
313impl private::Sealed for Alarm<'static> {}
314
315impl TimerSource for TimeBase {
316    fn timer(self) -> TimeBase {
317        self
318    }
319}
320
321impl TimerSource for AnyTimer<'static> {
322    fn timer(self) -> TimeBase {
323        TimeBase::new(self)
324    }
325}
326
327#[cfg(timergroup_driver_supported)]
328impl TimerSource for Timer<'static> {
329    fn timer(self) -> TimeBase {
330        TimeBase::new(self.degrade())
331    }
332}
333
334#[cfg(systimer_driver_supported)]
335impl TimerSource for Alarm<'static> {
336    fn timer(self) -> TimeBase {
337        TimeBase::new(self.degrade())
338    }
339}
340
341fn init_tracing() {
342    #[cfg(feature = "rtos-trace")]
343    {
344        rtos_trace::trace::name_marker(TraceEvents::YieldTask as u32, "yield task");
345        rtos_trace::trace::name_marker(TraceEvents::RunSchedule as u32, "run scheduler");
346        rtos_trace::trace::name_marker(TraceEvents::TimerTickHandler as u32, "timer tick handler");
347        rtos_trace::trace::name_marker(
348            TraceEvents::ProcessTimerQueue as u32,
349            "process timer queue",
350        );
351        rtos_trace::trace::name_marker(
352            TraceEvents::ProcessEmbassyTimerQueue as u32,
353            "process embassy timer queue",
354        );
355        rtos_trace::trace::start();
356    }
357}
358
359fn assert_thread_mode(function: &str) {
360    assert!(
361        esp_hal::interrupt::RunLevel::current().is_thread(),
362        "{} must not be called from an interrupt handler",
363        function
364    );
365}
366
367/// Starts the scheduler.
368///
369/// The current context will be converted into the main task, and will be pinned to the first core.
370///
371/// This function is equivalent to [`start_with_idle_hook`], with the default idle hook. The default
372/// idle hook will wait for an interrupt.
373///
374/// For information about the arguments, see [`start_with_idle_hook`].
375pub fn start(timer: impl TimerSource, int0: FROM_CPU_INTR0<'static>) {
376    start_with_idle_hook(timer, int0, crate::task::idle_hook)
377}
378
379/// Starts the scheduler, with a custom idle hook.
380///
381/// The current context will be converted into the main task, and will be pinned to the first core.
382///
383/// The idle hook will be called when no tasks are ready to run. The idle hook's context is not
384/// preserved. If you need to execute a longer process to enter a low-power state, make sure to call
385/// the relevant code in a critical section.
386///
387/// The `timer` argument is a timer source that is used by the scheduler to
388/// schedule internal tasks. The timer source can be any of the following:
389///
390/// - A timg `Timer` instance
391/// - A systimer `Alarm` instance
392/// - An `AnyTimer` instance
393/// - A `OneShotTimer` instance
394///
395/// For an example, see the [crate-level documentation][self].
396pub fn start_with_idle_hook(
397    timer: impl TimerSource,
398    int0: FROM_CPU_INTR0<'static>,
399    idle_hook: IdleFn,
400) {
401    init_tracing();
402
403    trace!("Starting scheduler for the first core");
404    assert_eq!(Cpu::current(), Cpu::ProCpu);
405    assert_thread_mode("esp_rtos::start");
406
407    SCHEDULER.with(move |scheduler| {
408        scheduler.setup(TimeDriver::new(timer.timer()), idle_hook);
409        syscall::setup_syscalls();
410
411        // Allocate the default task.
412
413        unsafe extern "C" {
414            static _stack_start_cpu0: u32;
415            static _stack_end_cpu0: u32;
416            static __stack_chk_guard: u32;
417        }
418        let stack_top = &raw const _stack_start_cpu0;
419        let stack_bottom = (&raw const _stack_end_cpu0).cast::<MaybeUninit<u32>>();
420        let stack_slice = core::ptr::slice_from_raw_parts_mut(
421            stack_bottom.cast_mut(),
422            (stack_top as usize - stack_bottom as usize) / 4,
423        );
424
425        task::allocate_main_task(
426            scheduler,
427            stack_slice,
428            esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET"),
429            // For compatibility with -Zstack-protector, we read and use the value of
430            // `__stack_chk_guard`.
431            unsafe { (&raw const __stack_chk_guard).read_volatile() },
432        );
433
434        task::setup_multitasking(int0);
435
436        // Set up the main task's context.
437        task::yield_task();
438    })
439}
440
441/// Starts the scheduler on the second CPU core.
442///
443/// Note that the scheduler must be started first, before starting the second core.
444///
445/// The supplied stack and function will be used as the main thread of the second core. The thread
446/// will be pinned to the second core.
447///
448/// You can return from the second core's main thread function. This will cause the scheduler to
449/// enter the idle state, but the second core will continue to run interrupt handlers and other
450/// tasks.
451#[cfg(multi_core)]
452pub fn start_second_core<const STACK_SIZE: usize>(
453    cpu_control: CPU_CTRL,
454    int1: FROM_CPU_INTR1<'static>,
455    stack: &'static mut Stack<STACK_SIZE>,
456    func: impl FnOnce() + Send + 'static,
457) {
458    start_second_core_with_stack_guard_offset::<STACK_SIZE>(cpu_control, int1, stack, None, func);
459}
460
461/// The stack of the second core, in a form that can be moved into the second core's main function.
462#[cfg(multi_core)]
463struct SecondCoreStack {
464    stack: *mut [MaybeUninit<u32>],
465}
466
467#[cfg(multi_core)]
468unsafe impl Send for SecondCoreStack {}
469
470#[cfg(multi_core)]
471impl SecondCoreStack {
472    fn new<const STACK_SIZE: usize>(stack: &mut Stack<STACK_SIZE>) -> Self {
473        Self {
474            stack: core::ptr::slice_from_raw_parts_mut(
475                stack.bottom().cast::<MaybeUninit<u32>>(),
476                STACK_SIZE / 4,
477            ),
478        }
479    }
480
481    /// Turns the current context of the second core into its main task.
482    ///
483    /// This takes `self` by value, so that a `move` closure captures the whole struct instead of
484    /// its `!Send` field only.
485    fn allocate_main_task(self, scheduler: &mut scheduler::SchedulerState, guard_offset: usize) {
486        // esp-hal may be configured to use a watchpoint. To work around that, we read the memory at
487        // the stack guard, and we'll use whatever we find as the main task's stack guard value,
488        // instead of writing our own stack guard value.
489        let stack_bottom = self.stack.cast::<u32>();
490        let stack_guard = unsafe { stack_bottom.byte_add(guard_offset) };
491
492        task::allocate_main_task(scheduler, self.stack, guard_offset, unsafe {
493            stack_guard.read()
494        });
495    }
496}
497
498#[cfg(multi_core)]
499fn default_stack_guard_offset() -> usize {
500    esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET")
501}
502
503/// Waits for the scheduler of the second core to start.
504#[cfg(multi_core)]
505fn wait_for_second_core_scheduler() {
506    let start = Instant::now();
507
508    while start.elapsed() < Duration::from_secs(1) {
509        if SCHEDULER.with(|s| s.active_cores.contains(Cpu::AppCpu)) {
510            return;
511        }
512        esp_hal::rom::ets_delay_us(1);
513    }
514
515    panic!(
516        "Second core scheduler failed to initialize. \
517        This can happen if its main function overflowed the stack."
518    );
519}
520
521#[cfg(multi_core)]
522fn suspend_main_task() {
523    loop {
524        SCHEDULER.sleep_until(Instant::EPOCH + Duration::MAX);
525    }
526}
527
528/// Starts the scheduler on the second CPU core.
529///
530/// Note that the scheduler must be started first, before starting the second core.
531///
532/// The supplied stack and function will be used as the main thread of the second core. The thread
533/// will be pinned to the second core.
534///
535/// The stack guard offset is used to reserve a portion of the stack for the stack guard, for safety
536/// purposes. Passing `None` will result in the default value configured by the
537/// `ESP_HAL_CONFIG_STACK_GUARD_OFFSET` esp-hal configuration.
538///
539/// You can return from the second core's main thread function. This will cause the scheduler to
540/// enter the idle state, but the second core will continue to run interrupt handlers and other
541/// tasks.
542#[cfg(multi_core)]
543pub fn start_second_core_with_stack_guard_offset<const STACK_SIZE: usize>(
544    cpu_control: CPU_CTRL,
545    int1: FROM_CPU_INTR1<'static>,
546    stack: &'static mut Stack<STACK_SIZE>,
547    stack_guard_offset: Option<usize>,
548    func: impl FnOnce() + Send + 'static,
549) {
550    trace!("Starting scheduler for the second core");
551
552    match SCHEDULER.with(|scheduler| scheduler.active_cores) {
553        ActiveCores::Single(Cpu::ProCpu) => {}
554        _ => unreachable!(),
555    }
556
557    let stack_ptrs = SecondCoreStack::new(stack);
558    let stack_guard_offset = stack_guard_offset.unwrap_or_else(default_stack_guard_offset);
559
560    let mut cpu_control = CpuControl::new(cpu_control);
561    let guard = cpu_control
562        .start_app_core_with_stack_guard_offset(stack, Some(stack_guard_offset), move || {
563            trace!("Second core running");
564            SCHEDULER.with(move |scheduler| {
565                task::setup_smp(int1);
566                assert!(
567                    scheduler.time_driver.is_some(),
568                    "The scheduler must be started on the first core first."
569                );
570
571                scheduler.active_cores = ActiveCores::All;
572
573                stack_ptrs.allocate_main_task(scheduler, stack_guard_offset);
574                task::yield_task();
575                trace!("Second core scheduler initialized");
576            });
577
578            func();
579            suspend_main_task();
580        })
581        .unwrap();
582
583    wait_for_second_core_scheduler();
584
585    core::mem::forget(guard);
586}
587
588/// Starts the scheduler on the second CPU core only.
589///
590/// Use this function if you want to keep the first core free for bare-metal code, and run all
591/// RTOS-managed work on the second core. Call it from the first core. It returns on the first core,
592/// which then continues to run bare-metal code.
593///
594/// The scheduler does not run on the first core in this configuration. This function must not be
595/// combined with [`start`] or [`start_second_core`]; either combination panics.
596///
597/// The supplied stack and function become the main thread of the second core. The thread is pinned
598/// to the second core. For the list of accepted timer sources, see [`start_with_idle_hook`].
599///
600/// You can return from the second core's main thread function. This will cause the scheduler to
601/// enter the idle state, but the second core will continue to run interrupt handlers and other
602/// tasks.
603///
604/// ## Restrictions
605///
606/// - The first core must not call any `esp-rtos` API, not even to wake a task that runs on the
607///   second core. The scheduler protects its state with a lock that disables interrupts and spins,
608///   which would break the timing of the bare-metal code on the first core.
609/// - Automatic light sleep is not available, because this function takes no idle hook.
610/// - The Wi-Fi and Bluetooth LE drivers of `esp-radio` are not supported.
611/// - `#[esp_rtos::main]` on an `async fn` cannot be used, because it creates a thread-mode executor
612///   on the first core. Create the thread-mode executor (`embassy::Executor`) inside `func`
613///   instead.
614///
615/// ## Example
616///
617/// ```rust, no_run
618#[doc = esp_hal::before_snippet!()]
619/// # struct FakeHeap;
620/// # unsafe impl core::alloc::GlobalAlloc for FakeHeap {
621/// #     unsafe fn alloc(&self, _: core::alloc::Layout) -> *mut u8 {
622/// #         unimplemented!()
623/// #     }
624/// #     unsafe fn dealloc(&self, _: *mut u8, _: core::alloc::Layout) {
625/// #         unimplemented!()
626/// #     }
627/// # }
628/// # #[global_allocator]
629/// # static ALLOCATOR: FakeHeap = FakeHeap;
630/// use esp_hal::{
631///     system::Stack,
632///     timer::timg::TimerGroup,
633/// };
634/// use static_cell::ConstStaticCell;
635///
636/// static STACK: ConstStaticCell<Stack<8192>> = ConstStaticCell::new(Stack::new());
637///
638/// let timg0 = TimerGroup::new(peripherals.TIMG0);
639///
640/// esp_rtos::start_on_second_core_only(
641///     peripherals.CPU_CTRL,
642///     peripherals.FROM_CPU_INTR1,
643///     timg0.timer0,
644///     STACK.take(),
645///     || {
646///         // The main thread of the second core.
647///     },
648/// );
649///
650/// // The first core continues here, and must not call any esp-rtos API.
651#[doc = esp_hal::after_snippet!()]
652/// ```
653#[cfg(multi_core)]
654pub fn start_on_second_core_only<const STACK_SIZE: usize>(
655    cpu_control: CPU_CTRL<'static>,
656    int1: FROM_CPU_INTR1<'static>,
657    timer: impl TimerSource + Send,
658    stack: &'static mut Stack<STACK_SIZE>,
659    func: impl FnOnce() + Send + 'static,
660) {
661    init_tracing();
662
663    trace!("Starting scheduler for the second core only");
664    assert_eq!(Cpu::current(), Cpu::ProCpu);
665    assert_thread_mode("esp_rtos::start_on_second_core_only");
666    assert!(
667        SCHEDULER.with(|scheduler| scheduler.time_driver.is_none()),
668        "The scheduler has already been started. \
669        esp_rtos::start_on_second_core_only must not be combined with esp_rtos::start."
670    );
671
672    let stack_ptrs = SecondCoreStack::new(stack);
673    let stack_guard_offset = default_stack_guard_offset();
674
675    let mut cpu_control = CpuControl::new(cpu_control);
676    let guard = cpu_control
677        .start_app_core_with_stack_guard_offset(stack, Some(stack_guard_offset), move || {
678            trace!("Second core running");
679            SCHEDULER.with(move |scheduler| {
680                task::setup_multitasking(int1);
681
682                // The time driver must be created here, and not on the first core:
683                // `Timer::set_interrupt_handler` binds the timer interrupt to the core that calls
684                // it.
685                scheduler.setup(TimeDriver::new(timer.timer()), crate::task::idle_hook);
686                syscall::setup_syscalls();
687
688                stack_ptrs.allocate_main_task(scheduler, stack_guard_offset);
689                task::yield_task();
690                trace!("Second core scheduler initialized");
691            });
692
693            func();
694            suspend_main_task();
695        })
696        .unwrap();
697
698    wait_for_second_core_scheduler();
699
700    core::mem::forget(guard);
701}
702
703const TICK_RATE: u32 = esp_config::esp_config_int!(u32, "ESP_RTOS_CONFIG_TICK_RATE_HZ");
704
705pub(crate) fn now() -> u64 {
706    Instant::now().duration_since_epoch().as_micros()
707}
708
709/// Rearms the alarm timer.
710///
711/// This function may be necessary to call after waking up from light sleep.
712pub fn rearm_alarm() {
713    SCHEDULER.with(|s| {
714        if let Some(time_driver) = s.time_driver.as_mut() {
715            time_driver.rearm(now());
716        }
717    });
718}
719
720#[cfg(feature = "embassy")]
721embassy_time_driver::time_driver_impl!(static TIMER_QUEUE: crate::timer::embassy::EmbassyTimeDriver = crate::timer::embassy::EmbassyTimeDriver);