From 869f2f355f44daa85f1497529fadf627dbe239f9 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Wed, 24 Jun 2026 11:17:38 +0200 Subject: [PATCH 1/4] refactor(ulp): assemble LP-core linker scripts from base, layout and checks --- components/ulp/ld/lp_core_riscv.ld.in | 120 ++---------------- components/ulp/ld/lp_core_riscv_base.ld.in | 120 ++++++++++++++++++ components/ulp/ld/lp_core_riscv_checks.ld.in | 41 ++++++ .../ulp/ld/lp_core_riscv_default_layout.ld.in | 35 +++++ 4 files changed, 204 insertions(+), 112 deletions(-) create mode 100644 components/ulp/ld/lp_core_riscv_base.ld.in create mode 100644 components/ulp/ld/lp_core_riscv_checks.ld.in create mode 100644 components/ulp/ld/lp_core_riscv_default_layout.ld.in diff --git a/components/ulp/ld/lp_core_riscv.ld.in b/components/ulp/ld/lp_core_riscv.ld.in index 315faefb5bc..1c9c95cc405 100644 --- a/components/ulp/ld/lp_core_riscv.ld.in +++ b/components/ulp/ld/lp_core_riscv.ld.in @@ -1,120 +1,16 @@ /* * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD - * * SPDX-License-Identifier: Apache-2.0 + * + * LP-core RISC-V linker script, assembled from base + layout + checks. + * A custom layout replaces the default via LP_CORE_LINKER_INCLUDE. */ -#include "sdkconfig.h" -#include "soc/soc.h" -#include "ld.common" -#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM -#include "ld.hp_mem_defs" -#endif +#include "lp_core_riscv_base.ld.in" -#if CONFIG_ESP_ROM_HAS_LP_ROM -/* With LP-ROM memory layout is different due to LP ROM stack/data */ -/* For P4 ECO5 we also reserve some RTC MEM at the first for MSPI workaround */ -#define ULP_MEM_START_ADDRESS SOC_RTC_DRAM_LOW + RESERVE_RTC_MEM + MSPI_WORKAROUND_SIZE +#ifdef LP_CORE_LINKER_INCLUDE +#include LP_CORE_LINKER_INCLUDE #else -#define ULP_MEM_START_ADDRESS (SOC_RTC_DRAM_LOW) +#include "lp_core_riscv_default_layout.ld.in" #endif -#define ALIGN_DOWN(SIZE, AL) (SIZE & ~(AL - 1)) -/* Ensure the end where the shared memory starts is aligned to 8 bytes - if updating this also update the same in ulp_lp_core_memory_shared.c - */ -#define ALIGNED_COPROC_MEM ALIGN_DOWN(CONFIG_ULP_COPROC_RESERVE_MEM, 0x8) - -ENTRY(reset_vector) - -MEMORY -{ - /* First 128 bytes for exception/interrupt vectors */ - vector_table(RX) : ORIGIN = ULP_MEM_START_ADDRESS, LENGTH = 0x80 - lp_ram(RWX) : ORIGIN = ULP_MEM_START_ADDRESS + 0x80, LENGTH = ALIGNED_COPROC_MEM - 0x80 - CONFIG_ULP_SHARED_MEM - shared_mem_ram(RW) : ORIGIN = ULP_MEM_START_ADDRESS + ALIGNED_COPROC_MEM - CONFIG_ULP_SHARED_MEM, LENGTH = CONFIG_ULP_SHARED_MEM -#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM - hp_ram(RWX) : ORIGIN = ULP_HP_MEM_START, LENGTH = ULP_HP_MEM_SIZE -#endif -} - -#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM -REGION_ALIAS("default_app_seg", hp_ram); -#else -REGION_ALIAS("default_app_seg", lp_ram); -#endif - -SECTIONS -{ - .vector.text : - { - /* Exception/interrupt vectors */ - __mtvec_base = .; - KEEP (*(.init.vector .init.vector.*)) - } > vector_table - - . = ORIGIN(lp_ram); - - /* Interrupt/exception handlers stay in LP RAM (reachable on wakeup before HP SRAM is up). */ - .rtc_text ALIGN(4): - { - _lp_text_start = .; - *(.text.vectors) /* Default reset vector must link to offset 0x80 */ - *(.text.handlers) - *(.text.handlers.*) - } > lp_ram - -#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM - /* End of LP-resident code (handlers only); align to 128 bytes. */ - . = ALIGN(128); - _lp_text_end = .; - . = ORIGIN(hp_ram); -#endif - - .text ALIGN(4): - { - *(.text) - *(.text*) - } > default_app_seg - - .rodata ALIGN(4): - { - *(.rodata) - *(.rodata*) - } > default_app_seg - - /* 128-byte alignment required for PMP TOR granularity (SOC_CPU_PMP_REGION_GRANULARITY) */ - . = ALIGN(128); -#if !CONFIG_ULP_COPROC_RUN_FROM_HP_MEM - _lp_text_end = .; -#endif - _lp_data_start = .; - - .data ALIGN(4): - { - _data_start = .; - *(.data) - *(.data*) - *(.sdata) - *(.sdata*) - _data_end = .; - } > default_app_seg - - .bss ALIGN(4) : - { - _bss_start = .; - *(.bss) - *(.bss*) - *(.sbss) - *(.sbss*) - PROVIDE(end = .); - _bss_end = .; - } > default_app_seg - - __stack_top = ORIGIN(lp_ram) + LENGTH(lp_ram); - - . = ORIGIN(shared_mem_ram); - .shared_mem (ALIGN(4)) : - { - KEEP(*(.shared_mem)) - } > shared_mem_ram -} +#include "lp_core_riscv_checks.ld.in" diff --git a/components/ulp/ld/lp_core_riscv_base.ld.in b/components/ulp/ld/lp_core_riscv_base.ld.in new file mode 100644 index 00000000000..cad4307dce4 --- /dev/null +++ b/components/ulp/ld/lp_core_riscv_base.ld.in @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * Base part of the LP-core linker script (base + layout + checks). It defines: + * - the usable LP-RAM window bounds LP_CORE_USER_MEMORY_REGION_START and + * LP_CORE_USER_MEMORY_REGION_END; + * - ENTRY, the interrupt vector table, the HP/LP shared-memory section and a + * default __stack_top; + * - the section-boundary macros a custom layout composes: LP_CORE_TEXT_START, + * LP_CORE_TEXT_END, LP_CORE_DATA_START and LP_CORE_DATA_END, plus the + * LP_CORE_DEFAULT_TEXT / LP_CORE_DEFAULT_DATA convenience macros used by the + * default layout. + */ +#include "sdkconfig.h" +#include "soc/soc.h" +#include "ld.common" +#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM +#include "ld.hp_mem_defs" +#endif + +#if CONFIG_ESP_ROM_HAS_LP_ROM +/* With LP-ROM memory layout is different due to LP ROM stack/data */ +/* For P4 ECO5 we also reserve some RTC MEM at the first for MSPI workaround */ +#define ULP_MEM_START_ADDRESS SOC_RTC_DRAM_LOW + RESERVE_RTC_MEM + MSPI_WORKAROUND_SIZE +#else +#define ULP_MEM_START_ADDRESS (SOC_RTC_DRAM_LOW) +#endif + +#define ALIGN_DOWN(SIZE, AL) (SIZE & ~(AL - 1)) +/* Ensure the end where the shared memory starts is aligned to 8 bytes + if updating this also update the same in ulp_lp_core_memory_shared.c + */ +#define ALIGNED_COPROC_MEM ALIGN_DOWN(CONFIG_ULP_COPROC_RESERVE_MEM, 0x8) +#define ULP_VECTOR_TABLE_LENGTH 0x80 + +/* Free LP-RAM window a layout may use, between the vector table and shared memory. */ +#define LP_CORE_USER_MEMORY_REGION_START (ULP_MEM_START_ADDRESS + ULP_VECTOR_TABLE_LENGTH) +#define LP_CORE_USER_MEMORY_REGION_END (ULP_MEM_START_ADDRESS + ALIGNED_COPROC_MEM - CONFIG_ULP_SHARED_MEM) + +/* + * Section-boundary macros a custom layout composes at each region boundary. + * ESP-IDF injects any config-dependent handling here, so the layout composes + * them the same way regardless of the active configuration. + */ + +/* Reset vector and early handlers at the boot offset, placed first in `region` + (which must start at LP_CORE_USER_MEMORY_REGION_START). */ +#define LP_CORE_TEXT_START(region) \ + . = ORIGIN(region) ; \ + .rtc_text ALIGN(4) : { \ + *(.text.vectors) /* Reset vector must link to offset 0x80 */ \ + *(.text.handlers) \ + *(.text.handlers.*) \ + } > region + +/* End of the executable region in LP RAM: the RX/RW boundary the PMP setup uses + under memory protection. (Memory protection and running from HP RAM are + mutually exclusive, so the boundary always follows the LP-RAM .text here.) */ +#if CONFIG_ULP_LP_CORE_MEMPROT +#define LP_CORE_TEXT_END() . = ALIGN(128) ; _lp_text_end = . ; +#else +#define LP_CORE_TEXT_END() +#endif + +/* Start of the writable data region. */ +#define LP_CORE_DATA_START() . = ALIGN(128) ; _lp_data_start = . ; + +/* End of the writable data region, before the stack. Marks the lowest address + the stack may descend to: the framework derives the default __stack_size from + it and bounds-checks the stack against it. A layout must compose this (or + assign _lp_data_end directly). */ +#define LP_CORE_DATA_END() . = ALIGN(4) ; _lp_data_end = . ; + +/* Convenience: standard executable and data section placement into `region`. */ +#define LP_CORE_DEFAULT_TEXT(region) \ + .text ALIGN(4) : { *(.text) *(.text*) } > region \ + .rodata ALIGN(4) : { *(.rodata) *(.rodata*) } > region +#define LP_CORE_DEFAULT_DATA(region) \ + .data ALIGN(4) : { _data_start = . ; *(.data) *(.data*) *(.sdata) *(.sdata*) _data_end = . ; } > region \ + .bss ALIGN(4) : { _bss_start = . ; *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .) ; _bss_end = . ; } > region + +/* Default stack top, at the top of the usable window. A layout whose data does + not reach the window end may override it to the top of its own data region. + The companion __stack_size defaults to __stack_top - _lp_data_end (computed in + the checks part, once the layout has placed its data); a layout may override + either. Both are weak defaults, so a custom layout can take ownership. */ +PROVIDE(__stack_top = LP_CORE_USER_MEMORY_REGION_END); + +ENTRY(reset_vector) + +MEMORY +{ + /* First 128 bytes for exception/interrupt vectors */ + vector_table(RX) : ORIGIN = ULP_MEM_START_ADDRESS, LENGTH = ULP_VECTOR_TABLE_LENGTH + shared_mem_ram(RW) : ORIGIN = LP_CORE_USER_MEMORY_REGION_END, LENGTH = CONFIG_ULP_SHARED_MEM +#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM + hp_ram(RWX) : ORIGIN = ULP_HP_MEM_START, LENGTH = ULP_HP_MEM_SIZE +#endif +} + +SECTIONS +{ + .vector.text : + { + /* Exception/interrupt vectors */ + __mtvec_base = .; + KEEP (*(.init.vector .init.vector.*)) + } > vector_table + + /* Shared memory, agreed with ulp_lp_core_memory_shared.c. */ + . = ORIGIN(shared_mem_ram); + _lp_shared_start = ORIGIN(shared_mem_ram); + .shared_mem (ALIGN(4)) : { KEEP(*(.shared_mem)) } > shared_mem_ram + + /* Position the location counter at the window start so the layout part's + sections flow into their region. A layout that omits LP_CORE_TEXT_START + is then caught by the checks part instead of an obscure overflow. */ + . = LP_CORE_USER_MEMORY_REGION_START; +} diff --git a/components/ulp/ld/lp_core_riscv_checks.ld.in b/components/ulp/ld/lp_core_riscv_checks.ld.in new file mode 100644 index 00000000000..bd50c619a6b --- /dev/null +++ b/components/ulp/ld/lp_core_riscv_checks.ld.in @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * Checks part (base + layout + checks). Link-time ASSERTs that catch + * misplacement, overflow, and missing symbols in the layout part. + */ +ASSERT(DEFINED(reset_vector), + "LP core linker: reset_vector is not defined") + +ASSERT(__mtvec_base == ULP_MEM_START_ADDRESS, + "LP core linker: vector table is not at the reserved LP base") + +ASSERT(reset_vector == LP_CORE_USER_MEMORY_REGION_START, + "LP core linker: reset vector is not at the required offset") + +/* The layout must mark the end of writable data so the framework can size and + bounds-check the stack. */ +ASSERT(DEFINED(_lp_data_end), + "LP core linker: end of writable data is not marked; compose LP_CORE_DATA_END() (or assign _lp_data_end) in the layout") + +/* Default stack size: all free space between the data end and the stack top. + A layout that assigns __stack_size wins (PROVIDE yields and does not evaluate + the expression). */ +PROVIDE(__stack_size = __stack_top - _lp_data_end); + +ASSERT(__stack_top <= _lp_shared_start, + "LP core linker: application overruns into the reserved shared-memory region") + +ASSERT(__stack_top <= ULP_MEM_START_ADDRESS + ALIGNED_COPROC_MEM, + "LP core linker: image exceeds the reserved LP coprocessor memory") + +/* The stack grows down from __stack_top for __stack_size bytes; its lowest + address must not descend into the data region. */ +ASSERT(__stack_top - __stack_size >= _lp_data_end, + "LP core linker: stack region overlaps the data region (__stack_size too large or __stack_top too low)") + +#if CONFIG_ULP_LP_CORE_MEMPROT +ASSERT(DEFINED(_lp_text_end), + "LP core linker: _lp_text_end must be defined when CONFIG_ULP_LP_CORE_MEMPROT is enabled (it marks the RX/RW boundary used by the PMP setup)") +#endif diff --git a/components/ulp/ld/lp_core_riscv_default_layout.ld.in b/components/ulp/ld/lp_core_riscv_default_layout.ld.in new file mode 100644 index 00000000000..624a4429f42 --- /dev/null +++ b/components/ulp/ld/lp_core_riscv_default_layout.ld.in @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * Default layout part (base + layout + checks). Replaced by a custom layout + * when LP_CORE_LINKER_INCLUDE is set via the LINKER_LAYOUT option. Routes the + * application to HP RAM when CONFIG_ULP_COPROC_RUN_FROM_HP_MEM is enabled. + */ +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM +REGION_ALIAS("default_app_seg", hp_ram); +#else +REGION_ALIAS("default_app_seg", lp_ram); +#endif + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) +#if CONFIG_ULP_COPROC_RUN_FROM_HP_MEM + . = ORIGIN(hp_ram); +#endif + + LP_CORE_DEFAULT_TEXT(default_app_seg) + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + LP_CORE_DEFAULT_DATA(default_app_seg) + + LP_CORE_DATA_END() +} From 7c3b82562a78b401d14b2ed5aee7e4d81ebe2517 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Wed, 24 Jun 2026 09:41:47 +0200 Subject: [PATCH 2/4] feat(ulp): support custom LP-core linker layouts via the LINKER_LAYOUT option --- components/ulp/CMakeLists_v2.txt | 10 +++++ components/ulp/cmake/IDFULPProject.cmake | 38 ++++++++++++++++--- components/ulp/project_include.cmake | 48 ++++++++++++++++++++---- tools/cmakev2/utilities.cmake | 11 +++++- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/components/ulp/CMakeLists_v2.txt b/components/ulp/CMakeLists_v2.txt index e17023fc259..3bdd8fc778f 100644 --- a/components/ulp/CMakeLists_v2.txt +++ b/components/ulp/CMakeLists_v2.txt @@ -320,6 +320,16 @@ elseif(ULP_TYPE STREQUAL "lp_core") # via FLAGS. idf_component_get_property(esp_system_dir esp_system COMPONENT_DIR) set(lp_core_ld_flags "${ulp_ld_flags} -I\"${esp_system_dir}/ld\" -I\"${esp_system_dir}/ld/${target}\"") + # A user-supplied LINKER layout replaces the default one composed into + # lp_core_riscv.ld.in. It reaches this child project as LP_CORE_LINKER_SCRIPT + # (passed by __setup_ulp_project, same as the CMake v1 path). Hand it to the + # wrapper as the LP_CORE_LINKER_INCLUDE macro it #includes. The escaped quotes + # survive separate_arguments() in the linker-script preprocessor so the macro + # expands to a quoted header-name token ("/abs/path.ld"). + if(LP_CORE_LINKER_SCRIPT) + message(STATUS "Using custom LP-core linker layout: ${LP_CORE_LINKER_SCRIPT}") + string(APPEND lp_core_ld_flags " -DLP_CORE_LINKER_INCLUDE=\\\"${LP_CORE_LINKER_SCRIPT}\\\"") + endif() target_linker_script(${COMPONENT_LIB} INTERFACE "ld/lp_core_riscv.ld.in" MEMORY FLAGS "${lp_core_ld_flags}") diff --git a/components/ulp/cmake/IDFULPProject.cmake b/components/ulp/cmake/IDFULPProject.cmake index 5c447ffb925..ed082b1763f 100644 --- a/components/ulp/cmake/IDFULPProject.cmake +++ b/components/ulp/cmake/IDFULPProject.cmake @@ -32,9 +32,14 @@ function(__ulp_create_arg_file arguments output_file) endfunction() function(__ulp_add_preprocessed_linker_script ulp_app_name ld_template ld_script ld_script_target) - # Use the C preprocessor so sdkconfig and SoC constants can shape the - # linker template before the ULP executable is linked. - set(preprocessor_args -D__ASSEMBLER__ -E -P -xc -o ${ld_script} ${ARGN} ${ld_template}) + # Use the C preprocessor so sdkconfig and SoC constants can shape the linker + # template before the ULP executable is linked. -MD -MF -MT records every file + # the preprocessor reads (sdkconfig.h, SoC headers, the base/layout/checks + # parts and a custom LINKER_LAYOUT with its own includes) into a depfile, so + # the script regenerates when any of them changes. + set(ld_depfile ${CMAKE_CURRENT_BINARY_DIR}/${ld_script}.d) + set(preprocessor_args -D__ASSEMBLER__ -E -P -xc -MD -MF ${ld_depfile} -MT ${ld_script} + -o ${ld_script} ${ARGN} ${ld_template}) set(compiler_arguments_file ${CMAKE_CURRENT_BINARY_DIR}/${ld_script}_args.txt) __ulp_create_arg_file("${preprocessor_args}" "${compiler_arguments_file}") @@ -42,13 +47,21 @@ function(__ulp_add_preprocessed_linker_script ulp_app_name ld_template ld_script COMMAND ${CMAKE_C_COMPILER} @${compiler_arguments_file} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} MAIN_DEPENDENCY ${ld_template} - DEPENDS ${SDKCONFIG_HEADER} + # The response file is a dependency too: a change visible only + # in the preprocessor flags (e.g. a different LINKER layout + # path) must regenerate the script even though no file recorded + # in the depfile changed. __ulp_create_arg_file rewrites it + # only when its content actually changes, so this does not + # retrigger on every reconfigure. + DEPENDS ${compiler_arguments_file} + DEPFILE ${ld_depfile} COMMENT "Generating ${ld_script} linker script..." VERBATIM) add_custom_target(${ld_script_target} DEPENDS ${ld_script}) add_dependencies(${ulp_app_name} ${ld_script_target}) target_link_options(${ulp_app_name} PRIVATE SHELL:-T ${CMAKE_CURRENT_BINARY_DIR}/${ld_script}) + set_property(TARGET ${ulp_app_name} APPEND PROPERTY LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${ld_script}) endfunction() function(ulp_apply_default_options ulp_app_name) @@ -85,7 +98,10 @@ function(ulp_apply_default_sources ulp_app_name) target_include_directories(${ulp_app_name} PRIVATE ${COMPONENT_INCLUDES} ${sdkconfig_dir}) - # Pre-process the linker script + # Pre-process the linker script. The LP-core script is assembled from + # base + layout + checks. A user-supplied LINKER_LAYOUT (LP_CORE_LINKER_SCRIPT) + # replaces the layout, swapped in by the wrapper. This is supported for the + # LP-core type only. if(BUILD_RISCV) set(ULP_LD_TEMPLATE ${IDF_PATH}/components/ulp/ld/ulp_riscv.ld.in) elseif(BUILD_LP_CORE) @@ -96,6 +112,18 @@ function(ulp_apply_default_sources ulp_app_name) message(FATAL_ERROR "Unable to determine ULP type. ") endif() + if(LP_CORE_LINKER_SCRIPT) + # The LINKER_LAYOUT-is-LP-core-only check is enforced once in + # ulp_embed_binary/ulp_add_project, so LP_CORE_LINKER_SCRIPT only ever + # reaches an LP-core child. + message(STATUS "Using custom LP-core linker layout: ${LP_CORE_LINKER_SCRIPT}") + # The inner escaped quotes are required: the wrapper does + # `#include LP_CORE_LINKER_INCLUDE`, so the macro must expand to a quoted + # header-name token ("/abs/path.ld"). __ulp_create_arg_file() additionally + # escapes spaces in the path, keeping it a single preprocessor token. + list(APPEND ULP_PREPRO_ARGS "-DLP_CORE_LINKER_INCLUDE=\\\"${LP_CORE_LINKER_SCRIPT}\\\"") + endif() + # Strip the .in suffix so the generated script keeps its .ld name. get_filename_component(ULP_LD_SCRIPT ${ULP_LD_TEMPLATE} NAME_WLE) __ulp_add_preprocessed_linker_script(${ulp_app_name} ${ULP_LD_TEMPLATE} ${ULP_LD_SCRIPT} diff --git a/components/ulp/project_include.cmake b/components/ulp/project_include.cmake index 20f184aeecf..7ef3b4d7ea4 100644 --- a/components/ulp/project_include.cmake +++ b/components/ulp/project_include.cmake @@ -7,7 +7,7 @@ endif() # Create ULP binary and embed into the application. function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name type binary_names - s_sources exp_dep_srcs) + s_sources exp_dep_srcs linker_script) if(NOT CMAKE_BUILD_EARLY_EXPANSION) set(ulp_cmake_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake") @@ -189,6 +189,9 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name -DADD_PICOLIBC_SPECS=${CONFIG_LIBC_PICOLIBC} -DULP_VAR_PREFIX=${prefix} -DULP_TYPE=${type} + # Read by the LP-core child to swap in a custom layout; + # empty and ignored otherwise. + -DLP_CORE_LINKER_SCRIPT=${linker_script} ${ulp_project_args} -DIDF_TARGET=${idf_target} -DIDF_PATH=${idf_path} @@ -276,26 +279,52 @@ function(__ulp_resolve_type out_var type) set(${out_var} "${resolved_type}" PARENT_SCOPE) endfunction() +# Resolve a user-supplied LINKER_LAYOUT for a ULP of the given (already resolved) +# type: reject it for non-LP-core types, since only the LP-core linker script has +# a swappable layout part, then resolve the layout to an absolute path and verify +# it exists. The result is written back to the variable named by out_var. A +# relative path is resolved against CMAKE_CURRENT_LIST_DIR, the same base used for +# relative ULP source files in __setup_ulp_project, so that sources and the linker +# layout in one call are resolved consistently. Enforcing this here, at the public +# API, gives a single check that covers every ULP child build style. +function(__resolve_lp_core_linker linker type out_var) + if(linker) + if(NOT type STREQUAL "lp_core") + message(FATAL_ERROR "A custom LINKER_LAYOUT script is only supported for the LP-core ULP type.") + endif() + get_filename_component(linker "${linker}" ABSOLUTE BASE_DIR ${CMAKE_CURRENT_LIST_DIR}) + if(NOT EXISTS "${linker}") + message(FATAL_ERROR "LINKER_LAYOUT linker script not found: ${linker}") + endif() + endif() + set(${out_var} "${linker}" PARENT_SCOPE) +endfunction() + function(ulp_embed_binary app_name s_sources exp_dep_srcs) - cmake_parse_arguments(ULP "" "PREFIX;TYPE" "" ${ARGN}) + cmake_parse_arguments(ULP "" "PREFIX;TYPE;LINKER_LAYOUT" "" ${ARGN}) if(NOT ULP_PREFIX) set(ULP_PREFIX "ulp_") endif() validate_ulp_type("${ULP_TYPE}") + # Resolve the effective ULP type (from TYPE or the project config) so the + # LINKER_LAYOUT guard is applied uniformly; the full-subproject child also + # consumes the resolved type. + __ulp_resolve_type(ulp_resolved_type "${ULP_TYPE}") if(IDF_BUILD_V2) - __ulp_resolve_type(ULP_TYPE "${ULP_TYPE}") + set(ULP_TYPE "${ulp_resolved_type}") endif() + __resolve_lp_core_linker("${ULP_LINKER_LAYOUT}" "${ulp_resolved_type}" LP_CORE_LINKER) set(ulp_cmake_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake") __setup_ulp_project("${app_name}" "${ulp_cmake_dir}" "${ULP_PREFIX}" FALSE "${ULP_TYPE}" "${app_name}" - "${s_sources}" "${exp_dep_srcs}") + "${s_sources}" "${exp_dep_srcs}" "${LP_CORE_LINKER}") endfunction() function(ulp_add_project app_name project_path) - cmake_parse_arguments(ULP "" "PREFIX;TYPE" "BINARIES" ${ARGN}) + cmake_parse_arguments(ULP "" "PREFIX;TYPE;LINKER_LAYOUT" "BINARIES" ${ARGN}) if(NOT ULP_BINARIES) set(ULP_BINARIES "${app_name}") endif() @@ -311,10 +340,15 @@ function(ulp_add_project app_name project_path) endif() validate_ulp_type("${ULP_TYPE}") + # Resolve the effective ULP type (from TYPE or the project config) so the + # LINKER_LAYOUT guard is applied uniformly; the full-subproject child also + # consumes the resolved type. + __ulp_resolve_type(ulp_resolved_type "${ULP_TYPE}") if(IDF_BUILD_V2) - __ulp_resolve_type(ULP_TYPE "${ULP_TYPE}") + set(ULP_TYPE "${ulp_resolved_type}") endif() + __resolve_lp_core_linker("${ULP_LINKER_LAYOUT}" "${ulp_resolved_type}" LP_CORE_LINKER) __setup_ulp_project("${app_name}" "${project_path}" "${ULP_PREFIX}" - "${ULP_PREFIX_APPEND_BIN_NAME}" "${ULP_TYPE}" "${ULP_BINARIES}" "" "") + "${ULP_PREFIX_APPEND_BIN_NAME}" "${ULP_TYPE}" "${ULP_BINARIES}" "" "" "${LP_CORE_LINKER}") endfunction() diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index e0b655da583..a077ccb5f0b 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -1279,16 +1279,25 @@ function(__preprocess_linker_script script_in script_out flags component_include set(linker_script_generator "${idf_path}/tools/cmake/linker_script_preprocessor.cmake") + # -MD -MF -MT makes the C preprocessor record every file it reads (the + # template plus everything it #includes, transitively) into a depfile, so + # the output is regenerated when any of them changes. MAIN_DEPENDENCY and + # DEPENDS only cover the top-level script and sdkconfig; the depfile closes + # the gap for includes the build system cannot enumerate up front. + set(depfile "${script_out}.d") + set(depfile_flags "-MD -MF \"${depfile}\" -MT \"${script_out}\"") + add_custom_command( OUTPUT ${script_out} COMMAND ${CMAKE_COMMAND} "-DCC=${CMAKE_C_COMPILER}" "-DSOURCE=${script_in}" "-DTARGET=${script_out}" - "-DCFLAGS=-I\"${config_dir}\" ${base_flags} ${component_includes}" + "-DCFLAGS=-I\"${config_dir}\" ${depfile_flags} ${base_flags} ${component_includes}" -P "${linker_script_generator}" MAIN_DEPENDENCY "${script_in}" DEPENDS "${sdkconfig_header}" + DEPFILE "${depfile}" COMMENT "Preprocessing linker script ${script_in} -> ${script_out}" VERBATIM) endfunction() From 06963baf0b888721ea08383db92a0edeb896bc02 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Wed, 24 Jun 2026 10:10:07 +0200 Subject: [PATCH 3/4] test(ulp): add custom linker host tests Add build-only host tests for LP-core custom linker layouts supplied through the LINKER option. Cover the positive paths (a full-replacement layout, a multi-region layout with a fixed-address section, a run-from-HP-mem layout, and memory protection) and the link-time and configure-time checks (reset-vector placement, sizing, shared-memory overrun, the MEMPROT RX/RW boundary, a missing reset vector, and a missing LINKER script). --- .gitlab/ci/dependencies/dependencies.yml | 6 + .gitlab/ci/host-test.yml | 16 ++ .gitlab/ci/rules.yml | 24 ++ .idf_ci.toml | 2 + .../ulp/test_apps/.build-test-rules.yml | 12 + .../lp_core_custom_linker/CMakeLists.txt | 7 + .../lp_core_custom_linker/README.md | 10 + .../lp_core_custom_linker/main/CMakeLists.txt | 14 + .../lp_core_custom_linker/main/shared.h | 13 + .../lp_core_custom_linker/main/test_main.c | 9 + .../main/ulp/layouts/bad_no_data_end.ld | 32 +++ .../main/ulp/layouts/bad_no_reset_vector.ld | 32 +++ .../main/ulp/layouts/bad_no_text_end.ld | 32 +++ .../main/ulp/layouts/bad_origin.ld | 32 +++ .../main/ulp/layouts/bad_oversize.ld | 34 +++ .../main/ulp/layouts/bad_shared_overrun.ld | 35 +++ .../main/ulp/layouts/bad_stack_oversize.ld | 35 +++ .../main/ulp/layouts/good.ld | 31 +++ .../main/ulp/layouts/good_hp_mem.ld | 34 +++ .../main/ulp/layouts/good_multiregion.ld | 41 +++ .../main/ulp/layouts/good_stack_override.ld | 39 +++ .../main/ulp/layouts/good_stack_window.ld | 49 ++++ .../main/ulp/layouts/good_switch.ld | 32 +++ .../lp_core_custom_linker/main/ulp/main.c | 25 ++ .../lp_core_custom_linker/sdkconfig.defaults | 3 + .../test_lp_core_custom_linker.py | 262 ++++++++++++++++++ .../lp_core_custom_linker_v2/CMakeLists.txt | 12 + .../lp_core_custom_linker_v2/README.md | 12 + .../main/CMakeLists.txt | 15 + .../lp_core_custom_linker_v2/main/test_main.c | 9 + .../main/ulp/CMakeLists.txt | 9 + .../main/ulp/layouts/good.ld | 31 +++ .../main/ulp/layouts/good_switch.ld | 32 +++ .../main/ulp/main/CMakeLists.txt | 4 + .../main/ulp/main/main.c | 23 ++ .../sdkconfig.defaults | 3 + .../test_lp_core_custom_linker_v2.py | 96 +++++++ 37 files changed, 1107 insertions(+) create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/README.md create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/shared.h create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/test_main.c create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_data_end.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_reset_vector.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_text_end.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_origin.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_oversize.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_shared_overrun.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_stack_oversize.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_hp_mem.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_multiregion.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_override.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_window.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_switch.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/main.c create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/sdkconfig.defaults create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/test_lp_core_custom_linker.py create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/README.md create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/test_main.c create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good_switch.ld create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/CMakeLists.txt create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/main.c create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/sdkconfig.defaults create mode 100644 components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/test_lp_core_custom_linker_v2.py diff --git a/.gitlab/ci/dependencies/dependencies.yml b/.gitlab/ci/dependencies/dependencies.yml index 67e715caebe..bb28b1d435c 100644 --- a/.gitlab/ci/dependencies/dependencies.yml +++ b/.gitlab/ci/dependencies/dependencies.yml @@ -59,6 +59,12 @@ patterns: - host_test_ldgen +"test:host_test_ulp_linker": + labels: + - host_test + patterns: + - host_test_ulp_linker + "test:host_test_cli_installer": labels: - host_test diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index e9b5de09e9b..373cdeba4f7 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -107,6 +107,22 @@ test_ldgen_on_host: variables: LC_ALL: C.UTF-8 +test_lp_core_custom_linker_on_host: + extends: + - .host_test_template + - .rules:test:host_test_ulp_linker + script: + - cd components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker + - pytest_for_ut . + +test_lp_core_custom_linker_buildv2_on_host: + extends: + - .host_test_template + - .rules:test:host_test_ulp_linker + script: + - cd components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2 + - pytest_for_ut . + test_spiffs_on_host: extends: .host_test_template script: diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index a7c5a707820..27c0e086d8c 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -48,6 +48,19 @@ - "tools/ldgen/**/*" +.patterns-host_test_ulp_linker: &patterns-host_test_ulp_linker + - ".gitlab/ci/host-test.yml" + + - "components/ulp/ld/**/*" + - "components/ulp/cmake/**/*" + - "components/ulp/CMakeLists*.txt" + - "components/ulp/component_ulp_common.cmake" + - "components/ulp/project_include.cmake" + - "components/ulp/lp_core/**/*" + - "components/ulp/test_apps/lp_core/host_tests/**/*" + # Shared linker-script preprocessing used by the buildv2 ULP path. + - "tools/cmakev2/utilities.cmake" + .patterns-build_system_win: &patterns-build_system_win - "tools/test_build_system/**/*" @@ -395,6 +408,17 @@ - <<: *if-dev-push changes: *patterns-host_test_tools +.rules:test:host_test_ulp_linker: + rules: + - <<: *if-revert-branch + when: never + - <<: *if-protected-check + - <<: *if-label-build-only + when: never + - <<: *if-label-host_test + - <<: *if-dev-push + changes: *patterns-host_test_ulp_linker + .rules:test:submodule: rules: - <<: *if-revert-branch diff --git a/.idf_ci.toml b/.idf_ci.toml index c79ce7dc432..83ee84649f9 100644 --- a/.idf_ci.toml +++ b/.idf_ci.toml @@ -10,6 +10,8 @@ exclude_dirs = [ 'tools/ci/test_autocomplete', 'tools/test_build_system', 'tools/bt/ble_log_console', + 'components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker', + 'components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2', ] [local_runtime_envs] diff --git a/components/ulp/test_apps/.build-test-rules.yml b/components/ulp/test_apps/.build-test-rules.yml index 8d8d7163435..cb62926709c 100644 --- a/components/ulp/test_apps/.build-test-rules.yml +++ b/components/ulp/test_apps/.build-test-rules.yml @@ -1,5 +1,17 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps +components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker: + enable: + - if: SOC_LP_CORE_SUPPORTED == 1 + depends_components: + - ulp + +components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2: + enable: + - if: SOC_LP_CORE_SUPPORTED == 1 + depends_components: + - ulp + components/ulp/test_apps/lp_core/lp_core_basic_tests: disable: - if: SOC_LP_CORE_SUPPORTED != 1 diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/CMakeLists.txt new file mode 100644 index 00000000000..89b4ee3adf3 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(lp_core_custom_linker) diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/README.md b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/README.md new file mode 100644 index 00000000000..d992e10e022 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/README.md @@ -0,0 +1,10 @@ +| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-P4 | ESP32-S31 | +| ----------------- | -------- | -------- | -------- | --------- | + +# LP Core Custom Linker Host Tests + +Build-only host tests for the ULP custom linker feature. No target board is required: every check is resolved at configure or link time on the build host. + +`test_lp_core_custom_linker.py` builds this app with different layouts supplied via the `LINKER_LAYOUT` option and asserts on the outcome (both the positive paths and the layouts that must fail the build). + +Being a build-time test, it drives `idf.py` itself and is run as plain pytest by the dedicated `test_lp_core_custom_linker_on_host` host-test job, outside the idf-ci flow (see `exclude_dirs` in `.idf_ci.toml`). Run it locally with `pytest_for_ut .` from this directory, or directly with `pytest -p no:idf-ci -p no:pytest_embedded .`. diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/CMakeLists.txt new file mode 100644 index 00000000000..f3304a256c0 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +idf_component_register(SRCS "test_main.c" + INCLUDE_DIRS "." + REQUIRES ulp) + +# Pytest overrides CUSTOM_ULP_LAYOUT per test case via -DCUSTOM_ULP_LAYOUT=... +if(NOT DEFINED CUSTOM_ULP_LAYOUT) + set(CUSTOM_ULP_LAYOUT "${CMAKE_CURRENT_LIST_DIR}/ulp/layouts/good.ld") +endif() + +set(ulp_app_name ulp_${COMPONENT_NAME}) +ulp_embed_binary(${ulp_app_name} "ulp/main.c" "test_main.c" + LINKER_LAYOUT "${CUSTOM_ULP_LAYOUT}") diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/shared.h b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/shared.h new file mode 100644 index 00000000000..acec954ae8d --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/shared.h @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +typedef struct { + volatile uint32_t marker; + volatile uint32_t lp_heartbeat; +} custom_linker_shared_t; diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/test_main.c b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/test_main.c new file mode 100644 index 00000000000..c4d4598eceb --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/test_main.c @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/* Stub app_main — build-only test. The pytest harness checks the ULP ELF. */ +void app_main(void) +{ +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_data_end.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_data_end.ld new file mode 100644 index 00000000000..74a22d55851 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_data_end.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: omits LP_CORE_DATA_END() and never assigns _lp_data_end, so the + * framework cannot size or bounds-check the stack. The _lp_data_end contract + * ASSERT must fire. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + /* Intentionally missing: LP_CORE_DATA_END() (marks _lp_data_end) */ +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_reset_vector.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_reset_vector.ld new file mode 100644 index 00000000000..d5b6581f26c --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_reset_vector.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: omits LP_CORE_TEXT_START entirely (it places the reset vector). + * reset_vector then lands away from the boot offset and the position ASSERT + * must fire. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + /* Intentionally missing: LP_CORE_TEXT_START(lp_ram) (places the reset vector) */ + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_text_end.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_text_end.ld new file mode 100644 index 00000000000..6c0e2a57ee9 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_no_text_end.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: omits LP_CORE_TEXT_END(). Built with CONFIG_ULP_LP_CORE_MEMPROT=y, + * where lp_core_pmp.c needs the RX/RW boundary for the PMP split. + * Must fail with the MEMPROT ASSERT. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + /* Intentionally missing: LP_CORE_TEXT_END() */ + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_origin.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_origin.ld new file mode 100644 index 00000000000..0b3e9cd6276 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_origin.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: the region starts at LP_CORE_USER_MEMORY_REGION_START + 0x180 + * instead of LP_CORE_USER_MEMORY_REGION_START, so LP_CORE_TEXT_START places the + * reset vector at the wrong address and the reset-vector position ASSERT fires. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START + 0x180, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START - 0x180 +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_oversize.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_oversize.ld new file mode 100644 index 00000000000..b8c7a2877c9 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_oversize.ld @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: lp_ram LENGTH == ALIGNED_COPROC_MEM (too large). The __stack_top + * override lands past the reserved coprocessor memory, so the sizing ASSERT + * must fire. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = ALIGNED_COPROC_MEM +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() + + __stack_top = ORIGIN(lp_ram) + LENGTH(lp_ram); +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_shared_overrun.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_shared_overrun.ld new file mode 100644 index 00000000000..288343c256c --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_shared_overrun.ld @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: lp_ram extends to the end of the coprocessor reservation (no + * shared-mem gap), so the __stack_top override exceeds _lp_shared_start. The + * image still fits the absolute reservation, but the shared-overrun ASSERT + * must fire. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = ALIGNED_COPROC_MEM - ULP_VECTOR_TABLE_LENGTH +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() + + __stack_top = ORIGIN(lp_ram) + LENGTH(lp_ram); +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_stack_oversize.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_stack_oversize.ld new file mode 100644 index 00000000000..e138a72d58c --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/bad_stack_oversize.ld @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * NEGATIVE: overrides __stack_size to a value larger than the free space above + * the data end, so the stack's lowest address falls inside the data region. + * The bottom-bounds ASSERT must fire. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() + + /* Stack larger than the whole window: bottom lands below _lp_data_end. */ + __stack_size = (LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START) + 0x100; +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good.ld new file mode 100644 index 00000000000..7fddbabc81b --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good.ld @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: full-replacement layout. Composes LP_CORE_TEXT_START and the + * section-boundary macros; shared memory and __stack_top are owned by the base. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_hp_mem.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_hp_mem.ld new file mode 100644 index 00000000000..af5e272a725 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_hp_mem.ld @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: custom layout under CONFIG_ULP_COPROC_RUN_FROM_HP_MEM. Keeps the + * reset vector in LP RAM and routes the application to hp_ram, which is + * declared by the base part. __stack_top defaults to the top of the window. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + . = ORIGIN(hp_ram); + + .text ALIGN(4): { *(.text) *(.text*) } > hp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > hp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > hp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > hp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > hp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_multiregion.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_multiregion.ld new file mode 100644 index 00000000000..9f92a30d2d5 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_multiregion.ld @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: multi-region layout. Splits the window into custom-named regions + * using only LP_CORE_USER_MEMORY_REGION_START / _END. __stack_top defaults to + * the top of the window (the data region reaches it). + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +_fixed_addr = LP_CORE_USER_MEMORY_REGION_START + 0x700; + +MEMORY +{ + code(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = _fixed_addr - LP_CORE_USER_MEMORY_REGION_START + fixed(RWX) : ORIGIN = _fixed_addr, LENGTH = 0x100 + data(RW) : ORIGIN = _fixed_addr + 0x100, LENGTH = LP_CORE_USER_MEMORY_REGION_END - (_fixed_addr + 0x100) +} + +SECTIONS +{ + LP_CORE_TEXT_START(code) + + .text ALIGN(4): { *(.text) *(.text*) } > code + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > code + + LP_CORE_TEXT_END() + + . = ORIGIN(fixed); + .fixed ALIGN(4): { KEEP(*(.fixed_region .fixed_region.*)) } > fixed + + . = ORIGIN(data); + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > data + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > data + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > data + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_override.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_override.ld new file mode 100644 index 00000000000..3b5fedc8284 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_override.ld @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: overrides __stack_top and __stack_size to values that differ from + * the base defaults (default __stack_top is the window end, default + * __stack_size is all the free space above the data). The stack top is pulled + * 0x40 below the window end and the size is pinned to 0x200, so the test can + * confirm the layout's assignments win over the base PROVIDE() defaults. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() + + /* Distinct, valid overrides: reserve 0x40 at the top of the window and cap + the stack at 0x200 instead of taking all the remaining space. */ + __stack_top = LP_CORE_USER_MEMORY_REGION_END - 0x40; + __stack_size = 0x200; +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_window.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_window.ld new file mode 100644 index 00000000000..b35cfd82450 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_stack_window.ld @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: esp-bist-style layout that takes ownership of the stack window. + * It ends its data with a NOLOAD stack-overflow guard band, marks the data end + * with _lp_data_end, and overrides both __stack_top and __stack_size (the size + * being all the space left between the data end and the stack top). The + * framework's bottom-bounds ASSERT confirms the stack does not reach the data. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + /* Stack-overflow guard band, kept out of the loadable image. */ + .stack_guard (NOLOAD): + { + . = ALIGN(4); + . += 0x8; + _stack_overflow_protection_start = .; + . += 0x4; + _stack_overflow_protection_end = .; + } > lp_ram + + LP_CORE_DATA_END() + + /* Take ownership of the stack window: stack starts at the top of lp_ram and + may grow down over all the remaining free space above the data end. */ + __stack_top = ORIGIN(lp_ram) + LENGTH(lp_ram); + __stack_size = __stack_top - _lp_data_end; +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_switch.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_switch.ld new file mode 100644 index 00000000000..cc75c10eebe --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/layouts/good_switch.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: same shape as good.ld but with a distinct marker value. Used by the + * incremental-switch test to tell which layout the image was linked against + * after the LINKER option is changed without wiping the build directory. + */ +PROVIDE(_custom_linker_marker = 0xD00DFEED); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/main.c b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/main.c new file mode 100644 index 00000000000..ee12597c0f7 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/main/ulp/main.c @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include "shared.h" + +/* Defined by the custom layout via PROVIDE(); reference keeps it from --gc-sections. */ +extern const uint32_t _custom_linker_marker; + +custom_linker_shared_t shared_state __attribute__((used, section(".custom_state"))); + +int main(void) +{ + shared_state.marker = (uint32_t)(uintptr_t)&_custom_linker_marker; + + while (1) { + shared_state.lp_heartbeat++; + for (volatile uint32_t i = 0; i < 10000; i++) { + __asm__ volatile("nop"); + } + } + return 0; +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/sdkconfig.defaults b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/sdkconfig.defaults new file mode 100644 index 00000000000..1f3cae5037f --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_ULP_COPROC_ENABLED=y +CONFIG_ULP_COPROC_TYPE_LP_CORE=y +CONFIG_ULP_COPROC_RESERVE_MEM=4096 diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/test_lp_core_custom_linker.py b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/test_lp_core_custom_linker.py new file mode 100644 index 00000000000..aa78a53b039 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker/test_lp_core_custom_linker.py @@ -0,0 +1,262 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +# +# Host build tests for custom LP-core linker layouts. Each test builds this app +# with a different layout via the LINKER_LAYOUT option and checks the result: +# positive cases verify the build succeeds and the marker symbol resolves; +# negative cases verify that the expected ASSERT fires and the build fails. +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile + +import pytest +from esp_bool_parser import parse_bool_expr +from esp_bool_parser.constants import PREVIEW_TARGETS +from esp_bool_parser.constants import SUPPORTED_TARGETS + +APP_DIR = os.path.dirname(os.path.abspath(__file__)) +LAYOUTS = os.path.join(APP_DIR, 'main', 'ulp', 'layouts') + + +def _targets(soc_expr: str) -> list[str]: + # Evaluate a SoC-caps expression against every known target, so the set + # tracks the config as the LP core lands on new targets. + expr = parse_bool_expr(soc_expr) + return [t for t in (*SUPPORTED_TARGETS, *PREVIEW_TARGETS) if expr.get_value(t, '')] + + +LP_CORE_TARGETS = _targets('SOC_LP_CORE_SUPPORTED == 1') +# MEMPROT needs the LP core PMP. +LP_CORE_MEMPROT_TARGETS = _targets('SOC_LP_CORE_SUPPORTED == 1 and SOC_LP_CORE_HAS_PMP == 1') +HP_MEM_DEFAULTS = 'CONFIG_ULP_COPROC_RUN_FROM_HP_MEM=y\nCONFIG_ULP_COPROC_RESERVE_HP_MEM_BYTES=0x4000' + + +def _build(tag: str, custom_layout: str, target: str, extra_defaults: str | None = None) -> tuple[int, str, str]: + build_dir = os.path.join(APP_DIR, f'build_{tag}_{target}') + shutil.rmtree(build_dir, ignore_errors=True) + # --preview keeps the command uniform across released and preview targets. + # Keep sdkconfig inside the build dir so parametrized targets never share (or + # stale-contaminate) the app-dir sdkconfig. + common = [ + 'idf.py', + '--preview', + '-B', + build_dir, + f'-DSDKCONFIG={os.path.join(build_dir, "sdkconfig")}', + f'-DCUSTOM_ULP_LAYOUT={custom_layout}', + ] + combined: str | None = None + if extra_defaults: + fd, combined = tempfile.mkstemp(suffix='.defaults') + with os.fdopen(fd, 'w') as out_f: + with open(os.path.join(APP_DIR, 'sdkconfig.defaults')) as base_f: + out_f.write(base_f.read()) + out_f.write('\n' + extra_defaults + '\n') + common.append(f'-DSDKCONFIG_DEFAULTS={combined}') + try: + cfg = subprocess.run(common + ['set-target', target], cwd=APP_DIR, capture_output=True, text=True) + out = cfg.stdout + cfg.stderr + if cfg.returncode != 0: + return cfg.returncode, out, build_dir + bld = subprocess.run(common + ['build'], cwd=APP_DIR, capture_output=True, text=True) + return bld.returncode, out + bld.stdout + bld.stderr, build_dir + finally: + if combined: + os.unlink(combined) + + +def _rebuild(build_dir: str, custom_layout: str) -> tuple[int, str]: + # Incremental reconfigure + build of an existing build dir with a different + # LINKER_LAYOUT: no wipe, no re-run of set-target. This is the path that must + # still pick up the new layout. + r = subprocess.run( + [ + 'idf.py', + '-B', + build_dir, + f'-DSDKCONFIG={os.path.join(build_dir, "sdkconfig")}', + f'-DCUSTOM_ULP_LAYOUT={custom_layout}', + 'build', + ], + cwd=APP_DIR, + capture_output=True, + text=True, + ) + return r.returncode, r.stdout + r.stderr + + +def _nm(build_dir: str) -> str: + # The ULP ELF is emitted at a fixed path under the build dir. + elf = os.path.join(build_dir, 'esp-idf', 'main', 'ulp_main', 'ulp_main.elf') + assert os.path.isfile(elf), 'ULP elf was not produced' + return subprocess.run(['riscv32-esp-elf-nm', elf], capture_output=True, text=True).stdout + + +def _symbols(build_dir: str) -> dict[str, int]: + # nm prints " "; keep the defined symbols we can resolve. + syms: dict[str, int] = {} + for line in _nm(build_dir).splitlines(): + parts = line.split() + if len(parts) == 3 and all(c in '0123456789abcdefABCDEF' for c in parts[0]): + syms[parts[2]] = int(parts[0], 16) + return syms + + +def _assert_marker(syms: dict[str, int]) -> None: + assert '_custom_linker_marker' in syms, 'custom marker symbol absent' + assert syms['_custom_linker_marker'] == 0xCAFEBABE, 'custom marker not resolved to its PROVIDE value' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_positive_custom_layout(target: str) -> None: + rc, out, build_dir = _build('good', os.path.join(LAYOUTS, 'good.ld'), target) + assert rc == 0, f'custom layout should build:\n{out[-2000:]}' + _assert_marker(_symbols(build_dir)) + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_positive_multiregion_layout(target: str) -> None: + rc, out, build_dir = _build('good_multiregion', os.path.join(LAYOUTS, 'good_multiregion.ld'), target) + assert rc == 0, f'multi-region custom layout should build:\n{out[-2000:]}' + _assert_marker(_symbols(build_dir)) + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_positive_hp_mem_custom_layout(target: str) -> None: + rc, out, build_dir = _build( + 'good_hp_mem', os.path.join(LAYOUTS, 'good_hp_mem.ld'), target, extra_defaults=HP_MEM_DEFAULTS + ) + assert rc == 0, f'custom layout routing to HP RAM should build:\n{out[-2000:]}' + _assert_marker(_symbols(build_dir)) + + +@pytest.mark.parametrize('target', LP_CORE_MEMPROT_TARGETS) +def test_positive_custom_layout_memprot(target: str) -> None: + rc, out, build_dir = _build( + 'good_memprot', os.path.join(LAYOUTS, 'good.ld'), target, extra_defaults='CONFIG_ULP_LP_CORE_MEMPROT=y' + ) + assert rc == 0, f'custom layout defining _lp_text_end should build under MEMPROT:\n{out[-2000:]}' + _assert_marker(_symbols(build_dir)) + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_positive_stack_window_override(target: str) -> None: + rc, out, build_dir = _build('good_stack_window', os.path.join(LAYOUTS, 'good_stack_window.ld'), target) + assert rc == 0, f'layout overriding __stack_top/__stack_size should build:\n{out[-2000:]}' + syms = _symbols(build_dir) + _assert_marker(syms) + for sym in ('__stack_top', '__stack_size', '_lp_data_end'): + assert sym in syms, f'{sym} symbol absent from the linked image' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_positive_stack_override_takes_effect(target: str) -> None: + # A layout that sets __stack_top/__stack_size to values distinct from the + # base defaults must win: the linked symbols resolve to the layout's values, + # proving the base PROVIDE() defaults yield. + rc, out, build_dir = _build('good_stack_override', os.path.join(LAYOUTS, 'good_stack_override.ld'), target) + assert rc == 0, f'layout with distinct stack overrides should build:\n{out[-2000:]}' + syms = _symbols(build_dir) + _assert_marker(syms) + for sym in ('__stack_top', '__stack_size', '_lp_shared_start', '_lp_data_end'): + assert sym in syms, f'{sym} symbol absent from the linked image' + window_end = syms['_lp_shared_start'] # the base default for __stack_top + # __stack_top overridden to window_end - 0x40 (not the default window end). + assert syms['__stack_top'] == window_end - 0x40, ( + f'__stack_top override ignored: {syms["__stack_top"]:#x} != {window_end - 0x40:#x}' + ) + assert syms['__stack_top'] != window_end, '__stack_top still at the default (override did not win)' + # __stack_size overridden to 0x200 (not the free-space default). + assert syms['__stack_size'] == 0x200, f'__stack_size override ignored: {syms["__stack_size"]:#x} != 0x200' + default_size = syms['__stack_top'] - syms['_lp_data_end'] + assert syms['__stack_size'] != default_size, '__stack_size still at the default (override did not win)' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_incremental_layout_switch_picks_up_new_layout(target: str) -> None: + # The LINKER_LAYOUT path reaches the preprocessor only as a flag inside the + # response file, and a committed layout file's mtime is normally older than + # the already-generated script. Switching the layout on an incremental build + # (no wipe) must still regenerate the linker script; otherwise the image + # silently links against the previous layout. + rc, out, build_dir = _build('switch', os.path.join(LAYOUTS, 'good.ld'), target) + assert rc == 0, f'initial build with good.ld should succeed:\n{out[-2000:]}' + assert _symbols(build_dir)['_custom_linker_marker'] == 0xCAFEBABE, 'initial layout not linked' + + # Age the target layout so it is older than the script the first build just + # generated -- the mtime relationship that hides the change from the build. + switch_layout = os.path.join(LAYOUTS, 'good_switch.ld') + old = 1600000000 # 2020-09-13, safely older than any build artifact + os.utime(switch_layout, (old, old)) + + rc, out = _rebuild(build_dir, switch_layout) + assert rc == 0, f'incremental rebuild after switching the layout should succeed:\n{out[-2000:]}' + marker = _symbols(build_dir)['_custom_linker_marker'] + assert marker == 0xD00DFEED, ( + f'stale linker script: switching the LINKER_LAYOUT on an incremental build did not ' + f'regenerate the script (marker still {marker:#x}, expected 0xd00dfeed)' + ) + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_stack_size_too_large(target: str) -> None: + rc, out, _ = _build('bad_stack_oversize', os.path.join(LAYOUTS, 'bad_stack_oversize.ld'), target) + assert rc != 0, 'a __stack_size that overruns the data region must fail the build' + assert 'stack region overlaps the data region' in out, 'bottom-bounds stack check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_missing_data_end(target: str) -> None: + rc, out, _ = _build('bad_no_data_end', os.path.join(LAYOUTS, 'bad_no_data_end.ld'), target) + assert rc != 0, 'a layout not marking _lp_data_end must fail the build' + assert 'end of writable data is not marked' in out, 'the _lp_data_end contract check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_MEMPROT_TARGETS) +def test_negative_missing_text_end_under_memprot(target: str) -> None: + rc, out, _ = _build( + 'bad_no_text_end', + os.path.join(LAYOUTS, 'bad_no_text_end.ld'), + target, + extra_defaults='CONFIG_ULP_LP_CORE_MEMPROT=y', + ) + assert rc != 0, 'a MEMPROT layout missing _lp_text_end must fail the build' + assert '_lp_text_end must be defined' in out, 'the _lp_text_end MEMPROT check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_reset_vector_check_fails_build(target: str) -> None: + rc, out, _ = _build('bad_origin', os.path.join(LAYOUTS, 'bad_origin.ld'), target) + assert rc != 0, 'a misplaced lp_ram origin must fail the build' + assert 'reset vector is not at the required offset' in out, 'reset-vector check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_sizing_check_fails_build(target: str) -> None: + rc, out, _ = _build('bad_oversize', os.path.join(LAYOUTS, 'bad_oversize.ld'), target) + assert rc != 0, 'an oversized lp_ram must fail the build' + assert 'exceeds the reserved LP coprocessor memory' in out, 'sizing check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_shared_overrun_check_fails_build(target: str) -> None: + rc, out, _ = _build('bad_shared', os.path.join(LAYOUTS, 'bad_shared_overrun.ld'), target) + assert rc != 0, 'a stack overrunning the shared region must fail the build' + assert 'overruns into the reserved shared-memory region' in out, 'shared-overrun guardrail did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_missing_reset_vector(target: str) -> None: + rc, out, _ = _build('bad_no_rv', os.path.join(LAYOUTS, 'bad_no_reset_vector.ld'), target) + assert rc != 0, 'omitting LP_CORE_TEXT_START must fail the build' + assert 'reset vector is not at the required offset' in out, 'reset-vector position check did not fire' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_negative_missing_linker_rejected_at_configure(target: str) -> None: + rc, out, _ = _build('missing', '/nonexistent/custom_layout.ld', target) + assert rc != 0, 'a non-existent LINKER_LAYOUT path must be rejected' + assert 'LINKER_LAYOUT linker script not found' in out, 'API path validation did not fire' diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/CMakeLists.txt new file mode 100644 index 00000000000..b12714d1c6c --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +cmake_minimum_required(VERSION 3.22) + +# v2-native parent: the ULP program is built as a full CMake v2 subproject +# (ulp_project_default), so the custom LINKER layout is exercised through the +# buildv2 linker-script path rather than the legacy CMake v1 one. +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(lp_core_custom_linker_v2 C CXX ASM) + +idf_project_default() diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/README.md b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/README.md new file mode 100644 index 00000000000..8b666af7964 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/README.md @@ -0,0 +1,12 @@ +| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-P4 | ESP32-S31 | +| ----------------- | -------- | -------- | -------- | --------- | + +# LP Core Custom Linker Host Tests (CMake v2 full subproject) + +Build-only host tests for the ULP custom linker feature under the **CMake v2** build system. The parent app is v2-native (its `CMakeLists.txt` includes `tools/cmakev2/idf.cmake`) and the ULP program is built as a full subproject via `ulp_project_default()`, so these tests exercise the buildv2 linker-script path (`components/ulp/CMakeLists_v2.txt`) rather than the CMake v1 one covered by `../lp_core_custom_linker`. + +No target board is required: every check is resolved at build time on the host. + +`test_lp_core_custom_linker_v2.py` builds this app with layouts supplied via the `LINKER_LAYOUT` option of `ulp_add_project` and asserts on the linked ULP image — that a custom layout is applied, and that switching the layout on an incremental build relinks against the new one. + +Being a build-time test, it drives `idf.py` itself and is run as plain pytest by the dedicated `test_lp_core_custom_linker_buildv2_on_host` host-test job, outside the idf-ci flow (see `exclude_dirs` in `.idf_ci.toml`). Run it locally with `pytest_for_ut .` from this directory, or directly with `pytest -p no:idf-ci -p no:pytest_embedded .`. diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/CMakeLists.txt new file mode 100644 index 00000000000..74966c30861 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +idf_component_register(SRCS "test_main.c" + REQUIRES ulp) + +# Pytest overrides CUSTOM_ULP_LAYOUT per test case via -DCUSTOM_ULP_LAYOUT=... +if(NOT DEFINED CUSTOM_ULP_LAYOUT) + set(CUSTOM_ULP_LAYOUT "${CMAKE_CURRENT_LIST_DIR}/ulp/layouts/good.ld") +endif() + +# Build the ULP program as a full CMake v2 subproject (see ulp/CMakeLists.txt), +# supplying the custom memory layout through the LINKER_LAYOUT option. +ulp_add_project("ulp_main" "${CMAKE_CURRENT_LIST_DIR}/ulp/" + TYPE lp_core + LINKER_LAYOUT "${CUSTOM_ULP_LAYOUT}") diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/test_main.c b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/test_main.c new file mode 100644 index 00000000000..b663e9092e4 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/test_main.c @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/* Stub app_main -- build-only test. The pytest harness checks the ULP ELF. */ +void app_main(void) +{ +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/CMakeLists.txt new file mode 100644 index 00000000000..46cc1d8f144 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +cmake_minimum_required(VERSION 3.22) + +include(${IDF_PATH}/components/ulp/cmake/ulp_project.cmake) + +project(${IDF_DEFAULT_PROJECT_NAME} C CXX ASM) + +ulp_project_default() diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good.ld new file mode 100644 index 00000000000..7fddbabc81b --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good.ld @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: full-replacement layout. Composes LP_CORE_TEXT_START and the + * section-boundary macros; shared memory and __stack_top are owned by the base. + */ +PROVIDE(_custom_linker_marker = 0xCAFEBABE); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good_switch.ld b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good_switch.ld new file mode 100644 index 00000000000..cc75c10eebe --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/layouts/good_switch.ld @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + * + * POSITIVE: same shape as good.ld but with a distinct marker value. Used by the + * incremental-switch test to tell which layout the image was linked against + * after the LINKER option is changed without wiping the build directory. + */ +PROVIDE(_custom_linker_marker = 0xD00DFEED); + +MEMORY +{ + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START +} + +SECTIONS +{ + LP_CORE_TEXT_START(lp_ram) + + .text ALIGN(4): { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4): { *(.rodata) *(.rodata*) } > lp_ram + + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + + .data ALIGN(4): { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4): { *(.bss) *(.bss*) *(.sbss) *(.sbss*) PROVIDE(end = .); } > lp_ram + + .custom_state ALIGN(4): { KEEP(*(.custom_state)) } > lp_ram + + LP_CORE_DATA_END() +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/CMakeLists.txt b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/CMakeLists.txt new file mode 100644 index 00000000000..7a031e58974 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +idf_component_register(SRCS "main.c" + REQUIRES ulp) diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/main.c b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/main.c new file mode 100644 index 00000000000..2146b71e944 --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/main/ulp/main/main.c @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +/* Defined by the custom layout via PROVIDE(); reference keeps it from --gc-sections. */ +extern const uint32_t _custom_linker_marker; + +volatile uint32_t custom_marker_copy __attribute__((used, section(".custom_state"))); + +int main(void) +{ + custom_marker_copy = (uint32_t)(uintptr_t)&_custom_linker_marker; + + while (1) { + custom_marker_copy++; + for (volatile uint32_t i = 0; i < 10000; i++) { + __asm__ volatile("nop"); + } + } + return 0; +} diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/sdkconfig.defaults b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/sdkconfig.defaults new file mode 100644 index 00000000000..1f3cae5037f --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_ULP_COPROC_ENABLED=y +CONFIG_ULP_COPROC_TYPE_LP_CORE=y +CONFIG_ULP_COPROC_RESERVE_MEM=4096 diff --git a/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/test_lp_core_custom_linker_v2.py b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/test_lp_core_custom_linker_v2.py new file mode 100644 index 00000000000..15638d8e24d --- /dev/null +++ b/components/ulp/test_apps/lp_core/host_tests/lp_core_custom_linker_v2/test_lp_core_custom_linker_v2.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +# +# Host build tests for custom LP-core linker layouts under the CMake v2 +# full-subproject build. The ULP program is built through ulp_project_default(), +# so these exercise the buildv2 linker-script path +# (components/ulp/CMakeLists_v2.txt), the counterpart of the CMake v1 tests in +# ../lp_core_custom_linker. +from __future__ import annotations + +import os +import shutil +import subprocess + +import pytest +from esp_bool_parser import parse_bool_expr +from esp_bool_parser.constants import PREVIEW_TARGETS +from esp_bool_parser.constants import SUPPORTED_TARGETS + +APP_DIR = os.path.dirname(os.path.abspath(__file__)) +LAYOUTS = os.path.join(APP_DIR, 'main', 'ulp', 'layouts') + +# Evaluate the SoC-caps expression against every known target so the set tracks +# the config as the LP core lands on new targets. +_expr = parse_bool_expr('SOC_LP_CORE_SUPPORTED == 1') +LP_CORE_TARGETS = [t for t in (*SUPPORTED_TARGETS, *PREVIEW_TARGETS) if _expr.get_value(t, '')] + + +def _build_dir(target: str) -> str: + return os.path.join(APP_DIR, f'build_{target}') + + +def _build(target: str, custom_layout: str, clean: bool) -> tuple[int, str]: + # The parent app is CMake v2-native (its CMakeLists includes cmakev2/idf.cmake + # directly), so a plain build with -DIDF_TARGET drives the buildv2 path. + build_dir = _build_dir(target) + if clean: + shutil.rmtree(build_dir, ignore_errors=True) + # Keep sdkconfig inside the build dir: the buildv2 flow refuses to switch + # IDF_TARGET while a mismatched app-dir sdkconfig exists, so per-target + # isolation is required for the parametrized targets. + r = subprocess.run( + [ + 'idf.py', + '--preview', + '-B', + build_dir, + f'-DIDF_TARGET={target}', + f'-DSDKCONFIG={os.path.join(build_dir, "sdkconfig")}', + f'-DCUSTOM_ULP_LAYOUT={custom_layout}', + 'build', + ], + cwd=APP_DIR, + capture_output=True, + text=True, + ) + return r.returncode, r.stdout + r.stderr + + +def _marker(target: str) -> int: + # The ULP executable of a full subproject is emitted under build/subprojects/. + elf = os.path.join(_build_dir(target), 'subprojects', 'ulp_main', 'ulp_main.elf') + assert os.path.isfile(elf), 'ULP elf was not produced' + out = subprocess.run(['riscv32-esp-elf-nm', elf], capture_output=True, text=True).stdout + for line in out.splitlines(): + parts = line.split() + if len(parts) == 3 and parts[2] == '_custom_linker_marker': + return int(parts[0], 16) + raise AssertionError('custom marker symbol absent from the linked ULP image') + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_v2_positive_custom_layout(target: str) -> None: + rc, out = _build(target, os.path.join(LAYOUTS, 'good.ld'), clean=True) + assert rc == 0, f'custom layout should build under buildv2:\n{out[-2000:]}' + assert _marker(target) == 0xCAFEBABE, 'custom layout marker not linked under buildv2' + + +@pytest.mark.parametrize('target', LP_CORE_TARGETS) +def test_v2_incremental_layout_switch(target: str) -> None: + # Frantisek's concern, verified for buildv2: switch the LINKER_LAYOUT on an + # incremental build (no wipe) with the new layout aged older than the + # previously generated script; the image must relink against the new layout. + rc, out = _build(target, os.path.join(LAYOUTS, 'good.ld'), clean=True) + assert rc == 0, f'initial buildv2 build should succeed:\n{out[-2000:]}' + assert _marker(target) == 0xCAFEBABE + + switch_layout = os.path.join(LAYOUTS, 'good_switch.ld') + os.utime(switch_layout, (1600000000, 1600000000)) # 2020-09-13, older than the built script + rc, out = _build(target, switch_layout, clean=False) + assert rc == 0, f'incremental buildv2 rebuild after switching layout should succeed:\n{out[-2000:]}' + marker = _marker(target) + assert marker == 0xD00DFEED, ( + f'stale linker script under buildv2: switching the LINKER_LAYOUT on an incremental ' + f'build did not relink (marker still {marker:#x}, expected 0xd00dfeed)' + ) From 2de7def60bf980012844b96ab470175242421bf0 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Wed, 24 Jun 2026 10:12:29 +0200 Subject: [PATCH 4/4] docs(ulp): document the custom linker script feature Describe the base/layout/checks split and how to supply a custom LP-core layout through the LINKER option, covering the usable-window symbols (LP_CORE_USER_MEMORY_REGION_START / _END), the section-boundary macros (LP_CORE_TEXT_START / TEXT_END / DATA_START / DATA_END), and minimal and multi-region layout examples. --- docs/en/api-reference/system/ulp-lp-core.rst | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/docs/en/api-reference/system/ulp-lp-core.rst b/docs/en/api-reference/system/ulp-lp-core.rst index 49457d21603..42c414a169a 100644 --- a/docs/en/api-reference/system/ulp-lp-core.rst +++ b/docs/en/api-reference/system/ulp-lp-core.rst @@ -401,6 +401,102 @@ Limitations #. OpenOCD flash support is disabled. It does not matter for LP core application because it is run completely from RAM and GDB can use SW breakpoints for it. But if you want to set a breakpoint on function from flash used by the code running on HP core (e.g., `app_main`), you should request to set HW breakpoint explicitly via ``hb`` and ``thb`` GDB commands. #. Since the main and ULP programs are linked as separate binaries, it is possible for them to have global symbols (such as functions or variables) with the same name. If you set a breakpoint using the function name, GDB will apply it to all instances of that function. This can cause issues if one of the functions is located in the flash, as OpenOCD currently doesn't support flash when debugging the LP core. In such cases, you can set breakpoints using the source line or the function's memory address instead. +Using a Custom Linker Script +---------------------------- + +The default LP core linker script is assembled from three parts — a **base** part, a **layout** part, and a **checks** part — that are concatenated and run through the C preprocessor at build time. The split defines a stable interface between ESP-IDF and the LP Core application: ESP-IDF owns the boot-critical invariants, the LP Core application owns the memory map and section placement, and the two evolve independently. + +- **base** part — defines ``ENTRY``, the interrupt vector table, the HP/LP shared-memory section, the default stack top (``__stack_top``), the usable window bounds and the section-boundary macros below. This part is never replaced. +- **layout** part — declares the memory regions and section placement within the window. It composes the macros so it never re-implements boot code. This is the part the LP Core application replaces. +- **checks** part — provides the default ``__stack_size`` (computed as the free space between ``_lp_data_end`` and ``__stack_top``, once the layout has marked ``_lp_data_end``) and the link-time ``ASSERT`` statements that verify reset vectoring, that the image fits the reserved memory and that the stack stays within its bounds, failing the build with a clear message rather than an obscure linker error. + +A custom layout uses only the following: + +- ``LP_CORE_USER_MEMORY_REGION_START`` / ``LP_CORE_USER_MEMORY_REGION_END`` — the bounds of the free LP-RAM window. Declare regions and place sections only within it. +- ``LP_CORE_TEXT_START(region)`` — composed first, into a region that starts at ``LP_CORE_USER_MEMORY_REGION_START``. Places the reset vector and early handlers at the boot offset. +- ``LP_CORE_TEXT_END()`` — composed after the last executable section. +- ``LP_CORE_DATA_START()`` / ``LP_CORE_DATA_END()`` — composed around the writable data. ``LP_CORE_DATA_END()`` marks the end of writable data (``_lp_data_end``), which the base uses to size and bounds-check the stack, so a layout must compose it (or assign ``_lp_data_end`` itself). + +The macros carry any config-dependent handling for the layout, so a layout composes them the same way regardless of the active configuration. The vector table and the HP/LP shared-memory section are provided by the base part; a custom layout does not declare or place them. The stack window is provided for the layout as well: ``__stack_top`` defaults (in the base part) to the top of the window, and ``__stack_size`` (in the checks part, once ``_lp_data_end`` is known) to all the free space between ``_lp_data_end`` and ``__stack_top``. Both are weak defaults, so a layout that needs to reserve space at the top of the window — a stack guard band, for example — can override either. + +To replace the layout with the LP Core application's own, pass the ``LINKER_LAYOUT`` option to ``ulp_embed_binary`` (or ``ulp_add_project``): + +.. code-block:: cmake + + ulp_embed_binary(${ulp_app_name} "${ulp_sources}" "${ulp_exp_dep_srcs}" + LINKER_LAYOUT "${CMAKE_CURRENT_LIST_DIR}/ulp/custom_layout.ld") + +A minimal custom layout declares one region over the window and composes the section-boundary macros: + +.. code-block:: none + + PROVIDE(_my_marker = 0xCAFEBABE); + + MEMORY + { + lp_ram(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = LP_CORE_USER_MEMORY_REGION_END - LP_CORE_USER_MEMORY_REGION_START + } + + SECTIONS + { + LP_CORE_TEXT_START(lp_ram) /* LP core reset vector at the boot offset */ + .text ALIGN(4) : { *(.text) *(.text*) } > lp_ram + .rodata ALIGN(4) : { *(.rodata) *(.rodata*) } > lp_ram + LP_CORE_TEXT_END() + LP_CORE_DATA_START() + .data ALIGN(4) : { *(.data) *(.data*) *(.sdata) *(.sdata*) } > lp_ram + .bss ALIGN(4) : { *(.bss) *(.bss*) *(.sbss) *(.sbss*) } > lp_ram + LP_CORE_DATA_END() + } + +Shared memory and the stack window come from the base part, so the layout does not declare them; it only marks the end of its data with ``LP_CORE_DATA_END()``. + +For full control — to repartition LP RAM into several regions, for example with code or data pinned at fixed addresses — the LP Core application declares its own region map, deriving every address from the two window symbols and still composing the section-boundary macros. In outline: + +.. code-block:: none + + PROVIDE(_my_marker = 0xCAFEBABE); + _fixed_addr = LP_CORE_USER_MEMORY_REGION_START + 0x1780; + + MEMORY + { + text(RWX) : ORIGIN = LP_CORE_USER_MEMORY_REGION_START, LENGTH = _fixed_addr - LP_CORE_USER_MEMORY_REGION_START + fixed_region(RWX) : ORIGIN = _fixed_addr, LENGTH = 0x100 + data(RW) : ORIGIN = _fixed_addr + 0x100, LENGTH = LP_CORE_USER_MEMORY_REGION_END - (_fixed_addr + 0x100) + } + + SECTIONS + { + LP_CORE_TEXT_START(text) /* LP core reset vector, placed in the application's text region */ + + .text ALIGN(4) : { *(.text) *(.text*) } > text + .rodata ALIGN(4) : { *(.rodata) *(.rodata*) } > text + LP_CORE_TEXT_END() + + . = ORIGIN(fixed_region); /* jump to a non-contiguous fixed address */ + .fixed_region ALIGN(4) : { KEEP(*(.fixed_region .fixed_region.*)) } > fixed_region + + . = ORIGIN(data); + LP_CORE_DATA_START() + .data ALIGN(4) : { *(.data) *(.data*) *(.sdata) *(.sdata*) } > data + .bss ALIGN(4) : { *(.bss) *(.bss*) *(.sbss) *(.sbss*) } > data + LP_CORE_DATA_END() + } + +A layout can also take ownership of the stack — for instance to keep a stack-overflow guard band out of the loadable image — by ending its data with the marker and setting the stack window explicitly: + +.. code-block:: none + + .stack_guard (NOLOAD) : { . += 0x8; _guard = .; . += 0x4; } > lp_ram + LP_CORE_DATA_END() /* marks _lp_data_end */ + __stack_top = ORIGIN(lp_ram) + LENGTH(lp_ram); /* stack starts at the top of the region ... */ + __stack_size = __stack_top - _lp_data_end; /* ... and grows down over the remaining space */ + +The checks then confirm the stack neither reaches into the data below nor overruns the shared region above. + +The link-time checks turn a malformed layout into a build failure with an explicit message (for example, a misplaced reset vector, an image that exceeds the reserved memory or a stack that overlaps the data), and a non-existent ``LINKER_LAYOUT`` path is rejected at configuration time. The ``LINKER_LAYOUT`` option is supported for the LP-core ULP type only; it is rejected for the FSM and classic RISC-V ULP types. + + Application Examples --------------------