Improve executor naming. Add docs.

This commit is contained in:
Dario Nieuwenhuis 2021-08-26 00:20:52 +02:00
parent e56c6166dc
commit 297de612e5
11 changed files with 289 additions and 55 deletions

View File

@ -115,12 +115,12 @@ pub fn task(args: TokenStream, item: TokenStream) -> TokenStream {
let result = quote! {
#(#attrs)*
#visibility fn #name(#args) -> #embassy_path::executor::SpawnToken<#impl_ty> {
use #embassy_path::executor::raw::Task;
use #embassy_path::executor::raw::TaskStorage;
#task_fn
type F = #impl_ty;
const NEW_TASK: Task<F> = Task::new();
static POOL: [Task<F>; #pool_size] = [NEW_TASK; #pool_size];
unsafe { Task::spawn_pool(&POOL, move || task(#arg_names)) }
const NEW_TASK: TaskStorage<F> = TaskStorage::new();
static POOL: [TaskStorage<F>; #pool_size] = [NEW_TASK; #pool_size];
unsafe { TaskStorage::spawn_pool(&POOL, move || task(#arg_names)) }
}
};
result.into()

View File

@ -4,12 +4,23 @@ use core::ptr;
use super::{raw, Spawner};
use crate::interrupt::{Interrupt, InterruptExt};
/// Thread mode executor, using WFE/SEV.
///
/// This is the simplest and most common kind of executor. It runs on
/// thread mode (at the lowest priority level), and uses the `WFE` ARM instruction
/// to sleep when it has no more work to do. When a task is woken, a `SEV` instruction
/// is executed, to make the `WFE` exit from sleep and poll the task.
///
/// This executor allows for ultra low power consumption for chips where `WFE`
/// triggers low-power sleep without extra steps. If your chip requires extra steps,
/// you may use [`raw::Executor`] directly to program custom behavior.
pub struct Executor {
inner: raw::Executor,
not_send: PhantomData<*mut ()>,
}
impl Executor {
/// Create a new Executor.
pub fn new() -> Self {
Self {
inner: raw::Executor::new(|_| cortex_m::asm::sev(), ptr::null_mut()),
@ -17,14 +28,29 @@ impl Executor {
}
}
/// Runs the executor.
/// Run the executor.
///
/// The `init` closure is called with a [`Spawner`] that spawns tasks on
/// this executor. Use it to spawn the initial task(s). After `init` returns,
/// the executor starts running the tasks.
///
/// To spawn more tasks later, you may keep copies of the [`Spawner`] (it is `Copy`),
/// for example by passing it as an argument to the initial tasks.
///
/// This function requires `&'static mut self`. This means you have to store the
/// Executor instance in a place where it'll live forever and grants you mutable
/// access. There's a few ways to do this:
///
/// - a [Forever](crate::util::Forever) (safe)
/// - a `static mut` (unsafe)
/// - a local variable in a function you know never returns (like `fn main() -> !`), upgrading its lifetime with `transmute`. (unsafe)
///
/// This function never returns.
pub fn run(&'static mut self, init: impl FnOnce(Spawner)) -> ! {
init(unsafe { self.inner.spawner() });
init(self.inner.spawner());
loop {
unsafe { self.inner.run_queued() };
unsafe { self.inner.poll() };
cortex_m::asm::wfe();
}
}
@ -41,6 +67,27 @@ fn pend_by_number(n: u16) {
cortex_m::peripheral::NVIC::pend(N(n))
}
/// Interrupt mode executor.
///
/// This executor runs tasks in interrupt mode. The interrupt handler is set up
/// to poll tasks, and when a task is woken the interrupt is pended from software.
///
/// This allows running async tasks at a priority higher than thread mode. One
/// use case is to leave thread mode free for non-async tasks. Another use case is
/// to run multiple executors: one in thread mode for low priority tasks and another in
/// interrupt mode for higher priority tasks. Higher priority tasks will preempt lower
/// priority ones.
///
/// It is even possible to run multiple interrupt mode executors at different priorities,
/// by assigning different priorities to the interrupts. For an example on how to do this,
/// See the 'multiprio' example for 'embassy-nrf'.
///
/// To use it, you have to pick an interrupt that won't be used by the hardware.
/// Some chips reserve some interrupts for this purpose, sometimes named "software interrupts" (SWI).
/// If this is not the case, you may use an interrupt from any unused peripheral.
///
/// It is somewhat more complex to use, it's recommended to use the thread-mode
/// [`Executor`] instead, if it works for your use case.
pub struct InterruptExecutor<I: Interrupt> {
irq: I,
inner: raw::Executor,
@ -48,6 +95,7 @@ pub struct InterruptExecutor<I: Interrupt> {
}
impl<I: Interrupt> InterruptExecutor<I> {
/// Create a new Executor.
pub fn new(irq: I) -> Self {
let ctx = irq.number() as *mut ();
Self {
@ -59,16 +107,29 @@ impl<I: Interrupt> InterruptExecutor<I> {
/// Start the executor.
///
/// `init` is called in the interrupt context, then the interrupt is
/// configured to run the executor.
/// The `init` closure is called from interrupt mode, with a [`Spawner`] that spawns tasks on
/// this executor. Use it to spawn the initial task(s). After `init` returns,
/// the interrupt is configured so that the executor starts running the tasks.
/// Once the executor is started, `start` returns.
///
/// To spawn more tasks later, you may keep copies of the [`Spawner`] (it is `Copy`),
/// for example by passing it as an argument to the initial tasks.
///
/// This function requires `&'static mut self`. This means you have to store the
/// Executor instance in a place where it'll live forever and grants you mutable
/// access. There's a few ways to do this:
///
/// - a [Forever](crate::util::Forever) (safe)
/// - a `static mut` (unsafe)
/// - a local variable in a function you know never returns (like `fn main() -> !`), upgrading its lifetime with `transmute`. (unsafe)
pub fn start(&'static mut self, init: impl FnOnce(Spawner) + Send) {
self.irq.disable();
init(unsafe { self.inner.spawner() });
init(self.inner.spawner());
self.irq.set_handler(|ctx| unsafe {
let executor = &*(ctx as *const raw::Executor);
executor.run_queued();
executor.poll();
});
self.irq.set_handler_context(&self.inner as *const _ as _);
self.irq.enable();

View File

@ -3,6 +3,7 @@ use std::sync::{Condvar, Mutex};
use super::{raw, Spawner};
/// Single-threaded std-based executor.
pub struct Executor {
inner: raw::Executor,
not_send: PhantomData<*mut ()>,
@ -10,6 +11,7 @@ pub struct Executor {
}
impl Executor {
/// Create a new Executor.
pub fn new() -> Self {
let signaler = &*Box::leak(Box::new(Signaler::new()));
Self {
@ -25,14 +27,29 @@ impl Executor {
}
}
/// Runs the executor.
/// Run the executor.
///
/// The `init` closure is called with a [`Spawner`] that spawns tasks on
/// this executor. Use it to spawn the initial task(s). After `init` returns,
/// the executor starts running the tasks.
///
/// To spawn more tasks later, you may keep copies of the [`Spawner`] (it is `Copy`),
/// for example by passing it as an argument to the initial tasks.
///
/// This function requires `&'static mut self`. This means you have to store the
/// Executor instance in a place where it'll live forever and grants you mutable
/// access. There's a few ways to do this:
///
/// - a [Forever](crate::util::Forever) (safe)
/// - a `static mut` (unsafe)
/// - a local variable in a function you know never returns (like `fn main() -> !`), upgrading its lifetime with `transmute`. (unsafe)
///
/// This function never returns.
pub fn run(&'static mut self, init: impl FnOnce(Spawner)) -> ! {
init(unsafe { self.inner.spawner() });
init(self.inner.spawner());
loop {
unsafe { self.inner.run_queued() };
unsafe { self.inner.poll() };
self.signaler.wait()
}
}

View File

@ -1,3 +1,7 @@
//! Async task executor.
#![deny(missing_docs)]
#[cfg_attr(feature = "std", path = "arch/std.rs")]
#[cfg_attr(not(feature = "std"), path = "arch/arm.rs")]
mod arch;

View File

@ -1,3 +1,12 @@
//! Raw executor.
//!
//! This module exposes "raw" Executor and Task structs for more low level control.
//!
//! ## WARNING: here be dragons!
//!
//! Using this module requires respecting subtle safety contracts. If you can, prefer using the safe
//! executor wrappers in [`crate::executor`] and the [`crate::task`] macro, which are fully safe.
mod run_queue;
#[cfg(feature = "time")]
mod timer_queue;
@ -30,6 +39,10 @@ pub(crate) const STATE_RUN_QUEUED: u32 = 1 << 1;
#[cfg(feature = "time")]
pub(crate) const STATE_TIMER_QUEUED: u32 = 1 << 2;
/// Raw task header for use in task pointers.
///
/// This is an opaque struct, used for raw pointers to tasks, for use
/// with funtions like [`wake_task`] and [`task_from_waker`].
pub struct TaskHeader {
pub(crate) state: AtomicU32,
pub(crate) run_queue_item: RunQueueItem,
@ -85,15 +98,29 @@ impl TaskHeader {
}
}
/// Raw storage in which a task can be spawned.
///
/// This struct holds the necessary memory to spawn one task whose future is `F`.
/// At a given time, the `Task` may be in spawned or not-spawned state. You may spawn it
/// with [`Task::spawn()`], which will fail if it is already spawned.
///
/// A `TaskStorage` must live forever, it may not be deallocated even after the task has finished
/// running. Hence the relevant methods require `&'static self`. It may be reused, however.
///
/// Internally, the [embassy::task](crate::task) macro allocates an array of `TaskStorage`s
/// in a `static`. The most common reason to use the raw `Task` is to have control of where
/// the memory for the task is allocated: on the stack, or on the heap with e.g. `Box::leak`, etc.
// repr(C) is needed to guarantee that the Task is located at offset 0
// This makes it safe to cast between Task and Task pointers.
#[repr(C)]
pub struct Task<F: Future + 'static> {
pub struct TaskStorage<F: Future + 'static> {
raw: TaskHeader,
future: UninitCell<F>, // Valid if STATE_SPAWNED
}
impl<F: Future + 'static> Task<F> {
impl<F: Future + 'static> TaskStorage<F> {
/// Create a new Task, in not-spawned state.
pub const fn new() -> Self {
Self {
raw: TaskHeader::new(),
@ -101,6 +128,12 @@ impl<F: Future + 'static> Task<F> {
}
}
/// Try to spawn a task in a pool.
///
/// See [`Self::spawn()`] for details.
///
/// This will loop over the pool and spawn the task in the first storage that
/// is currently free. If none is free,
pub fn spawn_pool(pool: &'static [Self], future: impl FnOnce() -> F) -> SpawnToken<F> {
for task in pool {
if task.spawn_allocate() {
@ -111,6 +144,19 @@ impl<F: Future + 'static> Task<F> {
SpawnToken::new_failed()
}
/// Try to spawn the task.
///
/// The `future` closure constructs the future. It's only called if spawning is
/// actually possible. It is a closure instead of a simple `future: F` param to ensure
/// the future is constructed in-place, avoiding a temporary copy in the stack thanks to
/// NRVO optimizations.
///
/// This function will fail if the task is already spawned and has not finished running.
/// In this case, the error is delayed: a "poisoned" SpawnToken is returned, which will
/// cause [`Executor::spawn()`] to return the error.
///
/// Once the task has finished running, you may spawn it again. It is allowed to spawn it
/// on a different executor.
pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<F> {
if self.spawn_allocate() {
unsafe { self.spawn_initialize(future) }
@ -136,7 +182,7 @@ impl<F: Future + 'static> Task<F> {
}
unsafe fn poll(p: NonNull<TaskHeader>) {
let this = &*(p.as_ptr() as *const Task<F>);
let this = &*(p.as_ptr() as *const TaskStorage<F>);
let future = Pin::new_unchecked(this.future.as_mut());
let waker = waker::from_task(p);
@ -155,8 +201,27 @@ impl<F: Future + 'static> Task<F> {
}
}
unsafe impl<F: Future + 'static> Sync for Task<F> {}
unsafe impl<F: Future + 'static> Sync for TaskStorage<F> {}
/// Raw executor.
///
/// This is the core of the Embassy executor. It is low-level, requiring manual
/// handling of wakeups and task polling. If you can, prefer using one of the
/// higher level executors in [`crate::executor`].
///
/// The raw executor leaves it up to you to handle wakeups and scheduling:
///
/// - To get the executor to do work, call `poll()`. This will poll all queued tasks (all tasks
/// that "want to run").
/// - You must supply a `signal_fn`. The executor will call it to notify you it has work
/// to do. You must arrange for `poll()` to be called as soon as possible.
///
/// `signal_fn` can be called from *any* context: any thread, any interrupt priority
/// level, etc. It may be called synchronously from any `Executor` method call as well.
/// You must deal with this correctly.
///
/// In particular, you must NOT call `poll` directly from `signal_fn`, as this violates
/// the requirement for `poll` to not be called reentrantly.
pub struct Executor {
run_queue: RunQueue,
signal_fn: fn(*mut ()),
@ -169,6 +234,12 @@ pub struct Executor {
}
impl Executor {
/// Create a new executor.
///
/// When the executor has work to do, it will call `signal_fn` with
/// `signal_ctx` as argument.
///
/// See [`Executor`] docs for details on `signal_fn`.
pub fn new(signal_fn: fn(*mut ()), signal_ctx: *mut ()) -> Self {
#[cfg(feature = "time")]
let alarm = unsafe { unwrap!(driver::allocate_alarm()) };
@ -187,23 +258,51 @@ impl Executor {
}
}
pub fn set_signal_ctx(&mut self, signal_ctx: *mut ()) {
self.signal_ctx = signal_ctx;
}
unsafe fn enqueue(&self, item: *mut TaskHeader) {
if self.run_queue.enqueue(item) {
/// Enqueue a task in the task queue
///
/// # Safety
/// - `task` must be a valid pointer to a spawned task.
/// - `task` must be set up to run in this executor.
/// - `task` must NOT be already enqueued (in this executor or another one).
unsafe fn enqueue(&self, task: *mut TaskHeader) {
if self.run_queue.enqueue(task) {
(self.signal_fn)(self.signal_ctx)
}
}
pub unsafe fn spawn(&'static self, task: NonNull<TaskHeader>) {
/// Spawn a task in this executor.
///
/// # Safety
///
/// `task` must be a valid pointer to an initialized but not-already-spawned task.
///
/// It is OK to use `unsafe` to call this from a thread that's not the executor thread.
/// In this case, the task's Future must be Send. This is because this is effectively
/// sending the task to the executor thread.
pub(super) unsafe fn spawn(&'static self, task: NonNull<TaskHeader>) {
let task = task.as_ref();
task.executor.set(self);
self.enqueue(task as *const _ as _);
}
pub unsafe fn run_queued(&'static self) {
/// Poll all queued tasks in this executor.
///
/// This loops over all tasks that are queued to be polled (i.e. they're
/// freshly spawned or they've been woken). Other tasks are not polled.
///
/// You must call `poll` after receiving a call to `signal_fn`. It is OK
/// to call `poll` even when not requested by `signal_fn`, but it wastes
/// energy.
///
/// # Safety
///
/// You must NOT call `poll` reentrantly on the same executor.
///
/// In particular, note that `poll` may call `signal_fn` synchronously. Therefore, you
/// must NOT directly call `poll()` from your `signal_fn`. Instead, `signal_fn` has to
/// somehow schedule for `poll()` to be called later, at a time you know for sure there's
/// no `poll()` already running.
pub unsafe fn poll(&'static self) {
#[cfg(feature = "time")]
self.timer_queue.dequeue_expired(Instant::now(), |p| {
p.as_ref().enqueue();
@ -235,18 +334,26 @@ impl Executor {
#[cfg(feature = "time")]
{
// If this is in the past, set_alarm will immediately trigger the alarm,
// which will make the wfe immediately return so we do another loop iteration.
// If this is already in the past, set_alarm will immediately trigger the alarm.
// This will cause `signal_fn` to be called, which will cause `poll()` to be called again,
// so we immediately do another poll loop iteration.
let next_expiration = self.timer_queue.next_expiration();
driver::set_alarm(self.alarm, next_expiration.as_ticks());
}
}
pub unsafe fn spawner(&'static self) -> super::Spawner {
/// Get a spawner that spawns tasks in this executor.
///
/// It is OK to call this method multiple times to obtain multiple
/// `Spawner`s. You may also copy `Spawner`s.
pub fn spawner(&'static self) -> super::Spawner {
super::Spawner::new(self)
}
}
/// Wake a task by raw pointer.
///
/// You can obtain task pointers from `Waker`s using [`task_from_waker`].
pub unsafe fn wake_task(task: NonNull<TaskHeader>) {
task.as_ref().enqueue();
}

View File

@ -39,13 +39,17 @@ impl RunQueue {
}
/// Enqueues an item. Returns true if the queue was empty.
pub(crate) unsafe fn enqueue(&self, item: *mut TaskHeader) -> bool {
///
/// # Safety
///
/// `item` must NOT be already enqueued in any queue.
pub(crate) unsafe fn enqueue(&self, task: *mut TaskHeader) -> bool {
let mut prev = self.head.load(Ordering::Acquire);
loop {
(*item).run_queue_item.next.store(prev, Ordering::Relaxed);
(*task).run_queue_item.next.store(prev, Ordering::Relaxed);
match self
.head
.compare_exchange_weak(prev, item, Ordering::AcqRel, Ordering::Acquire)
.compare_exchange_weak(prev, task, Ordering::AcqRel, Ordering::Acquire)
{
Ok(_) => break,
Err(next_prev) => prev = next_prev,
@ -55,17 +59,25 @@ impl RunQueue {
prev.is_null()
}
pub(crate) unsafe fn dequeue_all(&self, on_task: impl Fn(NonNull<TaskHeader>)) {
let mut task = self.head.swap(ptr::null_mut(), Ordering::AcqRel);
/// Empty the queue, then call `on_task` for each task that was in the queue.
/// NOTE: It is OK for `on_task` to enqueue more tasks. In this case they're left in the queue
/// and will be processed by the *next* call to `dequeue_all`, *not* the current one.
pub(crate) fn dequeue_all(&self, on_task: impl Fn(NonNull<TaskHeader>)) {
// Atomically empty the queue.
let mut ptr = self.head.swap(ptr::null_mut(), Ordering::AcqRel);
while !task.is_null() {
// Iterate the linked list of tasks that were previously in the queue.
while let Some(task) = NonNull::new(ptr) {
// If the task re-enqueues itself, the `next` pointer will get overwritten.
// Therefore, first read the next pointer, and only then process the task.
let next = (*task).run_queue_item.next.load(Ordering::Relaxed);
let next = unsafe { task.as_ref() }
.run_queue_item
.next
.load(Ordering::Relaxed);
on_task(NonNull::new_unchecked(task));
on_task(task);
task = next
ptr = next
}
}
}

View File

@ -22,6 +22,17 @@ pub(crate) unsafe fn from_task(p: NonNull<TaskHeader>) -> Waker {
Waker::from_raw(RawWaker::new(p.as_ptr() as _, &VTABLE))
}
/// Get a task pointer from a waker.
///
/// This can used as an optimization in wait queues to store task pointers
/// (1 word) instead of full Wakers (2 words). This saves a bit of RAM and helps
/// avoid dynamic dispatch.
///
/// You can use the returned task pointer to wake the task with [`wake_task`](super::wake_task).
///
/// # Panics
///
/// Panics if the waker is not created by the Embassy executor.
pub unsafe fn task_from_waker(waker: &Waker) -> NonNull<TaskHeader> {
let hack: &WakerHack = mem::transmute(waker);
if hack.vtable != &VTABLE {

View File

@ -4,7 +4,17 @@ use core::ptr::NonNull;
use super::raw;
#[must_use = "Calling a task function does nothing on its own. You must pass the returned SpawnToken to Executor::spawn()"]
/// Token to spawn a newly-created task in an executor.
///
/// When calling a task function (like `#[embassy::task] async fn my_task() { ... }`), the returned
/// value is a `SpawnToken` that represents an instance of the task, ready to spawn. You must
/// then spawn it into an executor, typically with [`Spawner::spawn()`].
///
/// # Panics
///
/// Dropping a SpawnToken instance panics. You may not "abort" spawning a task in this way.
/// Once you've invoked a task function and obtained a SpawnToken, you *must* spawn it.
#[must_use = "Calling a task function does nothing on its own. You must spawn the returned SpawnToken, typically with Spawner::spawn()"]
pub struct SpawnToken<F> {
raw_task: Option<NonNull<raw::TaskHeader>>,
phantom: PhantomData<*mut F>,
@ -29,13 +39,19 @@ impl<F> SpawnToken<F> {
impl<F> Drop for SpawnToken<F> {
fn drop(&mut self) {
// TODO deallocate the task instead.
panic!("SpawnToken instances may not be dropped. You must pass them to Executor::spawn()")
panic!("SpawnToken instances may not be dropped. You must pass them to Spawner::spawn()")
}
}
/// Error returned when spawning a task.
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum SpawnError {
/// Too many instances of this task are already running.
///
/// By default, a task marked with `#[embassy::task]` can only have one instance
/// running at a time. You may allow multiple instances to run in parallel with
/// `#[embassy::task(pool_size = 4)]`, at the cost of higher RAM usage.
Busy,
}
@ -52,13 +68,16 @@ pub struct Spawner {
}
impl Spawner {
pub(crate) unsafe fn new(executor: &'static raw::Executor) -> Self {
pub(crate) fn new(executor: &'static raw::Executor) -> Self {
Self {
executor,
not_send: PhantomData,
}
}
/// Spawn a task into an executor.
///
/// You obtain the `token` by calling a task function (i.e. one marked with `#[embassy::task]).
pub fn spawn<F>(&self, token: SpawnToken<F>) -> Result<(), SpawnError> {
let task = token.raw_task;
mem::forget(token);
@ -93,10 +112,11 @@ impl Spawner {
/// Handle to spawn tasks into an executor from any thread.
///
/// This Spawner can be used from any thread (it implements Send and Sync, so after any task (Send and non-Send ones), but it can
/// only be used in the executor thread (it is not Send itself).
/// This Spawner can be used from any thread (it is Send), but it can
/// only spawn Send tasks. The reason for this is spawning is effectively
/// "sending" the tasks to the executor thread.
///
/// If you want to spawn tasks from another thread, use [SendSpawner].
/// If you want to spawn non-Send tasks, use [Spawner].
#[derive(Copy, Clone)]
pub struct SendSpawner {
executor: &'static raw::Executor,
@ -106,13 +126,10 @@ pub struct SendSpawner {
unsafe impl Send for SendSpawner {}
unsafe impl Sync for SendSpawner {}
/// Handle to spawn tasks to an executor.
///
/// This Spawner can spawn any task (Send and non-Send ones), but it can
/// only be used in the executor thread (it is not Send itself).
///
/// If you want to spawn tasks from another thread, use [SendSpawner].
impl SendSpawner {
/// Spawn a task into an executor.
///
/// You obtain the `token` by calling a task function (i.e. one marked with `#[embassy::task]).
pub fn spawn<F: Send>(&self, token: SpawnToken<F>) -> Result<(), SpawnError> {
let header = token.raw_task;
mem::forget(token);

View File

@ -95,12 +95,15 @@ pub trait Driver: Send + Sync + 'static {
/// The callback may be called from any context (interrupt or thread mode).
fn set_alarm_callback(&self, alarm: AlarmHandle, callback: fn(*mut ()), ctx: *mut ());
/// Sets an alarm at the given timestamp. When the current timestamp reaches that
/// Sets an alarm at the given timestamp. When the current timestamp reaches the alarm
/// timestamp, the provided callback funcion will be called.
///
/// If `timestamp` is already in the past, the alarm callback must be immediately fired.
/// In this case, it is allowed (but not mandatory) to call the alarm callback synchronously from `set_alarm`.
///
/// When callback is called, it is guaranteed that now() will return a value greater or equal than timestamp.
///
/// Only one alarm can be active at a time. This overwrites any previously-set alarm if any.
/// Only one alarm can be active at a time for each AlarmHandle. This overwrites any previously-set alarm if any.
fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64);
}

View File

@ -40,6 +40,8 @@
//!
//! For more details, check the [`driver`] module.
#![deny(missing_docs)]
mod delay;
pub mod driver;
mod duration;

View File

@ -8,7 +8,7 @@ use example_common::*;
use core::mem;
use cortex_m_rt::entry;
use embassy::executor::raw::Task;
use embassy::executor::raw::TaskStorage;
use embassy::executor::Executor;
use embassy::time::{Duration, Timer};
use embassy::util::Forever;
@ -36,8 +36,8 @@ fn main() -> ! {
let _p = embassy_nrf::init(Default::default());
let executor = EXECUTOR.put(Executor::new());
let run1_task = Task::new();
let run2_task = Task::new();
let run1_task = TaskStorage::new();
let run2_task = TaskStorage::new();
// Safety: these variables do live forever if main never returns.
let run1_task = unsafe { make_static(&run1_task) };