Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6489a72
sdt_devices: add assist to generate SDT devices YAML for glob matching
Mar 2, 2026
1b0f8ba
sdt_devices: extend to discover CPUs, memory, firmware, and toplevel …
Mar 2, 2026
9c67de4
sdt_devices: filter structural nodes using compatible property
Mar 2, 2026
561594b
sdt_devices: filter infrastructure devices and improve memory info
Mar 2, 2026
f78c747
sdt_devices: add --include-clocks option for clock node inclusion
Mar 2, 2026
ef58284
sdt_devices: add cpumask, status, and reserved-memory flags
Mar 2, 2026
08b9d3e
yaml: preserve HexInt values for proper hex integer output
Mar 3, 2026
4b91680
sdt_devices: add infrastructure category filtering
Mar 9, 2026
617f427
yaml: add cpu_expand support for sdt_devices format
Mar 10, 2026
7fa1e21
sdt_devices: use openamp,domain-v1,devices compatible string
Mar 10, 2026
97b455a
lopper_lib: add CPU access map visualization
Mar 16, 2026
e2ff149
schema: create unified schema package
Mar 30, 2026
2bd746c
schema: add resolve_property_spec for unified type resolution
Mar 30, 2026
232a7b3
audit/schema: use unified ConstraintType from lopper.schema.core
Mar 30, 2026
e00ec02
audit/schema: add learned schema type validation
Mar 30, 2026
25998da
audit/schema: fix learned type validation edge cases
Mar 30, 2026
0ea03c7
schema: add deprecation warnings for legacy APIs
Mar 30, 2026
2bc9681
tests: fix 5 always-skipped address_map tests
Apr 2, 2026
b7137b3
schema/tree: add path-ref/alias-ref types and strict-mode dangling re…
Apr 8, 2026
b5736dd
tests: add regression coverage for path-ref and alias-ref pruning
Apr 8, 2026
5fbebe5
tree: suppress path-ref warnings for lopper-comment nodes
Apr 29, 2026
806cf2e
tests: regression coverage for domain-to-domain /axi node survival
May 5, 2026
3822a36
overlay: fix child_nodes deserialization in multi-pass lopper invocat…
arthokal May 15, 2026
bc2d6db
tests: regression coverage for child_nodes deserialization in overlays
May 15, 2026
851a769
tree: coerce comment prop values to str in resolve()
May 15, 2026
86e4209
tree: fix comment prop coercion to handle bare ints from libfdt
May 15, 2026
446fcb2
overlay: Resolve phandle fixups in child nodes
arthokal May 19, 2026
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
50 changes: 24 additions & 26 deletions lopper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,36 +289,33 @@ def _unwrap_overlay_tree(ov_tree, base_tree):
clean_node.__dict__['abs_path'] = target_abs_path
clean_node.label = label

# Copy properties, resolving 0xffffffff placeholders
# Resolve 0xffffffff phandle placeholders in-place across
# overlay_child and all its children (ov_tree is temporary)
ov_node_prefix = frag_path + '/__overlay__'
for fix_label, fix_refs in label_to_fixups.items():
ph = label_to_phandle.get(fix_label)
if not ph: continue
for ref in fix_refs:
try:
rn, rp, bo = ref.rsplit(':', 2)
if not rn.startswith(ov_node_prefix): continue
prop = ov_tree[rn].__props__.get(rp)
if not prop: continue
val = list(prop.__dict__.get('value', []))
idx = int(bo) // 4
if idx < len(val) and val[idx] == 4294967295:
val[idx] = ph
prop.__dict__['value'] = val
except Exception:
pass

# Copy resolved properties to clean_node
for prop in overlay_child.__props__.values():
new_prop = copy.deepcopy(prop)
new_prop.node = clean_node

val = new_prop.__dict__.get('value')
if isinstance(val, list) and 4294967295 in val:
val = list(val)
ov_node_prefix = frag_path + '/__overlay__'
for fix_label, fix_refs in label_to_fixups.items():
ph = label_to_phandle.get(fix_label)
if ph is None:
continue
for ref in fix_refs:
try:
ref_node, ref_prop, byte_off = ref.rsplit(':', 2)
if ref_prop != prop.name:
continue
if not ref_node.startswith(ov_node_prefix):
continue
idx = int(byte_off) // 4
if idx < len(val) and val[idx] == 4294967295:
val[idx] = ph
except Exception:
pass
new_prop.__dict__['value'] = val

clean_node.__props__[prop.name] = new_prop

# Recursively copy child nodes from __overlay__
# Recursively copy child nodes (fixups already resolved in-place)
def _copy_children(src_node, dst_node):
for child in src_node.child_nodes.values():
child_copy = copy.deepcopy(child)
Expand Down Expand Up @@ -571,6 +568,7 @@ def _deserialize_overlay_node(data, parent=None, tree=None):
from lopper.tree import LopperNode, LopperProp
node = LopperNode(-1, data["abs_path"])
node.tree = tree
node.parent = parent

for prop_name, val, pclass in data.get("props", []):
lp = LopperProp(prop_name, -1, node, val)
Expand All @@ -584,7 +582,7 @@ def _deserialize_overlay_node(data, parent=None, tree=None):

for child_data in data.get("children", []):
child = _deserialize_overlay_node(child_data, parent=node, tree=tree)
node.child_nodes.append(child)
node.child_nodes[child.abs_path] = child

return node

Expand Down
13 changes: 13 additions & 0 deletions lopper/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def usage():
print(' schema_all (enable all schema checks)' )
print(' all (enable all warnings)' )
print(' , --memmap output file for memory map visualization (use - for stdout)' )
print(' , --cpumap output file for CPU access map visualization (use - for stdout)' )
print(' , --cpumap-expand expand bus nodes to show child devices in cpumap' )
print(' , --symbols generate (and maintain) the __symbols__ node during processing' )
print(' -o, --output output file')
print(' , --overlay Allow input files (dts or yaml) to overlay system device tree nodes' )
Expand Down Expand Up @@ -569,6 +571,17 @@ def main():
f.write(memmap_output)
_info(f"memory map written to {memmap_file}")

# Generate CPU access map visualization if requested
if cpumap_file:
from lopper.assists.lopper_lib import render_all_cpu_access_maps
cpumap_output = render_all_cpu_access_maps(device_tree.tree, expand=cpumap_expand)
if cpumap_file == "-":
print(cpumap_output)
else:
with open(cpumap_file, 'w') as f:
f.write(cpumap_output)
_info(f"CPU access map written to {cpumap_file}")

if not dryrun:
# write any changes to the FDT, before we do our write
if device_tree.dts:
Expand Down
263 changes: 263 additions & 0 deletions lopper/assists/lopper_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,269 @@ def find_address_in_map(entries, address):
return None


def _is_cpu_cluster(node):
"""Check if a node is a CPU cluster.

A CPU cluster is identified by:
- Having an address-map property (restricted access cluster), or
- Being named 'cpus' or 'cpus-cluster*', or
- Having compatible containing 'cpus,cluster'
"""
if 'address-map' in node.__props__:
return True

# Check node name
if node.name == 'cpus' or node.name.startswith('cpus-cluster'):
return True

# Check compatible property
try:
compat = node['compatible'].value
if isinstance(compat, list):
for c in compat:
if 'cpus' in str(c).lower():
return True
elif 'cpus' in str(compat).lower():
return True
except (KeyError, TypeError):
pass

return False


def _get_child_devices(tree, bus_node, parent_addr, map_size, max_children=10):
"""Get child devices of a bus node with their addresses.

Args:
tree: LopperTree
bus_node: The bus/interconnect node
parent_addr: Base address of the bus in parent's view
map_size: Size of the mapped region
max_children: Maximum number of children to return

Returns:
List of (address, size, label) tuples for child devices
"""
children = []

# Get address/size cells for this bus
try:
child_ac = bus_node['#address-cells'].value[0]
child_sc = bus_node['#size-cells'].value[0]
except (KeyError, IndexError, TypeError):
child_ac = 2
child_sc = 2

# Parse ranges to understand address translation
# Empty ranges (or ['']) means 1:1 mapping (identity)
# ranges format: child_addr(child_ac) parent_addr(parent_ac) size(child_sc)
ranges_offset = 0
try:
ranges = bus_node['ranges'].value
if ranges and ranges != [''] and len(ranges) >= child_ac:
# Get the child base address from ranges
ranges_offset = cell_value_get(ranges, child_ac, 0)[0]
except (KeyError, IndexError, TypeError):
pass

# Iterate children
for child in bus_node.child_nodes.values():
if 'reg' not in child.__props__:
continue

try:
reg = child['reg'].value
if not reg or reg == ['']:
continue

# Get child's address from reg property
child_addr = cell_value_get(reg, child_ac, 0)[0]

# Get size if available
if len(reg) >= child_ac + child_sc:
child_size = cell_value_get(reg, child_sc, child_ac)[0]
else:
child_size = 0

# For 1:1 mapping (empty ranges), child_addr IS the system address
# Check if this falls within the mapped region
if child_addr >= parent_addr and child_addr < parent_addr + map_size:
label = child.label if child.label else child.name
children.append((child_addr, child_size, label))

except (IndexError, TypeError, ValueError):
continue

if len(children) >= max_children:
break

# Sort by address
children.sort(key=lambda x: x[0])
return children


def render_cpu_access_map(tree, cpu_cluster=None, width=60, expand=False):
"""Render an ASCII visualization of CPU cluster device accessibility.

Shows which devices each CPU cluster can access based on its address-map
property. Clusters without address-map are shown as having unrestricted
system access.

Args:
tree: LopperTree to visualize
cpu_cluster: Specific CPU cluster node or path (None for all clusters)
width: Width of the visualization in characters
expand: If True, show child devices of bus nodes

Returns:
String containing the ASCII visualization
"""
lines = []

# Find CPU clusters to visualize
clusters = []
if cpu_cluster is not None:
if isinstance(cpu_cluster, str):
node = tree.deref(cpu_cluster)
if node:
clusters = [node]
else:
clusters = [cpu_cluster]
else:
# Find all CPU cluster nodes
for node in tree.__nodes__.values():
if _is_cpu_cluster(node):
clusters.append(node)

if not clusters:
return "No CPU clusters found.\n"

# Sort clusters by path for consistent output
clusters = sorted(clusters, key=lambda n: n.abs_path)

# Render each cluster
for cluster in clusters:
lines.append("=" * width)
cluster_label = cluster.label if cluster.label else cluster.name
title = f"CPU Cluster: {cluster_label}"
lines.append(title.center(width))
lines.append(f"Path: {cluster.abs_path}".center(width))
lines.append("=" * width)

# Check if cluster has address-map
if 'address-map' not in cluster.__props__:
lines.append("")
lines.append(" (no address-map - unrestricted system access)")
lines.append("")
continue

# Parse address-map
try:
address_map = cluster['address-map'].value
na = cluster['#ranges-address-cells'].value[0]
ns = cluster['#ranges-size-cells'].value[0]
except (KeyError, IndexError, TypeError):
lines.append(" (unable to parse address-map)")
lines.append("")
continue

entries = parse_address_map(address_map, na, ns)

if not entries:
lines.append(" (empty address-map)")
lines.append("")
continue

# Header
lines.append("")
lines.append(f" {'Address Range':<36} {'Size':<12} Device")
lines.append(f" {'-' * 36} {'-' * 12} {'-' * (width - 54)}")

# Sort entries by address
sorted_entries = sorted(entries, key=lambda e: e.child_addr)

# Track unique devices (same phandle may appear multiple times)
seen_devices = {}

for entry in sorted_entries:
# Skip zero-size entries (typically non-memory mapped devices)
if entry.size == 0:
continue

# Format address range
addr_start = f"0x{entry.child_addr:08x}"
addr_end = f"0x{entry.child_addr + entry.size - 1:08x}"
addr_range = f"{addr_start} - {addr_end}"

# Format size
if entry.size >= 0x100000:
size_str = f"{entry.size // 0x100000} MB"
elif entry.size >= 0x400:
size_str = f"{entry.size // 0x400} KB"
else:
size_str = f"{entry.size} B"

# Resolve device node
device_node = tree.pnode(entry.phandle)
if device_node:
device_label = device_node.label if device_node.label else device_node.name
device_path = device_node.abs_path

# Track for summary
if entry.phandle not in seen_devices:
seen_devices[entry.phandle] = device_node
else:
device_label = f"<phandle {entry.phandle}>"
device_path = ""

lines.append(f" {addr_range:<36} {size_str:<12} {device_label}")

# Expand bus nodes to show children
if expand and device_node and device_node.child_nodes:
children = _get_child_devices(tree, device_node, entry.child_addr, entry.size)
for i, (child_addr, child_size, child_label) in enumerate(children):
# Use tree drawing characters
if i == len(children) - 1:
prefix = " └─"
else:
prefix = " ├─"

# Format child address and size
child_addr_str = f"0x{child_addr:08x}"
if child_size >= 0x100000:
child_size_str = f"{child_size // 0x100000} MB"
elif child_size >= 0x400:
child_size_str = f"{child_size // 0x400} KB"
elif child_size > 0:
child_size_str = f"{child_size} B"
else:
child_size_str = ""

lines.append(f"{prefix} {child_addr_str:<31} {child_size_str:<12} {child_label}")

# Summary
lines.append("")
lines.append(f" Total mappings: {len(entries)}")
lines.append(f" Unique devices: {len(seen_devices)}")
lines.append("")

return "\n".join(lines)


def render_all_cpu_access_maps(tree, width=60, expand=False):
"""Render ASCII visualization for all CPU clusters in a tree.

Args:
tree: LopperTree to visualize
width: Width of the visualization in characters
expand: If True, show child devices of bus nodes

Returns:
String containing the ASCII visualization
"""
return render_cpu_access_map(tree, cpu_cluster=None, width=width, expand=expand)


def _normalize_start_size_value(raw_value, default_value):
"""Convert YAML-provided start/size representations into integers."""
if raw_value is None:
Expand Down
Loading
Loading