Merge branch 'trace_encoder_snapshot_coredump' into 'master'

Add trace snapshot to the coredump file

See merge request espressif/esp-idf!52451
This commit is contained in:
Erhan Kurubas
2026-09-17 14:41:18 +02:00
22 changed files with 1306 additions and 76 deletions
@@ -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
@@ -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)
@@ -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('<II', desc, 0)
abi_major, abi_minor, hdr_sz, rec_sz, core_count, reason, sha_sz, params_sz = struct.unpack_from(
'<8B', desc, 12
)
assert magic == ESP_RISCV_TRACE_NOTE_MAGIC
assert (write_seq & 1) == 0
assert abi_major == 1 and abi_minor == 0
assert hdr_sz == ESP_RISCV_TRACE_NOTE_HEADER_SIZE
assert rec_sz == ESP_RISCV_TRACE_NOTE_RECORD_SIZE
assert core_count == expected_cores
assert reason == ESP_RISCV_TRACE_CAPTURE_REASON_PANIC
assert sha_sz == 32 and params_sz == 28
assert len(desc) == ESP_RISCV_TRACE_NOTE_HEADER_SIZE + core_count * ESP_RISCV_TRACE_NOTE_RECORD_SIZE
for i in range(core_count):
off = ESP_RISCV_TRACE_NOTE_HEADER_SIZE + i * ESP_RISCV_TRACE_NOTE_RECORD_SIZE
core_id, state, _mem, _pkt, _addr, _resync, reserved0, head_valid, fifo_empty = struct.unpack_from(
'<9B', desc, off
)
seg_index, buf_addr, capacity, seg_size, head_offset = struct.unpack_from('<5I', desc, off + 12)
assert core_id == i
assert state == ESP_RISCV_TRACE_SNAPSHOT_STATE_FROZEN
assert reserved0 == 0
assert head_valid == 1
assert fifo_empty == 1
assert seg_index != ESP_RISCV_TRACE_NOTE_NO_SEGMENT
assert buf_addr != 0
assert capacity >= 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)
@@ -0,0 +1,3 @@
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y
CONFIG_ESP_RISCV_TRACE_ENABLE=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y