feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+137
@@ -0,0 +1,137 @@
|
||||
//! Interrupt-mode executor.
|
||||
|
||||
use core::{cell::UnsafeCell, mem::MaybeUninit};
|
||||
|
||||
use embassy_executor::{raw, SendSpawner};
|
||||
use esp_hal::{
|
||||
interrupt::{self, software::SoftwareInterrupt, InterruptHandler},
|
||||
Cpu,
|
||||
};
|
||||
use portable_atomic::{AtomicUsize, Ordering};
|
||||
|
||||
const COUNT: usize = 3 + cfg!(not(multi_core)) as usize;
|
||||
static mut EXECUTORS: [CallbackContext; COUNT] = [const { CallbackContext::new() }; COUNT];
|
||||
|
||||
/// 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.
|
||||
pub struct InterruptExecutor<const SWI: u8> {
|
||||
core: AtomicUsize,
|
||||
executor: UnsafeCell<MaybeUninit<raw::Executor>>,
|
||||
interrupt: SoftwareInterrupt<SWI>,
|
||||
}
|
||||
|
||||
unsafe impl<const SWI: u8> Send for InterruptExecutor<SWI> {}
|
||||
unsafe impl<const SWI: u8> Sync for InterruptExecutor<SWI> {}
|
||||
|
||||
struct CallbackContext {
|
||||
raw_executor: UnsafeCell<*mut raw::Executor>,
|
||||
}
|
||||
|
||||
impl CallbackContext {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
raw_executor: UnsafeCell::new(core::ptr::null_mut()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self) -> *mut raw::Executor {
|
||||
unsafe { *self.raw_executor.get() }
|
||||
}
|
||||
|
||||
fn set(&self, executor: *mut raw::Executor) {
|
||||
unsafe { self.raw_executor.get().write(executor) };
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_interrupt<const NUM: u8>() {
|
||||
let swi = unsafe { SoftwareInterrupt::<NUM>::steal() };
|
||||
swi.reset();
|
||||
|
||||
unsafe {
|
||||
let executor = unwrap!(EXECUTORS[NUM as usize].get().as_mut());
|
||||
executor.poll();
|
||||
}
|
||||
}
|
||||
|
||||
impl<const SWI: u8> InterruptExecutor<SWI> {
|
||||
/// Create a new `InterruptExecutor`.
|
||||
/// This takes the software interrupt to be used internally.
|
||||
#[inline]
|
||||
pub const fn new(interrupt: SoftwareInterrupt<SWI>) -> Self {
|
||||
Self {
|
||||
core: AtomicUsize::new(usize::MAX),
|
||||
executor: UnsafeCell::new(MaybeUninit::uninit()),
|
||||
interrupt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the executor at the given priority level.
|
||||
///
|
||||
/// This initializes the executor, enables the interrupt, and returns.
|
||||
/// The executor keeps running in the background through the interrupt.
|
||||
///
|
||||
/// This returns a [`SendSpawner`] you can use to spawn tasks on it. A
|
||||
/// [`SendSpawner`] is returned instead of a
|
||||
/// [`Spawner`](embassy_executor::Spawner) because the
|
||||
/// executor effectively runs in a different "thread" (the interrupt),
|
||||
/// so spawning tasks on it is effectively sending them.
|
||||
///
|
||||
/// To obtain a [`Spawner`](embassy_executor::Spawner) for this executor,
|
||||
/// use [`Spawner::for_current_executor`](embassy_executor::Spawner::for_current_executor)
|
||||
/// from a task running in it.
|
||||
pub fn start(&'static mut self, priority: interrupt::Priority) -> SendSpawner {
|
||||
if self
|
||||
.core
|
||||
.compare_exchange(
|
||||
usize::MAX,
|
||||
Cpu::current() as usize,
|
||||
Ordering::Acquire,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
panic!("InterruptExecutor::start() called multiple times on the same executor.");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
(*self.executor.get())
|
||||
.as_mut_ptr()
|
||||
.write(raw::Executor::new((SWI as usize) as *mut ()));
|
||||
|
||||
EXECUTORS[SWI as usize].set((*self.executor.get()).as_mut_ptr());
|
||||
}
|
||||
|
||||
let swi_handler = match SWI {
|
||||
0 => handle_interrupt::<0>,
|
||||
1 => handle_interrupt::<1>,
|
||||
2 => handle_interrupt::<2>,
|
||||
#[cfg(not(multi_core))]
|
||||
3 => handle_interrupt::<3>,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.interrupt
|
||||
.set_interrupt_handler(InterruptHandler::new(swi_handler, priority));
|
||||
|
||||
let executor = unsafe { (*self.executor.get()).assume_init_ref() };
|
||||
executor.spawner().make_send()
|
||||
}
|
||||
|
||||
/// Get a SendSpawner for this executor
|
||||
///
|
||||
/// This returns a [`SendSpawner`] you can use to spawn tasks on this
|
||||
/// executor.
|
||||
///
|
||||
/// This MUST only be called on an executor that has already been started.
|
||||
/// The function will panic otherwise.
|
||||
pub fn spawner(&'static self) -> SendSpawner {
|
||||
if self.core.load(Ordering::Acquire) == usize::MAX {
|
||||
panic!("InterruptExecutor::spawner() called on uninitialized executor.");
|
||||
}
|
||||
let executor = unsafe { (*self.executor.get()).assume_init_ref() };
|
||||
executor.spawner().make_send()
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
pub use self::{interrupt::*, thread::*};
|
||||
|
||||
mod interrupt;
|
||||
mod thread;
|
||||
|
||||
#[export_name = "__pender"]
|
||||
fn __pender(context: *mut ()) {
|
||||
use esp_hal::interrupt::software::SoftwareInterrupt;
|
||||
|
||||
match context as usize {
|
||||
// For interrupt executors, the context value is the
|
||||
// software interrupt number
|
||||
0 => unsafe { SoftwareInterrupt::<0>::steal().raise() },
|
||||
1 => unsafe { SoftwareInterrupt::<1>::steal().raise() },
|
||||
2 => unsafe { SoftwareInterrupt::<2>::steal().raise() },
|
||||
#[cfg(not(multi_core))]
|
||||
3 => unsafe { SoftwareInterrupt::<3>::steal().raise() },
|
||||
// THREAD_MODE_CONTEXT + core ID
|
||||
16 => thread::pend_thread_mode(0),
|
||||
#[cfg(multi_core)]
|
||||
17 => thread::pend_thread_mode(1),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
//! Multicore-aware thread-mode embassy executor.
|
||||
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use embassy_executor::{raw, Spawner};
|
||||
use esp_hal::Cpu;
|
||||
#[cfg(multi_core)]
|
||||
use esp_hal::{interrupt::software::SoftwareInterrupt, macros::handler};
|
||||
#[cfg(low_power_wait)]
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
|
||||
pub(crate) const THREAD_MODE_CONTEXT: usize = 16;
|
||||
|
||||
/// global atomic used to keep track of whether there is work to do since sev()
|
||||
/// is not available on either Xtensa or RISC-V
|
||||
static SIGNAL_WORK_THREAD_MODE: [AtomicBool; Cpu::COUNT] =
|
||||
[const { AtomicBool::new(false) }; Cpu::COUNT];
|
||||
|
||||
#[cfg(all(multi_core, low_power_wait))]
|
||||
#[handler]
|
||||
fn software3_interrupt() {
|
||||
// This interrupt is fired when the thread-mode executor's core needs to be
|
||||
// woken. It doesn't matter which core handles this interrupt first, the
|
||||
// point is just to wake up the core that is currently executing
|
||||
// `waiti`.
|
||||
unsafe { SoftwareInterrupt::<3>::steal().reset() };
|
||||
}
|
||||
|
||||
pub(crate) fn pend_thread_mode(_core: usize) {
|
||||
#[cfg(low_power_wait)]
|
||||
{
|
||||
// Signal that there is work to be done.
|
||||
SIGNAL_WORK_THREAD_MODE[_core].store(true, Ordering::SeqCst);
|
||||
|
||||
// If we are pending a task on the current core, we're done. Otherwise, we
|
||||
// need to make sure the other core wakes up.
|
||||
#[cfg(multi_core)]
|
||||
if _core != Cpu::current() as usize {
|
||||
// We need to clear the interrupt from software. We don't actually
|
||||
// need it to trigger and run the interrupt handler, we just need to
|
||||
// kick waiti to return.
|
||||
unsafe { SoftwareInterrupt::<3>::steal().raise() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread mode executor.
|
||||
///
|
||||
/// This is the simplest and most common kind of executor. It runs on thread
|
||||
/// mode (at the lowest priority level).
|
||||
#[cfg_attr(multi_core, doc = "")]
|
||||
#[cfg_attr(
|
||||
multi_core,
|
||||
doc = "This executor is safe to use on multiple cores. You need to
|
||||
create one instance per core. The executors don't steal tasks from each other."
|
||||
)]
|
||||
pub struct Executor {
|
||||
inner: raw::Executor,
|
||||
not_send: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
/// Create a new Executor.
|
||||
#[cfg_attr(
|
||||
multi_core,
|
||||
doc = r#"
|
||||
|
||||
This will use software-interrupt 3 which isn't available for anything else to wake the other core(s)."#
|
||||
)]
|
||||
pub fn new() -> Self {
|
||||
#[cfg(all(multi_core, low_power_wait))]
|
||||
unsafe {
|
||||
SoftwareInterrupt::<3>::steal().set_interrupt_handler(software3_interrupt);
|
||||
}
|
||||
|
||||
Self {
|
||||
inner: raw::Executor::new((THREAD_MODE_CONTEXT + Cpu::current() as usize) as *mut ()),
|
||||
not_send: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 [StaticCell](https://docs.rs/static_cell/latest/static_cell/) (safe)
|
||||
/// - a `static mut` (unsafe, not recommended)
|
||||
/// - 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(self.inner.spawner());
|
||||
|
||||
#[cfg(low_power_wait)]
|
||||
let cpu = Cpu::current() as usize;
|
||||
|
||||
loop {
|
||||
unsafe { self.inner.poll() };
|
||||
|
||||
#[cfg(low_power_wait)]
|
||||
Self::wait_impl(cpu);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(xtensa, low_power_wait))]
|
||||
fn wait_impl(cpu: usize) {
|
||||
// Manual critical section implementation that only masks interrupts handlers.
|
||||
// We must not acquire the cross-core on dual-core systems because that would
|
||||
// prevent the other core from doing useful work while this core is sleeping.
|
||||
let token: critical_section::RawRestoreState;
|
||||
unsafe { core::arch::asm!("rsil {0}, 5", out(reg) token) };
|
||||
|
||||
// we do not care about race conditions between the load and store operations,
|
||||
// interrupts will only set this value to true.
|
||||
if SIGNAL_WORK_THREAD_MODE[cpu].load(Ordering::SeqCst) {
|
||||
// if there is work to do, exit critical section and loop back to polling
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"wsr.ps {0}",
|
||||
"rsync",
|
||||
in(reg) token
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// `waiti` sets the `PS.INTLEVEL` when slipping into sleep because critical
|
||||
// sections in Xtensa are implemented via increasing `PS.INTLEVEL`.
|
||||
// The critical section ends here. Take care not add code after
|
||||
// `waiti` if it needs to be inside the CS.
|
||||
unsafe { core::arch::asm!("waiti 0") };
|
||||
}
|
||||
// If this races and some waker sets the signal, we'll reset it, but still poll.
|
||||
SIGNAL_WORK_THREAD_MODE[cpu].store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg(all(riscv, low_power_wait))]
|
||||
fn wait_impl(cpu: usize) {
|
||||
// we do not care about race conditions between the load and store operations,
|
||||
// interrupts will only set this value to true.
|
||||
critical_section::with(|_| {
|
||||
// if there is work to do, loop back to polling
|
||||
if !SIGNAL_WORK_THREAD_MODE[cpu].load(Ordering::SeqCst) {
|
||||
// if not, wait for interrupt
|
||||
unsafe { core::arch::asm!("wfi") };
|
||||
}
|
||||
});
|
||||
// if an interrupt occurred while waiting, it will be serviced here
|
||||
// If this races and some waker sets the signal, we'll reset it, but still poll.
|
||||
SIGNAL_WORK_THREAD_MODE[cpu].store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Executor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Vendored
+226
@@ -0,0 +1,226 @@
|
||||
#![macro_use]
|
||||
#![allow(unused_macros)]
|
||||
|
||||
#[cfg(all(feature = "defmt", feature = "log"))]
|
||||
compile_error!("You may not enable both `defmt` and `log` features.");
|
||||
|
||||
macro_rules! assert {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_eq {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert_eq!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert_eq!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_ne {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert_ne!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert_ne!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert_eq {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert_eq!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert_eq!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert_ne {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert_ne!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert_ne!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! todo {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::todo!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::todo!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! unreachable {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::unreachable!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::unreachable!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! panic {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::panic!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::panic!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! trace {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "log")]
|
||||
::log::trace!($s $(, $x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::trace!($s $(, $x)*);
|
||||
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "log")]
|
||||
::log::debug!($s $(, $x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug!($s $(, $x)*);
|
||||
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! info {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "log")]
|
||||
::log::info!($s $(, $x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::info!($s $(, $x)*);
|
||||
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! warn {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "log")]
|
||||
::log::warn!($s $(, $x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::warn!($s $(, $x)*);
|
||||
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! error {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "log")]
|
||||
::log::error!($s $(, $x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::error!($s $(, $x)*);
|
||||
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
macro_rules! unwrap {
|
||||
($($x:tt)*) => {
|
||||
::defmt::unwrap!($($x)*)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
macro_rules! unwrap {
|
||||
($arg:expr) => {
|
||||
match $crate::fmt::Try::into_result($arg) {
|
||||
::core::result::Result::Ok(t) => t,
|
||||
::core::result::Result::Err(e) => {
|
||||
::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
($arg:expr, $($msg:expr),+ $(,)? ) => {
|
||||
match $crate::fmt::Try::into_result($arg) {
|
||||
::core::result::Result::Ok(t) => t,
|
||||
::core::result::Result::Err(e) => {
|
||||
::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct NoneError;
|
||||
|
||||
pub trait Try {
|
||||
type Ok;
|
||||
type Error;
|
||||
#[allow(unused)]
|
||||
fn into_result(self) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> Try for Option<T> {
|
||||
type Ok = T;
|
||||
type Error = NoneError;
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Result<T, NoneError> {
|
||||
self.ok_or(NoneError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Try for Result<T, E> {
|
||||
type Ok = T;
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
Vendored
+169
@@ -0,0 +1,169 @@
|
||||
//! Embassy support for [esp-hal].
|
||||
//!
|
||||
//! [Embassy] is a modern asynchronous framework intended for use with embedded
|
||||
//! systems. This package provides support for building applications using
|
||||
//! Embassy with [esp-hal].
|
||||
//!
|
||||
//! [esp-hal]: https://github.com/esp-rs/esp-hal
|
||||
//! [embassy]: https://github.com/embassy-rs/embassy
|
||||
//!
|
||||
//! ## Executors
|
||||
//!
|
||||
//! Two types of executors are provided:
|
||||
//!
|
||||
//! - [Executor]: A thread-mode executor
|
||||
//! - [InterruptExecutor]: An interrupt-mode executor
|
||||
//!
|
||||
//! [InterruptExecutor] can be used to achieve preemptive multitasking in
|
||||
//! asynchronous applications, which is typically something reserved for more
|
||||
//! traditional RTOS. More information can be found in the [Embassy
|
||||
//! documentation].
|
||||
//!
|
||||
//! [embassy documentation]: https://embassy.dev/book/
|
||||
//!
|
||||
//! ## Initialization
|
||||
//!
|
||||
//! Embassy **must** be initialized by calling the [init] function. This
|
||||
//! initialization must be performed *prior* to spawning any tasks.
|
||||
//!
|
||||
//! ## Feature Flags
|
||||
#![doc = document_features::document_features!()]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(xtensa, feature(asm_experimental_arch))]
|
||||
#![no_std]
|
||||
|
||||
// MUST be the first module
|
||||
mod fmt;
|
||||
|
||||
#[cfg(not(feature = "esp32"))]
|
||||
use esp_hal::timer::systimer::Alarm;
|
||||
use esp_hal::timer::{timg::Timer as TimgTimer, AnyTimer};
|
||||
pub use macros::main;
|
||||
|
||||
#[cfg(feature = "executors")]
|
||||
pub use self::executor::{Executor, InterruptExecutor};
|
||||
use self::time_driver::{EmbassyTimer, Timer};
|
||||
|
||||
#[cfg(feature = "executors")]
|
||||
mod executor;
|
||||
mod time_driver;
|
||||
|
||||
macro_rules! mk_static {
|
||||
($t:ty,$val:expr) => {{
|
||||
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
||||
#[deny(unused_attributes)]
|
||||
let x = STATIC_CELL.uninit().write(($val));
|
||||
x
|
||||
}};
|
||||
}
|
||||
|
||||
/// A trait to allow better UX for initializing the timers.
|
||||
///
|
||||
/// This trait is meant to be used only for the `init` function.
|
||||
/// Calling `timers()` multiple times may panic.
|
||||
pub trait TimerCollection {
|
||||
/// Returns the timers as a slice.
|
||||
fn timers(self) -> &'static mut [Timer];
|
||||
}
|
||||
|
||||
/// Helper trait to reduce boilerplate.
|
||||
///
|
||||
/// We can't blanket-implement for `Into<AnyTimer>` because of possible
|
||||
/// conflicting implementations.
|
||||
trait IntoAnyTimer: Into<AnyTimer> {}
|
||||
|
||||
impl IntoAnyTimer for AnyTimer {}
|
||||
|
||||
impl<T, DM> IntoAnyTimer for TimgTimer<T, DM>
|
||||
where
|
||||
DM: esp_hal::Mode,
|
||||
Self: Into<AnyTimer>,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "esp32"))]
|
||||
impl<T, DM, COMP, UNIT> IntoAnyTimer for Alarm<'_, T, DM, COMP, UNIT>
|
||||
where
|
||||
DM: esp_hal::Mode,
|
||||
Self: Into<AnyTimer>,
|
||||
{
|
||||
}
|
||||
|
||||
impl<T> TimerCollection for T
|
||||
where
|
||||
T: IntoAnyTimer,
|
||||
{
|
||||
fn timers(self) -> &'static mut [Timer] {
|
||||
Timer::new(self.into()).timers()
|
||||
}
|
||||
}
|
||||
|
||||
impl TimerCollection for Timer {
|
||||
fn timers(self) -> &'static mut [Timer] {
|
||||
let timers = mk_static!([Timer; 1], [self]);
|
||||
timers.timers()
|
||||
}
|
||||
}
|
||||
|
||||
impl TimerCollection for &'static mut [Timer] {
|
||||
fn timers(self) -> &'static mut [Timer] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> TimerCollection for &'static mut [Timer; N] {
|
||||
fn timers(self) -> &'static mut [Timer] {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_array {
|
||||
($n:literal) => {
|
||||
impl<T> TimerCollection for [T; $n]
|
||||
where
|
||||
T: IntoAnyTimer,
|
||||
{
|
||||
fn timers(self) -> &'static mut [Timer] {
|
||||
mk_static!([Timer; $n], self.map(|t| Timer::new(t.into())))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_array!(2);
|
||||
impl_array!(3);
|
||||
impl_array!(4);
|
||||
|
||||
/// Initialize embassy.
|
||||
///
|
||||
/// Call this as soon as possible, before the first timer-related operation.
|
||||
///
|
||||
/// The time driver can be one of a number of different options:
|
||||
///
|
||||
/// - A timg `Timer` instance
|
||||
/// - A systimer `Alarm` instance
|
||||
/// - An `AnyTimer` instance
|
||||
/// - A `OneShotTimer` instance
|
||||
/// - A mutable static slice of `OneShotTimer` instances
|
||||
/// - A mutable static array of `OneShotTimer` instances
|
||||
/// - A 2, 3, 4 element array of `AnyTimer` instances
|
||||
///
|
||||
/// Note that if you use the `integrated-timers` feature,
|
||||
/// you need to pass as many timers as you start executors.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust, no_run
|
||||
#[doc = esp_hal::before_snippet!()]
|
||||
/// use esp_hal::timg::TimerGroup;
|
||||
///
|
||||
/// let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||
/// esp_hal_embassy::init(timg0.timer0);
|
||||
///
|
||||
/// // ... now you can spawn embassy tasks or use `Timer::after` etc.
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn init(time_driver: impl TimerCollection) {
|
||||
EmbassyTimer::init(time_driver.timers())
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use critical_section::Mutex;
|
||||
use embassy_time_driver::{AlarmHandle, Driver};
|
||||
use esp_hal::{
|
||||
interrupt::{InterruptHandler, Priority},
|
||||
prelude::*,
|
||||
time::now,
|
||||
timer::{AnyTimer, OneShotTimer},
|
||||
};
|
||||
|
||||
pub const MAX_SUPPORTED_ALARM_COUNT: usize = 7;
|
||||
|
||||
pub type Timer = OneShotTimer<'static, AnyTimer>;
|
||||
|
||||
static TIMERS: Mutex<RefCell<Option<&'static mut [Timer]>>> = Mutex::new(RefCell::new(None));
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
struct AlarmState {
|
||||
pub callback: Cell<Option<(fn(*mut ()), *mut ())>>,
|
||||
pub allocated: Cell<bool>,
|
||||
}
|
||||
|
||||
unsafe impl Send for AlarmState {}
|
||||
|
||||
impl AlarmState {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
callback: Cell::new(None),
|
||||
allocated: Cell::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct EmbassyTimer {
|
||||
alarms: Mutex<[AlarmState; MAX_SUPPORTED_ALARM_COUNT]>,
|
||||
}
|
||||
|
||||
embassy_time_driver::time_driver_impl!(static DRIVER: EmbassyTimer = EmbassyTimer {
|
||||
alarms: Mutex::new([const { AlarmState::new() }; MAX_SUPPORTED_ALARM_COUNT]),
|
||||
});
|
||||
|
||||
impl EmbassyTimer {
|
||||
pub(super) fn init(timers: &'static mut [Timer]) {
|
||||
if timers.len() > MAX_SUPPORTED_ALARM_COUNT {
|
||||
panic!(
|
||||
"Maximum of {} timers can be used.",
|
||||
MAX_SUPPORTED_ALARM_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
critical_section::with(|cs| {
|
||||
timers.iter_mut().enumerate().for_each(|(n, timer)| {
|
||||
timer.enable_interrupt(false);
|
||||
timer.stop();
|
||||
|
||||
if DRIVER.alarms.borrow(cs)[n].allocated.get() {
|
||||
// FIXME: we should track which core allocated an alarm and bind the interrupt
|
||||
// to that core.
|
||||
timer.set_interrupt_handler(HANDLERS[n]);
|
||||
}
|
||||
});
|
||||
|
||||
TIMERS.replace(cs, Some(timers));
|
||||
});
|
||||
}
|
||||
|
||||
fn on_interrupt(&self, id: usize) {
|
||||
let cb = critical_section::with(|cs| {
|
||||
let mut timers = TIMERS.borrow_ref_mut(cs);
|
||||
let timers = unwrap!(timers.as_mut(), "Time driver not initialized");
|
||||
let timer = &mut timers[id];
|
||||
|
||||
timer.clear_interrupt();
|
||||
|
||||
let alarm = &self.alarms.borrow(cs)[id];
|
||||
alarm.callback.get()
|
||||
});
|
||||
|
||||
if let Some((f, ctx)) = cb {
|
||||
f(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn arm(timer: &mut Timer, timestamp: u64) {
|
||||
let now = now().duration_since_epoch();
|
||||
let ts = timestamp.micros();
|
||||
// if the TS is already in the past make the timer fire immediately
|
||||
let timeout = if ts > now { ts - now } else { 0.micros() };
|
||||
unwrap!(timer.schedule(timeout));
|
||||
timer.enable_interrupt(true);
|
||||
}
|
||||
}
|
||||
|
||||
impl Driver for EmbassyTimer {
|
||||
fn now(&self) -> u64 {
|
||||
now().ticks()
|
||||
}
|
||||
|
||||
unsafe fn allocate_alarm(&self) -> Option<AlarmHandle> {
|
||||
critical_section::with(|cs| {
|
||||
for (i, alarm) in self.alarms.borrow(cs).iter().enumerate() {
|
||||
if alarm.allocated.get() {
|
||||
continue;
|
||||
}
|
||||
let mut timer = TIMERS.borrow_ref_mut(cs);
|
||||
// `allocate_alarm` may be called before `esp_hal_embassy::init()`, so
|
||||
// we need to check if we have timers.
|
||||
if let Some(timer) = &mut *timer {
|
||||
// If we do, bind the interrupt handler to the timer.
|
||||
// This ensures that alarms allocated after init are correctly bound to the
|
||||
// core that created the executor.
|
||||
let timer = unwrap!(
|
||||
timer.get_mut(i),
|
||||
"There are not enough timers to allocate a new alarm. Call `esp_hal_embassy::init()` with the correct number of timers."
|
||||
);
|
||||
timer.set_interrupt_handler(HANDLERS[i]);
|
||||
}
|
||||
|
||||
// set alarm so it is not overwritten
|
||||
alarm.allocated.set(true);
|
||||
return Some(AlarmHandle::new(i as u8));
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn set_alarm_callback(&self, alarm: AlarmHandle, callback: fn(*mut ()), ctx: *mut ()) {
|
||||
let n = alarm.id() as usize;
|
||||
critical_section::with(|cs| {
|
||||
let alarm = &self.alarms.borrow(cs)[n];
|
||||
alarm.callback.set(Some((callback, ctx)));
|
||||
})
|
||||
}
|
||||
|
||||
fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) -> bool {
|
||||
// If `embassy-executor/integrated-timers` is enabled and there are no pending
|
||||
// timers, embassy still calls `set_alarm` with `u64::MAX`. By returning
|
||||
// `true` we signal that no re-polling is necessary.
|
||||
if timestamp == u64::MAX {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The hardware fires the alarm even if timestamp is lower than the current
|
||||
// time. In this case the interrupt handler will pend a wake-up when we exit the
|
||||
// critical section.
|
||||
//
|
||||
// This is correct behavior. See https://docs.rs/embassy-time-driver/0.1.0/embassy_time_driver/trait.Driver.html#tymethod.set_alarm
|
||||
// (... the driver should return true and arrange to call the alarm callback as
|
||||
// soon as possible, but not synchronously.)
|
||||
critical_section::with(|cs| {
|
||||
let mut timers = TIMERS.borrow_ref_mut(cs);
|
||||
let timers = unwrap!(timers.as_mut(), "Time driver not initialized");
|
||||
let timer = &mut timers[alarm.id() as usize];
|
||||
|
||||
Self::arm(timer, timestamp);
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
static HANDLERS: [InterruptHandler; MAX_SUPPORTED_ALARM_COUNT] = [
|
||||
handler0, handler1, handler2, handler3, handler4, handler5, handler6,
|
||||
];
|
||||
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler0() {
|
||||
DRIVER.on_interrupt(0);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler1() {
|
||||
DRIVER.on_interrupt(1);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler2() {
|
||||
DRIVER.on_interrupt(2);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler3() {
|
||||
DRIVER.on_interrupt(3);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler4() {
|
||||
DRIVER.on_interrupt(4);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler5() {
|
||||
DRIVER.on_interrupt(5);
|
||||
}
|
||||
#[handler(priority = Priority::max())]
|
||||
fn handler6() {
|
||||
DRIVER.on_interrupt(6);
|
||||
}
|
||||
Reference in new issue
Block a user