diff --git a/docs/command_reference.md b/docs/command_reference.md
index aec1943e1..653330082 100644
--- a/docs/command_reference.md
+++ b/docs/command_reference.md
@@ -492,6 +492,16 @@ Show the target's current state.
Control thread awareness.
+
|
@@ -1104,6 +1114,14 @@ Show the target's current state.
Control thread awareness.
+### Trace
+
+##### `ctrace`
+
+**Usage**: ctrace reload \
+Reload and immediately apply the ctrace-run configuration.
+
+
### Utility
##### `sleep`
@@ -1185,8 +1203,8 @@ The current HNONSEC attribute value used by the selected MEM-AP. This value cont
**Usage**: show hprot, set hprot VALUE \
The current memory transfer attributes value used by the selected MEM-AP. This integer value controls attributes of memory transfers. It is a direct mapping of the AHB
or AXI attribute settings, depending on the type of MEM-AP. For AHB-APs, the value is HPROT[4:0].
-For AXI-APs, the value is {AxPROT[2:0}, AxCACHE[3:0]}, e.g. AxPROT in bits 6-4 and AxCACHE in
-its 3-0. Not all MEM-AP implementations support all attributes. See the Arm Technical Reference
+For AXI-APs, the value is {AxPROT[2:0], AxCACHE[3:0]}, e.g. AxPROT in bits 6-4 and AxCACHE in
+bits 3-0. Not all MEM-AP implementations support all attributes. See the Arm Technical Reference
Manual for your device's MEM-AP for details.
##### `locked`
diff --git a/pyocd/core/session.py b/pyocd/core/session.py
index c0bf0cd5e..4aef39877 100644
--- a/pyocd/core/session.py
+++ b/pyocd/core/session.py
@@ -35,7 +35,8 @@
from .options_manager import OptionsManager
from .target import Target
from ..utility.notification import Notifier
-from ..target.pack.cbuild_run import CbuildRun
+from ..target.csolution.cbuild_run import CbuildRun
+from ..target.csolution.ctrace_run import CTraceRun
if TYPE_CHECKING:
from types import TracebackType
@@ -112,6 +113,10 @@ class Event(Enum):
"""Session notification events."""
## Sent after target-specific trace support has been reinitialized following a reset.
TRACE_RESTART = 1
+ ## Sent before resuming target execution. Opens a new raw trace output file for capture.
+ TRACE_DATA_CAPTURE = 2
+ ## Sent after target has been halted. Flushes buffered raw trace data to its output file.
+ TRACE_DATA_FLUSH = 3
@classmethod
def get_current(cls) -> Self:
@@ -187,6 +192,7 @@ def __init__(
self._probeserver: Optional[DebugProbeServer] = None
self._context_state = SimpleNamespace()
self._cbuild_run: Optional[CbuildRun] = None
+ self._ctrace_run: Optional[CTraceRun] = None
self._trace_started: bool = False
# Set this session on the probe, if we were given a probe.
@@ -215,6 +221,11 @@ def __init__(
self._cbuild_run = CbuildRun(self.options.get('cbuild_run'))
cbuild_run_config = self._get_cbuild_run_config(command)
self._options.add_back(cbuild_run_config)
+ if self.cbuild_run is not None and self.cbuild_run.trace.enabled:
+ try:
+ self._ctrace_run = CTraceRun(self)
+ except (exceptions.Error, OSError) as err:
+ LOG.error("Failed to initialize ctrace-run support: %s", err)
# Load options from the config file.
config = self._get_config()
@@ -306,6 +317,17 @@ def _get_cbuild_run_config(self, command: Optional[str]) -> Dict[str, Any]:
debugger_options['systemview_auto_start'] = self.cbuild_run.systemview_auto_start
debugger_options['systemview_auto_stop'] = self.cbuild_run.systemview_auto_stop
+ swo_uart = self.cbuild_run.trace.swo_uart
+ if swo_uart is not None and swo_uart.enabled:
+ debugger_options['enable_swv'] = True
+ debugger_options['swv_system_clock'] = swo_uart.input_clock
+ debugger_options['swv_clock'] = swo_uart.output_clock
+ debugger_options['swv_raw_enable'] = True
+ if swo_uart.mode == 'file':
+ debugger_options['swv_raw_file'] = swo_uart.file
+ elif swo_uart.mode == 'server':
+ debugger_options['swv_raw_port'] = swo_uart.server_port
+
# Set reset types for load operations.
debugger_options['load.pre_reset'] = self.cbuild_run.pre_reset
debugger_options['load.post_reset'] = self.cbuild_run.post_reset
@@ -478,6 +500,11 @@ def cbuild_run(self) -> Optional[CbuildRun]:
"""@brief The CbuildRun instance if a cbuild-run file was loaded."""
return self._cbuild_run
+ @property
+ def ctrace_run(self) -> Optional[CTraceRun]:
+ """@brief The CTraceRun instance if trace was enabled by cbuild-run."""
+ return self._ctrace_run
+
def __enter__(self) -> "Session":
assert self._probe is not None
if self._auto_open:
@@ -604,7 +631,7 @@ def _trace_start(self) -> None:
if self._trace_started:
return
- if self.target is not None:
+ if self.target is not None and self.target.trace_enabled:
self.target.trace_start()
self._trace_started = True
self.subscribe(self._reset_handler, Target.Event.POST_RESET)
@@ -661,8 +688,7 @@ def open(self, init_board: bool = True) -> None:
if init_board:
self._board.init()
self._inited = True
- if self.options.get('enable_swv'):
- self._trace_start()
+ self._trace_start()
def disconnect(self) -> None:
"""@brief Disconnect the session without closing the probe.
diff --git a/pyocd/core/soc_target.py b/pyocd/core/soc_target.py
index 47d8c018c..9a6120473 100644
--- a/pyocd/core/soc_target.py
+++ b/pyocd/core/soc_target.py
@@ -314,6 +314,12 @@ def get_target_context(self, core: Optional[int] = None) -> DebugContext:
core_obj = self.selected_core_or_raise
return core_obj.get_target_context()
+ @property
+ def trace_enabled(self) -> bool:
+ """@brief Whether trace capture and flush handling is enabled."""
+ delegate = self.debug_sequence_delegate
+ return delegate is not None and delegate.trace_enabled
+
def trace_start(self):
self.call_delegate('trace_start', target=self, mode=0)
diff --git a/pyocd/coresight/coresight_target.py b/pyocd/coresight/coresight_target.py
index b1e8b0340..238d6f879 100644
--- a/pyocd/coresight/coresight_target.py
+++ b/pyocd/coresight/coresight_target.py
@@ -26,17 +26,19 @@
from ..flash.flash import Flash
from . import (dap, discovery)
from ..debug.svd.loader import SVDLoader
+from ..debug.sequences.delegates import TraceSetup
+from ..trace.sink import TraceBufferSinks
from ..utility.sequencer import CallSequence
from ..target.pack.flm_region_builder import FlmFlashRegionBuilder
if TYPE_CHECKING:
from ..core.session import Session
- from ..core.memory_map import MemoryMap
from .ap import (APAddressBase, AccessPort)
from ..debug.svd.model import SVDDevice
LOG = logging.getLogger(__name__)
+
class CoreSightTarget(SoCTarget):
"""@brief Represents an SoC that uses CoreSight debug infrastructure.
@@ -55,6 +57,7 @@ def __init__(self, session: "Session", memory_map: Optional["MemoryMap"] = None)
self._svd_load_thread: Optional[SVDLoader] = None
self._irq_table: Optional[Dict[int, str]] = None
self._discoverer: Optional[Callable] = None
+ self._trace_buffer_sinks: Optional[TraceBufferSinks] = None
self.session.context_state.is_performing_pre_reset = False
@@ -375,26 +378,78 @@ def first_ap(self) -> Optional["AccessPort"]:
return None
return sorted(self.aps.values(), key=lambda v: v.address)[0]
+ @property
+ def trace_enabled(self) -> bool:
+ """@brief Whether trace capture and flush handling is enabled."""
+ return self.session.options.get('enable_swv') or super().trace_enabled
+
def trace_start(self) -> None:
- result = self.call_delegate('trace_start', target=self, mode=0)
- if not result and self.has_debug_sequence('TraceStart', pname=self.selected_core_or_raise.node_name):
- assert self.debug_sequence_delegate
- self.debug_sequence_delegate.run_sequence('TraceStart',
- pname=self.selected_core_or_raise.node_name)
- result = True
- return result
+ if not self.trace_enabled:
+ return
+
+ self._ensure_trace_buffer_sinks()
+ try:
+ result = self.call_delegate('trace_start', target=self, mode=0)
+ if not result:
+ self._run_trace_sequence('TraceStart')
+ except Exception:
+ self._shutdown_trace_buffer_sinks()
+ raise
def trace_stop(self) -> None:
- result = self.call_delegate('trace_stop', target=self, mode=0)
- if not result and self.has_debug_sequence('TraceStop', pname=self.selected_core_or_raise.node_name):
- assert self.debug_sequence_delegate
- self.debug_sequence_delegate.run_sequence('TraceStop',
- pname=self.selected_core_or_raise.node_name)
- result = True
- return result
+ if not self.trace_enabled:
+ return
+
+ try:
+ result = self.call_delegate('trace_stop', target=self, mode=0)
+ if not result:
+ self._run_trace_sequence('TraceStop')
+ finally:
+ self._shutdown_trace_buffer_sinks()
+
+ def _run_trace_sequence(self, name: str) -> None:
+ delegate = self.debug_sequence_delegate
+ if delegate is None:
+ return
+
+ for seq in delegate.all_sequences:
+ if seq.name == name:
+ delegate.run_sequence(name, pname=seq.pname)
def trace_capture(self) -> None:
- pass
+ changed = False
+ if self.session.ctrace_run is not None:
+ changed = self.session.ctrace_run.apply(self)
+
+ result = self.call_delegate('trace_capture', target=self, mode=0)
+ if not result and self.has_debug_sequence('TraceCapture'):
+ assert self.debug_sequence_delegate
+ if self.debug_sequence_delegate.trace_setup == TraceSetup.FULL:
+ self.debug_sequence_delegate.run_sequence('TraceCapture')
+ self.session.notify(self.session.Event.TRACE_DATA_CAPTURE, self.session, changed)
def trace_flush(self) -> None:
- pass
+ result = self.call_delegate('trace_flush', target=self, mode=0)
+ if not result and self.has_debug_sequence('TraceFlush'):
+ assert self.debug_sequence_delegate
+ if self.debug_sequence_delegate.trace_setup == TraceSetup.FULL:
+ self.debug_sequence_delegate.run_sequence('TraceFlush')
+ self.session.notify(self.session.Event.TRACE_DATA_FLUSH, self.session)
+
+ def write_trace_buffer(self, name: str, data: bytes) -> int:
+ """Write data to a configured trace buffer output."""
+ if self._trace_buffer_sinks is None:
+ raise ValueError("Trace buffer output is not initialized")
+ return self._trace_buffer_sinks.write(name, data)
+
+ def _ensure_trace_buffer_sinks(self) -> None:
+ delegate = self.debug_sequence_delegate
+ if self._trace_buffer_sinks is None and delegate is not None:
+ trace_buffers = delegate.trace_buffers
+ if trace_buffers:
+ self._trace_buffer_sinks = TraceBufferSinks(self.session, trace_buffers)
+
+ def _shutdown_trace_buffer_sinks(self) -> None:
+ if self._trace_buffer_sinks is not None:
+ self._trace_buffer_sinks.shutdown()
+ self._trace_buffer_sinks = None
diff --git a/pyocd/debug/sequences/delegates.py b/pyocd/debug/sequences/delegates.py
index ee57eeefd..75ff44e4a 100644
--- a/pyocd/debug/sequences/delegates.py
+++ b/pyocd/debug/sequences/delegates.py
@@ -18,7 +18,8 @@
from __future__ import annotations
import logging
-from typing import (TYPE_CHECKING, Optional, Set)
+from enum import Enum
+from typing import (Any, Mapping, TYPE_CHECKING, Optional, Set)
from .scope import Scope
@@ -28,6 +29,12 @@
from .sequences import (DebugSequence, DebugSequenceExecutionContext, FlashSequenceParams)
from ...target.pack.cmsis_pack import CmsisPackDevice
+class TraceSetup(Enum):
+ """@brief Trace setup modes for debug sequences."""
+ LEGACY = 'legacy'
+ FULL = 'full'
+
+
class DebugSequenceDelegate:
"""@brief Delegate interface for handling sequence operations."""
@@ -36,6 +43,21 @@ def all_sequences(self) -> Set[DebugSequence]:
"""@brief Returns a set containing all defined debug sequence objects."""
raise NotImplementedError()
+ @property
+ def trace_setup(self) -> TraceSetup:
+ """@brief Returns the trace setup mode."""
+ return TraceSetup.LEGACY
+
+ @property
+ def trace_enabled(self) -> bool:
+ """@brief Whether trace capture and flush handling is enabled."""
+ return False
+
+ @property
+ def trace_buffers(self) -> dict[str, Any]:
+ """@brief Selected named trace buffers."""
+ return {}
+
@property
def cmsis_pack_device(self) -> CmsisPackDevice:
"""@brief Accessor for the pack device that contains the sequences."""
diff --git a/pyocd/debug/sequences/functions.py b/pyocd/debug/sequences/functions.py
index dc981f6f9..e219118ff 100644
--- a/pyocd/debug/sequences/functions.py
+++ b/pyocd/debug/sequences/functions.py
@@ -79,6 +79,7 @@ def ensure_capacity(buffer: _Buffer, size: int) -> None:
if size > len(buffer.data):
buffer.data.extend(b"\x00" * (size - len(buffer.data)))
+
# Disable warnings for the non-standard methods names we use to match the sequences function
# names, since introspection is used to look up functions.
# pylint: disable=invalid_name
@@ -819,14 +820,14 @@ def bufferstreamout(self, id: int, offset: int, length: int, path: str, mode: in
@param path: Constant string value representing the destination to which to stream data to. \
Refer to character sequences for path/file name place holders.
@param mode: Specifies how to treat the data source in the specified mode.
- - Bit 0..3: Format of the data source: 0 - Binary File
+ - Bit 0..3: Format of the data source: 0 - Binary File 1 - Trace Buffer
- Bit 4..7: Communication options for data sink: 0 - Overwrite 1 - Append
@param timeout: Timeout in microseconds. If 0, then synchronously wait for the operation to finish.
@return: Number of bytes streamed to data sink.
"""
fmt = mode & 0xF
- if fmt not in (0,):
- raise DebugSequenceRuntimeError("Only binary file streams (mode 0) are supported")
+ if fmt not in (0, 1):
+ raise DebugSequenceRuntimeError("Unsupported buffer stream output format")
buffer = self._buffer_manager.get(id, create=False)
available = len(buffer.data) - offset
@@ -836,6 +837,12 @@ def bufferstreamout(self, id: int, offset: int, length: int, path: str, mode: in
write_len = min(length, available)
data = bytes(buffer.data[offset:offset + write_len])
+ if fmt == 1:
+ try:
+ return self.target.write_trace_buffer(path, data)
+ except ValueError as err:
+ raise DebugSequenceRuntimeError(str(err)) from err
+
file_path = self._expand_path(path)
append = (mode & 0x10) != 0
@@ -861,6 +868,14 @@ def _do_write() -> None:
return write_len
+ def tracebufferselected(self, name: str) -> int:
+ """Return whether a configured trace buffer is selected by name."""
+ trace_buffers = self.context.delegate.trace_buffers
+ if not name and len(trace_buffers) > 1:
+ raise DebugSequenceRuntimeError("Empty trace buffer name is ambiguous")
+ sink = trace_buffers.get(name)
+ return int(sink is not None and sink.enabled)
+
def runapplication(self, path: str, args: str, workdir: str, timeout: int) -> int:
"""@brief Run an external application.
diff --git a/pyocd/gdbserver/gdbserver.py b/pyocd/gdbserver/gdbserver.py
index 54a740776..e47af762a 100644
--- a/pyocd/gdbserver/gdbserver.py
+++ b/pyocd/gdbserver/gdbserver.py
@@ -425,12 +425,12 @@ def __init__(self, session, core=None):
# pylint: enable=invalid-name
def trace_flush(self) -> None:
- # TraceFlush stub
- pass
+ if self.board.target.trace_enabled:
+ self.board.target.trace_flush()
def trace_capture(self) -> None:
- # TraceCapture stub
- pass
+ if self.board.target.trace_enabled:
+ self.board.target.trace_capture()
def _init_remote_commands(self):
"""@brief Initialize the remote command processor infrastructure."""
diff --git a/pyocd/gdbserver/gdbserver_commands.py b/pyocd/gdbserver/gdbserver_commands.py
index 5698b1705..a3f7542cd 100644
--- a/pyocd/gdbserver/gdbserver_commands.py
+++ b/pyocd/gdbserver/gdbserver_commands.py
@@ -1,5 +1,5 @@
# pyOCD debugger
-# Copyright (c) 2020 Arm Limited
+# Copyright (c) 2020,2026 Arm Limited
# Copyright (c) 2021 Chris Reed
# SPDX-License-Identifier: Apache-2.0
#
@@ -127,6 +127,33 @@ def execute(self):
for server in self.context.session.gdbservers.values():
server.stop(wait=False)
+class CTraceRunCommand(CommandBase):
+ INFO = {
+ 'names': ['ctrace'],
+ 'group': 'gdbserver',
+ 'category': 'trace',
+ 'nargs': 1,
+ 'usage': "reload",
+ 'help': "Reload and immediately apply the ctrace-run configuration.",
+ }
+
+ def parse(self, args):
+ if args[0] != 'reload':
+ raise exceptions.CommandError("invalid action")
+
+ def execute(self):
+ ctrace_run = self.context.session.ctrace_run
+ if ctrace_run is None:
+ raise exceptions.CommandError("ctrace-run support is not enabled")
+ if not self.context.target.is_halted():
+ raise exceptions.CommandError("target must be halted to reload and apply ctrace-run configuration")
+
+ reloaded = ctrace_run.reload(self.context.target)
+ if reloaded:
+ self.context.write("ctrace-run configuration reloaded and applied")
+ else:
+ self.context.write("ctrace-run configuration could not be reloaded or applied")
+
class RTTCommand(CommandBase):
INFO = {
'names': ['rtt'],
diff --git a/pyocd/subcommands/run_cmd.py b/pyocd/subcommands/run_cmd.py
index a0e98bf06..589caa825 100644
--- a/pyocd/subcommands/run_cmd.py
+++ b/pyocd/subcommands/run_cmd.py
@@ -154,6 +154,10 @@ def invoke(self) -> int:
for run_server in self._run_servers:
run_server.start()
+ # Trace Capture
+ if session.board.target.trace_enabled:
+ session.board.target.trace_capture()
+
# Wait for all servers to complete or timelimit to expire
start_time = time()
timelimit = self._args.timelimit
@@ -177,6 +181,8 @@ def invoke(self) -> int:
self.shutdown()
return 1
finally:
+ if session.board.target.trace_enabled:
+ session.board.target.trace_flush()
if swv_reader:
swv_reader.stop()
diff --git a/pyocd/target/csolution/__init__.py b/pyocd/target/csolution/__init__.py
new file mode 100644
index 000000000..efc12eaac
--- /dev/null
+++ b/pyocd/target/csolution/__init__.py
@@ -0,0 +1,15 @@
+# pyOCD debugger
+# Copyright (c) 2026 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.
diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/csolution/cbuild_run.py
similarity index 85%
rename from pyocd/target/pack/cbuild_run.py
rename to pyocd/target/csolution/cbuild_run.py
index 3ec731845..d4a6ccc23 100644
--- a/pyocd/target/pack/cbuild_run.py
+++ b/pyocd/target/csolution/cbuild_run.py
@@ -24,9 +24,9 @@
from pathlib import Path
from copy import deepcopy
from dataclasses import dataclass
-from typing import (cast, Optional, Set, Dict, List, Tuple, Any, TYPE_CHECKING)
+from typing import (cast, Optional, Set, Dict, List, Tuple, Any, Literal, TYPE_CHECKING)
-from .flash_algo import PackFlashAlgo
+from ..pack.flash_algo import PackFlashAlgo
from ...flash.flash_dsq import FlashDebugSequence
from .. import (normalise_target_type_name, TARGET)
from ...coresight.coresight_target import CoreSightTarget
@@ -39,14 +39,13 @@
from ...debug.svd.loader import SVDFile
from ...utility.cmdline import convert_reset_type
from ...debug.sequences.scope import Scope
-from ...debug.sequences.delegates import DebugSequenceDelegate
+from ...debug.sequences.delegates import DebugSequenceDelegate, TraceSetup
from ...debug.sequences.functions import DebugSequenceCommonFunctions
from ...debug.sequences.sequences import (Block, DebugSequence, DebugSequenceExecutionContext)
from ...debug.sequences.default_sequences import (DefaultDebugSequences, _YAMLSequenceParser)
if TYPE_CHECKING:
from ...core.session import Session
- from ...coresight.cortex_m import CortexM
from ...core.core_target import CoreTarget
from ...utility.sequencer import CallSequence
from ...commands.execution_context import CommandSet
@@ -219,6 +218,7 @@ def __init__(self, yml_path: str) -> None:
self._vars: Optional[Dict[str, str]] = None
self._sequences: Optional[List[dict]] = None
self._debugger: Optional[Dict[str, Any]] = None
+ self._trace_config: Optional[TraceConfig] = None
self._debug_topology: Optional[Dict[str, Any]] = None
self._memory_map: Optional[MemoryMap] = None
self._programming: Optional[List[dict]] = None
@@ -252,9 +252,9 @@ def __init__(self, yml_path: str) -> None:
raise CbuildRunError(f"Invalid header in .cbuild-run.yml file '{yml_file_path}'")
except OSError as err:
if yml_path == "":
- raise CbuildRunError("Cannot access *.cbuild-run.yml file: no path provided")
+ raise CbuildRunError("Cannot access .cbuild-run.yml file: no path provided")
else:
- raise CbuildRunError(f"Cannot access *.cbuild-run.yml file '{yml_path}': {err.strerror}") from err
+ raise CbuildRunError(f"Cannot access .cbuild-run.yml file '{yml_path}': {err.strerror}") from err
def _cmsis_pack_root(self) -> None:
"""@brief Sets the CMSIS_PACK_ROOT environment variable if not already set.
@@ -410,6 +410,14 @@ def pack_path(self) -> str:
# Returns device-pack-path if specified, otherwise an empty string.
return self._data.get('device-pack-path', '')
+ @property
+ def solution_set(self) -> str:
+ """@brief Combined name of the +@."""
+ target_set = self._data.get('target-set')
+ if target_set in (None, ''):
+ return self._cbuild_name
+ return f"{self._cbuild_name}@{target_set}"
+
@property
def svd(self) -> Optional[str]:
"""@brief Path to the SVD file for the target device."""
@@ -453,6 +461,17 @@ def debug_vars(self) -> Dict[str, str]:
LOG.debug("Read debug variables")
return self._vars
+ @property
+ def trace_setup(self) -> TraceSetup:
+ """@brief Debug sequence configuration."""
+ conf = self._data.get('debug-sequences-conf') or {}
+ value = conf.get('trace-setup', TraceSetup.LEGACY.value)
+ try:
+ return TraceSetup(value)
+ except ValueError:
+ LOG.warning("Invalid trace-setup value '%s' in debug-sequences-conf; using '%s'", value, TraceSetup.LEGACY.value)
+ return TraceSetup.LEGACY
+
@property
def valid_dps(self) -> List[int]:
"""@brief List of valid debug ports."""
@@ -643,10 +662,8 @@ def stdio_mode(self) -> Tuple:
"""@brief STDIO mode assignments from debugger section.
The method will not be called frequently, so performance is not critical.
"""
- SUPPORTED_MODES = { 'off', 'server', 'file', 'console' }
- MODE_ALIASES = { False: 'off',
- 'monitor': 'server'
- }
+ SUPPORTED_MODES = {'off', 'server', 'file', 'console'}
+ MODE_ALIASES = {False: 'off', 'monitor': 'server'}
# Get STDIO configuration from debugger section
stdio_config = self._get_stdio_config()
valid_config = any('mode' in s for s in stdio_config)
@@ -776,6 +793,19 @@ def systemview_auto_stop(self) -> Optional[bool]:
sv = self.debugger.get('systemview') or {}
return sv.get('auto-stop')
+ @property
+ def trace(self) -> "TraceConfig":
+ """@brief Parsed trace configuration from the debugger and debug topology sections."""
+ if self._trace_config is None:
+ self._trace_config = TraceConfig(
+ self.debugger,
+ self.debug_topology,
+ self._base_path,
+ self.proj_path,
+ self.solution_set,
+ )
+ return self._trace_config
+
def populate_target(self, target: Optional[str] = None) -> None:
"""@brief Generates and populates the target defined by the .cbuild-run.yml file."""
if target is None:
@@ -792,7 +822,7 @@ def populate_target(self, target: Optional[str] = None) -> None:
"_cbuild_device": self,
"__init__": CbuildRunTargetMethods._cbuild_target_init,
"create_init_sequence": CbuildRunTargetMethods._cbuild_target_create_init_sequence,
- "update_processor_name" : CbuildRunTargetMethods._cbuild_target_update_processor_name,
+ "update_processor_name": CbuildRunTargetMethods._cbuild_target_update_processor_name,
"configure_core_reset": CbuildRunTargetMethods._cbuild_target_configure_core_reset,
"add_core": CbuildRunTargetMethods._cbuild_target_add_core,
"get_output": CbuildRunTargetMethods._cbuild_target_get_output,
@@ -1154,6 +1184,163 @@ def get_svd_path(pname: Optional[str] = None) -> Optional[str]:
ap_address=APv1Address(0),
svd_path=get_svd_path())
+
+@dataclass(frozen=True)
+class TraceSink:
+ """@brief A resolved trace source selected by a cbuild-run configuration."""
+ type: Literal['swo-uart', 'trace-buffer']
+ name: Optional[str]
+ mode: Literal['off', 'server', 'file']
+ input_clock: Optional[int] = None
+ output_clock: Optional[int] = None
+ server_port: Optional[int] = None
+ file: Optional[str] = None
+
+ @property
+ def enabled(self) -> bool:
+ return self.mode != 'off'
+
+
+class TraceConfig:
+ """@brief Parse and validate trace sources selected in a cbuild-run file."""
+
+ SUPPORTED_MODES = {'off', 'server', 'file'}
+
+ def __init__(
+ self,
+ debugger: Dict[str, Any],
+ topology: Dict[str, Any],
+ base_path: Path,
+ project_path: str,
+ solution_set: str,
+ ) -> None:
+ trace_sinks = topology.get('trace-sinks')
+ if trace_sinks is None:
+ trace_sinks = [{'serialwire': None}, {'tracebuffer': None}]
+ elif not isinstance(trace_sinks, list):
+ LOG.warning("Debug topology trace-sinks must be a list")
+ trace_sinks = []
+
+ serialwire_count = 0
+ trace_buffer_names = []
+ for sink in trace_sinks:
+ if not isinstance(sink, dict):
+ LOG.warning("Ignoring invalid trace sink in debug topology")
+ continue
+ serialwire_count += 'serialwire' in sink
+ if 'tracebuffer' not in sink:
+ continue
+ name = sink['tracebuffer']
+ if name is None or isinstance(name, str):
+ trace_buffer_names.append(name)
+ else:
+ LOG.warning("Ignoring trace buffer with invalid name %r in debug topology", name)
+
+ has_serialwire = serialwire_count > 0
+ if serialwire_count > 1:
+ LOG.warning("Debug topology has multiple serialwire sinks; ignoring all but the first")
+
+ if len(trace_buffer_names) > 1 and None in trace_buffer_names:
+ LOG.warning("Ignoring unnamed trace buffer: all trace buffers must be named when more than one is present")
+ trace_buffer_names.remove(None)
+ for name in set(trace_buffer_names):
+ if trace_buffer_names.count(name) > 1:
+ LOG.warning("Ignoring duplicate trace buffer '%s' in debug topology", name or '')
+ trace_buffer_names = [buffer_name for buffer_name in trace_buffer_names if buffer_name != name]
+ self._sources: List[TraceSink] = []
+ self._swo_uart: Optional[TraceSink] = None
+ self._trace_buffers: Dict[str, TraceSink] = {}
+ next_server_port = 5555
+
+ for entry in debugger.get('trace') or []:
+ if not isinstance(entry, dict):
+ LOG.warning("Trace configuration entries in cbuild-run must be mappings")
+ continue
+
+ if 'swo-uart' in entry:
+ source_type: Literal['swo-uart', 'trace-buffer'] = 'swo-uart'
+ elif 'trace-buffer' in entry:
+ source_type = 'trace-buffer'
+ else:
+ LOG.warning("Trace configuration in cbuild-run must specify either 'swo-uart' or 'trace-buffer'")
+ continue
+
+ if source_type == 'swo-uart' and self._swo_uart is not None:
+ LOG.warning("Ignoring additional 'swo-uart' trace configuration in cbuild-run")
+ continue
+
+ name = entry.get(source_type)
+ if name is not None and not isinstance(name, str):
+ LOG.warning("Ignoring %s trace configuration with invalid name %r", source_type, name)
+ continue
+ if source_type == 'swo-uart' and not has_serialwire:
+ LOG.warning("Ignoring 'swo-uart' trace configuration: serialwire is not supported by debug topology")
+ continue
+ if source_type == 'trace-buffer':
+ if name is None and trace_buffer_names != [None]:
+ LOG.warning("Ignoring unnamed trace-buffer configuration: debug topology must have exactly one unnamed trace buffer")
+ continue
+ if name is not None and name not in trace_buffer_names:
+ LOG.warning("Ignoring trace-buffer configuration '%s': not supported by debug topology", name)
+ continue
+ buffer_key = name or ''
+ if buffer_key in self._trace_buffers:
+ LOG.warning("Ignoring duplicate trace-buffer configuration '%s'", name or '')
+ continue
+
+ mode = entry.get('mode', 'off')
+ mode = 'off' if mode is False else mode
+ if mode not in self.SUPPORTED_MODES:
+ LOG.warning("Invalid trace mode '%s' in cbuild-run; disabling trace source", mode)
+ mode = 'off'
+
+ input_clock = entry.get('input-clock')
+ if source_type == 'swo-uart' and mode != 'off' and input_clock is None:
+ LOG.warning("Trace input clock not specified in cbuild-run; disabling SWO trace")
+ mode = 'off'
+
+ server_port = None
+ if mode == 'server':
+ server_port = entry.get('server-port')
+ if server_port is None:
+ server_port = next_server_port
+ next_server_port += 1
+ file = None
+ if mode == 'file':
+ channel = 'SWO' if source_type == 'swo-uart' else 'TB'
+ if source_type == 'trace-buffer' and name:
+ channel += f"_{name}"
+ default_file = Path(project_path) / '.trace' / f"{solution_set}.{channel}.raw"
+ file_path = Path(entry.get('file', default_file)).expanduser()
+ if not file_path.is_absolute():
+ file_path = base_path / file_path
+ file = str(file_path.resolve())
+
+ sink = TraceSink(source_type, name, cast(Any, mode), input_clock, entry.get('output-clock'), server_port, file)
+ self._sources.append(sink)
+ if source_type == 'swo-uart':
+ self._swo_uart = sink
+ else:
+ self._trace_buffers[name or ''] = sink
+
+ LOG.debug("Read %d trace configurations", len(self._sources))
+
+ @property
+ def swo_uart(self) -> Optional[TraceSink]:
+ """@brief The sole configured SWO UART source, if any."""
+ return self._swo_uart
+
+ @property
+ def trace_buffers(self) -> Dict[str, TraceSink]:
+ """@brief Trace-buffer configurations keyed by their topology names."""
+ return self._trace_buffers.copy()
+
+ @property
+ def enabled(self) -> Tuple[TraceSink, ...]:
+ """@brief All enabled trace sources in declaration order."""
+ return tuple(source for source in self._sources if source.enabled)
+
+
class CbuildRunSequences(_YAMLSequenceParser):
"""@brief Parses debug sequences and debug variable definitions from .cbuild-run.yml."""
@@ -1165,6 +1352,7 @@ def __init__(self, device: CbuildRun) -> None:
self._debugvars: Optional[Block] = None
self._debugvars_conf: Optional[Block] = None
+ self._device_settings: Optional[Block] = None
self._sequences: Set[DebugSequence] = set()
@property
@@ -1186,6 +1374,15 @@ def dbgconf_variables(self) -> Optional[Block]:
self._debugvars_conf = self._dbgconf_variables(dbgconf_file)
return self._debugvars_conf
+ @property
+ def device_settings(self) -> Optional[Block]:
+ if self._device_settings is None:
+ _device_settings = self._cbuild_debugger.get('device-settings') or {}
+ settings = "".join(f"{key} = {value};\n" for key, value in _device_settings.items())
+ if settings:
+ self._device_settings = Block(settings, info='device-settings')
+ return self._device_settings
+
@property
def sequences(self) -> Set[DebugSequence]:
if not self._sequences:
@@ -1245,6 +1442,21 @@ def all_sequences(self) -> Set[DebugSequence]:
self._all_sequences.update(pname_dict.values())
return self._all_sequences
+ @property
+ def trace_setup(self) -> TraceSetup:
+ """@brief Returns the trace setup mode from cbuild-run configuration."""
+ return self._device.trace_setup
+
+ @property
+ def trace_enabled(self) -> bool:
+ """@brief Whether cbuild-run enables trace capture and flush debug sequences."""
+ return bool(self._device.trace.enabled)
+
+ @property
+ def trace_buffers(self) -> Dict[str, TraceSink]:
+ """@brief Selected named trace buffers."""
+ return self._device.trace.trace_buffers
+
@property
def cmsis_pack_device(self) -> CbuildRun:
return self._device
@@ -1262,18 +1474,25 @@ def get_root_scope(self, context: DebugSequenceExecutionContext) -> Scope:
if self._debugvars is not None:
return self._debugvars
- # Populate default debugvars with values from *.cbuild-run.yml file.
+ # Populate default debugvars with values from .cbuild-run.yml file.
self._debugvars = Scope(name='debugvars')
debugvars_block = self._cbuild_sequences.variables
if debugvars_block is not None:
with context.push(debugvars_block, self._debugvars):
debugvars_block.execute(context)
- # Override default debugvars with values from *.dbgconf file.
- debugvars_conf_block = self._cbuild_sequences.dbgconf_variables
- if debugvars_conf_block is not None:
- with context.push(debugvars_conf_block, self._debugvars):
- debugvars_conf_block.execute(context)
+ # if `device-settings:` is present then an also specified `*.dbgconf` file is ignored.
+ device_settings_block = self._cbuild_sequences.device_settings
+ if device_settings_block is not None:
+ # Override debugvars with values from the 'device-settings' node in .cbuild-run.yml
+ with context.push(device_settings_block, self._debugvars):
+ device_settings_block.execute(context)
+ else:
+ # Override default debugvars with values from *.dbgconf file.
+ debugvars_conf_block = self._cbuild_sequences.dbgconf_variables
+ if debugvars_conf_block is not None:
+ with context.push(debugvars_conf_block, self._debugvars):
+ debugvars_conf_block.execute(context)
# Make all vars read-only.
self._debugvars.freeze()
@@ -1425,8 +1644,10 @@ def get_traceout(self) -> int:
- [2] trace buffer enabled?
- [21:16] selected parallel trace port size
"""
- # Set SWO bit depending on the option value.
- return 1 if self._target.session.options.get('enable_swv') else 0
+ traceout = 1 if self._target.session.options.get('enable_swv') else 0
+ if any(sink.enabled for sink in self._device.trace.trace_buffers.values()):
+ traceout |= 1 << 2
+ return traceout
def get_traceclockin(self) -> int:
"""@brief Return the system clock frequency in Hz for __traceclockin.
diff --git a/pyocd/target/csolution/ctrace_run.py b/pyocd/target/csolution/ctrace_run.py
new file mode 100644
index 000000000..9b42d4d70
--- /dev/null
+++ b/pyocd/target/csolution/ctrace_run.py
@@ -0,0 +1,418 @@
+# pyOCD debugger
+# Copyright (c) 2026 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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+import hashlib
+import logging
+from pathlib import Path
+import re
+import threading
+from typing import (Any, Dict, Iterable, List, Mapping, Optional, Set, Tuple, TYPE_CHECKING, Union)
+
+import yaml
+
+from ...core import exceptions
+
+if TYPE_CHECKING:
+ from ...core.core_target import CoreTarget
+ from ...core.session import Session
+ from ...core.soc_target import SoCTarget
+ from ...utility.notification import Notification
+
+LOG = logging.getLogger(__name__)
+
+DEMCR = 0xE000EDFC
+DEMCR_TRCENA = (1 << 24)
+
+CORESIGHT_LAR_OFFSET = 0xFB0
+CORESIGHT_LAR_KEY = 0xC5ACCE55
+
+
+class CTraceRunError(exceptions.Error):
+ """Custom exception for errors encountered during processing of .ctrace-run.yml."""
+
+
+class CTraceRunTargetError(CTraceRunError):
+ """Error encountered while applying ctrace-run configuration to the target."""
+
+
+@dataclass(frozen=True)
+class _CTraceRunRegister:
+ """A resolved ctrace-run register access."""
+
+ component: str
+ register: str
+ address: int
+ value: int
+ base_address: int
+ mask: Optional[int] = None
+ pname: Optional[str] = None
+
+
+@dataclass(frozen=True)
+class _CTraceRunData:
+ """Immutable parsed ctrace-run configuration."""
+
+ references: Tuple[Tuple[str, Optional[str]], ...]
+ register_access: Tuple[_CTraceRunRegister, ...]
+
+
+class _CTraceRunParser:
+ """Cached parser for a .ctrace-run.yml file."""
+
+ _DEFAULT_BASE_ADDRESSES: Dict[str, int] = {
+ 'ITM': 0xE0000000,
+ 'DWT': 0xE0001000,
+ 'PMU': 0xE0003000,
+ 'ETM': 0xE0041000,
+ }
+
+ _REGISTER_OFFSETS: Dict[str, Dict[str, int]] = {
+ 'ITM': {
+ 'ITM_TPR': 0xE40,
+ 'ITM_TCR': 0xE80,
+ },
+ 'DWT': {
+ 'DWT_CTRL': 0x000,
+ 'DWT_CYCCNT': 0x004,
+ 'DWT_CPICNT': 0x008,
+ 'DWT_EXCCNT': 0x00C,
+ 'DWT_SLEEPCNT': 0x010,
+ 'DWT_LSUCNT': 0x014,
+ 'DWT_FOLDCNT': 0x018,
+ },
+ 'PMU': {
+ 'PMU_CCNTR': 0x07C,
+ 'PMU_CCFILTR': 0x47C,
+ 'PMU_CNTENSET': 0xC00,
+ 'PMU_CNTENCLR': 0xC20,
+ 'PMU_INTENSET': 0xC40,
+ 'PMU_INTENCLR': 0xC60,
+ 'PMU_OVSCLR': 0xC80,
+ 'PMU_SWINC': 0xCA0,
+ 'PMU_OVSSET': 0xCC0,
+ 'PMU_CTRL': 0xE04,
+ },
+ }
+
+ _INDEXED_REGISTER_PATTERNS: Dict[str, Tuple[Tuple[re.Pattern[str], int, int], ...]] = {
+ 'ITM': (
+ (re.compile(r'^ITM_TER(\d+)$'), 0xE00, 4),
+ ),
+ 'DWT': (
+ (re.compile(r'^DWT_COMP(\d+)$'), 0x020, 0x10),
+ (re.compile(r'^DWT_MASK(\d+)$'), 0x024, 0x10),
+ (re.compile(r'^DWT_FUNCTION(\d+)$'), 0x028, 0x10),
+ ),
+ 'PMU': (
+ (re.compile(r'^PMU_EVCNTR(\d+)$'), 0x000, 4),
+ (re.compile(r'^PMU_EVTYPER(\d+)$'), 0x400, 4),
+ ),
+ }
+
+ def __init__(self, yml_path: Union[str, Path]) -> None:
+ """Create a ctrace-run processor for a YAML file."""
+ self._path = Path(yml_path).expanduser().resolve()
+ self._digest: Optional[bytes] = None
+ self._data: Optional[_CTraceRunData] = None
+
+ def load(self, force: bool = False) -> Optional[Tuple[bytes, _CTraceRunData]]:
+ """Read and parse the file if its content has changed."""
+ try:
+ yml_content = self._path.read_bytes()
+ except FileNotFoundError:
+ LOG.debug("No ctrace-run file found at '%s'", self._path)
+ self._digest = None
+ self._data = None
+ return None
+ except OSError as err:
+ raise CTraceRunError(f"cannot access file: {err.strerror}") from err
+
+ digest = hashlib.sha256(yml_content).digest()
+ if not force and digest == self._digest and self._data is not None:
+ return digest, self._data
+
+ data = self._parse(yml_content)
+ self._digest = digest
+ self._data = data
+ return digest, data
+
+ def _parse(self, yml_content: bytes) -> _CTraceRunData:
+ try:
+ yml_data = yaml.safe_load(yml_content)
+ except yaml.YAMLError as err:
+ raise CTraceRunError(f"invalid YAML: {err}") from err
+
+ if not isinstance(yml_data, dict) or 'ctrace-run' not in yml_data:
+ raise CTraceRunError("invalid header")
+
+ data = yml_data['ctrace-run']
+ if not isinstance(data, dict):
+ raise CTraceRunError("'ctrace-run' must be a dictionary")
+
+ refs = data.get('ctrace-refs')
+ if not isinstance(refs, list):
+ raise CTraceRunError("'ctrace-refs' must be a list")
+
+ references: List[Tuple[str, Optional[str]]] = []
+ register_access: List[_CTraceRunRegister] = []
+ for ref_index, ref in enumerate(refs, start=1):
+ try:
+ reference, ref_access = self._parse_reference(ref, ref_index)
+ except CTraceRunError as err:
+ LOG.warning("Ignoring invalid ctrace-run reference in '%s': %s", self._path, err)
+ continue
+ if ref_access:
+ references.append(reference)
+ register_access.extend(ref_access)
+
+ LOG.debug("Parsed ctrace-run configuration from '%s'", self._path)
+ return _CTraceRunData(tuple(references), tuple(register_access))
+
+ def _parse_reference(self, ref: Any, ref_index: int) -> Tuple[Tuple[str, Optional[str]], List[_CTraceRunRegister]]:
+ if not isinstance(ref, dict):
+ raise CTraceRunError(f"ctrace-run reference entry {ref_index} must be a dictionary")
+
+ ref_name = ref.get('ctrace-ref')
+ if not isinstance(ref_name, str) or not ref_name:
+ raise CTraceRunError(f"ctrace-run reference entry {ref_index} requires a non-empty 'ctrace-ref'")
+
+ regs = ref.get('regs')
+ if regs is None:
+ regs = []
+ if not isinstance(regs, list):
+ raise CTraceRunError(f"'regs' in ctrace-run reference '{ref_name}' must be a list")
+
+ pname = ref.get('pname')
+ if regs and pname is not None and (not isinstance(pname, str) or not pname):
+ raise CTraceRunError(f"'pname' in ctrace-run reference '{ref_name}' must be a non-empty string")
+
+ return ((ref_name, pname), self._parse_regs(regs, pname, ref_name))
+
+ def _parse_regs(self, regs: Iterable[Mapping[str, Any]], pname: Optional[str], ref_name: str) -> List[_CTraceRunRegister]:
+ register_access: List[_CTraceRunRegister] = []
+
+ for reg in regs:
+ if not isinstance(reg, dict):
+ raise CTraceRunError(f"register entry in ctrace-run reference '{ref_name}' must be a dictionary")
+
+ reg_name = reg.get('name')
+ value = reg.get('value')
+ if not isinstance(reg_name, str) or not reg_name:
+ raise CTraceRunError(f"register entry in ctrace-run reference '{ref_name}' requires a non-empty 'name'")
+ if value is None:
+ raise CTraceRunError(f"register '{reg_name}' in ctrace-run reference '{ref_name}' requires a 'value'")
+
+ resolved = self._resolve_register(reg_name)
+ if resolved is None:
+ raise CTraceRunError(f"unknown register '{reg_name}' in ctrace-run reference '{ref_name}'")
+ resolved_component, resolved_base, address = resolved
+
+ try:
+ register_access.append(_CTraceRunRegister(
+ component=resolved_component,
+ register=reg_name,
+ address=address,
+ value=self._parse_u32(value, 'value'),
+ mask=self._parse_u32(reg['mask'], 'mask') if 'mask' in reg else None,
+ base_address=resolved_base,
+ pname=pname,
+ ))
+ except (TypeError, ValueError) as err:
+ raise CTraceRunError(f"invalid register '{reg_name}' in ctrace-run reference '{ref_name}': {err}") from err
+
+ return register_access
+
+ @classmethod
+ def _resolve_register(cls, reg_name: str) -> Optional[Tuple[str, int, int]]:
+ component = reg_name.partition('_')[0]
+ base_address = cls._DEFAULT_BASE_ADDRESSES.get(component)
+ if base_address is None:
+ return None
+
+ offsets = cls._REGISTER_OFFSETS.get(component)
+ offset = offsets.get(reg_name) if offsets is not None else None
+
+ if offset is None:
+ for pattern, base, stride in cls._INDEXED_REGISTER_PATTERNS.get(component, ()):
+ match = pattern.match(reg_name)
+ if match:
+ offset = base + (int(match.group(1)) * stride)
+ break
+
+ return None if offset is None else (component, base_address, base_address + offset)
+
+ @staticmethod
+ def _parse_u32(value: Any, name: str) -> int:
+ if isinstance(value, bool):
+ raise ValueError(f"{name} must be an integer")
+ if isinstance(value, int):
+ result = value
+ elif isinstance(value, str):
+ result = int(value, 0)
+ else:
+ raise TypeError(f"{name} must be an integer")
+
+ if not 0 <= result <= 0xFFFFFFFF:
+ raise ValueError(f"{name} must be an unsigned 32-bit integer")
+ return result
+
+
+class CTraceRun:
+ """Load and apply a CMSIS-Toolbox .ctrace-run.yml configuration."""
+
+ def __init__(self, session: "Session") -> None:
+ self._lock = threading.RLock()
+ self._last_applied_digest: Optional[bytes] = None
+ self._last_error: Optional[str] = None
+
+ cbuild_run = session.cbuild_run
+ if cbuild_run is None or not cbuild_run.trace.enabled:
+ raise CTraceRunError("Cannot create CTraceRun when cbuild-run trace is not enabled")
+
+ cbuild_run_path = session.options.get('cbuild_run')
+ if cbuild_run_path is None:
+ raise CTraceRunError("Cannot derive ctrace-run path without a cbuild-run path")
+
+ suffix = '.cbuild-run.yml'
+ source_path = Path(str(cbuild_run_path)).expanduser().resolve()
+ source_name = source_path.name
+ if not source_name.lower().endswith(suffix):
+ raise CTraceRunError(f"Cannot derive ctrace-run name from cbuild-run file '{cbuild_run_path}'")
+
+ project_path = cbuild_run.proj_path if cbuild_run.proj_path_name else None
+ trace_root = (Path(project_path).expanduser().resolve() if project_path else source_path.parent)
+ self._path = trace_root / '.trace' / f"{cbuild_run.solution_set}.ctrace-run.yml"
+ self._parser = _CTraceRunParser(self._path)
+ session.subscribe(self._trace_restart_handler, session.Event.TRACE_RESTART, session)
+
+ def apply(self, target: "SoCTarget", force: bool = False) -> bool:
+ """Apply new or explicitly reloaded configuration and return whether it was applied."""
+ with self._lock:
+ try:
+ loaded = self._parser.load(force=force)
+ if loaded is None:
+ self._last_applied_digest = None
+ self._last_error = None
+ return False
+
+ digest, data = loaded
+ if not force and digest == self._last_applied_digest:
+ return False
+
+ self._apply_to_target(target, data)
+ self._last_applied_digest = digest
+ self._last_error = None
+ return True
+ except exceptions.Error as err:
+ self._report_error(err)
+ return False
+
+ def reload(self, target: "SoCTarget") -> bool:
+ """Reload and apply the file even if it has not changed."""
+ return self.apply(target, force=True)
+
+ def invalidate(self) -> None:
+ """Invalidate the last applied configuration, so that it will be reapplied on the next call to apply()."""
+ with self._lock:
+ self._last_applied_digest = None
+
+ def _trace_restart_handler(self, notification: "Notification") -> None:
+ """Invalidate the applied configuration after target trace support restarts."""
+ self.invalidate()
+
+ def _report_error(self, error: exceptions.Error) -> None:
+ error_message = str(error)
+ if error_message == self._last_error:
+ LOG.debug("ctrace-run configuration '%s' was not applied: %s", self._path, error)
+ else:
+ if isinstance(error, CTraceRunTargetError):
+ LOG.error("Failed to apply ctrace-run configuration '%s': %s", self._path, error)
+ else:
+ LOG.warning("Ignoring invalid ctrace-run configuration '%s': %s", self._path, error)
+ self._last_error = error_message
+
+ def _apply_to_target(self, target: "SoCTarget", data: _CTraceRunData) -> None:
+ if not data.register_access:
+ LOG.debug("No ctrace-run register access to apply")
+ return
+ access_targets = self._resolve_access_targets(target, data.references)
+
+ enabled_targets: Set[int] = set()
+ unlocked_components: Set[Tuple[int, int]] = set()
+
+ for reg in data.register_access:
+ access_target = access_targets[reg.pname]
+ target_key = id(access_target)
+
+ if target_key not in enabled_targets:
+ self._enable_trace_access(access_target)
+ enabled_targets.add(target_key)
+
+ if reg.component in ('DWT', 'ITM'):
+ component_key = (target_key, reg.base_address)
+ if component_key not in unlocked_components:
+ self._unlock_component(access_target, reg.component, reg.base_address)
+ unlocked_components.add(component_key)
+
+ self._modify_register(access_target, reg)
+
+ def _resolve_access_targets(self, target: "SoCTarget", references: Iterable[Tuple[str, Optional[str]]]) -> Dict[Optional[str], "CoreTarget"]:
+ cores_by_pname = {core.node_name: core for core in target.cores.values()}
+ access_targets: Dict[Optional[str], "CoreTarget"] = {}
+
+ for ref_name, pname in references:
+ if pname is None and len(target.cores) > 1:
+ raise CTraceRunError(f"missing 'pname' in ctrace-run reference '{ref_name}' for a multicore target")
+ if pname is not None and pname not in cores_by_pname:
+ raise CTraceRunError(f"unknown pname '{pname}' in ctrace-run reference '{ref_name}'")
+ access_targets[pname] = (target.selected_core_or_raise if pname is None else cores_by_pname[pname])
+
+ return access_targets
+
+ @staticmethod
+ def _modify_register(target: "CoreTarget", reg: _CTraceRunRegister) -> None:
+ try:
+ value = reg.value
+ if reg.mask is not None:
+ current = target.read32(reg.address)
+ value = (current & ~reg.mask) | (reg.value & reg.mask)
+ target.write32(reg.address, value)
+ LOG.debug("ctrace-run wrote %s = 0x%08x at 0x%08x%s",
+ reg.register, value, reg.address, f" for processor '{target.node_name}'" if target.node_name else "",)
+ except exceptions.Error as err:
+ pname = f" for processor '{target.node_name}'" if target.node_name else ""
+ raise CTraceRunTargetError(f"register {reg.register} access at 0x{reg.address:08x}{pname} failed: {err}") from err
+
+ @staticmethod
+ def _enable_trace_access(target: "CoreTarget") -> None:
+ try:
+ demcr = target.read32(DEMCR)
+ if (demcr & DEMCR_TRCENA) == 0:
+ target.write32(DEMCR, demcr | DEMCR_TRCENA)
+ except exceptions.Error as err:
+ pname = f" for processor '{target.node_name}'" if target.node_name else ""
+ raise CTraceRunTargetError(f"enabling trace access{pname} failed: {err}") from err
+
+ @staticmethod
+ def _unlock_component(target: "CoreTarget", component: str, base_address: int) -> None:
+ try:
+ target.write32(base_address + CORESIGHT_LAR_OFFSET, CORESIGHT_LAR_KEY)
+ except exceptions.Error as err:
+ pname = f" for processor '{target.node_name}'" if target.node_name else ""
+ raise CTraceRunTargetError(f"unlocking {component} at 0x{base_address:08x}{pname} failed: {err}") from err
diff --git a/pyocd/target/pack/cmsis_pack.py b/pyocd/target/pack/cmsis_pack.py
index 69d8babe9..912b4431c 100644
--- a/pyocd/target/pack/cmsis_pack.py
+++ b/pyocd/target/pack/cmsis_pack.py
@@ -32,6 +32,7 @@
from .flash_algo import PackFlashAlgo
from ...core import exceptions
+from ...debug.sequences.delegates import TraceSetup
from ...core.memory_map import (
FlashRegion,
MemoryMap,
@@ -74,6 +75,7 @@ class _DeviceInfo:
debugports: List[Element] = field(default_factory=list)
accessports: List[Element] = field(default_factory=list)
flashinfo: List[Element] = field(default_factory=list)
+ trace_setup: Optional[TraceSetup] = None
@dataclass
class ProcessorInfo:
@@ -280,6 +282,12 @@ def _parse_devices(self, parent: Element) -> None:
elif elem.tag == 'debug':
newState.debugs.append(elem)
elif elem.tag == 'sequences':
+ if 'traceSetup' in elem.attrib:
+ value = elem.attrib['traceSetup']
+ try:
+ newState.trace_setup = TraceSetup(value)
+ except ValueError:
+ LOG.warning("Invalid traceSetup value '%s'; ignoring attribute", value)
newState.sequences += elem.findall('sequence')
elif elem.tag == 'debugvars':
newState.debugvars.append(elem)
@@ -310,6 +318,7 @@ def _parse_devices(self, parent: Element) -> None:
debugports=self._extract_debugports(),
accessports=self._extract_accessports(),
flashinfo=self._extract_flashinfo(),
+ trace_setup=self._extract_trace_setup(),
)
# Support ._pack being None for testing.
@@ -579,6 +588,14 @@ def filter(map: Dict, elem: Element) -> None:
return self._extract_items('sequences', filter)
+ def _extract_trace_setup(self) -> TraceSetup:
+ """@brief Extract the traceSetup attribute from inherited elements."""
+ result = TraceSetup.LEGACY
+ for state in self._state_stack:
+ if state.trace_setup is not None:
+ result = state.trace_setup
+ return result
+
def _extract_debugvars(self) -> List[Element]:
"""@brief Extract debugvar elements.
@@ -1229,6 +1246,11 @@ def debug_vars_sequence(self) -> Optional[Block]:
self._debugvars = Block(elem.text, info="debugvars")
return self._debugvars
+ @property
+ def trace_setup(self) -> TraceSetup:
+ """@brief The trace setup mode for debug sequences."""
+ return self._info.trace_setup or TraceSetup.LEGACY
+
@property
def valid_dps(self) -> List[int]:
"""@brief List of valid DP indices.
diff --git a/pyocd/target/pack/pack_target.py b/pyocd/target/pack/pack_target.py
index b1e57a8fb..efac7e477 100644
--- a/pyocd/target/pack/pack_target.py
+++ b/pyocd/target/pack/pack_target.py
@@ -30,7 +30,7 @@
from ...coresight.ap import APv1Address
from ...coresight.coresight_target import CoreSightTarget
from ...coresight.cortex_m import CortexM
-from ...debug.sequences.delegates import DebugSequenceDelegate
+from ...debug.sequences.delegates import DebugSequenceDelegate, TraceSetup
from ...debug.sequences.functions import DebugSequenceCommonFunctions
from ...debug.sequences.sequences import (Block, DebugSequence, DebugSequenceExecutionContext)
from ...debug.sequences.default_sequences import DefaultDebugSequences
@@ -175,6 +175,11 @@ def all_sequences(self) -> Set[DebugSequence]:
self._all_sequences.update(pname_dict.values())
return self._all_sequences
+ @property
+ def trace_setup(self) -> TraceSetup:
+ """@brief Returns the trace setup mode from pack device."""
+ return self._pack_device.trace_setup
+
@property
def cmsis_pack_device(self) -> CmsisPackDevice:
"""@brief Accessor for the pack device that contains the sequences."""
diff --git a/pyocd/trace/sink.py b/pyocd/trace/sink.py
index 31991d60b..fecbcfb4b 100644
--- a/pyocd/trace/sink.py
+++ b/pyocd/trace/sink.py
@@ -1,5 +1,5 @@
# pyOCD debugger
-# Copyright (c) 2017-2019 Arm Limited
+# Copyright (c) 2017-2019,2026 Arm Limited
# COpyright (c) 2021-2022 Chris Reed
# SPDX-License-Identifier: Apache-2.0
#
@@ -16,11 +16,165 @@
# limitations under the License.
import collections.abc
-from typing import (TYPE_CHECKING, Iterable, List, Optional, Sequence, Union)
+import logging
+from pathlib import Path
+from typing import (Any, BinaryIO, Dict, Mapping, TYPE_CHECKING, Iterable, List, Optional, Sequence, Union)
+
+from ..utility.server import StreamServer
if TYPE_CHECKING:
from .events import TraceEvent
+LOG = logging.getLogger(__name__)
+
+
+class TraceDataSink:
+ """Base interface for a raw trace output destination."""
+
+ @staticmethod
+ def file(path: Path) -> "TraceDataSink":
+ """Create a raw trace file destination."""
+ return _TraceFileSink(path)
+
+ @staticmethod
+ def server(port: int, serve_local_only: bool, name: str) -> "TraceDataSink":
+ """Create a raw trace TCP server destination."""
+ return _TraceServerSink(port, serve_local_only, name)
+
+ def start(self, changed: bool) -> None:
+ """Start a trace capture, resetting output if the configuration changed."""
+ raise NotImplementedError()
+
+ def write(self, data: bytes) -> int:
+ """Write raw trace data."""
+ raise NotImplementedError()
+
+ def flush(self) -> None:
+ """Flush data at the end of a capture."""
+ raise NotImplementedError()
+
+ def shutdown(self) -> None:
+ """Release the output destination."""
+ raise NotImplementedError()
+
+
+class _TraceFileSink(TraceDataSink):
+ """Raw trace data written to a file from capture through flush."""
+
+ def __init__(self, path: Path) -> None:
+ self._path = path
+ self._file: Optional[BinaryIO] = None
+ self._started = False
+
+ def start(self, changed: bool) -> None:
+ self.flush()
+ if self._path.parent.name == '.trace':
+ self._path.parent.mkdir(exist_ok=True)
+ self._file = self._path.open('wb' if changed or not self._started else 'ab')
+ self._started = True
+
+ def write(self, data: bytes) -> int:
+ if self._file is None:
+ return 0
+ self._file.write(data)
+ return len(data)
+
+ def flush(self) -> None:
+ if self._file is not None:
+ self._file.flush()
+ self._file.close()
+ self._file = None
+
+ def shutdown(self) -> None:
+ self.flush()
+
+
+class _TraceServerSink(TraceDataSink):
+ """Raw trace data delivered to a TCP client by a StreamServer."""
+
+ def __init__(self, port: int, serve_local_only: bool, name: str) -> None:
+ self._server = StreamServer(
+ port,
+ serve_local_only=serve_local_only,
+ name=name,
+ is_read_only=True,
+ )
+
+ def start(self, changed: bool) -> None:
+ pass
+
+ def write(self, data: bytes) -> int:
+ return self._server.write(data)
+
+ def flush(self) -> None:
+ pass
+
+ def shutdown(self) -> None:
+ self._server.stop()
+
+
+class TraceBufferSinks:
+ """Routes named raw trace streams to configured sinks."""
+
+ def __init__(self, session: Any, trace_buffers: Mapping[str, Any]) -> None:
+ self._session = session
+ self._trace_buffers = trace_buffers
+ self._outputs: Dict[str, TraceDataSink] = {}
+ self._capture_changed = True
+ for name, sink in trace_buffers.items():
+ if not sink.enabled:
+ continue
+ try:
+ self._outputs[name] = self._create_output(sink)
+ except OSError as err:
+ LOG.warning("Failed to initialize TB '%s' output: %s", name, err)
+ session.subscribe(self._trace_data_handler, session.Event.TRACE_DATA_CAPTURE, session)
+ session.subscribe(self._trace_data_handler, session.Event.TRACE_DATA_FLUSH, session)
+
+ def shutdown(self) -> None:
+ self._session.unsubscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_CAPTURE)
+ self._session.unsubscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_FLUSH)
+ for output in self._outputs.values():
+ output.shutdown()
+ self._outputs.clear()
+
+ def _trace_data_handler(self, notification: Any) -> None:
+ if notification.event == self._session.Event.TRACE_DATA_CAPTURE:
+ self._capture_changed = bool(notification.data)
+ for name, output in tuple(self._outputs.items()):
+ try:
+ output.start(self._capture_changed)
+ except OSError as err:
+ LOG.warning("Failed to start TB '%s' output: %s", name, err)
+ del self._outputs[name]
+ else:
+ for output in self._outputs.values():
+ output.flush()
+
+ def write(self, name: str, data: bytes) -> int:
+ sink = self._trace_buffers.get(name)
+ if sink is None or not sink.enabled:
+ raise ValueError(f"TB '{name}' is not selected")
+
+ output = self._outputs.get(name)
+ try:
+ if output is None:
+ output = self._create_output(sink)
+ output.start(self._capture_changed)
+ self._outputs[name] = output
+ return output.write(data)
+ except OSError as err:
+ raise ValueError(f"failed to write TB '{name}': {err}") from err
+
+ def _create_output(self, trace_buffer: Any) -> TraceDataSink:
+ if trace_buffer.mode == 'file' and trace_buffer.file is not None:
+ return TraceDataSink.file(Path(trace_buffer.file))
+ if trace_buffer.mode == 'server' and trace_buffer.server_port is not None:
+ return TraceDataSink.server(trace_buffer.server_port, self._session.options.get('serve_local_only'),
+ f"TB {trace_buffer.name or 'default'} raw")
+ raise ValueError(f"TB '{trace_buffer.name}' has no output destination")
+
+
class TraceEventSink:
"""@brief Abstract interface for a trace event sink."""
def receive(self, event: "TraceEvent") -> None:
@@ -93,4 +247,3 @@ def receive(self, event: "TraceEvent") -> None:
"""
for sink in self._sinks:
sink.receive(event)
-
diff --git a/pyocd/trace/swo.py b/pyocd/trace/swo.py
index 85dd5d05c..a6c106f58 100644
--- a/pyocd/trace/swo.py
+++ b/pyocd/trace/swo.py
@@ -1,5 +1,5 @@
# pyOCD debugger
-# Copyright (c) 2017-2019 Arm Limited
+# Copyright (c) 2017-2019,2026 Arm Limited
# Copyright (c) 2021-2022 Chris Reed
# SPDX-License-Identifier: Apache-2.0
#
@@ -93,13 +93,14 @@ def _merge_data_trace_events(self, event: events.TraceEvent) -> bool:
# queue separately.
if event.comparator == self._pending_data_trace.comparator:
# Merge the two data trace events.
+ pending = self._pending_data_trace
ev = events.TraceDataTraceEvent(cmpn=event.comparator,
- pc=(event.pc or self._pending_data_trace.pc),
- addr=(event.address or self._pending_data_trace.address),
- value=(event.value or self._pending_data_trace.value),
- rnw=(event.is_read or self._pending_data_trace.is_read),
- sz=(event.transfer_size or self._pending_data_trace.transfer_size),
- ts=self._pending_data_trace.timestamp)
+ pc=(event.pc if event.pc is not None else pending.pc),
+ addr=(event.address if event.address is not None else pending.address),
+ value=(event.value if event.value is not None else pending.value),
+ rnw=(event.is_read if event.is_read is not None else pending.is_read),
+ sz=(event.transfer_size if event.transfer_size is not None else pending.transfer_size),
+ ts=pending.timestamp)
else:
ev = self._pending_data_trace
self._pending_events.append(ev)
diff --git a/pyocd/trace/swv.py b/pyocd/trace/swv.py
index 0e1cf0c85..c405dce71 100644
--- a/pyocd/trace/swv.py
+++ b/pyocd/trace/swv.py
@@ -20,16 +20,16 @@
from pathlib import Path
import threading
from time import sleep
-from typing import (Optional, BinaryIO, TextIO, TYPE_CHECKING)
+from typing import (Optional, TextIO, TYPE_CHECKING)
-from .sink import TraceEventSink
+from .sink import (TraceDataSink, TraceEventSink)
from .events import (TraceEvent, TraceITMEvent)
from .swo import SWOParser
from ..coresight.itm import ITM
from ..coresight.tpiu import TPIU
from ..core import exceptions
from ..probe.debug_probe import DebugProbe
-from ..utility.server import StreamServer
+from ..debug.sequences.delegates import TraceSetup
if TYPE_CHECKING:
from ..core.session import Session
@@ -37,6 +37,7 @@
LOG = logging.getLogger(__name__)
+
class SWVEventSink(TraceEventSink):
"""@brief Trace event sink that converts ITM packets to a text stream."""
@@ -95,14 +96,23 @@ def __init__(self, session: "Session", core_number: int = 0) -> None:
self._sys_clock = 0
self._swo_clock = 0
self._is_subscribed = False
+ self._raw_output: Optional[TraceDataSink] = None
target = self._session.target
assert target
self._target = target
self._core = target.cores[core_number]
+ if target.debug_sequence_delegate is not None:
+ self._trace_setup = target.debug_sequence_delegate.trace_setup
+ else:
+ self._trace_setup = TraceSetup.LEGACY
def _init_components(self, sys_clock: int, swo_clock: int) -> bool:
"""@brief Configure the target's standard SWV trace components."""
+ if self._trace_setup == TraceSetup.FULL:
+ # The target's debug sequence delegate is responsible for configuring the trace components
+ return True
+
itm = self._target.get_first_child_of_type(ITM)
if not itm:
LOG.warning("SWV not initalized: Target does not have ITM component")
@@ -156,7 +166,20 @@ def init(self, sys_clock: int, swo_clock: int, console: Optional[TextIO]) -> boo
self._sink = SWVEventSink(console)
self._parser.connect(self._sink)
+ if self._session.options.get('swv_raw_enable') and self._raw_output is None:
+ raw_file = self._session.options.get('swv_raw_file')
+ if raw_file:
+ self._raw_output = TraceDataSink.file(Path(raw_file).expanduser())
+ else:
+ self._raw_output = TraceDataSink.server(
+ self._session.options.get('swv_raw_port'),
+ self._session.options.get('serve_local_only'),
+ 'SWV raw',
+ )
+
self._session.subscribe(self._reset_handler, self._session.Event.TRACE_RESTART, self._session)
+ self._session.subscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_CAPTURE, self._session)
+ self._session.subscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_FLUSH, self._session)
self._is_subscribed = True
self.start()
@@ -172,18 +195,22 @@ def stop(self) -> None:
"""
if self._is_subscribed:
self._session.unsubscribe(self._reset_handler, self._session.Event.TRACE_RESTART)
+ self._session.unsubscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_CAPTURE)
+ self._session.unsubscribe(self._trace_data_handler, self._session.Event.TRACE_DATA_FLUSH)
self._is_subscribed = False
- if not self.is_alive():
- return
+ if self.is_alive():
+ self._shutdown_event.set()
+ self.join()
- self._shutdown_event.set()
- self.join()
+ if self._raw_output is not None:
+ self._raw_output.shutdown()
- # init() should never have started the SWV thread unless the target has ITM and TPIU.
- itm = self._target.get_first_child_of_type(ITM)
- assert itm
- itm.disable()
+ if self._trace_setup == TraceSetup.LEGACY:
+ # init() should never have started the SWV thread unless the target has ITM and TPIU.
+ itm = self._target.get_first_child_of_type(ITM)
+ assert itm
+ itm.disable()
def run(self) -> None:
"""@brief SWV reader thread routine.
@@ -194,24 +221,6 @@ def run(self) -> None:
"""
assert self._session.probe
- swv_raw_server = None
- swv_raw_file: Optional[BinaryIO] = None
- if self._session.options.get('swv_raw_enable'):
- raw_file_name = self._session.options.get('swv_raw_file')
- if raw_file_name:
- # swv_raw_file takes precedence over swv_raw_port.
- raw_file_path = Path(raw_file_name).expanduser()
- try:
- swv_raw_file = raw_file_path.open('wb')
- except OSError as err:
- LOG.warning("Failed to open SWV raw output file '%s': %s", raw_file_path, err)
- else:
- swv_raw_server = StreamServer(
- self._session.options.get('swv_raw_port'),
- serve_local_only=self._session.options.get('serve_local_only'),
- name="SWV raw",
- is_read_only=True)
-
# Stop SWO first in case the probe already had it started. Ignore if this fails.
try:
self._session.probe.swo_stop()
@@ -222,23 +231,14 @@ def run(self) -> None:
while not self._shutdown_event.is_set():
data = self._session.probe.swo_read()
if data:
- if swv_raw_file:
- swv_raw_file.write(data)
- elif swv_raw_server:
- swv_raw_server.write(data)
+ if self._raw_output is not None:
+ self._raw_output.write(data)
self._parser.parse(data)
sleep(0.001)
self._session.probe.swo_stop()
- if swv_raw_file:
- swv_raw_file.flush()
- swv_raw_file.close()
-
- if swv_raw_server:
- swv_raw_server.stop()
-
def _reset_handler(self, notification: "Notification") -> None:
"""@brief Reconfigure SWV components after target trace support has restarted."""
if not self.is_alive():
@@ -248,3 +248,21 @@ def _reset_handler(self, notification: "Notification") -> None:
self._init_components(self._sys_clock, self._swo_clock)
except exceptions.Error:
LOG.warning("Failed to reinitialize SWV after reset", exc_info=self._session.log_tracebacks)
+
+ def _trace_data_handler(self, notification: "Notification") -> None:
+ """Open or flush the raw trace output destination."""
+ if self._raw_output is None:
+ return
+
+ if notification.event == self._session.Event.TRACE_DATA_CAPTURE:
+ try:
+ self._raw_output.start(bool(notification.data))
+ except OSError as err:
+ LOG.warning("Failed to start SWV raw output: %s", err)
+ return
+ elif notification.event == self._session.Event.TRACE_DATA_FLUSH:
+ try:
+ sleep(0.003)
+ self._raw_output.flush()
+ except OSError as err:
+ LOG.warning("Failed to update SWV raw output file: %s", err)
|