mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-03 03:31:41 +03:00
Merge branch 'feat/ulp_custom_linker_support' into 'master'
ULP: Support custom LP Core linker layouts via the LINKER option Closes IDF-12902 See merge request espressif/esp-idf!49991
This commit is contained in:
@@ -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}")
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 .`.
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t marker;
|
||||
volatile uint32_t lp_heartbeat;
|
||||
} custom_linker_shared_t;
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
+32
@@ -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) */
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
+31
@@ -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()
|
||||
}
|
||||
+34
@@ -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()
|
||||
}
|
||||
+41
@@ -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()
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+49
@@ -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;
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
CONFIG_ULP_COPROC_ENABLED=y
|
||||
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
|
||||
CONFIG_ULP_COPROC_RESERVE_MEM=4096
|
||||
+262
@@ -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 "<hex value> <type> <name>"; 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'
|
||||
@@ -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()
|
||||
@@ -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 .`.
|
||||
+15
@@ -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}")
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
+9
@@ -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()
|
||||
+31
@@ -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()
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
+4
@@ -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)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CONFIG_ULP_COPROC_ENABLED=y
|
||||
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
|
||||
CONFIG_ULP_COPROC_RESERVE_MEM=4096
|
||||
+96
@@ -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)'
|
||||
)
|
||||
Reference in New Issue
Block a user