From 3f2bb60ccf8920829b585ab9d9fc6857cbf74b97 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:52:29 +0200 Subject: [PATCH 01/13] trace: add CMSIS TraceSetup support --- pyocd/coresight/coresight_target.py | 13 +++++++++++-- pyocd/debug/sequences/delegates.py | 12 ++++++++++++ pyocd/target/pack/cbuild_run.py | 18 +++++++++++++++++- pyocd/target/pack/cmsis_pack.py | 22 ++++++++++++++++++++++ pyocd/target/pack/pack_target.py | 7 ++++++- pyocd/trace/swv.py | 18 ++++++++++++++---- 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/pyocd/coresight/coresight_target.py b/pyocd/coresight/coresight_target.py index b1e8b0340..c4622f8ad 100644 --- a/pyocd/coresight/coresight_target.py +++ b/pyocd/coresight/coresight_target.py @@ -26,6 +26,7 @@ from ..flash.flash import Flash from . import (dap, discovery) from ..debug.svd.loader import SVDLoader +from ..debug.sequences.delegates import TraceSetup from ..utility.sequencer import CallSequence from ..target.pack.flm_region_builder import FlmFlashRegionBuilder @@ -394,7 +395,15 @@ def trace_stop(self) -> None: return result def trace_capture(self) -> None: - pass + 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') 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') diff --git a/pyocd/debug/sequences/delegates.py b/pyocd/debug/sequences/delegates.py index ee57eeefd..110ae6954 100644 --- a/pyocd/debug/sequences/delegates.py +++ b/pyocd/debug/sequences/delegates.py @@ -18,6 +18,7 @@ from __future__ import annotations import logging +from enum import Enum from typing import (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,11 @@ 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 cmsis_pack_device(self) -> CmsisPackDevice: """@brief Accessor for the pack device that contains the sequences.""" diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/pack/cbuild_run.py index 3ec731845..40774f60e 100644 --- a/pyocd/target/pack/cbuild_run.py +++ b/pyocd/target/pack/cbuild_run.py @@ -39,7 +39,7 @@ 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) @@ -453,6 +453,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.""" @@ -1245,6 +1256,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 cbuild-run configuration.""" + return self._device.trace_setup + @property def cmsis_pack_device(self) -> CbuildRun: return self._device 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/swv.py b/pyocd/trace/swv.py index 0e1cf0c85..8eba65755 100644 --- a/pyocd/trace/swv.py +++ b/pyocd/trace/swv.py @@ -30,6 +30,7 @@ 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 @@ -100,9 +101,17 @@ def __init__(self, session: "Session", core_number: int = 0) -> None: 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") @@ -180,10 +189,11 @@ def stop(self) -> None: self._shutdown_event.set() self.join() - # 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. From c53514e151622c766aba8bc48c01d16d18f1a506 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 09:04:13 +0200 Subject: [PATCH 02/13] coresight: run trace start/stop sequences for all processors --- pyocd/coresight/coresight_target.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pyocd/coresight/coresight_target.py b/pyocd/coresight/coresight_target.py index c4622f8ad..50fb1a8e1 100644 --- a/pyocd/coresight/coresight_target.py +++ b/pyocd/coresight/coresight_target.py @@ -378,21 +378,22 @@ def first_ap(self) -> Optional["AccessPort"]: 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 result: + self._run_trace_sequence('TraceStart') 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 result: + self._run_trace_sequence('TraceStop') + + 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: result = self.call_delegate('trace_capture', target=self, mode=0) From d4b1f994da57a7f8547074b8734a34b879a1a5ca Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:52:29 +0200 Subject: [PATCH 03/13] cbuild-run: add device-settings processing --- pyocd/target/pack/cbuild_run.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/pack/cbuild_run.py index 40774f60e..44af43a64 100644 --- a/pyocd/target/pack/cbuild_run.py +++ b/pyocd/target/pack/cbuild_run.py @@ -1176,6 +1176,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 @@ -1197,6 +1198,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: @@ -1285,11 +1295,18 @@ def get_root_scope(self, context: DebugSequenceExecutionContext) -> Scope: 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() From e69bc6a1eb8f08dd2fcab599ba4e25c08d92f935 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:52:30 +0200 Subject: [PATCH 04/13] cbuild-run: add support for trace node and sink configuration --- pyocd/core/session.py | 11 ++ pyocd/target/pack/cbuild_run.py | 214 ++++++++++++++++++++++++++++++-- 2 files changed, 212 insertions(+), 13 deletions(-) diff --git a/pyocd/core/session.py b/pyocd/core/session.py index c0bf0cd5e..b8f145029 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -306,6 +306,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 diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/pack/cbuild_run.py index 44af43a64..4ee253e9c 100644 --- a/pyocd/target/pack/cbuild_run.py +++ b/pyocd/target/pack/cbuild_run.py @@ -24,7 +24,7 @@ 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 ...flash.flash_dsq import FlashDebugSequence @@ -46,7 +46,6 @@ 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.""" @@ -654,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) @@ -787,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: @@ -803,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, @@ -1165,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.""" @@ -1271,6 +1447,16 @@ 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 @@ -1288,7 +1474,7 @@ 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: @@ -1298,7 +1484,7 @@ def get_root_scope(self, context: DebugSequenceExecutionContext) -> Scope: # 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 + # 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: @@ -1458,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. From d17201e2f830db2916f28773c729cd70680aff85 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:54:14 +0200 Subject: [PATCH 05/13] ctrace-run: add configuration and raw capture support --- pyocd/core/session.py | 16 ++ pyocd/coresight/coresight_target.py | 8 +- pyocd/trace/ctrace_run.py | 418 ++++++++++++++++++++++++++++ 3 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 pyocd/trace/ctrace_run.py diff --git a/pyocd/core/session.py b/pyocd/core/session.py index b8f145029..17dd26f7c 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -36,6 +36,7 @@ from .target import Target from ..utility.notification import Notifier from ..target.pack.cbuild_run import CbuildRun +from ..trace.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 cbuild_run_config.get('enable_swv'): + 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() @@ -489,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: diff --git a/pyocd/coresight/coresight_target.py b/pyocd/coresight/coresight_target.py index 50fb1a8e1..fb9e550e0 100644 --- a/pyocd/coresight/coresight_target.py +++ b/pyocd/coresight/coresight_target.py @@ -32,12 +32,12 @@ 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. @@ -396,11 +396,16 @@ def _run_trace_sequence(self, name: str) -> None: delegate.run_sequence(name, pname=seq.pname) def trace_capture(self) -> None: + 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: result = self.call_delegate('trace_flush', target=self, mode=0) @@ -408,3 +413,4 @@ def trace_flush(self) -> None: 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) diff --git a/pyocd/trace/ctrace_run.py b/pyocd/trace/ctrace_run.py new file mode 100644 index 000000000..d3a659531 --- /dev/null +++ b/pyocd/trace/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 From bbec72011d6b49d9151f26ad04ad8086e52acc89 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 11:03:23 +0200 Subject: [PATCH 06/13] gdbserver: commands: add ctrace reload --- docs/command_reference.md | 22 ++++++++++++++++++-- pyocd/gdbserver/gdbserver_commands.py | 29 ++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) 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. +Trace + + +ctrace + +reload + +Reload and immediately apply the ctrace-run configuration. + + Utility @@ -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/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'], From eaba48b2ffb64266dab1f08dd63fccabde7e80c6 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 08:24:11 +0200 Subject: [PATCH 07/13] swo: fix merging of zero-valued data trace fields --- pyocd/trace/swo.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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) From 9c5970c8e53fdeb6b805a7d108c20cdf2acd43c2 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 08:14:59 +0200 Subject: [PATCH 08/13] trace: add raw trace data sinks --- pyocd/trace/sink.py | 94 +++++++++++++++++++++++++++++++++++++++++++-- pyocd/trace/swv.py | 80 +++++++++++++++++++++----------------- 2 files changed, 135 insertions(+), 39 deletions(-) diff --git a/pyocd/trace/sink.py b/pyocd/trace/sink.py index 31991d60b..fdec647f6 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,100 @@ # limitations under the License. import collections.abc -from typing import (TYPE_CHECKING, Iterable, List, Optional, Sequence, Union) +from pathlib import Path +from typing import (BinaryIO, TYPE_CHECKING, Iterable, List, Optional, Sequence, Union) + +from ..utility.server import StreamServer if TYPE_CHECKING: from .events import TraceEvent + +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 TraceEventSink: """@brief Abstract interface for a trace event sink.""" def receive(self, event: "TraceEvent") -> None: @@ -93,4 +182,3 @@ def receive(self, event: "TraceEvent") -> None: """ for sink in self._sinks: sink.receive(event) - diff --git a/pyocd/trace/swv.py b/pyocd/trace/swv.py index 8eba65755..c405dce71 100644 --- a/pyocd/trace/swv.py +++ b/pyocd/trace/swv.py @@ -20,16 +20,15 @@ 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: @@ -38,6 +37,7 @@ LOG = logging.getLogger(__name__) + class SWVEventSink(TraceEventSink): """@brief Trace event sink that converts ITM packets to a text stream.""" @@ -96,6 +96,7 @@ 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 @@ -165,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() @@ -181,13 +195,16 @@ 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() if self._trace_setup == TraceSetup.LEGACY: # init() should never have started the SWV thread unless the target has ITM and TPIU. @@ -204,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() @@ -232,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(): @@ -258,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) From c46734268f36b8aa1b5807a9620bea991b1ce88c Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 09:04:48 +0200 Subject: [PATCH 09/13] trace: enable capture/flush debug sequences --- pyocd/core/session.py | 7 +++---- pyocd/core/soc_target.py | 6 ++++++ pyocd/coresight/coresight_target.py | 5 +++++ pyocd/debug/sequences/delegates.py | 5 +++++ pyocd/gdbserver/gdbserver.py | 8 ++++---- pyocd/subcommands/run_cmd.py | 6 ++++++ 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pyocd/core/session.py b/pyocd/core/session.py index 17dd26f7c..19bf8f1ea 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -221,7 +221,7 @@ 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 cbuild_run_config.get('enable_swv'): + 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: @@ -631,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) @@ -688,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 fb9e550e0..cdfd284c5 100644 --- a/pyocd/coresight/coresight_target.py +++ b/pyocd/coresight/coresight_target.py @@ -376,6 +376,11 @@ 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: diff --git a/pyocd/debug/sequences/delegates.py b/pyocd/debug/sequences/delegates.py index 110ae6954..0aefe6d1f 100644 --- a/pyocd/debug/sequences/delegates.py +++ b/pyocd/debug/sequences/delegates.py @@ -48,6 +48,11 @@ 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 cmsis_pack_device(self) -> CmsisPackDevice: """@brief Accessor for the pack device that contains the sequences.""" 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/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() From 5d866f17aa72e7adb2179496a743157a35658f01 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 15 Sep 2026 10:44:14 +0200 Subject: [PATCH 10/13] trace: add trace buffer sinks --- pyocd/trace/sink.py | 67 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/pyocd/trace/sink.py b/pyocd/trace/sink.py index fdec647f6..fecbcfb4b 100644 --- a/pyocd/trace/sink.py +++ b/pyocd/trace/sink.py @@ -16,14 +16,17 @@ # limitations under the License. import collections.abc +import logging from pathlib import Path -from typing import (BinaryIO, TYPE_CHECKING, Iterable, List, Optional, Sequence, Union) +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.""" @@ -110,6 +113,68 @@ 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: From de946bb54a1e679a73e8af37906d1e09e66eb1e5 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:51:22 +0200 Subject: [PATCH 11/13] coresight: enable trace buffer functionality --- pyocd/coresight/coresight_target.py | 46 +++++++++++++++++++++++++---- pyocd/debug/sequences/delegates.py | 7 ++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pyocd/coresight/coresight_target.py b/pyocd/coresight/coresight_target.py index cdfd284c5..238d6f879 100644 --- a/pyocd/coresight/coresight_target.py +++ b/pyocd/coresight/coresight_target.py @@ -27,6 +27,7 @@ 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 @@ -56,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 @@ -382,14 +384,28 @@ def trace_enabled(self) -> bool: 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: - self._run_trace_sequence('TraceStart') + 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: - self._run_trace_sequence('TraceStop') + 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 @@ -419,3 +435,21 @@ def trace_flush(self) -> None: 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 0aefe6d1f..75ff44e4a 100644 --- a/pyocd/debug/sequences/delegates.py +++ b/pyocd/debug/sequences/delegates.py @@ -19,7 +19,7 @@ import logging from enum import Enum -from typing import (TYPE_CHECKING, Optional, Set) +from typing import (Any, Mapping, TYPE_CHECKING, Optional, Set) from .scope import Scope @@ -53,6 +53,11 @@ 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.""" From 60678959e12ceee743f8d3c832d4940aab109b33 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:51:22 +0200 Subject: [PATCH 12/13] sequences: stream trace buffer data - add Trace Buffer mode support in BufferStreamOut - add TraceBufferSelected function --- pyocd/debug/sequences/functions.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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. From 82c93f677dd2b74321b51093f2b52fa5b073c187 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Mon, 14 Sep 2026 13:51:39 +0200 Subject: [PATCH 13/13] csolution: move run configuration support --- pyocd/core/session.py | 4 ++-- pyocd/target/csolution/__init__.py | 15 +++++++++++++++ pyocd/target/{pack => csolution}/cbuild_run.py | 2 +- pyocd/{trace => target/csolution}/ctrace_run.py | 10 +++++----- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 pyocd/target/csolution/__init__.py rename pyocd/target/{pack => csolution}/cbuild_run.py (99%) rename pyocd/{trace => target/csolution}/ctrace_run.py (98%) diff --git a/pyocd/core/session.py b/pyocd/core/session.py index 19bf8f1ea..4aef39877 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -35,8 +35,8 @@ from .options_manager import OptionsManager from .target import Target from ..utility.notification import Notifier -from ..target.pack.cbuild_run import CbuildRun -from ..trace.ctrace_run import CTraceRun +from ..target.csolution.cbuild_run import CbuildRun +from ..target.csolution.ctrace_run import CTraceRun if TYPE_CHECKING: from types import TracebackType 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 99% rename from pyocd/target/pack/cbuild_run.py rename to pyocd/target/csolution/cbuild_run.py index 4ee253e9c..d4a6ccc23 100644 --- a/pyocd/target/pack/cbuild_run.py +++ b/pyocd/target/csolution/cbuild_run.py @@ -26,7 +26,7 @@ from dataclasses import dataclass 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 diff --git a/pyocd/trace/ctrace_run.py b/pyocd/target/csolution/ctrace_run.py similarity index 98% rename from pyocd/trace/ctrace_run.py rename to pyocd/target/csolution/ctrace_run.py index d3a659531..9b42d4d70 100644 --- a/pyocd/trace/ctrace_run.py +++ b/pyocd/target/csolution/ctrace_run.py @@ -26,13 +26,13 @@ import yaml -from ..core import exceptions +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 + 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__)