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.
This commit is contained in:
Erhan Kurubas
2026-09-16 15:00:57 +02:00
parent 7ed58ee6a5
commit 4d8f4f3b25
8 changed files with 109 additions and 6 deletions
+5 -1
View File
@@ -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"
@@ -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;
@@ -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);
}
@@ -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. */
@@ -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