From 9aeb1aa44de0c2ab953fd0dbc556875b2845a609 Mon Sep 17 00:00:00 2001 From: Jonathan Nilsen Date: Wed, 20 Nov 2024 12:59:30 +0100 Subject: [PATCH 1/5] DNM: various preliminary changes See #14, #15, #16 Signed-off-by: Jonathan Nilsen --- pyproject.toml | 17 ++++++------- src/svd/__init__.py | 51 +++++++++++++++++++++++++++++++------ src/svd/_bindings.py | 1 - src/svd/bindings.py | 3 +-- src/svd/device.py | 56 ++++++++++------------------------------- src/svd/errors.py | 3 +-- src/svd/memory_block.py | 5 +--- src/svd/parsing.py | 2 +- src/svd/path.py | 2 -- 9 files changed, 69 insertions(+), 71 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87fe377..61351fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "setuptools_scm[toml]>=6.2", "lxml~=5.3", "numpy~=2.1", "typing_extensions>=4.4.0", @@ -24,17 +23,17 @@ homepage = "https://github.com/nordicsemiconductor/svada" repository = "https://github.com/nordicsemiconductor/svada.git" [build-system] -requires = ["setuptools>=61", "setuptools_scm[toml]>=6.2"] -build-backend = "setuptools.build_meta" +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" -[tool.setuptools] -package-dir = { svd = "src/svd" } -include-package-data = true +[tool.hatch.version] +source = "vcs" -[tool.setuptools.package-data] -svd = ["py.typed"] +[tool.hatch.build.targets.sdist] +packages = ["src/svd"] -[tool.setuptools_scm] +[tool.hatch.build.targets.wheel] +packages = ["src/svd"] [tool.mypy] disallow_untyped_defs = true diff --git a/src/svd/__init__.py b/src/svd/__init__.py index 8d0ddd3..a49928a 100644 --- a/src/svd/__init__.py +++ b/src/svd/__init__.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: Apache-2.0 # +import importlib.metadata + from .bindings import ( Access, ReadAction, @@ -49,11 +51,46 @@ Struct, ) -import importlib.metadata +__version__ = importlib.metadata.version("svada") + +__all__ = [ + "Access", + "ReadAction", + "Endian", + "SauAccess", + "AddressBlockUsage", + "Protection", + "EnumUsage", + "WriteAction", + "DataType", + "CpuName", + "Cpu", + "AddressBlock", + "SauRegion", + "SvdError", + "SvdParseError", + "SvdDefinitionError", + "SvdMemoryError", + "SvdPathError", + "SvdIndexError", + "SvdKeyError", + "parse", + "Options", + "FEPath", + "EPath", + "Array", + "Field", + "FlatRegister", + "FlatRegisterUnion", + "FlatRegisterType", + "FlatStruct", + "FlatField", + "Device", + "Peripheral", + "Register", + "RegisterUnion", + "RegisterType", + "Struct", +] -try: - __version__ = importlib.metadata.version("svada") -except importlib.metadata.PackageNotFoundError: - # Package is not installed - import setuptools_scm - __version__ = setuptools_scm.get_version(root="../..", relative_to=__file__) +del importlib.metadata diff --git a/src/svd/_bindings.py b/src/svd/_bindings.py index a48c1c0..9dcfd27 100644 --- a/src/svd/_bindings.py +++ b/src/svd/_bindings.py @@ -20,7 +20,6 @@ Dict, Generic, Iterable, - Iterator, List, Literal, Mapping, diff --git a/src/svd/bindings.py b/src/svd/bindings.py index 32097bb..a662477 100644 --- a/src/svd/bindings.py +++ b/src/svd/bindings.py @@ -15,7 +15,6 @@ from __future__ import annotations -import dataclasses as dc import enum import typing from dataclasses import dataclass @@ -23,7 +22,7 @@ from lxml import objectify from lxml.objectify import BoolElement, StringElement -from typing_extensions import TypeGuard, override +from typing_extensions import TypeGuard from ._bindings import ( SELF_CLASS, diff --git a/src/svd/device.py b/src/svd/device.py index 8bc3bbf..03c03c4 100644 --- a/src/svd/device.py +++ b/src/svd/device.py @@ -15,13 +15,9 @@ from __future__ import annotations -import dataclasses as dc import math -import re import typing from abc import ABC, abstractmethod -from collections import defaultdict -from dataclasses import dataclass from functools import cached_property from types import MappingProxyType from typing import ( @@ -37,7 +33,6 @@ Literal, Mapping, NamedTuple, - NoReturn, Optional, Protocol, Reversible, @@ -70,7 +65,6 @@ ReadAction, RegisterProperties, WriteAction, - WriteConstraint, ) from .errors import ( SvdDefinitionError, @@ -1421,21 +1415,17 @@ def content(self, new_content: Union[int, str]) -> None: write to the field. """ if isinstance(new_content, int): - val = self._trailing_zero_adjusted(new_content) - - if val not in self.allowed_values: + if new_content not in self.allowed_values: raise ValueError( f"{self!r} does not accept" - f" the bit value '{val}' ({hex(val)})." - " Are you sure you have an up to date .svd file?" + f" the bit value '{new_content}' ({hex(new_content)})." ) - resolved_value = val + resolved_value = new_content elif isinstance(new_content, str): if new_content not in self.enums: raise ValueError( f"{self!r} does not accept" f" the enum '{new_content}'." - " Are you sure you have an up to date .svd file?" ) resolved_value = self.enums[new_content] else: @@ -1446,6 +1436,16 @@ def content(self, new_content: Union[int, str]) -> None: self._register.set_content(resolved_value << self.bit_offset, self.mask) + @property + def content_enum(self) -> str: + """The name of the enum corresponding to the field value.""" + content = self.content + for enum_str, value in self.enums.items(): + if content == value: + return enum_str + + raise LookupError(f"{self!r} content {content} does not correspond to an enum.") + @property def modified(self) -> bool: """True if the field contains a different value now than at reset.""" @@ -1458,36 +1458,6 @@ def unconstrain(self) -> None: """ self._allowed_values = range(2**self.bit_width) - def _trailing_zero_adjusted(self, content: int) -> int: - """ - Internal method that checks and adjusts a given value for trailing zeroes if it exceeds - the bit width of its field. Some values are simplest to encode as a full 32-bit value even - though their field is comprised of less than 32 bits, such as an address. - - :param value: A numeric value to check against the field bits - - :return: Field value without any trailing zeroes - """ - - width_max = 2**self.bit_width - - if content > width_max: - max_val = width_max - 1 - max_val_hex_len = len(f"{max_val:x}") - hex_val = f"{content:0{8}x}" # leading zeros, 8-byte max, in hex - trailing = hex_val[max_val_hex_len:] # Trailing zeros - - if int(trailing, 16) != 0: - raise SvdMemoryError(f"Unexpected trailing value: {trailing}") - - cropped = hex_val[:max_val_hex_len] # value w/o trailing - adjusted = int(cropped, 16) - - if adjusted <= max_val: - return adjusted - - return content - def __repr__(self) -> str: """Short field description.""" bool_props = ("modified",) if self.modified else () diff --git a/src/svd/errors.py b/src/svd/errors.py index 9c51a54..11c205c 100644 --- a/src/svd/errors.py +++ b/src/svd/errors.py @@ -4,10 +4,9 @@ # SPDX-License-Identifier: Apache-2.0 # -from abc import ABC from typing import Any, Iterable, Union -from .path import EPath, EPathType, EPathUnion +from .path import EPathUnion class SvdError(Exception): diff --git a/src/svd/memory_block.py b/src/svd/memory_block.py index aa2e1d8..cda4e14 100644 --- a/src/svd/memory_block.py +++ b/src/svd/memory_block.py @@ -8,15 +8,12 @@ from functools import partial from typing import ( - Any, Callable, - Dict, Iterator, List, Mapping, Optional, Tuple, - Type, TypeVar, Union, overload, @@ -24,7 +21,7 @@ import numpy as np import numpy.ma as ma -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike from typing_extensions import Self from .errors import SvdMemoryError diff --git a/src/svd/parsing.py b/src/svd/parsing.py index 2a5a008..934d6cc 100644 --- a/src/svd/parsing.py +++ b/src/svd/parsing.py @@ -46,7 +46,7 @@ class Options: # Make the addressOffset in cluster elements be relative to the immediate parent # element instead of relative to the containing peripheral. - parent_relative_cluster_address: bool = False + parent_relative_cluster_address: bool = True # Cluster/register/field elements to remove from the XML document prior to parsing. # This can be used to remove outdated/deprecated elements from the device if they cause diff --git a/src/svd/path.py b/src/svd/path.py index ea3fa54..ed8a8a8 100644 --- a/src/svd/path.py +++ b/src/svd/path.py @@ -12,11 +12,9 @@ import re from abc import ABC, abstractmethod -from functools import singledispatch from itertools import chain from typing import ( Any, - Generic, Iterable, List, Optional, From 85e381a45a37263126ad1a1c9def9543a22fbcba Mon Sep 17 00:00:00 2001 From: Jonathan Nilsen Date: Wed, 20 Nov 2024 14:45:14 +0100 Subject: [PATCH 2/5] svd: add helper for populating and outputting device memory Adds a utility module with helpers for populating and outputting device memory. The following formats are supported for input/output: * A dict mapping address -> value (at a byte level) * A dict mapping a device-like structure to register/field values The following format is currently only supported for output: * A bytearray containing device memory The APIs for generating outputs offer a flexible "selector" mechanism that lets the user decide which part of the device to output. Signed-off-by: Jonathan Nilsen --- src/svd/__init__.py | 2 + src/svd/util.py | 392 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 src/svd/util.py diff --git a/src/svd/__init__.py b/src/svd/__init__.py index a49928a..8d5e60a 100644 --- a/src/svd/__init__.py +++ b/src/svd/__init__.py @@ -6,6 +6,7 @@ import importlib.metadata +from . import util from .bindings import ( Access, ReadAction, @@ -54,6 +55,7 @@ __version__ = importlib.metadata.version("svada") __all__ = [ + "util", "Access", "ReadAction", "Endian", diff --git a/src/svd/util.py b/src/svd/util.py new file mode 100644 index 0000000..eaa907b --- /dev/null +++ b/src/svd/util.py @@ -0,0 +1,392 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import annotations + +import enum +import sys +from dataclasses import dataclass +from itertools import pairwise +from typing import Any, Optional + +import svd + + +@dataclass +class BuildSelector: + """Used to select select which parts of the device to write to an output.""" + + class ContentStatus(str, enum.Enum): + ANY = "any" + WRITTEN = "written" + MODIFIED = "modified" + + peripherals: Optional[list[str]] = None + address_range: Optional[tuple[int, int]] = None + content_status: ContentStatus = ContentStatus.ANY + + def is_periph_selected(self, periph: svd.Peripheral) -> bool: + if self.peripherals and periph.name not in self.peripherals: + return False + + if self.address_range is not None and not _ranges_overlap_inclusive( + *periph.address_bounds, *self.address_range + ): + return False + + return True + + def is_addr_selected(self, address: int) -> bool: + if self.address_range is not None and not ( + self.address_range[0] <= address <= self.address_range[1] + ): + return False + return True + + def is_reg_selected(self, reg: svd.Register) -> bool: + if self.address_range is not None: + reg_addr_range = reg.address_range + if not _ranges_overlap_inclusive( + reg_addr_range.start, reg_addr_range.stop - 1, *self.address_range + ): + return False + + if ( + self.content_status == BuildSelector.ContentStatus.WRITTEN + and not reg.written + ): + return False + + if ( + self.content_status == BuildSelector.ContentStatus.MODIFIED + and not reg.modified + ): + return False + + return True + + def is_field_selected(self, field: svd.Field) -> bool: + # svada does not have a way to check written status at this level currently + if ( + self.content_status == BuildSelector.ContentStatus.MODIFIED + and not field.modified + ): + return False + + return True + + +class DeviceBuilder: + """Used to populate peripheral registers and output the register contents in various formats.""" + + def __init__( + self, device: svd.Device, enforce_svd_constraints: bool = True + ) -> None: + self._device = device + self._enforce_svd_constraints = enforce_svd_constraints + self._written_peripherals = {} + self._cached_device_periph_map = [] + self._cached_periph_reg_maps = {} + + @property + def device(self) -> svd.Device: + """The device structure the builder was initialized with.""" + return self._device + + @property + def written_peripherals(self) -> list[svd.Peripheral]: + """The list of peripherals that have been written to as part of the API calls.""" + return list(self._written_peripherals.values()) + + def build_bytes(self, selector: BuildSelector = BuildSelector()) -> bytearray: + """Encode device content as bytes. + + :param selector: selected parts of the device. + :returns: content bytes. + """ + memory = self.build_memory(selector) + out = bytearray() + + for (addr_a, value_a), (addr_b, value_b) in pairwise(memory.items()): + if not out: + out.append(value_a) + + num_empty = addr_b - addr_a - 1 + if num_empty > 0: + # TODO: 0 may not always be valid + out.extend([0] * num_empty) + + out.append(value_b) + + return out + + def build_memory(self, selector: BuildSelector = BuildSelector()) -> dict[int, int]: + """Encode device content as a mapping between address and value. + + :param selector: selected parts of the device. + :returns: content memory map. + """ + memory = {} + + for peripheral in self._device.values(): + if not selector.is_periph_selected(peripheral): + continue + + if selector.content_status == BuildSelector.ContentStatus.MODIFIED: + # There's no good way to determine this in svada at the moment + periph_modified_filter = set() + for reg in peripheral.register_iter(leaf_only=True): + if reg.modified: + periph_modified_filter.update(reg.address_range) + else: + periph_modified_filter = None + + memory_iter = peripheral.memory_iter( + absolute_addresses=True, + written_only=( + selector.content_status == BuildSelector.ContentStatus.WRITTEN + ), + ) + + for addr, val in memory_iter: + if ( + periph_modified_filter is not None + and addr not in periph_modified_filter + ): + continue + memory[addr] = val + + return memory + + def build_dict(self, selector: BuildSelector = BuildSelector()) -> dict: + """Encode device content as a dictionary representation of the registers and content. + + :param selector: selected parts of the device. + :returns: content dictionary. + """ + config = {} + + for peripheral in self._device.values(): + if not selector.is_periph_selected(peripheral): + continue + + cfg_periph = {} + nonleaf_stack = [cfg_periph] + for reg in peripheral.register_iter(): + reg_depth = len(reg.path) + if reg_depth < len(nonleaf_stack): + # We are returning from a nested context. + # Prune any empty supertables on the way up. + for elem in list(nonleaf_stack[reg_depth:]): + for key, val in list(elem.items()): + if not val: + del elem[key] + + nonleaf_stack = nonleaf_stack[:reg_depth] + + if not reg.leaf: + reg_table = {} + nonleaf_stack[reg_depth - 1][str(reg.path[-1])] = reg_table + nonleaf_stack.append(reg_table) + continue + + assert isinstance(reg, svd.Register) + + if not selector.is_reg_selected(reg): + continue + + if reg.fields: + reg_table = {} + + for field_name, field in reg.fields.items(): + if not selector.is_field_selected(field): + continue + + try: + reg_table[field_name] = field.content_enum + except LookupError: + # Content does not match any defined enum + reg_table[field_name] = field.content + + # Include if at least one field was selected + if reg_table: + nonleaf_stack[reg_depth - 1][str(reg.path[-1])] = reg_table + else: + # No fields, just a value + nonleaf_stack[reg_depth - 1][str(reg.path[-1])] = reg.content + + # Prune empty tables from the top level config + for key, val in list(cfg_periph.items()): + if not val: + del cfg_periph[key] + + if cfg_periph: + config[peripheral.name] = cfg_periph + else: + # Ensure a non-empty config file + config[peripheral.name] = {} + + return config + + def apply_memory(self, content: dict[int, int]) -> DeviceBuilder: + """Update device content based on a memory map. + + The content is assumed to be at byte granularity and sorted by ascending address. + + :param content: content memory map. + :returns: builder instance. + """ + periph_map = self._device_periph_map() + reg_map = {} + current_periph = None + current_periph_range = range(-1, 0) + current_periph_regs = {} + + map_iter = iter(content.items()) + + while True: + try: + addr_0, val_0 = next(map_iter) + except StopIteration: + break + + if addr_0 not in current_periph_range: + for periph_range, periph in periph_map: + if addr_0 in periph_range: + current_periph = periph + current_periph_range = periph_range + periph_id = _get_periph_id(periph) + if periph_id in reg_map: + current_periph_regs = reg_map[periph_id] + else: + current_periph_regs = self._periph_reg_map(current_periph) + reg_map[periph_id] = current_periph_regs + self._written_peripherals.setdefault(periph_id, periph) + break + else: + # TODO: logger? + print( + f"Address 0x{addr_0:08x} does not correspond to any peripheral", + file=sys.stderr, + ) + continue + + assert current_periph_regs is not None + + try: + reg = current_periph_regs[addr_0] + except KeyError: + # TODO: logger? + print( + f"Address 0x{addr_0:08x} is within the address range of {current_periph} " + f"[0x{current_periph_range.start:x}-0x{current_periph_range.stop:x}), but " + "does not correspond to any register in the peripheral", + file=sys.stderr, + ) + continue + + reg_len = reg.bit_width // 8 + reg_content_bytes = [val_0] + for i in range(reg_len - 1): + try: + _, val_i = next(map_iter) + reg_content_bytes.append(val_i) + except StopIteration: + raise ValueError( + f"Content for {reg} was only partially specified. " + f"Missing value for address 0x{addr_0 + i:08x}" + ) + + # TODO: don't need to call this more than once + if not self._enforce_svd_constraints: + reg.unconstrain() + + reg.content = int.from_bytes(bytes(reg_content_bytes), byteorder="little") + + return self + + def apply_dict(self, config: dict[str, Any]) -> DeviceBuilder: + """Populate device content from a dictionary representation of the registers and content. + + The dictionary structure should match the structure of the device peripherals and registers. + Content can be set either at the register or field level. + Field content can be set either using an enum name or with a numeric value. + + :param content: content dictionary. + :returns: builder instance. + """ + affected_periphs = [] + + for periph_name, content in config.items(): + peripheral = self._device[periph_name] + for reg_name, reg_value in content.items(): + self._reg_apply_dict( + peripheral[reg_name], + reg_value, + ) + affected_periphs.append(peripheral) + + for periph in affected_periphs: + self._written_peripherals.setdefault(_get_periph_id(periph), periph) + + return self + + def _reg_apply_dict( + self, + reg: svd.Array | svd.Struct | svd.Register | svd.Field, + value: dict | int | str, + ) -> None: + match (reg, value): + case (svd.Array(), dict()): + for index_str, rest in value.items(): + try: + index = int(index_str) + except ValueError: + raise ValueError( + f"{index_str} is not a valid index for {reg!r}" + ) + self._reg_apply_dict(reg[index], rest) + + case (svd.Struct() | svd.Register(), dict()): + for name, rest in value.items(): + self._reg_apply_dict(reg[name], rest) + + case (svd.Register() | svd.Field(), int()) | (svd.Field(), str()): + if not self._enforce_svd_constraints: + reg.unconstrain() + reg.content = value + + case _: + raise ValueError(f"{value} is not a valid value for {reg!r}") + + def _device_periph_map(self) -> list[tuple[range, svd.Peripheral]]: + if self._cached_device_periph_map: + return self._cached_device_periph_map + + for periph in self._device.values(): + start_addr, end_addr = periph.address_bounds + self._cached_device_periph_map.append((range(start_addr, end_addr), periph)) + + return self._cached_device_periph_map + + def _periph_reg_map(self, peripheral: svd.Peripheral) -> dict[int, svd.Register]: + periph_id = _get_periph_id(peripheral) + if periph_id in self._cached_periph_reg_maps: + return self._cached_periph_reg_maps[periph_id] + + new_map = {reg.address: reg for reg in peripheral.register_iter(leaf_only=True)} + self._cached_periph_reg_maps[periph_id] = new_map + return new_map + + +def _get_periph_id(peripheral: svd.Peripheral) -> tuple: + return peripheral.name, peripheral.base_address + + +def _ranges_overlap_inclusive( + a_start: int, a_end: int, b_start: int, b_end: int +) -> bool: + return a_end >= b_start and b_end >= a_start From 042d12a59bba1b4ef6b0cbcc8d5de6cc6eb772fb Mon Sep 17 00:00:00 2001 From: Jonathan Nilsen Date: Thu, 21 Nov 2024 13:08:12 +0100 Subject: [PATCH 3/5] svd: add a CLI with utility scripts Add a CLI intended for various utility scripts. For now there is a single command that can be used to generate register content descriptions. Signed-off-by: Jonathan Nilsen --- pyproject.toml | 4 + src/svd/__main__.py | 295 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 src/svd/__main__.py diff --git a/pyproject.toml b/pyproject.toml index 61351fd..14ce11d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,10 @@ dependencies = [ "numpy~=2.1", "typing_extensions>=4.4.0", ] +optional-dependencies = { cli = ["intelhex", "tomlkit"] } + +[project.scripts] +svada = "svd.__main__:cli" [project.urls] homepage = "https://github.com/nordicsemiconductor/svada" diff --git a/src/svd/__main__.py b/src/svd/__main__.py new file mode 100644 index 0000000..be7fcf6 --- /dev/null +++ b/src/svd/__main__.py @@ -0,0 +1,295 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import annotations + +import argparse +import dataclasses +import enum +import importlib.util +import json +import sys +from pathlib import Path +from textwrap import dedent + +import svd +from svd.util import BuildSelector, DeviceBuilder + +_HAS_INTELHEX = importlib.util.find_spec("intelhex") is not None +_HAS_TOMLKIT = importlib.util.find_spec("tomlkit") is not None + + +class Format(enum.Enum): + JSON = enum.auto() + BIN = enum.auto() + IHEX = enum.auto() + TOML = enum.auto() + + +def cli() -> None: + top = argparse.ArgumentParser( + description=dedent( + """\ + Collection of utility scripts for working with System View Description (SVD) files. + + Use the --help option with each command to see more information about + them and their individual options/arguments. + """ + ), + allow_abbrev=False, + add_help=False, # To override -h + ) + _add_help_option(top) + + sub = top.add_subparsers(title="subcommands") + + gen = sub.add_parser( + "content-gen", + help="Encode and decode device content to and from various formats.", + description=dedent( + """\ + Encode device content from one of the supported formats and output it to another + supported format. + """ + ), + allow_abbrev=False, + add_help=False, + ) + gen.set_defaults(_command="content-gen") + _add_help_option(gen) + + gen_in = gen.add_argument_group("input options") + gen_in_mutex = gen_in.add_mutually_exclusive_group(required=True) + gen_in_mutex.add_argument( + "-j", "--in-json", action="store_true", help="Input is in JSON format." + ) + if _HAS_INTELHEX: + gen_in_mutex.add_argument( + "-h", + "--in-hex", + action="store_true", + help="Input is in Intel HEX format.", + ) + if _HAS_TOMLKIT: + gen_in_mutex.add_argument( + "-t", + "--in-toml", + action="store_true", + help="Input is in TOML format.", + ) + gen_in.add_argument( + "-i", + "--input-file", + type=Path, + help="File to read the input from. If not given, stdin is used.", + ) + + gen_svd = gen.add_argument_group("SVD options") + gen_svd.add_argument( + "-s", + "--svd-file", + required=True, + type=Path, + help="Path to the device SVD file.", + ) + gen_svd.add_argument( + "-n", + "--no-strict", + action="store_true", + help="Don't enforce constraints on register and field values based on the SVD file.", + ) + gen_svd.add_argument( + "--svd-parse-options", + type=json.loads, + help=( + "JSON object used to override fields in the Options object to customize svada parsing " + "behavior. Mainly intended for advanced use cases such as working around " + "difficult SVD files. " + ), + ) + + gen_sel = gen.add_argument_group("selection options") + gen_sel.add_argument( + "-p", + "--peripheral", + metavar="NAME", + dest="peripherals", + action="append", + help="Limit output content to the given peripheral. May be given multiple times.", + ) + gen_sel.add_argument( + "-a", + "--address-range", + metavar=("START", "END"), + nargs=2, + type=_parse_address_range, + help="Limit output to a specific address range. Addresses can be given as hex or decimal.", + ) + gen_sel.add_argument( + "-c", + "--content-status", + choices=[c.value for c in BuildSelector.ContentStatus.__members__.values()], + help="Limit output based on the status of the register content.", + ) + + gen_out = gen.add_argument_group("output options") + gen_out_mutex = gen_out.add_mutually_exclusive_group(required=True) + gen_out_mutex.add_argument( + "-J", + "--out-json", + action="store_true", + help="Output in JSON format.", + ) + gen_out_mutex.add_argument( + "-B", + "--out-bin", + action="store_true", + help="Output in binary format.", + ) + if _HAS_INTELHEX: + gen_out_mutex.add_argument( + "-H", + "--out-hex", + action="store_true", + help="Output in Intel HEX format.", + ) + if _HAS_TOMLKIT: + gen_out_mutex.add_argument( + "-T", + "--out-toml", + action="store_true", + help="Output in TOML format.", + ) + gen_out.add_argument( + "-o", + "--output-file", + type=Path, + help="File to write the output to. If not given, output is written to stdout.", + ) + + args = top.parse_args() + if not hasattr(args, "_command"): + top.print_usage() + sys.exit(2) + + if args._command == "content-gen": + cmd_content_gen(args) + else: + top.print_usage() + sys.exit(2) + + sys.exit(0) + + +def cmd_content_gen(args: argparse.Namespace) -> None: + input_format = None + input_mode = None + if args.in_json: + input_format = Format.JSON + input_mode = "rb" + elif _HAS_INTELHEX and getattr(args, "in_hex", False): + input_format = Format.IHEX + input_mode = "r" + elif _HAS_TOMLKIT and getattr(args, "in_toml", False): + input_format = Format.TOML + input_mode = "rb" + + assert input_format is not None + assert input_mode is not None + + if args.input_file: + # TODO: encoding + input_file = open(args.input_file, input_mode) + else: + input_file = sys.stdin.buffer if "b" in input_mode else sys.stdin + + options = svd.Options( + parent_relative_cluster_address=True, + ) + if args.svd_parse_options: + options = dataclasses.replace(options, **args.svd_parse_options) + + device = svd.parse(args.svd_file, options=options) + device_builder = DeviceBuilder(device, enforce_svd_constraints=not args.no_strict) + + if input_format == Format.JSON: + input_dict = json.load(input_file) + device_builder.apply_dict(input_dict) + elif input_format == Format.IHEX: + from intelhex import IntelHex + + ihex = IntelHex(input_file) + ihex_memory = {a: ihex[a] for a in ihex.addresses()} + device_builder.apply_memory(ihex_memory) + elif input_format == Format.TOML: + import tomlkit + + input_dict = tomlkit.load(input_file).unwrap() + device_builder.apply_dict(input_dict) + + selector = BuildSelector( + peripherals=args.peripherals if args.peripherals else None, + address_range=args.address_range if args.address_range is not None else None, + content_status=( + BuildSelector.ContentStatus(args.content_status) + if args.content_status + else BuildSelector.ContentStatus.ANY + ), + ) + + output_format = None + output_mode = None + if args.out_json: + output_format = Format.JSON + output_mode = "w" + if args.out_bin: + output_format = Format.BIN + output_mode = "wb" + elif _HAS_INTELHEX and getattr(args, "out_hex", False): + output_format = Format.IHEX + output_mode = "w" + elif _HAS_TOMLKIT and getattr(args, "out_toml", False): + output_format = Format.TOML + output_mode = "w" + + assert output_format is not None + assert output_mode is not None + + if args.output_file: + output_file = open(args.output_file, output_mode, encoding="utf-8") + else: + output_file = sys.stdout.buffer if "b" in output_mode else sys.stdout + + if output_format == Format.JSON: + output_dict = device_builder.build_dict(selector) + json.dump(output_dict, output_file) + elif output_format == Format.BIN: + output_bin = device_builder.build_bytes(selector) + output_file.write(output_bin) + elif output_format == Format.IHEX: + from intelhex import IntelHex + + output_ihex = IntelHex(device_builder.build_memory(selector)) + output_ihex.write_hex_file(output_file) + elif output_format == Format.TOML: + import tomlkit + + output_dict = device_builder.build_dict(selector) + tomlkit.dump(output_dict, output_file) + + +def _add_help_option(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--help", action="help", help="Print help message") + + +def _parse_address_range(addr_range: str) -> tuple[int, int]: + start, end = [int(a.strip(), 0) for a in addr_range.split()] + return start, end + + +# Entry point when running with python -m svd +if __name__ == "__main__": + cli() From ea809c82ab6c3b67b9a2542612edf2c1728a48ad Mon Sep 17 00:00:00 2001 From: Jonathan Nilsen Date: Thu, 21 Nov 2024 17:15:43 +0100 Subject: [PATCH 4/5] fixup! svd: add a CLI with utility scripts --- src/svd/__main__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/svd/__main__.py b/src/svd/__main__.py index be7fcf6..b9d7f3e 100644 --- a/src/svd/__main__.py +++ b/src/svd/__main__.py @@ -206,9 +206,7 @@ def cmd_content_gen(args: argparse.Namespace) -> None: else: input_file = sys.stdin.buffer if "b" in input_mode else sys.stdin - options = svd.Options( - parent_relative_cluster_address=True, - ) + options = svd.Options() if args.svd_parse_options: options = dataclasses.replace(options, **args.svd_parse_options) From 9a13a2307048bd6b6cccabd7bf0dab4c16da9ee9 Mon Sep 17 00:00:00 2001 From: Jonathan Nilsen Date: Fri, 22 Nov 2024 15:29:32 +0100 Subject: [PATCH 5/5] fixup! svd: add helper for populating and outputting device memory --- src/svd/util.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/svd/util.py b/src/svd/util.py index eaa907b..6fd89af 100644 --- a/src/svd/util.py +++ b/src/svd/util.py @@ -268,10 +268,10 @@ def apply_memory(self, content: dict[int, int]) -> DeviceBuilder: break else: # TODO: logger? - print( - f"Address 0x{addr_0:08x} does not correspond to any peripheral", - file=sys.stderr, - ) + # print( + # f"Address 0x{addr_0:08x} does not correspond to any peripheral", + # file=sys.stderr, + # ) continue assert current_periph_regs is not None @@ -280,12 +280,12 @@ def apply_memory(self, content: dict[int, int]) -> DeviceBuilder: reg = current_periph_regs[addr_0] except KeyError: # TODO: logger? - print( - f"Address 0x{addr_0:08x} is within the address range of {current_periph} " - f"[0x{current_periph_range.start:x}-0x{current_periph_range.stop:x}), but " - "does not correspond to any register in the peripheral", - file=sys.stderr, - ) + # print( + # f"Address 0x{addr_0:08x} is within the address range of {current_periph} " + # f"[0x{current_periph_range.start:x}-0x{current_periph_range.stop:x}), but " + # "does not correspond to any register in the peripheral", + # file=sys.stderr, + # ) continue reg_len = reg.bit_width // 8