Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ pio run -t download_fatfs # Download and extract FatFS from device

See the [arduino-fatfs example](examples/arduino-fatfs/) for a complete working example.

## LP-Core ULP Coprocessor Support

pioarduino supports building LP-Core (Ultra Low Power) coprocessor programs directly from Arduino-only projects — no ESP-IDF CMake pipeline or custom build scripts required.

**Supported MCUs:** ESP32-C5, ESP32-C6, ESP32-P4

**Quick Start:**

1. Create a `ulp/` directory in your project root with your LP-Core C or assembly sources.
2. Build normally — the platform detects `ulp/` and compiles + embeds the ULP binary automatically.

```ini
[env:esp32c6]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
framework = arduino
board = esp32-c6-devkitc-1
```

The builder generates `ulp_main.h` (symbol map) and `ulp_main_bin.h` (binary declarations) in the build directory, available via `#include`. The first build triggers a one-time lib recompilation to enable ULP loader APIs. An optional `ulp/sdkconfig.h` can override the default ULP configuration (8 KB reserved memory).

See the [arduino-ulp-blink](examples/arduino-ulp-blink/) example for a complete working project. For ESP-IDF or hybrid (arduino + espidf) projects, the existing CMake-based ULP pipeline is used instead — see [espidf-ulp-lp](examples/espidf-ulp-lp/) and [espidf-arduino-C6-ULP-blink](examples/espidf-arduino-C6-ULP-blink/).

### Stable Arduino
currently espressif Arduino 3.3.7 and IDF v5.5.2+

Expand Down
82 changes: 80 additions & 2 deletions builder/frameworks/arduino.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,74 @@ def safe_remove_sdkconfig_files():
if board_sdkconfig:
flag_custom_sdkconfig = True

# Auto-configure ULP support when ulp/ directory with sources is present on an
# LP-Core MCU. Injects custom_sdkconfig entries to trigger lib recompilation
# with ULP loader functions, and removes components that break the recompile.
# Keep in sync with LP_CORE_MCUS and ULP_SOURCE_SUFFIXES in ulp_lp_core.py
_lp_core_mcus = ("esp32c5", "esp32c6", "esp32p4")
_ulp_source_suffixes = (".c", ".S", ".s")


def _has_ulp_sources(ulp_dir):
"""Check if a directory tree contains LP-Core source files."""
return ulp_dir.is_dir() and any(
f.suffix in _ulp_source_suffixes
for f in ulp_dir.rglob("*") if f.is_file()
)


_ulp_dir = Path(project_dir) / "ulp"
if mcu in _lp_core_mcus and _has_ulp_sources(_ulp_dir):
_ulp_sdkconfig_entries = [
"CONFIG_ULP_COPROC_ENABLED=y",
"CONFIG_ULP_COPROC_TYPE_LP_CORE=y",
# 8192 gives headroom beyond IDF's 4096 default. Users can
# override via custom_sdkconfig if they need more (or less).
"CONFIG_ULP_COPROC_RESERVE_MEM=8192",
]
for entry in _ulp_sdkconfig_entries:
key = entry.split("=")[0]
if key not in entry_custom_sdkconfig:
entry_custom_sdkconfig += "\n" + entry
flag_custom_sdkconfig = True
config.set(current_env_section, "custom_sdkconfig",
entry_custom_sdkconfig)

# Components that fail the lib-recompile build on most pioarduino
# targets. If future IDF versions fix these, they can be removed.
_ulp_component_remove = [
"espressif/esp_insights",
"espressif/esp_rainmaker",
"espressif/rmaker_common",
"espressif/esp_diag_data_store",
"espressif/esp_diagnostics",
]
existing_removes = env.GetProjectOption("custom_component_remove", "")
new_removes = []
for comp in _ulp_component_remove:
if comp not in existing_removes:
new_removes.append(comp)
if new_removes:
combined = existing_removes.strip()
if combined:
combined += "\n"
combined += "\n".join(new_removes)
config.set(current_env_section, "custom_component_remove", combined)
flag_custom_component_remove = True

_ulp_lib_ignore = ["RainMaker", "Insights"]
existing_ignores = env.GetProjectOption("lib_ignore", [])
if isinstance(existing_ignores, str):
existing_ignores = [existing_ignores]
new_ignores = [lib for lib in _ulp_lib_ignore
if lib not in existing_ignores]
if new_ignores:
config.set(current_env_section, "lib_ignore",
existing_ignores + new_ignores)
flag_lib_ignore = True

print("*** ULP auto-config: sdkconfig, component_remove, lib_ignore ***")

extra_flags_raw = board.get("build.extra_flags", [])
if isinstance(extra_flags_raw, list):
extra_flags = " ".join(extra_flags_raw).replace("-D", " ")
Expand All @@ -568,8 +636,14 @@ def safe_remove_sdkconfig_files():

SConscript("_embed_files.py", exports="env")

flag_any_custom_sdkconfig = (FRAMEWORK_LIB_DIR is not None and
exists(str(Path(FRAMEWORK_LIB_DIR) / "sdkconfig")))
# Check if libs were previously recompiled for THIS chip. idf_lib_copy()
# renames the stock sdkconfig to sdkconfig.orig during recompilation, so its
# presence is a reliable per-chip indicator. Using the chip-specific path
# (instead of the root "sdkconfig") prevents cross-env flip-flopping: an env
# for a different MCU won't see another chip's recompilation artefact and
# trigger a spurious framework reinstall (check_reinstall_frwrk line 722).
flag_any_custom_sdkconfig = (FRAMEWORK_LIB_DIR is not None and
exists(str(Path(FRAMEWORK_LIB_DIR) / chip_variant / "sdkconfig.orig")))


def has_unicore_flags():
Expand Down Expand Up @@ -996,3 +1070,7 @@ def custom_object_wrapper(_node, **kwargs):

build_script_path = str(Path(FRAMEWORK_DIR) / "tools" / "pioarduino-build.py")
SConscript(build_script_path)

# LP-Core ULP support for Arduino-only builds
if _has_ulp_sources(Path(env.subst("$PROJECT_DIR")) / "ulp"):
SConscript("ulp_lp_core.py", exports="env")
18 changes: 17 additions & 1 deletion builder/frameworks/espidf.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,12 @@ def HandleCOMPONENTsettings(env):
if not bool(os.path.exists(str(Path(PROJECT_DIR) / ".dummy"))):
shutil.copytree(LIB_SOURCE, str(Path(PROJECT_DIR) / ".dummy"))
PROJECT_SRC_DIR = str(Path(PROJECT_DIR) / ".dummy")
# Save user's build_flags before clearing — PlatformIO core already
# parsed them into CPPDEFINES/CCFLAGS, and env.Replace() only clears
# the string variables, not the parsed effects. Removing the user's
# build_flags prevents conflicts with CMake code model defines during
# lib recompilation (e.g. -DARDUINO_USB_CDC_ON_BOOT=0 vs =1).
_saved_build_flags = env.get("BUILD_FLAGS", "")
env.Replace(
PROJECT_SRC_DIR=PROJECT_SRC_DIR,
BUILD_FLAGS="",
Expand All @@ -820,6 +826,10 @@ def HandleCOMPONENTsettings(env):
PIOFRAMEWORK="arduino",
ARDUINO_LIB_COMPILE_FLAG="Build",
)
if _saved_build_flags:
env.ProcessUnFlags(
_saved_build_flags if isinstance(_saved_build_flags, str)
else " ".join(_saved_build_flags))
env["INTEGRATION_EXTRA_DATA"].update({"arduino_lib_compile_flag": env.subst("$ARDUINO_LIB_COMPILE_FLAG")})

def get_project_lib_includes(env):
Expand Down Expand Up @@ -2652,6 +2662,9 @@ def _replace_copy(src, dst):

_replace_copy(str(Path(lib_dst) / "libspi_flash.a"), str(Path(mem_var) / "libspi_flash.a"))
_replace_copy(str(Path(env_build) / "memory.ld"), str(Path(ld_dst) / "memory.ld"))
sections_ld = str(Path(env_build) / "sections.ld")
if os.path.isfile(sections_ld):
_replace_copy(sections_ld, str(Path(ld_dst) / "sections.ld"))
if mcu == "esp32s3":
_replace_copy(str(Path(lib_dst) / "libesp_psram.a"), str(Path(mem_var) / "libesp_psram.a"))
_replace_copy(str(Path(lib_dst) / "libesp_system.a"), str(Path(mem_var) / "libesp_system.a"))
Expand All @@ -2664,7 +2677,10 @@ def _replace_copy(src, dst):
if not bool(os.path.isfile(str(Path(arduino_libs) / chip_variant / "sdkconfig.orig"))):
shutil.move(str(Path(arduino_libs) / chip_variant / "sdkconfig"), str(Path(arduino_libs) / chip_variant / "sdkconfig.orig"))
shutil.copyfile(str(Path(env.subst("$PROJECT_DIR")) / ("sdkconfig." + env["PIOENV"])), str(Path(arduino_libs) / chip_variant / "sdkconfig"))
shutil.copyfile(str(Path(env.subst("$PROJECT_DIR")) / ("sdkconfig." + env["PIOENV"])), str(Path(arduino_libs) / "sdkconfig"))
# Note: intentionally NOT copying to the libs root (arduino_libs / "sdkconfig").
# The root copy caused cross-env flip-flopping: any env for a different MCU
# without custom_sdkconfig would see it, trigger check_reinstall_frwrk(),
# and reinstall the stock libs — wiping out this chip's recompiled artefacts.
try:
# clean env build folder to avoid issues with following Arduino build
shutil.rmtree(env_build)
Expand Down
Loading