| // 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), | |
| }; |