diff --git a/docs/target_support.md b/docs/target_support.md index 9f8841c02..ce34b5941 100644 --- a/docs/target_support.md +++ b/docs/target_support.md @@ -211,3 +211,19 @@ _Note:_ PyOCD can work with expanded packs just like zipped .pack files. Pass th of the pack using the `--pack` argument, as above. This is very useful for cases such as development or debugging of a pack, or for working with other CMSIS-Pack managers that store packs in decompressed form. +#### CMSIS-Toolbox Run and Debug Management + +Run and Debug Management is a part of the [CMSIS-Toolbox](https://github.com/Open-CMSIS-Pack/cmsis-toolbox) build +information files when working with CSolution projects. Each `*.cbuild-run.yml` file contains relevant run and +debug information for a single `target-type` defined in a CSolution project. The tool gathers and filters the +target information from the BSP and DFP. + +To use the `cbuild-run` target information in PyOCD, pass the `--cbuild-run` option followed by the path to +desired `*.cbuild-run.yml` file. The target information is not cached in PyOCD, which means the command-line +option should always be passed when launching the debugger. + +Because a single `target-type` is described in the YAML file, the `--target` +command-line argument is not needed, as the target is automatically selected. + +More information on CMSIS-Toolbox and the CSolution project format for Run and Debug Management can be found in the +[CMSIS-Toolbox Documentation](https://open-cmsis-pack.github.io/cmsis-toolbox/YML-CBuild-Format/#run-and-debug-management). diff --git a/pyocd/board/board.py b/pyocd/board/board.py index 20c29b843..d28389e89 100644 --- a/pyocd/board/board.py +++ b/pyocd/board/board.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2006-2013,2018 Arm Limited +# Copyright (c) 2006-2013,2018,2025 Arm Limited # Copyright (c) 2021-2022 Chris Reed # Copyright (c) 2023 Benjamin Sølberg # SPDX-License-Identifier: Apache-2.0 @@ -22,6 +22,7 @@ from ..core import exceptions from ..target import (TARGET, normalise_target_type_name) from ..target.pack import pack_target +from ..target.pack.cbuild_run import CbuildRun from ..utility.graph import GraphNode if TYPE_CHECKING: @@ -63,10 +64,18 @@ def __init__(self, """ super().__init__() + # Create cbuild_run if option is provided + if session.options.is_set('cbuild_run'): + cbuild_run = CbuildRun(session.options.get('cbuild_run')) + else: + cbuild_run = None + # Use the session option if no target type was given to us. if target is None: if session.options.is_set('target_override'): target = session.options.get('target_override') + elif cbuild_run and cbuild_run.target: + target = cbuild_run.target elif board_info: target = board_info.target @@ -99,11 +108,15 @@ def __init__(self, self._delegate = None self._inited = False + # Create target from cbuild-run information. + if cbuild_run and cbuild_run.target: + cbuild_run.populate_target(target) + # Create targets from provided CMSIS pack. if session.options['pack'] is not None: pack_target.PackTargets.populate_targets_from_pack(session.options['pack']) - # Create targets from the cmsis-pack-manager cache. + # Create target from the cmsis-pack-manager cache. if self._target_type not in TARGET: pack_target.ManagedPacks.populate_target(target) diff --git a/pyocd/coresight/ap.py b/pyocd/coresight/ap.py index b685e6c7a..63d2ba54e 100644 --- a/pyocd/coresight/ap.py +++ b/pyocd/coresight/ap.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2015-2020 Arm Limited +# Copyright (c) 2015-2020,2025 Arm Limited # Copyright (c) 2021-2023 Chris Reed # SPDX-License-Identifier: Apache-2.0 # @@ -206,10 +206,11 @@ class APAddressBase: address format. """ - def __init__(self, address: int, dp: int = 0) -> None: + def __init__(self, address: int, dp: int = 0, apid: int = 0) -> None: """@brief Constructor accepting the nominal address.""" self._nominal_address = address self._dp = dp + self._apid = apid @property def ap_version(self) -> APVersion: @@ -241,6 +242,11 @@ def dp_index(self) -> int: """@brief Index of the DP to which this AP is attached.""" return self._dp + @property + def apid(self) -> int: + """@brief Unique AP identifier.""" + return self._apid + def __hash__(self) -> int: return hash(self.nominal_address | (self._dp << 64)) @@ -788,6 +794,10 @@ def _init_hprot() -> None: LOG.debug("%s implemented HPROT=%x HNONSEC=%x", self.short_description, self._impl_hprot, self._impl_hnonsec) + if (self._flags & AP_SPROT): + if (original_csw & CSW_SDEVICEEN) == 0: + self._hnonsec = NONSECURE + # Update current HPROT and HNONSEC, and the current base CSW value. self.hprot = self._hprot & self._impl_hprot self.hnonsec = self._hnonsec & self._impl_hnonsec @@ -1355,6 +1365,7 @@ def find_components(self) -> None: AP_ALL_TX_SZ = 0x2 # The AP is known to support 8-, 16-, and 32-bit transfers, *unless* Large Data is implemented. AP_MSTRTYPE = 0x4 # The AP is known to support the MSTRTYPE field. AP_DBGSWEN = 0x8 # The AP is known to support the DBGSWEN flag. +AP_SPROT = 0x10 # The AP is known to support the SPROT field. ## Map from AP IDR fields to AccessPort subclass. # @@ -1380,20 +1391,20 @@ def find_components(self) -> None: # |JEP106 |Class |Var|Type |Name |Class (AP_JEP106_ARM, AP_CLASS_JTAG_AP, 0, 0): ("JTAG-AP", AccessPort, 0 ), (AP_JEP106_ARM, AP_CLASS_COM_AP, 0, 0): ("SDC-600", AccessPort, 0 ), - (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ ), + (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ|AP_SPROT ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 1, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ|AP_4K_WRAP|AP_MSTRTYPE ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 2, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 3, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 4, AP_TYPE_AHB): ("AHB-AP", AHB_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_APB): ("APB-AP", MEM_AP, AP_DBGSWEN ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AXI): ("AXI-AP", MEM_AP, AP_ALL_TX_SZ ), - (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB5): ("AHB5-AP", AHB_AP, AP_ALL_TX_SZ ), + (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB5): ("AHB5-AP", AHB_AP, AP_ALL_TX_SZ|AP_SPROT ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 1, AP_TYPE_AHB5): ("AHB5-AP", AHB_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 2, AP_TYPE_AHB5): ("AHB5-AP", AHB_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_APB4): ("APB4-AP", MEM_AP, 0 ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AXI5): ("AXI5-AP", MEM_AP, AP_ALL_TX_SZ ), (AP_JEP106_ARM, AP_CLASS_MEM_AP, 1, AP_TYPE_AXI5): ("AXI5-AP", MEM_AP, AP_ALL_TX_SZ ), - (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB5_HPROT): ("AHB5-AP", MEM_AP, AP_ALL_TX_SZ ), + (AP_JEP106_ARM, AP_CLASS_MEM_AP, 0, AP_TYPE_AHB5_HPROT): ("AHB5-AP", MEM_AP, AP_ALL_TX_SZ|AP_SPROT ), (AP_JEP106_ARM_CHINA, AP_CLASS_MEM_AP, 1, AP_TYPE_AHB5): ("AHB5-AP", AHB_AP, AP_ALL_TX_SZ ), } diff --git a/pyocd/coresight/discovery.py b/pyocd/coresight/discovery.py index becb2533c..7f431f91f 100644 --- a/pyocd/coresight/discovery.py +++ b/pyocd/coresight/discovery.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2019-2020 Arm Limited +# Copyright (c) 2019-2020,2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -258,8 +258,9 @@ def _create_root_component(self, cmpid): """ try: # Create a memory interface for this component. - ap_address = APv2Address(cmpid.address) - memif = APAccessMemoryInterface(self.dp, ap_address) + # ap_address = APv2Address(cmpid.address) + # memif = APAccessMemoryInterface(self.dp, ap_address) + memif = APAccessMemoryInterface(self.dp) # Instantiate the component and attach to the target. component = cmpid.factory(memif, cmpid, cmpid.address) diff --git a/pyocd/debug/sequences/sequences.py b/pyocd/debug/sequences/sequences.py index be3c2520e..f2b627794 100644 --- a/pyocd/debug/sequences/sequences.py +++ b/pyocd/debug/sequences/sequences.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2020 Arm Limited +# Copyright (c) 2020,2025 Arm Limited # Copyright (c) 2021-2023 Chris Reed # SPDX-License-Identifier: Apache-2.0 # @@ -380,7 +380,7 @@ def _create_scope(self, context: DebugSequenceExecutionContext) -> Scope: scope.set('__ap', default_ap_address.nominal_address if isinstance(default_ap_address, APv1Address) else 0) - scope.set('__apid', default_ap_address.nominal_address + scope.set('__apid', default_ap_address.apid if isinstance(default_ap_address, APv2Address) else 0) diff --git a/pyocd/gdbserver/gdbserver.py b/pyocd/gdbserver/gdbserver.py index 36af8f406..7f5be80af 100644 --- a/pyocd/gdbserver/gdbserver.py +++ b/pyocd/gdbserver/gdbserver.py @@ -108,7 +108,7 @@ class GDBServer(threading.Thread): ## Timer delay for sending the notification that the server is listening. START_LISTENING_NOTIFY_DELAY = 0.03 # 30 ms - def __init__(self, session, core=None): + def __init__(self, session, core=None, port=None): super().__init__() self.session = session self.board = session.board @@ -120,9 +120,13 @@ def __init__(self, session, core=None): self.target = self.board.target.cores[core] self.name = "gdb-server-core%d" % self.core - self.port = session.options.get('gdbserver_port') - if self.port != 0: - self.port += self.core + if port is None: + self.port = session.options.get('gdbserver_port') + if self.port != 0: + self.port += self.core + else: + self.port = port + self.telnet_port = session.options.get('telnet_port') if self.telnet_port != 0: self.telnet_port += self.core @@ -328,7 +332,7 @@ def run(self): while not self.shutdown_event.is_set(): connected = self.abstract_socket.connect() - if connected != None: + if connected is not None: self.packet_io = GDBServerPacketIOThread(self.abstract_socket) break diff --git a/pyocd/subcommands/base.py b/pyocd/subcommands/base.py index 4a65cdcd6..03ab43d15 100644 --- a/pyocd/subcommands/base.py +++ b/pyocd/subcommands/base.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021-2023 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -68,6 +69,8 @@ class CommonOptions: help="(Deprecated) Send setting to DAPAccess layer.") CONFIG_GROUP.add_argument("--pack", metavar="PATH", action="append", help="Path to the .pack file for a CMSIS Device Family Pack.") + CONFIG_GROUP.add_argument("--cbuild-run", metavar="PATH", + help="Path to the .cbuild-run.yml file for CSolution Run and Debug Management.") # Define common options for all subcommands, including logging options. COMMON = argparse.ArgumentParser(description='common', diff --git a/pyocd/subcommands/erase_cmd.py b/pyocd/subcommands/erase_cmd.py index 602d3fff0..67c242aab 100644 --- a/pyocd/subcommands/erase_cmd.py +++ b/pyocd/subcommands/erase_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -80,6 +81,7 @@ def invoke(self) -> int: user_script=self._args.script, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, unique_id=self._args.unique_id, target_override=self._args.target_override, frequency=self._args.frequency, @@ -99,5 +101,3 @@ def invoke(self) -> int: eraser.erase(addresses) return 0 - - diff --git a/pyocd/subcommands/gdbserver_cmd.py b/pyocd/subcommands/gdbserver_cmd.py index f976e7ee2..e03a9c47c 100644 --- a/pyocd/subcommands/gdbserver_cmd.py +++ b/pyocd/subcommands/gdbserver_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -91,6 +92,8 @@ def get_args(cls) -> List[argparse.ArgumentParser]: help="Run command (OpenOCD compatibility).") gdbserver_options.add_argument("-bh", "--soft-bkpt-as-hard", dest="soft_bkpt_as_hard", default=False, action="store_true", help="Replace software breakpoints with hardware breakpoints.") + gdbserver_options.add_argument("--reset-run", action="store_true", + help="Reset and run before running GDB server") return [cls.CommonOptions.COMMON, cls.CommonOptions.CONNECT, gdbserver_parser] @@ -182,6 +185,7 @@ def invoke(self) -> int: config_file=self._args.config, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, unique_id=self._args.unique_id, target_override=self._args.target_override, frequency=self._args.frequency, @@ -215,6 +219,10 @@ def invoke(self) -> int: session.probeserver = probe_server probe_server.start() + # Reset and run the target + if self._args.reset_run: + session.board.target.reset() + # Start up the gdbservers. for core_number, core in session.board.target.cores.items(): # Don't create a server for CPU-less memory Access Port. @@ -223,7 +231,11 @@ def invoke(self) -> int: # Don't create a server if this core is not listed by the user. if core_number not in core_list: continue - gdb = GDBServer(session, core=core_number) + # Read pname and port mapping from cbuild-run. + port_number = None + if self._args.cbuild_run: + port_number = session.target.get_gdbserver_port(core.node_name) + gdb = GDBServer(session, core=core_number, port=port_number) # Only subscribe to the server for the first core, so echo messages aren't printed # multiple times. if not gdbs: diff --git a/pyocd/subcommands/json_cmd.py b/pyocd/subcommands/json_cmd.py index e71eb38d1..f48db51c6 100644 --- a/pyocd/subcommands/json_cmd.py +++ b/pyocd/subcommands/json_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +24,7 @@ from .base import SubcommandBase from ..core.session import Session from ..tools.lists import ListGenerator -from ..target.pack import pack_target +from ..target.pack import pack_target, cbuild_run from ..utility.cmdline import convert_session_options from .. import __version__ @@ -92,6 +93,7 @@ def invoke(self) -> int: config_file=self._args.config, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, **convert_session_options(self._args.options) ) @@ -99,6 +101,9 @@ def invoke(self) -> int: # Create targets from provided CMSIS pack. if session.options['pack'] is not None: pack_target.PackTargets.populate_targets_from_pack(session.options['pack']) + # Create target from provided CbuildRun file. + if session.options['cbuild_run'] is not None: + cbuild_run.CbuildRun(session.options['cbuild_run']).populate_target() if self._args.probes: obj = ListGenerator.list_probes() @@ -122,4 +127,3 @@ def invoke(self) -> int: print(json.dumps(obj, indent=4)) return exit_status - diff --git a/pyocd/subcommands/list_cmd.py b/pyocd/subcommands/list_cmd.py index b7bdc7e50..ab36df72d 100644 --- a/pyocd/subcommands/list_cmd.py +++ b/pyocd/subcommands/list_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +23,7 @@ from ..core.helpers import ConnectHelper from ..core.session import Session from ..tools.lists import ListGenerator -from ..target.pack import pack_target +from ..target.pack import pack_target, cbuild_run from ..utility.cmdline import convert_session_options LOG = logging.getLogger(__name__) @@ -60,7 +61,7 @@ def get_args(cls) -> List[argparse.ArgumentParser]: help="Restrict listing to items matching the given name substring. Applies to targets and boards.") list_options.add_argument('-r', '--vendor', help="Restrict listing to items whose vendor matches the given name substring. Applies only to targets.") - list_options.add_argument('-s', '--source', choices=('builtin', 'pack'), + list_options.add_argument('-s', '--source', choices=('builtin', 'pack', 'cbuild-run'), help="Restrict listing to targets from the specified source. Applies to targets.") list_options.add_argument('-H', '--no-header', action='store_true', help="Don't print a table header.") @@ -87,6 +88,7 @@ def invoke(self) -> int: config_file=self._args.config, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, **convert_session_options(self._args.options) ) @@ -96,6 +98,9 @@ def invoke(self) -> int: # Create targets from provided CMSIS pack. if session.options['pack'] is not None: pack_target.PackTargets.populate_targets_from_pack(session.options['pack']) + # Create target from provided CbuildRun file. + if session.options['cbuild_run'] is not None: + cbuild_run.CbuildRun(session.options['cbuild_run']).populate_target() obj = ListGenerator.list_targets(name_filter=self._args.name, vendor_filter=self._args.vendor, @@ -135,4 +140,3 @@ def invoke(self) -> int: print(pt) return 0 - diff --git a/pyocd/subcommands/load_cmd.py b/pyocd/subcommands/load_cmd.py index 29f7b2ef2..63e485be6 100644 --- a/pyocd/subcommands/load_cmd.py +++ b/pyocd/subcommands/load_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021-2022 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,9 +66,9 @@ def get_args(cls) -> List[argparse.ArgumentParser]: parser_options.add_argument("--no-reset", action="store_true", help="Specify to prevent resetting device after programming has finished.") - parser.add_argument("file", metavar="", nargs="+", + parser.add_argument("file", metavar="", nargs="*", help="File to write to memory. Binary files can have an optional base address appended to the file " - "name as '@
', for instance 'app.bin@0x20000'.") + "name as '@
', for instance 'app.bin@0x20000'. Optional if '--cbuild-run' is used.") return [cls.CommonOptions.COMMON, cls.CommonOptions.CONNECT, parser] @@ -76,6 +77,9 @@ def invoke(self) -> int: self._increase_logging(["pyocd.flash.loader", __name__]) # Validate arguments. + if (self._args.cbuild_run is None) and not self._args.file: + raise ValueError("Positional argument is required when '--cbuild-run' is not used.") + if (self._args.base_address is not None) and (len(self._args.file) > 1): raise ValueError("--base-address cannot be set when loading more than one file; " "use a base address suffix instead") @@ -86,6 +90,7 @@ def invoke(self) -> int: user_script=self._args.script, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, unique_id=self._args.unique_id, target_override=self._args.target_override, frequency=self._args.frequency, @@ -102,6 +107,10 @@ def invoke(self) -> int: chip_erase=self._args.erase, trust_crc=self._args.trust_crc, no_reset=self._args.no_reset) + if not self._args.file and self._args.cbuild_run: + # Populate file list from cbuild-run output if not provided explicitly + cbuild_files = session.target.get_output() + self._args.file = cbuild_files.keys() for filename in self._args.file: # Get an initial path with the argument as-is. file_path = Path(filename).expanduser() @@ -117,6 +126,8 @@ def invoke(self) -> int: return 1 else: base_address = self._args.base_address + if base_address is None and self._args.cbuild_run: + base_address = cbuild_files[filename] # Resolve our path. file_path = Path(filename).expanduser().resolve() @@ -133,5 +144,3 @@ def invoke(self) -> int: file_format=self._args.format) return 0 - - diff --git a/pyocd/subcommands/reset_cmd.py b/pyocd/subcommands/reset_cmd.py index d14075c29..3e95a64cc 100644 --- a/pyocd/subcommands/reset_cmd.py +++ b/pyocd/subcommands/reset_cmd.py @@ -1,5 +1,6 @@ # pyOCD debugger # Copyright (c) 2021-2022 Chris Reed +# Copyright (c) 2025 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,7 +53,7 @@ def get_args(cls) -> List[argparse.ArgumentParser]: "from the core chosen with --core will be used (usually 'sw' but can be differ based " "on the target type).") reset_options.add_argument("-c", "--core", default=0, type=int_base_0, - help="Core number used to perform software reset. Only applies to software reset methods." + help="Core number used to perform software reset. Only applies to software reset methods. " "Default is core 0.") reset_options.add_argument("-l", "--halt", action="store_true", help="Halt the core on the first instruction after reset. Defaults to disabled.") @@ -77,6 +78,7 @@ def invoke(self) -> None: user_script=self._args.script, no_config=self._args.no_config, pack=self._args.pack, + cbuild_run=self._args.cbuild_run, unique_id=self._args.unique_id, target_override=self._args.target_override, frequency=self._args.frequency, @@ -130,4 +132,3 @@ def invoke(self) -> None: LOG.info("Done.") finally: session.close() - diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/pack/cbuild_run.py new file mode 100644 index 000000000..bf76cdbce --- /dev/null +++ b/pyocd/target/pack/cbuild_run.py @@ -0,0 +1,1030 @@ +# pyOCD debugger +# Copyright (c) 2025 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. + +import logging +import yaml +import os +import io +import platform + +from pathlib import Path +from copy import deepcopy +from dataclasses import dataclass +from typing import (cast, Optional, Set, Dict, List, Tuple, IO, Any) +from .flash_algo import PackFlashAlgo +from .reset_sequence_maps import (RESET_SEQUENCE_TO_TYPE_MAP, RESET_TYPE_TO_SEQUENCE_MAP) +from .. import (normalise_target_type_name, TARGET) +from ...coresight.cortex_m import CortexM +from ...coresight.coresight_target import CoreSightTarget +from ...coresight.ap import (APAddressBase, APv1Address, APv2Address) +from ...core import exceptions +from ...core.target import Target +from ...core.session import Session +from ...core.core_target import CoreTarget +from ...core.memory_map import (MemoryMap, MemoryType, MEMORY_TYPE_CLASS_MAP) +from ...utility.sequencer import CallSequence +from ...probe.debug_probe import DebugProbe +from ...debug.svd.loader import SVDFile +from ...debug.sequences.scope import Scope +from ...debug.sequences.delegates import DebugSequenceDelegate +from ...debug.sequences.functions import DebugSequenceCommonFunctions +from ...debug.sequences.sequences import ( + Block, + DebugSequence, + DebugSequenceNode, + IfControl, + WhileControl, + DebugSequenceExecutionContext +) + +LOG = logging.getLogger(__name__) + +class CbuildRunError(exceptions.Error): + """Custom exception for errors encountered during processing of .cbuild-run.yml""" + pass + +@dataclass +class ProcessorInfo: + """@brief Descriptor for a processor defined in a DFP.""" + ## The Pname attribute, or Dcore if not Pname was provided. + name: str = "Unknown" + ## PE unit number within an MPCore. For single cores this will be 0. + unit: int = 0 + ## Total number of cores in an MPCore. + total_units: int = 1 + ## Address of AP through which the PE can be accessed. + ap_address: APAddressBase = APv1Address(-1) + ## Base address of the PE's memory mapped debug registers. Not used and 0 for M-profile. + address: int = 0 + ## SVD file path relative to the pack. + svd_path: Optional[str] = None + ## Default reset sequence name. + default_reset_sequence: str = "ResetSystem" + + +class CbuildRunTargetMethods: + """@brief Namespace of static methods to dynamically configure CoreSight targets. + + These methods are used to generate and initialize runtime targets from a .cbuild-run.yml file, + including memory mapping, core reset configuration, and processor name updates. + """ + @staticmethod + def _cbuild_target_init(self, session: Session) -> None: + """@brief Initializes a target dynamically based on a parsed .cbuild-run.yml description. + + Sets memory maps, SVD files, and debug sequence delegates. + """ + super(self.__class__, self).__init__(session, self._cbuild_device.memory_map) + self.vendor = self._cbuild_device.vendor + self.part_number = self._cbuild_device.target + self._svd_location = SVDFile(filename=self._cbuild_device.svd) + self.debug_sequence_delegate = CbuildRunDebugSequenceDelegate(self, self._cbuild_device) + + @staticmethod + def _cbuild_target_create_init_sequence(self) -> CallSequence: + """@brief Creates an initialization call sequence for runtime-configured targets. + + Extends the standard discovery sequence to configure processor names + and reset behavior after core discovery. + """ + seq = super(self.__class__, self).create_init_sequence() + seq.wrap_task('discovery', + lambda seq: seq.insert_after('create_cores', + ('update_processor_name', self.update_processor_name), + ('update_primary_core', self.update_primary_core), + ('configure_core_reset', self.configure_core_reset) + ) + ) + return seq + + @staticmethod + def _cbuild_target_update_processor_name(self) -> None: + """@brief Updates processor names post-discovery based on Access Port (AP) addresses. + + Maps discovered cores to known processors to ensure consistent naming. + """ + processors_map = {} + for core in self.cores.values(): + if core.node_name is None: + core.node_name = core.name + + for proc in self._cbuild_device.processors_map.values(): + if ('Unknown' in proc.name) and (proc.ap_address == core.ap.address): + proc.name = core.name + processors_map[core.name] = proc + break + + if processors_map: + self._cbuild_device.processors_map = processors_map + + @staticmethod + def _cbuild_target_start_processor(self) -> None: + """@brief Updates the primary processor, based on 'start-pname' node in .cbuild-run.yml""" + start_pname = self._cbuild_device.debugger.get('start-pname') + if start_pname is not None and self.primary_core_pname != start_pname: + core_number = next((core.core_number for core in self.cores.values() if core.node_name == start_pname), None) + if core_number is not None: + self.session.options['primary_core'] = core_number + self.selected_core = core_number + + @staticmethod + def _cbuild_target_configure_core_reset(self) -> None: + """@brief Configures supported and default reset types for each core. + + Based on the debug sequences defined in the .cbuild-run.yml, updates each core's + reset capabilities and selects fallback mechanisms if needed. + """ + for core_num, core in self.cores.items(): + core_ap_addr = core.ap.address + try: + proc_info = self._cbuild_device.processors_ap_map[core_ap_addr] + except KeyError: + LOG.debug("core #%d not specified", core_num) + continue + + # Get this processor's list of sequences. + sequences = self.debug_sequence_delegate.sequences_for_pname(proc_info.name) + + def is_reset_sequence_enabled(name: str) -> bool: + return (name not in sequences) or sequences[name].is_enabled + + # Set the supported reset types by filtering existing supported reset types. + updated_reset_types: Set[Target.ResetType] = set() + for resettype in core._supported_reset_types: + # These two types are not in the map, and should always be present. + if resettype in (Target.ResetType.SW, Target.ResetType.SW_EMULATED): + updated_reset_types.add(resettype) + continue + + resettype_sequence_name = RESET_TYPE_TO_SEQUENCE_MAP[resettype] + if is_reset_sequence_enabled(resettype_sequence_name): + updated_reset_types.add(resettype) + + # Special case to enable processor reset even when the core doesn't support VECTRESET, if + # there is a non-default ResetProcessor sequence definition. + if ((Target.ResetType.SW_CORE not in updated_reset_types) # type:ignore + and ('ResetProcessor' in sequences) + and sequences['ResetProcessor'].is_enabled): + updated_reset_types.add(Target.ResetType.SW_CORE) # type:ignore + + core._supported_reset_types = updated_reset_types + LOG.debug("updated DFP core #%d reset types: %s", core_num, core._supported_reset_types) + + default_reset_seq = proc_info.default_reset_sequence + + # Check that the default reset sequence is a standard sequence. The specification allows for + # custom reset sequences to be used, but that is not supported by pyocd yet. + # TODO support custom default reset sequences (requires a new reset type) + if default_reset_seq not in RESET_SEQUENCE_TO_TYPE_MAP: + if default_reset_seq in sequences: + # Custom reset sequence, not yet supported by pyocd. + LOG.warning("DFP device definition error: custom reset sequences are not yet supported " + "by pyocd; core #%d (%s) requested default reset sequence %s", + core_num, proc_info.name, default_reset_seq) + else: + # Invalid/unknown default reset sequence. + LOG.warning("DFP device definition error: specified default reset sequence %s " + "for core #%d (%s) does not exist", + default_reset_seq, core_num, proc_info.name) + + # Handle multicore debug mode causing secondary cores to default to processor reset. + did_force_core_reset = False + if (self.session.options.get('enable_multicore_debug') + and (core_num != self.session.options.get('primary_core'))): + if not is_reset_sequence_enabled('ResetProcessor'): + LOG.warning("Multicore debug mode cannot select processor reset for secondary core " + "#%d (%s) because it is disabled by the DFP; using emulated processor " + "reset instead", core_num, proc_info.name) + core.default_reset_type = Target.ResetType.SW_EMULATED + continue + else: + default_reset_seq = 'ResetProcessor' + did_force_core_reset = True + + # Verify that the specified default reset sequence hasn't been disabled. + if not is_reset_sequence_enabled(default_reset_seq): + # Only log a warning if we didn't decide to use core reset due to multicore mode. + if not did_force_core_reset: + LOG.warning("DFP device definition conflict: specified default reset sequence %s " + "for core #%d (%s) is disabled by the DFP", + default_reset_seq, core_num, proc_info.name) + + # Map from disabled default to primary and secondary fallbacks. + RESET_FALLBACKS: Dict[str, Tuple[str, str]] = { + 'ResetSystem': ('ResetProcessor', 'ResetHardware'), + 'ResetHardware': ('ResetSystem', 'ResetProcessor'), + 'ResetProcessor': ('ResetSystem', 'ResetHardware'), + } + + # Select another default. + fallbacks = RESET_FALLBACKS[default_reset_seq] + if is_reset_sequence_enabled(fallbacks[0]): + default_reset_seq = fallbacks[0] + elif is_reset_sequence_enabled(fallbacks[1]): + default_reset_seq = fallbacks[1] + else: + LOG.warning("DFP device definition conflict: all reset types are disabled for " + "core #%d (%s) by the DFP; using emulated core reset", + default_reset_seq, core_num) + core.default_reset_type = Target.ResetType.SW_EMULATED + continue + + LOG.info("Setting core #%d (%s) default reset sequence to %s", + core_num, proc_info.name, default_reset_seq) + core.default_reset_type = RESET_SEQUENCE_TO_TYPE_MAP[default_reset_seq] + + @staticmethod + def _cbuild_target_add_core(_self, core: CoreTarget) -> None: + """@brief Override to set node name of added core to its pname.""" + pname = _self._cbuild_device.processors_ap_map[cast(CortexM, core).ap.address].name + if 'Unknown' not in pname: + core.node_name = pname + CoreSightTarget.add_core(_self, core) + + @staticmethod + def _cbuild_target_get_gdbserver_port(self, pname: str) -> Optional[int]: + """@brief GDB Server port for processor name.""" + assert pname + server_map = self._cbuild_device.debugger.get('gdbserver', []) + if any('pname' in server for server in server_map): + port = next((i['port'] for i in server_map if i.get('pname') == pname), None) + else: + port = next((i['port'] for i in server_map), None) + return port + + @staticmethod + def _cbuild_target_get_output(self) -> Dict[str, Optional[int]]: + return self._cbuild_device.output + + +class CbuildRun: + """@brief Parser for the .cbuild-run.yml file (CSolution Run and Debug Management).""" + def __init__(self, yml_path: str) -> None: + """@brief Reads a .cbuild-run.yml file and validates its content.""" + self._data: Dict[str, Any] = {} + self._valid: bool = False + self._vars: Optional[Dict[str, str]] = None + self._sequences: Optional[List[dict]] = None + self._debug_topology: Optional[Dict[str, Any]] = None + self._memory_map: Optional[MemoryMap] = None + self._valid_dps: List[int] = [] + self._apids: Dict[int, APAddressBase] = {} + self._uses_apv2: bool = False + self._built_apid_map: bool = False + self._processors_map: Dict[str, ProcessorInfo] = {} + self._processors_ap_map: Dict[APAddressBase, ProcessorInfo] = {} + self._use_default_memory_map: bool = True + + try: + with open(yml_path, 'r') as yml_file: + yml_data = yaml.safe_load(yml_file) + if 'cbuild-run' in yml_data: + self._data = yml_data['cbuild-run'] + self._cmsis_pack_root() + self._valid = True + else: + raise CbuildRunError(f"Invalid .cbuild-run.yml file '{yml_path}'") + except IOError as err: + LOG.error("Error attempting to access .cbuild-run.yml file '%s': %s", yml_path, err) + + def _cmsis_pack_root(self) -> None: + """@brief Sets the CMSIS_PACK_ROOT environment variable if not already set. + + Platform dependant default values are defined in: + https://open-cmsis-pack.github.io/cmsis-toolbox/installation/#environment-variables + """ + # Check if the CMSIS_PACK_ROOT environment variable is already set. + # This variable specifies the root directory for CMSIS packs, which are essential for device support. + if 'CMSIS_PACK_ROOT' in os.environ: + return + + # Get the system platform + system = platform.system() + + # Set the CMSIS_PACK_ROOT environment variable based on the platform + if system == 'Windows': + # Windows detected, set the Windows default path + os.environ['CMSIS_PACK_ROOT'] = os.path.expandvars("${LOCALAPPDATA}\\Arm\\Packs") + elif system in {'Linux', 'Darwin'}: + # Note: WSL is treated as 'Linux' + # Linux or macOS detected, set the Linux/macOS default path + os.environ['CMSIS_PACK_ROOT'] = os.path.expandvars("${HOME}/.cache/arm/packs") + else: + raise CbuildRunError(f"Unsupported platform '{system}' for CMSIS_PACK_ROOT. " + "Please set the CMSIS_PACK_ROOT environment variable manually.") + + @property + def target(self) -> str: + """@brief Target identifier string. + + Read `device` field from .cbuild-run.yml file, without 'vendor'. + """ + if self._valid: + device = self._data.get('device', '') + return device.split('::')[1] if '::' in device else device + else: + return '' + + @property + def part_number(self) -> str: + return self.target + + @property + def vendor(self) -> str: + """@brief Vendor identifier string. + + Read 'vendor' part of `device` field from .cbuild-run.yml file. + """ + if self._valid and ('device' in self._data): + device = self._data.get('device', '') + return device.split('::')[0] if '::' in device else '' + else: + return '' + + @property + def families(self) -> List[str]: + """@brief List of target device families. + + Currently unsupported in cbuild-run. Returns an empty list. + """ + return [] + + @property + def memory_map(self) -> MemoryMap: + """@brief Returns the parsed memory map for the device. + + Memory regions are constructed by merging default maps with user-defined regions, + and flash algorithms are applied where appropriate. + """ + if self._memory_map is None: + self._build_memory_map() + return self._memory_map + + @property + def svd(self) -> Optional[IO[bytes]]: + """@brief File-like object for the device's SVD file.""" + #TODO handle multicore devices + try: + for desc in self.system_descriptions: + if desc['type'] == 'svd': + norm_path = os.path.normpath(desc['file']) + svd_path = Path(os.path.expandvars(norm_path)) + return io.BytesIO(svd_path.read_bytes()) + except (KeyError, IndexError): + LOG.error("Could not locate SVD in cbuild-run system-descriptions.") + return None + + @property + def output(self) -> Dict[str, Optional[int]]: + """@brief Set of loadable output files (file, offset).""" + if not self._valid: + return {} + + # Supported loadable files + f_type = {'elf', 'hex', 'bin'} + + # Filter only loadable supported files from the output node + loadable_files = [f for f in self._data.get('output', []) + if 'image' in f.get('load', '') and f.get('type') in f_type] + + load_files = {} + for f in loadable_files: + # Get load offset (None if not specified) + offset = f.get('load-offset') + # Add filename and it's offset to return value + load_files[f['file']] = offset + + return load_files + + @property + def debug_sequences(self) -> List[dict]: + """@brief Debug sequences node.""" + if self._valid and ('debug-sequences' in self._data): + if self._sequences is None: + self._sequences = self._data.get('debug-sequences', []) + return self._sequences + return [] + + @property + def debug_vars(self) -> Dict[str, str]: + """@brief Debug variables.""" + if self._valid and ('debug-vars' in self._data): + if self._vars is None: + self._vars = self._data.get('debug-vars', {}) + return self._vars + return {} + + @property + def valid_dps(self) -> List[int]: + """@brief List of valid debug ports.""" + if not self._valid_dps: + self._build_aps_map() + return self._valid_dps + + @property + def uses_apid(self) -> bool: + """@brief Accessport V2 apid is used.""" + if not self._built_apid_map: + self._build_aps_map() + return self._uses_apv2 + + @property + def apid_map(self) -> Dict[int, APAddressBase]: + """@brief Map of apid and AP address objects.""" + if not self._built_apid_map: + self._build_aps_map() + return self._apids + + @property + def processors_map(self) -> Dict[str, ProcessorInfo]: + """@brief Map of processor names and processor info objects.""" + if not self._processors_map: + self._build_aps_map() + return self._processors_map + + @processors_map.setter + def processors_map(self, proc_map: Dict[str, ProcessorInfo]) -> None: + self._processors_map = proc_map + + @property + def processors_ap_map(self) -> Dict[APAddressBase, ProcessorInfo]: + """@brief Map of AP address objects and processor info objects.""" + if not self._processors_ap_map: + self._processors_ap_map = { + proc.ap_address: proc + for proc in self.processors_map.values() + } + return self._processors_ap_map + + @property + def programming(self) -> List[dict]: + """@brief Programming section of cbuild-run.""" + if self._valid: + return self._data.get('programming', []) + return [] + + @property + def debugger(self) -> Dict[str, Any]: + """@brief Debugger section of cbuild-run.""" + if self._valid: + return self._data.get('debugger', {}) + return {} + + @property + def start_pname(self) -> Optional[str]: + """@brief Selected start processor name.""" + pname = self.debugger.get('start-pname') + return pname + + @property + def system_resources(self) -> Dict[str, list]: + """@brief System Resources section of cbuild-run.""" + if self._valid: + return self._data.get('system-resources', {}) + return {} + + @property + def system_descriptions(self) -> List[dict]: + """@brief System Descriptions section of cbuild-run.""" + if self._valid: + return self._data.get('system-descriptions', []) + return [] + + @property + def debug_topology(self) -> Dict[str, Any]: + """@brief Debug Topology section of cbuild-run.""" + if self._valid and ('debug-topology' in self._data): + if self._debug_topology is None: + self._debug_topology = self._data.get('debug-topology', {}) + return self._debug_topology + return {} + + @property + def device_pack(self) -> List[str]: + """@brief Value of 'device-pack' (DFP) prefixed with CMSIS_PACK_ROOT.""" + if self._valid and ('device-pack' in self._data): + vendor, _pack = self._data['device-pack'].split('::', 1) + name, version = _pack.split('@', 1) + pack = os.path.normpath(f"${{CMSIS_PACK_ROOT}}/{vendor}/{name}/{version}") + return [os.path.expandvars(pack)] + return [] + + def populate_target(self, target: Optional[str] = None) -> None: + """@brief Generates and populates the target defined by the .cbuild-run.yml file.""" + if not self._valid: + return + + if target is None: + target = normalise_target_type_name(self.target) + elif target != normalise_target_type_name(self.target): + return + + # Check if we're even going to populate this target. + if target in TARGET: + LOG.debug("did not populate target from cbuild-run.yml for device %s because " + "there is already a %s target installed", self.target, target) + return + + # Generate target subclass and install it. + tgt = type(target.capitalize(), (CoreSightTarget,), { + "_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_primary_core" : CbuildRunTargetMethods._cbuild_target_start_processor, + "configure_core_reset": CbuildRunTargetMethods._cbuild_target_configure_core_reset, + "add_core": CbuildRunTargetMethods._cbuild_target_add_core, + "get_gdbserver_port": CbuildRunTargetMethods._cbuild_target_get_gdbserver_port, + "get_output": CbuildRunTargetMethods._cbuild_target_get_output + }) + TARGET[target] = tgt + + def _get_memory_to_process(self) -> List[dict]: + DEFAULT_MEMORY_MAP = sorted([ + {"name": "Code", "access": "rx", "start": 0x00000000, "size": 0x20000000}, + {"name": "SRAM", "access": "rwx", "start": 0x20000000, "size": 0x20000000}, + {"name": "Peripherals", "access": "rwp", "start": 0x40000000, "size": 0x20000000}, + {"name": "RAM1", "access": "rwx", "start": 0x60000000, "size": 0x20000000}, + {"name": "RAM2", "access": "rwx", "start": 0x80000000, "size": 0x20000000}, + {"name": "Devices-Shareable", "access": "rwp", "start": 0xA0000000, "size": 0x20000000}, + {"name": "Devices-NonShareable", "access": "rwp", "start": 0xC0000000, "size": 0x20000000}, + {"name": "System-Peripherals", "access": "rwp", "start": 0xE0000000, "size": 0x20000000} + ], key=lambda mem: mem['start']) + + def _fill_memory_gap(region: dict, start: int, end: int) -> dict: + _memory = region.copy() + _memory['start'] = start + _memory['size'] = end - start + return _memory + + # Create a copy of PDSC and user-defined memory regions from system resources + defined_memory = deepcopy(self.system_resources.get('memory', [])) + # Mark memory as 'defined' + for memory in defined_memory: + memory['defined'] = True + # Filter out memory regions that have alias and start at the same address ('s'/'n' access) + alias_memory = {(m['alias'], m['start']) for m in defined_memory if 'alias' in m} + if alias_memory: + defined_memory = [m for m in defined_memory if (m['name'], m['start']) not in alias_memory] + # Check if default memory map should be used + if self._use_default_memory_map: + # If no user-defined memory is present, use only the default memory map + if not defined_memory: + memory_to_process = DEFAULT_MEMORY_MAP + else: + memory_to_process = [] + # Sort PDSC and user-defined memory by start address + defined_memory.sort(key=lambda mem: mem['start']) + # Get first defined memory + mem_iter = iter(defined_memory) + memory = next(mem_iter, None) + # Start from beginning of the address space + next_memory_start = 0x00000000 + # Loop over the default memory regions + for default_memory in DEFAULT_MEMORY_MAP: + default_memory_end = default_memory['start'] + default_memory['size'] + # Search for region overlaps with PDSC and user-defined memory regions + while next_memory_start < default_memory_end: + if memory is not None: + # Exact match: insert defined memory region + if next_memory_start == memory['start']: + memory.pop('pname', None) + memory_to_process.append(memory) + next_memory_start = memory['start'] + memory['size'] + while memory is not None and memory['start'] < next_memory_start: + memory = next(mem_iter, None) + continue + # Partial overlap: fill gap to next defined memory region with default memory region + if next_memory_start <= memory['start'] < default_memory_end: + _memory = _fill_memory_gap(default_memory, next_memory_start, memory['start']) + memory_to_process.append(_memory) + next_memory_start = memory['start'] + continue + # No region overlap: use default memory region + _memory = _fill_memory_gap(default_memory, next_memory_start, default_memory_end) + memory_to_process.append(_memory) + next_memory_start = default_memory_end + return memory_to_process + else: + # If default map is not used, return only PDSC and user-defined memory regions + return defined_memory + + def _build_memory_map(self) -> None: + """@brief Constructs the device's memory map including flash and RAM segmentation. + + Processes defined regions, fills gaps, and handles flash algorithm overlays. + """ + regions = [] + memory_to_process = self._get_memory_to_process() + + def _memory_slice(memory: dict, start: int, size: int) -> None: + # Create a copy of current memory and amend region start and length + # and add updated memory to list of memories to process. + _memory = memory.copy() + _memory['start'] = start + _memory['size'] = size + memory_to_process.append(_memory) + + while memory_to_process: + memory = memory_to_process.pop() + # Determine memory type based on access permissions + if 'p' in memory['access']: + memory_type = MemoryType.DEVICE + elif 'w' in memory['access']: + memory_type = MemoryType.RAM + else: + memory_type = MemoryType.ROM + + # Define attributes for memory region + attrs = { + 'name': memory['name'], + 'start': memory['start'], + 'length': memory['size'], + 'access': memory['access'], + 'pname': memory.get('pname'), + 'alias': memory.get('alias'), + } + + if memory.get('defined', False): + for algorithm in self.programming: + if 'pname' in memory and 'pname' in algorithm: + if memory['pname'] != algorithm['pname']: + # Skip this algorithm if 'Pname' exists and does not match + continue + + memory_end = memory['start'] + memory['size'] + algorithm_end = algorithm['start'] + algorithm['size'] + + if (memory['start'] < algorithm_end) and (algorithm['start'] < memory_end): + # Create a local copy of attributes + flash_attrs = attrs.copy() + # If memory region and algorithm overlap, classify this part of region as FLASH + memory_type = MemoryType.FLASH + # Split memory into covered and uncovered section + flash_start = max(memory['start'], algorithm['start']) + flash_end = min(memory_end, algorithm_end) + if memory['start'] < algorithm['start']: + _memory_slice(memory, memory['start'], algorithm['start'] - memory['start']) + if memory_end > algorithm_end: + _memory_slice(memory, algorithm_end, memory_end - algorithm_end) + # Update flash attributes + flash_attrs['start'] = flash_start + flash_attrs['length'] = flash_end - flash_start + # Amend region 'pname' attribute if it is not already set + if (flash_attrs['pname'] is None) and ('pname' in algorithm): + flash_attrs['pname'] = algorithm['pname'] + # Add additional attributes related to the algorithm + if 'ram-start' in algorithm: + flash_attrs['_RAMstart'] = algorithm['ram-start'] + if 'ram-size' in algorithm: + flash_attrs['_RAMsize'] = algorithm['ram-size'] + if ('_RAMstart' not in flash_attrs) or ('_RAMsize' not in flash_attrs): + LOG.error("Flash algorithm '%s' has no RAMstart or RAMsize", algorithm['algorithm']) + algorithm_path = os.path.normpath(algorithm['algorithm']) + flash_attrs['flm'] = PackFlashAlgo(os.path.expandvars(algorithm_path)) + # Set sector size to a fixed value to prevent any possibility of infinite recursion due to + # the default lambdas for sector_size and blocksize returning each other's value. + flash_attrs['sector_size'] = 0 + # Create appropriate memory region object and store it + regions.append(MEMORY_TYPE_CLASS_MAP[memory_type](**flash_attrs)) + # Stop searching for algorithms if one without pname was found + if flash_attrs['pname'] is None: + break + + if memory_type != MemoryType.FLASH: + # Create appropriate memory region object and store it + regions.append(MEMORY_TYPE_CLASS_MAP[memory_type](**attrs)) + + self._memory_map = MemoryMap(regions) + + def _build_aps_map(self) -> None: + """@brief Builds mappings between Access Ports (APs) and processor descriptions. + + Populates valid APs, processor maps, and resolves SVD paths for debug topology. + """ + self._built_apid_map = True + + def get_svd_path(pname: Optional[str] = None) -> Optional[str]: + svd_path = None + for item in self.system_descriptions: + if item['type'] == 'svd': + if (pname is not None) and (item.get('pname') not in (None, pname)): + continue + norm_path = os.path.normpath(item['file']) + svd_path = os.path.expandvars(norm_path) + break + return svd_path + + _processors = {} + for processor in self.debug_topology.get('processors', {}): + apid = processor.get('apid') + pname = processor.get('pname', 'Unknown') + reset_sequence = processor.get('reset-sequence', 'ResetSystem') + if apid is not None: + _processors[apid] = (pname, reset_sequence) + + for debugport in self.debug_topology.get('debugports', {}): + dpid = debugport.get('dpid', 0) + self._valid_dps.append(dpid) + for accessport in debugport.get('accessports', {}): + apid = accessport.get('apid', 0) + + if 'address' in accessport: + self._uses_apv2 = True + ap_address = APv2Address(accessport['address'], dpid, apid) + elif 'index' in accessport: + ap_address = APv1Address(accessport['index'], dpid, apid) + else: + ap_address = APv1Address(0, dpid, apid) + + self._apids[apid] = ap_address + pname, reset_sequence = _processors.get(apid, (f'Unknown{apid}', 'ResetSystem')) + self._processors_map[pname] = ProcessorInfo(name=pname, + ap_address=ap_address, + svd_path=get_svd_path(pname), + default_reset_sequence=reset_sequence) + if not self._valid_dps: + # Use default __dp of 0. + self._valid_dps.append(0) + # At least one processor must have been defined. + if not self._processors_map: + # Add dummy processor. + self._processors_map['Unknown'] = ProcessorInfo(name='Unknown', + ap_address=APv1Address(0), + svd_path=get_svd_path()) + + +class CbuildRunSequences: + """@brief Parses debug sequences and debug variable definitions from .cbuild-run.yml.""" + def __init__(self, device: CbuildRun) -> None: + self._cbuild_vars = device.debug_vars + self._cbuild_debugger = device.debugger + self._cbuild_sequences = device.debug_sequences + + self._debugvars: Optional[Block] = None + self._debugvars_conf: Optional[Block] = None + self._sequences: Set[DebugSequence] = set() + self._control_nodes = {'if', 'while'} + + @property + def variables(self) -> Optional[Block]: + if (self._debugvars is None) and (self._cbuild_vars.get('vars') is not None): + self._debugvars = Block(self._cbuild_vars['vars'], info='debugvars') + return self._debugvars + + @property + def dbgconf_variables(self) -> Optional[Block]: + if self._debugvars_conf is None: + self._dbgconf_variables() + return self._debugvars_conf + + @property + def sequences(self) -> Set[DebugSequence]: + if self._sequences == set(): + self._build_sequences() + return self._sequences + + def _dbgconf_variables(self) -> Optional[Block]: + dbgconf_file = self._cbuild_debugger.get('dbgconf') + if dbgconf_file is not None: + try: + with open(dbgconf_file) as f: + dbgconf = f.read() + self._debugvars_conf = Block(dbgconf, info='dbgconf') + except FileNotFoundError: + LOG.warning("dbgconf file '%s' was not found", dbgconf_file) + + def _build_sequences(self) -> None: + for elem in self._cbuild_sequences: + name = elem.get('name') + if name is None: + LOG.warning("invalid debug sequence; missing name") + continue + + pname = elem.get('pname') + info = elem.get('info', '') + sequence = DebugSequence(name, True, pname, info) + + if 'blocks' in elem: + for child in elem['blocks']: + self._build_sequence_node(sequence, child) + self._sequences.add(sequence) + + def _build_sequence_node(self, parent: DebugSequenceNode, elem: dict) -> None: + info = elem.get('info', "") + if any(node in elem for node in self._control_nodes): + if 'if' in elem: + node = IfControl(str(elem['if']), info) + elif 'while' in elem: + node = WhileControl(str(elem['while']), info, int(elem.get('timeout', 0))) + + parent.add_child(node) + + if 'blocks' in elem: + for child in elem['blocks']: + self._build_sequence_node(node, child) + elif 'execute' in elem: + child = {k: v for k, v in elem.items() if k not in self._control_nodes} + self._build_sequence_node(node, child) + else: + if 'execute' in elem: + is_atomic = True if 'atomic' in elem else False + node = Block(elem['execute'], is_atomic, info) + parent.add_child(node) + + +class CbuildRunDebugSequenceDelegate(DebugSequenceDelegate): + """@brief Delegate class for running debug sequences parsed from cbuild-run files. + + Responsible for: + - Managing debug variables and overrides from .dbgconf files. + - Providing runtime execution contexts for sequences. + - Handling pyOCD wire protocols and connection parameters. + """ + ## Map from pyocd reset types to the __connection variable reset type field. + # 0=error, 1=hw, 2=SYSRESETREQ, 3=VECTRESET + RESET_TYPE_MAP = { + Target.ResetType.HW: 1, + Target.ResetType.SW: 2, # TODO pick default sw reset type + Target.ResetType.SW_SYSRESETREQ: 2, + Target.ResetType.SW_VECTRESET: 3, + Target.ResetType.SW_EMULATED: 2, # no direct match + } + + def __init__(self, target: CoreSightTarget, device: CbuildRun) -> None: + self._target = target + self._session = target.session + self._device = device + self._cbuild_sequences = CbuildRunSequences(device) + self._sequences: Set[DebugSequence] = self._cbuild_sequences.sequences + self._debugvars: Optional[Scope] = None + self._functions = DebugSequenceCommonFunctions() + + @property + def all_sequences(self) -> Set[DebugSequence]: + return self._sequences + + @property + def cmsis_pack_device(self) -> CbuildRun: + return self._device + + 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. + 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) + + # Make all vars read-only. + self._debugvars.freeze() + + if LOG.isEnabledFor(logging.INFO): + for name in sorted(self._debugvars.variables): + value = self._debugvars.get(name) + LOG.info("debugvar '%s' = %#x (%d)", name, value, value) + + return self._debugvars + + def run_sequence(self, name: str, pname: Optional[str] = None) -> Optional[Scope]: + """@brief Executes a debug sequence by name for the specified processor.""" + pname_desc = f" ({pname})" if (pname and LOG.isEnabledFor(logging.DEBUG)) else "" + + # Error out for invalid sequence. + if not self.has_sequence_with_name(name, pname): + raise NameError(name) + + # Get sequence object. + seq = self.get_sequence_with_name(name, pname) + + LOG.debug("Running debug sequence '%s'%s", name, pname_desc) + + # Create runtime context and contextified functions instance. + context = DebugSequenceExecutionContext(self._session, self, pname) + + # Map optional pname to AP address. If the pname is not specified, then use the device's + # first available AP. If not APs are known (eg haven't been discovered yet) then use 0. + if pname: + proc_map = self._device.processors_map + ap_address = proc_map[pname].ap_address + else: + ap = self._target.first_ap + if ap is not None: + ap_address = ap.address + else: + ap_address = APv1Address(0) + + # Set the default AP in the exec context. + context.default_ap = ap_address + + with context: + try: + executed_scope = seq.execute(context) + except exceptions.Error as err: + if pname: + LOG.error("Error while running debug sequence '%s' (core %s): %s", name, pname, err) + else: + LOG.error("Error while running debug sequence '%s': %s", name, err) + raise + + return executed_scope + + + def sequences_for_pname(self, pname: Optional[str]) -> Dict[str, DebugSequence]: + # Return *only* sequences with no Pname when passed pname=None. Otherwise we'd have + # to mangle the dict keys to include pname since there can be multiple sequences with + # the same name but different + return { + seq.name: seq + for seq in self._sequences + if (seq.pname is None) or (seq.pname == pname) + } + + def has_sequence_with_name(self, name: str, pname: Optional[str] = None) -> bool: + return name in self.sequences_for_pname(pname) + + def get_sequence_with_name(self, name: str, pname: Optional[str] = None) -> DebugSequence: + return self.sequences_for_pname(pname)[name] + + def get_protocol(self) -> int: + """@brief Return the value for the __protocol variable. + __protocol fields: + - [15:0] 0=error, 1=JTAG, 2=SWD, 3=cJTAG + - [16] SWJ-DP present? + - [17] switch through dormant state? + """ + session = self._target.session + assert session.probe, "must have a valid probe" + # Not having a wire protocol set is allowed if performing pre-reset since it will only + # execute ResetHardware (or equivalent), which can only access pins and such (theoretically). + assert self._session.context_state.is_performing_pre_reset or session.probe.wire_protocol, \ + "must have valid, connected probe" + if session.probe.wire_protocol == DebugProbe.Protocol.JTAG: + protocol = 1 + elif session.probe.wire_protocol == DebugProbe.Protocol.SWD: + protocol = 2 + else: + protocol = 0 # Error + if self._device.debug_topology.get('swj', True): + protocol |= 1 << 16 + if self._device.debug_topology.get('dormant', False): + protocol |= 1 << 17 + return protocol + + def get_connection_type(self) -> int: + """@brief Return the value for the __connection variable. + __connection fields: + - [7:0] connection type: 0=error/disconnected, 1=for debug, 2=for flashing + - [15:8] reset type: 0=error, 1=hw, 2=SYSRESETREQ, 3=VECTRESET + - [16] connect under reset? + - [17] pre-connect reset? + """ + ctype = 1 + ctype |= self.RESET_TYPE_MAP.get(self._session.options.get('reset_type'), 0) << 8 + + connect_mode = self._target.session.options.get('connect_mode') + if connect_mode == 'under-reset': + ctype |= 1 << 16 + + # The pre-reset bit should only be set when running ResetHardware for a connect pre-reset. + # This is stored in the is_performing_pre_reset session state variable, set by CoreSightTarget's + # pre_connect() method. + if self._session.context_state.is_performing_pre_reset: + ctype |= 1 << 17 + return ctype + + def get_traceout(self) -> int: + """@brief Return the value for the __traceout variable. + __traceout fields: + - [0] SWO enabled? + - [1] parallel trace enabled? + - [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 + + def get_sequence_functions(self) -> DebugSequenceCommonFunctions: + return self._functions diff --git a/pyocd/target/pack/cmsis_pack.py b/pyocd/target/pack/cmsis_pack.py index a04d3685e..f0e822a03 100644 --- a/pyocd/target/pack/cmsis_pack.py +++ b/pyocd/target/pack/cmsis_pack.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # pyOCD debugger -# Copyright (c) 2019-2020 Arm Limited +# Copyright (c) 2019-2020,2025 Arm Limited # Copyright (c) 2020 Men Shiyun # Copyright (c) 2020 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. # Copyright (c) 2021-2023 Chris Reed @@ -352,7 +352,7 @@ def _extract_items( return list(map.values()) def _inherit_attributes(self, to_elem: Element, from_elem: Optional[Element]) -> Element: - """@brief Add attributes missing from an elemnt but present in another. + """@brief Add attributes missing from an element but present in another. Copy to `to_elem` any attributes defined in `from_elem` but not defined, and therefore overridden, in `to_elem`. @@ -1157,20 +1157,22 @@ def _handle_accessports(self) -> None: f"{self.pack_description.pack_name} DFP ({self.part_number}): <{accessport.tag}> " f"'__dp' attribute is invalid ({ap_dp})") + # Get the __apid attribute. + apid = self._get_int_attribute(accessport, '__apid') + # APv1 if accessport.tag == 'accessportV1': index = self._get_int_attribute(accessport, 'index') - ap_address = APv1Address(index, ap_dp) + ap_address = APv1Address(index, ap_dp, apid) # APv2 elif accessport.tag == 'accessportV2': address = self._get_int_attribute(accessport, 'address') - ap_address = APv2Address(address, ap_dp) + ap_address = APv2Address(address, ap_dp, apid) else: raise exceptions.InternalError( f"unexpected element <{accessport.tag}> in access ports list") - # Save this AP address and the specified __apid. - apid = self._get_int_attribute(accessport, '__apid') + # Save this AP address. self._apids[apid] = ap_address except MalformedCmsisPackError as err: LOG.warning("%s", err) diff --git a/pyocd/tools/lists.py b/pyocd/tools/lists.py index 743f4dbc0..65fe2d197 100644 --- a/pyocd/tools/lists.py +++ b/pyocd/tools/lists.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2018-2020 Arm Limited +# Copyright (c) 2018-2020,2025 Arm Limited # Copyright (c) 2021-2023 Chris Reed # SPDX-License-Identifier: Apache-2.0 # @@ -167,7 +167,12 @@ def list_targets(name_filter=None, vendor_filter=None, source_filter=None): continue # Filter by source. - source = 'pack' if hasattr(t, '_pack_device') else 'builtin' + if hasattr(t, '_pack_device'): + source = 'pack' + elif hasattr(t, '_cbuild_device'): + source = 'cbuild-run' + else: + source = 'builtin' if source_filter and source_filter != source: continue