Repository: esp-rs/esp-hal
Pull request #3403: Fix esp32 coex
Author: login: bjoernQ
Created: 2025-04-22T07:27:23Z | Merged: 2025-04-24T11:54:13Z

## Thank you for your contribution!

We appreciate the time and effort you've put into this pull request.
To help us review it efficiently, please ensure you've gone through the following checklist:

### Submission Checklist 📝
- [ ] I have updated existing examples or added new ones (if applicable).
- [x] I have used `cargo xtask fmt-packages` command to ensure that all changed code is formatted correctly.
- [x] My changes were added to the [`CHANGELOG.md`](https://github.com/esp-rs/esp-hal/blob/main/esp-hal/CHANGELOG.md) in the **_proper_** section.
- [ ] I have added necessary changes to user code to the [Migration Guide](https://github.com/esp-rs/esp-hal/blob/main/esp-hal/MIGRATING-0.21.md).
- [x] My changes are in accordance to the [esp-rs developer guidelines](https://github.com/esp-rs/esp-hal/blob/main/documentation/DEVELOPER-GUIDELINES.md)

#### Extra:
- [x] I have read the [CONTRIBUTING.md guide](https://github.com/esp-rs/esp-hal/blob/main/documentation/CONTRIBUTING.md) and followed its instructions.

### Pull Request Details 📖

#### Description

This makes COEX work on ESP32. While `coex_bt_wakeup_request` and `coex_bt_wakeup_request_end`  doesn't look right it won't work with a "correct" implementation, and I don't think it was better before (and it also wasn't working)

#### Testing
Run the `wifi_coex` example



Diff:
diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md
index e263af59a1b..87ac578f719 100644
--- a/esp-hal/CHANGELOG.md
+++ b/esp-hal/CHANGELOG.md
@@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - The `ESP_HAL_CONFIG_PLACE_SPI_DRIVER_IN_RAM` configuration option has been renamed to `ESP_HAL_CONFIG_PLACE_SPI_MASTER_DRIVER_IN_RAM`. (#3402)
 - Made the `ParlIo` traits for `TxPins`, `RxPins`, `ConfigurePins` public (#3398)
 - Renamed `Flex::enable_input` to `set_input_enable` (#3387)
+- Make `esp_hal::interrupt::current_runlevel` public under the unstable feature (#3403)
 
 ### Fixed
 
diff --git a/esp-hal/src/interrupt/riscv.rs b/esp-hal/src/interrupt/riscv.rs
index 3cc73e72058..28d65884641 100644
--- a/esp-hal/src/interrupt/riscv.rs
+++ b/esp-hal/src/interrupt/riscv.rs
@@ -704,7 +704,7 @@ mod classic {
     }
 
     /// Get the current run level (the level below which interrupts are masked).
-    pub(crate) fn current_runlevel() -> Priority {
+    pub fn current_runlevel() -> Priority {
         let intr = INTERRUPT_CORE0::regs();
         let prev_interrupt_priority = intr.cpu_int_thresh().read().bits().saturating_sub(1) as u8;
 
@@ -872,7 +872,7 @@ mod plic {
     }
 
     /// Get the current run level (the level below which interrupts are masked).
-    pub(crate) fn current_runlevel() -> Priority {
+    pub fn current_runlevel() -> Priority {
         let prev_interrupt_priority = PLIC_MX::regs()
             .mxint_thresh()
             .read()
diff --git a/esp-hal/src/interrupt/xtensa.rs b/esp-hal/src/interrupt/xtensa.rs
index 9bfb31144b3..2b60fe95a6c 100644
--- a/esp-hal/src/interrupt/xtensa.rs
+++ b/esp-hal/src/interrupt/xtensa.rs
@@ -333,7 +333,7 @@ unsafe fn core1_interrupt_peripheral() -> *const crate::pac::interrupt_core1::Re
 }
 
 /// Get the current run level (the level below which interrupts are masked).
-pub(crate) fn current_runlevel() -> Priority {
+pub fn current_runlevel() -> Priority {
     let ps: u32;
     unsafe { core::arch::asm!("rsr.ps {0}", out(reg) ps) };
 
diff --git a/esp-wifi/CHANGELOG.md b/esp-wifi/CHANGELOG.md
index 3f4c839d3e8..3fdd446415c 100644
--- a/esp-wifi/CHANGELOG.md
+++ b/esp-wifi/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Allow `Configuration::None`, set country early, changed default power-save-mode to None (#3364)
 
 - Enterprise WPA fixed for ESP32-S2 (#3406)
+- COEX on ESP32 is now working (#3403)
 
 ### Removed
 
diff --git a/esp-wifi/src/ble/btdm.rs b/esp-wifi/src/ble/btdm.rs
index ac347dddb07..4936312e244 100644
--- a/esp-wifi/src/ble/btdm.rs
+++ b/esp-wifi/src/ble/btdm.rs
@@ -258,7 +258,7 @@ unsafe extern "C" fn task_delete(task: *const ()) {
 
 #[ram]
 unsafe extern "C" fn is_in_isr() -> i32 {
-    0
+    crate::is_interrupts_disabled() as i32
 }
 
 #[ram]
@@ -302,7 +302,6 @@ unsafe extern "C" fn btdm_hus_2_lpcycles(us: u32) -> u32 {
     // Converts a duration in half us into a number of low power clock cycles.
     let cycles: u64 = (us as u64) << (g_btdm_lpcycle_us_frac as u64 / g_btdm_lpcycle_us as u64);
     trace!("btdm_hus_2_lpcycles {} {}", us, cycles);
-    // probably not right ... NX returns half of the values we calculate here
 
     cycles as u32
 }
@@ -485,7 +484,19 @@ pub fn send_hci(data: &[u8]) {
                 }
 
                 PACKET_SENT.store(false, Ordering::Relaxed);
+
+                #[cfg(all(esp32, coex))]
+                ble_os_adapter_chip_specific::async_wakeup_request(
+                    ble_os_adapter_chip_specific::BTDM_ASYNC_WAKEUP_REQ_HCI,
+                );
+
                 API_vhci_host_send_packet(packet.as_ptr(), packet.len() as u16);
+
+                #[cfg(all(esp32, coex))]
+                ble_os_adapter_chip_specific::async_wakeup_request_end(
+                    ble_os_adapter_chip_specific::BTDM_ASYNC_WAKEUP_REQ_HCI,
+                );
+
                 trace!("sent vhci host packet");
 
                 super::dump_packet_info(packet);
diff --git a/esp-wifi/src/ble/os_adapter_esp32.rs b/esp-wifi/src/ble/os_adapter_esp32.rs
index cef02e4c497..1261c7c84a9 100644
--- a/esp-wifi/src/ble/os_adapter_esp32.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32.rs
@@ -202,16 +202,23 @@ extern "C" fn patch_apply() {
 
 extern "C" fn coex_version_get_wrapper(major: *mut u32, minor: *mut u32, patch: *mut u32) -> i32 {
     unsafe {
-        let coex_version = unwrap!(
-            core::ffi::CStr::from_ptr(crate::binary::include::coex_version_get())
-                .to_str()
-                .ok()
-        );
-        info!("COEX Version {}", coex_version);
-        // we should parse it ... for now just hardcoded
-        major.write_volatile(1);
-        minor.write_volatile(2);
-        patch.write_volatile(0);
+        let mut version = crate::binary::include::coex_version_t {
+            major: 0,
+            minor: 0,
+            patch: 0,
+        };
+        if coex_version_get_value(&mut version) == 0 {
+            info!(
+                "COEX Version {}.{}.{}",
+                version.major, version.minor, version.patch
+            );
+
+            major.write_volatile(version.major as u32);
+            minor.write_volatile(version.minor as u32);
+            patch.write_volatile(version.patch as u32);
+        } else {
+            error!("Unable to get COEX version");
+        }
     }
 
     0
@@ -283,6 +290,8 @@ static BTDM_DRAM_AVAILABLE_REGION: [btdm_dram_available_region_t; 7] = [
 ];
 
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
+    // keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+    // ideally _some_ of these values should be configurable
     esp_bt_controller_config_t {
         controller_task_stack_size: 4096,
         controller_task_prio: 110,
@@ -380,18 +389,31 @@ pub(crate) fn disable_sleep_mode() {
 
 pub(crate) unsafe extern "C" fn coex_bt_wakeup_request() -> bool {
     trace!("coex_bt_wakeup_request");
-    #[cfg(coex)]
-    return async_wakeup_request(BTDM_ASYNC_WAKEUP_REQ_COEX);
 
-    #[cfg(not(coex))]
+    // This should really be
+    // ```rust,norun
+    // #[cfg(coex)]
+    // return async_wakeup_request(BTDM_ASYNC_WAKEUP_REQ_COEX);
+    // #[cfg(not(coex))]
+    // true
+    // ```
+    //
+    // But doing the right thing here keeps BT from working.
+    // In a similar scenario this function isn't called in ESP-IDF.
     true
 }
 
 pub(crate) unsafe extern "C" fn coex_bt_wakeup_request_end() {
-    warn!("coex_bt_wakeup_request_end");
-
-    #[cfg(coex)]
-    async_wakeup_request_end(BTDM_ASYNC_WAKEUP_REQ_COEX);
+    trace!("coex_bt_wakeup_request_end");
+
+    // This should really be
+    // ```rust,norun
+    // #[cfg(coex)]
+    // async_wakeup_request_end(BTDM_ASYNC_WAKEUP_REQ_COEX);
+    // ```
+    //
+    // But doing the right thing here keeps BT from working.
+    // In a similar scenario this function isn't called in ESP-IDF.
 }
 
 #[allow(unused_variables)]
@@ -427,7 +449,7 @@ pub(crate) unsafe extern "C" fn coex_bt_release(event: u32) -> i32 {
 pub(crate) unsafe extern "C" fn coex_register_bt_cb_wrapper(
     callback: unsafe extern "C" fn(),
 ) -> i32 {
-    warn!("coex_register_bt_cb {:?}", callback);
+    trace!("coex_register_bt_cb {:?}", callback);
     unsafe extern "C" {
         #[cfg(coex)]
         fn coex_register_bt_cb(callback: unsafe extern "C" fn()) -> i32;
@@ -471,7 +493,7 @@ pub(crate) unsafe extern "C" fn coex_bb_reset_unlock(event: u32) {
 pub(crate) unsafe extern "C" fn coex_schm_register_btdm_callback_wrapper(
     callback: unsafe extern "C" fn(),
 ) -> i32 {
-    warn!("coex_schm_register_btdm_callback {:?}", callback);
+    trace!("coex_schm_register_btdm_callback {:?}", callback);
     unsafe extern "C" {
         #[cfg(coex)]
         fn coex_schm_register_callback(kind: u32, callback: unsafe extern "C" fn()) -> i32;
@@ -516,7 +538,7 @@ pub(crate) unsafe extern "C" fn coex_schm_curr_phase_get() -> *const () {
 
 #[allow(unused_variables)]
 pub(crate) unsafe extern "C" fn coex_wifi_channel_get(primary: *mut u8, secondary: *mut u8) -> i32 {
-    warn!("coex_wifi_channel_get");
+    trace!("coex_wifi_channel_get");
     unsafe extern "C" {
         #[cfg(coex)]
         fn coex_wifi_channel_get(primary: *mut u8, secondary: *mut u8) -> i32;
@@ -533,7 +555,7 @@ pub(crate) unsafe extern "C" fn coex_wifi_channel_get(primary: *mut u8, secondar
 pub(crate) unsafe extern "C" fn coex_register_wifi_channel_change_callback(
     callback: unsafe extern "C" fn(),
 ) -> i32 {
-    warn!("coex_register_wifi_channel_change_callback");
+    trace!("coex_register_wifi_channel_change_callback");
     unsafe extern "C" {
         #[cfg(coex)]
         fn coex_register_wifi_channel_change_callback(callback: unsafe extern "C" fn()) -> i32;
@@ -558,6 +580,10 @@ pub(crate) unsafe extern "C" fn set_isr(n: i32, f: unsafe extern "C" fn(), arg:
                 Interrupt::RWBT,
                 interrupt::Priority::Priority1
             ));
+            unwrap!(interrupt::enable(
+                Interrupt::RWBLE,
+                interrupt::Priority::Priority1
+            ));
         }
         7 => unsafe {
             ISR_INTERRUPT_7 = (f as *mut c_types::c_void, arg as *mut c_types::c_void);
@@ -585,10 +611,10 @@ pub(crate) unsafe extern "C" fn ints_on(mask: u32) {
 }
 
 #[cfg(coex)]
-const BTDM_ASYNC_WAKEUP_REQ_HCI: i32 = 0;
+pub(crate) const BTDM_ASYNC_WAKEUP_REQ_HCI: i32 = 0;
 
 #[cfg(coex)]
-const BTDM_ASYNC_WAKEUP_REQ_COEX: i32 = 1;
+pub(crate) const BTDM_ASYNC_WAKEUP_REQ_COEX: i32 = 1;
 
 // const BTDM_ASYNC_WAKEUP_REQMAX: i32 = 2;
 
@@ -606,26 +632,41 @@ const BTDM_ASYNC_WAKEUP_REQ_COEX: i32 = 1;
 ///
 /// *************************************************************************
 #[cfg(coex)]
-fn async_wakeup_request(event: i32) -> bool {
-    let mut do_wakeup_request = false;
-
-    let request_lock = match event {
-        e if e == BTDM_ASYNC_WAKEUP_REQ_HCI => true,
-        e if e == BTDM_ASYNC_WAKEUP_REQ_COEX => false,
-        _ => return false,
-    };
+pub(crate) fn async_wakeup_request(event: i32) -> bool {
+    trace!("async_wakeup_request {event}");
 
     unsafe extern "C" {
+        fn btdm_in_wakeup_requesting_set(set: bool);
+
         fn btdm_power_state_active() -> bool;
-        fn btdm_wakeup_request(request_lock: bool);
-    }
 
-    if !unsafe { btdm_power_state_active() } {
-        do_wakeup_request = true;
-        unsafe { btdm_wakeup_request(request_lock) };
+        fn btdm_wakeup_request();
     }
 
-    do_wakeup_request
+    let request_lock = match event {
+        e if e == BTDM_ASYNC_WAKEUP_REQ_HCI => {
+            unsafe {
+                btdm_in_wakeup_requesting_set(true);
+            }
+            false
+        }
+        e if e == BTDM_ASYNC_WAKEUP_REQ_COEX => {
+            unsafe {
+                btdm_in_wakeup_requesting_set(true);
+            }
+
+            if !unsafe { btdm_power_state_active() } {
+                unsafe {
+                    btdm_wakeup_request();
+                }
+                true
+            } else {
+                false
+            }
+        }
+        _ => return false,
+    };
+    request_lock
 }
 
 /// **************************************************************************
@@ -642,7 +683,9 @@ fn async_wakeup_request(event: i32) -> bool {
 ///
 /// *************************************************************************
 #[cfg(coex)]
-fn async_wakeup_request_end(event: i32) {
+pub(crate) fn async_wakeup_request_end(event: i32) {
+    trace!("async_wakeup_request_end {event}");
+
     let request_lock = match event {
         e if e == BTDM_ASYNC_WAKEUP_REQ_HCI => true,
         e if e == BTDM_ASYNC_WAKEUP_REQ_COEX => false,
@@ -650,12 +693,10 @@ fn async_wakeup_request_end(event: i32) {
     };
 
     unsafe extern "C" {
-        // this isn't found anywhere ... not a ROM function
-        // not in any of the libs - but the code will never call this anyway
-
-        // fn btdm_wakeup_request_end();
+        fn btdm_in_wakeup_requesting_set(set: bool);
     }
+
     if request_lock {
-        // unsafe { btdm_wakeup_request_end() };
+        unsafe { btdm_in_wakeup_requesting_set(false) };
     }
 }
diff --git a/esp-wifi/src/ble/os_adapter_esp32c2.rs b/esp-wifi/src/ble/os_adapter_esp32c2.rs
index de31718f045..4237e2fa0ee 100644
--- a/esp-wifi/src/ble/os_adapter_esp32c2.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32c2.rs
@@ -17,6 +17,8 @@ pub(crate) static mut ISR_INTERRUPT_7: (
     *mut crate::binary::c_types::c_void,
 ) = (core::ptr::null_mut(), core::ptr::null_mut());
 
+// keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+// ideally _some_ of these values should be configurable
 pub(crate) static BLE_CONFIG: esp_bt_controller_config_t = esp_bt_controller_config_t {
     config_version: 0x20231124,
     ble_ll_resolv_list_size: 4,
diff --git a/esp-wifi/src/ble/os_adapter_esp32c3.rs b/esp-wifi/src/ble/os_adapter_esp32c3.rs
index 42db50d9115..2764b5d79f7 100644
--- a/esp-wifi/src/ble/os_adapter_esp32c3.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32c3.rs
@@ -251,6 +251,8 @@ unsafe extern "C" {
 }
 
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
+    // keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+    // ideally _some_ of these values should be configurable
     esp_bt_controller_config_t {
         magic: 0x5a5aa5a5,
         version: 0x02404010,
diff --git a/esp-wifi/src/ble/os_adapter_esp32c6.rs b/esp-wifi/src/ble/os_adapter_esp32c6.rs
index bf344d1a691..34f47fb0e66 100644
--- a/esp-wifi/src/ble/os_adapter_esp32c6.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32c6.rs
@@ -17,6 +17,8 @@ pub(crate) static mut ISR_INTERRUPT_7: (
     *mut crate::binary::c_types::c_void,
 ) = (core::ptr::null_mut(), core::ptr::null_mut());
 
+// keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+// ideally _some_ of these values should be configurable
 pub(crate) static BLE_CONFIG: esp_bt_controller_config_t = esp_bt_controller_config_t {
     config_version: 0x20240422,
     ble_ll_resolv_list_size: 4,
diff --git a/esp-wifi/src/ble/os_adapter_esp32h2.rs b/esp-wifi/src/ble/os_adapter_esp32h2.rs
index 2059b2c1cae..e6b0e77f946 100644
--- a/esp-wifi/src/ble/os_adapter_esp32h2.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32h2.rs
@@ -17,6 +17,8 @@ pub(crate) static mut ISR_INTERRUPT_3: (
     *mut crate::binary::c_types::c_void,
 ) = (core::ptr::null_mut(), core::ptr::null_mut());
 
+// keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+// ideally _some_ of these values should be configurable
 pub(crate) static BLE_CONFIG: esp_bt_controller_config_t = esp_bt_controller_config_t {
     config_version: 0x20240422,
     ble_ll_resolv_list_size: 4,
diff --git a/esp-wifi/src/ble/os_adapter_esp32s3.rs b/esp-wifi/src/ble/os_adapter_esp32s3.rs
index 1402ca4fcdd..7a84d7e30e9 100644
--- a/esp-wifi/src/ble/os_adapter_esp32s3.rs
+++ b/esp-wifi/src/ble/os_adapter_esp32s3.rs
@@ -251,6 +251,8 @@ unsafe extern "C" {
 }
 
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
+    // keep them aligned with BT_CONTROLLER_INIT_CONFIG_DEFAULT in ESP-IDF
+    // ideally _some_ of these values should be configurable
     esp_bt_controller_config_t {
         magic: 0x5a5aa5a5,
         version: 0x02404010,
diff --git a/esp-wifi/src/compat/common.rs b/esp-wifi/src/compat/common.rs
index c1e3bd0248d..5bcb5fe027d 100644
--- a/esp-wifi/src/compat/common.rs
+++ b/esp-wifi/src/compat/common.rs
@@ -205,6 +205,16 @@ pub(crate) fn sem_delete(semphr: *mut c_void) {
 }
 
 pub(crate) fn sem_take(semphr: *mut c_void, tick: u32) -> i32 {
+    // This shouldn't normally happen if we always report the correct state from
+    // `is_in_isr`. This is a last resort if the driver calls this anyways.
+    // (I haven't observed this to happen)
+    let tick = if tick == OSI_FUNCS_TIME_BLOCKING && crate::is_interrupts_disabled() {
+        warn!("blocking sem_take probably called from an ISR - return early");
+        1
+    } else {
+        tick
+    };
+
     trace!(">>>> semphr_take {:?} block_time_tick {}", semphr, tick);
 
     let forever = tick == OSI_FUNCS_TIME_BLOCKING;
diff --git a/esp-wifi/src/lib.rs b/esp-wifi/src/lib.rs
index 6b8dbedfab6..428d1f4ba93 100644
--- a/esp-wifi/src/lib.rs
+++ b/esp-wifi/src/lib.rs
@@ -349,6 +349,8 @@ impl private::Sealed for Trng<'_> {}
 
 /// Initialize for using WiFi and or BLE.
 ///
+/// Make sure to **not** call this function while interrupts are disabled.
+///
 /// # The `timer` argument
 ///
 /// The `timer` argument is a timer source that is used by the WiFi driver to
@@ -378,6 +380,10 @@ pub fn init<'d>(
     _rng: impl EspWifiRngSource + 'd,
     _radio_clocks: RADIO_CLK<'d>,
 ) -> Result<EspWifiController<'d>, InitializationError> {
+    if crate::is_interrupts_disabled() {
+        return Err(InitializationError::InterruptsDisabled);
+    }
+
     // A minimum clock of 80MHz is required to operate WiFi module.
     const MIN_CLOCK: Rate = Rate::from_mhz(80);
     let clocks = Clocks::get();
@@ -467,6 +473,17 @@ pub unsafe fn deinit_unchecked() -> Result<(), InitializationError> {
     Ok(())
 }
 
+/// Returns true if at least some interrupt levels are disabled.
+fn is_interrupts_disabled() -> bool {
+    #[cfg(target_arch = "xtensa")]
+    return hal::xtensa_lx::interrupt::get_level() != 0
+        || hal::xtensa_lx::interrupt::get_mask() == 0;
+
+    #[cfg(target_arch = "riscv32")]
+    return !hal::riscv::register::mstatus::read().mie()
+        || hal::interrupt::current_runlevel() >= hal::interrupt::Priority::Priority1;
+}
+
 pub(crate) mod private {
     pub trait Sealed {}
 }
@@ -474,11 +491,19 @@ pub(crate) mod private {
 #[derive(Debug, Clone, Copy)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
 /// Error which can be returned during [`init`].
+#[non_exhaustive]
 pub enum InitializationError {
+    /// A general error occurred.
+    /// The internal error code is reported.
     General(i32),
+    /// An error from the WiFi driver.
     #[cfg(feature = "wifi")]
     WifiError(WifiError),
+    /// The current CPU clock frequency is too low.
     WrongClockConfig,
+    /// Tried to initialize while interrupts are disabled.
+    /// This is not supported.
+    InterruptsDisabled,
 }
 
 #[cfg(feature = "wifi")]
diff --git a/esp-wifi/src/wifi/internal.rs b/esp-wifi/src/wifi/internal.rs
index 639d810392a..c7b055abdaf 100644
--- a/esp-wifi/src/wifi/internal.rs
+++ b/esp-wifi/src/wifi/internal.rs
@@ -63,8 +63,7 @@ unsafe extern "C" fn semphr_give_from_isr_wrapper(
 
 #[cfg(coex)]
 unsafe extern "C" fn is_in_isr_wrapper() -> i32 {
-    // like original implementation
-    0
+    crate::is_interrupts_disabled() as i32
 }
 
 #[unsafe(no_mangle)]
diff --git a/esp-wifi/src/wifi/mod.rs b/esp-wifi/src/wifi/mod.rs
index f0a4f9ab006..e9595349f2a 100644
--- a/esp-wifi/src/wifi/mod.rs
+++ b/esp-wifi/src/wifi/mod.rs
@@ -2608,10 +2608,16 @@ pub struct Interfaces<'d> {
 /// Create a WiFi controller and it's associated interfaces.
 ///
 /// Dropping the controller will deinitialize / stop WiFi.
+///
+/// Make sure to **not** call this function while interrupts are disabled.
 pub fn new<'d>(
     inited: &'d EspWifiController<'d>,
     _device: crate::hal::peripherals::WIFI<'d>,
 ) -> Result<(WifiController<'d>, Interfaces<'d>), WifiError> {
+    if crate::is_interrupts_disabled() {
+        return Err(WifiError::Unsupported);
+    }
+
     let mut controller = WifiController {
         _phantom: Default::default(),
     };
diff --git a/esp-wifi/src/wifi/os_adapter.rs b/esp-wifi/src/wifi/os_adapter.rs
index c7abda80e7c..f9774ea64ea 100644
--- a/esp-wifi/src/wifi/os_adapter.rs
+++ b/esp-wifi/src/wifi/os_adapter.rs
@@ -177,10 +177,9 @@ pub unsafe extern "C" fn is_from_isr() -> bool {
 ///   Spin lock data pointer
 ///
 /// *************************************************************************
-static mut FAKE_SPIN_LOCK: u8 = 1;
 pub unsafe extern "C" fn spin_lock_create() -> *mut crate::binary::c_types::c_void {
-    // original: return (void *)1;
-    let ptr = addr_of_mut!(FAKE_SPIN_LOCK);
+    let ptr = crate::compat::common::sem_create(1, 1);
+
     trace!("spin_lock_create {:?}", ptr);
     ptr as *mut crate::binary::c_types::c_void
 }
@@ -199,8 +198,9 @@ pub unsafe extern "C" fn spin_lock_create() -> *mut crate::binary::c_types::c_vo
 ///
 /// *************************************************************************
 pub unsafe extern "C" fn spin_lock_delete(lock: *mut crate::binary::c_types::c_void) {
-    // original: DEBUGASSERT((int)lock == 1);
     trace!("spin_lock_delete {:?}", lock);
+
+    crate::compat::common::sem_delete(lock);
 }
 
 /// **************************************************************************
diff --git a/examples/src/bin/wifi_coex.rs b/examples/src/bin/wifi_coex.rs
index 9f55669354c..087d2bea55f 100644
--- a/examples/src/bin/wifi_coex.rs
+++ b/examples/src/bin/wifi_coex.rs
@@ -58,9 +58,17 @@ fn main() -> ! {
     let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
     let peripherals = esp_hal::init(config);
 
-    esp_alloc::heap_allocator!(size: 72 * 1024);
     // COEX needs more RAM - add some more
-    esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024);
+    #[cfg(feature = "esp32")]
+    {
+        esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 96 * 1024);
+        esp_alloc::heap_allocator!(size: 24 * 1024);
+    }
+    #[cfg(not(feature = "esp32"))]
+    {
+        esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024);
+        esp_alloc::heap_allocator!(size: 64 * 1024);
+    }
 
     let timg0 = TimerGroup::new(peripherals.TIMG0);
 
@@ -68,7 +76,29 @@ fn main() -> ! {
 
     let esp_wifi_ctrl = init(timg0.timer0, rng.clone(), peripherals.RADIO_CLK).unwrap();
 
-    let bluetooth = peripherals.BT;
+    let now = || time::Instant::now().duration_since_epoch().as_millis();
+
+    // initializing Bluetooth first results in a more stable WiFi connection on ESP32
+    let connector = BleConnector::new(&esp_wifi_ctrl, peripherals.BT);
+    let hci = HciConnector::new(connector, now);
+    let mut ble = Ble::new(&hci);
+
+    println!("{:?}", ble.init());
+    println!("{:?}", ble.cmd_set_le_advertising_parameters());
+    println!(
+        "{:?}",
+        ble.cmd_set_le_advertising_data(
+            create_advertising_data(&[
+                AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
+                AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]),
+                AdStructure::CompleteLocalName(esp_hal::chip!()),
+            ])
+            .unwrap()
+        )
+    );
+    println!("{:?}", ble.cmd_set_le_advertise_enable(true));
+
+    println!("started advertising");
 
     let (mut controller, interfaces) =
         esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).unwrap();
@@ -90,7 +120,6 @@ fn main() -> ! {
     }]);
     socket_set.add(dhcp_socket);
 
-    let now = || time::Instant::now().duration_since_epoch().as_millis();
     let stack = Stack::new(iface, device, socket_set, now, rng.random());
 
     let client_config = Configuration::Client(ClientConfiguration {
@@ -131,27 +160,6 @@ fn main() -> ! {
         }
     }
 
-    let connector = BleConnector::new(&esp_wifi_ctrl, bluetooth);
-    let hci = HciConnector::new(connector, now);
-    let mut ble = Ble::new(&hci);
-
-    println!("{:?}", ble.init());
-    println!("{:?}", ble.cmd_set_le_advertising_parameters());
-    println!(
-        "{:?}",
-        ble.cmd_set_le_advertising_data(
-            create_advertising_data(&[
-                AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
-                AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]),
-                AdStructure::CompleteLocalName(esp_hal::chip!()),
-            ])
-            .unwrap()
-        )
-    );
-    println!("{:?}", ble.cmd_set_le_advertise_enable(true));
-
-    println!("started advertising");
-
     println!("Start busy loop on main");
 
     let mut rx_buffer = [0u8; 128];
diff --git a/hil-test/Cargo.toml b/hil-test/Cargo.toml
index 78110df82a4..52a69338403 100644
--- a/hil-test/Cargo.toml
+++ b/hil-test/Cargo.toml
@@ -214,6 +214,11 @@ name              = "esp_wifi_ble_controller"
 harness           = false
 required-features = ["esp-wifi", "esp-alloc"]
 
+[[test]]
+name              = "esp_wifi_init"
+harness           = false
+required-features = ["esp-wifi", "esp-alloc"]
+
 [dependencies]
 allocator-api2    = { version = "0.2.0", default-features = false, features = ["alloc"] }
 cfg-if             = "1.0.0"
diff --git a/hil-test/tests/esp_wifi_floats.rs b/hil-test/tests/esp_wifi_floats.rs
index 8e931ae0b3f..67a2af2b262 100644
--- a/hil-test/tests/esp_wifi_floats.rs
+++ b/hil-test/tests/esp_wifi_floats.rs
@@ -129,6 +129,19 @@ mod tests {
             .start_app_core(
                 unsafe { &mut *core::ptr::addr_of_mut!(APP_CORE_STACK) },
                 move || {
+                    // app core starts with interrupts disabled
+                    unsafe {
+                        esp_hal::xtensa_lx::interrupt::enable_mask(
+                            esp_hal::xtensa_lx_rt::interrupt::CpuInterruptLevel::Level1.mask()
+                                | esp_hal::xtensa_lx_rt::interrupt::CpuInterruptLevel::Level2
+                                    .mask()
+                                | esp_hal::xtensa_lx_rt::interrupt::CpuInterruptLevel::Level3
+                                    .mask()
+                                | esp_hal::xtensa_lx_rt::interrupt::CpuInterruptLevel::Level6
+                                    .mask(),
+                        );
+                    }
+
                     let timg0 = TimerGroup::new(peripherals.TIMG0);
                     let _init = esp_wifi::init(
                         timg0.timer1,
diff --git a/hil-test/tests/esp_wifi_init.rs b/hil-test/tests/esp_wifi_init.rs
new file mode 100644
index 00000000000..ec256deb07c
--- /dev/null
+++ b/hil-test/tests/esp_wifi_init.rs
@@ -0,0 +1,133 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32s2 esp32c2 esp32c3 esp32c6 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi embassy
+
+#![no_std]
+#![no_main]
+
+use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
+#[cfg(target_arch = "riscv32")]
+use esp_hal::riscv::interrupt::free as interrupt_free;
+#[cfg(target_arch = "xtensa")]
+use esp_hal::xtensa_lx::interrupt::free as interrupt_free;
+use esp_hal::{
+    clock::CpuClock,
+    interrupt::{Priority, software::SoftwareInterruptControl},
+    peripherals::{Peripherals, RADIO_CLK, RNG, TIMG0},
+    rng::Rng,
+    timer::timg::TimerGroup,
+};
+use esp_hal_embassy::InterruptExecutor;
+use esp_wifi::InitializationError;
+use hil_test as _;
+use static_cell::StaticCell;
+
+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
+    }};
+}
+
+#[embassy_executor::task]
+async fn try_init(
+    signal: &'static Signal<CriticalSectionRawMutex, Option<InitializationError>>,
+    timer: TIMG0<'static>,
+    rng: RNG<'static>,
+    radio_clk: RADIO_CLK<'static>,
+) {
+    let timg0 = TimerGroup::new(timer);
+
+    let init = esp_wifi::init(timg0.timer0, Rng::new(rng), radio_clk);
+
+    match init {
+        Ok(_) => {
+            signal.signal(None);
+        }
+        Err(err) => {
+            signal.signal(Some(err));
+        }
+    }
+}
+
+#[cfg(test)]
+#[embedded_test::tests(default_timeout = 3, executor = esp_hal_embassy::Executor::new())]
+mod tests {
+    use super::*;
+
+    #[init]
+    fn init() -> Peripherals {
+        esp_alloc::heap_allocator!(size: 72 * 1024);
+
+        let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
+        esp_hal::init(config)
+    }
+
+    #[test]
+    fn test_init_fails_cs(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = critical_section::with(|_| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+
+    #[test]
+    fn test_init_fails_interrupt_free(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = interrupt_free(|| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+
+    #[test]
+    async fn test_init_fails_in_interrupt_executor_task(peripherals: Peripherals) {
+        let sw_ints = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
+
+        static EXECUTOR_CORE_0: StaticCell<InterruptExecutor<1>> = StaticCell::new();
+        let executor_core0 = InterruptExecutor::new(sw_ints.software_interrupt1);
+        let executor_core0 = EXECUTOR_CORE_0.init(executor_core0);
+
+        let spawner = executor_core0.start(Priority::Priority1);
+
+        let signal =
+            mk_static!(Signal<CriticalSectionRawMutex, Option<InitializationError>>, Signal::new());
+
+        spawner
+            .spawn(try_init(
+                signal,
+                peripherals.TIMG0,
+                peripherals.RNG,
+                peripherals.RADIO_CLK,
+            ))
+            .ok();
+
+        let res = signal.wait().await;
+
+        assert!(matches!(
+            res,
+            Some(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+}


Reviews:
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T07:39:31Z
user: login: bugadani
body: I'm not quite sure if all the changes are related to the original problem.

Changes to `coex_bt_wakeup_request` are concerning - why do we need to emulate what the blobs should handle for us?

The interrupt-free check is iffy, it doesn't take `INTENABLE` into account I think, and `xtensa_lx::interrupt::free` just clears those bits.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T07:50:17Z
user: login: MabezDev
body: I'm wondering how we can start to test this stuff, we have https://github.com/esp-rs/esp-hal/issues/3071 but I don't know how much time/effort is needed to start testing the RF parts
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T11:23:26Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:36:41Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:45:55Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:09:43Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:11:11Z
user: login: Dominaezzz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T13:17:35Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:32:34Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:36:23Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:40:27Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:43:06Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:35:40Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:42:58Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:44:58Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:45:19Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:47:25Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:50:03Z
user: login: bugadani
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:07:41Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:13:46Z
user: login: bjoernQ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:15:18Z
user: login: bugadani
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:39:59Z
user: login: JurajSadel
body: LGTM, thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T11:35:55Z

Review comments:
diff_hunk: @@ -691,7 +691,7 @@ mod classic {
     }
 
     /// Get the current run level (the level below which interrupts are masked).
-    pub(crate) fn current_runlevel() -> Priority {
+    pub fn current_runlevel() -> Priority {
path: esp-hal/src/interrupt/riscv.rs
user: login: bugadani
body: Why does the risc-v API need to be public for ESP32?
created_at: 2025-04-22T07:39:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,12 +292,12 @@ static BTDM_DRAM_AVAILABLE_REGION: [btdm_dram_available_region_t; 7] = [
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
     esp_bt_controller_config_t {
         controller_task_stack_size: 4096,
-        controller_task_prio: 110,
+        controller_task_prio: 23,
path: esp-wifi/src/ble/os_adapter_esp32.rs
user: login: bugadani
body: It would be very nice to have some explanation, or a cross-reference to where this comes from.
created_at: 2025-04-22T07:40:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,10 +178,9 @@ pub unsafe extern "C" fn is_from_isr() -> bool {
 ///   Spin lock data pointer
 ///
 /// *************************************************************************
-static mut FAKE_SPIN_LOCK: u8 = 1;
 pub unsafe extern "C" fn spin_lock_create() -> *mut crate::binary::c_types::c_void {
-    // original: return (void *)1;
-    let ptr = addr_of_mut!(FAKE_SPIN_LOCK);
+    let ptr = crate::compat::common::sem_create(1, 1);
path: esp-wifi/src/wifi/os_adapter.rs
user: login: bugadani
body: 🚀
created_at: 2025-04-22T07:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -691,7 +691,7 @@ mod classic {
     }
 
     /// Get the current run level (the level below which interrupts are masked).
-    pub(crate) fn current_runlevel() -> Priority {
+    pub fn current_runlevel() -> Priority {
path: esp-hal/src/interrupt/riscv.rs
user: login: bugadani
body: Ah nevermind I see
created_at: 2025-04-22T07:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -201,6 +201,15 @@ pub(crate) fn sem_delete(semphr: *mut c_void) {
 }
 
 pub(crate) fn sem_take(semphr: *mut c_void, tick: u32) -> i32 {
+    // This shouldn't normally happen if we always report the correct state from
+    // `is_in_isr`. This is a last resort if we do it wrong.
path: esp-wifi/src/compat/common.rs
user: login: MabezDev
body: How can we "do it wrong"? Is this something we can prevent elsewhere than doing this?
created_at: 2025-04-22T10:21:44Z
author_association: MEMBER
diff_hunk: @@ -201,6 +201,15 @@ pub(crate) fn sem_delete(semphr: *mut c_void) {
 }
 
 pub(crate) fn sem_take(semphr: *mut c_void, tick: u32) -> i32 {
+    // This shouldn't normally happen if we always report the correct state from
+    // `is_in_isr`. This is a last resort if we do it wrong.
path: esp-wifi/src/compat/common.rs
user: login: bjoernQ
body: We probably can just remove this - it would protect us from the drivers "doing it wrong" (if they don't check `is_in_isr` / so the comment is at least mis-leading)

t.b.h. I haven't seen this happening but if it happens it's not super obvious to diagnose - so I'm a bit undecided to remove it. I don't think the check is too expense but - if everything is fine - unnecessary
created_at: 2025-04-22T11:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi
+
+#![no_std]
+#![no_main]
+
+use esp_hal::clock::CpuClock;
+use hil_test as _;
+
+#[cfg(test)]
+#[embedded_test::tests(default_timeout = 3)]
+mod tests {
+    use esp_hal::{peripherals::Peripherals, rng::Rng, timer::timg::TimerGroup};
+
+    use super::*;
+
+    #[init]
+    fn init() -> Peripherals {
+        esp_alloc::heap_allocator!(size: 72 * 1024);
+
+        let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
+        esp_hal::init(config)
+    }
+
+    #[test]
+    fn test_init_fails_cs(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = critical_section::with(|_| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+
+    #[test]
+    fn test_init_fails_interrupt_free(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = interrupt_free(|| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+}
+
+#[cfg(target_arch = "riscv32")]
+fn interrupt_free<F, R>(f: F) -> R
path: hil-test/tests/esp_wifi_init.rs
user: login: bugadani
body: This could just be two import lines :)

`use esp_hal::riscv::interrupt::free as interrupt_free` etc.
created_at: 2025-04-22T11:45:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi
+
+#![no_std]
+#![no_main]
+
+use esp_hal::clock::CpuClock;
+use hil_test as _;
+
+#[cfg(test)]
+#[embedded_test::tests(default_timeout = 3)]
+mod tests {
+    use esp_hal::{peripherals::Peripherals, rng::Rng, timer::timg::TimerGroup};
+
+    use super::*;
+
+    #[init]
+    fn init() -> Peripherals {
+        esp_alloc::heap_allocator!(size: 72 * 1024);
+
+        let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
+        esp_hal::init(config)
+    }
+
+    #[test]
+    fn test_init_fails_cs(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = critical_section::with(|_| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+
+    #[test]
+    fn test_init_fails_interrupt_free(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = interrupt_free(|| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+}
+
+#[cfg(target_arch = "riscv32")]
+fn interrupt_free<F, R>(f: F) -> R
path: hil-test/tests/esp_wifi_init.rs
user: login: bjoernQ
body: Yes :) Anything else I could / should test here?
created_at: 2025-04-22T12:09:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi
+
+#![no_std]
+#![no_main]
+
+use esp_hal::clock::CpuClock;
+use hil_test as _;
+
+#[cfg(test)]
+#[embedded_test::tests(default_timeout = 3)]
+mod tests {
+    use esp_hal::{peripherals::Peripherals, rng::Rng, timer::timg::TimerGroup};
+
+    use super::*;
+
+    #[init]
+    fn init() -> Peripherals {
+        esp_alloc::heap_allocator!(size: 72 * 1024);
+
+        let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
+        esp_hal::init(config)
+    }
+
+    #[test]
+    fn test_init_fails_cs(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = critical_section::with(|_| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+
+    #[test]
+    fn test_init_fails_interrupt_free(peripherals: Peripherals) {
+        let timg0 = TimerGroup::new(peripherals.TIMG0);
+        let init = interrupt_free(|| {
+            esp_wifi::init(
+                timg0.timer0,
+                Rng::new(peripherals.RNG),
+                peripherals.RADIO_CLK,
+            )
+        });
+
+        assert!(matches!(
+            init,
+            Err(esp_wifi::InitializationError::InterruptsDisabled),
+        ));
+    }
+}
+
+#[cfg(target_arch = "riscv32")]
+fn interrupt_free<F, R>(f: F) -> R
path: hil-test/tests/esp_wifi_init.rs
user: login: bugadani
body: Should be fine for now, except for H2 which doesn't have wifi 🙃
created_at: 2025-04-22T12:11:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -467,18 +473,37 @@ pub unsafe fn deinit_unchecked() -> Result<(), InitializationError> {
     Ok(())
 }
 
+/// Returns true if at least some interrupt levels are disabled.
+fn interrupts_disabled() -> bool {
path: esp-wifi/src/lib.rs
user: login: Dominaezzz
body: There should be a `is_` or `are_` infront of this functions name imo
created_at: 2025-04-22T13:17:35Z
author_association: COLLABORATOR
diff_hunk: @@ -129,6 +129,19 @@ mod tests {
             .start_app_core(
                 unsafe { &mut *core::ptr::addr_of_mut!(APP_CORE_STACK) },
                 move || {
+                    // app core starts with interrupts disabled
+                    unsafe {
+                        esp_hal::xtensa_lx::interrupt::enable_mask(
path: hil-test/tests/esp_wifi_floats.rs
user: login: bjoernQ
body: On core-0 enabling level 1 interrupts is a side effect of `esp_hal::init`
created_at: 2025-04-23T08:32:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -129,6 +129,19 @@ mod tests {
             .start_app_core(
                 unsafe { &mut *core::ptr::addr_of_mut!(APP_CORE_STACK) },
                 move || {
+                    // app core starts with interrupts disabled
+                    unsafe {
+                        esp_hal::xtensa_lx::interrupt::enable_mask(
path: hil-test/tests/esp_wifi_floats.rs
user: login: bugadani
body: How does esp_hal::init achieve it? Why don't we change `start_app_core` to enable interrupts?
created_at: 2025-04-23T08:36:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -129,6 +129,19 @@ mod tests {
             .start_app_core(
                 unsafe { &mut *core::ptr::addr_of_mut!(APP_CORE_STACK) },
                 move || {
+                    // app core starts with interrupts disabled
+                    unsafe {
+                        esp_hal::xtensa_lx::interrupt::enable_mask(
path: hil-test/tests/esp_wifi_floats.rs
user: login: bjoernQ
body: It's a side effect of installing the GPIO interrupt handler IIRC

Currently we explicitly disable all interrupt on core-1 - we should define what we do for each core (or if we should try to get them in a similar state). But IMHO tinkering with that is out of scope of this PR

I will create an issue for that (so we don't forget about it and more important not do some random change in an unrelated PR)
created_at: 2025-04-23T08:40:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -129,6 +129,19 @@ mod tests {
             .start_app_core(
                 unsafe { &mut *core::ptr::addr_of_mut!(APP_CORE_STACK) },
                 move || {
+                    // app core starts with interrupts disabled
+                    unsafe {
+                        esp_hal::xtensa_lx::interrupt::enable_mask(
path: hil-test/tests/esp_wifi_floats.rs
user: login: bjoernQ
body: Issue created #3410

created_at: 2025-04-23T08:43:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - The `ESP_HAL_CONFIG_PLACE_SPI_DRIVER_IN_RAM` configuration option has been renamed to `ESP_HAL_CONFIG_PLACE_SPI_MASTER_DRIVER_IN_RAM`. (#3402)
 - Made the `ParlIo` traits for `TxPins`, `RxPins`, `ConfigurePins` public (#3398)
 - Renamed `Flex::enable_input` to `set_input_enable` (#3387)
+- Make `esp_hal::interrupt::current_runlevel` public under the unstable feature (#3403)
path: esp-hal/CHANGELOG.md
user: login: bugadani
body: ```suggestion
- Made `esp_hal::interrupt::current_runlevel` public under the unstable feature (#3403)
```

This change only seems to apply to riscv.
created_at: 2025-04-23T13:28:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,12 +292,12 @@ static BTDM_DRAM_AVAILABLE_REGION: [btdm_dram_available_region_t; 7] = [
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
     esp_bt_controller_config_t {
         controller_task_stack_size: 4096,
-        controller_task_prio: 110,
+        controller_task_prio: 23,
path: esp-wifi/src/ble/os_adapter_esp32.rs
user: login: bugadani
body: This change is missing from the changelog, and its effects are unknown.
created_at: 2025-04-23T13:30:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
path: hil-test/tests/esp_wifi_init.rs
user: login: bugadani
body: Does this include initializing esp-wifi in an interrupt executor?
created_at: 2025-04-23T13:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - The `ESP_HAL_CONFIG_PLACE_SPI_DRIVER_IN_RAM` configuration option has been renamed to `ESP_HAL_CONFIG_PLACE_SPI_MASTER_DRIVER_IN_RAM`. (#3402)
 - Made the `ParlIo` traits for `TxPins`, `RxPins`, `ConfigurePins` public (#3398)
 - Renamed `Flex::enable_input` to `set_input_enable` (#3387)
+- Make `esp_hal::interrupt::current_runlevel` public under the unstable feature (#3403)
path: esp-hal/CHANGELOG.md
user: login: bjoernQ
body: that's true - I guess we should align that and make it public for Xtensa, too
created_at: 2025-04-23T13:42:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,12 +292,12 @@ static BTDM_DRAM_AVAILABLE_REGION: [btdm_dram_available_region_t; 7] = [
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
     esp_bt_controller_config_t {
         controller_task_stack_size: 4096,
-        controller_task_prio: 110,
+        controller_task_prio: 23,
path: esp-wifi/src/ble/os_adapter_esp32.rs
user: login: bjoernQ
body: we can revert the change to `controller_task_prio` - I doubt it will have any effect for our built-in-scheduler
was just aligning the settings when comparing things to esp-idf but none of the config changes should be needed for the fix
created_at: 2025-04-23T13:44:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,12 +292,12 @@ static BTDM_DRAM_AVAILABLE_REGION: [btdm_dram_available_region_t; 7] = [
 pub(crate) fn create_ble_config() -> esp_bt_controller_config_t {
     esp_bt_controller_config_t {
         controller_task_stack_size: 4096,
-        controller_task_prio: 110,
+        controller_task_prio: 23,
path: esp-wifi/src/ble/os_adapter_esp32.rs
user: login: bjoernQ
body: so all of them can get reverted
created_at: 2025-04-23T13:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
path: hil-test/tests/esp_wifi_init.rs
user: login: bjoernQ
body: I think I never even tried to use esp-wifi from an interrupt executor controlled task - did that ever work? If not we should document that
created_at: 2025-04-23T13:47:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
path: hil-test/tests/esp_wifi_init.rs
user: login: bugadani
body: Right now you can't initialize esp_wifi, I think, but with the builtin scheduler it's probably for the best anyway.
created_at: 2025-04-23T13:50:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32s2 esp32c2 esp32c3 esp32c6 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi embassy
+
+#![no_std]
+#![no_main]
+
+#[cfg(target_arch = "riscv32")]
+use esp_hal::riscv::interrupt::free as interrupt_free;
+#[cfg(target_arch = "xtensa")]
+use esp_hal::xtensa_lx::interrupt::free as interrupt_free;
+use esp_hal::{
+    clock::CpuClock,
+    interrupt::{Priority, software::SoftwareInterruptControl},
+    peripherals::{Peripherals, RADIO_CLK, RNG, TIMG0},
+    rng::Rng,
+    timer::timg::TimerGroup,
+};
+use esp_hal_embassy::InterruptExecutor;
+use hil_test as _;
+use portable_atomic::AtomicUsize;
+use static_cell::StaticCell;
+
+static ASYNC_TEST_STATE: AtomicUsize = AtomicUsize::new(0);
+
+#[embassy_executor::task]
+async fn try_init(timer: TIMG0<'static>, rng: RNG<'static>, radio_clk: RADIO_CLK<'static>) {
+    let timg0 = TimerGroup::new(timer);
+
+    defmt::info!("before");
+    let init = esp_wifi::init(timg0.timer0, Rng::new(rng), radio_clk);
+    defmt::info!("after {}", init.is_ok());
+
+    match init {
path: hil-test/tests/esp_wifi_init.rs
user: login: bugadani
body: Use a Signal - the test case can be async without esp-hal-embassy being initialized (we just can't use Timer)
created_at: 2025-04-24T08:07:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32s2 esp32c2 esp32c3 esp32c6 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi embassy
+
+#![no_std]
+#![no_main]
+
+#[cfg(target_arch = "riscv32")]
+use esp_hal::riscv::interrupt::free as interrupt_free;
+#[cfg(target_arch = "xtensa")]
+use esp_hal::xtensa_lx::interrupt::free as interrupt_free;
+use esp_hal::{
+    clock::CpuClock,
+    interrupt::{Priority, software::SoftwareInterruptControl},
+    peripherals::{Peripherals, RADIO_CLK, RNG, TIMG0},
+    rng::Rng,
+    timer::timg::TimerGroup,
+};
+use esp_hal_embassy::InterruptExecutor;
+use hil_test as _;
+use portable_atomic::AtomicUsize;
+use static_cell::StaticCell;
+
+static ASYNC_TEST_STATE: AtomicUsize = AtomicUsize::new(0);
+
+#[embassy_executor::task]
+async fn try_init(timer: TIMG0<'static>, rng: RNG<'static>, radio_clk: RADIO_CLK<'static>) {
+    let timg0 = TimerGroup::new(timer);
+
+    defmt::info!("before");
+    let init = esp_wifi::init(timg0.timer0, Rng::new(rng), radio_clk);
+    defmt::info!("after {}", init.is_ok());
+
+    match init {
path: hil-test/tests/esp_wifi_init.rs
user: login: bjoernQ
body: was considering it - should definitely remove the logs

> the test case can be async without esp-hal-embassy being initialized (we just can't use Timer)

I don't init esp-hal-embassy 🤔 

created_at: 2025-04-24T08:13:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+//! Test we get an error when attempting to initialize esp-wifi with interrupts
+//! disabled in common ways
+
+//% CHIPS: esp32 esp32s2 esp32c2 esp32c3 esp32c6 esp32s3
+//% FEATURES: unstable esp-wifi esp-alloc esp-wifi/wifi embassy
+
+#![no_std]
+#![no_main]
+
+#[cfg(target_arch = "riscv32")]
+use esp_hal::riscv::interrupt::free as interrupt_free;
+#[cfg(target_arch = "xtensa")]
+use esp_hal::xtensa_lx::interrupt::free as interrupt_free;
+use esp_hal::{
+    clock::CpuClock,
+    interrupt::{Priority, software::SoftwareInterruptControl},
+    peripherals::{Peripherals, RADIO_CLK, RNG, TIMG0},
+    rng::Rng,
+    timer::timg::TimerGroup,
+};
+use esp_hal_embassy::InterruptExecutor;
+use hil_test as _;
+use portable_atomic::AtomicUsize;
+use static_cell::StaticCell;
+
+static ASYNC_TEST_STATE: AtomicUsize = AtomicUsize::new(0);
+
+#[embassy_executor::task]
+async fn try_init(timer: TIMG0<'static>, rng: RNG<'static>, radio_clk: RADIO_CLK<'static>) {
+    let timg0 = TimerGroup::new(timer);
+
+    defmt::info!("before");
+    let init = esp_wifi::init(timg0.timer0, Rng::new(rng), radio_clk);
+    defmt::info!("after {}", init.is_ok());
+
+    match init {
path: hil-test/tests/esp_wifi_init.rs
user: login: bjoernQ
body: ah I see - we already have embassy-sync in the deps 👍 
created_at: 2025-04-24T08:15:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: bjoernQ
created_at: 2025-04-22T07:36:36Z
body: Fixes #3062 
author_association: CONTRIBUTOR
user: login: bjoernQ
created_at: 2025-04-22T09:16:42Z
body: > I'm not quite sure if all the changes are related to the original problem.

Not the `esp_bt_controller_config_t` change - it's just a bit more aligned and it's even not really clear what every field is for.

Most relevant change is `is_in_isr` 

> Changes to coex_bt_wakeup_request are concerning - why do we need to emulate what the blobs should handle for us?

It's unfortunately what ESP-IDF does for ESP32 and given there is no documentation I don't dare to question that

> The interrupt-free check is iffy, it doesn't take INTENABLE into account I think, and xtensa_lx::interrupt::free just clears those bits.

Probably yes - most of that needs some more love but I'm not sure what exactly you are referring to - if its code touched here in this PR, mind going into more details? (i.e. which change)

author_association: CONTRIBUTOR
user: login: bugadani
created_at: 2025-04-22T09:51:32Z
body: > if its code touched here in this PR, mind going into more details? (i.e. which change)

Sure! This check doesn't work when called inside `xtensa_lx::interrupt::free`, at least I don't think it does:

```rust
    if crate::interrupts_disabled() {
        return Err(WifiError::Unsupported);
    }
```
author_association: CONTRIBUTOR
user: login: bjoernQ
created_at: 2025-04-22T10:05:47Z
body: > > if its code touched here in this PR, mind going into more details? (i.e. which change)
> 
> Sure! This check doesn't work when called inside `xtensa_lx::interrupt::free`, at least I don't think it does:
> 
> ```rust
>     if crate::interrupts_disabled() {
>         return Err(WifiError::Unsupported);
>     }
> ```

Thanks! Yes, that is true - well noticed
author_association: CONTRIBUTOR
user: login: bugadani
created_at: 2025-04-22T11:13:21Z
body: > Thanks! Yes, that is true - well noticed

Would you please add a few test cases somewhere that we are supposed to return an error in various interrupts-are-disabled cases?
author_association: CONTRIBUTOR
user: login: bjoernQ
created_at: 2025-04-22T11:42:18Z
body: > I'm wondering how we can start to test this stuff, we have https://github.com/esp-rs/esp-hal/issues/3071 but I don't know how much time/effort is needed to start testing the RF parts

For WiFi we would need some AP to connect to with e.g. a TCP server to connect to echoing what we send.

For BLE I could imagine using Trouble to implement the central role and connect to some BLE peripheral which is available in the test setup (but that device needs to support multiple concurrent connections in case that test happens to run concurrently on multiple targets)

author_association: CONTRIBUTOR
user: login: jessebraham
created_at: 2025-04-22T12:14:16Z
body: > For WiFi we would need some AP to connect to with e.g. a TCP server to connect to echoing what we send.

As I mentioned last time this came up, I've been told that due to the amount of interference in the server room, other teams have found that trying to test Wi-Fi in the HIL setups is not reliable, and would likely require a Faraday cage.
author_association: MEMBER