From 4d8f4f3b25f8f6f34b72337c33ee32aeb480f27b Mon Sep 17 00:00:00 2001 From: Erhan Kurubas Date: Thu, 3 Sep 2026 22:32:11 +0200 Subject: [PATCH] test(esp_riscv_trace): Add coredump snapshot note coverage Check the ESP_RISCV_TRACE ELF note and per-core PT_LOAD segments after an abort, without decoding the trace stream. --- components/esp_riscv_trace/Kconfig | 6 +- .../esp_riscv_trace/src/esp_riscv_trace.c | 2 - .../src/esp_riscv_trace_coredump.c | 5 +- .../src/esp_riscv_trace_priv.h | 1 - tools/test_apps/system/.build-test-rules.yml | 2 + .../system/panic/coredump/CMakeLists.txt | 2 +- .../panic/coredump/pytest_panic_coredump.py | 94 +++++++++++++++++++ .../sdkconfig.ci.coredump_flash_riscv_trace | 3 + 8 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 tools/test_apps/system/panic/coredump/sdkconfig.ci.coredump_flash_riscv_trace diff --git a/components/esp_riscv_trace/Kconfig b/components/esp_riscv_trace/Kconfig index 3bef58aa61b..9adaf293e16 100644 --- a/components/esp_riscv_trace/Kconfig +++ b/components/esp_riscv_trace/Kconfig @@ -215,9 +215,13 @@ menu "RISC-V Trace Encoder Configurations" choice ESP_RISCV_TRACE_AHB_BURST_SELECT prompt "AHB burst type" depends on SOC_RISCV_TRACE_AHB_CONFIGURABLE + default ESP_RISCV_TRACE_AHB_BURST_INCR4 if ESP_RISCV_TRACE_BUFFER_IN_EXTERNAL default ESP_RISCV_TRACE_AHB_BURST_SINGLE help - AHB burst type used by the trace write master. + AHB burst type used by the trace write master. Single-beat + writes are too slow for PSRAM and lose most of the trace, so + the default is INCR4 when the buffer is in PSRAM. Switch back + to single if your board reports PSRAM controller errors. config ESP_RISCV_TRACE_AHB_BURST_SINGLE bool "Single transfer" diff --git a/components/esp_riscv_trace/src/esp_riscv_trace.c b/components/esp_riscv_trace/src/esp_riscv_trace.c index 896379d103b..d0d810f040c 100644 --- a/components/esp_riscv_trace/src/esp_riscv_trace.c +++ b/components/esp_riscv_trace/src/esp_riscv_trace.c @@ -249,7 +249,6 @@ err_alloc: return ret; } -/* Common start work. Lock must be held. */ static esp_err_t trace_start_locked(esp_riscv_trace_handle_t handle) { esp_err_t ret = ESP_OK; @@ -266,7 +265,6 @@ static esp_err_t trace_start_locked(esp_riscv_trace_handle_t handle) return ret; } -/* Common stop work. Lock must be held. Writes the final snapshot fields on the way out. */ static esp_err_t trace_stop_locked(esp_riscv_trace_handle_t handle, uint32_t timeout_us) { esp_err_t ret = ESP_OK; diff --git a/components/esp_riscv_trace/src/esp_riscv_trace_coredump.c b/components/esp_riscv_trace/src/esp_riscv_trace_coredump.c index f5588691cfe..365be264e03 100644 --- a/components/esp_riscv_trace/src/esp_riscv_trace_coredump.c +++ b/components/esp_riscv_trace/src/esp_riscv_trace_coredump.c @@ -64,8 +64,11 @@ static bool core_buffer_stable(const esp_riscv_trace_snapshot_core_desc_t *cd) if (!cd->head_valid) { return false; } + /* Bytes up to head_offset are already committed to memory. A non-empty + * FIFO at freeze time only means the tail may be truncated. */ if (!cd->fifo_empty) { - return false; + ESP_DRAM_LOGD(TAG, "core %d FIFO not empty at freeze (tail may be truncated, head=%u/%u)", + (int)cd->core_id, (unsigned)cd->head_offset, (unsigned)cd->capacity); } return range_readable(cd->buffer_addr, cd->capacity); } diff --git a/components/esp_riscv_trace/src/esp_riscv_trace_priv.h b/components/esp_riscv_trace/src/esp_riscv_trace_priv.h index c36c8a39416..a1a49c8de44 100644 --- a/components/esp_riscv_trace/src/esp_riscv_trace_priv.h +++ b/components/esp_riscv_trace/src/esp_riscv_trace_priv.h @@ -58,7 +58,6 @@ typedef struct { bool fifo_overflow; } esp_riscv_trace_snapshot_panic_core_t; -/* IRAM. No lock. Skips cores that are not present. */ void esp_riscv_trace_snapshot_panic_write(const esp_riscv_trace_snapshot_panic_core_t *cores, int count); /* Waits for the encoder FIFOs to empty and records the final per-core status. */ diff --git a/tools/test_apps/system/.build-test-rules.yml b/tools/test_apps/system/.build-test-rules.yml index 67b807a9508..e350444ba70 100644 --- a/tools/test_apps/system/.build-test-rules.yml +++ b/tools/test_apps/system/.build-test-rules.yml @@ -165,11 +165,13 @@ tools/test_apps/system/panic/coredump: disable: - if: CONFIG_NAME in ["coredump_flash_extram_stack_bss", "coredump_flash_extram_stack_heap"] and SOC_SPIRAM_SUPPORTED != 1 - if: CONFIG_NAME == "coredump_flash_extram_stack_bss_xip" and SOC_SPIRAM_XIP_SUPPORTED != 1 + - if: CONFIG_NAME == "coredump_flash_riscv_trace" and SOC_RISCV_TRACE_SUPPORTED != 1 - if: IDF_TARGET in ["esp32h21"] temporary: true reason: IDF-11543 depends_components: - espcoredump + - esp_riscv_trace - esp_system - spi_flash diff --git a/tools/test_apps/system/panic/coredump/CMakeLists.txt b/tools/test_apps/system/panic/coredump/CMakeLists.txt index e1304f0189b..503dd441671 100644 --- a/tools/test_apps/system/panic/coredump/CMakeLists.txt +++ b/tools/test_apps/system/panic/coredump/CMakeLists.txt @@ -2,7 +2,7 @@ # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.22) -set(COMPONENTS main esp_gdbstub espcoredump) +set(COMPONENTS main esp_gdbstub espcoredump esp_riscv_trace) set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../common") include($ENV{IDF_PATH}/tools/cmake/project.cmake) diff --git a/tools/test_apps/system/panic/coredump/pytest_panic_coredump.py b/tools/test_apps/system/panic/coredump/pytest_panic_coredump.py index 634c20717e6..a8e89b84db0 100644 --- a/tools/test_apps/system/panic/coredump/pytest_panic_coredump.py +++ b/tools/test_apps/system/panic/coredump/pytest_panic_coredump.py @@ -2,10 +2,12 @@ # SPDX-License-Identifier: CC0-1.0 import re +import struct import sys from pathlib import Path import pytest +from elftools.elf.elffile import ELFFile from pytest_embedded_idf.utils import idf_parametrize from pytest_embedded_idf.utils import soc_filtered_targets @@ -66,6 +68,7 @@ CONFIG_GDBSTUB_COREDUMP = panic_tests.configs_for_app(COREDUMP_APP, ['gdbstub_co # faults in idle-task context, whose small stack overflows the FreeRTOS end-of-stack watchpoint # if the coredump runs in place, causing a double panic. Do not switch back to coredump_flash_default. CONFIG_TCB_CORRUPTED = panic_tests.configs_for_app(COREDUMP_APP, ['coredump_flash_custom_stack']) +CONFIG_RISCV_TRACE = panic_tests.configs_for_app(COREDUMP_APP, ['coredump_flash_riscv_trace']) @pytest.mark.generic @@ -374,3 +377,94 @@ def test_tcb_corrupted(dut: PanicTestDut, target: str, config: str, test_func_na coredump_pattern = [re.compile(pattern.decode('utf-8')) for pattern in regex_patterns] common_test(dut, config, expected_backtrace=None, expected_coredump=coredump_pattern) + + +ESP_RISCV_TRACE_NOTE_NAME = 'ESP_RISCV_TRACE' +ESP_RISCV_TRACE_NOTE_TYPE = 680 +ESP_RISCV_TRACE_NOTE_MAGIC = 0x53545652 +ESP_RISCV_TRACE_NOTE_HEADER_SIZE = 80 +ESP_RISCV_TRACE_NOTE_RECORD_SIZE = 44 +ESP_RISCV_TRACE_NOTE_NO_SEGMENT = 0xFFFFFFFF +ESP_RISCV_TRACE_CAPTURE_REASON_PANIC = 2 +ESP_RISCV_TRACE_SNAPSHOT_STATE_FROZEN = 4 + + +def _riscv_trace_note_desc(elf: ELFFile) -> bytes: + for seg in elf.iter_segments(): + if seg.header['p_type'] != 'PT_NOTE': + continue + for note in seg.iter_notes(): + name = note['n_name'].rstrip('\x00') + if name == ESP_RISCV_TRACE_NOTE_NAME and note['n_type'] == ESP_RISCV_TRACE_NOTE_TYPE: + desc = note['n_desc'] + return desc.encode('latin1') if isinstance(desc, str) else bytes(desc) + raise AssertionError('coredump ELF has no ESP_RISCV_TRACE note') + + +def _check_riscv_trace_coredump(core_path: str, expected_cores: int) -> None: + with open(core_path, 'rb') as f: + assert f.read(4) == b'\x7fELF', f'{core_path} is not an ELF file' + f.seek(0) + elf = ELFFile(f) + desc = _riscv_trace_note_desc(elf) + assert len(desc) >= ESP_RISCV_TRACE_NOTE_HEADER_SIZE + + magic, write_seq = struct.unpack_from('= 2048 and capacity % 4 == 0 + assert seg_size == capacity + assert 0 < head_offset <= capacity + + # Match the trace buffer by address and size. + matches = [ + seg + for seg in elf.iter_segments() + if seg.header['p_type'] == 'PT_LOAD' + and seg.header['p_vaddr'] == buf_addr + and seg.header['p_filesz'] == capacity + ] + assert len(matches) == 1, f'core {core_id} expected one PT_LOAD at 0x{buf_addr:x} size {capacity}' + assert len(matches[0].data()) == capacity + + +@pytest.mark.generic +@pytest.mark.parametrize('app_path, config', CONFIG_RISCV_TRACE, indirect=True) +@idf_parametrize('target', soc_filtered_targets('SOC_RISCV_TRACE_SUPPORTED == 1'), indirect=['target']) +def test_riscv_trace_coredump(dut: PanicTestDut, config: str) -> None: + dut.run_test_func('test_abort') + regex_pattern = rb'abort\(\) was called at PC [0-9xa-f]+ on core 0' + dut.expect(regex_pattern) + dut.expect_stack_dump() + dut.expect_elf_sha256() + dut.expect_none(['Guru Meditation', 'Re-entered core dump']) + + coredump_pattern = re.compile(PANIC_ABORT_PREFIX + regex_pattern.decode('utf-8')) + expect_coredump_flash_write_logs(dut, config) + core_path = dut.process_coredump_flash([coredump_pattern]) + _check_riscv_trace_coredump(core_path, 2 if dut.is_multi_core else 1) diff --git a/tools/test_apps/system/panic/coredump/sdkconfig.ci.coredump_flash_riscv_trace b/tools/test_apps/system/panic/coredump/sdkconfig.ci.coredump_flash_riscv_trace new file mode 100644 index 00000000000..29e259ce0e0 --- /dev/null +++ b/tools/test_apps/system/panic/coredump/sdkconfig.ci.coredump_flash_riscv_trace @@ -0,0 +1,3 @@ +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y +CONFIG_ESP_RISCV_TRACE_ENABLE=y +CONFIG_LOG_DEFAULT_LEVEL_INFO=y