| /* |
| * Driver to expose PCI-express hot-removal of devices via file |
| * handles. |
| * |
| * Copyright (C) 2018 Google, Inc. |
| * Author: June Tate-Gans <jtgans@google.com> |
| * |
| * Shamelessly the general structure of the character driver has been |
| * borrowed from bsr.c, the POWER architecture's method for accessing |
| * BSR registers. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * 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/types.h> |
| #include <linux/module.h> |
| |
| #include <asm/cmpxchg.h> |
| #include <linux/cdev.h> |
| #include <linux/device.h> |
| #include <linux/delay.h> |
| #include <linux/fs.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/of_platform.h> |
| #include <linux/pci.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/workqueue.h> |
| |
| #if defined(CONFIG_IMX8MQ_PHANBELL_POWERSAVE) |
| #include <linux/busfreq-imx.h> |
| #endif |
| |
| #define APEX_PCI_VENDOR_ID 0x1ac1 |
| #define APEX_PCI_DEVICE_ID 0x089a |
| |
| static DEFINE_MUTEX(power_owned_lock); |
| |
| static struct class *apex_power_class; |
| static struct device *apex_power_device; |
| static struct cdev apex_power_cdev; |
| static int apex_power_major; |
| static bool apex_power_owned; |
| static struct delayed_work apex_power_delayed_init; |
| |
| static struct regulator *get_apex_regulator(void) { |
| struct regulator *supply; |
| supply = regulator_get(NULL, "apex_regulators"); |
| if (IS_ERR(supply)) { |
| printk(KERN_ERR "apex_power: Unable to find regulator.\n"); |
| } |
| return supply; |
| } |
| static int apex_power_down(void) |
| { |
| struct pci_dev *apex_dev = NULL; |
| struct pci_dev *apex_connected_bus = NULL; |
| struct regulator *supply; |
| int ret = 0; |
| |
| #if defined(CONFIG_IMX8MQ_PHANBELL_POWERSAVE) |
| if (apex_power_owned) { |
| release_bus_freq(BUS_FREQ_HIGH); |
| } |
| #endif |
| |
| apex_dev = pci_get_device(APEX_PCI_VENDOR_ID, APEX_PCI_DEVICE_ID, NULL); |
| if (!apex_dev) { |
| printk(KERN_ERR "apex_power: can't find Apex on PCI bus?!\n"); |
| return -EIO; |
| } |
| apex_connected_bus = apex_dev->bus->self; |
| pci_dev_put(apex_dev); |
| |
| pci_stop_and_remove_bus_device_locked(apex_connected_bus); |
| |
| supply = get_apex_regulator(); |
| ret = regulator_disable(supply); |
| if (ret) { |
| printk(KERN_ERR "apex_power: Unable to disable regulator.\n"); |
| } |
| |
| return 0; |
| } |
| |
| static int apex_power_up(void) |
| { |
| struct pci_bus *pci_bus = NULL; |
| struct pci_dev *apex_dev = NULL; |
| int ret = 0; |
| struct regulator *supply; |
| |
| supply = get_apex_regulator(); |
| ret = regulator_enable(supply); |
| if (ret) { |
| printk(KERN_ERR "apex_power: Unable to enable regulator.\n"); |
| } |
| |
| #if defined(CONFIG_IMX8MQ_PHANBELL_POWERSAVE) |
| request_bus_freq(BUS_FREQ_HIGH); |
| #endif |
| |
| pci_lock_rescan_remove(); |
| while ((pci_bus = pci_find_next_bus(pci_bus)) != NULL) { |
| pci_rescan_bus(pci_bus); |
| } |
| pci_unlock_rescan_remove(); |
| |
| apex_dev = pci_get_device(APEX_PCI_VENDOR_ID, APEX_PCI_DEVICE_ID, NULL); |
| if (!apex_dev) { |
| printk(KERN_ERR "apex_power: can't find Apex on PCI bus?!\n"); |
| |
| #if defined(CONFIG_IMX8MQ_PHANBELL_POWERSAVE) |
| release_bus_freq(BUS_FREQ_HIGH); |
| #endif |
| |
| return -EIO; |
| } |
| pci_dev_put(apex_dev); |
| |
| return 0; |
| } |
| |
| static int apex_power_release(struct inode* inode, struct file* filep) |
| { |
| int ret = 0; |
| |
| if (mutex_lock_interruptible(&power_owned_lock)) { |
| return -ENOLCK; |
| } |
| |
| if (apex_power_owned != true) { |
| ret = -EPERM; |
| goto out; |
| } |
| |
| ret = apex_power_down(); |
| apex_power_owned = false; |
| |
| out: |
| mutex_unlock(&power_owned_lock); |
| return ret; |
| } |
| |
| static int apex_power_open(struct inode* inode, struct file* filep) |
| { |
| int ret = 0; |
| |
| if (mutex_lock_interruptible(&power_owned_lock)) { |
| return -ENOLCK; |
| } |
| |
| if (apex_power_owned != false) { |
| ret = -EPERM; |
| goto out; |
| } |
| apex_power_owned = true; |
| |
| /* Ensure that we don't accidentally run afoul of the initial delayed |
| power down. */ |
| cancel_delayed_work_sync(&apex_power_delayed_init); |
| |
| ret = apex_power_up(); |
| |
| out: |
| mutex_unlock(&power_owned_lock); |
| return ret; |
| } |
| |
| static const struct file_operations apex_power_fops = { |
| .owner = THIS_MODULE, |
| .open = apex_power_open, |
| .release = apex_power_release, |
| }; |
| |
| static void apex_power_delayed_init_callback(struct work_struct *work) |
| { |
| struct pci_dev *apex_dev = NULL; |
| apex_dev = pci_get_device(APEX_PCI_VENDOR_ID, APEX_PCI_DEVICE_ID, NULL); |
| |
| if (!apex_dev) { |
| printk(KERN_INFO "apex_power: rescheduling late init power down\n"); |
| schedule_delayed_work(&apex_power_delayed_init, |
| msecs_to_jiffies(1000)); |
| return; |
| } |
| |
| pci_dev_put(apex_dev); |
| |
| printk(KERN_INFO "apex_power: init routines powering down apex\n"); |
| |
| if (mutex_lock_interruptible(&power_owned_lock)) { |
| printk(KERN_ERR "apex_power: Unable to lock mutex.\n"); |
| return; |
| } |
| |
| apex_power_down(); |
| |
| mutex_unlock(&power_owned_lock); |
| } |
| |
| static int __init apex_power_init(void) |
| { |
| dev_t apex_power_dev; |
| int apex_power_major; |
| int ret = 0; |
| struct regulator *supply; |
| // Enable the supply in regulator framework to ensure power isn't removed |
| // until init is complete. |
| |
| supply = get_apex_regulator(); |
| ret = regulator_enable(supply); |
| if (ret) { |
| printk(KERN_ERR "apex_power: Unable to enable regulator.\n"); |
| } |
| |
| apex_power_class = class_create(THIS_MODULE, "apex_power"); |
| if (IS_ERR(apex_power_class)) { |
| printk(KERN_ERR "apex_power: could not allocate device class\n"); |
| return -ENODEV; |
| } |
| |
| ret = alloc_chrdev_region(&apex_power_dev, 0, 1, "apex_power"); |
| apex_power_major = MAJOR(apex_power_dev); |
| if (ret < 0) { |
| printk(KERN_ERR "apex_power: alloc_chrdev_region() failed\n"); |
| class_destroy(apex_power_class); |
| return -ENODEV; |
| } |
| |
| apex_power_dev = MKDEV(apex_power_major, 0); |
| cdev_init(&apex_power_cdev, &apex_power_fops); |
| ret = cdev_add(&apex_power_cdev, apex_power_dev, 1); |
| |
| if (ret) { |
| printk(KERN_ERR "apex_power: Unable to add cdev!\n"); |
| unregister_chrdev_region(apex_power_dev, 1); |
| class_destroy(apex_power_class); |
| } |
| |
| apex_power_device = device_create(apex_power_class, NULL, apex_power_dev, |
| NULL, "%s", "apex_power"); |
| if (IS_ERR(apex_power_device)) { |
| printk(KERN_ERR "apex_power: device_create failed.\n"); |
| cdev_del(&apex_power_cdev); |
| unregister_chrdev_region(apex_power_dev, 1); |
| class_destroy(apex_power_class); |
| } |
| |
| INIT_DELAYED_WORK(&apex_power_delayed_init, |
| apex_power_delayed_init_callback); |
| schedule_delayed_work(&apex_power_delayed_init, msecs_to_jiffies(7000)); |
| |
| printk(KERN_INFO "apex_power: initialized.\n"); |
| |
| return 0; |
| } |
| |
| static void __exit apex_power_exit(void) |
| { |
| device_del(apex_power_device); |
| cdev_del(&apex_power_cdev); |
| unregister_chrdev_region(MKDEV(apex_power_major, 0), 1); |
| class_destroy(apex_power_class); |
| } |
| |
| module_init(apex_power_init); |
| module_exit(apex_power_exit); |
| MODULE_DESCRIPTION("Google Apex power management driver"); |
| MODULE_AUTHOR("June Tate-Gans <jtgans@google.com>"); |
| MODULE_ALIAS("platform:apex-power"); |
| MODULE_LICENSE("GPL"); |