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:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"8a83ee6cf028c8b0a5814315fee580b03b458a23dc2d39e1a21975852cf2fc99","CHANGELOG.md":"4cac2db23fb802bf2d7e5e66dc1f108a706e26b79d280f612507790e03ab4acb","Cargo.toml":"45950e27807af03f857860894f8b6f9fdc51897479b7f32ccca8a48b521cd531","Cargo.toml.orig":"2358f9028fff44f0971a6409bd5da348e22f4a1e617db996d9d0e8de957b7a83","MIGRATING-0.3.md":"3967d441bad1c90a791eea83a6dcb56d352a12b3fc7b3225df9313dcb393cd2d","MIGRATING-0.4.md":"b2feba195d8279e9a0090821cb9028e703c4fcc2b3038cafdd78c556c82b03fa","README.md":"6944768d98fe8278f0226a5a1aa97f8eafc27470339b5f03cfbb8431f0b84801","build.rs":"b1448257101c4a799f8b574673caebfe8e6e4474c273a47f20d26798eb27eb3e","src/executor/interrupt.rs":"cc7d2e4e10f9b5b269a1214eec2c3b3b922053e2a011fe1813c5617be3c1c9ba","src/executor/mod.rs":"e2ac0459621fafde02d269f6d75dc62e4405c434cf4f5d7a4366c002db67bde8","src/executor/thread.rs":"f59c75d9d64b14d6f3c90e635e5e2733f3fb920a27e499ec481e0764008e8303","src/fmt.rs":"ecadf3ab68ae254b4a766108a63c8b55c7ef4108ee5ab069250daacf44314c78","src/lib.rs":"8c2bcaa88066d1991ddb889bb2107c12cc4996f2a8d6d03dd1d3ce9d97cff760","src/time_driver.rs":"e9e946d749fa69efb2609f67dd10fca1ecdc4256c3f4e9fe9a1d0f4407a1eeb6"},"package":"c7d0f2537ea2ff9bea26a1c8bfe43ad580d4c89febf27189653a9cf95f1f7961"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "bdfec3781fd59c4d51173706e3cbee3760bbf8b6"
},
"path_in_vcs": "esp-hal-embassy"
}
+68
View File
@@ -0,0 +1,68 @@
# Changelog
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]
### Added
### Changed
### Fixed
### Removed
## 0.5.0 - 2024-11-20
### Added
- `ESP_HAL_EMBASSY_LOW_POWER_WAIT` configuration option. (#2329)
### Changed
- Reduce memory footprint by 4 bytes on multi-core MCUs.
### Fixed
- Alarm interrupts are now handled on the core that allocated them. (For executors created on the second core after calling `esp_hal_embassy::init`) (#2451)
### Removed
## 0.4.0 - 2024-10-10
### Changed
- MSRV bump to 1.79 (#2156)
### Removed
- Removed the `clocks` parameter from `esp_hal_embassy::init`. (#1999)
## 0.3.0 - 2024-08-29
### Added
- This package now re-exports the `esp_hal_procmacros::main` macro (#1828)
### Changed
- Updated to latest release (`0.6.0`) for `embassy-executor` (#1942)
- Changed `init` to accept timers of multiple types (#1957, #2012)
### Fixed
- Fixed a bug where the timeout was huge whenever the timestamp at the time of scheduling was already in the past (#1875)
- Fixed interrupt executors looping endlessly when `integrated-timers` is used. (#1936)
## 0.2.0 - 2024-07-15
### Changed
- Removed the TIMG and SYSTIMER time drivers, replaced by a generic time driver taking `OneShotTimer<ErasedTimer>` (#1753)
## 0.1.0 - 2024-06-04
[Unreleased]: https://github.com/esp-rs/esp-hal/commits/main/esp-hal-embassy?since=2024-11-20
+104
View File
@@ -0,0 +1,104 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.79.0"
name = "esp-hal-embassy"
version = "0.5.0"
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Embassy support for esp-hal"
readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/esp-rs/esp-hal"
[package.metadata.docs.rs]
default-target = "riscv32imac-unknown-none-elf"
features = ["esp32c6"]
[lib]
name = "esp_hal_embassy"
path = "src/lib.rs"
[dependencies.critical-section]
version = "1.2.0"
[dependencies.defmt]
version = "0.3.8"
optional = true
[dependencies.document-features]
version = "0.2.10"
[dependencies.embassy-executor]
version = "0.6.3"
optional = true
[dependencies.embassy-time-driver]
version = "0.1.0"
features = ["tick-hz-1_000_000"]
[dependencies.esp-hal]
version = "0.22.0"
[dependencies.log]
version = "0.4.22"
optional = true
[dependencies.macros]
version = "0.15.0"
features = ["embassy"]
package = "esp-hal-procmacros"
[dependencies.portable-atomic]
version = "1.9.0"
[dependencies.static_cell]
version = "2.1.0"
[build-dependencies.esp-build]
version = "0.1.0"
[build-dependencies.esp-config]
version = "0.2.0"
features = ["build"]
[build-dependencies.esp-metadata]
version = "0.4.0"
[features]
default = ["executors"]
defmt = [
"dep:defmt",
"embassy-executor?/defmt",
"esp-hal/defmt",
]
esp32 = ["esp-hal/esp32"]
esp32c2 = ["esp-hal/esp32c2"]
esp32c3 = ["esp-hal/esp32c3"]
esp32c6 = ["esp-hal/esp32c6"]
esp32h2 = ["esp-hal/esp32h2"]
esp32s2 = ["esp-hal/esp32s2"]
esp32s3 = ["esp-hal/esp32s3"]
executors = [
"dep:embassy-executor",
"esp-hal/__esp_hal_embassy",
]
integrated-timers = ["embassy-executor?/integrated-timers"]
log = ["dep:log"]
[lints.rust]
unexpected_cfgs = "allow"
+63
View File
@@ -0,0 +1,63 @@
# Migration Guide from 0.3.x to v0.4.x
## Initialization
You no longer have to set up clocks and pass them to `esp_hal_embassy::init`.
```diff
use esp_hal::{
- clock::ClockControl,
- peripherals::Peripherals,
prelude::*,
- system::SystemControl,
};
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) -> ! {
- let peripherals = Peripherals::take();
- let system = SystemControl::new(peripherals.SYSTEM);
- let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
+ let peripherals = esp_hal::init(esp_hal::Config::default());
let timg0 = TimerGroup::new(peripherals.TIMG0);
- esp_hal_embassy::init(&clocks, timg0);
+ esp_hal_embassy::init(timg0.timer0);
// ...
}
```
You have to specify a timer instance (that may be a `TimerGroup` timer unit
or a `SystemTimer` alarm) or an array of `AnyTimer`s when calling `init`.
An example of how you can set multiple timers (for example when using
multiple executors):
```rust
use esp_hal::{
prelude::*,
timer::{
AnyTimer,
systimer::SystemTimer
}
};
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) -> ! {
let peripherals = esp_hal::init(esp_hal::Config::default());
let timg0 = TimerGroup::new(peripherals.TIMG0);
let timer0: AnyTimer = timg0.timer0.into();
let timer1: AnyTimer = timg0.timer1.into();
// You can use either a TimerGroup timer, a SystemTimer alarm,
// or you can mix and match them as well.
let systimer = SystemTimer::new(peripherals.SYSTIMER).split::<Target>();
let timer2: AnyTimer = systimer.alarm0;
esp_hal_embassy::init([timer0, timer1, timer2]);
// ...
}
```
Note that you only have to convert into `AnyTimer` if you want to use multiple timers.
+3
View File
@@ -0,0 +1,3 @@
# Migration Guide from 0.4.x to v0.5.x
You don't have to make changes to your code to update esp-hal-embassy.
+35
View File
@@ -0,0 +1,35 @@
# esp-hal-embassy
[![Crates.io](https://img.shields.io/crates/v/esp-hal-embassy?labelColor=1C2C2E&color=C96329&logo=Rust&style=flat-square)](https://crates.io/crates/esp-hal-embassy)
[![docs.rs](https://img.shields.io/docsrs/esp-hal-embassy?labelColor=1C2C2E&color=C96329&logo=rust&style=flat-square)](https://docs.rs/esp-hal-embassy)
![MSRV](https://img.shields.io/badge/MSRV-1.76-blue?labelColor=1C2C2E&style=flat-square)
![Crates.io](https://img.shields.io/crates/l/esp-hal-embassy?labelColor=1C2C2E&style=flat-square)
[![Matrix](https://img.shields.io/matrix/esp-rs:matrix.org?label=join%20matrix&labelColor=1C2C2E&color=BEC5C9&logo=matrix&style=flat-square)](https://matrix.to/#/#esp-rs:matrix.org)
[Embassy] support for `esp-hal`.
[embassy]: https://github.com/embassy-rs/embassy
## [Documentation]
[documentation]: https://docs.rs/esp-hal-embassy/
## Minimum Supported Rust Version (MSRV)
This crate is guaranteed to compile on stable Rust 1.76 and up. It _might_
compile with older versions but that may change in any new patch release.
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](../LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without
any additional terms or conditions.
+54
View File
@@ -0,0 +1,54 @@
use std::{error::Error, str::FromStr};
use esp_build::assert_unique_used_features;
use esp_config::{generate_config, Value};
use esp_metadata::{Chip, Config};
fn main() -> Result<(), Box<dyn Error>> {
// NOTE: update when adding new device support!
// Ensure that exactly one chip has been specified:
assert_unique_used_features!(
"esp32", "esp32c2", "esp32c3", "esp32c6", "esp32h2", "esp32s2", "esp32s3"
);
// NOTE: update when adding new device support!
// Determine the name of the configured device:
let device_name = if cfg!(feature = "esp32") {
"esp32"
} else if cfg!(feature = "esp32c2") {
"esp32c2"
} else if cfg!(feature = "esp32c3") {
"esp32c3"
} else if cfg!(feature = "esp32c6") {
"esp32c6"
} else if cfg!(feature = "esp32h2") {
"esp32h2"
} else if cfg!(feature = "esp32s2") {
"esp32s2"
} else if cfg!(feature = "esp32s3") {
"esp32s3"
} else {
unreachable!() // We've confirmed exactly one known device was selected
};
// Load the configuration file for the configured device:
let chip = Chip::from_str(device_name)?;
let config = Config::for_chip(&chip);
// Define all necessary configuration symbols for the configured device:
config.define_symbols();
// emit config
generate_config(
"esp_hal_embassy",
&[(
"low-power-wait",
"Enables the lower-power wait if no tasks are ready to run on the thread-mode executor. This allows the MCU to use less power if the workload allows. Recommended for battery-powered systems. May impact analog performance.",
Value::Bool(true),
None
)],
true,
);
Ok(())
}
+137
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
+226
View File
@@ -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
}
}
+169
View File
@@ -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
View File
@@ -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);
}