// SPDX-License-Identifier: GPL-2.0 /* * Xilinx Event Management Driver * * Copyright (C) 2021 Xilinx, Inc. * * Abhyuday Godhasara <abhyuday.godhasara@xilinx.com> */ #include <linux/cpuhotplug.h> #include <linux/firmware/xlnx-event-manager.h> #include <linux/firmware/xlnx-zynqmp.h> #include <linux/hashtable.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/irqdomain.h> #include <linux/module.h> #include <linux/of_irq.h> #include <linux/platform_device.h> #include <linux/slab.h> static DEFINE_PER_CPU_READ_MOSTLY(int, cpu_number1); static int virq_sgi; static int event_manager_availability = -EACCES; /* SGI number used for Event management driver */ #define XLNX_EVENT_SGI_NUM (15) /* Max number of driver can register for same event */ #define MAX_DRIVER_PER_EVENT (10U) /* Max HashMap Order for PM API feature check (1<<7 = 128) */ #define REGISTERED_DRIVER_MAX_ORDER (7) #define MAX_BITS (32U) /* Number of bits available for error mask */ #define FIRMWARE_VERSION_MASK (0xFFFFU) #define REGISTER_NOTIFIER_FIRMWARE_VERSION (2U) static DEFINE_HASHTABLE(reg_driver_map, REGISTERED_DRIVER_MAX_ORDER); static int sgi_num = XLNX_EVENT_SGI_NUM; static bool is_need_to_unregister; /** * struct agent_cb - Registered callback function and private data. * @agent_data: Data passed back to handler function. * @eve_cb: Function pointer to store the callback function. * @list: member to create list. */ struct agent_cb { void *agent_data; event_cb_func_t eve_cb; struct list_head list; }; /** * struct registered_event_data - Registered Event Data. * @key: key is the combine id(Node-Id | Event-Id) of type u64 * where upper u32 for Node-Id and lower u32 for Event-Id, * And this used as key to index into hashmap. * @cb_type: Type of Api callback, like PM_NOTIFY_CB, etc. * @wake: If this flag set, firmware will wake up processor if is * in sleep or power down state. * @cb_list_head: Head of call back data list which contain the information * about registered handler and private data. * @hentry: hlist_node that hooks this entry into hashtable. */ struct registered_event_data { u64 key; enum pm_api_cb_id cb_type; bool wake; struct list_head cb_list_head; struct hlist_node hentry; }; static bool xlnx_is_error_event(const u32 node_id) { if (node_id == EVENT_ERROR_PMC_ERR1 || node_id == EVENT_ERROR_PMC_ERR2 || node_id == EVENT_ERROR_PSM_ERR1 || node_id == EVENT_ERROR_PSM_ERR2) return true; return false; } static int xlnx_add_cb_for_notify_event(const u32 node_id, const u32 event, const bool wake, event_cb_func_t cb_fun, void *data) { u64 key = 0; bool present_in_hash = false; struct registered_event_data *eve_data; struct agent_cb *cb_data; struct agent_cb *cb_pos; struct agent_cb *cb_next; key = ((u64)node_id << 32U) | (u64)event; /* Check for existing entry in hash table for given key id */ hash_for_each_possible(reg_driver_map, eve_data, hentry, key) { if (eve_data->key == key) { present_in_hash = true; break; } } if (!present_in_hash) { /* Add new entry if not present in HASH table */ eve_data = kmalloc(sizeof(*eve_data), GFP_KERNEL); if (!eve_data) return -ENOMEM; eve_data->key = key; eve_data->cb_type = PM_NOTIFY_CB; eve_data->wake = wake; INIT_LIST_HEAD(&eve_data->cb_list_head); cb_data = kmalloc(sizeof(*cb_data), GFP_KERNEL); if (!cb_data) { kfree(eve_data); return -ENOMEM; } cb_data->eve_cb = cb_fun; cb_data->agent_data = data; /* Add into callback list */ list_add(&cb_data->list, &eve_data->cb_list_head); /* Add into HASH table */ hash_add(reg_driver_map, &eve_data->hentry, key); } else { /* Search for callback function and private data in list */ list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { if (cb_pos->eve_cb == cb_fun && cb_pos->agent_data == data) { return 0; } } /* Add multiple handler and private data in list */ cb_data = kmalloc(sizeof(*cb_data), GFP_KERNEL); if (!cb_data) return -ENOMEM; cb_data->eve_cb = cb_fun; cb_data->agent_data = data; list_add(&cb_data->list, &eve_data->cb_list_head); } return 0; } static int xlnx_add_cb_for_suspend(event_cb_func_t cb_fun, void *data) { struct registered_event_data *eve_data; struct agent_cb *cb_data; /* Check for existing entry in hash table for given cb_type */ hash_for_each_possible(reg_driver_map, eve_data, hentry, PM_INIT_SUSPEND_CB) { if (eve_data->cb_type == PM_INIT_SUSPEND_CB) { pr_err("Found as already registered\n"); return -EINVAL; } } /* Add new entry if not present */ eve_data = kmalloc(sizeof(*eve_data), GFP_KERNEL); if (!eve_data) return -ENOMEM; eve_data->key = 0; eve_data->cb_type = PM_INIT_SUSPEND_CB; INIT_LIST_HEAD(&eve_data->cb_list_head); cb_data = kmalloc(sizeof(*cb_data), GFP_KERNEL); if (!cb_data) return -ENOMEM; cb_data->eve_cb = cb_fun; cb_data->agent_data = data; /* Add into callback list */ list_add(&cb_data->list, &eve_data->cb_list_head); hash_add(reg_driver_map, &eve_data->hentry, PM_INIT_SUSPEND_CB); return 0; } static int xlnx_remove_cb_for_suspend(event_cb_func_t cb_fun) { bool is_callback_found = false; struct registered_event_data *eve_data; struct agent_cb *cb_pos; struct agent_cb *cb_next; struct hlist_node *tmp; is_need_to_unregister = false; /* Check for existing entry in hash table for given cb_type */ hash_for_each_possible_safe(reg_driver_map, eve_data, tmp, hentry, PM_INIT_SUSPEND_CB) { if (eve_data->cb_type == PM_INIT_SUSPEND_CB) { /* Delete the list of callback */ list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { if (cb_pos->eve_cb == cb_fun) { is_callback_found = true; list_del_init(&cb_pos->list); kfree(cb_pos); } } /* remove an object from a hashtable */ hash_del(&eve_data->hentry); kfree(eve_data); is_need_to_unregister = true; } } if (!is_callback_found) { pr_warn("Didn't find any registered callback for suspend event\n"); return -EINVAL; } return 0; } static int xlnx_remove_cb_for_notify_event(const u32 node_id, const u32 event, event_cb_func_t cb_fun, void *data) { bool is_callback_found = false; struct registered_event_data *eve_data; u64 key = ((u64)node_id << 32U) | (u64)event; struct agent_cb *cb_pos; struct agent_cb *cb_next; struct hlist_node *tmp; is_need_to_unregister = false; /* Check for existing entry in hash table for given key id */ hash_for_each_possible_safe(reg_driver_map, eve_data, tmp, hentry, key) { if (eve_data->key == key) { /* Delete the list of callback */ list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { if (cb_pos->eve_cb == cb_fun && cb_pos->agent_data == data) { is_callback_found = true; list_del_init(&cb_pos->list); kfree(cb_pos); } } /* Remove HASH table if callback list is empty */ if (list_empty(&eve_data->cb_list_head)) { /* remove an object from a HASH table */ hash_del(&eve_data->hentry); kfree(eve_data); is_need_to_unregister = true; } } } if (!is_callback_found) { pr_warn("Didn't find any registered callback for 0x%x 0x%x\n", node_id, event); return -EINVAL; } return 0; } /** * xlnx_register_event() - Register for the event. * @cb_type: Type of callback from pm_api_cb_id, * PM_NOTIFY_CB - for Error Events, * PM_INIT_SUSPEND_CB - for suspend callback. * @node_id: Node-Id related to event. * @event: Event Mask for the Error Event. * @wake: Flag specifying whether the subsystem should be woken upon * event notification. * @cb_fun: Function pointer to store the callback function. * @data: Pointer for the driver instance. * * Return: Returns 0 on successful registration else error code. */ int xlnx_register_event(const enum pm_api_cb_id cb_type, const u32 node_id, const u32 event, const bool wake, event_cb_func_t cb_fun, void *data) { int ret = 0; u32 eve; int pos; if (event_manager_availability) return event_manager_availability; if (cb_type != PM_NOTIFY_CB && cb_type != PM_INIT_SUSPEND_CB) { pr_err("%s() Unsupported Callback 0x%x\n", __func__, cb_type); return -EINVAL; } if (!cb_fun) return -EFAULT; if (cb_type == PM_INIT_SUSPEND_CB) { ret = xlnx_add_cb_for_suspend(cb_fun, data); } else { if (!xlnx_is_error_event(node_id)) { /* Add entry for Node-Id/Event in hash table */ ret = xlnx_add_cb_for_notify_event(node_id, event, wake, cb_fun, data); } else { /* Add into Hash table */ for (pos = 0; pos < MAX_BITS; pos++) { eve = event & (1 << pos); if (!eve) continue; /* Add entry for Node-Id/Eve in hash table */ ret = xlnx_add_cb_for_notify_event(node_id, eve, wake, cb_fun, data); /* Break the loop if got error */ if (ret) break; } if (ret) { /* Skip the Event for which got the error */ pos--; /* Remove registered(during this call) event from hash table */ for ( ; pos >= 0; pos--) { eve = event & (1 << pos); if (!eve) continue; xlnx_remove_cb_for_notify_event(node_id, eve, cb_fun, data); } } } if (ret) { pr_err("%s() failed for 0x%x and 0x%x: %d\r\n", __func__, node_id, event, ret); return ret; } /* Register for Node-Id/Event combination in firmware */ ret = zynqmp_pm_register_notifier(node_id, event, wake, true); if (ret) { pr_err("%s() failed for 0x%x and 0x%x: %d\r\n", __func__, node_id, event, ret); /* Remove already registered event from hash table */ if (xlnx_is_error_event(node_id)) { for (pos = 0; pos < MAX_BITS; pos++) { eve = event & (1 << pos); if (!eve) continue; xlnx_remove_cb_for_notify_event(node_id, eve, cb_fun, data); } } else { xlnx_remove_cb_for_notify_event(node_id, event, cb_fun, data); } return ret; } } return ret; } EXPORT_SYMBOL_GPL(xlnx_register_event); /** * xlnx_unregister_event() - Unregister for the event. * @cb_type: Type of callback from pm_api_cb_id, * PM_NOTIFY_CB - for Error Events, * PM_INIT_SUSPEND_CB - for suspend callback. * @node_id: Node-Id related to event. * @event: Event Mask for the Error Event. * @cb_fun: Function pointer of callback function. * @data: Pointer of agent's private data. * * Return: Returns 0 on successful unregistration else error code. */ int xlnx_unregister_event(const enum pm_api_cb_id cb_type, const u32 node_id, const u32 event, event_cb_func_t cb_fun, void *data) { int ret = 0; u32 eve, pos; is_need_to_unregister = false; if (event_manager_availability) return event_manager_availability; if (cb_type != PM_NOTIFY_CB && cb_type != PM_INIT_SUSPEND_CB) { pr_err("%s() Unsupported Callback 0x%x\n", __func__, cb_type); return -EINVAL; } if (!cb_fun) return -EFAULT; if (cb_type == PM_INIT_SUSPEND_CB) { ret = xlnx_remove_cb_for_suspend(cb_fun); } else { /* Remove Node-Id/Event from hash table */ if (!xlnx_is_error_event(node_id)) { xlnx_remove_cb_for_notify_event(node_id, event, cb_fun, data); } else { for (pos = 0; pos < MAX_BITS; pos++) { eve = event & (1 << pos); if (!eve) continue; xlnx_remove_cb_for_notify_event(node_id, eve, cb_fun, data); } } /* Un-register if list is empty */ if (is_need_to_unregister) { /* Un-register for Node-Id/Event combination */ ret = zynqmp_pm_register_notifier(node_id, event, false, false); if (ret) { pr_err("%s() failed for 0x%x and 0x%x: %d\n", __func__, node_id, event, ret); return ret; } } } return ret; } EXPORT_SYMBOL_GPL(xlnx_unregister_event); static void xlnx_call_suspend_cb_handler(const u32 *payload) { bool is_callback_found = false; struct registered_event_data *eve_data; u32 cb_type = payload[0]; struct agent_cb *cb_pos; struct agent_cb *cb_next; /* Check for existing entry in hash table for given cb_type */ hash_for_each_possible(reg_driver_map, eve_data, hentry, cb_type) { if (eve_data->cb_type == cb_type) { list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { cb_pos->eve_cb(&payload[0], cb_pos->agent_data); is_callback_found = true; } } } if (!is_callback_found) pr_warn("Didn't find any registered callback for suspend event\n"); } static void xlnx_call_notify_cb_handler(const u32 *payload) { bool is_callback_found = false; struct registered_event_data *eve_data; u64 key = ((u64)payload[1] << 32U) | (u64)payload[2]; int ret; struct agent_cb *cb_pos; struct agent_cb *cb_next; /* Check for existing entry in hash table for given key id */ hash_for_each_possible(reg_driver_map, eve_data, hentry, key) { if (eve_data->key == key) { list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { cb_pos->eve_cb(&payload[0], cb_pos->agent_data); is_callback_found = true; } /* re register with firmware to get future events */ ret = zynqmp_pm_register_notifier(payload[1], payload[2], eve_data->wake, true); if (ret) { pr_err("%s() failed for 0x%x and 0x%x: %d\r\n", __func__, payload[1], payload[2], ret); list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { /* Remove already registered event from hash table */ xlnx_remove_cb_for_notify_event(payload[1], payload[2], cb_pos->eve_cb, cb_pos->agent_data); } } } } if (!is_callback_found) pr_warn("Didn't find any registered callback for 0x%x 0x%x\n", payload[1], payload[2]); } static void xlnx_get_event_callback_data(u32 *buf) { zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, 0, 0, 0, 0, buf); } static irqreturn_t xlnx_event_handler(int irq, void *dev_id) { u32 cb_type, node_id, event, pos; u32 payload[CB_MAX_PAYLOAD_SIZE] = {0}; u32 event_data[CB_MAX_PAYLOAD_SIZE] = {0}; /* Get event data */ xlnx_get_event_callback_data(payload); /* First element is callback type, others are callback arguments */ cb_type = payload[0]; if (cb_type == PM_NOTIFY_CB) { node_id = payload[1]; event = payload[2]; if (!xlnx_is_error_event(node_id)) { xlnx_call_notify_cb_handler(payload); } else { /* * Each call back function expecting payload as an input arguments. * We can get multiple error events as in one call back through error * mask. So payload[2] may can contain multiple error events. * In reg_driver_map database we store data in the combination of single * node_id-error combination. * So coping the payload message into event_data and update the * event_data[2] with Error Mask for single error event and use * event_data as input argument for registered call back function. * */ memcpy(event_data, payload, (4 * CB_MAX_PAYLOAD_SIZE)); /* Support Multiple Error Event */ for (pos = 0; pos < MAX_BITS; pos++) { if ((0 == (event & (1 << pos)))) continue; event_data[2] = (event & (1 << pos)); xlnx_call_notify_cb_handler(event_data); } } } else if (cb_type == PM_INIT_SUSPEND_CB) { xlnx_call_suspend_cb_handler(payload); } else { pr_err("%s() Unsupported Callback %d\n", __func__, cb_type); } return IRQ_HANDLED; } static int xlnx_event_cpuhp_start(unsigned int cpu) { enable_percpu_irq(virq_sgi, IRQ_TYPE_NONE); return 0; } static int xlnx_event_cpuhp_down(unsigned int cpu) { disable_percpu_irq(virq_sgi); return 0; } static void xlnx_disable_percpu_irq(void *data) { disable_percpu_irq(virq_sgi); } static int xlnx_event_init_sgi(struct platform_device *pdev) { int ret = 0; int cpu = smp_processor_id(); /* * IRQ related structures are used for the following: * for each SGI interrupt ensure its mapped by GIC IRQ domain * and that each corresponding linux IRQ for the HW IRQ has * a handler for when receiving an interrupt from the remote * processor. */ struct irq_domain *domain; struct irq_fwspec sgi_fwspec; struct device_node *interrupt_parent = NULL; struct device *parent = pdev->dev.parent; /* Find GIC controller to map SGIs. */ interrupt_parent = of_irq_find_parent(parent->of_node); if (!interrupt_parent) { dev_err(&pdev->dev, "Failed to find property for Interrupt parent\n"); return -EINVAL; } /* Each SGI needs to be associated with GIC's IRQ domain. */ domain = irq_find_host(interrupt_parent); of_node_put(interrupt_parent); /* Each mapping needs GIC domain when finding IRQ mapping. */ sgi_fwspec.fwnode = domain->fwnode; /* * When irq domain looks at mapping each arg is as follows: * 3 args for: interrupt type (SGI), interrupt # (set later), type */ sgi_fwspec.param_count = 1; /* Set SGI's hwirq */ sgi_fwspec.param[0] = sgi_num; virq_sgi = irq_create_fwspec_mapping(&sgi_fwspec); per_cpu(cpu_number1, cpu) = cpu; ret = request_percpu_irq(virq_sgi, xlnx_event_handler, "xlnx_event_mgmt", &cpu_number1); WARN_ON(ret); if (ret) { irq_dispose_mapping(virq_sgi); return ret; } irq_to_desc(virq_sgi); irq_set_status_flags(virq_sgi, IRQ_PER_CPU); return ret; } static void xlnx_event_cleanup_sgi(struct platform_device *pdev) { int cpu = smp_processor_id(); per_cpu(cpu_number1, cpu) = cpu; cpuhp_remove_state(CPUHP_AP_ONLINE_DYN); on_each_cpu(xlnx_disable_percpu_irq, NULL, 1); irq_clear_status_flags(virq_sgi, IRQ_PER_CPU); free_percpu_irq(virq_sgi, &cpu_number1); irq_dispose_mapping(virq_sgi); } static int xlnx_event_manager_probe(struct platform_device *pdev) { int ret; ret = zynqmp_pm_feature(PM_REGISTER_NOTIFIER); if (ret < 0) { dev_err(&pdev->dev, "Feature check failed with %d\n", ret); return ret; } if ((ret & FIRMWARE_VERSION_MASK) < REGISTER_NOTIFIER_FIRMWARE_VERSION) { dev_err(&pdev->dev, "Register notifier version error. Expected Firmware: v%d - Found: v%d\n", REGISTER_NOTIFIER_FIRMWARE_VERSION, ret & FIRMWARE_VERSION_MASK); return -EOPNOTSUPP; } /* Initialize the SGI */ ret = xlnx_event_init_sgi(pdev); if (ret) { dev_err(&pdev->dev, "SGI Init has been failed with %d\n", ret); return ret; } /* Setup function for the CPU hot-plug cases */ cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "soc/event:starting", xlnx_event_cpuhp_start, xlnx_event_cpuhp_down); ret = zynqmp_pm_register_sgi(sgi_num, 0); if (ret) { dev_err(&pdev->dev, "SGI %d Registration over TF-A failed with %d\n", sgi_num, ret); xlnx_event_cleanup_sgi(pdev); return ret; } event_manager_availability = 0; dev_info(&pdev->dev, "SGI %d Registered over TF-A\n", sgi_num); dev_info(&pdev->dev, "Xilinx Event Management driver probed\n"); return ret; } static void xlnx_event_manager_remove(struct platform_device *pdev) { int i; struct registered_event_data *eve_data; struct hlist_node *tmp; int ret; struct agent_cb *cb_pos; struct agent_cb *cb_next; hash_for_each_safe(reg_driver_map, i, tmp, eve_data, hentry) { list_for_each_entry_safe(cb_pos, cb_next, &eve_data->cb_list_head, list) { list_del_init(&cb_pos->list); kfree(cb_pos); } hash_del(&eve_data->hentry); kfree(eve_data); } ret = zynqmp_pm_register_sgi(0, 1); if (ret) dev_err(&pdev->dev, "SGI unregistration over TF-A failed with %d\n", ret); xlnx_event_cleanup_sgi(pdev); event_manager_availability = -EACCES; } static struct platform_driver xlnx_event_manager_driver = { .probe = xlnx_event_manager_probe, .remove_new = xlnx_event_manager_remove, .driver = { .name = "xlnx_event_manager", }, }; module_param(sgi_num, uint, 0); module_platform_driver(xlnx_event_manager_driver);