mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 04:58:43 +03:00
test(ulp): add custom linker host tests
Add build-only host tests for LP-core custom linker layouts supplied through the LINKER option. Cover the positive paths (a full-replacement layout, a multi-region layout with a fixed-address section, a run-from-HP-mem layout, and memory protection) and the link-time and configure-time checks (reset-vector placement, sizing, shared-memory overrun, the MEMPROT RX/RW boundary, a missing reset vector, and a missing LINKER script).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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) */
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 .`.
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,4 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
idf_component_register(SRCS "main.c"
|
||||
REQUIRES ulp)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
CONFIG_ULP_COPROC_ENABLED=y
|
||||
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
|
||||
CONFIG_ULP_COPROC_RESERVE_MEM=4096
|
||||
@@ -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