scripts: dts: add dependency information to edtlib
Device tree nodes have a dependency on their parents, as well as other nodes. To ensure driver instances are initialized in the proper we need to identify these dependencies and construct a total order on nodes that ensures no node is initialized before something it depends on. Add a Graph class that calculates a partial order on nodes, taking into account the potential for cycles in the dependency graph. When generating devicetree value headers calculate the dependency graph and document the order and dependencies in the derived files. In the future these dependencies may be expressed in binding data. Signed-off-by: Peter A. Bigot <pab@pabigot.com>
This commit is contained in:
parent
46b728204e
commit
ea956f4ac3
4 changed files with 259 additions and 1 deletions
|
@ -36,6 +36,7 @@ except ImportError:
|
|||
|
||||
from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY, TYPE_NUMS, \
|
||||
TYPE_PHANDLE, TYPE_PHANDLES_AND_NUMS
|
||||
from grutils import Graph
|
||||
|
||||
# NOTE: testedtlib.py is the test suite for this library. It can be run
|
||||
# directly as a script:
|
||||
|
@ -127,6 +128,8 @@ class EDT:
|
|||
self._init_compat2binding(bindings_dirs)
|
||||
self._init_nodes()
|
||||
|
||||
self._define_order()
|
||||
|
||||
def get_node(self, path):
|
||||
"""
|
||||
Returns the Node at the DT path or alias 'path'. Raises EDTError if the
|
||||
|
@ -158,6 +161,68 @@ class EDT:
|
|||
return "<EDT for '{}', binding directories '{}'>".format(
|
||||
self.dts_path, self.bindings_dirs)
|
||||
|
||||
def required_by(self, node):
|
||||
"""
|
||||
Returns a list of Nodes that have direct dependencies on 'node'.
|
||||
"""
|
||||
return self._graph.required_by(node)
|
||||
|
||||
def depends_on(self, node):
|
||||
"""
|
||||
Returns a list of Nodes on which 'node' directly depends.
|
||||
"""
|
||||
return self._graph.depends_on(node)
|
||||
|
||||
def scc_order(self):
|
||||
"""
|
||||
Returns a list of lists of Nodes where all elements of each list
|
||||
depend on each other, and the Nodes in any list do not depend
|
||||
on any Node in a subsequent list. Each list defines a Strongly
|
||||
Connected Component (SCC) of the graph.
|
||||
|
||||
For an acyclic graph each list will be a singleton. Cycles
|
||||
will be represented by lists with multiple nodes. Cycles are
|
||||
not expected to be present in devicetree graphs.
|
||||
"""
|
||||
try:
|
||||
return self._graph.scc_order()
|
||||
except Exception as e:
|
||||
raise EDTError(e)
|
||||
|
||||
def _define_order(self):
|
||||
# Constructs a graph of dependencies between Node instances,
|
||||
# then calculates a partial order over the dependencies. The
|
||||
# algorithm supports detecting dependency loops.
|
||||
|
||||
self._graph = Graph()
|
||||
|
||||
for node in self.nodes:
|
||||
# A Node always depends on its parent.
|
||||
for child in node.children.values():
|
||||
self._graph.add_edge(child, node)
|
||||
|
||||
# A Node depends on any Nodes present in 'phandle',
|
||||
# 'phandles', or 'phandle-array' property values.
|
||||
for prop in node.props.values():
|
||||
if prop.type == 'phandle':
|
||||
self._graph.add_edge(node, prop.val)
|
||||
elif prop.type == 'phandles':
|
||||
for phandle_node in prop.val:
|
||||
self._graph.add_edge(node, phandle_node)
|
||||
elif prop.type == 'phandle-array':
|
||||
for cd in prop.val:
|
||||
self._graph.add_edge(node, cd.controller)
|
||||
|
||||
# A Node depends on whatever supports the interrupts it
|
||||
# generates.
|
||||
for intr in node.interrupts:
|
||||
self._graph.add_edge(node, intr.controller)
|
||||
|
||||
# Calculate an order that ensures no node is before any node
|
||||
# it depends on. This sets the dep_ordinal field in each
|
||||
# Node.
|
||||
self.scc_order()
|
||||
|
||||
def _init_compat2binding(self, bindings_dirs):
|
||||
# Creates self._compat2binding. This is a dictionary that maps
|
||||
# (<compatible>, <bus>) tuples (both strings) to (<binding>, <path>)
|
||||
|
@ -583,6 +648,13 @@ class Node:
|
|||
A dictionary with the Node instances for the devicetree children of the
|
||||
node, indexed by name
|
||||
|
||||
dep_ordinal:
|
||||
A non-negative integer value such that the value for a Node is
|
||||
less than the value for all Nodes that depend on it.
|
||||
|
||||
The ordinal is defined for all Nodes including those that are not
|
||||
'enabled', and is unique among nodes in its EDT 'nodes' list.
|
||||
|
||||
enabled:
|
||||
True unless the node has 'status = "disabled"'
|
||||
|
||||
|
|
|
@ -51,6 +51,15 @@ Directories with bindings:
|
|||
args.dts, ", ".join(map(relativize, args.bindings_dirs))
|
||||
), blank_before=False)
|
||||
|
||||
out_comment("Nodes in dependency order (ordinal : path):")
|
||||
for scc in edt.scc_order():
|
||||
if len(scc) > 1:
|
||||
err("Cycle in devicetree involving: {}".format(
|
||||
", ".join([n.path for n in scc])))
|
||||
node = scc[0]
|
||||
out_comment("{} : {}".format(node.dep_ordinal, node.path),
|
||||
blank_before = False)
|
||||
|
||||
active_compats = set()
|
||||
|
||||
for node in edt.nodes:
|
||||
|
@ -60,6 +69,20 @@ Directories with bindings:
|
|||
if node.matching_compat == "fixed-partitions":
|
||||
continue
|
||||
|
||||
requires_text = ""
|
||||
if edt.depends_on(node):
|
||||
requires_text = "Requires:\n"
|
||||
for depends in edt.depends_on(node):
|
||||
requires_text += " {} {}\n".format(depends.dep_ordinal, depends.path)
|
||||
requires_text += "\n"
|
||||
|
||||
supports_text = ""
|
||||
if edt.required_by(node):
|
||||
supports_text = "Supports:\n"
|
||||
for required in edt.required_by(node):
|
||||
supports_text += " {} {}\n".format(required.dep_ordinal, required.path)
|
||||
supports_text += "\n"
|
||||
|
||||
out_comment("""\
|
||||
Devicetree node:
|
||||
{}
|
||||
|
@ -67,9 +90,12 @@ Devicetree node:
|
|||
Binding (compatible = {}):
|
||||
{}
|
||||
|
||||
Description:
|
||||
Dependency Ordinal: {}
|
||||
|
||||
{}{}Description:
|
||||
{}""".format(
|
||||
node.path, node.matching_compat, relativize(node.binding_path),
|
||||
node.dep_ordinal, requires_text, supports_text,
|
||||
# Indent description by two spaces
|
||||
"\n".join(" " + line for line in node.description.splitlines())
|
||||
))
|
||||
|
|
142
scripts/dts/grutils.py
Normal file
142
scripts/dts/grutils.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
# Copyright 2009-2013, 2019 Peter A. Bigot
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This implementation is derived from the one in
|
||||
# [PyXB](https://github.com/pabigot/pyxb), stripped down and modified
|
||||
# specifically to manage edtlib Node instances.
|
||||
|
||||
import collections
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
class Graph:
|
||||
"""
|
||||
Represent a directed graph with edtlib Node objects as nodes.
|
||||
|
||||
This is used to determine order dependencies among nodes in a
|
||||
devicetree. An edge from C{source} to C{target} indicates that
|
||||
some aspect of C{source} requires that some aspect of C{target}
|
||||
already be available.
|
||||
"""
|
||||
|
||||
def __init__ (self, root=None):
|
||||
self.__roots = None
|
||||
if root is not None:
|
||||
self.__roots = {root}
|
||||
self.__edge_map = collections.defaultdict(set)
|
||||
self.__reverse_map = collections.defaultdict(set)
|
||||
self.__nodes = set()
|
||||
|
||||
def add_edge (self, source, target):
|
||||
"""
|
||||
Add a directed edge from the C{source} to the C{target}.
|
||||
|
||||
The nodes are added to the graph if necessary.
|
||||
"""
|
||||
self.__edge_map[source].add(target)
|
||||
if source != target:
|
||||
self.__reverse_map[target].add(source)
|
||||
self.__nodes.add(source)
|
||||
self.__nodes.add(target)
|
||||
|
||||
def roots (self):
|
||||
"""
|
||||
Return the set of nodes calculated to be roots (i.e., those
|
||||
that have no incoming edges).
|
||||
|
||||
This caches the roots calculated in a previous invocation.
|
||||
|
||||
@rtype: C{set}
|
||||
"""
|
||||
if not self.__roots:
|
||||
self.__roots = set()
|
||||
for n in self.__nodes:
|
||||
if n not in self.__reverse_map:
|
||||
self.__roots.add(n)
|
||||
return self.__roots
|
||||
|
||||
def _tarjan (self):
|
||||
# Execute Tarjan's algorithm on the graph.
|
||||
#
|
||||
# Tarjan's algorithm
|
||||
# (http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm)
|
||||
# computes the strongly-connected components
|
||||
# (http://en.wikipedia.org/wiki/Strongly_connected_component)
|
||||
# of the graph: i.e., the sets of nodes that form a minimal
|
||||
# closed set under edge transition. In essence, the loops.
|
||||
# We use this to detect groups of components that have a
|
||||
# dependency cycle, and to impose a total order on components
|
||||
# based on dependencies.
|
||||
|
||||
self.__stack = []
|
||||
self.__scc_order = []
|
||||
self.__index = 0
|
||||
self.__tarjan_index = {}
|
||||
self.__tarjan_low_link = {}
|
||||
for v in self.__nodes:
|
||||
self.__tarjan_index[v] = None
|
||||
roots = self.roots()
|
||||
if self.__nodes and not roots:
|
||||
raise Exception('TARJAN: No roots found in graph with {} nodes'.format(len(self.__nodes)))
|
||||
for r in sorted(roots, key=attrgetter('path')):
|
||||
self._tarjan_root(r)
|
||||
|
||||
# Assign ordinals for edtlib
|
||||
ordinal = 0
|
||||
for scc in self.__scc_order:
|
||||
# Zephyr customization: devicetree Node graphs should have
|
||||
# no loops, so all SCCs should be singletons. That may
|
||||
# change in the future, but for now we only give an
|
||||
# ordinal to singletons.
|
||||
if len(scc) == 1:
|
||||
scc[0].dep_ordinal = ordinal
|
||||
ordinal += 1
|
||||
|
||||
def _tarjan_root (self, v):
|
||||
# Do the work of Tarjan's algorithm for a given root node.
|
||||
|
||||
if self.__tarjan_index.get(v) is not None:
|
||||
# "Root" was already reached.
|
||||
return
|
||||
self.__tarjan_index[v] = self.__tarjan_low_link[v] = self.__index
|
||||
self.__index += 1
|
||||
self.__stack.append(v)
|
||||
source = v
|
||||
for target in sorted(self.__edge_map[source], key=attrgetter('path')):
|
||||
if self.__tarjan_index[target] is None:
|
||||
self._tarjan_root(target)
|
||||
self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target])
|
||||
elif target in self.__stack:
|
||||
self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target])
|
||||
|
||||
if self.__tarjan_low_link[v] == self.__tarjan_index[v]:
|
||||
scc = []
|
||||
while True:
|
||||
scc.append(self.__stack.pop())
|
||||
if v == scc[-1]:
|
||||
break
|
||||
self.__scc_order.append(scc)
|
||||
|
||||
def scc_order (self):
|
||||
"""Return the strongly-connected components in order.
|
||||
|
||||
The data structure is a list, in dependency order, of strongly
|
||||
connected components (which can be single nodes). Appearance
|
||||
of a node in a set earlier in the list indicates that it has
|
||||
no dependencies on any node that appears in a subsequent set.
|
||||
This order is preferred over a depth-first-search order for
|
||||
code generation, since it detects loops.
|
||||
"""
|
||||
if not self.__scc_order:
|
||||
self._tarjan()
|
||||
return self.__scc_order
|
||||
__scc_order = None
|
||||
|
||||
def depends_on (self, node):
|
||||
"""Get the nodes that 'node' directly depends on."""
|
||||
return sorted(self.__edge_map[node], key=attrgetter('path'))
|
||||
|
||||
def required_by (self, node):
|
||||
"""Get the nodes that directly depend on 'node'."""
|
||||
return sorted(self.__reverse_map[node], key=attrgetter('path'))
|
|
@ -212,6 +212,24 @@ warning: "#cells:" in test-bindings/deprecated.yaml is deprecated and will be re
|
|||
"test-bindings-2/multidir.yaml")
|
||||
|
||||
|
||||
#
|
||||
# Test dependency relations
|
||||
#
|
||||
|
||||
#for node in edt.nodes:
|
||||
# print("{}: {}".format(node.path, node.ordinal))
|
||||
# for before in edt.depends_on(node):
|
||||
# print(" Requires: {} {}".format(before.ordinal, before.path))
|
||||
# for after in edt.required_by(node):
|
||||
# print(" Supports: {} {}".format(after.ordinal, after.path))
|
||||
|
||||
verify_eq(edt.get_node("/").ordinal, 0)
|
||||
verify_eq(edt.get_node("/in-dir-1").ordinal, 1)
|
||||
if edt.get_node("/") not in edt.depends_on(edt.get_node("/in-dir-1")):
|
||||
fail("/ depends-on /in-dir-1")
|
||||
if edt.get_node("/in-dir-1") not in edt.required_by(edt.get_node("/")):
|
||||
fail("/in-dir-1 required-by /")
|
||||
|
||||
print("all tests passed")
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue