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)' + )