Skip to content

Commit

Permalink
embassy-time-driver V0.2 impl
Browse files Browse the repository at this point in the history
  • Loading branch information
ivmarkov committed Jan 6, 2025
1 parent 6453bed commit f36bf21
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 97 deletions.
1 change: 1 addition & 0 deletions .github/configs/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y

# Examples often require a larger than the default stack size for the main thread and for the sysloop event task.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096 # Necessary when the `embassy-time-driver` feature is enabled
CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=8192
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=8192
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Breaking
- New `embassy-time-driver` implementation compatible with `embassy-time-driver` V0.2 (#548)

## [0.50.1] - 2025-01-06
### Fixed
- Fix ambiguous name error (a compilation issue when the NimBLE component is enabled in esp-idf-sys)
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ harness = false
default = ["std", "binstart"]

std = ["alloc", "log/std", "esp-idf-hal/std", "embedded-svc/std", "futures-io"]
embassy-time-driver = ["dep:embassy-time-driver", "embassy-time-queue-utils"]
alloc = ["esp-idf-hal/alloc", "embedded-svc/alloc", "uncased/alloc"]
nightly = ["embedded-svc/nightly", "esp-idf-hal/nightly"]
experimental = ["embedded-svc/experimental", "esp-idf-hal/experimental"]
Expand All @@ -48,7 +49,8 @@ uncased = { version = "0.9.7", default-features = false }
embedded-hal-async = { version = "1", default-features = false }
embedded-svc = { version = "0.28", default-features = false }
esp-idf-hal = { version = "0.45", default-features = false }
embassy-time-driver = { version = "0.1", optional = true, features = ["tick-hz-1_000_000"] }
embassy-time-driver = { version = "0.2", optional = true, features = ["tick-hz-1_000_000"] }
embassy-time-queue-utils = { version = "0.1", optional = true }
embassy-futures = "0.1"
embedded-storage = { version = "0.3", optional = true }
futures-io = { version = "0.3", optional = true }
Expand Down
190 changes: 94 additions & 96 deletions src/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,134 +378,132 @@ mod isr {
}
}

/// This module is used to provide a time driver for the `embassy-time` crate.
///
/// The minimum provided resolution is ~ 20-30us when the CPU is at top speed of 240MHz
/// (https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/esp_timer.html#timeout-value-limits)
///
/// The tick-rate is 1MHz (i.e. 1 tick is 1us).
#[cfg(feature = "embassy-time-driver")]
pub mod embassy_time_driver {
use core::cell::UnsafeCell;
use core::cell::RefCell;
use core::task::Waker;

use heapless::Vec;

use ::embassy_time_driver::{AlarmHandle, Driver};

use crate::hal::task::CriticalSection;

use crate::sys::*;
use ::embassy_time_driver::Driver;
use embassy_time_queue_utils::Queue;

use crate::private::mutex::Mutex;
use crate::timer::*;

struct Alarm {
struct EspDriverInner {
queue: embassy_time_queue_utils::Queue,
timer: Option<EspTimer<'static>>,
#[allow(clippy::type_complexity)]
callback: Option<(fn(*mut ()), *mut ())>,
}

struct EspDriver<const MAX_ALARMS: usize = 16> {
alarms: UnsafeCell<Vec<Alarm, MAX_ALARMS>>,
cs: CriticalSection,
}

impl<const MAX_ALARMS: usize> EspDriver<MAX_ALARMS> {
const fn new() -> Self {
Self {
alarms: UnsafeCell::new(Vec::new()),
cs: CriticalSection::new(),
}
impl EspDriverInner {
fn now() -> u64 {
unsafe { esp_timer_get_time() as _ }
}

fn call(&self, id: u8) {
let callback = {
let _guard = self.cs.enter();

let alarm = self.alarm(id);

alarm.callback
};
fn schedule_next_expiration(&mut self) {
/// End of epoch minus one day
const MAX_SAFE_TIMEOUT_US: u64 = u64::MAX - 24 * 60 * 60 * 1000 * 1000;

let timer = self.timer.as_mut().unwrap();

loop {
let now = Self::now();
let next_at = self.queue.next_expiration(now);

if now < next_at {
let after = next_at - now;

if after <= MAX_SAFE_TIMEOUT_US {
// Why?
// The ESP-IDF Timer API does not have a `Timer::at` method so we have to call it with
// `Timer::after(next_at - now)` instead. The problem is - even though the ESP IDF
// Timer API does not have a `Timer::at` method - _internally_ it takes our `next_at - now`,
// adds to it a **newer** "now" and sets this as the moment in time when the timer should trigger.
//
// Consider what would happen if we call `Timer::after(u64::MAX - now)`:
// The result would be something like `u64::MAX - now + (now + 1)` which would silently overflow and
// trigger the timer after 1us:
// https://github.com/espressif/esp-idf/blob/b5ac4fbdf9e9fb320bb0a98ee4fbaa18f8566f37/components/esp_timer/src/esp_timer.c#L188
//
// To workaround this problem, we make sure to never call `Timer::after(ms)` with `ms` greater than `MAX_SAFE_TIMEOUT_US`
// (i.e. the end of epoch - one day).
//
// Thus, even if we are un-scheduled between the calculation of our own `now` and the driver's newer `now`,
// there is one extra **day** of millis to accomodate for the potential overflow. If the overflow does happen still
// (which is kinda unthinkable given the time scales we are working with), the timer will re-trigger immediately,
// but hopefully on the next (or next after next and so on) re-trigger, we won't have the overflow anymore.
timer.after(Duration::from_micros(after)).unwrap();
}

if let Some((func, arg)) = callback {
func(arg)
break;
}
}
}
}

#[allow(clippy::mut_from_ref)]
fn alarm(&self, id: u8) -> &mut Alarm {
&mut unsafe { self.alarms.get().as_mut() }.unwrap()[id as usize]
struct EspDriver {
inner: Mutex<RefCell<EspDriverInner>>,
}

impl EspDriver {
const fn new() -> Self {
Self {
inner: Mutex::new(RefCell::new(EspDriverInner {
queue: Queue::new(),
timer: None,
})),
}
}
}

unsafe impl<const MAX_ALARMS: usize> Send for EspDriver<MAX_ALARMS> {}
unsafe impl<const MAX_ALARMS: usize> Sync for EspDriver<MAX_ALARMS> {}
unsafe impl Send for EspDriver {}
unsafe impl Sync for EspDriver {}

impl<const MAX_ALARMS: usize> Driver for EspDriver<MAX_ALARMS> {
impl Driver for EspDriver {
fn now(&self) -> u64 {
unsafe { esp_timer_get_time() as _ }
EspDriverInner::now()
}

unsafe fn allocate_alarm(&self) -> Option<AlarmHandle> {
let id = {
let _guard = self.cs.enter();

let id = self.alarms.get().as_mut().unwrap().len();

if id < MAX_ALARMS {
self.alarms
.get()
.as_mut()
.unwrap()
.push(Alarm {
timer: None,
callback: None,
})
.unwrap_or_else(|_| unreachable!());

id as u8
} else {
return None;
}
};

fn schedule_wake(&self, at: u64, waker: &Waker) {
let service = EspTimerService::<Task>::new().unwrap();

// Driver is always statically allocated, so this is safe
let static_self: &'static Self = core::mem::transmute(self);

self.alarm(id).timer = Some(service.timer(move || static_self.call(id)).unwrap());

Some(AlarmHandle::new(id))
}

fn set_alarm_callback(&self, handle: AlarmHandle, callback: fn(*mut ()), ctx: *mut ()) {
let _guard = self.cs.enter();

let alarm = self.alarm(handle.id());

alarm.callback = Some((callback, ctx));
}
let guard = self.inner.lock();
let mut inner = guard.borrow_mut();

if inner.timer.is_none() {
// Driver is always statically allocated, so this is safe
let static_self: &'static Self = unsafe { core::mem::transmute(self) };

inner.timer = Some(
service
.timer(move || {
static_self
.inner
.lock()
.borrow_mut()
.schedule_next_expiration()
})
.unwrap(),
);
}

fn set_alarm(&self, handle: AlarmHandle, timestamp: u64) -> bool {
let alarm = self.alarm(handle.id());

let now = self.now();

if now < timestamp {
alarm
.timer
.as_mut()
.unwrap()
.after(Duration::from_micros(timestamp - now))
.unwrap();
true
} else {
false
if inner.queue.schedule_wake(at, waker) {
inner.schedule_next_expiration();
}
}
}

pub type LinkWorkaround = [*mut (); 4];
pub type LinkWorkaround = [*mut (); 2];

#[used]
static mut __INTERNAL_REFERENCE: LinkWorkaround = [
_embassy_time_now as *mut _,
_embassy_time_allocate_alarm as *mut _,
_embassy_time_set_alarm_callback as *mut _,
_embassy_time_set_alarm as *mut _,
_embassy_time_schedule_wake as *mut _,
];

pub fn link() -> LinkWorkaround {
Expand Down

0 comments on commit f36bf21

Please sign in to comment.