Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/src/pipeline/svd2variants.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 16 additions & 11 deletions src/modm_data/svd2variants/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
56 changes: 51 additions & 5 deletions src/modm_data/svd2variants/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
"""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -491,16 +530,19 @@ 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.

:param left: the register map to compare against, may be a merged map.
: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)
Expand All @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions src/modm_data/svd2variants/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand Down
Loading