diff options
Diffstat (limited to 'drivers/soc/xilinx')
-rw-r--r-- | drivers/soc/xilinx/Kconfig | 38 | ||||
-rw-r--r-- | drivers/soc/xilinx/Makefile | 4 | ||||
-rw-r--r-- | drivers/soc/xilinx/xlnx_event_manager.c | 706 | ||||
-rw-r--r-- | drivers/soc/xilinx/zynqmp_pm_domains.c | 322 | ||||
-rw-r--r-- | drivers/soc/xilinx/zynqmp_power.c | 303 |
5 files changed, 1373 insertions, 0 deletions
diff --git a/drivers/soc/xilinx/Kconfig b/drivers/soc/xilinx/Kconfig new file mode 100644 index 000000000..8a755a5c8 --- /dev/null +++ b/drivers/soc/xilinx/Kconfig @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-2.0 +menu "Xilinx SoC drivers" + +config ZYNQMP_POWER + bool "Enable Xilinx Zynq MPSoC Power Management driver" + depends on PM && ZYNQMP_FIRMWARE + default y + select MAILBOX + select ZYNQMP_IPI_MBOX + help + Say yes to enable power management support for ZyqnMP SoC. + This driver uses firmware driver as an interface for power + management request to firmware. It registers isr to handle + power management callbacks from firmware. It registers mailbox client + to handle power management callbacks from firmware. + + If in doubt, say N. + +config ZYNQMP_PM_DOMAINS + bool "Enable Zynq MPSoC generic PM domains" + default y + depends on PM && ZYNQMP_FIRMWARE + select PM_GENERIC_DOMAINS + help + Say yes to enable device power management through PM domains + If in doubt, say N. + +config XLNX_EVENT_MANAGER + bool "Enable Xilinx Event Management Driver" + depends on ZYNQMP_FIRMWARE + default ZYNQMP_FIRMWARE + help + Say yes to enable event management support for Xilinx. + This driver uses firmware driver as an interface for event/power + management request to firmware. + + If in doubt, say N. +endmenu diff --git a/drivers/soc/xilinx/Makefile b/drivers/soc/xilinx/Makefile new file mode 100644 index 000000000..41e585bc9 --- /dev/null +++ b/drivers/soc/xilinx/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_ZYNQMP_POWER) += zynqmp_power.o +obj-$(CONFIG_ZYNQMP_PM_DOMAINS) += zynqmp_pm_domains.o +obj-$(CONFIG_XLNX_EVENT_MANAGER) += xlnx_event_manager.o diff --git a/drivers/soc/xilinx/xlnx_event_manager.c b/drivers/soc/xilinx/xlnx_event_manager.c new file mode 100644 index 000000000..f9d9b82b5 --- /dev/null +++ b/drivers/soc/xilinx/xlnx_event_manager.c @@ -0,0 +1,706 @@ +// 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 int 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; + + 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); diff --git a/drivers/soc/xilinx/zynqmp_pm_domains.c b/drivers/soc/xilinx/zynqmp_pm_domains.c new file mode 100644 index 000000000..fcce2433b --- /dev/null +++ b/drivers/soc/xilinx/zynqmp_pm_domains.c @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ZynqMP Generic PM domain support + * + * Copyright (C) 2015-2019 Xilinx, Inc. + * + * Davorin Mista <davorin.mista@aggios.com> + * Jolly Shah <jollys@xilinx.com> + * Rajan Vaja <rajan.vaja@xilinx.com> + */ + +#include <linux/err.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/pm_domain.h> +#include <linux/slab.h> + +#include <linux/firmware/xlnx-zynqmp.h> + +#define ZYNQMP_NUM_DOMAINS (100) + +static int min_capability; + +/** + * struct zynqmp_pm_domain - Wrapper around struct generic_pm_domain + * @gpd: Generic power domain + * @node_id: PM node ID corresponding to device inside PM domain + * @requested: The PM node mapped to the PM domain has been requested + */ +struct zynqmp_pm_domain { + struct generic_pm_domain gpd; + u32 node_id; + bool requested; +}; + +#define to_zynqmp_pm_domain(pm_domain) \ + container_of(pm_domain, struct zynqmp_pm_domain, gpd) + +/** + * zynqmp_gpd_is_active_wakeup_path() - Check if device is in wakeup source + * path + * @dev: Device to check for wakeup source path + * @not_used: Data member (not required) + * + * This function is checks device's child hierarchy and checks if any device is + * set as wakeup source. + * + * Return: 1 if device is in wakeup source path else 0 + */ +static int zynqmp_gpd_is_active_wakeup_path(struct device *dev, void *not_used) +{ + int may_wakeup; + + may_wakeup = device_may_wakeup(dev); + if (may_wakeup) + return may_wakeup; + + return device_for_each_child(dev, NULL, + zynqmp_gpd_is_active_wakeup_path); +} + +/** + * zynqmp_gpd_power_on() - Power on PM domain + * @domain: Generic PM domain + * + * This function is called before devices inside a PM domain are resumed, to + * power on PM domain. + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_power_on(struct generic_pm_domain *domain) +{ + struct zynqmp_pm_domain *pd = to_zynqmp_pm_domain(domain); + int ret; + + ret = zynqmp_pm_set_requirement(pd->node_id, + ZYNQMP_PM_CAPABILITY_ACCESS, + ZYNQMP_PM_MAX_QOS, + ZYNQMP_PM_REQUEST_ACK_BLOCKING); + if (ret) { + dev_err(&domain->dev, + "failed to set requirement to 0x%x for PM node id %d: %d\n", + ZYNQMP_PM_CAPABILITY_ACCESS, pd->node_id, ret); + return ret; + } + + dev_dbg(&domain->dev, "set requirement to 0x%x for PM node id %d\n", + ZYNQMP_PM_CAPABILITY_ACCESS, pd->node_id); + + return 0; +} + +/** + * zynqmp_gpd_power_off() - Power off PM domain + * @domain: Generic PM domain + * + * This function is called after devices inside a PM domain are suspended, to + * power off PM domain. + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_power_off(struct generic_pm_domain *domain) +{ + struct zynqmp_pm_domain *pd = to_zynqmp_pm_domain(domain); + int ret; + struct pm_domain_data *pdd, *tmp; + u32 capabilities = min_capability; + bool may_wakeup; + + /* If domain is already released there is nothing to be done */ + if (!pd->requested) { + dev_dbg(&domain->dev, "PM node id %d is already released\n", + pd->node_id); + return 0; + } + + list_for_each_entry_safe(pdd, tmp, &domain->dev_list, list_node) { + /* If device is in wakeup path, set capability to WAKEUP */ + may_wakeup = zynqmp_gpd_is_active_wakeup_path(pdd->dev, NULL); + if (may_wakeup) { + dev_dbg(pdd->dev, "device is in wakeup path in %s\n", + domain->name); + capabilities = ZYNQMP_PM_CAPABILITY_WAKEUP; + break; + } + } + + ret = zynqmp_pm_set_requirement(pd->node_id, capabilities, 0, + ZYNQMP_PM_REQUEST_ACK_NO); + if (ret) { + dev_err(&domain->dev, + "failed to set requirement to 0x%x for PM node id %d: %d\n", + capabilities, pd->node_id, ret); + return ret; + } + + dev_dbg(&domain->dev, "set requirement to 0x%x for PM node id %d\n", + capabilities, pd->node_id); + + return 0; +} + +/** + * zynqmp_gpd_attach_dev() - Attach device to the PM domain + * @domain: Generic PM domain + * @dev: Device to attach + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_attach_dev(struct generic_pm_domain *domain, + struct device *dev) +{ + struct zynqmp_pm_domain *pd = to_zynqmp_pm_domain(domain); + struct device_link *link; + int ret; + + link = device_link_add(dev, &domain->dev, DL_FLAG_SYNC_STATE_ONLY); + if (!link) + dev_dbg(&domain->dev, "failed to create device link for %s\n", + dev_name(dev)); + + /* If this is not the first device to attach there is nothing to do */ + if (domain->device_count) + return 0; + + ret = zynqmp_pm_request_node(pd->node_id, 0, 0, + ZYNQMP_PM_REQUEST_ACK_BLOCKING); + if (ret) { + dev_err(&domain->dev, "%s request failed for node %d: %d\n", + domain->name, pd->node_id, ret); + return ret; + } + + pd->requested = true; + + dev_dbg(&domain->dev, "%s requested PM node id %d\n", + dev_name(dev), pd->node_id); + + return 0; +} + +/** + * zynqmp_gpd_detach_dev() - Detach device from the PM domain + * @domain: Generic PM domain + * @dev: Device to detach + */ +static void zynqmp_gpd_detach_dev(struct generic_pm_domain *domain, + struct device *dev) +{ + struct zynqmp_pm_domain *pd = to_zynqmp_pm_domain(domain); + int ret; + + /* If this is not the last device to detach there is nothing to do */ + if (domain->device_count) + return; + + ret = zynqmp_pm_release_node(pd->node_id); + if (ret) { + dev_err(&domain->dev, "failed to release PM node id %d: %d\n", + pd->node_id, ret); + return; + } + + pd->requested = false; + + dev_dbg(&domain->dev, "%s released PM node id %d\n", + dev_name(dev), pd->node_id); +} + +static struct generic_pm_domain *zynqmp_gpd_xlate + (struct of_phandle_args *genpdspec, void *data) +{ + struct genpd_onecell_data *genpd_data = data; + unsigned int i, idx = genpdspec->args[0]; + struct zynqmp_pm_domain *pd; + + pd = to_zynqmp_pm_domain(genpd_data->domains[0]); + + if (genpdspec->args_count != 1) + return ERR_PTR(-EINVAL); + + /* Check for existing pm domains */ + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++) { + if (pd[i].node_id == idx) + goto done; + } + + /** + * Add index in empty node_id of power domain list as no existing + * power domain found for current index. + */ + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++) { + if (pd[i].node_id == 0) { + pd[i].node_id = idx; + break; + } + } + +done: + if (!genpd_data->domains[i] || i == ZYNQMP_NUM_DOMAINS) + return ERR_PTR(-ENOENT); + + return genpd_data->domains[i]; +} + +static int zynqmp_gpd_probe(struct platform_device *pdev) +{ + int i; + struct genpd_onecell_data *zynqmp_pd_data; + struct generic_pm_domain **domains; + struct zynqmp_pm_domain *pd; + struct device *dev = &pdev->dev; + + pd = devm_kcalloc(dev, ZYNQMP_NUM_DOMAINS, sizeof(*pd), GFP_KERNEL); + if (!pd) + return -ENOMEM; + + zynqmp_pd_data = devm_kzalloc(dev, sizeof(*zynqmp_pd_data), GFP_KERNEL); + if (!zynqmp_pd_data) + return -ENOMEM; + + zynqmp_pd_data->xlate = zynqmp_gpd_xlate; + + domains = devm_kcalloc(dev, ZYNQMP_NUM_DOMAINS, sizeof(*domains), + GFP_KERNEL); + if (!domains) + return -ENOMEM; + + if (!of_device_is_compatible(dev->parent->of_node, + "xlnx,zynqmp-firmware")) + min_capability = ZYNQMP_PM_CAPABILITY_UNUSABLE; + + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++, pd++) { + pd->node_id = 0; + pd->gpd.name = kasprintf(GFP_KERNEL, "domain%d", i); + pd->gpd.power_off = zynqmp_gpd_power_off; + pd->gpd.power_on = zynqmp_gpd_power_on; + pd->gpd.attach_dev = zynqmp_gpd_attach_dev; + pd->gpd.detach_dev = zynqmp_gpd_detach_dev; + + domains[i] = &pd->gpd; + + /* Mark all PM domains as initially powered off */ + pm_genpd_init(&pd->gpd, NULL, true); + } + + zynqmp_pd_data->domains = domains; + zynqmp_pd_data->num_domains = ZYNQMP_NUM_DOMAINS; + of_genpd_add_provider_onecell(dev->parent->of_node, zynqmp_pd_data); + + return 0; +} + +static int zynqmp_gpd_remove(struct platform_device *pdev) +{ + of_genpd_del_provider(pdev->dev.parent->of_node); + + return 0; +} + +static void zynqmp_gpd_sync_state(struct device *dev) +{ + int ret; + + ret = zynqmp_pm_init_finalize(); + if (ret) + dev_warn(dev, "failed to release power management to firmware\n"); +} + +static struct platform_driver zynqmp_power_domain_driver = { + .driver = { + .name = "zynqmp_power_controller", + .sync_state = zynqmp_gpd_sync_state, + }, + .probe = zynqmp_gpd_probe, + .remove = zynqmp_gpd_remove, +}; +module_platform_driver(zynqmp_power_domain_driver); + +MODULE_ALIAS("platform:zynqmp_power_controller"); diff --git a/drivers/soc/xilinx/zynqmp_power.c b/drivers/soc/xilinx/zynqmp_power.c new file mode 100644 index 000000000..78a8a7545 --- /dev/null +++ b/drivers/soc/xilinx/zynqmp_power.c @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Xilinx Zynq MPSoC Power Management + * + * Copyright (C) 2014-2019 Xilinx, Inc. + * + * Davorin Mista <davorin.mista@aggios.com> + * Jolly Shah <jollys@xilinx.com> + * Rajan Vaja <rajan.vaja@xilinx.com> + */ + +#include <linux/mailbox_client.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/suspend.h> + +#include <linux/firmware/xlnx-zynqmp.h> +#include <linux/firmware/xlnx-event-manager.h> +#include <linux/mailbox/zynqmp-ipi-message.h> + +/** + * struct zynqmp_pm_work_struct - Wrapper for struct work_struct + * @callback_work: Work structure + * @args: Callback arguments + */ +struct zynqmp_pm_work_struct { + struct work_struct callback_work; + u32 args[CB_ARG_CNT]; +}; + +static struct zynqmp_pm_work_struct *zynqmp_pm_init_suspend_work; +static struct mbox_chan *rx_chan; +static bool event_registered; + +enum pm_suspend_mode { + PM_SUSPEND_MODE_FIRST = 0, + PM_SUSPEND_MODE_STD = PM_SUSPEND_MODE_FIRST, + PM_SUSPEND_MODE_POWER_OFF, +}; + +#define PM_SUSPEND_MODE_FIRST PM_SUSPEND_MODE_STD + +static const char *const suspend_modes[] = { + [PM_SUSPEND_MODE_STD] = "standard", + [PM_SUSPEND_MODE_POWER_OFF] = "power-off", +}; + +static enum pm_suspend_mode suspend_mode = PM_SUSPEND_MODE_STD; + +static void zynqmp_pm_get_callback_data(u32 *buf) +{ + zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, 0, 0, 0, 0, buf); +} + +static void suspend_event_callback(const u32 *payload, void *data) +{ + /* First element is callback API ID, others are callback arguments */ + if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) + return; + + /* Copy callback arguments into work's structure */ + memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], + sizeof(zynqmp_pm_init_suspend_work->args)); + + queue_work(system_unbound_wq, &zynqmp_pm_init_suspend_work->callback_work); +} + +static irqreturn_t zynqmp_pm_isr(int irq, void *data) +{ + u32 payload[CB_PAYLOAD_SIZE]; + + zynqmp_pm_get_callback_data(payload); + + /* First element is callback API ID, others are callback arguments */ + if (payload[0] == PM_INIT_SUSPEND_CB) { + switch (payload[1]) { + case SUSPEND_SYSTEM_SHUTDOWN: + orderly_poweroff(true); + break; + case SUSPEND_POWER_REQUEST: + pm_suspend(PM_SUSPEND_MEM); + break; + default: + pr_err("%s Unsupported InitSuspendCb reason " + "code %d\n", __func__, payload[1]); + } + } + + return IRQ_HANDLED; +} + +static void ipi_receive_callback(struct mbox_client *cl, void *data) +{ + struct zynqmp_ipi_message *msg = (struct zynqmp_ipi_message *)data; + u32 payload[CB_PAYLOAD_SIZE]; + int ret; + + memcpy(payload, msg->data, sizeof(msg->len)); + /* First element is callback API ID, others are callback arguments */ + if (payload[0] == PM_INIT_SUSPEND_CB) { + if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) + return; + + /* Copy callback arguments into work's structure */ + memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], + sizeof(zynqmp_pm_init_suspend_work->args)); + + queue_work(system_unbound_wq, + &zynqmp_pm_init_suspend_work->callback_work); + + /* Send NULL message to mbox controller to ack the message */ + ret = mbox_send_message(rx_chan, NULL); + if (ret) + pr_err("IPI ack failed. Error %d\n", ret); + } +} + +/** + * zynqmp_pm_init_suspend_work_fn - Initialize suspend + * @work: Pointer to work_struct + * + * Bottom-half of PM callback IRQ handler. + */ +static void zynqmp_pm_init_suspend_work_fn(struct work_struct *work) +{ + struct zynqmp_pm_work_struct *pm_work = + container_of(work, struct zynqmp_pm_work_struct, callback_work); + + if (pm_work->args[0] == SUSPEND_SYSTEM_SHUTDOWN) { + orderly_poweroff(true); + } else if (pm_work->args[0] == SUSPEND_POWER_REQUEST) { + pm_suspend(PM_SUSPEND_MEM); + } else { + pr_err("%s Unsupported InitSuspendCb reason code %d.\n", + __func__, pm_work->args[0]); + } +} + +static ssize_t suspend_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *s = buf; + int md; + + for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) + if (suspend_modes[md]) { + if (md == suspend_mode) + s += sprintf(s, "[%s] ", suspend_modes[md]); + else + s += sprintf(s, "%s ", suspend_modes[md]); + } + + /* Convert last space to newline */ + if (s != buf) + *(s - 1) = '\n'; + return (s - buf); +} + +static ssize_t suspend_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int md, ret = -EINVAL; + + for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) + if (suspend_modes[md] && + sysfs_streq(suspend_modes[md], buf)) { + ret = 0; + break; + } + + if (!ret && md != suspend_mode) { + ret = zynqmp_pm_set_suspend_mode(md); + if (likely(!ret)) + suspend_mode = md; + } + + return ret ? ret : count; +} + +static DEVICE_ATTR_RW(suspend_mode); + +static int zynqmp_pm_probe(struct platform_device *pdev) +{ + int ret, irq; + u32 pm_api_version; + struct mbox_client *client; + + zynqmp_pm_get_api_version(&pm_api_version); + + /* Check PM API version number */ + if (pm_api_version < ZYNQMP_PM_VERSION) + return -ENODEV; + + /* + * First try to use Xilinx Event Manager by registering suspend_event_callback + * for suspend/shutdown event. + * If xlnx_register_event() returns -EACCES (Xilinx Event Manager + * is not available to use) or -ENODEV(Xilinx Event Manager not compiled), + * then use ipi-mailbox or interrupt method. + */ + ret = xlnx_register_event(PM_INIT_SUSPEND_CB, 0, 0, false, + suspend_event_callback, NULL); + if (!ret) { + zynqmp_pm_init_suspend_work = devm_kzalloc(&pdev->dev, + sizeof(struct zynqmp_pm_work_struct), + GFP_KERNEL); + if (!zynqmp_pm_init_suspend_work) { + xlnx_unregister_event(PM_INIT_SUSPEND_CB, 0, 0, + suspend_event_callback, NULL); + return -ENOMEM; + } + event_registered = true; + + INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, + zynqmp_pm_init_suspend_work_fn); + } else if (ret != -EACCES && ret != -ENODEV) { + dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n", ret); + return ret; + } else if (of_find_property(pdev->dev.of_node, "mboxes", NULL)) { + zynqmp_pm_init_suspend_work = + devm_kzalloc(&pdev->dev, + sizeof(struct zynqmp_pm_work_struct), + GFP_KERNEL); + if (!zynqmp_pm_init_suspend_work) + return -ENOMEM; + + INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, + zynqmp_pm_init_suspend_work_fn); + client = devm_kzalloc(&pdev->dev, sizeof(*client), GFP_KERNEL); + if (!client) + return -ENOMEM; + + client->dev = &pdev->dev; + client->rx_callback = ipi_receive_callback; + + rx_chan = mbox_request_channel_byname(client, "rx"); + if (IS_ERR(rx_chan)) { + dev_err(&pdev->dev, "Failed to request rx channel\n"); + return PTR_ERR(rx_chan); + } + } else if (of_find_property(pdev->dev.of_node, "interrupts", NULL)) { + irq = platform_get_irq(pdev, 0); + if (irq <= 0) + return -ENXIO; + + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + zynqmp_pm_isr, + IRQF_NO_SUSPEND | IRQF_ONESHOT, + dev_name(&pdev->dev), + &pdev->dev); + if (ret) { + dev_err(&pdev->dev, "devm_request_threaded_irq '%d' " + "failed with %d\n", irq, ret); + return ret; + } + } else { + dev_err(&pdev->dev, "Required property not found in DT node\n"); + return -ENOENT; + } + + ret = sysfs_create_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); + if (ret) { + if (event_registered) { + xlnx_unregister_event(PM_INIT_SUSPEND_CB, 0, 0, suspend_event_callback, + NULL); + event_registered = false; + } + dev_err(&pdev->dev, "unable to create sysfs interface\n"); + return ret; + } + + return 0; +} + +static int zynqmp_pm_remove(struct platform_device *pdev) +{ + sysfs_remove_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); + if (event_registered) + xlnx_unregister_event(PM_INIT_SUSPEND_CB, 0, 0, suspend_event_callback, NULL); + + if (!rx_chan) + mbox_free_channel(rx_chan); + + return 0; +} + +static const struct of_device_id pm_of_match[] = { + { .compatible = "xlnx,zynqmp-power", }, + { /* end of table */ }, +}; +MODULE_DEVICE_TABLE(of, pm_of_match); + +static struct platform_driver zynqmp_pm_platform_driver = { + .probe = zynqmp_pm_probe, + .remove = zynqmp_pm_remove, + .driver = { + .name = "zynqmp_power", + .of_match_table = pm_of_match, + }, +}; +module_platform_driver(zynqmp_pm_platform_driver); |