Skip to content

feat: LP-Core ULP support for Arduino-only projects#425

Closed
antoinecellerier wants to merge 1 commit into
pioarduino:mainfrom
antoinecellerier:feature/lp-core-ulp-arduino
Closed

feat: LP-Core ULP support for Arduino-only projects#425
antoinecellerier wants to merge 1 commit into
pioarduino:mainfrom
antoinecellerier:feature/lp-core-ulp-arduino

Conversation

@antoinecellerier
Copy link
Copy Markdown

@antoinecellerier antoinecellerier commented Mar 11, 2026

Summary

  • Adds native LP-Core ULP coprocessor support for Arduino-only builds (ESP32-C5, ESP32-C6, ESP32-P4)
  • Zero-config: users just create a ulp/ directory with C or assembly sources — the platform handles sdkconfig injection, lib recompilation, ULP compilation, binary embedding, and linker script patching automatically
  • Includes arduino-ulp-blink example project

How it works

  1. Auto-configuration (arduino.py): detects ulp/ on LP-Core MCUs, injects custom_sdkconfig entries to enable ULP in lib recompilation, removes components that break the recompile
  2. ULP build (ulp_lp_core.py): delegates to IDF's own ULP CMake modules — cmake configure → cmake build → assembly embedding via data_file_embed_asm.cmake
  3. Lib recompilation fixes (espidf.py, arduino.py): copies sections.ld during recompilation, prevents cross-env flip-flopping, fixes build_flags leakage into lib recompilation
  4. Package management (platform.py): force-installs framework-espidf, CMake, and ninja when ulp/ sources are detected

Alternative approach: SCons-native builder

An earlier version of this feature used a fully SCons-native approach (no CMake delegation) and is preserved on the feature/lp-core-ulp-scons-native branch.

CMake delegation (this PR) SCons-native
Maintenance Leverages IDF's ULP CMake modules — toolchain flags, linker scripts, and mapgen stay in sync with IDF updates automatically Must manually track IDF changes to compiler flags, linker scripts, runtime sources
Code size ~450 lines in ulp_lp_core.py ~710 lines
Dependencies Requires CMake + Ninja at build time (auto-installed) No extra build tools beyond the RISC-V toolchain
Build speed CMake configure step adds ~1-2s on first build (cached afterwards) Slightly faster cold builds (no CMake overhead)
Debuggability CMake errors can be harder to trace Fully visible in SCons build log

The CMake approach was chosen for long-term maintainability — IDF's ULP build system is a moving target and delegating to their modules avoids fragile flag/path mirroring.

Test plan

  • Clean build on ESP32-C6 with ulp/ directory — verify one-time lib recompilation + ULP binary embedded
  • Incremental build after ULP source change — verify rebuild triggers correctly
  • Multi-env project (e.g. ESP32 + ESP32-C6) — verify no cross-env flip-flopping
  • Build without ulp/ directory on LP-Core MCU — verify no side effects
  • Build with ulp/ directory on non-LP-Core MCU (e.g. ESP32-S3) — verify silent skip
  • IDE IntelliSense — verify ULP headers resolve after index rebuild with correct env

Use of AI

This was built using Opus 4.6, GPT 5.3-Code and Gemini 3 Pro on GitHub Copilot Cli & Claude Code. I do not have deep understanding of ESP-IDF, Arduino or PlatformIO code bases but the changes seem to make sense to me and the code looks reasonable. Please do let me know if it's garbage.

I've tested this on a Seeed Xiao ESP32C6 device both with the example project and a personal project using timer based ULP wake-up from deep sleep, and have validated with a Nordic Semiconductor PPK2 that ULP was working as expected.

Summary by CodeRabbit

  • New Features

    • LP-Core Ultra-Low-Power coprocessor support for Arduino-only projects on ESP32-C5, ESP32-C6, and ESP32-P4 with automatic detection, build integration, and embedding of ULP binaries.
  • Documentation

    • Added LP-Core ULP guide with quick-start, configuration notes, example platform config, and references.
    • New Arduino-only ULP blink example demonstrating dual-core behavior, build steps, and expected output.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Adds LP‑Core ULP coprocessor support for Arduino-only projects on ESP32‑C5/C6/P4 by auto-detecting ulp/ sources, generating and building an isolated ULP CMake/Ninja build, embedding the ULP binary, patching LP SRAM layout, and integrating resulting objects into the SCons Arduino build.

Changes

Cohort / File(s) Summary
Docs & Examples
README.md, examples/arduino-ulp-blink/README.md, examples/arduino-ulp-blink/platformio.ini, examples/arduino-ulp-blink/.gitignore
Adds LP-Core ULP documentation, quick-start guidance, example PlatformIO config, and example project ignore rules.
Arduino example code
examples/arduino-ulp-blink/src/main.cpp, examples/arduino-ulp-blink/ulp/blink.c
New Arduino demo that loads/runs an LP-Core ULP program and an ulp/ blink C implementation.
Arduino build integration
builder/frameworks/arduino.py
Auto-detects UL P sources for LP-Core MCUs; injects sdkconfig entries, component_remove and lib_ignore hooks; invokes ULP SConscript when needed.
ULP LP‑Core build script
builder/frameworks/ulp_lp_core.py
New comprehensive integration: validates frameworks/libs, generates sdkconfig.cmake, configures and runs CMake/Ninja to build ULP, embeds binary, patches memory.ld, and wires targets into SCons.
ESP‑IDF framework tweaks
builder/frameworks/espidf.py
Preserves BUILD_FLAGS across Arduino lib recompile, copies sections.ld when present, and documents sdkconfig handling to avoid cross-env interference.
Platform/package selection
platform.py
Disables/installs ESP‑IDF packages conditionally when LP‑Core ULP sources exist for supported MCUs to enable the ULP workflow.

Sequence Diagram(s)

sequenceDiagram
    participant ArduinoBuild as Arduino Build System
    participant ULPConfig as ULP Auto-Config
    participant FrameworkCheck as Framework/Library Validation
    participant CMakeBuild as CMake/Ninja ULP Build
    participant Assembly as Binary Embedding
    participant MemoryPatch as Memory Layout Patching
    participant SConsGraph as SCons Build Graph

    ArduinoBuild->>ULPConfig: Detect `ulp/` directory with sources
    ULPConfig->>ULPConfig: Match MCU (esp32c5/c6/p4)
    ULPConfig->>FrameworkCheck: Inject sdkconfig & validate ESP‑IDF/libs
    FrameworkCheck->>CMakeBuild: Provide sdkconfig.cmake and include paths
    CMakeBuild->>CMakeBuild: Configure & compile ULP via CMake/Ninja
    CMakeBuild->>Assembly: Produce ulp_main.bin
    Assembly->>Assembly: Generate ulp_main.bin.S (embedded assembly)
    Assembly->>MemoryPatch: Provide size for LP SRAM reservation
    MemoryPatch->>SConsGraph: Emit patched memory.ld and UL P objects
    SConsGraph->>ArduinoBuild: Integrate ulp_main.o into main build
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I found a tiny core that blinks at night,
CMake hums, Ninja chips in, Arduino shines bright,
Bits tucked in memory, LP SRAM kept snug,
ulp/ sprang to life with a rabbit-sized hug,
Hop — the coprocessor blinks; the build is snug!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: LP-Core ULP support for Arduino-only projects' clearly and specifically describes the main change—adding LP-Core ULP coprocessor support for Arduino-only builds. It is concise, avoids vague terms, and aligns with the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@antoinecellerier
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@antoinecellerier antoinecellerier marked this pull request as ready for review March 11, 2026 15:13
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
builder/frameworks/arduino.py (1)

1074-1076: Redundant Path construction.

_ulp_dir is already defined at line 574 with the same value. Consider reusing it:

♻️ Suggested refactor
     # LP-Core ULP support for Arduino-only builds
-    if _has_ulp_sources(Path(env.subst("$PROJECT_DIR")) / "ulp"):
+    if _has_ulp_sources(_ulp_dir):
         SConscript("ulp_lp_core.py", exports="env")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/arduino.py` around lines 1074 - 1076, The code recomputes
the ULP directory instead of reusing the previously defined _ulp_dir; update the
conditional to call _has_ulp_sources(_ulp_dir) and pass
SConscript("ulp_lp_core.py", exports="env") unchanged so you don't rebuild
Path(env.subst("$PROJECT_DIR")) / "ulp"; locate the block using _has_ulp_sources
and SConscript and replace the Path(...) expression with the existing _ulp_dir
variable to remove redundancy.
examples/arduino-ulp-blink/src/main.cpp (1)

14-14: Consider ULP variable naming convention (optional).

In ulp/blink.c, the variable is named ulp_led_state, which becomes ulp_ulp_led_state after the ULP build system adds its ulp_ prefix. The ESP-IDF convention is to name ULP variables without the ulp_ prefix (e.g., led_stateulp_led_state).

This works correctly as-is, but aligning with the convention would avoid the double prefix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/arduino-ulp-blink/src/main.cpp` at line 14, The ULP variable in
ulp/blink.c is named ulp_led_state which the ULP build system then prefixes
again to produce ulp_ulp_led_state; rename the symbol in ulp/blink.c to
led_state (or another name without an ulp_ prefix) so the build-time prefix
produces the expected ulp_led_state; update any references to ulp_led_state in
the C file (and any host-side references if present) to the new unprefixed name
to avoid the double prefix.
platform.py (1)

595-605: Unify LP-Core ULP detection with the toolchain gate.

These branches and builder/frameworks/ulp_lp_core.py now use a project-root, source-based check, but _configure_mcu_toolchains() still installs the ULP toolchains off Path("ulp").is_dir(). That leaves package selection and toolchain selection on different rules. Please route all three call sites through one helper.

Also applies to: 778-785

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform.py` around lines 595 - 605, Extract the ULP detection logic into a
single helper (e.g., function named project_has_ulp_sources or uses_project_ulp)
that encapsulates the current check: resolve ProjectConfig.get_instance().path
parent/"ulp", ensure the directory exists, and return True if any file under it
has suffix in (".c", ".S", ".s") using Path.rglob and is_file(); then replace
the duplicated checks in platform.py (the block that sets
self.packages["framework-espidf"]["optional"] = False and the other branch at
lines ~778-785) and the call site in _configure_mcu_toolchains() to call this
helper so package selection, builder/frameworks/ulp_lp_core.py usage, and
toolchain installation all use the same project-root, source-based rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@builder/frameworks/arduino.py`:
- Around line 574-624: The ULP auto-config block has inconsistent indentation;
normalize all nested lines under the if that checks mcu in _lp_core_mcus and
_has_ulp_sources(_ulp_dir) to use 4-space indentation so Python block scoping is
correct. Update the entire block that manipulates _ulp_sdkconfig_entries,
entry_custom_sdkconfig, flag_custom_sdkconfig, _ulp_component_remove,
existing_removes/new_removes, flag_custom_component_remove, _ulp_lib_ignore,
existing_ignores/new_ignores, flag_lib_ignore and the final print to align with
4-space indentation and preserve the same logic and calls to config.set and
env.GetProjectOption.

In `@builder/frameworks/ulp_lp_core.py`:
- Around line 121-132: Update get_sdkconfig_value to prefer parsing the actual
SDKCONFIG header used by the ULP build (e.g., ulp/sdkconfig.h or the SDKCONFIG_H
selected by the build) before falling back to
env.GetProjectOption("custom_sdkconfig") or the hard-coded default: if that file
exists, read it and extract the requested key (such as
CONFIG_ULP_COPROC_RESERVE_MEM) and return its integer value; apply the same
change to the other call sites that compute ULP_RESERVE_MEM (references:
get_sdkconfig_value and the code that reads
CONFIG_ULP_COPROC_RESERVE_MEM/ULP_RESERVE_MEM in this module) so the reserved
SRAM comes from the same SDKCONFIG_H the ULP build uses.

In `@examples/arduino-ulp-blink/README.md`:
- Around line 31-38: The fenced code block showing runtime output ("Starting ULP
blink program..." through "ULP led_state: 1") needs a language specifier; edit
the README.md fenced block containing that output and change the opening fence
from ``` to ```text (or ```console) so syntax highlighters treat it as plain
text/console output.

---

Nitpick comments:
In `@builder/frameworks/arduino.py`:
- Around line 1074-1076: The code recomputes the ULP directory instead of
reusing the previously defined _ulp_dir; update the conditional to call
_has_ulp_sources(_ulp_dir) and pass SConscript("ulp_lp_core.py", exports="env")
unchanged so you don't rebuild Path(env.subst("$PROJECT_DIR")) / "ulp"; locate
the block using _has_ulp_sources and SConscript and replace the Path(...)
expression with the existing _ulp_dir variable to remove redundancy.

In `@examples/arduino-ulp-blink/src/main.cpp`:
- Line 14: The ULP variable in ulp/blink.c is named ulp_led_state which the ULP
build system then prefixes again to produce ulp_ulp_led_state; rename the symbol
in ulp/blink.c to led_state (or another name without an ulp_ prefix) so the
build-time prefix produces the expected ulp_led_state; update any references to
ulp_led_state in the C file (and any host-side references if present) to the new
unprefixed name to avoid the double prefix.

In `@platform.py`:
- Around line 595-605: Extract the ULP detection logic into a single helper
(e.g., function named project_has_ulp_sources or uses_project_ulp) that
encapsulates the current check: resolve ProjectConfig.get_instance().path
parent/"ulp", ensure the directory exists, and return True if any file under it
has suffix in (".c", ".S", ".s") using Path.rglob and is_file(); then replace
the duplicated checks in platform.py (the block that sets
self.packages["framework-espidf"]["optional"] = False and the other branch at
lines ~778-785) and the call site in _configure_mcu_toolchains() to call this
helper so package selection, builder/frameworks/ulp_lp_core.py usage, and
toolchain installation all use the same project-root, source-based rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1783da6e-0661-494d-bb75-36c70222e438

📥 Commits

Reviewing files that changed from the base of the PR and between a9fac07 and b60479b.

📒 Files selected for processing (10)
  • README.md
  • builder/frameworks/arduino.py
  • builder/frameworks/espidf.py
  • builder/frameworks/ulp_lp_core.py
  • examples/arduino-ulp-blink/.gitignore
  • examples/arduino-ulp-blink/README.md
  • examples/arduino-ulp-blink/platformio.ini
  • examples/arduino-ulp-blink/src/main.cpp
  • examples/arduino-ulp-blink/ulp/blink.c
  • platform.py

Comment thread builder/frameworks/arduino.py Outdated
Comment thread builder/frameworks/ulp_lp_core.py
Comment thread examples/arduino-ulp-blink/README.md Outdated
Add native LP-Core ULP coprocessor support for Arduino framework builds
on ESP32-C5, ESP32-C6, and ESP32-P4. Users only need a ulp/ directory
with C or assembly sources — the platform handles everything else.

Build pipeline (ulp_lp_core.py):
- Delegates to IDF's own ULP CMake modules for compilation
- Generates ulp_main.h (symbol map) and ulp_main.bin.S (binary embedding)
- Patches memory.ld to reserve LP SRAM for the ULP binary
- Links libulp.a for ulp_lp_core_load_binary / ulp_lp_core_run APIs
- Uses recompiled sdkconfig.h from libs, or user-provided ulp/sdkconfig.h

Auto-configuration (arduino.py):
- Detects ulp/ directory on LP-Core MCUs and injects custom_sdkconfig
  entries (CONFIG_ULP_COPROC_ENABLED, TYPE_LP_CORE, RESERVE_MEM=8192)
- Removes components that break lib recompilation (esp_insights, etc.)
- Sets lib_ignore for their Arduino wrapper libraries

Lib recompilation fixes (espidf.py, arduino.py):
- Copy sections.ld during recompilation (fixes orphan section errors)
- Check chip-specific sdkconfig.orig instead of root sdkconfig to
  prevent cross-env flip-flopping between different MCU environments
- ProcessUnFlags on saved build_flags to prevent user defines from
  leaking into lib recompilation

Package management (platform.py):
- Force-install framework-espidf when ulp/ sources detected
- Install CMake and ninja for Arduino-only ULP builds

Includes arduino-ulp-blink example project for ESP32-C6.

Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@antoinecellerier antoinecellerier force-pushed the feature/lp-core-ulp-arduino branch from b60479b to 4f084d9 Compare March 11, 2026 15:27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
builder/frameworks/ulp_lp_core.py (2)

129-151: Fallback chain is correct; consider narrowing exception types.

The priority order (SDKCONFIG_H → custom_sdkconfig → default) correctly addresses the past review concern. The broad Exception catches work but could be tightened to (IOError, ValueError, IndexError) for more precise error handling—though the current approach is pragmatically acceptable for a build script.

♻️ Optional: Narrow exception types
     try:
         for line in SDKCONFIG_H.read_text().splitlines():
             line = line.strip()
             if line.startswith("#define %s " % key):
                 return int(line.split(None, 2)[2])
-    except Exception:
+    except (IOError, ValueError, IndexError):
         pass
     # Fall back to platformio.ini custom_sdkconfig (Kconfig-style key=value)
     try:
         custom = env.GetProjectOption("custom_sdkconfig", "")
         for line in custom.splitlines():
             line = line.strip()
             if "://" in line:
                 continue
             if line.startswith(key + "="):
                 return int(line.split("=", 1)[1])
-    except Exception:
+    except (ValueError, IndexError):
         pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/ulp_lp_core.py` around lines 129 - 151, The try/except
blocks in get_sdkconfig_value are too broad; replace the generic except
Exception handlers with narrower exception tuples to avoid masking unrelated
errors: catch (IOError, OSError, ValueError, IndexError) when reading/parsing
SDKCONFIG_H and when parsing env.GetProjectOption("custom_sdkconfig") (or the
appropriate subset like (IOError, ValueError, IndexError) if OSError isn’t
needed), keeping the same fallback logic and return default otherwise.

373-408: Consider warning if memory.ld is missing.

The silent return on line 378 when memory.ld doesn't exist could mask configuration issues. Since the libs validation already passed, a missing memory.ld would be unexpected.

♻️ Optional: Add warning for missing memory.ld
     if not src_ld.exists():
+        print("Warning: %s not found — LP SRAM patching skipped" % src_ld)
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/frameworks/ulp_lp_core.py` around lines 373 - 408, In
patch_memory_ld, do not silently return when the expected memory.ld (src_ld) is
missing: detect the missing file (src_ld.exists() is False) and emit a WARNING
or stderr message that includes the path (src_ld) and context (e.g., that
LP-Core memory reservation will be skipped) before returning; keep the early
return but add the log to aid debugging and reference ULP_RESERVE_MEM,
FW_LIBS_DIR, and the function name patch_memory_ld so maintainers can find and
verify the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@builder/frameworks/ulp_lp_core.py`:
- Around line 129-151: The try/except blocks in get_sdkconfig_value are too
broad; replace the generic except Exception handlers with narrower exception
tuples to avoid masking unrelated errors: catch (IOError, OSError, ValueError,
IndexError) when reading/parsing SDKCONFIG_H and when parsing
env.GetProjectOption("custom_sdkconfig") (or the appropriate subset like
(IOError, ValueError, IndexError) if OSError isn’t needed), keeping the same
fallback logic and return default otherwise.
- Around line 373-408: In patch_memory_ld, do not silently return when the
expected memory.ld (src_ld) is missing: detect the missing file (src_ld.exists()
is False) and emit a WARNING or stderr message that includes the path (src_ld)
and context (e.g., that LP-Core memory reservation will be skipped) before
returning; keep the early return but add the log to aid debugging and reference
ULP_RESERVE_MEM, FW_LIBS_DIR, and the function name patch_memory_ld so
maintainers can find and verify the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bc583a6-9907-4147-a95d-77e746745bf9

📥 Commits

Reviewing files that changed from the base of the PR and between b60479b and 4f084d9.

📒 Files selected for processing (10)
  • README.md
  • builder/frameworks/arduino.py
  • builder/frameworks/espidf.py
  • builder/frameworks/ulp_lp_core.py
  • examples/arduino-ulp-blink/.gitignore
  • examples/arduino-ulp-blink/README.md
  • examples/arduino-ulp-blink/platformio.ini
  • examples/arduino-ulp-blink/src/main.cpp
  • examples/arduino-ulp-blink/ulp/blink.c
  • platform.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • platform.py
  • examples/arduino-ulp-blink/.gitignore
  • README.md
  • builder/frameworks/espidf.py
  • examples/arduino-ulp-blink/platformio.ini

@Jason2866
Copy link
Copy Markdown

@antoinecellerier First thank you for providing a PR to pioarduino. But i will not merge.
I am a big fan of AI coding too, but this is an example how AI clutters code.
There are hard coded excludes of components -> builder/frameworks/arduino.py. Thats a no go.
The COMPONENT_INCLUDES in builder/frameworks/ulp_lp_core.py is a fragile approach. Can change any time.
Next def patch_memory_ld() is typical for AI and the next no go. Needs to be solved without patching.

Honestly i dont get the benefit, since the PR needs and uses HybridCompile and injects there the needed changes for ULP compiling. So it is not a simple Arduino compile run. HybridCompile is already complex. Adding another "thing" makes maintain work really hard. Overall to much complex code for supporting ULP.

For using ULP with Arduino there is already the variant "compile Arduino as a component of IDF". There are no changes at all in the platform needed. See example https://github.com/pioarduino/platform-espressif32/tree/main/examples/espidf-arduino-C6-ULP-blink

@antoinecellerier
Copy link
Copy Markdown
Author

Fair enough. Thanks for taking time to provide feedback. The intent was to provide an easier out of the box experience for people wanting to get going with ULP but I do understand the maintainability concerns.

@Jason2866 Jason2866 closed this Mar 11, 2026
antoinecellerier added a commit to antoinecellerier/platform-espressif32 that referenced this pull request Mar 22, 2026
- Replace 23 hardcoded COMPONENT_INCLUDES paths with discover_component_includes()
  that scans framework-arduinoespressif32-libs for include/register/mcu dirs
- Replace patch_memory_ld() with validate_memory_ld() since lib recompile
  already produces correct linker script via CONFIG_ULP_COPROC_ENABLED
- Remove unused 'import re'

Addresses PR pioarduino#425 feedback about fragile hardcoded paths and patching.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@antoinecellerier
Copy link
Copy Markdown
Author

Hi @Jason2866, thanks again for the detailed feedback. I've been iterating on the branch to address the specific concerns you raised:

COMPONENT_INCLUDES fragility — Replaced the 23 hardcoded paths with discover_component_includes() which auto-scans the libs package for include/, register/, and MCU-named directories. No longer breaks when IDF reorganizes headers.

patch_memory_ld() — Eliminated entirely. Since the lib recompile already produces a correct memory.ld (IDF's template uses #if CONFIG_ULP_COPROC_ENABLED), we just validate the offset exists instead of patching.

Hardcoded component excludes in arduino.py — These use the same custom_component_remove mechanism the existing C6-ULP-blink example uses. The same set of components are excluded there — they're unrelated to ULP itself, but needed for HybridCompile's lib recompilation to succeed.

I understand the concern about maintainability and added complexity to HybridCompile. The motivation is the user experience gap: the existing espidf-arduino-C6-ULP-blink example requires users to write 3 boilerplate files (CMakeLists.txt, src/CMakeLists.txt, sdkconfig.defaults) and know about framework = arduino, espidf. With this PR, users just drop .c files in ulp/ and build — zero config.

The branch is at https://github.com/antoinecellerier/platform-espressif32/tree/feature/lp-core-ulp-arduino with the improvements if you'd like to take another look. Totally fine if not — no reply expected. Thanks for maintaining pioarduino!

@Jason2866
Copy link
Copy Markdown

Jason2866 commented Mar 22, 2026

@antoinecellerier The newest version is much better! Still some stuff left which needs to be discussed / worked on.
First PR needs to be done against branch develop. Second please rename the new ulp script with Arduino or similar in name. No irritating with espidf flow / files. I do not like the define of ULP capable MCUs in different files. Please one place of truth. I am still reworking such things (everything was everywhere...)

There is a chance for a merge ;-)

EDIT: Why is not the existing ulp script used? Code seems duplicated.

@antoinecellerier
Copy link
Copy Markdown
Author

Thanks for the second look @Jason2866 ! I've pushed changes addressing each point:

Renamed to arduino_ulp.py — clearly distinct from the espidf ulp.py flow. TBD if that name is slightly misleading as it only addresses MCUs with LP-Core co-processors.

Single source of truth for MCU list — added builder/frameworks/ulp_configs.py with LP_CORE_MCUS and ULP_SOURCE_SUFFIXES. Imported by arduino_ulp.py, arduino.py, and platform.py — no duplication.

PR base — the branch is based on develop already. Note it seems I cannot change the target branch atm as the PR is closed. Should you re-open it that should be a trivial change.

Why not reuse ulp.py — ulp.py depends on Import("env sdk_config project_config app_includes idf_variant"), all provided by the full IDF CMake build in espidf.py. In Arduino-only builds those variables don't exist. Synthesizing them would need ~200 lines of fragile glue to fake the IDF CMake codemodel output, which defeats the purpose. The actual overlap is just 3 cmake invocations — straightforward boilerplate not worth extracting into a shared module.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants