u-boot/drivers/interconnect/interconnect-uclass.c
Neil Armstrong 60a99d5ca3 Introduce the Generic System Interconnect Subsystem
Let's introduce the Generic System Interconnect subsystem based on
the counterpart Linux framework which is used to vote for bandwidth
across multiple SoC busses.

Documentation for the Linux Generic System Interconnect Subsystem can
be found at [1].

Each bus endpoints are materialised as "nodes" which are linked together,
and the DT will specify a pair of nodes to enable and set a bandwidth
on the route between those endpoints.

The hardware resources that provide those nodes and provides the way
to vote for the bandwidth are called "providers".

The Interconnect uclass code is heavily based on the Linux one, with
some small differences:
- nodes are allocated as udevices instead of Linux idr_alloc()
- tag management is minimal, only normal xlate is supported
- getting nodes states at probe is not implemented
- providers are probed on demand while the nodes links are traversed
- nodes are populated on bind
- id management is simplified, static IDs and dynamics IDs can be used
- identical consume API as Linux, only implementation differs

Fully tested with associated DM test suite.

[1] https://docs.kernel.org/driver-api/interconnect.html

Link: https://patch.msgid.link/20251120-topic-interconnect-next-v5-1-e8a82720da5d@linaro.org
Signed-off-by: Neil Armstrong <neil.armstrong@linaro.org>
2025-11-20 09:17:58 +01:00

546 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025 Linaro Limited
* Based on the Linux Driver:
* Copyright (c) 2017-2019, Linaro Ltd.
* Author: Georgi Djakov <georgi.djakov@linaro.org>
*/
#define LOG_CATEGORY UCLASS_INTERCONNECT
#include <dm.h>
#include <log.h>
#include <malloc.h>
#include <linux/err.h>
#include <interconnect.h>
#include <interconnect-uclass.h>
#include <dm/lists.h>
#include <dm/uclass-internal.h>
#include <dm/device-internal.h>
#include <dm/device_compat.h>
static struct icc_node *of_icc_get_from_provider(struct udevice *dev,
const struct ofnode_phandle_args *args);
static struct icc_path *icc_path_find(struct udevice *dev,
struct icc_node *src, struct icc_node *dst);
static struct icc_node *icc_node_find(const ulong id);
/* Public API */
struct icc_path *of_icc_get(struct udevice *dev, const char *name)
{
int index = 0;
if (!dev)
return ERR_PTR(-ENODEV);
if (!ofnode_has_property(dev_ofnode(dev), "interconnects"))
return NULL;
if (name) {
index = dev_read_stringlist_search(dev, "interconnect-names", name);
if (index < 0) {
debug("fdt_stringlist_search() failed: %d\n", index);
return ERR_PTR(index);
}
}
return of_icc_get_by_index(dev, index);
}
struct icc_path *of_icc_get_by_index(struct udevice *dev, int index)
{
struct ofnode_phandle_args src_args, dst_args;
struct icc_node *src_node, *dst_node;
struct icc_path *path;
int ret;
if (!dev)
return ERR_PTR(-ENODEV);
debug("(dev=%p,idx=%d)\n", dev, index);
if (!ofnode_has_property(dev_ofnode(dev), "interconnects"))
return NULL;
ret = dev_read_phandle_with_args(dev, "interconnects",
"#interconnect-cells", 0, index * 2,
&src_args);
if (ret) {
dev_err(dev, "dev_read_phandle_with_args src failed: %d\n", ret);
return ERR_PTR(ret);
}
ret = dev_read_phandle_with_args(dev, "interconnects",
"#interconnect-cells", 0, index * 2 + 1,
&dst_args);
if (ret) {
dev_err(dev, "dev_read_phandle_with_args dst failed: %d\n", ret);
return ERR_PTR(ret);
}
src_node = of_icc_get_from_provider(dev, &src_args);
if (IS_ERR(src_node)) {
dev_err(dev, "error finding src node\n");
return ERR_CAST(src_node);
}
dst_node = of_icc_get_from_provider(dev, &dst_args);
if (IS_ERR(dst_node)) {
dev_err(dev, "error finding dst node\n");
return ERR_CAST(dst_node);
}
path = icc_path_find(dev, src_node, dst_node);
if (IS_ERR(path))
dev_err(dev, "invalid path=%ld\n", PTR_ERR(path));
debug("(path=%p)\n", path);
return path;
}
int icc_put(struct icc_path *path)
{
struct icc_node *node;
size_t i;
int ret;
debug("(path=%p)\n", path);
if (!path || IS_ERR(path))
return 0;
ret = icc_set_bw(path, 0, 0);
if (ret) {
dev_err(path->dev, "failed to set bandwidth (%d)\n", ret);
return ret;
}
for (i = 0; i < path->num_nodes; i++) {
node = path->reqs[i].node;
if (node->users)
node->users--;
if (!node->users)
device_remove(node->dev, DM_REMOVE_NORMAL);
hlist_del(&path->reqs[i].req_node);
}
kfree(path);
return 0;
}
static int __icc_enable(struct icc_path *path, bool enable)
{
int i;
if (!path)
return 0;
if (IS_ERR(path) || !path->num_nodes)
return -EINVAL;
for (i = 0; i < path->num_nodes; i++)
path->reqs[i].enabled = enable;
return icc_set_bw(path, path->reqs[0].avg_bw,
path->reqs[0].peak_bw);
}
int icc_enable(struct icc_path *path)
{
debug("(path=%p)\n", path);
return __icc_enable(path, true);
}
int icc_disable(struct icc_path *path)
{
debug("(path=%p)\n", path);
return __icc_enable(path, false);
}
static int apply_constraints(struct icc_path *path)
{
struct icc_node *next, *prev = NULL;
const struct interconnect_ops *ops;
struct icc_provider *provider;
struct udevice *p;
int ret = -EINVAL;
int i;
debug("(path=%p)\n", path);
for (i = 0; i < path->num_nodes; i++) {
next = path->reqs[i].node;
p = next->dev->parent;
provider = dev_get_uclass_plat(p);
/* both endpoints should be valid master-slave pairs */
if (!prev || (p != prev->dev->parent && !provider->inter_set)) {
prev = next;
continue;
}
debug("(path=%p,req=%d,node=%s,provider=%s)\n",
path, i, next->dev->name, p->name);
ops = device_get_ops(p);
/* set the constraints */
if (ops->set) {
ret = ops->set(prev, next);
if (ret)
goto out;
}
prev = next;
}
out:
return ret;
}
/*
* We want the path to honor all bandwidth requests, so the average and peak
* bandwidth requirements from each consumer are aggregated at each node.
* The aggregation is platform specific, so each platform can customize it by
* implementing its own aggregate() function.
*/
static int aggregate_requests(struct icc_node *node)
{
const struct interconnect_ops *ops = device_get_ops(node->dev->parent);
struct icc_req *r;
u32 avg_bw, peak_bw;
debug("(dev=%s)\n", node->dev->name);
node->avg_bw = 0;
node->peak_bw = 0;
if (ops->pre_aggregate)
ops->pre_aggregate(node);
hlist_for_each_entry(r, &node->req_list, req_node) {
if (r->enabled) {
avg_bw = r->avg_bw;
peak_bw = r->peak_bw;
} else {
avg_bw = 0;
peak_bw = 0;
}
debug("(dev=%s,req=%s,avg=%d,peak=%d)\n",
node->dev->name, r->node->dev->name,
avg_bw, peak_bw);
if (ops->aggregate)
ops->aggregate(node, r->tag, avg_bw, peak_bw,
&node->avg_bw, &node->peak_bw);
}
return 0;
}
int icc_set_bw(struct icc_path *path, u32 avg_bw, u32 peak_bw)
{
struct icc_node *node;
u32 old_avg, old_peak;
size_t i;
int ret;
debug("(path=%p,avg=%d,peak=%d)\n", path, avg_bw, peak_bw);
if (!path)
return 0;
if (IS_ERR(path) || !path->num_nodes)
return -EINVAL;
old_avg = path->reqs[0].avg_bw;
old_peak = path->reqs[0].peak_bw;
for (i = 0; i < path->num_nodes; i++) {
node = path->reqs[i].node;
/* update the consumer request for this path */
path->reqs[i].avg_bw = avg_bw;
path->reqs[i].peak_bw = peak_bw;
/* aggregate requests for this node */
aggregate_requests(node);
}
ret = apply_constraints(path);
if (ret) {
dev_err(path->dev, "error applying constraints (%d)\n", ret);
for (i = 0; i < path->num_nodes; i++) {
node = path->reqs[i].node;
path->reqs[i].avg_bw = old_avg;
path->reqs[i].peak_bw = old_peak;
aggregate_requests(node);
}
apply_constraints(path);
}
return ret;
}
/* Provider API */
static struct icc_path *icc_path_init(struct udevice *dev, struct icc_node *dst,
ssize_t num_nodes)
{
struct icc_node *node = dst;
struct icc_path *path;
struct udevice *node_dev;
int i, ret;
debug("(dev=%s,node=%s)\n", dev->name, node->dev->name);
path = kzalloc(sizeof(struct icc_path) +
sizeof(struct icc_req) * num_nodes,
GFP_KERNEL);
if (!path)
return ERR_PTR(-ENOMEM);
path->dev = dev;
path->num_nodes = num_nodes;
for (i = num_nodes - 1; i >= 0; i--) {
debug("(req[%d]=%s)\n", i, node->dev->name);
hlist_add_head(&path->reqs[i].req_node, &node->req_list);
path->reqs[i].node = node;
path->reqs[i].enabled = true;
/* Probe this node since used in an active path */
ret = uclass_get_device_tail(node->dev, 0, &node_dev);
if (ret)
return ERR_PTR(ret);
node->users++;
/* reference to previous node was saved during path traversal */
node = node->reverse;
}
return path;
}
static struct icc_path *icc_path_find(struct udevice *dev, struct icc_node *src,
struct icc_node *dst)
{
struct icc_path *path = ERR_PTR(-EPROBE_DEFER);
struct icc_node *n, *node = NULL;
struct list_head traverse_list;
struct list_head edge_list;
struct list_head visited_list;
size_t i, depth = 1;
bool found = false;
debug("(dev=%s,src=%s,dest=%p\n",
dev->name, src->dev->name, dst->dev->name);
INIT_LIST_HEAD(&traverse_list);
INIT_LIST_HEAD(&edge_list);
INIT_LIST_HEAD(&visited_list);
list_add(&src->search_list, &traverse_list);
src->reverse = NULL;
do {
list_for_each_entry_safe(node, n, &traverse_list, search_list) {
if (node == dst) {
found = true;
list_splice_init(&edge_list, &visited_list);
list_splice_init(&traverse_list, &visited_list);
break;
}
for (i = 0; i < node->num_links; i++) {
struct icc_node *tmp;
tmp = icc_node_find(node->links[i]);
if (!tmp) {
dev_err(dev, "missing link to node id %lx\n",
node->links[i]);
path = ERR_PTR(-ENOENT);
goto out;
}
if (tmp->is_traversed)
continue;
tmp->is_traversed = true;
tmp->reverse = node;
list_add_tail(&tmp->search_list, &edge_list);
}
}
if (found)
break;
list_splice_init(&traverse_list, &visited_list);
list_splice_init(&edge_list, &traverse_list);
/* count the hops including the source */
depth++;
} while (!list_empty(&traverse_list));
out:
/* reset the traversed state */
list_for_each_entry_reverse(n, &visited_list, search_list)
n->is_traversed = false;
if (found)
path = icc_path_init(dev, dst, depth);
return path;
}
static struct icc_node *of_icc_get_from_provider(struct udevice *dev,
const struct ofnode_phandle_args *args)
{
const struct interconnect_ops *ops;
struct udevice *icc_dev;
int ret;
ret = uclass_get_device_by_ofnode(UCLASS_INTERCONNECT, args->node,
&icc_dev);
if (ret) {
dev_err(dev, "uclass_get_device_by_ofnode failed: %d\n", ret);
return ERR_PTR(ret);
}
ops = device_get_ops(icc_dev);
return ops->of_xlate(icc_dev, args);
}
static struct icc_node *icc_node_find(const ulong id)
{
struct udevice *dev;
for (uclass_find_first_device(UCLASS_ICC_NODE, &dev);
dev;
uclass_find_next_device(&dev)) {
if (dev_get_driver_data(dev) == id)
return dev_get_uclass_plat(dev);
}
return NULL;
}
static bool icc_node_busy(struct udevice *dev)
{
struct icc_node *node = dev_get_uclass_plat(dev);
debug("(dev=%s,users=%d)\n", dev->name, node->users);
return !!node->users;
}
struct icc_node *icc_node_create(struct udevice *dev,
ulong id, const char *name)
{
struct udevice *node;
struct driver *drv;
int ret;
drv = lists_driver_lookup_name("icc_node");
if (!drv)
return ERR_PTR(-ENOENT);
ret = device_bind_with_driver_data(dev, drv, strdup(name),
id, ofnode_null(), &node);
if (ret)
return ERR_PTR(ret);
device_set_name_alloced(node);
return dev_get_uclass_plat(node);
}
int icc_link_create(struct icc_node *node, const ulong dst_id)
{
ulong *new;
new = realloc(node->links,
(node->num_links + 1) * sizeof(*node->links));
if (!new)
return -ENOMEM;
node->links = new;
node->links[node->num_links++] = dst_id;
return 0;
}
static int icc_node_bind(struct udevice *dev)
{
struct icc_node *node = dev_get_uclass_plat(dev);
debug("(dev=%s)\n", dev->name);
node->dev = dev;
return 0;
}
static int icc_node_probe(struct udevice *dev)
{
struct icc_node *node = dev_get_uclass_plat(dev);
debug("(dev=%s,parent=%p,id=%lx)\n",
dev->name, dev->parent->name, dev_get_driver_data(dev));
node->avg_bw = 0;
node->peak_bw = 0;
return 0;
}
static int icc_node_remove(struct udevice *dev)
{
debug("(dev=%s,parent=%s,id=%lx)\n",
dev->name, dev->parent->name, dev_get_driver_data(dev));
if (icc_node_busy(dev))
return -EBUSY;
return 0;
}
static int icc_node_unbind(struct udevice *dev)
{
struct icc_node *node = dev_get_uclass_plat(dev);
debug("(dev=%s,id=%lx)\n",
dev->name, dev_get_driver_data(dev));
kfree(node->links);
return 0;
}
UCLASS_DRIVER(interconnect) = {
.id = UCLASS_INTERCONNECT,
.name = "interconnect",
.per_device_plat_auto = sizeof(struct icc_provider),
};
U_BOOT_DRIVER(icc_node) = {
.name = "icc_node",
.id = UCLASS_ICC_NODE,
.bind = icc_node_bind,
.probe = icc_node_probe,
.remove = icc_node_remove,
.unbind = icc_node_unbind,
};
UCLASS_DRIVER(icc_node) = {
.id = UCLASS_ICC_NODE,
.name = "icc_node",
.per_device_plat_auto = sizeof(struct icc_node),
};