Skip to content

arch/xtensa/espressif: initialize the HR Timer from the Wi-Fi init path - #19786

Merged
fdcavalcanti merged 1 commit into
apache:masterfrom
ricardgb:esp32s3-wifi-rt-timer-init
Aug 11, 2026
Merged

arch/xtensa/espressif: initialize the HR Timer from the Wi-Fi init path#19786
fdcavalcanti merged 1 commit into
apache:masterfrom
ricardgb:esp32s3-wifi-rt-timer-init

Conversation

@ricardgb

Copy link
Copy Markdown
Contributor

Summary

The Espressif Wi-Fi stack cannot work unless the esp_timer subsystem has been
initialized, but nothing in the Wi-Fi code does that — the initialization is
left to each board's bringup, which has to call esp_hr_timer_init() before
board_wlan_init(). Every in-tree ESP32/ESP32-S2/ESP32-S3 board with a Wi-Fi
defconfig happens to make that call, so in-tree configurations work today; any
board that does not (a new port, or a board file written by following another
subsystem's example) dies on the first RF enable, with a failure mode that gives
no diagnostic whatsoever.

This moves the initialization into the common Wi-Fi init path, where the
requirement actually originates, so the Wi-Fi stack no longer depends on board
code getting an undocumented ordering right.

The dependency

The requirement is invisible from the Wi-Fi sources. Bring-up runs:

board_wlan_init() -> esp_wlan_sta_initialize() -> esp_wlan_initialize()
  -> esp_wifi_initialize() -> esp_wifi_api_adapter_init()

and then, the first time the radio is powered up:

esp_phy_enable_wrapper()            (arch/{xtensa,risc-v}/src/<chip>/..._wifi_adapter.c)
  -> esp_phy_enable()               (esp-hal-3rdparty, components/esp_phy/src/phy_init.c)
    -> phy_track_pll_init()         (esp-hal-3rdparty, components/esp_phy/src/phy_common.c)
      -> ESP_ERROR_CHECK(esp_timer_create(...))
      -> ESP_ERROR_CHECK(esp_timer_start_periodic(...))

esp_timer_create()/esp_timer_start_periodic() return
ESP_ERR_INVALID_STATE while esp_timer is uninitialized. The HAL would
normally initialize itself from its esp_timer_init_os() startup hook, but that
hook is compiled out on NuttX (#ifndef __NuttX__ in
components/esp_timer/src/esp_timer.c), so the timer task and the timer ISR are
only ever created from NuttX's esp_hr_timer_init() -> esp_timer_init().
phy_track_pll_init() is reached on every target except the original ESP32
(#if !CONFIG_IDF_TARGET_ESP32 && !CONFIG_ESP_PHY_DISABLE_PLL_TRACK), and PLL
tracking is enabled in the NuttX sdkconfig.h for esp32s2/s3/c3/c6/h2.

The change

  • esp_wifi_api_adapter_init() (arch/xtensa/src/common/espressif/) now
    calls esp_hr_timer_init() before
    esp_wifi_init(), with a comment recording the phy_track_pll_init() ->
    esp_timer chain, since that constraint is not obvious from the call site.
    esp_hr_timer_init() is idempotent (it early-returns once the subsystem is
    up), so the boards that already initialize it during bringup are unaffected —
    the second call returns OK immediately.
  • ESPRESSIF_WIRELESS now selects ESPRESSIF_HR_TIMER explicitly.
    Today it inherits it only indirectly, through the deprecated
    ESP32_RT_TIMER / ESP32S2_RT_TIMER / ESP32S3_RT_TIMER symbols whose sole
    remaining job is to select ESPRESSIF_HR_TIMER. Since the timer adapter
    (esp_timer_adapter.c) is only compiled when ESPRESSIF_HR_TIMER=y, the
    dependency should be stated where the radio is enabled rather than routed
    through compatibility symbols. The RISC-V ESPRESSIF_WIRELESS already has
    this select.

No behavioral change for any in-tree defconfig: ESPRESSIF_HR_TIMER is already
y in every wireless configuration, and every in-tree Wi-Fi board already calls
esp_hr_timer_init() from bringup, so on those the added call is a no-op
early return.

How it was diagnosed

Found while bringing up an out-of-tree ESP32-S3 board whose bringup did not have
the call. The failure has no panic output at all and looks exactly like a CPU
lockup: the system tick stops, the console dies mid-line, and USB stays
enumerated but unresponsive — which is why it went unexplained for a month.

It was tracked down with ROM-level ets_printf() breadcrumbs along the whole
init path, plus a high-priority thread that busy-waits on ets_delay_us()
instead of sleeping:

  • the breadcrumb trail ends inside phy_track_pll_init() and never reaches the
    print immediately after it;
  • the busy-wait thread keeps printing while every sleep()-based thread stops
    waking — i.e. the tick is gone, the CPU is not.

Initializing the timer ahead of Wi-Fi init turns the same image into a working
one: the board associates to an AP, obtains a DHCP lease and serves telnet.

Testing

  • Silicon: validated on ESP32-S3 (240 MHz, no PSRAM, 16 MiB flash) — Wi-Fi
    associates, DHCP lease obtained, telnet session served. Without the
    initialization the same image locks up as described above.
  • Build: esp32s3-devkit:wifi (xtensa-esp32s3-elf) builds clean, no new
    warnings, links and images successfully.
  • tools/checkpatch.sh -g HEAD: all checks pass.
  • Scope is deliberately Xtensa-only. The RISC-V common-espressif tree has the
    same unenforced dependency, but nothing is broken there today: its
    ESPRESSIF_WIRELESS already selects both ESPRESSIF_HR_TIMER and
    RTC_DRIVER, and esp_rtc.c initializes the timer. An earlier draft of this
    patch mirrored the change there too; it was dropped rather than posted
    untested, since there is no RISC-V toolchain on this machine. Happy to add it
    if maintainers would prefer the common-source trees kept symmetric.

Disclosure

This root-cause analysis, patch, and hardware validation were performed by an AI
agent (Claude Code, operated and directed by the submitter), and the result was
reviewed by the submitter before posting.

The Espressif Wi-Fi stack cannot work unless the esp_timer subsystem has
been initialized, but nothing in the Wi-Fi code does that: it is left to
each board's bringup to call esp_hr_timer_init() first.  Any board that
does not happen to make that call dies on the first RF enable.

The dependency is not visible from the Wi-Fi sources.  The path is:

  board_wlan_init() -> esp_wlan_sta_initialize() -> esp_wlan_initialize()
    -> esp_wifi_initialize() -> esp_wifi_api_adapter_init()

and later, when the radio is first powered up:

  esp_phy_enable_wrapper() -> esp_phy_enable() (esp-hal-3rdparty,
  components/esp_phy/src/phy_init.c) -> phy_track_pll_init()
  (components/esp_phy/src/phy_common.c)

phy_track_pll_init() calls esp_timer_create() and
esp_timer_start_periodic() wrapped in ESP_ERROR_CHECK().  Both return
ESP_ERR_INVALID_STATE while esp_timer is uninitialized, because the HAL's
own esp_timer_init_os() startup hook is compiled out on NuttX
(#ifndef __NuttX__ in components/esp_timer/src/esp_timer.c), so the timer
task and the timer ISR only ever get created from NuttX's
esp_hr_timer_init() -> esp_timer_init().

Initialize the HR Timer at the top of esp_wifi_api_adapter_init(), where
the requirement actually originates.  esp_hr_timer_init() is idempotent
(it early-returns once the subsystem is up), so boards that already call
it during bringup are unaffected.  Also make ESPRESSIF_WIRELESS select
ESPRESSIF_HR_TIMER explicitly instead of inheriting it through the
deprecated ESP32{,S2,S3}_RT_TIMER symbols, so the timer adapter is
guaranteed to be built whenever the radio is.

This is deliberately limited to Xtensa.  The RISC-V common-espressif tree
has the same unenforced dependency, but nothing is broken there today: its
ESPRESSIF_WIRELESS already selects both ESPRESSIF_HR_TIMER and RTC_DRIVER,
and esp_rtc.c initializes the timer.  The mirror change can follow from
someone able to test it on RISC-V hardware.

This was diagnosed on an out-of-tree ESP32-S3 board whose bringup lacked
the call.  The failure gives no panic output at all and looks exactly like
a CPU lockup: the system tick stops, the console dies mid-line and USB
stays enumerated but unresponsive.  It was tracked down with ROM-level
ets_printf() breadcrumbs along the init path plus a high-priority thread
that busy-waits on ets_delay_us(): the breadcrumb trail ends inside
phy_track_pll_init() and never reaches the print after it, and the
busy-wait thread keeps printing while every sleep()-based thread stops
waking, showing the tick is gone.  Initializing the timer ahead of Wi-Fi
init makes the same image associate to an AP, obtain a DHCP lease and
serve telnet.  Validated on ESP32-S3 silicon (240 MHz, no PSRAM, 16 MiB
flash).

esp32s3-devkit:wifi builds clean with the change.

Signed-off-by: Ricard Rosson <ricard@groundbits.com>
Assisted-by: Claude Opus 5 (Claude Code)
@github-actions github-actions Bot added Arch: xtensa Issues related to the Xtensa architecture Size: S The size of the change in this PR is small labels Aug 11, 2026
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@fdcavalcanti

Copy link
Copy Markdown
Contributor

Nice catch!

@fdcavalcanti
fdcavalcanti merged commit 3d30272 into apache:master Aug 11, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: xtensa Issues related to the Xtensa architecture Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants