Add timers to preempt (#4053)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dániel Buga 2025-09-05 11:37:16 +02:00 committed by GitHub
parent 68a16055fc
commit 61ad37dd8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 503 additions and 259 deletions

View File

@ -28,6 +28,7 @@ mod queue;
mod semaphore;
mod task;
mod timer;
mod timer_queue;
use core::ffi::c_void;
@ -274,6 +275,7 @@ impl esp_radio_preempt_driver::Scheduler for Scheduler {
// allocate the main task
task::allocate_main_task();
timer::setup_multitasking();
timer_queue::create_timer_task();
TIMER.with(|t| {
let t = unwrap!(t.as_mut());

View File

@ -0,0 +1,300 @@
use alloc::boxed::Box;
use core::{
cell::{RefCell, UnsafeCell},
ffi::c_void,
ptr::NonNull,
};
use esp_radio_preempt_driver::{
Scheduler,
register_timer_implementation,
timer::{TimerImplementation, TimerPtr},
};
use esp_sync::NonReentrantMutex;
use crate::{SCHEDULER, timer::yield_task};
static TIMER_QUEUE: TimerQueue = TimerQueue::new();
struct TimerQueueInner {
// A linked list of active timers
head: Option<NonNull<Timer>>,
next_wakeup: u64,
}
unsafe impl Send for TimerQueueInner {}
impl TimerQueueInner {
const fn new() -> Self {
Self {
head: None,
next_wakeup: 0,
}
}
fn enqueue(&mut self, timer: &Timer) {
let head = self.head;
let props = timer.properties(self);
let due = props.next_due;
if !props.enqueued {
props.enqueued = true;
props.next = head;
self.head = Some(NonNull::from(timer));
}
if due < self.next_wakeup {
self.next_wakeup = due;
}
}
fn dequeue(&mut self, timer: &Timer) -> bool {
let mut current = self.head;
let mut prev: Option<NonNull<Timer>> = None;
// Scan through the queue until we find the timer
while let Some(current_timer) = current {
if core::ptr::eq(current_timer.as_ptr(), timer) {
// If we find the timer, remove it from the queue by bypassing it in the linked
// list. The previous element, if any, will point at the next element.
let timer_props = timer.properties(self);
let next = timer_props.next.take();
timer_props.enqueued = false;
if let Some(mut p) = prev {
unsafe { p.as_mut().properties(self).next = next };
} else {
self.head = next;
}
return true;
}
prev = current;
current = unsafe { current_timer.as_ref().properties(self).next };
}
false
}
}
struct TimerQueue {
inner: NonReentrantMutex<TimerQueueInner>,
}
unsafe impl Send for TimerQueue {}
impl TimerQueue {
const fn new() -> Self {
Self {
inner: NonReentrantMutex::new(TimerQueueInner::new()),
}
}
/// Trigger due timers.
///
/// The timer queue needs to be re-processed when a new timer is armed, because the new timer
/// may need to be triggered before the next scheduled wakeup.
fn process(&self) {
let mut timers = self.inner.with(|q| {
q.next_wakeup = u64::MAX;
q.head.take()
});
while let Some(current) = timers {
debug!("Checking timer: {:x}", current.addr());
let current_timer = unsafe { current.as_ref() };
let run_callback = self.inner.with(|q| {
let props = current_timer.properties(q);
// Remove current timer from the list.
timers = props.next.take();
if !props.is_active || props.drop {
return false;
}
if props.next_due > SCHEDULER.now() {
// Not our time yet.
return false;
}
if props.periodic {
props.next_due += props.period;
}
props.is_active = props.periodic;
true
});
if run_callback {
debug!("Triggering timer: {:x}", current_timer as *const _ as usize);
(current_timer.callback.borrow_mut())();
}
self.inner.with(|q| {
let props = current_timer.properties(q);
// Set this AFTER the callback so that the callback doesn't leave us in an unknown
// "queued?" state.
props.enqueued = false;
if props.drop {
debug!("Dropping timer {:x} (delayed)", current.addr());
let boxed = unsafe { Box::from_raw(current.as_ptr()) };
core::mem::drop(boxed);
} else if props.is_active {
let next_due = props.next_due;
if next_due < q.next_wakeup {
q.next_wakeup = next_due;
}
debug!("Re-queueing timer {:x}", current_timer as *const _ as usize);
q.enqueue(current_timer);
} else {
debug!("Timer {:x} inactive", current_timer as *const _ as usize);
}
});
}
}
fn next_wakeup(&self) -> u64 {
self.inner.with(|q| q.next_wakeup)
}
}
struct TimerProperties {
is_active: bool,
next_due: u64,
period: u64,
periodic: bool,
drop: bool,
enqueued: bool,
next: Option<NonNull<Timer>>,
}
struct TimerQueueCell<T>(UnsafeCell<T>);
impl<T> TimerQueueCell<T> {
const fn new(inner: T) -> Self {
Self(UnsafeCell::new(inner))
}
fn get_mut<'a>(&'a self, _q: &'a mut TimerQueueInner) -> &'a mut T {
unsafe { &mut *self.0.get() }
}
}
pub struct Timer {
callback: RefCell<Box<dyn FnMut() + Send>>,
// Timer properties, not available in `callback` due to how the timer is constructed.
timer_properties: TimerQueueCell<TimerProperties>,
}
impl Timer {
pub fn new(callback: Box<dyn FnMut() + Send>) -> Self {
Timer {
callback: RefCell::new(callback),
timer_properties: TimerQueueCell::new(TimerProperties {
is_active: false,
next_due: 0,
period: 0,
periodic: false,
drop: false,
enqueued: false,
next: None,
}),
}
}
unsafe fn from_ptr<'a>(ptr: TimerPtr) -> &'a Self {
unsafe { ptr.cast::<Self>().as_mut() }
}
fn arm(&self, q: &mut TimerQueueInner, timeout: u64, periodic: bool) {
let next_due = crate::SCHEDULER.now() + timeout;
let props = self.properties(q);
props.is_active = true;
props.next_due = next_due;
props.period = timeout;
props.periodic = periodic;
debug!(
"Arming timer: {:x} @ {} ({})",
self as *const _ as usize, next_due, timeout
);
q.enqueue(self);
}
fn disarm(&self, q: &mut TimerQueueInner) {
self.properties(q).is_active = false;
debug!("Disarming timer: {:x}", self as *const _ as usize);
// We don't dequeue the timer - processing the queue will just skip it. If we re-arm,
// the timer may already be in the queue.
}
fn properties<'a>(&'a self, q: &'a mut TimerQueueInner) -> &'a mut TimerProperties {
self.timer_properties.get_mut(q)
}
}
impl TimerImplementation for Timer {
fn create(callback: Box<dyn FnMut() + Send>) -> TimerPtr {
let timer = Box::new(Timer::new(callback));
NonNull::from(Box::leak(timer)).cast()
}
unsafe fn delete(timer: TimerPtr) {
debug!("Deleting timer: {:x}", timer.addr());
TIMER_QUEUE.inner.with(|q| {
let timer = unsafe { Box::from_raw(timer.cast::<Timer>().as_ptr()) };
// There are two cases:
// - We can remove the timer from the queue - we can drop it.
// - We can't remove the timer from the queue. There are the following cases:
// - The timer isn't in the queue. We can drop it.
// - The timer is in the queue and the queue is being processed. We need to mark it to
// be dropped by the timer queue.
if q.dequeue(&timer) {
core::mem::drop(timer);
} else {
timer.properties(q).drop = true;
core::mem::forget(timer);
}
})
}
unsafe fn arm(timer: TimerPtr, timeout: u64, periodic: bool) {
let timer = unsafe { Timer::from_ptr(timer) };
TIMER_QUEUE.inner.with(|q| timer.arm(q, timeout, periodic))
}
unsafe fn disarm(timer: TimerPtr) {
let timer = unsafe { Timer::from_ptr(timer) };
TIMER_QUEUE.inner.with(|q| timer.disarm(q))
}
}
register_timer_implementation!(Timer);
/// Initializes the `timer` task for the Wi-Fi driver.
pub(crate) fn create_timer_task() {
// schedule the timer task
SCHEDULER.task_create(timer_task, core::ptr::null_mut(), 8192);
}
/// Entry point for the timer task responsible for handling scheduled timer
/// events.
pub(crate) extern "C" fn timer_task(_: *mut c_void) {
loop {
debug!("Timer task");
TIMER_QUEUE.process();
while SCHEDULER.now() < TIMER_QUEUE.next_wakeup() {
yield_task();
}
}
}

View File

@ -31,9 +31,13 @@
pub mod mutex;
pub mod queue;
pub mod semaphore;
pub mod timer;
use core::ffi::c_void;
// Timer callbacks need to be heap-allocated.
extern crate alloc;
use crate::semaphore::SemaphorePtr;
unsafe extern "Rust" {

View File

@ -0,0 +1,130 @@
//! Timers
use alloc::boxed::Box;
use core::ptr::NonNull;
/// Pointer to an opaque timer created by the driver implementation.
pub type TimerPtr = NonNull<()>;
unsafe extern "Rust" {
fn esp_preempt_timer_create(callback: Box<dyn FnMut() + Send>) -> TimerPtr;
fn esp_preempt_timer_delete(timer: TimerPtr);
fn esp_preempt_timer_arm(timer: TimerPtr, timeout: u64, periodic: bool);
fn esp_preempt_timer_disarm(timer: TimerPtr);
}
pub trait TimerImplementation {
/// Creates a new timer instance from the given callback.
fn create(callback: Box<dyn FnMut() + Send>) -> TimerPtr;
/// Deletes a timer instance.
///
/// # Safety
///
/// `timer` must be a pointer returned from [`Self::create`].
unsafe fn delete(timer: TimerPtr);
/// Configures the timer to be triggered after the given timeout.
///
/// The timeout is specified in microsecond. If the timer is set to be periodic,
/// the timer will be triggered with a constant frequency.
///
/// # Safety
///
/// `timer` must be a pointer returned from [`Self::create`].
unsafe fn arm(timer: TimerPtr, timeout: u64, periodic: bool);
/// Stops the timer.
///
/// # Safety
///
/// `timer` must be a pointer returned from [`Self::create`].
unsafe fn disarm(timer: TimerPtr);
}
#[macro_export]
macro_rules! register_timer_implementation {
($t: ty) => {
#[unsafe(no_mangle)]
#[inline]
fn esp_preempt_timer_create(callback: Box<dyn FnMut() + Send>) -> $crate::timer::TimerPtr {
<$t as $crate::timer::TimerImplementation>::create(callback)
}
#[unsafe(no_mangle)]
#[inline]
fn esp_preempt_timer_delete(timer: $crate::timer::TimerPtr) {
unsafe { <$t as $crate::timer::TimerImplementation>::delete(timer) }
}
#[unsafe(no_mangle)]
#[inline]
fn esp_preempt_timer_arm(timer: $crate::timer::TimerPtr, timeout: u64, periodic: bool) {
unsafe { <$t as $crate::timer::TimerImplementation>::arm(timer, timeout, periodic) }
}
#[unsafe(no_mangle)]
#[inline]
fn esp_preempt_timer_disarm(timer: $crate::timer::TimerPtr) {
unsafe { <$t as $crate::timer::TimerImplementation>::disarm(timer) }
}
};
}
#[repr(transparent)]
pub struct TimerHandle(TimerPtr);
impl TimerHandle {
/// Creates a new timer instance from the given callback.
pub fn new(callback: Box<dyn FnMut() + Send>) -> Self {
let ptr = unsafe { esp_preempt_timer_create(callback) };
Self(ptr)
}
/// Converts this object into a pointer without dropping it.
pub fn leak(self) -> TimerPtr {
let ptr = self.0;
core::mem::forget(self);
ptr
}
/// Recovers the object from a leaked pointer.
///
/// # Safety
///
/// - The caller must only use pointers created using [`Self::leak`].
/// - The caller must ensure the pointer is not shared.
pub unsafe fn from_ptr(ptr: TimerPtr) -> Self {
Self(ptr)
}
/// Creates a reference to this object from a leaked pointer.
///
/// This function is used in the esp-radio code to interact with the timer.
///
/// # Safety
///
/// - The caller must only use pointers created using [`Self::leak`].
pub unsafe fn ref_from_ptr(ptr: &TimerPtr) -> &Self {
unsafe { core::mem::transmute(ptr) }
}
/// Configures the timer to be triggered after the given timeout.
///
/// The timeout is specified in microsecond. If the timer is set to be periodic,
/// the timer will be triggered with a constant frequency.
pub fn arm(&self, timeout: u64, periodic: bool) {
unsafe { esp_preempt_timer_arm(self.0, timeout, periodic) }
}
/// Stops the timer.
pub fn disarm(&self) {
unsafe { esp_preempt_timer_disarm(self.0) }
}
}
impl Drop for TimerHandle {
fn drop(&mut self) {
unsafe { esp_preempt_timer_delete(self.0) };
}
}

View File

@ -6,6 +6,8 @@ pub mod misc;
pub mod mutex;
pub mod queue;
pub mod semaphore;
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
pub mod timer_compat;
pub(crate) const OSI_FUNCS_TIME_BLOCKING: u32 = u32::MAX;

View File

@ -1,251 +1,103 @@
use alloc::boxed::Box;
use esp_sync::NonReentrantMutex;
use esp_radio_preempt_driver::timer::TimerHandle;
use crate::binary::{
c_types,
include::{esp_timer_create_args_t, ets_timer},
use crate::{
binary::{c_types::c_void, include::ets_timer},
preempt::timer::TimerPtr,
};
#[derive(Clone, Copy, Debug)]
pub(crate) struct TimerCallback {
f: unsafe extern "C" fn(*mut c_types::c_void),
args: *mut c_types::c_void,
}
impl TimerCallback {
fn new(f: unsafe extern "C" fn(*mut c_types::c_void), args: *mut c_types::c_void) -> Self {
Self { f, args }
}
pub(crate) fn call(self) {
unsafe { (self.f)(self.args) };
}
}
impl From<&esp_timer_create_args_t> for TimerCallback {
fn from(args: &esp_timer_create_args_t) -> Self {
Self::new(unwrap!(args.callback), args.arg)
}
}
#[repr(C)]
#[derive(Debug, Clone)]
pub(crate) struct Timer {
pub ets_timer: *mut ets_timer,
pub started: u64,
pub timeout: u64,
pub active: bool,
pub periodic: bool,
pub callback: TimerCallback,
next: Option<Box<Timer>>,
}
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
impl Timer {
pub(crate) fn id(&self) -> usize {
self.ets_timer as usize
}
}
pub(crate) struct TimerQueue {
head: Option<Box<Timer>>,
}
impl TimerQueue {
const fn new() -> Self {
Self { head: None }
}
pub(crate) unsafe fn find_next_due(
&mut self,
current_timestamp: u64,
) -> Option<&mut Box<Timer>> {
let mut current = self.head.as_mut();
while let Some(timer) = current {
if timer.active && current_timestamp - timer.started >= timer.timeout {
return Some(timer);
}
current = timer.next.as_mut();
}
None
}
}
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
impl TimerQueue {
fn find(&mut self, ets_timer: *mut ets_timer) -> Option<&mut Box<Timer>> {
let mut current = self.head.as_mut();
while let Some(timer) = current {
if core::ptr::eq(timer.ets_timer, ets_timer) {
return Some(timer);
}
current = timer.next.as_mut();
}
None
}
#[cfg(feature = "wifi")]
fn remove(&mut self, ets_timer: *mut ets_timer) {
if let Some(head) = self.head.as_mut()
&& core::ptr::eq(head.ets_timer, ets_timer)
{
self.head = head.next.take();
return;
}
let timer = self.find(ets_timer);
if let Some(to_remove) = timer {
let tail = to_remove.next.take();
let mut current = self.head.as_mut();
let before = {
let mut found = None;
while let Some(before) = current {
if core::ptr::eq(before.next.as_mut().unwrap().ets_timer, ets_timer) {
found = Some(before);
break;
}
current = before.next.as_mut();
}
found
};
if let Some(before) = before {
let to_remove = before.next.take().unwrap();
let to_remove = Box::into_raw(to_remove);
unsafe {
crate::compat::malloc::free(to_remove as *mut _);
}
before.next = tail;
}
}
}
fn push(&mut self, to_add: Box<Timer>) -> Result<(), ()> {
if self.head.is_none() {
self.head = Some(to_add);
return Ok(());
}
let mut current = self.head.as_mut();
while let Some(timer) = current {
if timer.next.is_none() {
timer.next = Some(to_add);
break;
}
current = timer.next.as_mut();
}
Ok(())
}
}
unsafe impl Send for TimerQueue {}
pub(crate) static TIMERS: NonReentrantMutex<TimerQueue> = NonReentrantMutex::new(TimerQueue::new());
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
pub(crate) fn compat_timer_arm(ets_timer: *mut ets_timer, tmout: u32, repeat: bool) {
compat_timer_arm_us(ets_timer, tmout * 1000, repeat);
}
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
pub(crate) fn compat_timer_arm_us(ets_timer: *mut ets_timer, us: u32, repeat: bool) {
let systick = crate::preempt::now();
let ticks = crate::time::micros_to_ticks(us as u64);
trace!(
"timer_arm_us {:x} current: {} ticks: {} repeat: {}",
ets_timer as usize, systick, ticks, repeat
ets_timer as usize,
crate::preempt::now(),
crate::time::micros_to_ticks(us as u64),
repeat
);
TIMERS.with(|timers| {
if let Some(timer) = timers.find(ets_timer) {
timer.started = systick;
timer.timeout = ticks;
timer.active = true;
timer.periodic = repeat;
} else {
trace!("timer_arm_us {:x} not found", ets_timer as usize);
}
})
let ets_timer = unwrap!(unsafe { ets_timer.as_mut() }, "ets_timer is null");
let timer = unwrap!(TimerPtr::new(ets_timer.priv_.cast()), "timer is null");
let timer = unsafe { TimerHandle::ref_from_ptr(&timer) };
timer.arm(us as u64, repeat);
}
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
pub(crate) fn compat_timer_disarm(ets_timer: *mut ets_timer) {
trace!("timer disarm");
TIMERS.with(|timers| {
if let Some(timer) = timers.find(ets_timer) {
trace!("timer_disarm {:x}", timer.id());
timer.active = false;
} else {
trace!("timer_disarm {:x} not found", ets_timer as usize);
}
})
let ets_timer = unwrap!(unsafe { ets_timer.as_mut() }, "ets_timer is null");
if let Some(timer) = TimerPtr::new(ets_timer.priv_.cast()) {
let timer = unsafe { TimerHandle::ref_from_ptr(&timer) };
timer.disarm();
}
}
fn delete_timer(ets_timer: &mut ets_timer) {
if let Some(timer) = TimerPtr::new(ets_timer.priv_.cast()) {
let timer = unsafe { TimerHandle::from_ptr(timer) };
core::mem::drop(timer);
}
}
#[cfg(feature = "wifi")]
pub(crate) fn compat_timer_done(ets_timer: *mut ets_timer) {
trace!("timer done");
TIMERS.with(|timers| {
if let Some(timer) = timers.find(ets_timer) {
trace!("timer_done {:x}", timer.id());
timer.active = false;
unsafe {
(*ets_timer).priv_ = core::ptr::null_mut();
(*ets_timer).expire = 0;
}
let ets_timer = unwrap!(unsafe { ets_timer.as_mut() }, "ets_timer is null");
timers.remove(ets_timer);
} else {
trace!("timer_done {:x} not found", ets_timer as usize);
}
})
delete_timer(ets_timer);
}
#[cfg(any(feature = "wifi", all(feature = "ble", npl)))]
pub(crate) fn compat_timer_setfn(
ets_timer: *mut ets_timer,
pfunction: unsafe extern "C" fn(*mut c_types::c_void),
parg: *mut c_types::c_void,
pfunction: unsafe extern "C" fn(*mut c_void),
parg: *mut c_void,
) {
trace!(
"timer_setfn {:x} {:?} {:?}",
ets_timer as usize, pfunction, parg
);
let set = TIMERS.with(|timers| unsafe {
if let Some(timer) = timers.find(ets_timer) {
timer.callback = TimerCallback::new(pfunction, parg);
timer.active = false;
(*ets_timer).expire = 0;
let ets_timer = unwrap!(unsafe { ets_timer.as_mut() }, "ets_timer is null");
true
} else {
(*ets_timer).next = core::ptr::null_mut();
(*ets_timer).period = 0;
(*ets_timer).func = None;
(*ets_timer).priv_ = core::ptr::null_mut();
// This function is expected to create timers. For the simplicity of the preempt API, we
// will not update existing timers, but create new ones.
delete_timer(ets_timer);
let timer =
crate::compat::malloc::calloc(1, core::mem::size_of::<Timer>()) as *mut Timer;
(*timer).next = None;
(*timer).ets_timer = ets_timer;
(*timer).started = 0;
(*timer).timeout = 0;
(*timer).active = false;
(*timer).periodic = false;
(*timer).callback = TimerCallback::new(pfunction, parg);
timers.push(Box::from_raw(timer)).is_ok()
}
});
if !set {
warn!("Failed to set timer function {:x}", ets_timer as usize);
// Unfortunately, Rust can't optimize this into a single fat pointer, so this will
// allocate on the heap. We could optimize for C functions in the preempt API, but that would
// require some unfortunate unsafe code.
struct CCallback {
func: unsafe extern "C" fn(*mut c_void),
data: *mut c_void,
}
unsafe impl Send for CCallback {}
impl CCallback {
unsafe fn call(&mut self) {
unsafe { (self.func)(self.data) }
}
}
let mut callback = CCallback {
func: pfunction,
data: parg,
};
let timer = TimerHandle::new(Box::new(move || unsafe { callback.call() }))
.leak()
.cast()
.as_ptr();
ets_timer.next = core::ptr::null_mut();
ets_timer.period = 0;
ets_timer.func = None;
ets_timer.priv_ = timer;
}

View File

@ -129,7 +129,6 @@ use crate::wifi::WifiError;
use crate::{
preempt::yield_task,
radio::{setup_radio_isr, shutdown_radio_isr},
tasks::init_tasks,
};
// can't use instability on inline module definitions, see https://github.com/rust-lang/rust/issues/54727
@ -177,10 +176,6 @@ unstable_module! {
}
pub(crate) mod common_adapter;
#[doc(hidden)]
pub mod tasks;
pub(crate) mod memory_fence;
pub(crate) static ESP_RADIO_LOCK: RawMutex = RawMutex::new();
@ -261,7 +256,6 @@ pub fn init<'d>() -> Result<Controller<'d>, InitializationError> {
// This initializes the task switcher
preempt::enable();
init_tasks();
yield_task();
wifi_set_log_verbose();

View File

@ -1,40 +0,0 @@
use crate::{
compat::timer_compat::TIMERS,
preempt::{task_create, yield_task},
};
/// Initializes the `timer` task for the Wi-Fi driver.
pub(crate) fn init_tasks() {
// schedule the timer task
unsafe {
task_create(timer_task, core::ptr::null_mut(), 8192);
}
}
/// Entry point for the timer task responsible for handling scheduled timer
/// events.
pub(crate) extern "C" fn timer_task(_param: *mut esp_wifi_sys::c_types::c_void) {
loop {
let current_timestamp = crate::preempt::now();
let to_run = TIMERS.with(|timers| {
let to_run = unsafe { timers.find_next_due(current_timestamp) }?;
to_run.active = to_run.periodic;
if to_run.periodic {
to_run.started = current_timestamp;
}
Some(to_run.callback)
});
// run the due timer callback NOT in an interrupt free context
if let Some(to_run) = to_run {
trace!("trigger timer....");
to_run.call();
trace!("timer callback called");
} else {
yield_task();
}
}
}