* Bring ieee802154 driver up to date with ESP-IDF C equivalent (#2) * Initial plan * Port ESP-IDF C driver improvements to Rust ieee802154 driver - Add RxAck and TxEnhAck states matching C driver state machine - Overhaul ISR handler with C driver event processing order: RX_ABORT(phase1) -> SFD -> TX_DONE -> RX_DONE -> ACK_TX_DONE -> ACK_RX_DONE -> RX_ABORT(phase2) -> TX_ABORT - TX done: transition to RxAck state when ACK required instead of immediately calling next_operation - RX done: queue frame then handle ACK state transitions - ACK TX done: clear security, deliver received frame, next_operation - ACK RX done: signal tx_done on ACK receipt - Two-phase RX abort: phase 1 for RX-state, phase 2 for TX-ACK-state - TX abort: differentiate reasons (timeout, coex break, CCA busy, etc.) - Per-state stop_current_operation dispatch (stop_rx, stop_tx, etc.) - Fix tx_init to only set RX buffer when ACK is actually required - Allow ieee802154_receive re-entry from TxAck state - Add HAL: get_tx/rx_abort_reason, rx_auto_ack, disable abort events - Add tx_failed callback for transmit failure notification - Align mac_init abort events with C driver configuration - Enable CCA TX support in ieee802154_transmit Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Address code review: fix duplicate frame queuing and doc comments Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix clippy warnings: remove unused HAL functions, needless returns, wire up per-state stop - Remove unused `clear_tx_abort_status`, `clear_rx_abort_status`, `disable_tx_abort_events`, `disable_rx_abort_events` from hal.rs - Remove needless `return` statements in match arms in raw.rs - Wire up `tx_init` and `rx_init` to use `stop_current_operation_inner` with proper per-state dispatch (matching C driver) instead of simple stop+clear which was hiding a bug - Remove unused `stop_current_operation` wrapper (callers now pass state directly) Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Revert unrelated xtask formatting changes Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix unused variable: remove unused `state` parameter from `stop_rx_ack` Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Add timer0-based 200ms ACK receive timeout matching C driver - Add HAL functions: timer0_set_threshold(), timer0_start(), timer0_stop() - Start timer0 with 200ms timeout when entering RxAck state in isr_handle_tx_done, matching C driver's receive_ack_timeout_timer_start - Handle Timer0Overflow event in ISR: signals tx_failed + next_operation - Stop timer0 in stop_rx_ack, isr_handle_ack_rx_done, and isr_handle_tx_abort(RxAckTimeout) to prevent stale timeouts - Add event_end_process() calls matching C driver: stops timer0 at the start of each ISR handler and in stop_current_operation_inner Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Extract ACK timeout magic number to named constant ACK_TIMEOUT_US Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix coex mode: align TX/RX abort events with C driver, add TX deferral, coex break notify Three critical coex-mode fixes matching the C driver: 1. TX abort events aligned with C driver's mac_init: only enable RxAckTimeout, TxCoexBreak, TxSecurityError, CcaFailed, CcaBusy. Previously enabled 9 extra events (RxAckSfdTimeout, RxAckCrcError, etc.) that the C driver deliberately disables — these cause premature TX failures in BLE coex mode by immediately failing on transient ACK reception errors instead of letting the timer0 200ms timeout handle them. 2. TX deferral: ieee802154_transmit() now checks if currently receiving a frame (is_current_rx_frame) or sending ACK (TxAck/ TxEnhAck state), and reports TX failure instead of aborting. This matches the C driver's transmit() and prevents losing in-flight frames during BLE coex arbitration. 3. Coex break notification: calls esp_coex_ieee802154_coex_break_notify() when TX is aborted due to BLE coex break, matching C driver's isr_handle_tx_abort. This allows the coex manager to adjust scheduling. Also aligns RX abort events with C driver (TxAckTimeout + TxAckCoexBreak only, removed extra RxStop and TxAckStop). Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Revert coex commit 6584a85: remove TX deferral that caused regression Reverts all changes from commit 6584a85 which caused severe stability regression — commissioning was no longer possible. Root cause: the TX deferral check (`is_current_rx_frame()` + TxAck/ TxEnhAck state check) was too aggressive. With `rx_when_idle` enabled (as in Thread/OpenThread), the driver is always in RX state, and `is_current_rx_frame()` returns true frequently in any RF environment. This caused most TX attempts to be deferred and reported as failures via `tx_failed()` callback, which the upper layer (openthread crate) doesn't handle as a retry signal. Reverted changes: - TX deferral in ieee802154_transmit() (the regression cause) - TX abort events reduction (from 14 to 5) — may need re-evaluation but needs to be done separately with proper testing - is_current_rx_frame() HAL function (no longer needed) - esp_coex_ieee802154_coex_break_notify() call (harmless but part of the regressed commit) The ISR overhaul, state machine, timer0 ACK timeout, and other improvements from earlier commits are preserved. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Re-introduce safe coex improvements from reverted 6584a85 Re-apply three improvements from the reverted coex commit that are independent of the TX deferral regression: 1. TX abort events reduced from 14 to 5 matching C driver's mac_init: only RxAckTimeout, TxCoexBreak, TxSecurityError, CcaFailed, CcaBusy. The 9 extra events (RxAckSfdTimeout, RxAckCrcError, etc.) caused duplicate tx_failed callbacks: first from the ACK error, then again from the timer0 200ms timeout. The C driver deliberately disables these so transient ACK errors don't immediately fail the TX. 2. RX abort events reduced to 2 matching C driver: TxAckTimeout and TxAckCoexBreak only. 3. esp_coex_ieee802154_coex_break_notify() called on TX coex break, allowing the coex manager to adjust BLE/802.15.4 scheduling. TX deferral (the regression cause) is NOT re-introduced - that requires the openthread crate to properly handle tx_failed as a retry signal. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix nightly rustfmt: collapse single-line enable_rx_abort_events call Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix CCA TODO: add `cca` parameter to transmit() and transmit_raw() The "// what about CCA?" comments were TODOs — both transmit() and transmit_raw() hardcoded `cca: false`, meaning CCA was never performed before transmitting. In IEEE 802.15.4, CCA is essential to avoid collisions (the C driver's esp_ieee802154_transmit passes through the cca parameter from its caller). The fix adds a `cca: bool` parameter to both public API functions, allowing callers (like the openthread crate) to request CCA before transmission, matching the C driver's behavior. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Re-introduce TX deferral matching C driver Now safe to re-introduce because the openthread crate's for-review branch properly handles tx_failed: - TX_SIGNAL changed from Signal<()> to Signal<bool> - tx_done_callback signals true, tx_failed_callback signals false - EspRadio::transmit() returns Err(RadioErrorKind::TxFailed) on false - OpenThread SubMac retries with CSMA backoff on any error The TX deferral checks in ieee802154_transmit() match the C driver: - If currently sending ACK (TxAck/TxEnhAck state), defer TX - If currently receiving a frame past SFD (is_current_rx_frame), defer TX - Deferred TX calls tx_failed() callback instead of aborting Also adds is_current_rx_frame() HAL function that reads rx_state > 1 from the rx_status register, matching the C driver's ieee802154_ll_is_current_rx_frame(). Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix TX power: default to 20 dBm matching C driver, fix >= boundary Two TX power bugs that explain instability at longer distances: 1. Default TX power was 10 dBm in both Config::default() and pib_init() The C driver initializes all channels to IEEE802154_TXPOWER_VALUE_MAX (20 dBm). This means the Rust driver was transmitting at half the power of the C driver — 10x less RF power, severely reducing range. 2. txpower_convert() used `>` instead of `>=` for the max boundary. When txpower == 20 (IEEE802154_TXPOWER_VALUE_MAX), the C driver returns index 15 (max), but our code fell through to the formula and returned 14 — one step below maximum power. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Align with ESP-IDF 5.5.2: ACK frame return, pending TX, fix double-callback Changes compared to 5.5.2 C driver: 1. ACK frame capture: isr_handle_ack_rx_done and stop_rx_ack now store the received ACK frame in ACK_FRAME, exposed via get_ack_frame(). OpenThread needs this for Frame Pending bit. 2. Pending TX mechanism: replaces tx_failed() deferral with the C driver's internal pending TX. When TX is deferred, the frame is stored and transmitted automatically via next_operation() when the current op completes. More efficient than OpenThread retry. 3. event_end_process() now includes set_transmit_security(false) matching 5.5.2. Removed redundant ieee802154_sec_update() calls from ISR handlers (now covered by event_end_process). 4. Fixed double-callback bug: removed notify_state() from next_operation() which generated duplicate tx_done/rx_available callbacks after ISR handlers already issued them. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix "Receive queue full": eliminate duplicate frame queuing Root cause: isr_handle_rx_done() queued frames immediately via receive_done(), but when the ACK phase completed and next_operation() dispatched a pending TX, stop_tx_ack() (called from stop_current_operation_inner) also called receive_done(), queuing the same frame twice. Every TX during frame reception doubled the queue entries. Fix aligns with the C driver pattern: 1. isr_handle_rx_done: only queue+notify when NO ACK needed. When ACK is required, defer queuing until ACK phase completes (frame is safe in RX_BUFFER during TX_ACK since hardware isn't receiving). 2. isr_handle_ack_tx_done: now calls receive_done() to queue the deferred frame, then rx_available() to notify upper layer. 3. Phase 2 abort handlers (TxAckTimeout, TxAckCoexBreak, EnhackSecurityError): also queue the deferred frame. 4. next_operation_inner: sets state to Idle before dispatching, so stop_current_operation_inner sees Idle (not stale TxAck) and just calls set_cmd(Stop) without re-queuing via stop_tx_ack. 5. stop_tx_ack: keeps receive_done() for the interrupted case (e.g., ieee802154_transmit called directly while in TxAck). Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix frame corruption: copy RX_BUFFER immediately, not deferred Root cause of "Failed to process Parent Response: Parse" errors: With a single RX_BUFFER, deferring the copy to isr_handle_ack_tx_done allowed the hardware to overwrite the buffer before the copy happened. The C driver can defer because it has multiple RX buffers and advances the index in isr_handle_rx_done via next_rx_buffer(). Fix: copy RX_BUFFER immediately in isr_handle_rx_done (for ALL cases including ACK), but defer the rx_available() notification until ACK completes. Remove receive_done() from isr_handle_ack_tx_done, phase 2 abort handlers, stop_tx_ack, and stop_tx (TxEnhAck case) — frame is already safely copied. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix unused assignment warning: remove dead events &= line The `events &= !(Event::Timer0Overflow as u16)` at line 578 was a dead assignment — events is never read after this point in the ISR function. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Remove dead `events &= !()` assignments in ISR All `events &= !(Event::X as u16)` lines were dead assignments — each event bit is only checked once in the sequential ISR, so clearing it after processing has no effect. The only multi-check event is RxAbort (phase 1 and phase 2) which is intentionally NOT cleared between phases. Also removed `mut` from `let events` since it's no longer mutated. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Add CHANGELOG.md entry for IEEE 802.15.4 driver improvements Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix CHANGELOG.md: add missing PR number (#2) Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix ACK_FRAME data race: move from static mut into mutex-protected IeeeState (#4) * Initial plan * fix: move ACK_FRAME from static mut into mutex-protected IeeeState Move the `ack_frame` field into `IeeeState` (protected by `NonReentrantMutex`) to eliminate a data race between ISR context writes and thread context reads. All accesses now go through `STATE.with()`, ensuring interrupt-safety. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix Timer0Overflow ISR to stop timer and disable event on ACK timeout (#6) * Initial plan * fix: stop Timer0 and disable Timer0Overflow event in isr_handle_timer0_done On ACK timeout (Timer0Overflow), isr_handle_timer0_done now stops Timer0 and disables the Timer0Overflow event before calling tx_failed(), mirroring the behavior in isr_handle_ack_rx_done and the RxAckTimeout abort handler. This prevents repeated Timer0Overflow interrupts and repeated tx_failed callbacks. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix ieee802154: route TxEnhAck to stop_tx() instead of stop_tx_ack() (#8) * Initial plan * Fix ieee802154: route TxEnhAck to stop_tx() instead of stop_tx_ack() In stop_current_operation_inner(), TxEnhAck was incorrectly routed to stop_tx_ack(), making the TxEnhAck check in stop_tx() unreachable dead code. The C driver routes both Transmit and TxEnhAck to stop_tx(). This fix matches that behavior, making stop_tx()'s TxEnhAck handling reachable. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Document IEEE 802.15.4 CCA parameter breaking change in migration guide (#13) * Initial plan * Add 802.15.4 transmit methods CCA parameter migration guide Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix IEEE 802.15.4 examples to use CCA parameter Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Fix ieee802154 driver: RxAbortReason mask and TX abort state transition (#15) * Initial plan * Fix RxAbortReason::all() mask and needs_next_op for RX ACK errors in ieee802154 driver Issue 1: Expand mask from 0x00FF_FFFF to 0x03FF_FFFF to cover EdStop (bit 24) and EdCoexReject (bit 25). Issue 3: Set needs_next_op to true for RX ACK error cases so the state machine transitions properly after tx_failed(). Issue 2 was a false positive - clear_events already executes unconditionally. Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com> * Update TX examples with the new cca parameter; fix CHANGELOG * Address code review feedback --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: ivmarkov <2607589+ivmarkov@users.noreply.github.com>
Examples
This directory contains a number of binary applications demonstrating the use of various hardware peripherals found within the ESP32 family of devices from Espressif.
Each device has its own unique set of peripherals, and as such not every example will run on every device. We recommend building and flashing the examples using the xtask method shown below (no need to install any additional external tools), which will greatly simplify the process.
To check if a device is compatible with a given example, check the features in the Cargo.toml file for the example application, which will include a feature for each supported device.
For more information regarding the examples, refer to the README.md file in any of the subdirectories within the examples/ directory.
Building Examples
You can build all examples for a given device using the build examples subcommand:
cargo xtask build examples --chip esp32 all
Or build a single example with:
cargo xtask build examples --chip esp32c6 hello_world
Running Examples
You can also build and then subsequently flash and run an example using the run example subcommand. With a target device connected to your host system, run:
cargo xtask run example embassy_hello_world --chip=esp32c6
Again, note that we must specify which package to build the example from, plus which example to build and flash to the target device.
Adding Examples
If you are contributing to esp-hal and would like to add an example, the process is generally the same as any other project. The Cargo.toml file should include a feature for each supported chip, which itself should enable any dependency's features required for the given chip.
Another thing to be aware of is the GPIO pins being used. We have tried to use pins available the DevKit-C boards from Espressif, however this is being done on a best-effort basis.
In general, the following GPIO are recommended for use, though be conscious of whether certain pins are used for UART, strapping pins, etc. on some devices:
- GPIO0
- GPIO1
- GPIO2
- GPIO3
- GPIO4
- GPIO5
- GPIO8
- GPIO9
- GPIO10