546 lines
12 KiB
C
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),
|
|
};
|