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()