Skip to content

Commit 19a824b

Browse files
authored
Merge pull request #967 from flit/bugfix/gdb_step_interrupt
Step fixes and improvements
2 parents d481549 + a403b1d commit 19a824b

File tree

14 files changed

+331
-42
lines changed

14 files changed

+331
-42
lines changed

docs/options.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ working directory.
7676
Controls how pyOCD connects to the target. One of 'halt', 'pre-reset', 'under-reset', 'attach'.
7777
</td></tr>
7878

79+
<tr><td>cpu.step.instruction.timeout</td>
80+
<td>float</td>
81+
<td>0.0</td>
82+
<td>
83+
<p>Timeout in seconds for instruction step operations. The default of 0 means no timeout.<p>
84+
<p>Note that stepping may take a very long time for to return in cases such as stepping over a branch
85+
into the Secure world where the debugger doesn't have secure debug access, or similar for Privileged
86+
code in the case of UDE.</p>
87+
</td></tr>
88+
7989
<tr><td>dap_protocol</td>
8090
<td>str</td>
8191
<td>'default'</td>

pyocd/core/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"Path to custom config file."),
4040
OptionInfo('connect_mode', str, "halt",
4141
"One of 'halt', 'pre-reset', 'under-reset', 'attach'. Default is 'halt'."),
42+
OptionInfo('cpu.step.instruction.timeout', float, 0.0,
43+
"Timeout in seconds for instruction step operations. Defaults to 0, or no timeout."),
4244
OptionInfo('dap_protocol', str, 'default',
4345
"Wire protocol, either 'swd', 'jtag', or 'default'."),
4446
OptionInfo('dap_swj_enable', bool, True,

pyocd/core/soc_target.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ def run_token(self):
142142
def halt(self):
143143
return self.selected_core.halt()
144144

145-
def step(self, disable_interrupts=True, start=0, end=0):
146-
return self.selected_core.step(disable_interrupts, start, end)
145+
def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None):
146+
return self.selected_core.step(disable_interrupts, start, end, hook_cb)
147147

148148
def resume(self):
149149
return self.selected_core.resume()

pyocd/core/target.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def flush(self):
235235
def halt(self):
236236
raise NotImplementedError()
237237

238-
def step(self, disable_interrupts=True, start=0, end=0):
238+
def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None):
239239
raise NotImplementedError()
240240

241241
def resume(self):

pyocd/coresight/cortex_m.py

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class CortexM(Target, CoreSightCoreComponent):
113113
C_STEP = (1 << 2)
114114
C_MASKINTS = (1 << 3)
115115
C_SNAPSTALL = (1 << 5)
116+
C_PMOV = (1 << 6)
116117
S_REGRDY = (1 << 16)
117118
S_HALT = (1 << 17)
118119
S_SLEEP = (1 << 18)
@@ -473,68 +474,118 @@ def halt(self):
473474
self.flush()
474475
self.session.notify(Target.Event.POST_HALT, self, Target.HaltReason.USER)
475476

476-
def step(self, disable_interrupts=True, start=0, end=0):
477+
def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None):
477478
"""! @brief Perform an instruction level step.
478479
479-
This function preserves the previous interrupt mask state.
480+
This API will execute one or more individual instructions on the core. With default parameters, it
481+
masks interrupts and only steps a single instruction. The _start_ and _stop_ parameters define an
482+
address range of [_start_, _end_). The core will be repeatedly stepped until the PC falls outside this
483+
range, a debug event occurs, or the optional callback returns True.
484+
485+
The _disable_interrupts_ parameter controls whether to allow stepping into interrupts. This function
486+
preserves the previous interrupt mask state.
487+
488+
If the _hook_cb_ parameter is set to a callable, it will be invoked repeatedly to give the caller a
489+
chance to check for interrupt requests or other reasons to exit.
490+
491+
Note that stepping may take a very long time for to return in cases such as stepping over a branch
492+
into the Secure world where the debugger doesn't have secure debug access, or similar for Privileged
493+
code in the case of UDE.
494+
495+
@param self The object.
496+
@param disable_interrupts Boolean specifying whether to mask interrupts during the step.
497+
@param start Integer start address for range stepping. Not included in the range.
498+
@param end Integer end address for range stepping. The range is inclusive of this address.
499+
@param hook_cb Optional callable taking no parameters and returning a boolean. The signature is
500+
`hook_cb() -> bool`. Invoked repeatedly while waiting for step operations to complete. If the
501+
callback returns True, then stepping is stopped immediately.
502+
503+
@exception DebugError Raised if debug is not enabled on the core.
480504
"""
481-
# Was 'if self.get_state() != TARGET_HALTED:'
482-
# but now value of dhcsr is saved
483-
dhcsr = self.read_memory(CortexM.DHCSR)
484-
if not (dhcsr & (CortexM.C_STEP | CortexM.C_HALT)):
485-
LOG.error('cannot step: target not halted')
505+
# Save DHCSR and make sure the core is halted. We also check that C_DEBUGEN is set because if it's
506+
# not, then C_HALT is UNKNOWN.
507+
dhcsr = self.read32(CortexM.DHCSR)
508+
if not (dhcsr & CortexM.C_DEBUGEN):
509+
raise exception.DebugError('cannot step: debug not enabled')
510+
if not (dhcsr & CortexM.C_HALT):
511+
LOG.error('cannot step: core not halted')
486512
return
487513

488-
LOG.debug("step core %d", self.core_number)
514+
if start != end:
515+
LOG.debug("step core %d (start=%#010x, end=%#010x)", self.core_number, start, end)
516+
else:
517+
LOG.debug("step core %d", self.core_number)
489518

490519
self.session.notify(Target.Event.PRE_RUN, self, Target.RunType.STEP)
491520

521+
self._run_token += 1
522+
492523
self.clear_debug_cause_bits()
493524

494-
# Save previous interrupt mask state
495-
interrupts_masked = (CortexM.C_MASKINTS & dhcsr) != 0
525+
# Get current state.
526+
saved_maskints = dhcsr & CortexM.C_MASKINTS
527+
saved_pmov = dhcsr & CortexM.C_PMOV
528+
maskints_differs = bool(saved_maskints) != disable_interrupts
496529

497-
# Mask interrupts - C_HALT must be set when changing to C_MASKINTS
498-
if not interrupts_masked and disable_interrupts:
499-
self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT | CortexM.C_MASKINTS)
530+
# Get the DHCSR value to use when stepping based on whether we're masking interrupts.
531+
dhcsr_step = CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_STEP | saved_pmov
532+
if disable_interrupts:
533+
dhcsr_step |= CortexM.C_MASKINTS
500534

501-
# Single step using current C_MASKINTS setting
502-
while True:
503-
if disable_interrupts or interrupts_masked:
504-
self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_MASKINTS | CortexM.C_STEP)
505-
else:
506-
self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_STEP)
535+
# Update mask interrupts setting - C_HALT must be set when changing to C_MASKINTS.
536+
if maskints_differs:
537+
self.write32(CortexM.DHCSR, dhcsr_step | CortexM.C_HALT)
507538

508-
# Wait for halt to auto set (This should be done before the first read)
509-
while not self.read_memory(CortexM.DHCSR) & CortexM.C_HALT:
510-
pass
539+
# Get the step timeout. A timeout of 0 means no timeout, so we have to pass None to the Timeout class.
540+
step_timeout = self.session.options.get('cpu.step.instruction.timeout') or None
541+
542+
while True:
543+
# Single step using current C_MASKINTS setting
544+
self.write32(CortexM.DHCSR, dhcsr_step)
545+
546+
# Wait for halt to auto set.
547+
#
548+
# Note that it may take a very long time for this loop to exit in cases such as stepping over
549+
# a branch into the Secure world where the debugger doesn't have secure debug access, or similar
550+
# for Privileged code in the case of UDE.
551+
with timeout.Timeout(step_timeout) as tmo:
552+
while tmo.check():
553+
if (self.read32(CortexM.DHCSR) & CortexM.C_HALT) != 0:
554+
break
555+
# Invoke the callback if provided. If it returns True, then exit the loop.
556+
if (hook_cb is not None) and hook_cb():
557+
break
511558

512559
# Range is empty, 'range step' will degenerate to 'step'
513560
if start == end:
514561
break
515562

516563
# Read program counter and compare to [start, end)
517564
program_counter = self.read_core_register('pc')
518-
if program_counter < start or end <= program_counter:
565+
if (program_counter < start) or (end <= program_counter):
519566
break
520567

521-
# Check other stop reasons
522-
if self.read_memory(CortexM.DFSR) & (CortexM.DFSR_DWTTRAP | CortexM.DFSR_BKPT):
568+
# Check for stop reasons other than HALTED, which will have been set by our step action.
569+
if (self.read32(CortexM.DFSR) & ~CortexM.DFSR_HALTED) != 0:
523570
break
524571

525-
# Restore interrupt mask state
526-
if not interrupts_masked and disable_interrupts:
527-
# Unmask interrupts - C_HALT must be set when changing to C_MASKINTS
528-
self.write_memory(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT)
572+
# Restore interrupt mask state.
573+
if maskints_differs:
574+
self.write32(CortexM.DHCSR,
575+
CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT | saved_maskints | saved_pmov)
529576

530577
self.flush()
531578

532-
self._run_token += 1
533-
534579
self.session.notify(Target.Event.POST_RUN, self, Target.RunType.STEP)
535580

536581
def clear_debug_cause_bits(self):
537-
self.write_memory(CortexM.DFSR, CortexM.DFSR_VCATCH | CortexM.DFSR_DWTTRAP | CortexM.DFSR_BKPT | CortexM.DFSR_HALTED)
582+
self.write32(CortexM.DFSR,
583+
CortexM.DFSR_EXTERNAL
584+
| CortexM.DFSR_VCATCH
585+
| CortexM.DFSR_DWTTRAP
586+
| CortexM.DFSR_BKPT
587+
| CortexM.DFSR_HALTED
588+
)
538589

539590
def _perform_emulated_reset(self):
540591
"""! @brief Emulate a software reset by writing registers.

pyocd/coresight/cortex_m_v8m.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,15 @@ def get_security_state(self):
152152
return Target.SecurityState.SECURE
153153
else:
154154
return Target.SecurityState.NONSECURE
155+
156+
def clear_debug_cause_bits(self):
157+
self.write32(CortexM.DFSR,
158+
self.DFSR_PMU
159+
| CortexM.DFSR_EXTERNAL
160+
| CortexM.DFSR_VCATCH
161+
| CortexM.DFSR_DWTTRAP
162+
| CortexM.DFSR_BKPT
163+
| CortexM.DFSR_HALTED)
155164

156165
def get_halt_reason(self):
157166
"""! @brief Returns the reason the core has halted.

pyocd/coresight/generic_mem_ap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def read_memory_block32(self, addr, size):
8686
def halt(self):
8787
pass
8888

89-
def step(self, disable_interrupts=True, start=0, end=0):
89+
def step(self, disable_interrupts=True, start=0, end=0, hook_cb=None):
9090
pass
9191

9292
def reset(self, reset_type=None):

pyocd/gdbserver/gdbserver.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,22 @@ def resume(self, data):
625625
def step(self, data, start=0, end=0):
626626
addr = self._get_resume_step_addr(data)
627627
LOG.debug("GDB step: %s (start=0x%x, end=0x%x)", data, start, end)
628-
self.target.step(not self.step_into_interrupt, start, end)
629-
return self.create_rsp_packet(self.get_t_response())
628+
629+
# Use the step hook to check for an interrupt event.
630+
def step_hook():
631+
# Note we don't clear the interrupt event here!
632+
return self.packet_io.interrupt_event.is_set()
633+
self.target.step(not self.step_into_interrupt, start, end, hook_cb=step_hook)
634+
635+
# Clear and handle an interrupt.
636+
if self.packet_io.interrupt_event.is_set():
637+
LOG.debug("Received Ctrl-C during step")
638+
self.packet_io.interrupt_event.clear()
639+
response = self.get_t_response(forceSignal=signals.SIGINT)
640+
else:
641+
response = self.get_t_response()
642+
643+
return self.create_rsp_packet(response)
630644

631645
def halt(self):
632646
self.target.halt()

pyocd/utility/timeout.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,15 @@ class Timeout(object):
5252
If you pass a non-zero value for _sleeptime_ to the constructor, the check() method will
5353
automatically sleep by default starting with the second call. You can disable auto-sleep
5454
by passing `autosleep=False` to check().
55+
56+
Passing a timeout of None to the constructor is allowed. In this case, check() will always return
57+
True and the loop must be exited via some other means.
5558
"""
5659

5760
def __init__(self, timeout, sleeptime=0):
5861
"""! @brief Constructor.
5962
@param self
60-
@param timeout The timeout in seconds.
63+
@param timeout The timeout in seconds. May be None to indicate no timeout.
6164
@param sleeptime Time in seconds to sleep during calls to check(). Defaults to 0, thus
6265
check() will not sleep unless you pass a different value.
6366
"""
@@ -83,12 +86,16 @@ def check(self, autosleep=True):
8386
- A non-zero _sleeptime_ was passed to the constructor.
8487
- The _autosleep_ parameter is True.
8588
89+
This method is intended to be used as the predicate of a while loop.
90+
8691
@param self
8792
@param autosleep Whether to sleep if not timed out yet. The sleeptime passed to the
8893
constructor must have been non-zero.
94+
@retval True The timeout has _not_ occurred.
95+
@retval False Timeout is passed and the loop should be exited.
8996
"""
9097
# Check for a timeout.
91-
if (time() - self._start) > self._timeout:
98+
if (self._timeout is not None) and ((time() - self._start) > self._timeout):
9299
self._timed_out = True
93100
# Sleep if appropriate.
94101
elif (not self._is_first_check) and autosleep and self._sleeptime:

src/range_step/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.lst
2+
*.bin
3+
*.elf

0 commit comments

Comments
 (0)