| // 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; |
| |
| /** |
| * 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. |
| * @agent_data: Data passed back to handler function. |
| * @cb_type: Type of Api callback, like PM_NOTIFY_CB, etc. |
| * @eve_cb: Function pointer to store the callback function. |
| * @wake: If this flag set, firmware will wakeup processor if is |
| * in sleep or power down state. |
| * @hentry: hlist_node that hooks this entry into hashtable. |
| */ |
| struct registered_event_data { |
| u64 key; |
| enum pm_api_cb_id cb_type; |
| void *agent_data; |
| |
| event_cb_func_t eve_cb; |
| bool wake; |
| 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; |
| struct registered_event_data *eve_data; |
| |
| 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) { |
| 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 = key; |
| eve_data->cb_type = PM_NOTIFY_CB; |
| eve_data->eve_cb = cb_fun; |
| eve_data->wake = wake; |
| eve_data->agent_data = data; |
| |
| hash_add(reg_driver_map, &eve_data->hentry, key); |
| |
| return 0; |
| } |
| |
| static int xlnx_add_cb_for_suspend(event_cb_func_t cb_fun, void *data) |
| { |
| struct registered_event_data *eve_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; |
| eve_data->eve_cb = cb_fun; |
| eve_data->agent_data = data; |
| |
| 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; |
| |
| /* 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 && |
| eve_data->eve_cb == cb_fun) { |
| is_callback_found = true; |
| /* remove an object from a hashtable */ |
| hash_del(&eve_data->hentry); |
| kfree(eve_data); |
| } |
| } |
| 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) |
| { |
| bool is_callback_found = false; |
| struct registered_event_data *eve_data; |
| u64 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 && |
| eve_data->eve_cb == cb_fun) { |
| is_callback_found = true; |
| /* remove an object from a hashtable */ |
| hash_del(&eve_data->hentry); |
| kfree(eve_data); |
| } |
| } |
| 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); |
| } |
| } |
| } |
| |
| 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); |
| } |
| } else { |
| xlnx_remove_cb_for_notify_event(node_id, event, cb_fun); |
| } |
| 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. |
| * |
| * 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) |
| { |
| int ret; |
| u32 eve, 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_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); |
| } 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); |
| } |
| } |
| |
| /* 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]; |
| |
| /* 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) { |
| eve_data->eve_cb(&payload[0], eve_data->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; |
| |
| /* 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) { |
| eve_data->eve_cb(&payload[0], eve_data->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); |
| /* Remove already registered event from hash table */ |
| xlnx_remove_cb_for_notify_event(payload[1], payload[2], |
| eve_data->eve_cb); |
| } |
| } |
| } |
| 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_invoke_fn(PM_IOCTL, 0, IOCTL_REGISTER_SGI, sgi_num, |
| 0, NULL); |
| 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 int xlnx_event_manager_remove(struct platform_device *pdev) |
| { |
| int i; |
| struct registered_event_data *eve_data; |
| struct hlist_node *tmp; |
| int ret; |
| |
| hash_for_each_safe(reg_driver_map, i, tmp, eve_data, hentry) { |
| hash_del(&eve_data->hentry); |
| kfree(eve_data); |
| } |
| |
| ret = zynqmp_pm_invoke_fn(PM_IOCTL, 0, IOCTL_REGISTER_SGI, 0, 1, NULL); |
| 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; |
| |
| return ret; |
| } |
| |
| static struct platform_driver xlnx_event_manager_driver = { |
| .probe = xlnx_event_manager_probe, |
| .remove = xlnx_event_manager_remove, |
| .driver = { |
| .name = "xlnx_event_manager", |
| }, |
| }; |
| module_param(sgi_num, uint, 0); |
| module_platform_driver(xlnx_event_manager_driver); |