Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright (c) 2026 TiaC Systems
# SPDX-License-Identifier: Apache-2.0
"""Shield parsing: a `.shield` translation unit -> model.Shield.
Loader-side validation done here:
- shield,plugs names a known connector type
- bus proxy nodes are allowed by the plug binding
- position references target one of THIS shield's plugs and exist in
that plug's connector type
- exactly one of reg / shield,addr-from on addressable-bus devices
(forgot-vs-deferred: address authority rule)
- authored reg matches the unit-address; symbolic unit-addresses are
linted against the addr-from target
**ONE authored form**: N plug nodes, N >= 1, each a child of the
template with `compatible = "shield,plug"` and its own `shield,plugs`
naming that plug's connector type. The child's NODE NAME is the slot
name (shield-owned); `plug` is the conventional name for a shield with
one, and carries no special meaning. Plurality is a COUNT, never an
authored form -- which is what every consumer below this module always
tested (`len(shield.plugs) > 1`).
Placement, the same rule at either count:
bus groups NEST UNDER their owning plug node -- that nesting is what
dissolves the sibling-name collision two same-kind buses would
otherwise have; a bus-shaped group at template level is rejected.
plain (non-bus) device groups stay at TEMPLATE level, plug-agnostic --
their devices' refs each carry their own plug by phandle ("one of
this shield's plugs"); a plain group nested under a plug is
rejected. With exactly one plug, such a device is attributed to it;
with more, to none.
`pads` and `config` are template-level too, whatever the plug count:
they are shield-level facts. Promotion and routing jumpers are
refused above one plug -- straps are unaffected (bus-scoped, not
plug-scoped). A carrier of any plug count may declare an exposed
socket: its gpio-map rows and socket,<bus> properties each resolve
through one of the carrier's plugs, exactly like a device's own
cross-plug refs.
**A plug node declares no cell counts.** `#gpio-cells`/`#pwm-cells`/
`#io-channel-cells` are refused there: every value the corpus ever gave
one was `_FUNCTION_DEFAULT_CELLS` restated, the node is never emitted so
nothing validates it, and a wrong value silently changed a reference's
arity. The `_ncells` mechanism stays for the nodes that genuinely
differ -- a routing jumper's own `<1>`.
**Diagnostics are RETURN values**: every parse function below returns
(value, diagnostics) rather than writing into a diags parameter handed
in from outside -- the local list a function builds and returns is not
the banned accumulator shape (nothing outside this module ever mutates
one), it is composition-by-return exactly like every other rigc module.
**The cpp/unit-test seam**: everything here operates on a `dtlib.DT`
that ALREADY EXISTS -- it never calls cpp itself -- so it is
unit-testable directly against a synthetic, cpp-free `.dts` text parsed
with `dtsio.get_dtlib().DT(path)`."""
from __future__ import annotations
from typing import Any
from ..buskind import BUS_PROP_RE as _BUS_PROP_RE
from ..buskind import CS_POOL_PROP_RE as _CS_POOL_PROP_RE
from ..buskind import bus_kind_of, is_bus_kind
from ..diag import Diagnostic, error, warning
from ..dtsio import get_dtlib, render_prop, src_of, words
from ..model import ConnectorType, Device, ExposedSocket, FunctionRef, Jumper, Pad, Shield, Strap
#: socket,<kind> or socket,<kind>-<role> -- an exposed socket's own bus
#: vocabulary is the qualified multi-bus pattern, the same shared pattern
#: board/project.py/registry.py read off their own inputs (a connector type's
#: bus names mean the same thing on either side of a pass-through) --
#: see buskind.py for the regex itself and why it lives there.
#:
#: socket,<kind>-<role>-cs-pool -- a named bus's own authored cs-pool
#: override on an exposed socket node, keyed the same qualified way. The
#: legacy, role-less "socket,cs-pool" (every carrier's own spelling
#: today) is handled separately below: it carries no kind in its own
#: name, so it is not this pattern's concern.
#: template-level group names that are NOT device groups. `plug` is
#: deliberately absent: a plug node is recognized structurally by its
#: `compatible` (`_is_plug_node`) and skipped by identity, so reserving
#: the name buys nothing. What keeps groups an author nests under a plug
#: from leaking into the template-level walk is the per-plug walk over
#: `nodes_by_slot` below, not this set.
_RESERVED = {"pads", "config"}
_MODEL_PROPS = {
"reg",
"compatible",
"shield,addr-from",
"shield,cs-position",
"shield,collect",
"shield,params",
"shield,param-includes",
}
#: path -> (slot name, connector type) for every plug this shield declares
#: -- one entry per plug node, at any count -- the map
#: `_parse_pos_ref` resolves a phandle against to decide which slot a
#: reference names and to validate its position, so a reference may name
#: "one of the shield's plugs" rather than assuming there is only one.
PlugsByPath = dict[str, tuple[str, ConnectorType | None]]
def parse_shields(
dt,
types: dict[str, ConnectorType],
) -> tuple[dict[str, Shield], list[Diagnostic]]:
"""Every `.shield` file has exactly one shield node under the
`shield-templates` wrapper (a marker that distinguishes a TEMPLATE
from a real Zephyr shield's applied `<name>.overlay`)."""
shields: dict[str, Shield] = {}
diags: list[Diagnostic] = []
root = dt.root.nodes.get("shield-templates")
if root is None:
return shields, diags
for node in root.nodes.values():
shield, d = _parse_shield(node, types)
diags += d
shields[shield.name] = shield
return shields, diags
def _is_plug_node(g) -> bool:
return "compatible" in g.props and g.props["compatible"].to_string() == "shield,plug"
def _is_exposed_node(g) -> bool:
return "compatible" in g.props and g.props["compatible"].to_string().startswith("socket,")
def _require_label(node, kind: str, shield_name: str) -> tuple[str, list[Diagnostic]]:
"""The DTS label a rig->shield reference (`config:`/`wires:`/
`socket:`) resolves against, for a device, pad, strap, jumper or
exposed socket. A node with none is refused rather than silently
addressed by its own node name: falling back to the node name would
reopen exactly the two-spellings ambiguity (label vs. name) a label
exists to remove.
Returns (label, diagnostics): label is `node.labels[0]` when
present; otherwise it is the node's own name, a placeholder that
lets the caller still build a well-formed model object -- the
accompanying error means the run fails regardless of what value
ends up here -- and diagnostics carries the one error naming the
node and what it needs."""
if node.labels:
return node.labels[0], []
return node.name, [
error(
"lang-shield-label",
f"{kind} '{node.name}' of shield '{shield_name}' has no DTS "
"label -- rig-facing references (config:/wires:/socket:) "
"resolve by label, never by node name -- give it one",
(src_of(node),),
)
]
def _parse_shield(
node,
types: dict[str, ConnectorType],
) -> tuple[Shield, list[Diagnostic]]:
diags: list[Diagnostic] = []
label = node.labels[0] if node.labels else node.name
plugs_prop = node.props.get("shield,plugs")
plug_children = [c for c in node.nodes.values() if _is_plug_node(c)]
if plugs_prop is not None:
diags.append(
error(
"lang-shield-plug",
f"shield '{node.name}' declares shield,plugs on the TEMPLATE "
"node -- that spelling is retired: move it onto the plug node "
"itself, beside a 'compatible = \"shield,plug\"', so one plug "
"and many are declared the same way",
(src_of(plugs_prop),),
)
)
return Shield(name=node.name, label=label, plugs={}, src=src_of(node)), diags
shield = Shield(name=node.name, label=label, plugs={}, src=src_of(node))
shield.by_path[node.path] = shield
if not plug_children:
diags.append(
error(
"lang-shield-plug",
f"shield '{shield.name}' declares no 'shield,plug'-compatible "
"child -- the plug is the position reference frame, and a "
"shield names its connector type on it",
(src_of(node),),
)
)
return shield, diags
ctypes_by_slot, nodes_by_slot, plugs_by_path, d = _parse_plugs(plug_children, shield, types)
diags += d
#: the slot a plug-agnostic (plain-group) device belongs to: the only
#: one when there IS only one, else none. Replaces the retired single
#: form's hardcoded `"plug"`, which was right only for a shield whose
#: plug node happened to carry that name.
only_slot = next(iter(shield.plugs)) if len(shield.plugs) == 1 else None
# two-phase: pads/config first -- devices reference straps
# (shield,addr-from) regardless of group order in the file. Both stay
# TEMPLATE-LEVEL regardless of plurality (shield-level facts) -- a
# routing jumper is the one exception: its position domain has no
# plug axis, so a plural shield declaring one is refused rather than
# silently mishandled; straps are address-domain and bus-scoped,
# unaffected either way.
diags += _parse_pads_and_config(node, shield)
# device groups FIRST -- an exposed socket may reference a device as
# its scope root (a mux channel), so the device must be in by_path
# already.
# template-level groups: plug-agnostic (plain groups) only -- a group
# whose name is bus-shaped is rejected, since bus groups nest under
# their owning plug (the placement rule), never sit at template level.
# This holds at ONE plug exactly as at many.
diags += _parse_template_groups(
node, shield, plug_children, ctypes_by_slot, plugs_by_path, only_slot
)
# each plug's OWN bus groups, matched against ITS OWN connector
# type's bus_proxies -- the plug binding, structural. A group nested
# under a plug that is NEITHER a bus this plug's ctype allows NOR
# bus-kind-named at all is a plain group in the wrong place: the
# placement rule keeps plain groups at template level (plug-agnostic),
# so nesting one under a plug is rejected here rather than silently
# recorded with Device.plug = slot -- the same symmetry the
# template-level walk above applies to a misplaced BUS group (its own
# lang-shield-proxy branch).
diags += _parse_plug_groups(shield, nodes_by_slot, ctypes_by_slot, plugs_by_path)
# then re-exported sockets, pass-through or scope creation -- a
# plural shield may declare one too: each gpio-map row and each
# socket,<bus> resolves through ONE of the carrier's plugs, per
# plugs_by_path, exactly as a device's own cross-plug refs do.
diags += _parse_exposed_sockets(node, shield, plug_children, plugs_by_path, types)
return shield, diags
def _parse_plugs(
plug_children,
shield: Shield,
types: dict[str, ConnectorType],
) -> tuple[dict[str, ConnectorType | None], dict[str, Any], PlugsByPath, list[Diagnostic]]:
"""Every plug child in authoring order: validates its cell counts and
resolves its connector type, and records `shield.plugs[slot]` (in that
same order -- shield.plugs' own ordering contract) alongside the three
lookup maps the rest of `_parse_shield` walks against."""
diags: list[Diagnostic] = []
ctypes_by_slot: dict[str, ConnectorType | None] = {}
nodes_by_slot: dict[str, Any] = {}
plugs_by_path: PlugsByPath = {}
for child in plug_children:
slot = child.name
for cells in _FUNCTION_CELLS.values():
if cells in child.props:
diags.append(
error(
"lang-shield-plug-cells",
f"shield '{shield.name}': plug '{slot}' declares "
f"{cells} -- a plug node declares no cell counts. A "
"position reference through a plug carries the generic "
"count for its function (2 gpio, 3 pwm, 1 adc); only a "
"node that genuinely differs, such as a routing "
"jumper, says so",
(src_of(child.props[cells]),),
)
)
type_v = child.props.get("shield,plugs")
if type_v is None:
diags.append(
error(
"lang-shield-type",
f"shield '{shield.name}': plug '{slot}' declares no "
"shield,plugs of its own -- every plug names its own "
"connector type",
(src_of(child),),
)
)
continue
type_name = type_v.to_string()
ctype = types.get(type_name)
if ctype is None:
diags.append(
error(
"lang-shield-type",
f"shield '{shield.name}': plug '{slot}' plugs unknown "
f"connector type '{type_name}'\nknown types: "
f"{', '.join(sorted(types))}",
(src_of(type_v),),
)
)
shield.plugs[slot] = type_name
ctypes_by_slot[slot] = ctype
nodes_by_slot[slot] = child
plugs_by_path[child.path] = (slot, ctype)
return ctypes_by_slot, nodes_by_slot, plugs_by_path, diags
def _parse_pads_and_config(node, shield: Shield) -> list[Diagnostic]:
"""The `pads` and `config` template-level groups, in that authoring
order. A `config` child with `shield,position-domain` is a routing
jumper (refused above one plug); every other `config` child is a
strap."""
diags: list[Diagnostic] = []
for group in node.nodes.values():
if group.name == "pads":
for pnode in group.nodes.values():
pad, d = _parse_pad(pnode, shield.name)
diags += d
shield.pads[pad.name] = pad
shield.by_path[pnode.path] = pad
elif group.name == "config":
for snode in group.nodes.values():
if "shield,position-domain" in snode.props:
if len(shield.plugs) > 1:
diags.append(
error(
"lang-shield-plurality",
f"shield '{shield.name}': a shield with more "
f"than one plug cannot declare a routing "
f"jumper ('{snode.name}') -- the position "
"domain has no plug axis",
(src_of(snode),),
)
)
continue
jmp, d = _parse_jumper(snode, shield.name)
diags += d
shield.jumpers[jmp.name] = jmp
shield.by_path[snode.path] = jmp
else:
strap, d = _parse_strap(snode, shield.name)
diags += d
shield.straps[strap.name] = strap
shield.by_path[snode.path] = strap
return diags
def _parse_template_groups(
node,
shield: Shield,
plug_children,
ctypes_by_slot: dict[str, ConnectorType | None],
plugs_by_path: PlugsByPath,
only_slot: str | None,
) -> list[Diagnostic]:
"""Plug-agnostic device groups at template level. A bus-shaped group
here is rejected -- bus groups nest under their owning plug -- but its
devices are still parsed, so a misplaced group's own diagnostics don't
mask its devices' problems."""
diags: list[Diagnostic] = []
for group in node.nodes.values():
if group.name in _RESERVED or _is_exposed_node(group) or group in plug_children:
continue
if bus_kind_of(group.name) is not None:
candidates = sorted(
slot for slot, ct in ctypes_by_slot.items() if ct and group.name in ct.bus_proxies
)
diags.append(
error(
"lang-shield-proxy",
f"shield '{shield.name}' has a '{group.name}' bus proxy "
"at template level -- bus groups nest under their owning "
"plug" + (f" — candidate plugs: {', '.join(candidates)}" if candidates else ""),
(src_of(group),),
)
)
for dnode in group.nodes.values():
dev, d = _parse_device(dnode, shield, plugs_by_path, None, group.name, only_slot)
diags += d
shield.devices.append(dev)
shield.by_path[dnode.path] = dev
return diags
def _parse_plug_groups(
shield: Shield,
nodes_by_slot: dict[str, Any],
ctypes_by_slot: dict[str, ConnectorType | None],
plugs_by_path: PlugsByPath,
) -> list[Diagnostic]:
"""Each plug's own nested groups, matched against its own connector
type's bus_proxies. A group that is neither an allowed bus proxy nor
bus-kind-named at all is a plain group in the wrong place; either way
its devices are still parsed and attributed to this plug."""
diags: list[Diagnostic] = []
for slot, plug_node in nodes_by_slot.items():
ctype = ctypes_by_slot[slot]
for group in plug_node.nodes.values():
bus = group.name if ctype and group.name in ctype.bus_proxies else None
if bus is None:
if ctype and bus_kind_of(group.name) is not None:
diags.append(
error(
"lang-shield-proxy",
f"shield '{shield.name}': plug '{slot}' has a "
f"'{group.name}' bus proxy but the '{ctype.name}' "
f"plug binding allows only: "
f"{', '.join(ctype.bus_proxies)}",
(src_of(group),),
)
)
else:
diags.append(
error(
"lang-shield-proxy",
f"shield '{shield.name}': plug '{slot}' has a "
f"'{group.name}' group nested under it -- plain "
"device groups belong at template level "
"(plug-agnostic; their devices' refs each carry "
"their own plug by phandle)",
(src_of(group),),
)
)
for dnode in group.nodes.values():
dev, d = _parse_device(
dnode,
shield,
plugs_by_path,
bus,
None if bus else group.name,
slot if bus else None,
)
diags += d
shield.devices.append(dev)
shield.by_path[dnode.path] = dev
return diags
def _parse_exposed_sockets(
node, shield: Shield, plug_children, plugs_by_path: PlugsByPath, types: dict[str, ConnectorType]
) -> list[Diagnostic]:
"""Every re-exported socket at template level, in authoring order."""
diags: list[Diagnostic] = []
for group in node.nodes.values():
if group.name in _RESERVED or not _is_exposed_node(group) or group in plug_children:
continue
exp, d = _parse_exposed(group, plugs_by_path, shield, types)
diags += d
shield.exposes[exp.name] = exp
shield.by_path[group.path] = exp
return diags
def _parse_device_addressing(
node,
shield: Shield,
bus,
unit: str,
) -> tuple[int | None, str | None, list[Diagnostic]]:
"""reg / shield,addr-from / unit-address, together: the address
authority rule requires exactly one of reg / addr-from on an
addressable bus, and the unit-address (when present) must agree with
whichever of the two actually authors the address.
Returns (reg, addr_from, diagnostics): addr_from is the strap's own
name when shield,addr-from resolves, else None."""
diags: list[Diagnostic] = []
reg = node.props["reg"].to_num() if "reg" in node.props else None
addr_from = None
if "shield,addr-from" in node.props:
target = node.props["shield,addr-from"].to_node()
strap = shield.by_path.get(target.path)
if not isinstance(strap, Strap):
diags.append(
error(
"lang-addr-from",
f"shield,addr-from on '{shield.name}/{node.name}' does not "
"point at a config strap of this shield",
(src_of(node.props["shield,addr-from"]),),
)
)
else:
addr_from = strap.name
# exactly-one-of rule: forgot-reg is detectable, deferred is explicit
if is_bus_kind(bus, "i2c") and (reg is None) == (addr_from is None):
which = "both" if reg is not None else "neither"
diags.append(
error(
"lang-addr-authority",
f"device '{shield.name}/{node.name}' on an addressable bus "
f"carries {which} of reg / shield,addr-from — exactly one "
"is required (address authority rule)",
(src_of(node),),
)
)
# authored reg == unit-address (validated); symbolic
# unit-address is a documentation marker linted against the addr-from
# target
if unit and reg is not None:
try:
if int(unit, 16) != reg:
diags.append(
error(
"lang-unit-addr",
f"'{node.name}': unit-address @{unit} != authored reg "
f"<{reg:#x}> — they must be a matching pair",
(src_of(node),),
)
)
except ValueError:
diags.append(
error(
"lang-unit-addr",
f"'{node.name}': symbolic unit-address with authored reg "
"— symbolic markers are for deferred addresses only",
(src_of(node),),
)
)
elif unit and addr_from and unit.replace("-", "_") != addr_from.replace("-", "_"):
diags.append(
warning(
"lang-unit-addr",
f"'{node.name}': symbolic unit-address @{unit} does not match "
f"its resolver '{addr_from}' (lint: marker must name the "
"addr-from target)",
(src_of(node),),
)
)
return reg, addr_from, diags
def _parse_device(
node, shield: Shield, plugs_by_path: PlugsByPath, bus, group, dev_plug: str | None
) -> tuple[Device, list[Diagnostic]]:
diags: list[Diagnostic] = []
name, _, unit = node.name.partition("@")
compat = node.props["compatible"].to_string() if "compatible" in node.props else None
reg, addr_from, d = _parse_device_addressing(node, shield, bus, unit)
diags += d
cs_position = None
if "shield,cs-position" in node.props:
cs_position = node.props["shield,cs-position"].to_num()
collect = None
if "shield,collect" in node.props:
collect = node.props["shield,collect"].to_string()
declared_params: list[str] = []
if "shield,params" in node.props:
declared_params = list(node.props["shield,params"].to_strings())
# The vocabulary declared_params' own tokens resolve against: a
# device-node property, sibling to shield,params, since the header
# is a contract of the parameter, not an accident of what the
# template happened to #include.
declared_param_includes: list[str] = []
if "shield,param-includes" in node.props:
declared_param_includes = list(node.props["shield,param-includes"].to_strings())
label, d = _require_label(node, "device", shield.name)
diags += d
dev = Device(
name=name,
label=label,
compatible=compat,
bus=bus,
group=group,
reg=reg,
addr_from=addr_from,
cs_position=cs_position,
plug=dev_plug,
collect=collect,
declared_params=declared_params,
declared_param_includes=declared_param_includes,
src=src_of(node),
)
for prop in node.props.values():
if prop.name in _MODEL_PROPS or prop.name == "phandle":
continue
fn = _function_of(prop.name)
if fn is not None:
refs, d = _parse_pos_ref(prop, fn, shield, plugs_by_path)
dev.function_refs.extend(refs)
diags += d
continue
dtlib = get_dtlib()
if prop.type is dtlib.Type.PHANDLES_AND_NUMS:
diags.append(
warning(
"lang-prop",
f"phandle property '{prop.name}' of "
f"'{shield.name}/{node.name}' is not a recognized function "
"ref (gpios/pwms/io-channels) — dropped",
(src_of(prop),),
)
)
continue
rendered = render_prop(prop)
if rendered is None:
diags.append(
warning(
"lang-prop",
f"property '{prop.name}' of '{shield.name}/{node.name}' "
"has a type the prototype cannot pass through — dropped "
"from output",
(src_of(prop),),
)
)
elif prop.name != "compatible":
dev.extra_props.append((prop.name, rendered))
if compat:
dev.extra_props.insert(0, ("compatible", f'compatible = "{compat}";'))
return dev, diags
_FUNCTION_CELLS = {"gpio": "#gpio-cells", "pwm": "#pwm-cells", "adc": "#io-channel-cells"}
_FUNCTION_DEFAULT_CELLS = {"gpio": 2, "pwm": 3, "adc": 1}
def _function_of(prop_name: str) -> str | None:
"""Which function-nexus a property resolves through, by name."""
if prop_name == "gpios" or prop_name.endswith("-gpios"):
return "gpio"
if prop_name == "pwms":
return "pwm"
if prop_name == "io-channels":
return "adc"
return None
def _parse_pos_ref(
prop,
function: str,
shield: Shield,
plugs_by_path: PlugsByPath,
) -> tuple[list[FunctionRef], list[Diagnostic]]:
"""Nexus-aware position reference, per function. A plug is a
multi-function nexus: a claim reads the plug's #<fn>-cells cells.
Granularity is PER-REFERENCE: the phandle names WHICH of the
shield's plugs this claim resolves through, independent of which
plug the surrounding device's own bus binds to -- a cross-plug
reference is zero new syntax, just a wider set of valid targets."""
refs: list[FunctionRef] = []
diags: list[Diagnostic] = []
cells = words(prop)
dt = prop.node.dt
i = 0
while i < len(cells):
target = dt.phandle2node.get(cells[i])
ncells = _ncells(target, function)
args = cells[i + 1 : i + 1 + ncells]
i += 1 + ncells
if target is None or len(args) < ncells:
diags.append(
error(
"lang-pos-ref",
f"'{prop.name}' has a malformed {function} entry",
(src_of(prop),),
)
)
return refs, diags
elem = shield.by_path.get(target.path)
plug_entry = plugs_by_path.get(target.path)
if plug_entry is not None: # fixed position
slot, ctype = plug_entry
pos = args[0]
ok, d = _valid_position(prop, pos, ctype)
diags += d
if not ok:
continue
if function == "gpio":
refs.append(
FunctionRef(
prop=prop.name,
position=pos,
flags=args[1],
function="gpio",
src=src_of(prop),
plug=slot,
)
)
elif function == "pwm":
refs.append(
FunctionRef(
prop=prop.name,
position=pos,
period=args[1],
flags=args[2],
function="pwm",
src=src_of(prop),
plug=slot,
)
)
else: # adc
refs.append(
FunctionRef(
prop=prop.name,
position=pos,
flags=0,
function="adc",
src=src_of(prop),
plug=slot,
)
)
elif function == "gpio" and isinstance(elem, Jumper): # deferred position
flags = args[0] if args else 0
refs.append(
FunctionRef(
prop=prop.name,
position=None,
flags=flags,
jumper=elem.name,
function="gpio",
src=src_of(prop),
)
)
else:
where = target.path if target else "?"
if len(plugs_by_path) > 1:
what = "one of this shield's plug nodes"
else:
what = "THIS shield's plug node"
diags.append(
error(
"lang-pos-ref",
f"'{prop.name}' must reference {what} (fixed position)"
+ ("" if function != "gpio" else " or one of its routing jumpers")
+ f" — it points at {where}",
(src_of(prop),),
)
)
return refs, diags
def _ncells(node, function: str) -> int:
prop = _FUNCTION_CELLS[function]
if node is not None and prop in node.props:
return node.props[prop].to_num()
return _FUNCTION_DEFAULT_CELLS[function]
def _valid_position(prop, pos: int, ctype) -> tuple[bool, list[Diagnostic]]:
if ctype and pos not in ctype.index2name:
return False, [
error(
"lang-position",
f"'{prop.name}' claims position index {pos}, which does not "
f"exist on connector type '{ctype.name}'",
(src_of(prop),),
)
]
if ctype and ctype.index2name[pos] not in ctype.positions:
return False, [
error(
"lang-position",
f"'{prop.name}' claims {ctype.index2name[pos]} — bus copper, "
f"not a claimable position of '{ctype.name}' (electrical "
"realization is not modeled)",
(src_of(prop),),
)
]
return True, []
def _parse_gpio_map(
node,
plugs_by_path: PlugsByPath,
is_plural: bool,
) -> tuple[dict[int, tuple[str, int, int]], list[Diagnostic]]:
"""gpio-map's own 5-cell rows: child pos, child flags, phandle, parent
pos, parent flags. Each phandle must land on one of the carrier's own
plugs (pass-through); RECORDS which slot it named, per row."""
diags: list[Diagnostic] = []
gpio_map: dict[int, tuple[str, int, int]] = {}
if "gpio-map" in node.props:
cells = words(node.props["gpio-map"])
dt = node.dt
for i in range(0, len(cells) - len(cells) % 5, 5):
pos, _f, phandle, parent_pos, parent_flags = cells[i : i + 5]
target = dt.phandle2node.get(phandle)
plug_entry = plugs_by_path.get(target.path) if target is not None else None
if plug_entry is None:
what = "one of the carrier's plugs" if is_plural else "the carrier's plug"
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': gpio-map parent must "
f"be {what} (pass-through)",
(src_of(node),),
)
)
continue
slot, _pctype = plug_entry
gpio_map[pos] = (slot, parent_pos, parent_flags)
return gpio_map, diags
def _parse_exposed_buses(
node,
shield: Shield,
plugs_by_path: PlugsByPath,
ctype: ConnectorType | None,
type_name: str,
is_plural: bool,
) -> tuple[dict[str, tuple[str, str]], list[Diagnostic]]:
"""The socket,<bus> (and role-qualified) properties, in sorted
property-name order: each is either a pass-through of one of the
carrier's plugs or a new scope rooted at a device of the shield."""
diags: list[Diagnostic] = []
buses: dict[str, tuple[str, str]] = {}
qualified_props = sorted(name for name in node.props if _BUS_PROP_RE.match(name))
for prop_name in qualified_props:
kind = prop_name[len("socket,") :]
if ctype is not None and kind not in ctype.bus_proxies:
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} names a bus "
f"'{kind}' that connector type '{type_name}' does not "
"declare -- declared buses: "
f"{', '.join(sorted(ctype.bus_proxies)) or 'none'}",
(src_of(node),),
)
)
continue
target = node.props[prop_name].to_node()
by_path = shield.by_path.get(target.path)
plug_entry = plugs_by_path.get(target.path)
if plug_entry is not None:
slot, _pctype = plug_entry
buses[kind] = ("plug", slot) # pass-through
elif isinstance(by_path, Device):
buses[kind] = ("scope", by_path.label) # new scope
else:
what = "one of the carrier's plugs" if is_plural else "<&plug>"
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} must be "
f"{what} (pass-through) or <&device> (new scope)",
(src_of(node),),
)
)
return buses, diags
def _parse_exposed(
node,
plugs_by_path: PlugsByPath,
shield: Shield,
types: dict[str, ConnectorType],
) -> tuple[ExposedSocket, list[Diagnostic]]:
"""A re-exported socket, potentially composed from SEVERAL named
parents. gpio-map binds exposed positions to ONE of the carrier's own
plug positions (pass-through) -- RECORDING which slot the phandle
named, per row, exactly as `_parse_pos_ref` widens "must be THIS
shield's plug" to "one of this shield's plugs". socket,<bus> (bare,
or role-qualified per the multi-bus vocabulary) is either <&some-plug>
(pass through THAT plug's own bus) or <&device> (a NEW scope rooted
in that device of the shield). The CHILD-side qualified name is the
EXPOSED connector type's OWN vocabulary -- validated exact-match
against its declared bus_proxies, no fallback, independent of
whichever parent-side bus a pass-through eventually selects (that
selection is compose_socket's own job, by KIND, once the parent is a
real resolved socket)."""
diags: list[Diagnostic] = []
type_name = node.props["compatible"].to_string().split(",", 1)[1]
ctype = types.get(type_name)
is_plural = len(plugs_by_path) > 1
gpio_map, d = _parse_gpio_map(node, plugs_by_path, is_plural)
diags += d
pwm_map, pwm_cells, d = _parse_channel_map(
node, "pwm-map", "#pwm-cells", "pwm", plugs_by_path, is_plural
)
diags += d
adc_map, adc_cells, d = _parse_channel_map(
node, "io-channel-map", "#io-channel-cells", "adc", plugs_by_path, is_plural
)
diags += d
buses, d = _parse_exposed_buses(node, shield, plugs_by_path, ctype, type_name, is_plural)
diags += d
cs_pool: dict[str, list[int]] = {}
if "socket,cs-pool" in node.props:
cs_pool["spi"] = list(node.props["socket,cs-pool"].to_nums())
for prop_name in sorted(node.props):
m = _CS_POOL_PROP_RE.match(prop_name)
if m is None:
continue
cs_pool[m.group(1)] = list(node.props[prop_name].to_nums())
channel = node.props["shield,channel"].to_num() if "shield,channel" in node.props else None
label, d = _require_label(node, "exposed socket", shield.name)
diags += d
return ExposedSocket(
name=node.name,
label=label,
type_name=type_name,
gpio_map=gpio_map,
buses=buses,
pwm_map=pwm_map,
pwm_cells=pwm_cells,
adc_map=adc_map,
adc_cells=adc_cells,
cs_pool=cs_pool,
channel=channel,
src=src_of(node),
), diags
def _parse_channel_map(
node,
prop_name: str,
cells_prop: str,
function: str,
plugs_by_path: PlugsByPath,
is_plural: bool,
) -> tuple[dict[int, tuple[str, int, int]], int | None, list[Diagnostic]]:
"""The pwm-map / io-channel-map twin of gpio-map's own loop above,
factored out because PWM and ADC share this one function's shape end
to end and because their STRIDE, unlike gpio-map's, is not a
tree-wide constant: with `#pwm-cells = <2>` a row is 5 words (2 child
+ phandle + 2 parent); with `#io-channel-cells = <1>` it is 3 --
derived below from the declared cell counts, never hardcoded.
require-and-check: `prop_name` without `cells_prop` alongside
it, or the reverse pairing, is a parse-time lang-exposed error -- the
carrier author's declared count is what compose_socket later checks
against the resolved parent's, so a map with no declared count (or a
count with no map to describe) is malformed before that check could
ever run. `function` selects `#<fn>-cells`'s fallback-to-generic-
Zephyr-form default (_FUNCTION_DEFAULT_CELLS) for whatever the
PHANDLE TARGET (the carrier's own plug, a template placeholder) itself
declares -- the row's PARENT-specifier length -- exactly the same
lookup `_parse_pos_ref` already applies to a device's own pwm/adc ref.
Returns (map, declared_cells, diagnostics): map is `{}`/declared_cells
is None when the node authors neither property (declared by absence,
matching gpio_map's own convention) or when parsing fails outright."""
diags: list[Diagnostic] = []
has_map = prop_name in node.props
has_cells = cells_prop in node.props
if has_map and not has_cells:
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} needs a "
f"{cells_prop} declaration alongside it (require-and-check: "
"the carrier states its own cell count, "
"the analyzer checks it against the resolved parent's)",
(src_of(node),),
)
)
return {}, None, diags
if has_cells and not has_map:
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {cells_prop} needs a "
f"{prop_name} declaration alongside it (require-and-check)",
(src_of(node),),
)
)
return {}, None, diags
if not has_map:
return {}, None, diags
declared_cells = node.props[cells_prop].to_num()
cells = words(node.props[prop_name])
dt = node.dt
result: dict[int, tuple[str, int, int]] = {}
i = 0
while i < len(cells):
if i + declared_cells + 1 > len(cells):
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} has a "
f"malformed entry (expected {declared_cells}-cell child "
f"specifiers, per {cells_prop})",
(src_of(node),),
)
)
break
pos = cells[i]
phandle = cells[i + declared_cells]
target = dt.phandle2node.get(phandle)
parent_cells = _ncells(target, function)
row_len = declared_cells + 1 + parent_cells
if i + row_len > len(cells):
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} has a truncated entry",
(src_of(node),),
)
)
break
plug_entry = plugs_by_path.get(target.path) if target is not None else None
if plug_entry is None:
what = "one of the carrier's plugs" if is_plural else "the carrier's plug"
diags.append(
error(
"lang-exposed",
f"exposed socket '{node.name}': {prop_name} parent must "
f"be {what} (pass-through)",
(src_of(node),),
)
)
i += row_len
continue
slot, _pctype = plug_entry
parent_pos = cells[i + declared_cells + 1]
result[pos] = (slot, parent_pos, 0)
i += row_len
return result, declared_cells, diags
def _parse_pad(node, shield_name: str) -> tuple[Pad, list[Diagnostic]]:
diags: list[Diagnostic] = []
role = node.props["shield,role"].to_string() if "shield,role" in node.props else "bidir"
if role not in ("driver", "listener", "bidir"):
diags.append(
error(
"lang-pad-role",
f"pad '{node.name}': unknown role '{role}' (driver / listener / bidir)",
(src_of(node),),
)
)
of = None
if "shield,of" in node.props:
of = node.props["shield,of"].to_node().name.partition("@")[0]
label, d = _require_label(node, "pad", shield_name)
diags += d
return Pad(name=node.name, label=label, role=role, of=of, src=src_of(node)), diags
def _parse_strap(node, shield_name: str) -> tuple[Strap, list[Diagnostic]]:
dom = node.props["shield,domain"].to_nums()
domain = [(dom[i], dom[i + 1]) for i in range(0, len(dom), 2)]
label, diags = _require_label(node, "strap", shield_name)
return Strap(
name=node.name, label=label, domain=domain, sheet_label=_sheet_label(node), src=src_of(node)
), diags
def _parse_jumper(node, shield_name: str) -> tuple[Jumper, list[Diagnostic]]:
dom = node.props["shield,position-domain"].to_nums()
domain = [(dom[i], dom[i + 1]) for i in range(0, len(dom), 2)]
label, diags = _require_label(node, "jumper", shield_name)
return Jumper(
name=node.name, label=label, domain=domain, sheet_label=_sheet_label(node), src=src_of(node)
), diags
def _sheet_label(node) -> str:
if "shield,sheet-label" in node.props:
return node.props["shield,sheet-label"].to_string()
return ""
efd4b04f2This file is pinned to the selected release. Download the original to open it in your design tool.