Skip to content

soc: espressif: load all LP-SRAM data sections under MCUboot - #117359

Open
mdrinnan wants to merge 1 commit into
zephyrproject-rtos:mainfrom
mdrinnan:soc-espressif-lp-load-descriptors
Open

soc: espressif: load all LP-SRAM data sections under MCUboot#117359
mdrinnan wants to merge 1 commit into
zephyrproject-rtos:mainfrom
mdrinnan:soc-espressif-lp-load-descriptors

Conversation

@mdrinnan

@mdrinnan mdrinnan commented Aug 25, 2026

Copy link
Copy Markdown

Bug Introduction

An ESP32-C6 application chainloaded by MCUboot draws 7x more deep-sleep current than the same application booted directly.
This fix is for SoCs whose LP sections all live in one memory region — esp32c3, esp32c5, esp32c6, esp32h2, esp32p4.
A reproducer is available here: https://github.com/mdrinnan/zephyr-esp32-lp-sram-repro

Root cause

LP-SRAM sections are not all loaded. The Espressif SoC linker scripts emit an esp_image_load_header_t (.metadata) that the MCUboot Espressif port uses to copy the application into RAM. Its two LP descriptors cover only two of the four loadable LP-SRAM sections:

section collects described by
.rtc.text wake-stub code LP_IRAM
.rtc.force_fast RTC_FAST_ATTR nothing
.rtc.data RTC_DATA_ATTR, RTC_RODATA_ATTR LP_DATA
.rtc.force_slow RTC_SLOW_ATTR nothing

.rtc.data is empty in any application not using those attributes, so LP_DATA is typically a zero-length descriptor while the sections that do need loading are described by nothing. The bytes are in the flashed image, they are simply never copied. Direct boot is unaffected — the ROM loader uses the esptool segment list — which is why this presents as "MCUboot costs current".

On ESP32-C6 the uninitialised statics include s_sleep_sub_mode_ref_cnt[] from esp_hw_support/sleep_modes.c. Garbage counts make get_sleep_flags() report the ADC/TSEN monitor, ultra-low and RTC-fast-uses-XTAL sub-modes active, so esp_deep_sleep_start() keeps the LP peripheral domain powered and skips sar_periph_ctrl_power_disable(). This is the ESP32-C6 instance of discussion #106900, which reports the same MCUboot-dependent regression on ESP32-S3.

Fix

Two changes per linker script, both required:

  1. LP_DATA descriptor spans .rtc.force_fast through .rtc.force_slow instead of the usually-empty .rtc.data.
  2. .rtc.force_slow moves ahead of the NOLOAD .rtc.bss / .rtc_noinit sections, so the four loadable LP sections are adjacent in both VMA and LMA.

Change 2 is not cosmetic. With only change 1, a non-empty .rtc_noinit or .rtc.bss pushes .rtc.force_slow past the descriptor's VMA span while the LMAs stay contiguous, so the loader writes .rtc.force_slow to the wrong address and overwrites .rtc.bss / .rtc_noinit.

Putting the retained data in LP_DATA rather than LP_IRAM is deliberate: the MCUboot port loads LP_DATA only when reset_reason != RESET_REASON_CORE_DEEP_SLEEP and loads LP_IRAM unconditionally, so retained sections must keep the conditional path.

Scope

Fixed: the SoCs whose LP sections all live in one memory region — esp32c3, esp32c5, esp32c6, esp32h2, esp32p4.

Deliberately not fixed:

SoC reason
esp32s3 LP sections split across rtc_iram_seg / rtc_slow_seg. Both descriptors fit, but .rtc.force_fast would land in the unconditionally-loaded LP_IRAM descriptor and lose deep-sleep retention. Needs a decision on intended behaviour.
esp32, esp32s2 Three distinct regions against two descriptors. Needs a third descriptor — it fits in _reserved[4], but requires a coordinated MCUboot change.

esp32c2 and the default_appcpu.ld variants emit no LP descriptors and are unaffected.

Testing

Build (hardware-free, all five targets). A test application populates every LP section using only section attributes, and a script reads the built ELF's .metadata and asserts that each loadable LP section is covered by exactly one descriptor with VMA offset equal to LMA offset — that second assertion is what catches the interleaved-NOLOAD case; a coverage-only check passes the incomplete fix. Built --sysbuild with SB_CONFIG_BOOTLOADER_MCUBOOT=y on esp32c3_devkitm, esp32c5_devkitc, esp32c6_devkitc, esp32h2_devkitm, esp32p4_function_ev_board against main at b6a5e6e8aa9: before 5/5 fail (.rtc.force_fast and .rtc.force_slow covered by 0 descriptors on every target), after 5/5 pass.

Hardware (ESP32-C6). Seeed XIAO ESP32-C6, PPK2 into the BAT pad at 3700 mV, time-averaged over deep-sleep gaps. Absolutes are board-specific; the relative change is the point. No application-side mitigation compiled in, so the delta is attributable to the linker fix alone.

build deep-sleep floor
release + MCUboot 92.94 µA
release + MCUboot, sub-mode counts re-zeroed from the application 17.15 µA
release, no MCUboot 12.56 µA
release + MCUboot + this patch 11.84 µA

A reproducer is available here: https://github.com/mdrinnan/zephyr-esp32-lp-sram-repro

The .metadata load header emitted for MCUboot builds describes only two
of the four loadable LP-SRAM sections. Its LP_DATA descriptor points at
.rtc.data, which collects RTC_DATA_ATTR / RTC_RODATA_ATTR and is empty in
any application that does not use those attributes, while .rtc.force_fast
and .rtc.force_slow are described by no descriptor at all. Both are
present in the flashed image but never copied, so the zero-initialised
ESP-IDF statics they hold come up as whatever LP-SRAM last contained.

On ESP32-C6 that leaves s_sleep_sub_mode_ref_cnt[] (sleep_modes.c) with
garbage reference counts. get_sleep_flags() then reports the ADC/TSEN
monitor, ultra-low and RTC-fast-uses-XTAL sub-modes as active, so
esp_deep_sleep_start() keeps the LP peripheral domain powered and skips
sar_periph_ctrl_power_disable(). On a Seeed XIAO ESP32-C6 this measured
at roughly eight times the deep-sleep current of the same application
booted without MCUboot. Absolute figures depend on the board and the
measurement setup; see the pull request for the numbers and caveats.

Point the LP_DATA descriptor at .rtc.force_fast through .rtc.force_slow,
and move .rtc.force_slow ahead of the NOLOAD .rtc.bss / .rtc_noinit
sections so the four loadable LP sections are adjacent in both VMA and
LMA. The reorder is required, not cosmetic: correcting the descriptor
alone leaves .rtc.force_slow displaced by any non-empty NOLOAD section,
so the loader writes it to the wrong address and overwrites
.rtc.bss / .rtc_noinit in the process.

Applies to the SoCs whose LP sections all share one memory region.
esp32, esp32s2 and esp32s3 split them across two or three regions and
need a separate change; see the pull request for details.

Signed-off-by: Martin Drinnan <mrdrinnan@gmail.com>
@github-actions

Copy link
Copy Markdown

Hello @mdrinnan, and thank you very much for your first Pull Request (PR) to the Zephyr Project!

All PRs must pass our Continuous Integration (CI) pipeline before merging. When the pipeline run for your PR completes, you are expected to investigate the results, fix any errors, and update your PR for a fresh round of review.

Since this is your first contribution, a project community member must manually approve your CI run (this helps us avoid abuse of our CI system). A bot should assign some reviewers who can start the run for you soon.

As a heads-up, you will probably have to update your PR to fix CI issues and address review feedback in order to get it ready for merge. Some key rules for updating your PR are:

  • do amend problematic commits on your computer and force push the fixed commits into your PR branch on GitHub
  • don't push new commits just to fix problems in existing PR commits (amend your commits instead)
  • don't close your PR and open an updated one unless reviewers specifically request it (force push to your branch instead)
  • do rebase your PR branch onto our main branch and force push to this PR to resolve merge conflicts
  • don't merge our main branch into your PR branch to fix merge conflicts

Also, see:

If you are stuck or need help, you can join us on Discord and ask questions; many community members try to help new contributors there 😊. Try to pick a Discord channel that is associated with the technical details of your request. If you're not sure, use the #general channel.

@mdrinnan
mdrinnan force-pushed the soc-espressif-lp-load-descriptors branch from b2ed368 to fe2f5c5 Compare August 25, 2026 21:41
@mdrinnan
mdrinnan marked this pull request as ready for review August 25, 2026 22:00
@mdrinnan
mdrinnan force-pushed the soc-espressif-lp-load-descriptors branch from fe2f5c5 to f5ee7f5 Compare August 25, 2026 22:25
@sylvioalves

Copy link
Copy Markdown
Contributor

@mdrinnan very nice catch, thanks for this!

@sylvioalves

Copy link
Copy Markdown
Contributor
  1. Current approach will break RTC_SLOW_ATTR as it needs VMA/LMA offsset.
    Please align all 4 RTC entries with ALIGN_WITH_INPUT in C3, C5, C6, H2 and P4:
  .rtc.text : ALIGN_WITH_INPUT

  .rtc.force_fast : ALIGN_WITH_INPUT

  .rtc.data : ALIGN_WITH_INPUT

  .rtc.force_slow : ALIGN_WITH_INPUT`

@sylvioalves

Copy link
Copy Markdown
Contributor

Since runtime breaking is silent, perhaps we could add an ASSERT in all those linkers like:

  ASSERT((LOADADDR(.rtc.force_slow) + SIZEOF(.rtc.force_slow) - LOADADDR(.rtc.force_fast)) ==
         (ADDR(.rtc.force_slow) + SIZEOF(.rtc.force_slow) - ADDR(.rtc.force_fast)),
         "LP-SRAM loadable sections must be contiguous with equal VMA and LMA offsets")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants