ivmarkov a3bdedf108
Update the 802.15.4 driver to match the ESP-IDF C one w.r.t. behavior (#5006)
* 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>
2026-02-19 14:21:04 +00:00
..

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