diff --git a/esp-radio/src/common_adapter.rs b/esp-radio/src/common_adapter.rs index aab6292ce35..eb488100b40 100644 --- a/esp-radio/src/common_adapter.rs +++ b/esp-radio/src/common_adapter.rs @@ -323,6 +323,10 @@ pub(crate) fn enable_wifi_power_domain() { .dig_pwc() .modify(|_, w| w.wifi_force_pd().clear_bit()); + // Give the domain time to power up before touching it, mirroring + // ESP-IDF's `esp_wifi_bt_power_domain_on`. + esp_rom_sys::rom::ets_delay_us(10); + #[cfg(not(esp32))] cfg_select! { soc_has_apb_ctrl => { @@ -405,26 +409,32 @@ pub(crate) fn enable_wifi_power_domain() { }); } // ESP32-C2 has no separate modem power domain (RTC_CNTL lacks - // wifi_force_pd/iso), but a system reset still leaves the shared modem - // subsystems in their previous state. - esp32c2 => { - regs!(APB_CTRL).wifi_rst_en().modify(|_, w| { - w.wifibb_rst().set_bit(); - w.fe_rst().set_bit(); - w.mac_rst().set_bit(); - w.ble_rpa_rst().set_bit() - }); - regs!(APB_CTRL).wifi_rst_en().modify(|_, w| { - w.wifibb_rst().clear_bit(); - w.fe_rst().clear_bit(); - w.mac_rst().clear_bit(); - w.ble_rpa_rst().clear_bit() - }); - } + // wifi_force_pd/iso) — nothing to power up here. ESP-IDF's + // `esp_wifi_bt_power_domain_on` is a no-op on this chip for the same + // reason; in particular it does not reset the shared modem, whose + // state the Wi-Fi driver retains across a deinit/init cycle. _ => {} } } +/// Power down the Wi-Fi power domain, mirroring `enable_wifi_power_domain` and +/// ESP-IDF's `esp_wifi_bt_power_domain_off`. +pub(crate) fn disable_wifi_power_domain() { + #[cfg(not(any(soc_has_pmu, esp32c2)))] + { + let rtc_cntl = regs!(RTC_CNTL); + + // Isolate before powering down. + rtc_cntl + .dig_iso() + .modify(|_, w| w.wifi_force_iso().set_bit()); + + rtc_cntl + .dig_pwc() + .modify(|_, w| w.wifi_force_pd().set_bit()); + } +} + /// ************************************************************************** /// Name: esp_queue_create /// diff --git a/esp-radio/src/ieee802154/mod.rs b/esp-radio/src/ieee802154/mod.rs index fa230db05bc..05427bc9e3d 100644 --- a/esp-radio/src/ieee802154/mod.rs +++ b/esp-radio/src/ieee802154/mod.rs @@ -114,6 +114,10 @@ pub struct Ieee802154<'a> { transmit_buffer: [u8; FRAME_SIZE], _phy_clock_guard: PhyClockGuard<'a>, _phy_init_guard: PhyInitGuard<'a>, + // Fields drop in declaration order: this guard must stay last so the PHY + // is torn down (which still needs the modem clocks) before the clocks are + // gated off. + _radio_clock_guard: RadioClockGuard, } impl<'a> Ieee802154<'a> { @@ -123,12 +127,13 @@ impl<'a> Ieee802154<'a> { /// things will break. #[instability::unstable] pub fn new(radio: IEEE802154<'a>) -> Self { - let (_phy_clock_guard, _phy_init_guard) = esp_ieee802154_enable(radio); + let (_phy_clock_guard, _phy_init_guard, _radio_clock_guard) = esp_ieee802154_enable(radio); Self { _align: 0, transmit_buffer: [0u8; FRAME_SIZE], _phy_clock_guard, _phy_init_guard, + _radio_clock_guard, } } diff --git a/esp-radio/src/ieee802154/raw.rs b/esp-radio/src/ieee802154/raw.rs index 12044062d86..58972f84aea 100644 --- a/esp-radio/src/ieee802154/raw.rs +++ b/esp-radio/src/ieee802154/raw.rs @@ -16,7 +16,7 @@ use super::{ pib::*, }; use crate::{ - radio_clocks::{clocks_ll::enable_ieee802154, init_radio_clocks}, + radio_clocks::{clocks_ll::enable_ieee802154, deinit_radio_clocks, init_radio_clocks}, sys::include::{ ieee802154_coex_event_t, ieee802154_coex_event_t_IEEE802154_IDLE, @@ -104,9 +104,25 @@ pub struct RawReceived { pub channel: u8, } +/// Gates off the 802.15.4 modem clocks and undoes the radio clock +/// initialization (`deinit_radio_clocks`) when dropped. +/// +/// Must be dropped only after the PHY guards: PHY teardown still requires the +/// modem clocks. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) struct RadioClockGuard; + +impl Drop for RadioClockGuard { + fn drop(&mut self) { + enable_ieee802154(false); + deinit_radio_clocks(); + } +} + pub(crate) fn esp_ieee802154_enable( radio: IEEE802154<'_>, -) -> (PhyClockGuard<'_>, PhyInitGuard<'_>) { +) -> (PhyClockGuard<'_>, PhyInitGuard<'_>, RadioClockGuard) { init_radio_clocks(); let phy_clock_guard = esp_phy::enable_phy_clock(); enable_ieee802154(true); @@ -117,7 +133,7 @@ pub(crate) fn esp_ieee802154_enable( ieee802154_mac_init(radio); info!("date={:x}", mac_date()); - (phy_clock_guard, phy_init_guard) + (phy_clock_guard, phy_init_guard, RadioClockGuard) } fn esp_btbb_enable() { diff --git a/esp-radio/src/lib.rs b/esp-radio/src/lib.rs index ea8d69db075..6959f0482bc 100644 --- a/esp-radio/src/lib.rs +++ b/esp-radio/src/lib.rs @@ -323,10 +323,15 @@ pub(crate) fn init() { ); } + // Ungate the modem clocks first: `enable_wifi_power_domain` pulses the + // modem reset, which is ineffective while the clocks are gated — and + // esp-phy's clock guard has gated them again by the time we re-init. + // (ESP-IDF never gates these clocks, so its power-up reset always lands.) + radio_clocks::init_radio_clocks(); + crate::common_adapter::enable_wifi_power_domain(); wifi_set_log_verbose(); - radio_clocks::init_radio_clocks(); #[cfg(feature = "coex")] match crate::wifi::coex_initialize() { @@ -350,6 +355,27 @@ pub(crate) fn deinit() { #[cfg(feature = "ble")] ble::shutdown_ble_isr(); + // Gate the BT clocks (the Wi-Fi driver gates its own clocks during + // `wifi_deinit`), power down the modem power domain, and gate the + // remaining modem clocks, mirroring ESP-IDF's fixed-mask clock control + // (`periph_ll_wifi_module_disable_clk_set_rst` and friends). This must + // only run once all radios are off: PHY teardown still needs the modem + // clocks. + #[cfg(feature = "ble")] + crate::radio_clocks::clocks_ll::enable_bt(false); + crate::common_adapter::disable_wifi_power_domain(); + crate::radio_clocks::deinit_radio_clocks(); + + // After the modem power domain has been powered down, the PHY driver's + // internal init flag must be reset, otherwise the next `phy_wakeup_init` + // assumes retained PHY registers that the power-down wiped (mirrors + // ESP-IDF's `esp_phy_modem_deinit`, "Fix the issue caused by the power + // domain off. This issue is only on ESP32C3."). + #[cfg(esp32c3)] + unsafe { + crate::sys::include::phy_init_flag() + }; + esp_hal::if_unstable_hal! { // Allow using `ADC2` again #[cfg(esp32)] diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32.rs index 56960befe45..ef8334fea4e 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32.rs @@ -62,6 +62,13 @@ pub(crate) fn init_clocks() { .write(|w| unsafe { w.bits(u32::MAX) }); } +pub(crate) fn deinit_clocks() { + // Nothing to do: the Wi-Fi clock bits (`DPORT_WIFI_CLK_WIFI_EN_M`) are + // cleared by `enable_wifi(false)` when the Wi-Fi driver deinitializes, + // and `disable_wifi_power_domain` powers the modem power domain off. + // ESP-IDF has no global clock-register restore on deinit either. +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32c2.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32c2.rs index 8889435893d..aa0f76f6ae9 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32c2.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32c2.rs @@ -31,6 +31,15 @@ pub(crate) fn init_clocks() { .modify(|r, w| unsafe { w.bits(r.bits() & !WIFI_BT_SDIO_CLK | SYSTEM_WIFI_CLK_EN) }); } +pub(crate) fn deinit_clocks() { + // Nothing to do: when the last `PhyClockGuard` drops, esp-phy gates the + // shared modem clocks (`SYSTEM_WIFI_CLK_WIFI_BT_COMMON_M`) — the same + // state ESP-IDF leaves behind via `wifi_bt_common_module_disable`, and + // its `periph_ll_wifi_module_disable_clk_set_rst` is a no-op on ESP32-C2. + // Gating anything beyond that here breaks Wi-Fi re-initialization on + // ESP32-C2. +} + pub(crate) fn ble_rtc_clk_init() { regs!(MODEM_CLKRST).modem_lp_timer_conf().modify(|_, w| { w.lp_timer_sel_xtal32k().clear_bit(); diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32c3.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32c3.rs index 8a7cb720d23..3b9546fb165 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32c3.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32c3.rs @@ -42,6 +42,24 @@ pub(crate) fn init_clocks() { .modify(|r, w| unsafe { w.bits(r.bits() & !WIFI_BT_SDIO_CLK | SYSTEM_WIFI_CLK_EN) }); } +pub(crate) fn deinit_clocks() { + // No `wifi_clk_en` restore: ESP-IDF's Wi-Fi disable mask + // (`SYSTEM_WIFI_CLK_WIFI_EN_M`) is 0 on this chip — its + // `periph_ll_wifi_module_disable_clk_set_rst` clears nothing. + + // Power the BT domain back down, re-asserting the state cleared by + // `init_clocks` and mirroring ESP-IDF's `esp_bt_power_domain_off` (the + // Wi-Fi domain is powered down by `disable_wifi_power_domain`, which + // `deinit` calls first). Isolate before powering down. + regs!(RTC_CNTL) + .dig_iso() + .modify(|_, w| w.bt_force_iso().set_bit()); + + regs!(RTC_CNTL) + .dig_pwc() + .modify(|_, w| w.bt_force_pd().set_bit()); +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32c5.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32c5.rs index 14ec70bf685..33608d46e9f 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32c5.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32c5.rs @@ -65,6 +65,10 @@ pub(crate) fn init_clocks() { // done in esp-hal } +pub(crate) fn deinit_clocks() { + // nothing to do, `init_clocks` is a no-op +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target (yet) } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32c6.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32c6.rs index 6deb1a146fe..fe9cb47d55a 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32c6.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32c6.rs @@ -49,7 +49,7 @@ pub(crate) fn enable_ieee802154(en: bool) { regs!(MODEM_LPCON) .clk_conf() - .modify(|_, w| w.clk_coex_en().set_bit()); + .modify(|_, w| w.clk_coex_en().bit(en)); } pub(crate) fn enable_bt(en: bool) { @@ -85,6 +85,10 @@ pub(crate) fn init_clocks() { // done in esp-hal } +pub(crate) fn deinit_clocks() { + // nothing to do, `init_clocks` is a no-op +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target (yet) } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32c61.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32c61.rs index 37de63c39a1..bd76b088cb3 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32c61.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32c61.rs @@ -40,6 +40,10 @@ pub(crate) fn init_clocks() { // done in esp-hal } +pub(crate) fn deinit_clocks() { + // nothing to do, `init_clocks` is a no-op +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target (yet) } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32h2.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32h2.rs index d6e9345c0ee..0960c4d9a1d 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32h2.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32h2.rs @@ -38,20 +38,17 @@ pub(crate) fn enable_ieee802154(en: bool) { } pub(crate) fn init_clocks() { - regs!(PMU) - .hp_sleep_icg_modem() + let pmu = regs!(PMU); + + pmu.hp_sleep_icg_modem() .modify(|_, w| unsafe { w.hp_sleep_dig_icg_modem_code().bits(0) }); - regs!(PMU) - .hp_modem_icg_modem() + pmu.hp_modem_icg_modem() .modify(|_, w| unsafe { w.hp_modem_dig_icg_modem_code().bits(1) }); - regs!(PMU) - .hp_active_icg_modem() + pmu.hp_active_icg_modem() .modify(|_, w| unsafe { w.hp_active_dig_icg_modem_code().bits(2) }); - regs!(PMU) - .imm_modem_icg() + pmu.imm_modem_icg() .write(|w| w.update_dig_icg_modem_en().set_bit()); - regs!(PMU) - .imm_sleep_sysclk() + pmu.imm_sleep_sysclk() .write(|w| w.update_dig_icg_switch().set_bit()); regs!(MODEM_LPCON).clk_conf().modify(|_, w| { @@ -61,6 +58,28 @@ pub(crate) fn init_clocks() { }); } +pub(crate) fn deinit_clocks() { + let pmu = regs!(PMU); + + // Restore ESP-IDF's `pmu_init` defaults for the modem clock-gating codes + // (`PMU_HP_*_CLOCK_CONFIG_DEFAULT` in `pmu_param.c`): IDF configures them + // once at startup and does not touch them on radio deinit. + pmu.hp_sleep_icg_modem() + .modify(|_, w| unsafe { w.hp_sleep_dig_icg_modem_code().bits(2) }); + pmu.hp_modem_icg_modem() + .modify(|_, w| unsafe { w.hp_modem_dig_icg_modem_code().bits(0) }); + pmu.hp_active_icg_modem() + .modify(|_, w| unsafe { w.hp_active_dig_icg_modem_code().bits(0) }); + pmu.imm_modem_icg() + .write(|w| w.update_dig_icg_modem_en().set_bit()); + + regs!(MODEM_LPCON).clk_conf().modify(|_, w| { + w.clk_i2c_mst_en().clear_bit(); + w.clk_coex_en().clear_bit(); + w.clk_fe_mem_en().clear_bit() + }); +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target (yet) } diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32s2.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32s2.rs index abacfcd56c8..52749c172cf 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32s2.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32s2.rs @@ -42,3 +42,12 @@ pub(crate) fn init_clocks() { .wifi_clk_en() .modify(|r, w| unsafe { w.bits(r.bits() & !WIFI_BT_SDIO_CLK | DPORT_WIFI_CLK_WIFI_EN) }); } + +pub(crate) fn deinit_clocks() { + // Nothing to do: `enable_wifi(false)` (called by the Wi-Fi driver on + // deinit) clears `DPORT_WIFI_CLK_WIFI_EN_M`. That mask (0x7cf) is smaller + // than the value `init_clocks` writes (0x3807cf), so the upper bits stay + // set — ESP-IDF's `periph_ll_wifi_module_disable_clk_set_rst` leaves them + // set as well, and they only feed the modem, which + // `disable_wifi_power_domain` powers off. +} diff --git a/esp-radio/src/radio_clocks/clocks_ll/esp32s3.rs b/esp-radio/src/radio_clocks/clocks_ll/esp32s3.rs index e418e4ded32..8f0bb20df18 100644 --- a/esp-radio/src/radio_clocks/clocks_ll/esp32s3.rs +++ b/esp-radio/src/radio_clocks/clocks_ll/esp32s3.rs @@ -33,6 +33,13 @@ pub(crate) fn init_clocks() { .modify(|r, w| unsafe { w.bits(r.bits() & !WIFI_BT_SDIO_CLK | SYSTEM_WIFI_CLK_EN) }); } +pub(crate) fn deinit_clocks() { + // Nothing to do: ESP-IDF's Wi-Fi disable mask + // (`SYSTEM_WIFI_CLK_WIFI_EN_M`) is 0 on this chip — its + // `periph_ll_wifi_module_disable_clk_set_rst` clears nothing — and + // `disable_wifi_power_domain` powers the modem power domain off. +} + pub(crate) fn ble_rtc_clk_init() { // nothing for this target } diff --git a/esp-radio/src/radio_clocks/mod.rs b/esp-radio/src/radio_clocks/mod.rs index 46e5eaad3a9..370968f47b3 100644 --- a/esp-radio/src/radio_clocks/mod.rs +++ b/esp-radio/src/radio_clocks/mod.rs @@ -15,3 +15,10 @@ pub(crate) mod clocks_ll; pub(crate) fn init_radio_clocks() { clocks_ll::init_clocks(); } + +/// Undo the clock initialization done by [`init_radio_clocks`], gating the +/// modem clocks again (mirroring ESP-IDF's per-module clock disable). +#[inline] +pub(crate) fn deinit_radio_clocks() { + clocks_ll::deinit_clocks(); +} diff --git a/qa-test/Cargo.toml b/qa-test/Cargo.toml index 3550c29cb92..3e8325733d2 100644 --- a/qa-test/Cargo.toml +++ b/qa-test/Cargo.toml @@ -34,6 +34,7 @@ esp-radio = { path = "../esp-radio", features = [ "unstable", ], optional = true } esp-storage = { path = "../esp-storage", optional = true } +ieee802154 = "0.6.1" lis3dh-async = "0.9.3" ssd1306 = "0.10.0" static_cell = "2.1.1" diff --git a/qa-test/src/bin/ble_reinit_scan.rs b/qa-test/src/bin/ble_reinit_scan.rs new file mode 100644 index 00000000000..523983c5c4e --- /dev/null +++ b/qa-test/src/bin/ble_reinit_scan.rs @@ -0,0 +1,144 @@ +//! Repeatedly initializes and deinitializes the BLE controller and scans for +//! advertisements. Every iteration must receive advertisements — the +//! iterations after the first exercise re-initialization after the modem +//! clocks were torn down. +//! +//! You need at least one BLE advertiser in range (e.g. a smartphone or a +//! second board running a BLE advertising example). +//! +//! The test is also suited to verify the power savings of the teardown with +//! a (slow) power meter: +//! - after boot there is a startup delay with the radio never initialized (baseline consumption), +//! - each iteration then has a 10s active phase (radio initialized, scanning) followed by a 10s +//! inactive phase (radio deinitialized). The inactive-phase consumption should match the +//! baseline. +//! +//! Success: every iteration receives at least one advertisement. +//! Failure: an iteration receives no advertisements or fails to start the +//! scan. + +//% FEATURES: esp-radio esp-radio/ble esp-radio/unstable esp-hal/unstable +//% CHIP_FILTER: bt_driver_supported + +#![no_std] +#![no_main] + +use core::sync::atomic::{AtomicU32, Ordering}; + +use embassy_executor::Spawner; +use embassy_futures::select::select; +use embassy_time::{Duration, Timer}; +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{ + clock::CpuClock, + interrupt::software::SoftwareInterruptControl, + timer::timg::TimerGroup, +}; +use esp_println::println; +use esp_radio::ble::controller::BleConnector; +use trouble_host::prelude::*; + +esp_bootloader_esp_idf::esp_app_desc!(); + +/// Max number of connections +const CONNECTIONS_MAX: usize = 1; +const L2CAP_CHANNELS_MAX: usize = 1; + +static ADV_COUNT: AtomicU32 = AtomicU32::new(0); + +/// Settle time after boot, with the radio never initialized (baseline power). +const STARTUP_DELAY: Duration = Duration::from_secs(5); +/// How long the radio stays initialized per iteration. +const ACTIVE_PHASE: Duration = Duration::from_secs(10); +/// How long the radio stays deinitialized per iteration. +const INACTIVE_PHASE: Duration = Duration::from_secs(10); + +#[esp_hal::main] +async fn main(_spawner: Spawner) { + esp_println::logger::init_logger_from_env(); + let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max())); + + esp_alloc::heap_allocator!(size: 72 * 1024); + + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + let timg0 = TimerGroup::new(peripherals.TIMG0); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + println!( + "Waiting {}s (baseline power, radio never initialized)", + STARTUP_DELAY.as_secs() + ); + Timer::after(STARTUP_DELAY).await; + + let mut bt = peripherals.BT; + let mut iteration = 0u32; + loop { + iteration += 1; + ADV_COUNT.store(0, Ordering::Relaxed); + + { + let connector = BleConnector::new(bt.reborrow(), esp_radio::ble::Config::default()) + .expect("failed to create BLE connector"); + let controller: ExternalController<_, 1> = ExternalController::new(connector); + + // Using a fixed "random" address can be useful for testing. In real scenarios, one + // would use e.g. the MAC 6 byte array as the address (how to get that varies by the + // platform). + let address: Address = Address::random([0xff, 0x8f, 0x1b, 0x05, 0xe4, 0xff]); + + let mut resources: HostResources< + _, + DefaultPacketPool, + CONNECTIONS_MAX, + L2CAP_CHANNELS_MAX, + > = HostResources::new(); + let stack = trouble_host::new(controller, &mut resources) + .set_random_address(address) + .build(); + let central = stack.central(); + let mut runner = stack.runner(); + + let printer = Printer; + let mut scanner = Scanner::new(central); + println!( + "Iteration {iteration}: active for {}s", + ACTIVE_PHASE.as_secs() + ); + select(runner.run_with_handler(&printer), async { + let config = ScanConfig { + active: false, + phys: PhySet::M1, + interval: Duration::from_millis(500), + window: Duration::from_millis(100), + timeout: Duration::from_secs(0), + ..Default::default() + }; + match scanner.scan(&config).await { + Ok(_session) => Timer::after(ACTIVE_PHASE).await, + Err(e) => println!("Iteration {iteration}: failed to start scan: {e:?}"), + } + }) + .await; + } + + println!( + "Iteration {iteration}: received {} advertisements, inactive for {}s (measure power now)", + ADV_COUNT.load(Ordering::Relaxed), + INACTIVE_PHASE.as_secs() + ); + + Timer::after(INACTIVE_PHASE).await; + } +} + +struct Printer; + +impl EventHandler for Printer { + fn on_adv_reports(&self, mut reports: LeAdvReportsIter<'_>) { + while let Some(Ok(report)) = reports.next() { + println!(" adv {:?} len {}", report.addr, report.data.len()); + ADV_COUNT.store(ADV_COUNT.load(Ordering::Relaxed) + 1, Ordering::Relaxed); + } + } +} diff --git a/qa-test/src/bin/ieee802154_reinit_broadcast.rs b/qa-test/src/bin/ieee802154_reinit_broadcast.rs new file mode 100644 index 00000000000..add91eeb663 --- /dev/null +++ b/qa-test/src/bin/ieee802154_reinit_broadcast.rs @@ -0,0 +1,119 @@ +//! Repeatedly initializes and deinitializes the IEEE 802.15.4 driver and +//! broadcasts data frames. The iterations after the first exercise +//! re-initialization after the modem clocks were torn down. +//! +//! Verify with a sniffer on channel 15 (e.g. a second board running the +//! `ieee802154_sniffer` example) that frames are received during every +//! active phase, with continuously increasing sequence numbers. +//! +//! The test is also suited to verify the power savings of the teardown with +//! a (slow) power meter: +//! - after boot there is a startup delay with the radio never initialized (baseline consumption), +//! - each iteration then has a 10s active phase (radio initialized, sending two frames per second) +//! followed by a 10s inactive phase (radio deinitialized). The inactive-phase consumption should +//! match the baseline. +//! +//! Success: the sniffer receives frames during every active phase. +//! Failure: the sniffer stops seeing frames after the first iteration. + +//% FEATURES: esp-radio esp-radio/ieee802154 esp-radio/unstable esp-hal/unstable +//% CHIP_FILTER: ieee802154_driver_supported + +#![no_std] +#![no_main] + +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{delay::Delay, main}; +use esp_println::println; +use esp_radio::ieee802154::{Config, Frame, Ieee802154}; +use ieee802154::mac::{ + Address, + FrameContent, + FrameType, + FrameVersion, + Header, + PanId, + ShortAddress, +}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +/// Settle time after boot, with the radio never initialized (baseline power). +const STARTUP_DELAY_MS: u32 = 5000; +/// How long the radio stays initialized per iteration. +const ACTIVE_PHASE_MS: u32 = 10_000; +/// How long the radio stays deinitialized per iteration. +const INACTIVE_PHASE_MS: u32 = 10_000; +/// Frame interval during the active phase. +const FRAME_INTERVAL_MS: u32 = 500; + +#[main] +fn main() -> ! { + esp_println::logger::init_logger_from_env(); + let peripherals = esp_hal::init(esp_hal::Config::default()); + + esp_alloc::heap_allocator!(size: 24 * 1024); + + let delay = Delay::new(); + + println!( + "Waiting {}s (baseline power, radio never initialized)", + STARTUP_DELAY_MS / 1000 + ); + delay.delay_millis(STARTUP_DELAY_MS); + + let mut radio = peripherals.IEEE802154; + let mut seq_number = 0u8; + loop { + { + let mut ieee802154 = Ieee802154::new(radio.reborrow()); + + ieee802154.set_config(Config { + channel: 15, + promiscuous: false, + pan_id: Some(0x4242), + short_addr: Some(0x2323), + ..Default::default() + }); + + println!("Active for {}s", ACTIVE_PHASE_MS / 1000); + for _ in 0..ACTIVE_PHASE_MS / FRAME_INTERVAL_MS { + seq_number = seq_number.wrapping_add(1); + + match ieee802154.transmit( + &Frame { + header: Header { + frame_type: FrameType::Data, + frame_pending: false, + ack_request: false, + pan_id_compress: false, + seq_no_suppress: false, + ie_present: false, + version: FrameVersion::Ieee802154_2003, + seq: seq_number, + destination: Some(Address::Short(PanId(0xffff), ShortAddress(0xffff))), + source: None, + auxiliary_security_header: None, + }, + content: FrameContent::Data, + payload: b"re-init broadcast".to_vec(), + footer: [0u8; 2], + }, + true, + ) { + Ok(()) => println!("Broadcast frame with seq {seq_number} sent"), + Err(e) => println!("Failed to send frame with seq {seq_number}: {e:?}"), + } + + delay.delay_millis(FRAME_INTERVAL_MS); + } + } + + println!( + "Radio deinitialized, inactive for {}s (measure power now)", + INACTIVE_PHASE_MS / 1000 + ); + delay.delay_millis(INACTIVE_PHASE_MS); + } +} diff --git a/qa-test/src/bin/wifi_reinit_scan.rs b/qa-test/src/bin/wifi_reinit_scan.rs new file mode 100644 index 00000000000..b8421af0d2c --- /dev/null +++ b/qa-test/src/bin/wifi_reinit_scan.rs @@ -0,0 +1,101 @@ +//! Repeatedly initializes and deinitializes the Wi-Fi driver and scans for +//! access points. Every iteration must find access points — the iterations +//! after the first exercise re-initialization after the modem power domain +//! and clocks were torn down. +//! +//! The test is also suited to verify the power savings of the teardown with +//! a (slow) power meter: +//! - after boot there is a startup delay with the radio never initialized (baseline consumption), +//! - each iteration then has a 10s active phase (radio initialized, scanning every second) followed +//! by a 10s inactive phase (radio deinitialized). The inactive-phase consumption should match the +//! baseline. +//! +//! Success: every scan finds at least one access point. +//! Failure: a scan finds no access points or errors. + +//% FEATURES: esp-radio esp-radio/wifi esp-radio/unstable esp-hal/unstable +//% CHIP_FILTER: wifi_driver_supported + +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_time::{Duration, Instant, Timer}; +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{ + clock::CpuClock, + interrupt::software::SoftwareInterruptControl, + ram, + timer::timg::TimerGroup, +}; +use esp_println::println; +use esp_radio::wifi::{ControllerConfig, WifiController, scan::ScanConfig}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +/// Settle time after boot, with the radio never initialized (baseline power). +const STARTUP_DELAY: Duration = Duration::from_secs(5); +/// How long the radio stays initialized per iteration. +const ACTIVE_PHASE: Duration = Duration::from_secs(10); +/// How long the radio stays deinitialized per iteration. +const INACTIVE_PHASE: Duration = Duration::from_secs(10); + +#[esp_hal::main] +async fn main(_spawner: Spawner) { + esp_println::logger::init_logger_from_env(); + let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); + let peripherals = esp_hal::init(config); + + esp_alloc::heap_allocator!(size: 32 * 1024); + // add some more RAM + esp_alloc::heap_allocator!(#[ram(reclaimed)] size: 64 * 1024); + + let timg0 = TimerGroup::new(peripherals.TIMG0); + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + println!( + "Waiting {}s (baseline power, radio never initialized)", + STARTUP_DELAY.as_secs() + ); + Timer::after(STARTUP_DELAY).await; + + let mut wifi = peripherals.WIFI; + let mut iteration = 0u32; + loop { + iteration += 1; + + let mut controller = WifiController::new(wifi.reborrow(), ControllerConfig::default()) + .expect("failed to create Wi-Fi controller"); + + println!( + "Iteration {iteration}: active for {}s", + ACTIVE_PHASE.as_secs() + ); + let active_until = Instant::now() + ACTIVE_PHASE; + while Instant::now() < active_until { + match controller + .scan_async(&ScanConfig::default().with_max(10)) + .await + { + Ok(aps) => { + println!("Iteration {iteration}: found {} access points", aps.len()); + for ap in aps { + println!(" {ap:?}"); + } + } + Err(e) => println!("Iteration {iteration}: scan failed: {e:?}"), + } + Timer::after(Duration::from_secs(1)).await; + } + + drop(controller); + println!( + "Iteration {iteration}: Wi-Fi deinitialized, inactive for {}s (measure power now)", + INACTIVE_PHASE.as_secs() + ); + + Timer::after(INACTIVE_PHASE).await; + } +}