From 91badff5eb2773e8f61d708032ca4c40f96c164f Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Thu, 4 Jul 2024 21:28:23 -0700 Subject: [PATCH] 3.3.0 - Add more advanced SVD support --- .github/workflows/python-package.yml | 13 ++- .gitignore | 1 + .pylintrc | 3 +- LICENSE | 2 +- config | 2 +- ifgen/commands/gen.py | 16 +++- ifgen/environment/field.py | 2 +- ifgen/svd/group/enums.py | 4 +- ifgen/svd/group/fields.py | 125 +++++++++++++++++++++++---- ifgen/svd/model/__init__.py | 12 ++- ifgen/svd/model/field.py | 5 +- ifgen/svd/model/peripheral.py | 25 ++++++ local/configs/python.yaml | 2 +- local/variables/package.yaml | 4 +- tasks/conf.py | 14 +-- tests/commands/test_svd.py | 2 +- 16 files changed, 189 insertions(+), 43 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 88d3214..9de706a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -10,6 +10,7 @@ on: env: TWINE_PASSWORD: ${{secrets.TWINE_PASSWORD}} GITHUB_API_TOKEN: ${{secrets.API_TOKEN}} + CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} jobs: build: @@ -57,20 +58,24 @@ jobs: - run: mk docs if: | - matrix.python-version == '3.11' + matrix.python-version == '3.12' && matrix.system == 'ubuntu-latest' - run: mk python-test env: PY_TEST_EXTRA_ARGS: --cov-report=xml - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v3.1.5 + with: + fail_ci_if_error: true + verbose: true + token: ${{secrets.CODECOV_TOKEN}} - run: mk pypi-upload-ci env: TWINE_USERNAME: __token__ if: | - matrix.python-version == '3.11' + matrix.python-version == '3.12' && matrix.system == 'ubuntu-latest' && env.TWINE_PASSWORD != '' && github.ref_name == 'master' @@ -79,7 +84,7 @@ jobs: mk python-release owner=vkottler \ repo=ifgen version=3.2.1 if: | - matrix.python-version == '3.11' + matrix.python-version == '3.12' && matrix.system == 'ubuntu-latest' && env.GITHUB_API_TOKEN != '' && github.ref_name == 'master' diff --git a/.gitignore b/.gitignore index e1ff920..b775a12 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ coverage*.xml tags mklocal docs +src diff --git a/.pylintrc b/.pylintrc index 9503d8e..7dc6b19 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,7 @@ [DESIGN] -max-args=8 +max-args=9 max-attributes=8 +max-locals=17 [MESSAGES CONTROL] disable=duplicate-code diff --git a/LICENSE b/LICENSE index e97d6f9..241562d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Vaughn Kottler +Copyright (c) 2024 Vaughn Kottler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/config b/config index 0c555bf..2fcfcfb 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 0c555bf6565cc5d90408adbad3c162edca43a7e8 +Subproject commit 2fcfcfb2c3f3470be6e3d55f75e9d68fbd948f7f diff --git a/ifgen/commands/gen.py b/ifgen/commands/gen.py index c41da95..40c6913 100644 --- a/ifgen/commands/gen.py +++ b/ifgen/commands/gen.py @@ -5,6 +5,7 @@ # built-in from argparse import ArgumentParser as _ArgumentParser from argparse import Namespace as _Namespace +import sys # third-party from vcorelib.args import CommandFunction as _CommandFunction @@ -22,6 +23,8 @@ def gen_cmd(args: _Namespace) -> int: root = normalize(args.root) + sys.setrecursionlimit(args.recursion) + generate(root.resolve(), load(combine_if_not_absolute(root, args.config))) return 0 @@ -30,16 +33,25 @@ def gen_cmd(args: _Namespace) -> int: def add_gen_cmd(parser: _ArgumentParser) -> _CommandFunction: """Add gen-command arguments to its parser.""" + parser.add_argument( + "--recursion", + type=int, + default=10000, + help="recursion limit to set (default: '%(default)s')", + ) parser.add_argument( "-c", "--config", default=f"{PKG_NAME}.yaml", - help="configuration file to use", + help="configuration file to use (default: '%(default)s')", ) parser.add_argument( "-r", "--root", default=".", - help="root directory to use for relative paths", + help=( + "root directory to use for relative " + "paths (default: '%(default)s')" + ), ) return gen_cmd diff --git a/ifgen/environment/field.py b/ifgen/environment/field.py index 1df8660..953458c 100644 --- a/ifgen/environment/field.py +++ b/ifgen/environment/field.py @@ -31,7 +31,7 @@ def process_field( difference = expected - size assert difference >= 0, ( - f"({struct_name}.{field_name}) current={size} " + f"{difference} ({struct_name}.{field_name}) current={size} " f"!= expected={expected}" ) diff --git a/ifgen/svd/group/enums.py b/ifgen/svd/group/enums.py index 8c96b04..3374292 100644 --- a/ifgen/svd/group/enums.py +++ b/ifgen/svd/group/enums.py @@ -144,7 +144,7 @@ def translate_enums(enum: EnumeratedValues) -> EnumValues: enum_data: dict[str, Any] = {} value.handle_description(enum_data) - value_str: str = value.raw_data["value"] + value_str: str = value.raw_data["value"].lower() prefix = "" for possible_prefix in ("#", "0b", "0x"): @@ -154,7 +154,7 @@ def translate_enums(enum: EnumeratedValues) -> EnumValues: if prefix in ("#", "0b"): enum_data["value"] = int( - value_str[len(prefix) :].replace("X", "1"), 2 + value_str[len(prefix) :].replace("x", "1"), 2 ) elif prefix == "0x": enum_data["value"] = int(value_str[len(prefix) :], 16) diff --git a/ifgen/svd/group/fields.py b/ifgen/svd/group/fields.py index 4b887a4..1151a10 100644 --- a/ifgen/svd/group/fields.py +++ b/ifgen/svd/group/fields.py @@ -3,11 +3,16 @@ """ # built-in -from typing import Any, Iterable +from typing import Any, Iterable, Optional # internal from ifgen.svd.group.enums import ENUM_DEFAULTS, get_enum_name, translate_enums -from ifgen.svd.model.peripheral import Cluster, Register, RegisterData +from ifgen.svd.model.peripheral import ( + Cluster, + Register, + RegisterData, + register_groups, +) StructMap = dict[str, Any] StructField = dict[str, Any] @@ -58,12 +63,13 @@ def handle_cluster( cluster.children, structs, enums, peripheral, min_enum_members ) - # Too difficult due to padding (may need to comment out). - cluster_struct["expected_size"] = size + # Too difficult due to padding (may need to comment out). Padding within + # a cluster isn't currently handled. + # cluster_struct["expected_size"] = size cluster_struct.update(DEFAULT_STRUCT) - raw_name = cluster.name.replace("[%s]", "") + raw_name = sanitize_name(cluster.name) cluster_name = cluster.raw_data.get( "headerStructName", f"{raw_name}_instance" @@ -71,14 +77,16 @@ def handle_cluster( structs[cluster_name] = cluster_struct # This needs to be an array element somehow. Use a namespace? + # dimIncrement not handled. array_dim = int(cluster.raw_data.get("dim", 1)) + size *= array_dim result: StructField = { "name": raw_name, "type": cluster_name, # Too difficult due to padding (may need to comment out). - "expected_size": size, - "expected_offset": parse_offset(cluster.raw_data), + # "expected_size": size, + # "expected_offset": parse_offset(cluster.raw_data), } if array_dim > 1: result["array_length"] = array_dim @@ -125,7 +133,7 @@ def process_bit_fields( # Check if enum is unique. enum_name = get_enum_name( - f"{peripheral}_{register.name}_{name}".replace("[%s]", ""), + sanitize_name(f"{peripheral}_{register.name}_{name}"), peripheral, raw, ) @@ -137,6 +145,12 @@ def process_bit_fields( output["fields"] = result +def sanitize_name(name: str) -> str: + """Remove special characters from a name.""" + + return name.replace("[%s]", "").replace("%s", "") + + def handle_register( register: Register, register_map: RegisterMap, @@ -151,14 +165,15 @@ def handle_register( if "alternateRegister" in register.raw_data: return 0, {} - # Ensure that a correct result will be produced. - check_not_handled_fields(register.raw_data, ["alternateGroup"]) + # Currently provided in metadata output. + # check_not_handled_fields(register.raw_data, ["alternateGroup"]) + # dimIncrement not handled. array_dim = int(register.raw_data.get("dim", 1)) size = register.size * array_dim data = { - "name": register.name.replace("[%s]", ""), + "name": sanitize_name(register.name), "type": register.c_type, "expected_size": size, "expected_offset": parse_offset(register.raw_data), @@ -207,6 +222,68 @@ def handle_register( return size, data +def handle_register_group( + name: str, + registers: list[Register], + register_map: RegisterMap, + enums: EnumMap, + peripheral: str, + min_enum_width: int, +) -> tuple[int, StructField]: + """TODO.""" + + # Handle melding register data at some point (aggregate more bit fields). + del name + + return handle_register( + registers[0], register_map, enums, peripheral, min_enum_width + ) + + +def handle_item( + item: Register | Cluster, + structs: StructMap, + enums: EnumMap, + peripheral: str, + min_enum_width: int, + groups_handled: set[str], + register_map: RegisterMap, + groups: dict[str, list[Register]], + by_name: dict[str, str], +) -> tuple[int, Optional[StructField]]: + """Handle creating a struct field from a register or cluster.""" + + inst_size = 0 + field = None + + if isinstance(item, Cluster): + inst_size, field = handle_cluster( + item, structs, enums, peripheral, min_enum_width + ) + else: + # Handle register groups. + if item.name in by_name: + group = by_name[item.name] + if group not in groups_handled: + inst_size, field = handle_register_group( + group, + groups[group], + register_map, + enums, + peripheral, + min_enum_width, + ) + groups_handled.add(group) + + # Handle normal registers. + else: + inst_size, field = handle_register( + item, register_map, enums, peripheral, min_enum_width + ) + + return inst_size, field + + def struct_fields( registers: RegisterData, structs: StructMap, @@ -228,15 +305,27 @@ def struct_fields( if isinstance(item, Register): register_map[item.name] = item + groups = register_groups(registers) + by_name = {} + for group, regs in groups.items(): + for reg in regs: + by_name[reg.name] = group + groups_handled: set[str] = set() + for item in registers: - inst_size, field = ( - handle_cluster(item, structs, enums, peripheral, min_enum_width) - if isinstance(item, Cluster) - else handle_register( - item, register_map, enums, peripheral, min_enum_width - ) + inst_size, field = handle_item( + item, + structs, + enums, + peripheral, + min_enum_width, + groups_handled, + register_map, + groups, + by_name, ) - if inst_size > 0: + + if inst_size > 0 and field is not None: fields.append(field) size += inst_size diff --git a/ifgen/svd/model/__init__.py b/ifgen/svd/model/__init__.py index 5b559ba..d04a5aa 100644 --- a/ifgen/svd/model/__init__.py +++ b/ifgen/svd/model/__init__.py @@ -32,13 +32,23 @@ def metadata(self) -> dict[str, Any]: result["cpu"] = self.cpu.raw_data for name, peripheral in self.peripherals.items(): - result[name] = { + data = { "interrupts": [x.raw_data for x in peripheral.interrupts], "address_blocks": [ x.raw_data for x in peripheral.address_blocks ], } + # Add alternate group metadata. + groups = peripheral.register_groups() + if groups: + data["register_groups"] = { # type: ignore + key: [x.name for x in value] + for key, value in groups.items() + } + + result[name] = data + return result def assign_device(self, device: Device) -> None: diff --git a/ifgen/svd/model/field.py b/ifgen/svd/model/field.py index e0e1220..30ebee2 100644 --- a/ifgen/svd/model/field.py +++ b/ifgen/svd/model/field.py @@ -91,8 +91,11 @@ def ifgen_data(self) -> dict[str, Any]: elif "lsb" in self.raw_data: lsb = int(self.raw_data["lsb"]) msb = int(self.raw_data["msb"]) + elif "bitOffset" in self.raw_data: + lsb = int(self.raw_data["bitOffset"]) + msb = lsb + (int(self.raw_data["bitWidth"]) - 1) - assert lsb != -1 and msb != -1 + assert lsb != -1 and msb != -1, self.raw_data output["index"] = lsb diff --git a/ifgen/svd/model/peripheral.py b/ifgen/svd/model/peripheral.py index 2b90d44..b8ba24e 100644 --- a/ifgen/svd/model/peripheral.py +++ b/ifgen/svd/model/peripheral.py @@ -91,6 +91,11 @@ def size(self) -> int: """Get the size of this register in bytes.""" return self.bits // 8 + @property + def alternate_group(self) -> Optional[str]: + """Get this register's possible alternate group.""" + return self.raw_data.get("alternateGroup") + @property def access(self) -> str: """Get the access setting for this register.""" @@ -143,6 +148,22 @@ def string_keys(cls) -> Iterable[StringKeyVal]: ) +def register_groups(registers: RegisterData) -> dict[str, list[Register]]: + """Get groups of registers.""" + + result: dict[str, list[Register]] = {} + + for item in registers: + if isinstance(item, Register): + alt = item.alternate_group + if alt: + if alt not in result: + result[alt] = [] + result[alt].append(item) + + return result + + @dataclass class Peripheral(DerivedMixin): """A container for peripheral information.""" @@ -155,6 +176,10 @@ class Peripheral(DerivedMixin): registers: RegisterData + def register_groups(self) -> dict[str, list[Register]]: + """Get register groups.""" + return register_groups(self.registers) + def __eq__(self, other) -> bool: """Determine if two peripherals are equivalent.""" diff --git a/local/configs/python.yaml b/local/configs/python.yaml index c7712f0..d48f2a8 100644 --- a/local/configs/python.yaml +++ b/local/configs/python.yaml @@ -3,7 +3,7 @@ author_info: name: Vaughn Kottler email: vaughnkottler@gmail.com username: vkottler -versions: ["3.11", "3.12"] +versions: ["3.12"] systems: - macos-latest diff --git a/local/variables/package.yaml b/local/variables/package.yaml index a346345..14870c4 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 3 -minor: 2 -patch: 1 +minor: 3 +patch: 0 entry: ig diff --git a/tasks/conf.py b/tasks/conf.py index 7356deb..0aebbed 100644 --- a/tasks/conf.py +++ b/tasks/conf.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.4 -# hash=9f62028523c3b5a953733ca89dcc3018 +# hash=7d378a1752611508007a77d4ca39a5af # ===================================== """ A module for project-specific task registration. @@ -20,14 +20,9 @@ def audit_local_tasks() -> None: """Ensure that shared task infrastructure is present.""" local = Path(__file__).parent.joinpath("mklocal") - - # Also link a top-level file. top_level = local.parent.parent.joinpath("mklocal") - if not top_level.is_symlink(): - assert not top_level.exists() - top_level.symlink_to(local) - if local.is_symlink(): + if local.is_symlink() and top_level.is_symlink(): return # If it's not a symlink, it shouldn't be any other kind of file. @@ -48,6 +43,11 @@ def audit_local_tasks() -> None: # Create the link. local.symlink_to(vmklib) + # Also link a top-level file. + if not top_level.is_symlink(): + assert not top_level.exists() + top_level.symlink_to(local) + def register( manager: TaskManager, diff --git a/tests/commands/test_svd.py b/tests/commands/test_svd.py index 8dc9d25..78bb577 100644 --- a/tests/commands/test_svd.py +++ b/tests/commands/test_svd.py @@ -14,7 +14,7 @@ def test_svd_command_basic(): """Test the 'svd' command.""" with TemporaryDirectory() as tmpdir: - for svd in ["XMC4700", "rp2040"]: + for svd in ["XMC4700", "rp2040", "imxrt1176_cm7"]: # Generate configurations. assert ( ifgen_main(