diff --git a/README.md b/README.md index 9ba475c84..9550e9654 100644 --- a/README.md +++ b/README.md @@ -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+ diff --git a/builder/frameworks/arduino.py b/builder/frameworks/arduino.py index 82cd33746..be82d18a7 100644 --- a/builder/frameworks/arduino.py +++ b/builder/frameworks/arduino.py @@ -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", " ") @@ -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(): @@ -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") diff --git a/builder/frameworks/espidf.py b/builder/frameworks/espidf.py index 4bae4fb06..056ed6309 100644 --- a/builder/frameworks/espidf.py +++ b/builder/frameworks/espidf.py @@ -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="", @@ -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): @@ -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")) @@ -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) diff --git a/builder/frameworks/ulp_lp_core.py b/builder/frameworks/ulp_lp_core.py new file mode 100644 index 000000000..01a3c6e2e --- /dev/null +++ b/builder/frameworks/ulp_lp_core.py @@ -0,0 +1,456 @@ +# Copyright 2020-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +from pathlib import Path + +from platformio import fs +from platformio.proc import exec_command + +from SCons.Script import Import, Return + +Import("env") + +# Skip when the CMake-based ulp.py handles ULP compilation +if "espidf" in env.subst("$PIOFRAMEWORK"): + Return() + +platform = env.PioPlatform() +board = env.BoardConfig() +mcu = board.get("build.mcu", "esp32") + +# +# Per-MCU LP-Core configuration +# Keep in sync with _lp_core_mcus in arduino.py and platform.py +# + +LP_CORE_MCUS = ("esp32c5", "esp32c6", "esp32p4") +ULP_SOURCE_SUFFIXES = (".c", ".S", ".s") + +if mcu not in LP_CORE_MCUS: + Return() + + +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() + ) + + +# +# Path resolution +# + +PROJECT_DIR = Path(env.subst("$PROJECT_DIR")) +BUILD_DIR = Path(env.subst("$BUILD_DIR")) +ULP_DIR = PROJECT_DIR / "ulp" + +if not _has_ulp_sources(ULP_DIR): + Return() + +ULP_BUILD_DIR = str(BUILD_DIR / "ulp_lp_core") + +FRAMEWORK_DIR = platform.get_package_dir("framework-espidf") +if not FRAMEWORK_DIR or not os.path.isdir(FRAMEWORK_DIR): + sys.stderr.write( + "Error: framework-espidf not found. Required for LP-Core ULP builds.\n" + ) + env.Exit(1) + +FRAMEWORK_DIR = str(FRAMEWORK_DIR) +IDF_COMPONENTS = Path(FRAMEWORK_DIR) / "components" + +_fw_libs_dir = platform.get_package_dir("framework-arduinoespressif32-libs") +if not _fw_libs_dir or not os.path.isdir(_fw_libs_dir): + sys.stderr.write("Error: framework-arduinoespressif32-libs not found.\n") + env.Exit(1) + +FW_LIBS_DIR = Path(_fw_libs_dir) + +# +# Validate libs have ULP support — detect stale/reset packages early +# + +MEMORY_TYPE = board.get( + "build.arduino.memory_type", + board.get("build.flash_mode", "dio") + "_qspi", +) + +_ulp_lib_path = FW_LIBS_DIR / mcu / "lib" / "libulp.a" +_recompiled_sdkconfig = FW_LIBS_DIR / mcu / MEMORY_TYPE / "include" / "sdkconfig.h" +_libs_have_ulp = ( + _ulp_lib_path.exists() + and _recompiled_sdkconfig.exists() + and "CONFIG_ULP_COPROC_ENABLED" in _recompiled_sdkconfig.read_text() +) + +if not _libs_have_ulp: + sys.stderr.write( + "Error: Arduino libs were not compiled with ULP support.\n" + " libulp.a exists: %s\n" + " The libs package may have been reset. Run a clean build to\n" + " trigger automatic recompilation with ULP enabled:\n" + " pio run -t clean && pio run\n" + % _ulp_lib_path.exists() + ) + env.Exit(1) + +# +# sdkconfig resolution +# +# Priority: user-provided ulp/sdkconfig.h > recompiled from libs (validated above) +# + +USER_SDKCONFIG_H = ULP_DIR / "sdkconfig.h" + + +if USER_SDKCONFIG_H.exists(): + SDKCONFIG_H = USER_SDKCONFIG_H +else: + # The validation above already confirmed _recompiled_sdkconfig exists and + # contains CONFIG_ULP_COPROC_ENABLED, so we can use it directly. + SDKCONFIG_H = _recompiled_sdkconfig + + +def get_sdkconfig_value(key, default): + """Read a config value, checking the selected SDKCONFIG_H first, then + custom_sdkconfig from platformio.ini, then falling back to default.""" + # Check the actual header the ULP build will use + 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: + 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: + pass + return default + +# +# Generate sdkconfig.cmake from sdkconfig.h +# +# IDF's ULP CMake build does `include(${SDKCONFIG_CMAKE})` to read config +# values as CMake variables. We convert #define lines to set() calls. +# + + +def generate_sdkconfig_cmake(): + Path(ULP_BUILD_DIR).mkdir(parents=True, exist_ok=True) + out = Path(ULP_BUILD_DIR) / "sdkconfig.cmake" + + lines = ["# Auto-generated from %s" % SDKCONFIG_H.name] + try: + for line in SDKCONFIG_H.read_text().splitlines(): + line = line.strip() + if line.startswith("#define CONFIG_"): + parts = line.split(None, 2) + if len(parts) == 3: + value = parts[2].strip('"') + lines.append('set(%s "%s")' % (parts[1], value)) + elif len(parts) == 2: + lines.append('set(%s "1")' % parts[1]) + except Exception as e: + sys.stderr.write("Error reading %s: %s\n" % (SDKCONFIG_H, e)) + env.Exit(1) + + content = "\n".join(lines) + "\n" + if out.exists() and out.read_text() == content: + return out + out.write_text(content) + return out + + +SDKCONFIG_CMAKE = generate_sdkconfig_cmake() + +# +# Component include paths for ULP compilation +# +# IDF's ULP CMake build adds core ULP includes automatically. We only need +# to provide the framework-arduinoespressif32-libs paths for soc/hal headers. +# Non-existent paths are filtered for forward-compatibility. +# + +FW_LIBS = FW_LIBS_DIR / mcu / "include" + +COMPONENT_INCLUDES = [ + str(ULP_DIR), + str(SDKCONFIG_H.parent), + str(FW_LIBS / "soc" / mcu / "include"), + str(FW_LIBS / "soc" / mcu / "register"), + str(FW_LIBS / "soc" / "include"), + str(FW_LIBS / "hal" / "include"), + str(FW_LIBS / "hal" / mcu / "include"), + str(FW_LIBS / "hal" / "platform_port" / "include"), + str(FW_LIBS / "esp_common" / "include"), + str(FW_LIBS / "esp_rom" / "include"), + str(FW_LIBS / "esp_rom" / mcu), + str(FW_LIBS / "esp_rom" / mcu / "include"), + str(FW_LIBS / "esp_rom" / mcu / "include" / mcu), + str(FW_LIBS / "esp_hw_support" / "include"), + str(FW_LIBS / "esp_hw_support" / "include" / "soc"), + str(FW_LIBS / "esp_hw_support" / "include" / "soc" / mcu), + str(FW_LIBS / "esp_hw_support" / "port" / mcu), + str(FW_LIBS / "esp_hw_support" / "port" / mcu / "include"), + str(FW_LIBS / "riscv" / "include"), + str(FW_LIBS / "log" / "include"), + str(FW_LIBS / "esp_timer" / "include"), + str(FW_LIBS / "esp_driver_uart" / "include"), + str(FW_LIBS / "heap" / "include"), +] +COMPONENT_INCLUDES = [p for p in COMPONENT_INCLUDES if os.path.isdir(p)] + +# +# Prepare build environment +# + +ulp_env = env.Clone() + + +def prepare_ulp_env_vars(): + ulp_env["ENV"]["IDF_PATH"] = FRAMEWORK_DIR + + additional_packages = [ + platform.get_package_dir("toolchain-riscv32-esp"), + platform.get_package_dir("tool-ninja"), + str(Path(platform.get_package_dir("tool-cmake")) / "bin"), + ] + + for package in additional_packages: + if package and os.path.isdir(package): + ulp_env.PrependENVPath("PATH", package) + + +prepare_ulp_env_vars() + +CMAKE = str(Path(platform.get_package_dir("tool-cmake")) / "bin" / "cmake") + +# +# Collect ULP sources +# + + +def collect_ulp_sources(): + return sorted( + str(f) for f in ULP_DIR.rglob("*") + if f.is_file() and f.suffix in ULP_SOURCE_SUFFIXES + ) + +# +# CMake configure — generates build.ninja for the ULP build +# + + +def generate_ulp_config(): + def _action(env, target, source): + ulp_toolchain = str( + Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake" + / "toolchain-lp-core-riscv.cmake" + ) + + cmd = ( + CMAKE, + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + "-DCMAKE_GENERATOR=Ninja", + "-DCMAKE_TOOLCHAIN_FILE=" + ulp_toolchain, + "-DULP_S_SOURCES=%s" % ";".join( + [fs.to_unix_path(s.get_abspath()) for s in source] + ), + "-DULP_APP_NAME=ulp_main", + "-DULP_VAR_PREFIX=ulp_", + "-DCOMPONENT_DIR=" + fs.to_unix_path(str(ULP_DIR)), + "-DCOMPONENT_INCLUDES=%s" % ";".join(COMPONENT_INCLUDES), + "-DIDF_TARGET=%s" % mcu, + "-DIDF_PATH=" + fs.to_unix_path(FRAMEWORK_DIR), + "-DSDKCONFIG_HEADER=" + str(SDKCONFIG_H), + "-DPYTHON=" + env.subst("$PYTHONEXE"), + "-DSDKCONFIG_CMAKE=" + str(SDKCONFIG_CMAKE), + "-DCMAKE_MODULE_PATH=" + fs.to_unix_path( + str(Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake") + ), + "-GNinja", + "-B", ULP_BUILD_DIR, + str(Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake"), + ) + + result = exec_command(cmd) + if result["returncode"] != 0: + sys.stderr.write("CMake ULP configure failed:\n%s\n" % result["err"]) + env.Exit(1) + + ulp_sources = collect_ulp_sources() + return ulp_env.Command( + str(Path(ULP_BUILD_DIR) / "build.ninja"), + ulp_sources, + ulp_env.VerboseAction(_action, "Configuring LP-Core ULP build"), + ) + +# +# CMake build — compiles the ULP binary +# + + +def compile_ulp_binary(): + cmd = (CMAKE, "--build", ULP_BUILD_DIR, "--target", "build") + + # build.ninja content is stable across re-configurations, so use + # timestamp-based decider to detect source changes + ulp_binary_env = ulp_env.Clone() + ulp_binary_env.Decider("timestamp-newer") + + return ulp_binary_env.Command( + [ + str(Path(ULP_BUILD_DIR) / "ulp_main.h"), + str(Path(ULP_BUILD_DIR) / "ulp_main.ld"), + str(Path(ULP_BUILD_DIR) / "ulp_main.bin"), + ], + None, + ulp_binary_env.VerboseAction( + " ".join(cmd), "Building LP-Core ULP binary" + ), + ) + +# +# Generate assembly embedding of the ULP binary +# + + +def generate_ulp_assembly(): + cmd = ( + CMAKE, + "-DDATA_FILE=$SOURCE", + "-DSOURCE_FILE=$TARGET", + "-DFILE_TYPE=BINARY", + "-P", + str(Path(FRAMEWORK_DIR) / "tools" / "cmake" / "scripts" + / "data_file_embed_asm.cmake"), + ) + + return ulp_env.Command( + str(BUILD_DIR / "ulp_main.bin.S"), + str(Path(ULP_BUILD_DIR) / "ulp_main.bin"), + ulp_env.VerboseAction(" ".join(cmd), "Generating ULP assembly file $TARGET"), + ) + +# +# Patch memory.ld to reserve LP SRAM for the ULP binary +# + +# Default matches the auto-injected value in arduino.py (_ulp_sdkconfig_entries) +ULP_RESERVE_MEM = get_sdkconfig_value("CONFIG_ULP_COPROC_RESERVE_MEM", 8192) + +_LP_RAM_SEG_RE = re.compile( + r"(lp_ram_seg\s*\(\s*RW\s*\)\s*:\s*org\s*=\s*)(.*?)" + r"(,\s*len\s*=\s*)" + r"([^\n]+)", + re.DOTALL, +) + + +def patch_memory_ld(): + fw_ld_dir = FW_LIBS_DIR / mcu / "ld" + src_ld = fw_ld_dir / "memory.ld" + + if not src_ld.exists(): + return + + text = src_ld.read_text() + + if "+ %d" % ULP_RESERVE_MEM in text or "+%d" % ULP_RESERVE_MEM in text: + return + + match = _LP_RAM_SEG_RE.search(text) + if not match: + sys.stderr.write( + "Error: lp_ram_seg not found in %s — cannot reserve LP SRAM.\n" + " The LP-Core binary will fail to load at runtime.\n" % src_ld + ) + env.Exit(1) + + org_expr = match.group(2).rstrip() + len_expr = match.group(4).rstrip() + patched_seg = "%s(%s) + %d%s(%s) - %d" % ( + match.group(1), org_expr, ULP_RESERVE_MEM, + match.group(3), len_expr, ULP_RESERVE_MEM, + ) + patched = text[:match.start()] + patched_seg + text[match.end():] + + patched_ld_dir = Path(ULP_BUILD_DIR) / "ld" + patched_ld_dir.mkdir(parents=True, exist_ok=True) + (patched_ld_dir / "memory.ld").write_text(patched) + env.Prepend(LIBPATH=[str(patched_ld_dir)]) + print( + "Patched memory.ld: lp_ram_seg offset by %d bytes for LP-Core binary" + % ULP_RESERVE_MEM + ) + +# +# SCons build graph +# + +ulp_config = generate_ulp_config() +ulp_binary = compile_ulp_binary() +ulp_assembly = generate_ulp_assembly() + +ulp_env.Depends(ulp_binary, ulp_config) + +# Compile the assembly file into an object with the main firmware toolchain +# and add it to the firmware's link inputs +ulp_obj = env.Object( + str(BUILD_DIR / "ulp_main.bin.o"), + str(BUILD_DIR / "ulp_main.bin.S"), +) +env.Depends(ulp_obj, ulp_assembly) +env.Append(PIOBUILDFILES=[ulp_obj]) + +# ULP build dir (ulp_main.h) + IDF ULP headers + soc/hal component includes. +# The component includes are needed for IDE IntelliSense on ULP source files +# (transitive headers like soc/gpio_num.h, hal/gpio_types.h, etc.). +env.AppendUnique(CPPPATH=[ + ULP_BUILD_DIR, + str(IDF_COMPONENTS / "ulp" / "lp_core" / "include"), + str(IDF_COMPONENTS / "ulp" / "lp_core" / "lp_core" / "include"), + str(IDF_COMPONENTS / "ulp" / "ulp_common" / "include"), +] + COMPONENT_INCLUDES) +env.Append(LINKFLAGS=["-T", str(Path(ULP_BUILD_DIR) / "ulp_main.ld")]) + +# Link libulp.a for ulp_lp_core_load_binary / ulp_lp_core_run +ulp_lib = FW_LIBS_DIR / mcu / "lib" / "libulp.a" +if ulp_lib.exists(): + env.Append(LIBS=[env.File(str(ulp_lib))]) +else: + sys.stderr.write( + "Error: libulp.a not found at %s\n" + " This library is produced by lib recompilation with ULP enabled.\n" + " The auto-injection should have triggered this — if you see this\n" + " error, the recompilation may have failed. Try a clean build:\n" + " pio run -t clean && pio run\n" % ulp_lib + ) + env.Exit(1) + +patch_memory_ld() + +print("LP-Core ULP support enabled for %s (reserve=%d bytes)" % (mcu, ULP_RESERVE_MEM)) diff --git a/examples/arduino-ulp-blink/.gitignore b/examples/arduino-ulp-blink/.gitignore new file mode 100644 index 000000000..e29bb1e6b --- /dev/null +++ b/examples/arduino-ulp-blink/.gitignore @@ -0,0 +1,7 @@ +.pio +.vscode +.dummy +managed_components +dependencies.lock +sdkconfig.* +CMakeLists.txt diff --git a/examples/arduino-ulp-blink/README.md b/examples/arduino-ulp-blink/README.md new file mode 100644 index 000000000..e543fa750 --- /dev/null +++ b/examples/arduino-ulp-blink/README.md @@ -0,0 +1,42 @@ +# Arduino-Only ULP Blink for ESP32-C6 + +| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-P4 | +| ----------------- | -------- | -------- | -------- | + +This example demonstrates running a C program on the LP-Core (ULP) coprocessor using **Arduino framework only** — no ESP-IDF CMake pipeline or hybrid build required. + +## Two programs run in parallel + +1. **Arduino on the HP Core:** Prints the ULP's shared `led_state` variable over serial. +2. **C program on the LP Core:** Blinks an external LED connected to GPIO3 via the ultra-low-power coprocessor. + +## How it works + +Place LP-Core sources in the `ulp/` directory. The platform automatically: +- Detects the `ulp/` directory and configures ULP support (sdkconfig, components, lib recompilation) +- Compiles ULP sources with the RISC-V LP-Core toolchain +- Generates `ulp_main.h` (symbol map) and `ulp_main_bin.h` (binary declarations) +- Embeds the binary into the main firmware +- Links `libulp.a` for `ulp_lp_core_load_binary()` and `ulp_lp_core_run()` APIs + +No `custom_sdkconfig`, `custom_component_remove`, or `lib_ignore` entries are needed — the platform handles everything. The first build triggers a one-time lib recompilation. + +## Hardware Required + +- ESP32-C6 (or C5/P4) development board +- LED + resistor on GPIO3 (active high) + +## Example Output + +```text +Starting ULP blink program... +ULP binary size: 8192 bytes +ULP program running — LED on GPIO3 should blink +ULP led_state: 1 +ULP led_state: 0 +ULP led_state: 1 +``` + +## Comparison with espidf-arduino-C6-ULP-blink + +That example uses `framework = arduino, espidf` (hybrid) and requires CMakeLists.txt, sdkconfig.defaults, and component management. This example uses `framework = arduino` only — the platform handles ULP compilation natively. diff --git a/examples/arduino-ulp-blink/platformio.ini b/examples/arduino-ulp-blink/platformio.ini new file mode 100644 index 000000000..4b48a72be --- /dev/null +++ b/examples/arduino-ulp-blink/platformio.ini @@ -0,0 +1,8 @@ +; PlatformIO Project Configuration File +; ULP support is auto-configured when the ulp/ directory is present. + +[env:esp32c6] +platform = espressif32 +framework = arduino +board = esp32-c6-devkitc-1 +monitor_speed = 115200 diff --git a/examples/arduino-ulp-blink/src/main.cpp b/examples/arduino-ulp-blink/src/main.cpp new file mode 100644 index 000000000..8795c69e8 --- /dev/null +++ b/examples/arduino-ulp-blink/src/main.cpp @@ -0,0 +1,35 @@ +#include +#include "ulp_lp_core.h" +#include "ulp_main.h" +#include "esp_err.h" + +extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start"); +extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end"); + +void start_ulp_program() { + ESP_ERROR_CHECK(ulp_lp_core_load_binary(ulp_main_bin_start, + (ulp_main_bin_end - ulp_main_bin_start))); + + ulp_lp_core_cfg_t cfg = { + .wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER, + .lp_timer_sleep_duration_us = 1000000, + }; + + ESP_ERROR_CHECK(ulp_lp_core_run(&cfg)); +} + +void setup() { + Serial.begin(115200); + delay(1000); + + Serial.println("Starting ULP blink program..."); + Serial.printf("ULP binary size: %lu bytes\n", + (unsigned long)(ulp_main_bin_end - ulp_main_bin_start)); + start_ulp_program(); + Serial.println("ULP program running — LED on GPIO3 should blink"); +} + +void loop() { + Serial.printf("ULP led_state: %d\n", (int)ulp_ulp_led_state); + delay(2000); +} diff --git a/examples/arduino-ulp-blink/ulp/blink.c b/examples/arduino-ulp-blink/ulp/blink.c new file mode 100644 index 000000000..e4ade7183 --- /dev/null +++ b/examples/arduino-ulp-blink/ulp/blink.c @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "ulp_lp_core_utils.h" +#include "ulp_lp_core_gpio.h" + +#define BLINK_PIN LP_IO_NUM_3 +#define BLINK_DELAY_MS 1000 + +volatile bool ulp_led_state; + +int main(void) +{ + ulp_lp_core_gpio_init(BLINK_PIN); + ulp_lp_core_gpio_output_enable(BLINK_PIN); + + ulp_led_state = !ulp_led_state; + ulp_lp_core_gpio_set_level(BLINK_PIN, (int)ulp_led_state); + + ulp_lp_core_delay_us(BLINK_DELAY_MS * 1000); + + return 0; +} diff --git a/platform.py b/platform.py index a6edd714b..1e9f794d3 100644 --- a/platform.py +++ b/platform.py @@ -592,6 +592,18 @@ def _configure_espidf_framework( safe_remove_directory_pattern(Path(self.packages_dir), f"framework-espidf.*") self.packages["framework-espidf"]["optional"] = False + # LP-Core ULP builds need framework-espidf for runtime sources, linker + # scripts, and esp32ulp_mapgen.py — even for Arduino-only projects. + # Keep in sync with LP_CORE_MCUS and ULP_SOURCE_SUFFIXES in ulp_lp_core.py + lp_core_mcus = ("esp32c5", "esp32c6", "esp32p4") + ulp_suffixes = (".c", ".S", ".s") + if mcu in lp_core_mcus: + ulp_dir = Path(ProjectConfig.get_instance().path).parent / "ulp" + if ulp_dir.is_dir() and any( + f.suffix in ulp_suffixes for f in ulp_dir.rglob("*") if f.is_file() + ): + self.packages["framework-espidf"]["optional"] = False + def _get_mcu_config(self, mcu: str) -> Optional[Dict]: """Get MCU configuration with optimized caching and search.""" if mcu in self._mcu_config_cache: @@ -763,6 +775,15 @@ def configure_default_packages(self, variables: Dict, targets: List[str]) -> Any if "espidf" in frameworks: self._install_common_idf_packages() + # LP-Core ULP lib recompilation needs CMake and ninja + # Keep in sync with LP_CORE_MCUS and ULP_SOURCE_SUFFIXES in ulp_lp_core.py + if "espidf" not in frameworks and mcu in ("esp32c5", "esp32c6", "esp32p4"): + ulp_dir = Path(ProjectConfig.get_instance().path).parent / "ulp" + if ulp_dir.is_dir() and any( + f.suffix in (".c", ".S", ".s") for f in ulp_dir.rglob("*") if f.is_file() + ): + self._install_common_idf_packages() + self._configure_rom_elfs_for_exception_decoder(variables) self._configure_check_tools(variables) self._handle_dfuutil_tool(variables)