feat(gdbstub): add riscv xesppie extension support

This commit is contained in:
Alexey Lapshin
2026-03-12 19:13:57 +07:00
parent aacde8f52b
commit 83004a3a71
7 changed files with 920 additions and 71 deletions
@@ -1,4 +1,9 @@
idf_component_register(SRCS "test_app_main.c"
set(srcs "test_app_main.c")
if(CONFIG_IDF_TARGET_ARCH_RISCV AND CONFIG_SOC_CPU_HAS_HWLOOP)
list(APPEND srcs "xesppie_loops.S")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS ""
REQUIRES esp_gdbstub)
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -7,6 +7,7 @@
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
int var_1;
int var_2;
@@ -19,12 +20,17 @@ void foo(void)
var_2--;
}
void test_xesppie_loops(void);
void app_main(void)
{
printf("tested app is running.\n");
vTaskDelay(5000 / portTICK_PERIOD_MS);
#if SOC_CPU_HAS_HWLOOP
test_xesppie_loops();
#endif
while(1) {
var_1++;
if (var_1 % 10 == 0) {
@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#define DEBUG_OUTPUT 0
#define LOOP_COUNT 3
#if DEBUG_OUTPUT
.section .data
loop_counter_str: .string "loop counter is %d\n"
#endif
.section .text
.type test_xesppie_loops, @function
.global test_xesppie_loops
/* This workaround is intended to help the MI2 interpreter find labels in
* assembly code. */
.globl test_loop_start
.type test_loop_start, @function
test_xesppie_loops:
addi sp, sp, -16
sw ra, 12(sp)
#if DEBUG_OUTPUT
sw s0, 8(sp)
la s0, loop_counter_str
#endif
li t0, LOOP_COUNT
test_loop_start:
esp.lp.setup 0, t0, test_loop_end
csrr a1, 0x7C8
#if DEBUG_OUTPUT
mv a0, s0
call ets_printf
#endif
test_loop_end:
nop
#if DEBUG_OUTPUT
lw s0, 8(sp)
#endif
lw ra, 12(sp)
addi sp, sp, 16
ret
.size test_xesppie_loops, .-test_xesppie_loops
@@ -3,6 +3,7 @@
import os
import os.path as path
import sys
from typing import Any
import pytest
from pytest_embedded_idf.utils import idf_parametrize
@@ -20,25 +21,93 @@ def get_line_number(lookup: str, offset: int = 0) -> int:
return -1
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
def test_gdbstub_runtime(dut: PanicTestDut) -> None:
def start_gdb(dut: PanicTestDut) -> None:
dut.expect_exact('tested app is running.')
dut.write(b'\x03') # send Ctrl-C
dut.start_gdb_for_gdbstub()
def run_and_break(dut: PanicTestDut, cmd: str) -> dict[Any, Any]:
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses): # have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
assert isinstance(payload, dict)
return payload
@pytest.mark.esp32p4
@pytest.mark.generic
def test_hwloop_jump(dut: PanicTestDut) -> None:
start_gdb(dut)
cmd = '-break-insert --source xesppie_loops.S --function test_loop_start'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
# go to the beginning of the loop
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
cmd = '-break-delete 1'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
# go through the loop
loop_count = 3
while loop_count:
inst_count = 2
while inst_count:
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
inst_count -= 1
cmd = '-data-list-register-values d 11'
responses = dut.gdb_write(cmd)
response = dut.find_gdb_response('done', 'result', responses)
assert response is not None
payload = response['payload']
assert payload['register-values'][0]['number'] == '11'
assert payload['register-values'][0]['value'] == f'{loop_count}'
loop_count -= 1
# go through the func prologue
remaining_instructions = 3
while remaining_instructions:
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
remaining_instructions -= 1
# Now we stepping back to app_main
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'app_main'
assert payload['stopped-threads'] == 'all'
@pytest.mark.supported_targets
@pytest.mark.generic
def test_gdbstub_runtime(dut: PanicTestDut) -> None:
start_gdb(dut)
# Test breakpoint
cmd = '-break-insert --source test_app_main.c --function app_main --label label_1'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'app_main'
@@ -47,13 +116,7 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
# Test step command
cmd = '-exec-step'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'foo'
assert payload['frame']['line'] == str(get_line_number('var_2+=2;'))
@@ -61,13 +124,7 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
# Test finish command
cmd = '-exec-finish'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'function-finished'
# On riscv we may have situation when returned from a function but stay on exactly the same line
# foo();
@@ -87,13 +144,7 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
# Test next command
cmd = '-exec-next'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['line'] == str(get_line_number('label_3:', 1))
assert payload['frame']['func'] == 'app_main'
@@ -120,12 +171,7 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'signal-received'
assert payload['frame']['func'] == 'foo'
assert payload['stopped-threads'] == 'all'
@@ -146,12 +192,7 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
# test panic handling
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'signal-received'
assert payload['signal-name'] == 'SIGSEGV'
assert payload['frame']['func'] == 'app_main'
@@ -163,22 +204,14 @@ def test_gdbstub_runtime(dut: PanicTestDut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2', 'esp32s3'], reason='fix IDF-7927')
@idf_parametrize('target', ['esp32', 'esp32s2', 'esp32s3'], indirect=['target'])
def test_gdbstub_runtime_xtensa_stepping_bug(dut: PanicTestDut) -> None:
dut.expect_exact('tested app is running.')
dut.write(b'\x03') # send Ctrl-C
dut.start_gdb_for_gdbstub()
start_gdb(dut)
# Test breakpoint
cmd = '-break-insert --source test_app_main.c --function app_main --label label_1'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'app_main'
@@ -187,13 +220,7 @@ def test_gdbstub_runtime_xtensa_stepping_bug(dut: PanicTestDut) -> None:
# Test step command
cmd = '-exec-step'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'foo'
assert payload['frame']['line'] == str(get_line_number('var_2+=2;'))
@@ -201,13 +228,7 @@ def test_gdbstub_runtime_xtensa_stepping_bug(dut: PanicTestDut) -> None:
# Test next command
cmd = '-exec-next'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses):
# have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['line'] == str(get_line_number('var_2--;', 0))
assert payload['frame']['func'] == 'foo'