| /* Copyright (c) 2016-2017, The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/cdev.h> |
| #include <linux/interrupt.h> |
| #include <linux/ipc_logging.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_irq.h> |
| #include <linux/platform_device.h> |
| #include <linux/poll.h> |
| #include <linux/slab.h> |
| #include <soc/qcom/subsystem_notif.h> |
| #include <soc/qcom/subsystem_restart.h> |
| |
| #define MODULE_NAME "qsee_ipc_irq_bridge" |
| #define DEVICE_NAME MODULE_NAME |
| #define NUM_LOG_PAGES 4 |
| |
| #define QIIB_DBG(x...) do { \ |
| if (qiib_info->log_ctx) \ |
| ipc_log_string(qiib_info->log_ctx, x); \ |
| else \ |
| pr_debug(x); \ |
| } while (0) |
| |
| #define QIIB_ERR(x...) do { \ |
| pr_err(x); \ |
| if (qiib_info->log_ctx) \ |
| ipc_log_string(qiib_info->log_ctx, x); \ |
| } while (0) |
| |
| static void qiib_cleanup(void); |
| |
| /** |
| * qiib_dev - QSEE IPC IRQ bridge device |
| * @dev_list: qiib device list. |
| * @i: Index to this character device. |
| * @dev_name: Device node name used by the clients. |
| * @cdev: structure to the internal character device. |
| * @devicep: Pointer to the qiib class device structure. |
| * @poll_wait_queue: poll thread wait queue. |
| * @irq_num: IRQ number usd for this device. |
| * @rx_irq_reset_reg: Reference to the register to reset the rx irq |
| * line, if applicable. |
| * @irq_mask: Mask written to @rx_irq_reset_reg to clear the irq. |
| * @irq_pending_count: The number of IRQs pending. |
| * @irq_pending_count_lock: Lock to protect @irq_pending_cont. |
| * @ssr_name: Name of the subsystem recognized by the SSR framework. |
| * @nb: SSR Notifier callback. |
| * @notifier_handle: SSR Notifier handle. |
| * @in_reset: Flag to check the SSR state. |
| */ |
| struct qiib_dev { |
| struct list_head dev_list; |
| uint32_t i; |
| |
| const char *dev_name; |
| struct cdev cdev; |
| struct device *devicep; |
| |
| wait_queue_head_t poll_wait_queue; |
| |
| uint32_t irq_line; |
| void __iomem *rx_irq_reset_reg; |
| uint32_t irq_mask; |
| uint32_t irq_pending_count; |
| spinlock_t irq_pending_count_lock; |
| |
| const char *ssr_name; |
| struct notifier_block nb; |
| void *notifier_handle; |
| bool in_reset; |
| }; |
| |
| /** |
| * qiib_driver_data - QSEE IPC IRQ bridge driver data |
| * @list: list of all nodes devices. |
| * @list_lock: lock to synchronize the @list access. |
| * @nprots: Number of device nodes. |
| * @classp: Pointer to the device class. |
| * @dev_num: qiib device number. |
| * @log_ctx: pointer to the ipc logging context. |
| */ |
| struct qiib_driver_data { |
| struct list_head list; |
| struct mutex list_lock; |
| |
| int nports; |
| struct class *classp; |
| dev_t dev_num; |
| |
| void *log_ctx; |
| }; |
| |
| static struct qiib_driver_data *qiib_info; |
| |
| /** |
| * qiib_driver_data_init() - Initialize the QIIB driver data. |
| * |
| * This function used to initialize the driver specific data |
| * during the module init. |
| * |
| * Return: 0 for success, Standard Linux errors |
| */ |
| static int qiib_driver_data_init(void) |
| { |
| qiib_info = kzalloc(sizeof(*qiib_info), GFP_KERNEL); |
| if (!qiib_info) |
| return -ENOMEM; |
| |
| INIT_LIST_HEAD(&qiib_info->list); |
| mutex_init(&qiib_info->list_lock); |
| |
| qiib_info->log_ctx = ipc_log_context_create(NUM_LOG_PAGES, |
| "qsee_ipc_irq_bridge", 0); |
| if (!qiib_info->log_ctx) |
| QIIB_ERR("%s: unable to create logging context\n", __func__); |
| |
| return 0; |
| } |
| |
| /** |
| * qiib_driver_data_deinit() - De-Initialize the QIIB driver data. |
| * |
| * This function used to de-initialize the driver specific data |
| * during the module exit. |
| */ |
| static void qiib_driver_data_deinit(void) |
| { |
| qiib_cleanup(); |
| if (!qiib_info->log_ctx) |
| ipc_log_context_destroy(qiib_info->log_ctx); |
| kfree(qiib_info); |
| qiib_info = NULL; |
| } |
| |
| /** |
| * qiib_restart_notifier_cb() - SSR restart notifier callback function |
| * @this: Notifier block used by the SSR framework |
| * @code: The SSR code for which stage of restart is occurring |
| * @data: Structure containing private data - not used here. |
| * |
| * This function is a callback for the SSR framework. From here we initiate |
| * our handling of SSR. |
| * |
| * Return: Status of SSR handling |
| */ |
| static int qiib_restart_notifier_cb(struct notifier_block *this, |
| unsigned long code, |
| void *data) |
| { |
| struct qiib_dev *devp = container_of(this, struct qiib_dev, nb); |
| |
| if (code == SUBSYS_BEFORE_SHUTDOWN) { |
| QIIB_DBG("%s: %s: subsystem restart for %s\n", __func__, |
| "SUBSYS_BEFORE_SHUTDOWN", |
| devp->ssr_name); |
| devp->in_reset = true; |
| wake_up_interruptible(&devp->poll_wait_queue); |
| } else if (code == SUBSYS_AFTER_POWERUP) { |
| QIIB_DBG("%s: %s: subsystem restart for %s\n", __func__, |
| "SUBSYS_AFTER_POWERUP", |
| devp->ssr_name); |
| devp->in_reset = false; |
| } |
| return NOTIFY_DONE; |
| } |
| |
| /** |
| * qiib_poll() - poll() syscall for the qiib device |
| * @file: Pointer to the file structure. |
| * @wait: pointer to Poll table. |
| * |
| * This function is used to poll on the qiib device when |
| * userspace client do a poll() system call. All input arguments are |
| * validated by the virtual file system before calling this function. |
| * |
| * Return: POLLIN for interrupt intercepted case and POLLRDHUP for SSR. |
| */ |
| static unsigned int qiib_poll(struct file *file, poll_table *wait) |
| { |
| struct qiib_dev *devp = file->private_data; |
| unsigned int mask = 0; |
| unsigned long flags; |
| |
| if (!devp) { |
| QIIB_ERR("%s on NULL device\n", __func__); |
| return POLLERR; |
| } |
| |
| if (devp->in_reset) |
| return POLLRDHUP; |
| |
| poll_wait(file, &devp->poll_wait_queue, wait); |
| spin_lock_irqsave(&devp->irq_pending_count_lock, flags); |
| if (devp->irq_pending_count) { |
| mask |= POLLIN; |
| QIIB_DBG("%s set POLLIN on [%s] count[%d]\n", |
| __func__, devp->dev_name, |
| devp->irq_pending_count); |
| devp->irq_pending_count = 0; |
| } |
| spin_unlock_irqrestore(&devp->irq_pending_count_lock, flags); |
| |
| if (devp->in_reset) { |
| mask |= POLLRDHUP; |
| QIIB_DBG("%s set POLLRDHUP on [%s] count[%d]\n", |
| __func__, devp->dev_name, |
| devp->irq_pending_count); |
| } |
| return mask; |
| } |
| |
| /** |
| * qiib_open() - open() syscall for the qiib device |
| * @inode: Pointer to the inode structure. |
| * @file: Pointer to the file structure. |
| * |
| * This function is used to open the qiib device when |
| * userspace client do a open() system call. All input arguments are |
| * validated by the virtual file system before calling this function. |
| * |
| * Return: 0 for success, Standard Linux errors |
| */ |
| static int qiib_open(struct inode *inode, struct file *file) |
| { |
| struct qiib_dev *devp = NULL; |
| |
| devp = container_of(inode->i_cdev, struct qiib_dev, cdev); |
| if (!devp) { |
| QIIB_ERR("%s on NULL device\n", __func__); |
| return -EINVAL; |
| } |
| file->private_data = devp; |
| QIIB_DBG("%s on [%s]\n", __func__, devp->dev_name); |
| return 0; |
| } |
| |
| /** |
| * qiib_release() - release operation on qiibdevice |
| * @inode: Pointer to the inode structure. |
| * @file: Pointer to the file structure. |
| * |
| * This function is used to release the qiib device when |
| * userspace client do a close() system call. All input arguments are |
| * validated by the virtual file system before calling this function. |
| */ |
| static int qiib_release(struct inode *inode, struct file *file) |
| { |
| struct qiib_dev *devp = file->private_data; |
| |
| if (!devp) { |
| QIIB_ERR("%s on NULL device\n", __func__); |
| return -EINVAL; |
| } |
| |
| QIIB_DBG("%s on [%s]\n", __func__, devp->dev_name); |
| return 0; |
| } |
| |
| static const struct file_operations qiib_fops = { |
| .owner = THIS_MODULE, |
| .open = qiib_open, |
| .release = qiib_release, |
| .poll = qiib_poll, |
| }; |
| |
| /** |
| * qiib_add_device() - Initialize qiib device and add cdev |
| * @devp: pointer to the qiib device. |
| * @i: index of the qiib device. |
| * |
| * Return: 0 for success, Standard Linux errors |
| */ |
| static int qiib_add_device(struct qiib_dev *devp, int i) |
| { |
| int ret = 0; |
| |
| devp->i = i; |
| init_waitqueue_head(&devp->poll_wait_queue); |
| spin_lock_init(&devp->irq_pending_count_lock); |
| |
| cdev_init(&devp->cdev, &qiib_fops); |
| devp->cdev.owner = THIS_MODULE; |
| |
| ret = cdev_add(&devp->cdev, qiib_info->dev_num + i, 1); |
| if (IS_ERR_VALUE((unsigned long)ret)) { |
| QIIB_ERR("%s: cdev_add() failed for dev [%s] ret:%i\n", |
| __func__, devp->dev_name, ret); |
| return ret; |
| } |
| |
| devp->devicep = device_create(qiib_info->classp, |
| NULL, |
| (qiib_info->dev_num + i), |
| NULL, |
| devp->dev_name); |
| |
| if (IS_ERR_OR_NULL(devp->devicep)) { |
| QIIB_ERR("%s: device_create() failed for dev [%s]\n", |
| __func__, devp->dev_name); |
| ret = -ENOMEM; |
| cdev_del(&devp->cdev); |
| return ret; |
| } |
| |
| mutex_lock(&qiib_info->list_lock); |
| list_add(&devp->dev_list, &qiib_info->list); |
| mutex_unlock(&qiib_info->list_lock); |
| |
| return ret; |
| } |
| |
| static irqreturn_t qiib_irq_handler(int irq, void *priv) |
| { |
| struct qiib_dev *devp = priv; |
| unsigned long flags; |
| |
| spin_lock_irqsave(&devp->irq_pending_count_lock, flags); |
| devp->irq_pending_count++; |
| spin_unlock_irqrestore(&devp->irq_pending_count_lock, flags); |
| wake_up_interruptible(&devp->poll_wait_queue); |
| |
| if (devp->rx_irq_reset_reg) |
| writel_relaxed(devp->irq_mask, devp->rx_irq_reset_reg); |
| |
| QIIB_DBG("%s name[%s] pend_count[%d]\n", __func__, |
| devp->dev_name, devp->irq_pending_count); |
| |
| return IRQ_HANDLED; |
| } |
| |
| /** |
| * qiib_parse_node() - parse node from device tree binding |
| * @node: pointer to device tree node |
| * @devp: pointer to the qiib device |
| * |
| * Return: 0 on success, -ENODEV on failure. |
| */ |
| static int qiib_parse_node(struct device_node *node, struct qiib_dev *devp) |
| { |
| char *key; |
| const char *subsys_name; |
| const char *dev_name; |
| uint32_t irqtype; |
| uint32_t irq_clear[2]; |
| struct irq_data *irqtype_data; |
| int ret = -ENODEV; |
| |
| key = "qcom,dev-name"; |
| dev_name = of_get_property(node, key, NULL); |
| if (!dev_name) { |
| QIIB_ERR("%s: missing key: %s\n", __func__, key); |
| goto missing_key; |
| } |
| QIIB_DBG("%s: %s = %s\n", __func__, key, dev_name); |
| |
| key = "interrupts"; |
| devp->irq_line = irq_of_parse_and_map(node, 0); |
| if (!devp->irq_line) { |
| QIIB_ERR("%s: missing key: %s\n", __func__, key); |
| goto missing_key; |
| } |
| QIIB_DBG("%s: %s = %d\n", __func__, key, devp->irq_line); |
| |
| irqtype_data = irq_get_irq_data(devp->irq_line); |
| if (!irqtype_data) { |
| QIIB_ERR("%s: get irqdata fail:%d\n", __func__, devp->irq_line); |
| goto missing_key; |
| } |
| irqtype = irqd_get_trigger_type(irqtype_data); |
| QIIB_DBG("%s: irqtype = %d\n", __func__, irqtype); |
| |
| key = "label"; |
| subsys_name = of_get_property(node, key, NULL); |
| if (!subsys_name) { |
| QIIB_ERR("%s: missing key: %s\n", __func__, key); |
| goto missing_key; |
| } |
| QIIB_DBG("%s: %s = %s\n", __func__, key, subsys_name); |
| |
| if (irqtype & IRQF_TRIGGER_HIGH) { |
| key = "qcom,rx-irq-clr-mask"; |
| ret = of_property_read_u32(node, key, &devp->irq_mask); |
| if (ret) { |
| QIIB_ERR("%s: missing key: %s\n", __func__, key); |
| ret = -ENODEV; |
| goto missing_key; |
| } |
| QIIB_DBG("%s: %s = %d\n", __func__, key, devp->irq_mask); |
| |
| key = "qcom,rx-irq-clr"; |
| ret = of_property_read_u32_array(node, key, irq_clear, |
| ARRAY_SIZE(irq_clear)); |
| if (ret) { |
| QIIB_ERR("%s: missing key: %s\n", __func__, key); |
| ret = -ENODEV; |
| goto missing_key; |
| } |
| |
| devp->rx_irq_reset_reg = ioremap_nocache(irq_clear[0], |
| irq_clear[1]); |
| if (!devp->rx_irq_reset_reg) { |
| QIIB_ERR("%s: unable to map rx reset reg\n", __func__); |
| ret = -ENOMEM; |
| goto missing_key; |
| } |
| } |
| |
| devp->dev_name = dev_name; |
| devp->ssr_name = subsys_name; |
| devp->nb.notifier_call = qiib_restart_notifier_cb; |
| |
| devp->notifier_handle = subsys_notif_register_notifier(devp->ssr_name, |
| &devp->nb); |
| if (IS_ERR_OR_NULL(devp->notifier_handle)) { |
| QIIB_ERR("%s: Could not register SSR notifier cb\n", __func__); |
| ret = -EINVAL; |
| goto ssr_reg_fail; |
| } |
| |
| ret = request_irq(devp->irq_line, qiib_irq_handler, |
| irqtype | IRQF_NO_SUSPEND, |
| devp->dev_name, devp); |
| if (ret < 0) { |
| QIIB_ERR("%s: request_irq() failed on %d\n", __func__, |
| devp->irq_line); |
| goto req_irq_fail; |
| } else { |
| ret = enable_irq_wake(devp->irq_line); |
| if (ret < 0) |
| QIIB_ERR("%s: enable_irq_wake() failed on %d\n", |
| __func__, devp->irq_line); |
| } |
| |
| return ret; |
| |
| req_irq_fail: |
| subsys_notif_unregister_notifier(devp->notifier_handle, &devp->nb); |
| ssr_reg_fail: |
| if (devp->rx_irq_reset_reg) { |
| iounmap(devp->rx_irq_reset_reg); |
| devp->rx_irq_reset_reg = NULL; |
| } |
| missing_key: |
| return ret; |
| } |
| |
| /** |
| * qiib_cleanup - cleanup all the resources |
| * |
| * This function remove all the memory and unregister |
| * the char device region. |
| */ |
| static void qiib_cleanup(void) |
| { |
| struct qiib_dev *devp; |
| struct qiib_dev *index; |
| |
| mutex_lock(&qiib_info->list_lock); |
| list_for_each_entry_safe(devp, index, &qiib_info->list, dev_list) { |
| cdev_del(&devp->cdev); |
| list_del(&devp->dev_list); |
| device_destroy(qiib_info->classp, |
| MKDEV(MAJOR(qiib_info->dev_num), devp->i)); |
| if (devp->notifier_handle) |
| subsys_notif_unregister_notifier(devp->notifier_handle, |
| &devp->nb); |
| kfree(devp); |
| } |
| mutex_unlock(&qiib_info->list_lock); |
| |
| if (!IS_ERR_OR_NULL(qiib_info->classp)) |
| class_destroy(qiib_info->classp); |
| |
| unregister_chrdev_region(MAJOR(qiib_info->dev_num), qiib_info->nports); |
| } |
| |
| /** |
| * qiib_alloc_chrdev_region() - allocate the char device region |
| * |
| * This function allocate memory for qiib character-device region and |
| * create the class. |
| */ |
| static int qiib_alloc_chrdev_region(void) |
| { |
| int ret; |
| |
| ret = alloc_chrdev_region(&qiib_info->dev_num, |
| 0, |
| qiib_info->nports, |
| DEVICE_NAME); |
| if (IS_ERR_VALUE((unsigned long)ret)) { |
| QIIB_ERR("%s: alloc_chrdev_region() failed ret:%i\n", |
| __func__, ret); |
| return ret; |
| } |
| |
| qiib_info->classp = class_create(THIS_MODULE, DEVICE_NAME); |
| if (IS_ERR(qiib_info->classp)) { |
| QIIB_ERR("%s: class_create() failed ENOMEM\n", __func__); |
| ret = -ENOMEM; |
| unregister_chrdev_region(MAJOR(qiib_info->dev_num), |
| qiib_info->nports); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int qsee_ipc_irq_bridge_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct device_node *node; |
| struct qiib_dev *devp; |
| int i = 0; |
| |
| qiib_info->nports = of_get_available_child_count(pdev->dev.of_node); |
| if (!qiib_info->nports) { |
| QIIB_ERR("%s:Fail nports = %d\n", __func__, qiib_info->nports); |
| return -EINVAL; |
| } |
| |
| ret = qiib_alloc_chrdev_region(); |
| if (ret) { |
| QIIB_ERR("%s: chrdev_region allocation failed ret:%i\n", |
| __func__, ret); |
| return ret; |
| } |
| |
| for_each_available_child_of_node(pdev->dev.of_node, node) { |
| devp = kzalloc(sizeof(*devp), GFP_KERNEL); |
| if (IS_ERR_OR_NULL(devp)) { |
| QIIB_ERR("%s:Allocation failed id:%d\n", __func__, i); |
| ret = -ENOMEM; |
| goto error; |
| } |
| |
| ret = qiib_parse_node(node, devp); |
| if (ret) { |
| QIIB_ERR("%s:qiib_parse_node failed %d\n", __func__, i); |
| kfree(devp); |
| goto error; |
| } |
| |
| ret = qiib_add_device(devp, i); |
| if (ret < 0) { |
| QIIB_ERR("%s: add [%s] device failed ret=%d\n", |
| __func__, devp->dev_name, ret); |
| kfree(devp); |
| goto error; |
| } |
| i++; |
| } |
| |
| QIIB_DBG("%s: Driver Initialized.\n", __func__); |
| return 0; |
| |
| error: |
| qiib_cleanup(); |
| return ret; |
| } |
| |
| static int qsee_ipc_irq_bridge_remove(struct platform_device *pdev) |
| { |
| qiib_cleanup(); |
| return 0; |
| } |
| |
| static const struct of_device_id qsee_ipc_irq_bridge_match_table[] = { |
| { .compatible = "qcom,qsee-ipc-irq-bridge" }, |
| {}, |
| }; |
| |
| static struct platform_driver qsee_ipc_irq_bridge_driver = { |
| .probe = qsee_ipc_irq_bridge_probe, |
| .remove = qsee_ipc_irq_bridge_remove, |
| .driver = { |
| .name = MODULE_NAME, |
| .owner = THIS_MODULE, |
| .of_match_table = qsee_ipc_irq_bridge_match_table, |
| }, |
| }; |
| |
| static int __init qsee_ipc_irq_bridge_init(void) |
| { |
| int ret; |
| |
| ret = qiib_driver_data_init(); |
| if (ret) { |
| QIIB_ERR("%s: driver data init failed %d\n", |
| __func__, ret); |
| return ret; |
| } |
| |
| ret = platform_driver_register(&qsee_ipc_irq_bridge_driver); |
| if (ret) { |
| QIIB_ERR("%s: platform driver register failed %d\n", |
| __func__, ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| module_init(qsee_ipc_irq_bridge_init); |
| |
| static void __exit qsee_ipc_irq_bridge_exit(void) |
| { |
| platform_driver_unregister(&qsee_ipc_irq_bridge_driver); |
| qiib_driver_data_deinit(); |
| } |
| module_exit(qsee_ipc_irq_bridge_exit); |
| MODULE_DESCRIPTION("QSEE IPC interrupt bridge"); |
| MODULE_LICENSE("GPL v2"); |