Separate esp-radio support code (#4175)

This commit is contained in:
Dániel Buga 2025-09-24 13:47:25 +02:00 committed by GitHub
parent 6baba85f9e
commit caadbbba41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 268 additions and 212 deletions

View File

@ -15,6 +15,7 @@ doc-config = { features = ["esp-hal/unstable"] }
check-configs = [
{ features = ["esp-hal/unstable"] },
{ features = ["esp-hal/unstable", "esp-alloc"] },
{ features = ["esp-hal/unstable", "esp-radio"] },
]
clippy-configs = [
{ features = ["esp-hal/unstable", "esp-alloc", "defmt"] },
@ -40,7 +41,7 @@ document-features = "0.2.11"
esp-alloc = { version = "0.8.0", path = "../esp-alloc", optional = true }
esp-config = { version = "0.5.0", path = "../esp-config" }
esp-sync = { version = "0.0.0", path = "../esp-sync" }
esp-radio-preempt-driver = { version = "0.0.1", path = "../esp-radio-preempt-driver" }
esp-radio-preempt-driver = { version = "0.0.1", path = "../esp-radio-preempt-driver", optional = true }
portable-atomic = { version = "1.11.0", default-features = false }
# Logging interfaces, they are mutually exclusive so they need to be behind separate features.
@ -55,7 +56,7 @@ esp-metadata-generated = { version = "0.1.0", path = "../esp-metadata-generated"
esp-hal = { version = "1.0.0-rc.0", path = "../esp-hal", features = ["unstable"] }
[features]
default = ["esp-alloc"]
default = ["esp-alloc", "esp-radio"]
## Enable the use of the `esp-alloc` crate for dynamic memory allocation.
##
@ -64,6 +65,9 @@ default = ["esp-alloc"]
## - `pub extern "C" fn free_internal(ptr: *mut u8)`
esp-alloc = ["dep:esp-alloc"]
## Enables esp-radio support.
esp-radio = ["dep:esp-radio-preempt-driver"]
#! ### Chip selection
#! One of the following features must be enabled to select the target chip:

View File

@ -0,0 +1,171 @@
//! esp-radio support
use core::{ffi::c_void, ptr::NonNull};
use allocator_api2::boxed::Box;
use esp_hal::{
system::Cpu,
time::{Duration, Instant},
};
use esp_radio_preempt_driver::{
register_semaphore_implementation,
semaphore::{SemaphoreImplementation, SemaphoreKind, SemaphorePtr},
};
use crate::{
SCHEDULER,
run_queue::MaxPriority,
scheduler::Scheduler,
semaphore::Semaphore,
task::{self, Task},
};
mod queue;
mod timer_queue;
impl esp_radio_preempt_driver::Scheduler for Scheduler {
fn initialized(&self) -> bool {
self.with(|scheduler| {
if scheduler.time_driver.is_none() {
warn!("Trying to initialize esp-radio before starting esp-preempt");
return false;
}
let current_cpu = Cpu::current() as usize;
if !scheduler.per_cpu[current_cpu].initialized {
warn!(
"Trying to initialize esp-radio on {:?} but esp-preempt is not running on this core",
current_cpu
);
return false;
}
true
})
}
fn yield_task(&self) {
task::yield_task();
}
fn yield_task_from_isr(&self) {
task::yield_task();
}
fn max_task_priority(&self) -> u32 {
MaxPriority::MAX_PRIORITY as u32
}
fn task_create(
&self,
name: &str,
task: extern "C" fn(*mut c_void),
param: *mut c_void,
priority: u32,
pin_to_core: Option<u32>,
task_stack_size: usize,
) -> *mut c_void {
self.create_task(
name,
task,
param,
task_stack_size,
priority.min(self.max_task_priority()),
pin_to_core.and_then(|core| match core {
0 => Some(Cpu::ProCpu),
#[cfg(multi_core)]
1 => Some(Cpu::AppCpu),
_ => {
warn!("Invalid core number: {}", core);
None
}
}),
)
.as_ptr()
.cast()
}
fn current_task(&self) -> *mut c_void {
self.current_task().as_ptr().cast()
}
fn schedule_task_deletion(&self, task_handle: *mut c_void) {
task::schedule_task_deletion(task_handle as *mut Task)
}
fn current_task_thread_semaphore(&self) -> SemaphorePtr {
task::with_current_task(|task| {
NonNull::from(
task.thread_semaphore
.get_or_insert_with(|| Semaphore::new_counting(0, 1)),
)
.cast()
})
}
fn usleep(&self, us: u32) {
SCHEDULER.sleep_until(Instant::now() + Duration::from_micros(us as u64));
}
fn now(&self) -> u64 {
// We're using a SingleShotTimer as the time driver, which lets us use the system timer's
// timestamps.
Instant::now().duration_since_epoch().as_micros()
}
}
impl Semaphore {
unsafe fn from_ptr<'a>(ptr: SemaphorePtr) -> &'a Self {
unsafe { ptr.cast::<Self>().as_ref() }
}
}
impl SemaphoreImplementation for Semaphore {
fn create(kind: SemaphoreKind) -> SemaphorePtr {
let sem = Box::new(match kind {
SemaphoreKind::Counting { max, initial } => Semaphore::new_counting(initial, max),
SemaphoreKind::Mutex => Semaphore::new_mutex(false),
SemaphoreKind::RecursiveMutex => Semaphore::new_mutex(true),
});
NonNull::from(Box::leak(sem)).cast()
}
unsafe fn delete(semaphore: SemaphorePtr) {
let sem = unsafe { Box::from_raw(semaphore.cast::<Semaphore>().as_ptr()) };
core::mem::drop(sem);
}
unsafe fn take(semaphore: SemaphorePtr, timeout_us: Option<u32>) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.take(timeout_us)
}
unsafe fn give(semaphore: SemaphorePtr) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.give()
}
unsafe fn current_count(semaphore: SemaphorePtr) -> u32 {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.current_count()
}
unsafe fn try_take(semaphore: SemaphorePtr) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.try_take()
}
unsafe fn try_give_from_isr(semaphore: SemaphorePtr, _hptw: Option<&mut bool>) -> bool {
unsafe { <Self as SemaphoreImplementation>::give(semaphore) }
}
unsafe fn try_take_from_isr(semaphore: SemaphorePtr, _hptw: Option<&mut bool>) -> bool {
unsafe { <Self as SemaphoreImplementation>::try_take(semaphore) }
}
}
register_semaphore_implementation!(Semaphore);

View File

@ -61,13 +61,13 @@ extern crate alloc;
// MUST be the first module
mod fmt;
mod queue;
#[cfg(feature = "esp-radio")]
mod esp_radio;
mod run_queue;
mod scheduler;
mod semaphore;
pub mod semaphore;
mod task;
mod timer;
mod timer_queue;
mod wait_queue;
use core::mem::MaybeUninit;
@ -87,6 +87,7 @@ use esp_hal::{
time::{Duration, Instant},
};
pub(crate) use scheduler::SCHEDULER;
pub use task::CurrentThreadHandle;
use crate::timer::TimeDriver;

View File

@ -1,17 +1,13 @@
#[cfg(feature = "esp-radio")]
use core::{ffi::c_void, ptr::NonNull};
use allocator_api2::boxed::Box;
use esp_hal::{
system::Cpu,
time::{Duration, Instant},
};
use esp_radio_preempt_driver::semaphore::{SemaphoreImplementation, SemaphoreKind, SemaphorePtr};
use esp_hal::{system::Cpu, time::Instant};
use esp_sync::NonReentrantMutex;
use crate::{
InternalMemory,
run_queue::{MaxPriority, RunQueue},
semaphore::Semaphore,
run_queue::RunQueue,
task::{
self,
CpuContext,
@ -93,6 +89,7 @@ impl CpuSchedulerState {
main_task: Task {
cpu_context: CpuContext::new(),
#[cfg(feature = "esp-radio")]
thread_semaphore: None,
state: TaskState::Ready,
stack: core::ptr::slice_from_raw_parts_mut(core::ptr::null_mut(), 0),
@ -201,6 +198,7 @@ impl SchedulerState {
task::yield_task();
}
#[cfg(feature = "esp-radio")]
pub(crate) fn create_task(
&mut self,
name: &str,
@ -365,6 +363,7 @@ impl SchedulerState {
});
}
#[cfg(feature = "esp-radio")]
pub(crate) fn schedule_task_deletion(&mut self, task_to_delete: *mut Task) -> bool {
let current_cpu = Cpu::current() as usize;
let current_task = unwrap!(self.per_cpu[current_cpu].current_task);
@ -408,7 +407,7 @@ impl SchedulerState {
let task = Box::from_raw_in(to_delete.as_ptr(), InternalMemory);
core::mem::drop(task);
} else {
to_delete.as_mut().thread_semaphore = None;
core::ptr::drop_in_place(to_delete.as_mut());
}
}
}
@ -436,10 +435,12 @@ impl Scheduler {
self.inner.with(cb)
}
#[cfg(feature = "esp-radio")]
pub(crate) fn current_task(&self) -> TaskPtr {
task::current_task()
}
#[cfg(feature = "esp-radio")]
pub(crate) fn create_task(
&self,
name: &str,
@ -466,102 +467,12 @@ impl Scheduler {
}
}
#[cfg(feature = "esp-radio")]
esp_radio_preempt_driver::scheduler_impl!(pub(crate) static SCHEDULER: Scheduler = Scheduler {
inner: NonReentrantMutex::new(SchedulerState::new())
});
impl esp_radio_preempt_driver::Scheduler for Scheduler {
fn initialized(&self) -> bool {
self.with(|scheduler| {
if scheduler.time_driver.is_none() {
warn!("Trying to initialize esp-radio before starting esp-preempt");
return false;
}
let current_cpu = Cpu::current() as usize;
if !scheduler.per_cpu[current_cpu].initialized {
warn!(
"Trying to initialize esp-radio on {:?} but esp-preempt is not running on this core",
current_cpu
);
return false;
}
true
})
}
fn yield_task(&self) {
task::yield_task();
}
fn yield_task_from_isr(&self) {
task::yield_task();
}
fn max_task_priority(&self) -> u32 {
MaxPriority::MAX_PRIORITY as u32
}
fn task_create(
&self,
name: &str,
task: extern "C" fn(*mut c_void),
param: *mut c_void,
priority: u32,
pin_to_core: Option<u32>,
task_stack_size: usize,
) -> *mut c_void {
self.create_task(
name,
task,
param,
task_stack_size,
priority.min(self.max_task_priority()),
pin_to_core.and_then(|core| match core {
0 => Some(Cpu::ProCpu),
#[cfg(multi_core)]
1 => Some(Cpu::AppCpu),
_ => {
warn!("Invalid core number: {}", core);
None
}
}),
)
.as_ptr()
.cast()
}
fn current_task(&self) -> *mut c_void {
self.current_task().as_ptr().cast()
}
fn schedule_task_deletion(&self, task_handle: *mut c_void) {
task::schedule_task_deletion(task_handle as *mut Task)
}
fn current_task_thread_semaphore(&self) -> SemaphorePtr {
task::with_current_task(|task| {
if task.thread_semaphore.is_none() {
task.thread_semaphore = Some(Semaphore::create(SemaphoreKind::Counting {
max: 1,
initial: 0,
}));
}
unwrap!(task.thread_semaphore)
})
}
fn usleep(&self, us: u32) {
SCHEDULER.sleep_until(Instant::now() + Duration::from_micros(us as u64));
}
fn now(&self) -> u64 {
// We're using a SingleShotTimer as the time driver, which lets us use the system timer's
// timestamps.
esp_hal::time::Instant::now()
.duration_since_epoch()
.as_micros()
}
}
#[cfg(not(feature = "esp-radio"))]
pub(crate) static SCHEDULER: Scheduler = Scheduler {
inner: NonReentrantMutex::new(SchedulerState::new()),
};

View File

@ -1,14 +1,7 @@
use alloc::boxed::Box;
use core::ptr::NonNull;
use esp_hal::{
system::Cpu,
time::{Duration, Instant},
};
use esp_radio_preempt_driver::{
register_semaphore_implementation,
semaphore::{SemaphoreImplementation, SemaphoreKind, SemaphorePtr},
};
use esp_sync::NonReentrantMutex;
use crate::{
@ -143,30 +136,27 @@ pub struct Semaphore {
}
impl Semaphore {
pub fn new(kind: SemaphoreKind) -> Self {
let inner = match kind {
SemaphoreKind::Counting { initial, max } => SemaphoreInner::Counting {
pub fn new_counting(initial: u32, max: u32) -> Self {
Semaphore {
inner: NonReentrantMutex::new(SemaphoreInner::Counting {
current: initial,
max,
waiting: WaitQueue::new(),
},
SemaphoreKind::Mutex | SemaphoreKind::RecursiveMutex => SemaphoreInner::Mutex {
recursive: matches!(kind, SemaphoreKind::RecursiveMutex),
}),
}
}
pub fn new_mutex(recursive: bool) -> Self {
Semaphore {
inner: NonReentrantMutex::new(SemaphoreInner::Mutex {
recursive,
owner: None,
lock_counter: 0,
original_priority: 0,
waiting: WaitQueue::new(),
},
};
Semaphore {
inner: NonReentrantMutex::new(inner),
}),
}
}
unsafe fn from_ptr<'a>(ptr: SemaphorePtr) -> &'a Self {
unsafe { ptr.cast::<Self>().as_ref() }
}
pub fn try_take(&self) -> bool {
self.inner.with(|sem| sem.try_take())
}
@ -220,49 +210,3 @@ impl Semaphore {
})
}
}
impl SemaphoreImplementation for Semaphore {
fn create(kind: SemaphoreKind) -> SemaphorePtr {
let sem = Box::new(Semaphore::new(kind));
NonNull::from(Box::leak(sem)).cast()
}
unsafe fn delete(semaphore: SemaphorePtr) {
let sem = unsafe { Box::from_raw(semaphore.cast::<Semaphore>().as_ptr()) };
core::mem::drop(sem);
}
unsafe fn take(semaphore: SemaphorePtr, timeout_us: Option<u32>) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.take(timeout_us)
}
unsafe fn give(semaphore: SemaphorePtr) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.give()
}
unsafe fn current_count(semaphore: SemaphorePtr) -> u32 {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.current_count()
}
unsafe fn try_take(semaphore: SemaphorePtr) -> bool {
let semaphore = unsafe { Semaphore::from_ptr(semaphore) };
semaphore.try_take()
}
unsafe fn try_give_from_isr(semaphore: SemaphorePtr, _hptw: Option<&mut bool>) -> bool {
unsafe { <Self as SemaphoreImplementation>::give(semaphore) }
}
unsafe fn try_take_from_isr(semaphore: SemaphorePtr, _hptw: Option<&mut bool>) -> bool {
unsafe { <Self as SemaphoreImplementation>::try_take(semaphore) }
}
}
register_semaphore_implementation!(Semaphore);

View File

@ -2,21 +2,22 @@
#[cfg_attr(xtensa, path = "xtensa.rs")]
pub(crate) mod arch_specific;
use core::{ffi::c_void, marker::PhantomData, mem::MaybeUninit, ptr::NonNull};
#[cfg(feature = "esp-radio")]
use core::ffi::c_void;
use core::{marker::PhantomData, mem::MaybeUninit, ptr::NonNull};
#[cfg(feature = "esp-radio")]
use allocator_api2::alloc::Allocator;
pub(crate) use arch_specific::*;
use esp_hal::system::Cpu;
use esp_radio_preempt_driver::semaphore::{SemaphoreHandle, SemaphorePtr};
use crate::{
InternalMemory,
SCHEDULER,
run_queue::RunQueue,
scheduler::SchedulerState,
wait_queue::WaitQueue,
use esp_hal::{
system::Cpu,
time::{Duration, Instant},
};
#[cfg(feature = "esp-radio")]
use crate::{InternalMemory, semaphore::Semaphore};
use crate::{SCHEDULER, run_queue::RunQueue, scheduler::SchedulerState, wait_queue::WaitQueue};
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum TaskState {
@ -59,6 +60,7 @@ task_list_item!(TaskTimerQueueElement, timer_queue_item);
/// Extension trait for common task operations. These should be inherent methods but we can't
/// implement stuff for NonNull.
pub(crate) trait TaskExt {
#[cfg(feature = "esp-radio")]
fn resume(self);
fn priority(self, _: &mut RunQueue) -> usize;
fn set_priority(self, _: &mut RunQueue, new_pro: usize);
@ -67,6 +69,7 @@ pub(crate) trait TaskExt {
}
impl TaskExt for TaskPtr {
#[cfg(feature = "esp-radio")]
fn resume(self) {
SCHEDULER.with(|scheduler| scheduler.resume_task(self))
}
@ -226,7 +229,8 @@ impl<E: TaskListElement> TaskQueue<E> {
#[repr(C)]
pub(crate) struct Task {
pub cpu_context: CpuContext,
pub thread_semaphore: Option<SemaphorePtr>,
#[cfg(feature = "esp-radio")]
pub thread_semaphore: Option<Semaphore>,
pub state: TaskState,
pub stack: *mut [MaybeUninit<u32>],
pub priority: usize,
@ -259,6 +263,7 @@ const STACK_CANARY: u32 =
const { esp_config::esp_config_int!(u32, "ESP_HAL_CONFIG_STACK_GUARD_VALUE") };
impl Task {
#[cfg(feature = "esp-radio")]
pub(crate) fn new(
name: &str,
task_fn: extern "C" fn(*mut c_void),
@ -299,6 +304,7 @@ impl Task {
Task {
cpu_context: new_task_context(task_fn, param, stack_top),
#[cfg(feature = "esp-radio")]
thread_semaphore: None,
state: TaskState::Ready,
stack: stack_words,
@ -333,10 +339,9 @@ impl Task {
impl Drop for Task {
fn drop(&mut self) {
debug!("Dropping task: {:?}", self as *mut Task);
if let Some(sem) = self.thread_semaphore {
let sem = unsafe { SemaphoreHandle::from_ptr(sem) };
core::mem::drop(sem)
}
#[cfg(feature = "esp-radio")]
let _ = self.thread_semaphore.take();
}
}
@ -390,6 +395,33 @@ pub(super) fn current_task() -> TaskPtr {
with_current_task(|task| NonNull::from(task))
}
/// A handle to the current thread.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct CurrentThreadHandle {
_task: TaskPtr,
}
impl CurrentThreadHandle {
/// Retrieves a handle to the current task.
pub fn get() -> Self {
Self {
_task: current_task(),
}
}
/// Delays the current task for the specified duration.
pub fn delay(self, duration: Duration) {
self.delay_until(Instant::now() + duration);
}
/// Delays the current task until the specified deadline.
pub fn delay_until(self, deadline: Instant) {
SCHEDULER.sleep_until(deadline);
}
}
#[cfg(feature = "esp-radio")]
pub(super) fn schedule_task_deletion(task: *mut Task) {
trace!("schedule_task_deletion {:?}", task);
SCHEDULER.with(|scheduler| {

View File

@ -1,3 +1,4 @@
#[cfg(feature = "esp-radio")]
use core::ffi::c_void;
use esp_hal::{interrupt::software::SoftwareInterrupt, riscv::register, system::Cpu};
@ -117,6 +118,7 @@ pub(crate) fn set_idle_hook_entry(idle_context: &mut CpuContext) {
idle_context.pc = idle_hook as usize;
}
#[cfg(feature = "esp-radio")]
pub(crate) fn new_task_context(
task: extern "C" fn(*mut c_void),
param: *mut c_void,

View File

@ -1,3 +1,4 @@
#[cfg(feature = "esp-radio")]
use core::ffi::c_void;
#[cfg(multi_core)]
@ -28,6 +29,7 @@ pub(crate) fn set_idle_hook_entry(idle_context: &mut CpuContext) {
idle_context.PS = current_ps;
}
#[cfg(feature = "esp-radio")]
pub(crate) fn new_task_context(
task_fn: extern "C" fn(*mut c_void),
param: *mut c_void,

View File

@ -120,7 +120,7 @@ macro_rules! scheduler_impl {
#[unsafe(no_mangle)]
#[inline]
fn esp_preempt_current_task_thread_semaphore() -> SemaphorePtr {
fn esp_preempt_current_task_thread_semaphore() -> $crate::semaphore::SemaphorePtr {
<$t as $crate::Scheduler>::current_task_thread_semaphore(&$driver)
}

View File

@ -12,7 +12,7 @@ use core::ffi::c_void;
use defmt::info;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
#[cfg(multi_core)]
use esp_hal::system::{CpuControl, Stack};
use esp_hal::system::{Cpu, CpuControl, Stack};
#[cfg(xtensa)]
use esp_hal::xtensa_lx::interrupt::free as interrupt_free;
use esp_hal::{
@ -25,13 +25,14 @@ use esp_hal::{
#[cfg(riscv)]
use esp_hal::{interrupt::software::SoftwareInterrupt, riscv::interrupt::free as interrupt_free};
use esp_hal_embassy::InterruptExecutor;
use esp_preempt::{CurrentThreadHandle, semaphore::Semaphore};
use esp_radio::InitializationError;
use esp_radio_preempt_driver::{
self as preempt,
semaphore::{SemaphoreHandle, SemaphoreKind},
};
use hil_test::mk_static;
use portable_atomic::{AtomicUsize, Ordering};
use portable_atomic::{AtomicBool, AtomicUsize, Ordering};
use static_cell::StaticCell;
#[allow(unused)] // compile test
@ -193,7 +194,7 @@ mod tests {
let now = Instant::now();
preempt::usleep(10_000);
CurrentThreadHandle::get().delay(Duration::from_millis(10));
hil_test::assert!(now.elapsed() >= Duration::from_millis(10));
}
@ -278,12 +279,6 @@ mod tests {
#[test]
fn test_esp_preempt_priority_inheritance(p: Peripherals) {
use core::ffi::c_void;
use esp_radio_preempt_driver as preempt;
use portable_atomic::{AtomicBool, Ordering};
use preempt::semaphore::{SemaphoreHandle, SemaphoreKind};
#[cfg(riscv)]
let sw_ints = SoftwareInterruptControl::new(p.SW_INTERRUPT);
let timg0 = TimerGroup::new(p.TIMG0);
@ -312,15 +307,15 @@ mod tests {
// The medium priority task will assert that the high priority task has finished.
struct TestContext {
ready_semaphore: SemaphoreHandle,
mutex: SemaphoreHandle,
ready_semaphore: Semaphore,
mutex: Semaphore,
high_priority_task_finished: AtomicBool,
}
let mut test_context = TestContext {
// This semaphore signals the end of the test
ready_semaphore: SemaphoreHandle::new(SemaphoreKind::Counting { max: 1, initial: 0 }),
ready_semaphore: Semaphore::new_counting(0, 1),
// We'll use this mutex to test priority inheritance
mutex: SemaphoreHandle::new(SemaphoreKind::Mutex),
mutex: Semaphore::new_mutex(false),
high_priority_task_finished: AtomicBool::new(false),
};
@ -395,12 +390,6 @@ mod tests {
#[test]
#[cfg(multi_core)]
fn test_esp_preempt_smp(p: Peripherals) {
use core::ffi::c_void;
use esp_hal::system::Cpu;
use esp_radio_preempt_driver as preempt;
use preempt::semaphore::{SemaphoreHandle, SemaphoreKind};
let sw_ints = SoftwareInterruptControl::new(p.SW_INTERRUPT);
let timg0 = TimerGroup::new(p.TIMG0);
@ -409,12 +398,12 @@ mod tests {
// increment a counter, if they are scheduled to run on their specific core.
struct TestContext {
ready_semaphore: SemaphoreHandle,
ready_semaphore: Semaphore,
}
let test_context = TestContext {
// This semaphore signals the end of the test. Each test case will give it once it is
// done.
ready_semaphore: SemaphoreHandle::new(SemaphoreKind::Counting { max: 2, initial: 0 }),
ready_semaphore: Semaphore::new_counting(0, 2),
};
fn count_impl(context: &TestContext, core: Cpu) {