From be1a6ca6c316df351374cae37009a31c693e7f99 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Thu, 24 Sep 2020 17:01:11 -0500 Subject: [PATCH 1/5] Timeout utility: allow None to mean no timeout. --- pyocd/utility/timeout.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyocd/utility/timeout.py b/pyocd/utility/timeout.py index 0f099a0e4..91746cc54 100644 --- a/pyocd/utility/timeout.py +++ b/pyocd/utility/timeout.py @@ -52,12 +52,15 @@ class Timeout(object): If you pass a non-zero value for _sleeptime_ to the constructor, the check() method will automatically sleep by default starting with the second call. You can disable auto-sleep by passing `autosleep=False` to check(). + + Passing a timeout of None to the constructor is allowed. In this case, check() will always return + True and the loop must be exited via some other means. """ def __init__(self, timeout, sleeptime=0): """! @brief Constructor. @param self - @param timeout The timeout in seconds. + @param timeout The timeout in seconds. May be None to indicate no timeout. @param sleeptime Time in seconds to sleep during calls to check(). Defaults to 0, thus check() will not sleep unless you pass a different value. """ @@ -83,12 +86,16 @@ def check(self, autosleep=True): - A non-zero _sleeptime_ was passed to the constructor. - The _autosleep_ parameter is True. + This method is intended to be used as the predicate of a while loop. + @param self @param autosleep Whether to sleep if not timed out yet. The sleeptime passed to the constructor must have been non-zero. + @retval True The timeout has _not_ occurred. + @retval False Timeout is passed and the loop should be exited. """ # Check for a timeout. - if (time() - self._start) > self._timeout: + if (self._timeout is not None) and ((time() - self._start) > self._timeout): self._timed_out = True # Sleep if appropriate. elif (not self._is_first_check) and autosleep and self._sleeptime: From 567c96c84115d32bd6e2588829d7a78f7d2dab38 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Thu, 24 Sep 2020 15:28:42 -0500 Subject: [PATCH 2/5] CortexM: improvement to clear_debug_cause_bits(). - Add DFSR_EXTERNAL to cleared flags. - For v8-M, also clear DFSR_PMU. --- pyocd/coresight/cortex_m.py | 8 +++++++- pyocd/coresight/cortex_m_v8m.py | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pyocd/coresight/cortex_m.py b/pyocd/coresight/cortex_m.py index c9e072428..6a8a46e29 100644 --- a/pyocd/coresight/cortex_m.py +++ b/pyocd/coresight/cortex_m.py @@ -534,7 +534,13 @@ def step(self, disable_interrupts=True, start=0, end=0): self.session.notify(Target.Event.POST_RUN, self, Target.RunType.STEP) def clear_debug_cause_bits(self): - self.write_memory(CortexM.DFSR, CortexM.DFSR_VCATCH | CortexM.DFSR_DWTTRAP | CortexM.DFSR_BKPT | CortexM.DFSR_HALTED) + self.write32(CortexM.DFSR, + CortexM.DFSR_EXTERNAL + | CortexM.DFSR_VCATCH + | CortexM.DFSR_DWTTRAP + | CortexM.DFSR_BKPT + | CortexM.DFSR_HALTED + ) def _perform_emulated_reset(self): """! @brief Emulate a software reset by writing registers. diff --git a/pyocd/coresight/cortex_m_v8m.py b/pyocd/coresight/cortex_m_v8m.py index d4713d3f5..405837984 100644 --- a/pyocd/coresight/cortex_m_v8m.py +++ b/pyocd/coresight/cortex_m_v8m.py @@ -152,6 +152,15 @@ def get_security_state(self): return Target.SecurityState.SECURE else: return Target.SecurityState.NONSECURE + + def clear_debug_cause_bits(self): + self.write32(CortexM.DFSR, + self.DFSR_PMU + | CortexM.DFSR_EXTERNAL + | CortexM.DFSR_VCATCH + | CortexM.DFSR_DWTTRAP + | CortexM.DFSR_BKPT + | CortexM.DFSR_HALTED) def get_halt_reason(self): """! @brief Returns the reason the core has halted. From 1e089f6e9dc654136a5f3bc1521949d74afe1a7d Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Sun, 20 Sep 2020 16:46:30 -0500 Subject: [PATCH 3/5] CortexM: improved step API. - Add hook callback to step() API. - Major cleanup touching almost every line of step(). - Addition of 'cpu.step.instruction.timeout' session option for a timeout for stepping. - Fixed the case where disable_interrupts is false but DHCSR.C_MASKINTS is already set. - Checking for all halt reasons to exit the loop, so, for instance, a vector catch will cause the range step loop to exit (if interrupts aren't masked). - Simplified some code. - Documented the method. --- docs/options.md | 10 +++ pyocd/core/options.py | 2 + pyocd/core/soc_target.py | 4 +- pyocd/core/target.py | 2 +- pyocd/coresight/cortex_m.py | 107 +++++++++++++++++++++--------- pyocd/coresight/generic_mem_ap.py | 2 +- 6 files changed, 92 insertions(+), 35 deletions(-) diff --git a/docs/options.md b/docs/options.md index 0782add18..ad44d2c8b 100644 --- a/docs/options.md +++ b/docs/options.md @@ -76,6 +76,16 @@ working directory. Controls how pyOCD connects to the target. One of 'halt', 'pre-reset', 'under-reset', 'attach'. +cpu.step.instruction.timeout +float +0.0 + +

Timeout in seconds for instruction step operations. The default of 0 means no timeout.

+

Note that stepping may take a very long time for to return in cases such as stepping over a branch +into the Secure world where the debugger doesn't have secure debug access, or similar for Privileged +code in the case of UDE.

+ + dap_protocol str 'default' diff --git a/pyocd/core/options.py b/pyocd/core/options.py index f200c5f9e..6dfa24376 100644 --- a/pyocd/core/options.py +++ b/pyocd/core/options.py @@ -39,6 +39,8 @@ "Path to custom config file."), OptionInfo('connect_mode', str, "halt", "One of 'halt', 'pre-reset', 'under-reset', 'attach'. Default is 'halt'."), + OptionInfo('cpu.step.instruction.timeout', float, 0.0, + "Timeout in seconds for instruction step operations. Defaults to 0, or no timeout."), OptionInfo('dap_protocol', str, 'default', "Wire protocol, either 'swd', 'jtag', or 'default'."), OptionInfo('dap_swj_enable', bool, True, diff --git a/pyocd/core/soc_target.py b/pyocd/core/soc_target.py index fa2b4d9a2..210716a53 100644 --- a/pyocd/core/soc_target.py +++ b/pyocd/core/soc_target.py @@ -142,8 +142,8 @@ def run_token(self): def halt(self): return self.selected_core.halt() - def step(self, disable_interrupts=True, start=0, end=0): - return self.selected_core.step(disable_interrupts, start, end) + def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None): + return self.selected_core.step(disable_interrupts, start, end, hook_cb) def resume(self): return self.selected_core.resume() diff --git a/pyocd/core/target.py b/pyocd/core/target.py index 7042e0fe9..b545c58d8 100644 --- a/pyocd/core/target.py +++ b/pyocd/core/target.py @@ -235,7 +235,7 @@ def flush(self): def halt(self): raise NotImplementedError() - def step(self, disable_interrupts=True, start=0, end=0): + def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None): raise NotImplementedError() def resume(self): diff --git a/pyocd/coresight/cortex_m.py b/pyocd/coresight/cortex_m.py index 6a8a46e29..d5c69e89a 100644 --- a/pyocd/coresight/cortex_m.py +++ b/pyocd/coresight/cortex_m.py @@ -113,6 +113,7 @@ class CortexM(Target, CoreSightCoreComponent): C_STEP = (1 << 2) C_MASKINTS = (1 << 3) C_SNAPSTALL = (1 << 5) + C_PMOV = (1 << 6) S_REGRDY = (1 << 16) S_HALT = (1 << 17) S_SLEEP = (1 << 18) @@ -473,41 +474,87 @@ def halt(self): self.flush() self.session.notify(Target.Event.POST_HALT, self, Target.HaltReason.USER) - def step(self, disable_interrupts=True, start=0, end=0): + def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None): """! @brief Perform an instruction level step. - This function preserves the previous interrupt mask state. + This API will execute one or more individual instructions on the core. With default parameters, it + masks interrupts and only steps a single instruction. The _start_ and _stop_ parameters define an + address range of [_start_, _end_). The core will be repeatedly stepped until the PC falls outside this + range, a debug event occurs, or the optional callback returns True. + + The _disable_interrupts_ parameter controls whether to allow stepping into interrupts. This function + preserves the previous interrupt mask state. + + If the _hook_cb_ parameter is set to a callable, it will be invoked repeatedly to give the caller a + chance to check for interrupt requests or other reasons to exit. + + Note that stepping may take a very long time for to return in cases such as stepping over a branch + into the Secure world where the debugger doesn't have secure debug access, or similar for Privileged + code in the case of UDE. + + @param self The object. + @param disable_interrupts Boolean specifying whether to mask interrupts during the step. + @param start Integer start address for range stepping. Not included in the range. + @param end Integer end address for range stepping. The range is inclusive of this address. + @param hook_cb Optional callable taking no parameters and returning a boolean. The signature is + `hook_cb() -> bool`. Invoked repeatedly while waiting for step operations to complete. If the + callback returns True, then stepping is stopped immediately. + + @exception DebugError Raised if debug is not enabled on the core. """ - # Was 'if self.get_state() != TARGET_HALTED:' - # but now value of dhcsr is saved - dhcsr = self.read_memory(CortexM.DHCSR) - if not (dhcsr & (CortexM.C_STEP | CortexM.C_HALT)): - LOG.error('cannot step: target not halted') + # Save DHCSR and make sure the core is halted. We also check that C_DEBUGEN is set because if it's + # not, then C_HALT is UNKNOWN. + dhcsr = self.read32(CortexM.DHCSR) + if not (dhcsr & CortexM.C_DEBUGEN): + raise exception.DebugError('cannot step: debug not enabled') + if not (dhcsr & CortexM.C_HALT): + LOG.error('cannot step: core not halted') return - LOG.debug("step core %d", self.core_number) + if start != end: + LOG.debug("step core %d (start=%#010x, end=%#010x)", self.core_number, start, end) + else: + LOG.debug("step core %d", self.core_number) self.session.notify(Target.Event.PRE_RUN, self, Target.RunType.STEP) + self._run_token += 1 + self.clear_debug_cause_bits() - # Save previous interrupt mask state - interrupts_masked = (CortexM.C_MASKINTS & dhcsr) != 0 + # Get current state. + saved_maskints = dhcsr & CortexM.C_MASKINTS + saved_pmov = dhcsr & CortexM.C_PMOV + maskints_differs = bool(saved_maskints) != disable_interrupts - # Mask interrupts - C_HALT must be set when changing to C_MASKINTS - if not interrupts_masked and disable_interrupts: - self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT | CortexM.C_MASKINTS) + # Get the DHCSR value to use when stepping based on whether we're masking interrupts. + dhcsr_step = CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_STEP | saved_pmov + if disable_interrupts: + dhcsr_step |= CortexM.C_MASKINTS - # Single step using current C_MASKINTS setting - while True: - if disable_interrupts or interrupts_masked: - self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_MASKINTS | CortexM.C_STEP) - else: - self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_STEP) + # Update mask interrupts setting - C_HALT must be set when changing to C_MASKINTS. + if maskints_differs: + self.write32(CortexM.DHCSR, dhcsr_step | CortexM.C_HALT) - # Wait for halt to auto set (This should be done before the first read) - while not self.read_memory(CortexM.DHCSR) & CortexM.C_HALT: - pass + # Get the step timeout. A timeout of 0 means no timeout, so we have to pass None to the Timeout class. + step_timeout = self.session.options.get('cpu.step.instruction.timeout') or None + + while True: + # Single step using current C_MASKINTS setting + self.write32(CortexM.DHCSR, dhcsr_step) + + # Wait for halt to auto set. + # + # Note that it may take a very long time for this loop to exit in cases such as stepping over + # a branch into the Secure world where the debugger doesn't have secure debug access, or similar + # for Privileged code in the case of UDE. + with timeout.Timeout(step_timeout) as tmo: + while tmo.check(): + if (self.read32(CortexM.DHCSR) & CortexM.C_HALT) != 0: + break + # Invoke the callback if provided. If it returns True, then exit the loop. + if (hook_cb is not None) and hook_cb(): + break # Range is empty, 'range step' will degenerate to 'step' if start == end: @@ -515,22 +562,20 @@ def step(self, disable_interrupts=True, start=0, end=0): # Read program counter and compare to [start, end) program_counter = self.read_core_register('pc') - if program_counter < start or end <= program_counter: + if (program_counter < start) or (end <= program_counter): break - # Check other stop reasons - if self.read_memory(CortexM.DFSR) & (CortexM.DFSR_DWTTRAP | CortexM.DFSR_BKPT): + # Check for stop reasons other than HALTED, which will have been set by our step action. + if (self.read32(CortexM.DFSR) & ~CortexM.DFSR_HALTED) != 0: break - # Restore interrupt mask state - if not interrupts_masked and disable_interrupts: - # Unmask interrupts - C_HALT must be set when changing to C_MASKINTS - self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT) + # Restore interrupt mask state. + if maskints_differs: + self.write32(CortexM.DHCSR, + CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT | saved_maskints | saved_pmov) self.flush() - self._run_token += 1 - self.session.notify(Target.Event.POST_RUN, self, Target.RunType.STEP) def clear_debug_cause_bits(self): diff --git a/pyocd/coresight/generic_mem_ap.py b/pyocd/coresight/generic_mem_ap.py index 794e87cee..86297d8ef 100644 --- a/pyocd/coresight/generic_mem_ap.py +++ b/pyocd/coresight/generic_mem_ap.py @@ -86,7 +86,7 @@ def read_memory_block32(self, addr, size): def halt(self): pass - def step(self, disable_interrupts=True, start=0, end=0): + def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None): pass def reset(self, reset_type=None): From 5cf0255d675607050975b9e9a062c53df7647422 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Thu, 24 Sep 2020 17:56:35 -0500 Subject: [PATCH 4/5] Test: add range step test to basic_test.py. The test code for the range_step test is under src/range_step. --- src/range_step/.gitignore | 3 ++ src/range_step/Makefile | 50 ++++++++++++++++++++++++++++++++ src/range_step/linker_script.ld | 50 ++++++++++++++++++++++++++++++++ src/range_step/range_step.S | 46 +++++++++++++++++++++++++++++ test/basic_test.py | 51 +++++++++++++++++++++++++++++++-- 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/range_step/.gitignore create mode 100644 src/range_step/Makefile create mode 100644 src/range_step/linker_script.ld create mode 100644 src/range_step/range_step.S diff --git a/src/range_step/.gitignore b/src/range_step/.gitignore new file mode 100644 index 000000000..2c7e78b08 --- /dev/null +++ b/src/range_step/.gitignore @@ -0,0 +1,3 @@ +*.lst +*.bin +*.elf diff --git a/src/range_step/Makefile b/src/range_step/Makefile new file mode 100644 index 000000000..54f6bff88 --- /dev/null +++ b/src/range_step/Makefile @@ -0,0 +1,50 @@ +# pyOCD debugger +# Copyright (c) 2020 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +PREFIX = arm-none-eabi- +CC = $(PREFIX)gcc +OBJCOPY = $(PREFIX)objcopy + +TARGET = range_step.elf +TARGET_BIN = range_step.bin + +OBJECTS = range_step.o + +LIBRARIES = + +INCLUDES = + +ASFLAGS = -std=gnu11 -MMD -MP $(INCLUDES) -O0 -fno-common -ffunction-sections \ + -fdata-sections -Wall -Werror -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -g3 -gdwarf-2 \ + -gstrict-dwarf -nostdlib -fpie -Wa,-adln=$(basename $@).lst + +LDFLAGS = -T"linker_script.ld" -Wl,-Map,$(basename $@).map,--gc-sections,-erange_step_test -nostdlib -fpie + +.PHONY: all +all: $(TARGET) $(TARGET_BIN) + +.PHONY: clean +clean: + rm -f *.o *.d *.map *.lst *.elf *.bin + +$(TARGET): $(OBJECTS) + $(CC) $(LDFLAGS) $(OBJECTS) $(LIBRARIES) -o $@ + +$(TARGET_BIN): $(TARGET) + $(OBJCOPY) -O binary $(TARGET) $(TARGET_BIN) + +# Include dependency files. +-include $(OBJECTS:.o=.d) diff --git a/src/range_step/linker_script.ld b/src/range_step/linker_script.ld new file mode 100644 index 000000000..19bdbc465 --- /dev/null +++ b/src/range_step/linker_script.ld @@ -0,0 +1,50 @@ +/* + Copyright (c) 2020 ARM Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* Memory regions */ +MEMORY +{ + /* Ram configurations are for smallest KL25 in family - 4K */ + m_all (rwx) : ORIGIN = 0x00000000, LENGTH = 0x600 +} + +/* Define output sections */ +SECTIONS +{ + + .text : + { + . = ALIGN(4); + + *(.text) /* .text sections (code) */ + *(.text*) /* .text* sections (code) */ + + . = ALIGN(4); + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + + . = ALIGN(4); + *(.bss) + *(.bss*) + *(COMMON) + + . = ALIGN(4); + *(.rodata) /* .rodata sections (constants, strings, etc.) */ + *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ + + } >m_all + +} diff --git a/src/range_step/range_step.S b/src/range_step/range_step.S new file mode 100644 index 000000000..6b517c3b2 --- /dev/null +++ b/src/range_step/range_step.S @@ -0,0 +1,46 @@ +// Copyright (c) 2020 Arm Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + .syntax unified + .text + .thumb + .align 3 + .cfi_sections .debug_frame + + .thumb_func + .type range_step_test,%function + .global range_step_test + .fnstart + .cfi_startproc +range_step_test: + + // Ridiculously simple (and random) instruction sequence. + // Importantly, there are no branches. + adds r0, #1 + mvns r1, r0 + adds r1, #1 + movs r2, r0, lsl #4 + movs r3, r2 + eors r3, r1 + adds r0, r1 + adds r0, r2 + subs r0, r3 + bkpt #0 + + .cfi_endproc + .cantunwind + .fnend + .size range_step_test, . - range_step_test + + diff --git a/test/basic_test.py b/test/basic_test.py index b6f10bd76..550aaf05d 100644 --- a/test/basic_test.py +++ b/test/basic_test.py @@ -26,7 +26,8 @@ from pyocd.core.helpers import ConnectHelper from pyocd.core.memory_map import MemoryType from pyocd.flash.file_programmer import FileProgrammer -from pyocd.utility.conversion import float32_to_u32 +from pyocd.utility.conversion import (float32_to_u32, u16le_list_to_byte_list) +from pyocd.utility.mask import same from test_util import ( Test, @@ -34,6 +35,21 @@ get_test_binary_path, ) +# Simple code sequence used to test range stepping. +# The important part is that it has no branches. +RANGE_STEP_CODE = u16le_list_to_byte_list([ + 0x3001, # adds r0, #1 + 0x43C1, # mvns r1, r0 + 0x3101, # adds r1, #1 + 0x0102, # movs r2, r0, lsl #4 + 0x0013, # movs r3, r2 + 0x404B, # eors r3, r1 + 0x1840, # adds r0, r1 + 0x1880, # adds r0, r2 + 0x1AC0, # subs r0, r3 + 0xBE00, # bkpt #0 + ]) + class BasicTest(Test): def __init__(self): super(BasicTest, self).__init__("Basic Test", run_basic_test) @@ -138,9 +154,40 @@ def basic_test(board_id, file): target.step() newPC = target.read_core_register('pc') print("STEP: pc: 0x%X" % newPC) - currentPC = newPC sleep(0.2) + print("\n\n------ TEST RANGE STEP ------") + + # Add some extra room before end of memory, and a second copy so there are instructions + # after the final bkpt. Add 1 because region end is always odd. + test_addr = ram_region.end + 1 - len(RANGE_STEP_CODE) * 2 - 32 + # Since the end address is inclusive, we need to exclude the last instruction. + test_end_addr = test_addr + len(RANGE_STEP_CODE) - 2 + print("range start = %#010x; range_end = %#010x" % (test_addr, test_end_addr)) + # Load up some code into ram to test range step. + target.write_memory_block8(test_addr, RANGE_STEP_CODE * 2) + check_data = target.read_memory_block8(test_addr, len(RANGE_STEP_CODE) * 2) + if not same(check_data, RANGE_STEP_CODE * 2): + print("Failed to write range step test code to RAM") + else: + print("wrote range test step code to RAM successfully") + + target.write_core_register('pc', test_addr) + currentPC = target.read_core_register('pc') + print("start PC: 0x%X" % currentPC) + target.step(start=test_addr, end=test_end_addr) + newPC = target.read_core_register('pc') + print("end PC: 0x%X" % newPC) + + # Now test again to ensure the bkpt stops it. + target.write_core_register('pc', test_addr) + currentPC = target.read_core_register('pc') + print("start PC: 0x%X" % currentPC) + target.step(start=test_addr, end=test_end_addr + 4) # include bkpt + newPC = target.read_core_register('pc') + print("end PC: 0x%X" % newPC) + halt_reason = target.get_halt_reason() + print("halt reason: %s (should be BREAKPOINT)" % halt_reason.name) print("\n\n------ TEST READ / WRITE MEMORY ------") target.halt() From a403b1dd7be337137d110046d46d6a0feeeceee0 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Thu, 24 Sep 2020 14:12:01 -0500 Subject: [PATCH 5/5] GDBServer: use hook callback for step() to check for gdb interrupt. --- pyocd/gdbserver/gdbserver.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pyocd/gdbserver/gdbserver.py b/pyocd/gdbserver/gdbserver.py index 9f5955161..efe39ab4c 100644 --- a/pyocd/gdbserver/gdbserver.py +++ b/pyocd/gdbserver/gdbserver.py @@ -625,8 +625,22 @@ def resume(self, data): def step(self, data, start=0, end=0): addr = self._get_resume_step_addr(data) LOG.debug("GDB step: %s (start=0x%x, end=0x%x)", data, start, end) - self.target.step(not self.step_into_interrupt, start, end) - return self.create_rsp_packet(self.get_t_response()) + + # Use the step hook to check for an interrupt event. + def step_hook(): + # Note we don't clear the interrupt event here! + return self.packet_io.interrupt_event.is_set() + self.target.step(not self.step_into_interrupt, start, end, hook_cb=step_hook) + + # Clear and handle an interrupt. + if self.packet_io.interrupt_event.is_set(): + LOG.debug("Received Ctrl-C during step") + self.packet_io.interrupt_event.clear() + response = self.get_t_response(forceSignal=signals.SIGINT) + else: + response = self.get_t_response() + + return self.create_rsp_packet(response) def halt(self): self.target.halt()