From 669011280203ab16c85f4cd83da0fcc418f9537e Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Fri, 18 Sep 2026 00:11:15 +0200 Subject: [PATCH] [svd2variants] Merge bit fields that only moved inside their own register Source compatibility located a register by its name, since a CMSIS header abstracts the offset away into a structure member, but still located a bit field by its position. A header abstracts the position just as much into its _Pos and _Msk macros, so a bit field that keeps both its name and its register is now relocated instead of a conflict. This is the only difference between the two IWDG implementations: the early wake-up flag IWDG_SR_EWIF is bit 15 on the STM32WBA2 and bit 14 on the STM32WBA5, which the headers, the ST SVD files and RM0521 and RM0493 all agree on, so the flag alone split every STM32 into two watchdogs. A bit field that is permuted inside its register is not relocated, since its position is what it means: bit 9 of GTZC_TZSC_SECCFGR1 is USART2 on one device and WWDG on another. A permutation therefore renames every position it touches, while a relocation moves into bits that nobody else claims. Source variants go from 319 to 300 and similar from 282 to 268, binary is unchanged. The relocated bit fields are drawn at each of their positions and listed per variant in the explorer. --- docs/src/pipeline/svd2variants.md | 20 +++++++++- src/modm_data/svd2variants/analyze.py | 27 +++++++------ src/modm_data/svd2variants/merge.py | 56 ++++++++++++++++++++++++--- src/modm_data/svd2variants/write.py | 4 ++ 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/docs/src/pipeline/svd2variants.md b/docs/src/pipeline/svd2variants.md index 9d9506e..fd30cdf 100644 --- a/docs/src/pipeline/svd2variants.md +++ b/docs/src/pipeline/svd2variants.md @@ -75,11 +75,29 @@ addressed, so the analysis runs in three modes: - `source`: elements are located by the **name** of their register, so a register may move as long as it keeps its name and its layout. This is what code written against a CMSIS header needs, since the structure member - abstracts the offset away. + abstracts the offset away. A header abstracts the *position* of a bit field + just as much, so a bit field may move too, see + `modm_data.svd2variants.merge.relocations`. - `similar`: like `source`, but differences that only exist in the documentation are tolerated, see `modm_data.svd2variants.merge.documentation_only`. This is a heuristic for how many implementations are *essentially* the same. +A bit field that keeps both its name and its register but sits at another +position is *relocated* rather than in conflict, since a header hides the +position behind its `_Pos` and `_Msk` macros exactly as it hides the offset of +a register behind a structure member. This is the only difference between the +two `IWDG` implementations: the early wake-up flag `IWDG_SR_EWIF` is bit 15 on +the STM32WBA2 and bit 14 on the STM32WBA5, which the headers, the ST SVD files +and RM0521 and RM0493 all agree on. Without this rule the flag alone splits +every STM32 into two watchdogs. + +A bit field that is *permuted* inside its register is not relocated, since its +position is what it means. Bit 9 of `GTZC_TZSC_SECCFGR1` is `USART2` on one +device and `WWDG` on another, so a permutation renames every position it +touches, while a relocation moves into bits that nobody else claims. The +relocated fields are drawn at each of their positions in the explorer and +listed per variant. + Neither mode is strictly coarser than the other. Source compatibility merges the two GPIO implementations into one, because the STM32F1 `IDR` at `0x08` and the `IDR` at `0x10` of every other device have the same bit fields. But it diff --git a/src/modm_data/svd2variants/analyze.py b/src/modm_data/svd2variants/analyze.py index 8ae047a..42d6455 100644 --- a/src/modm_data/svd2variants/analyze.py +++ b/src/modm_data/svd2variants/analyze.py @@ -62,6 +62,7 @@ class Variant: members: list["Member"] = _field(default_factory=list, repr=False) elements: ElementMap = _field(default_factory=ElementMap, repr=False) renames: list[Rename] = _field(default_factory=list, repr=False) + relocated: list[Conflict] = _field(default_factory=list, repr=False) core: set[LocationKey] = _field(default_factory=set, repr=False) features: list[Feature] = _field(default_factory=list, repr=False) @@ -81,10 +82,11 @@ def devices(self) -> set[str]: def names(self) -> set[str]: return {instance.name for member in self.members for instance in member.instances} - def add(self, member: "Member", renames: list[Rename]): + def add(self, member: "Member", renames: list[Rename], relocated: list[Conflict]): self.members.append(member) self.elements.update(member.elements) self.renames += renames + self.relocated += relocated def described(self, mode: str) -> dict[LocationKey, tuple[str, str, str, str]]: """:return: the kind, register name, name and description of every element.""" @@ -182,19 +184,20 @@ def variants_of( group.ignored = sorted({r.name for i in instances for r in i.shape.registers if ignored.match(r.name)}) for shape, shape_instances in _shapes(instances, ignored).items(): member = Member(shape, ElementMap.from_shape(shape, mode), shape_instances) - best, shared, renames = None, -1, [] + best, shared, renames, relocated = None, -1, [], [] for variant in group.variants: - difference = compare(variant.elements, member.elements, widening, mode == "similar") + difference = compare(variant.elements, member.elements, widening, mode) if not difference.compatible: continue common = len(set(variant.elements.names) & set(member.elements.names)) if common > shared: - best, shared, renames = variant, common, difference.renames + best, shared = variant, common + renames, relocated = difference.renames, difference.relocated if best is None: best = Variant(name) group.variants.append(best) - renames = [] - best.add(member, renames) + renames, relocated = [], [] + best.add(member, renames, relocated) # A merged map collects aliases that are later evidence for a rename, so a # conflict that kept two shapes apart may be gone once both variants are @@ -204,20 +207,22 @@ def variants_of( while merged: merged = False for small in sorted(group.variants, key=lambda v: len(v.instances)): - best, shared, renames = None, -1, [] + best, shared, renames, relocated = None, -1, [], [] for large in group.variants: if large is small: continue - difference = compare(large.elements, small.elements, widening, mode == "similar") + difference = compare(large.elements, small.elements, widening, mode) if not difference.compatible: continue common = len(set(large.elements.names) & set(small.elements.names)) if common > shared: - best, shared, renames = large, common, difference.renames + best, shared = large, common + renames, relocated = difference.renames, difference.relocated if best is not None: for member in small.members: - best.add(member, []) + best.add(member, [], []) best.renames += small.renames + renames + best.relocated += small.relocated + relocated group.variants.remove(small) merged = True break @@ -233,6 +238,6 @@ def conflicts_between(variants: list[Variant], mode: str = "binary") -> dict[tup conflicts = {} for left in range(len(variants)): for right in range(left + 1, len(variants)): - difference = compare(variants[left].elements, variants[right].elements, similar=mode == "similar") + difference = compare(variants[left].elements, variants[right].elements, mode=mode) conflicts[(left, right)] = difference.conflicts return conflicts diff --git a/src/modm_data/svd2variants/merge.py b/src/modm_data/svd2variants/merge.py index 59d16f2..5307931 100644 --- a/src/modm_data/svd2variants/merge.py +++ b/src/modm_data/svd2variants/merge.py @@ -53,7 +53,9 @@ register may move as long as it keeps its name and its layout. This is what code written against a CMSIS header needs, since the structure member abstracts the offset away. A renamed register is a different register here, - and simply becomes an optional feature of the merged map. + and simply becomes an optional feature of the merged map. A header + abstracts the position of a bit field just as much, so a bit field that + keeps its name and its register may move too, see `relocations`. - `similar`: like `source`, but conflicts that only exist in the documentation are tolerated, see `documentation_only`. """ @@ -366,6 +368,8 @@ class Difference: conflicts: list[Conflict | Overlap | Reuse] = _field(default_factory=list) widened: list[Conflict] = _field(default_factory=list) + relocated: list[Conflict] = _field(default_factory=list) + """Bit fields that moved inside their own register, see `relocations`.""" tolerated: list = _field(default_factory=list) """Conflicts that only differ in the documentation, see `documentation_only`.""" renames: list[Rename] = _field(default_factory=list) @@ -428,6 +432,41 @@ def _related(names: set[NameKey], others: set[NameKey], width: int) -> bool: return False +def relocations(conflicts: list, renames: list[Rename]) -> tuple[list, list]: + """ + Separates the conflicts that are only a bit field moving inside its own + register from the ones that are a different register map. + + Source compatibility means that code written against a CMSIS header keeps + working, and a header abstracts the position of a bit field into its + `_Pos` and `_Msk` macros exactly as much as it abstracts the offset of a + register into a structure member. A bit field that keeps both its name and + its register is therefore merely *relocated*, for example the `IWDG_SR` + early wake-up flag `EWIF`, which sits in bit 15 on the STM32WBA2 and in + bit 14 on the STM32WBA5. + + A bit field that is *permuted* inside its register is not relocated, since + its position is what it means: the `GTZC_TZSC_SECCFGR1` bit of every + peripheral differs per device, so bit 9 is `USART2` on one device and + `WWDG` on another. A permutation therefore renames every position it + touches, while a relocation moves into bits that nobody else claims. + + :return: the conflicts that are a different register map and the ones that + are only a relocation. + """ + renamed = {rename.location for rename in renames} + silicon, relocated = [], [] + for conflict in conflicts: + if isinstance(conflict, Conflict) and conflict.name[0] == "F" and conflict.kind == "moved": + locations = conflict.locations | conflict.other + same_register = len({location[1] for location in locations}) == 1 + if same_register and renamed.isdisjoint(locations): + relocated.append(conflict) + continue + silicon.append(conflict) + return silicon, relocated + + def documentation_only(conflicts: list) -> tuple[list, list]: """ Separates the conflicts that are only differences in how a register is @@ -491,7 +530,7 @@ def decomposes(outer: LocationKey) -> bool: return silicon, documentation -def compare(left: ElementMap, right: ElementMap, widening: bool = True, similar: bool = False) -> Difference: +def compare(left: ElementMap, right: ElementMap, widening: bool = True, mode: str = "binary") -> Difference: """ Compares two register maps, see the module documentation for the rules. @@ -499,8 +538,11 @@ def compare(left: ElementMap, right: ElementMap, widening: bool = True, similar: :param right: the register map to compare. :param widening: whether a bit field that only grew into the reserved bits above it is an optional extension instead of a conflict. - :param similar: whether differences in the documentation are tolerated, see - `documentation_only`. + :param mode: `binary` locates elements by register address, `source` by + register name and also tolerates a bit field that moved inside + its own register, see `relocations`, and `similar` also + tolerates differences in the documentation, see + `documentation_only`. :return: the differences between both maps. """ conflicts, renames = left.compare(right) @@ -511,14 +553,18 @@ def compare(left: ElementMap, right: ElementMap, widening: bool = True, similar: if widening: widened = [conflict for conflict in conflicts if conflict.kind == "widened"] conflicts = [conflict for conflict in conflicts if conflict.kind != "widened"] + relocated = [] + if mode != "binary": + conflicts, relocated = relocations(conflicts, renames) tolerated = [] - if similar: + if mode == "similar": conflicts, tolerated = documentation_only(conflicts) # Two maps without a single common register describe unrelated hardware disjoint = bool(left.names) and bool(right.names) and left.scopes.isdisjoint(right.scopes) return Difference( conflicts=conflicts, widened=widened, + relocated=relocated, tolerated=tolerated, renames=renames, only_left=set(left.names) - set(right.names), diff --git a/src/modm_data/svd2variants/write.py b/src/modm_data/svd2variants/write.py index ee377b3..5b22521 100644 --- a/src/modm_data/svd2variants/write.py +++ b/src/modm_data/svd2variants/write.py @@ -25,6 +25,8 @@ def _names(elements: ElementMap, location: tuple) -> list[str]: def _register_map(variant: Variant, features: dict[tuple, int], mode: str) -> list[dict]: """The merged register map of the variant, with every element tagged by its feature.""" + # A bit field that moved inside its own register appears at each of its positions + relocated = {location for conflict in variant.relocated for location in conflict.locations | conflict.other} registers = {} descriptions = {} offsets = defaultdict(set) @@ -57,6 +59,7 @@ def _register_map(variant: Variant, features: dict[tuple, int], mode: str) -> li "names": _names(variant.elements, flocation), "feature": features.get(flocation, -1), "description": field.description, + **({"relocated": True} if flocation in relocated else {}), }, ) for location, entry in registers.items(): @@ -98,6 +101,7 @@ def _variant(variant: Variant, index: int, devices: dict[str, int], conflicts: d for feature in variant.features ], "renames": sorted({str(rename) for rename in variant.renames}), + "relocated": sorted({str(conflict) for conflict in variant.relocated}), "conflicts": { str(other): [str(conflict) for conflict in found] for (left, other), found in conflicts.items()