blob: c4faaa5ab096428c5171710a0bf151edd4f114c5 [file] [log] [blame] [edit]
// 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),
};