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:
Peter A. Bigot 2019-09-27 11:31:36 -05:00 committed by Kumar Gala
commit ea956f4ac3
4 changed files with 259 additions and 1 deletions

View file

@ -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"'

View file

@ -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
View 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'))

View file

@ -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")