| /* |
| * drivers/platform/x86/mlxcpld-hotplug.c |
| * Copyright (c) 2016 Mellanox Technologies. All rights reserved. |
| * Copyright (c) 2016 Vadim Pasternak <vadimp@mellanox.com> |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the names of the copyright holders nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * Alternatively, this software may be distributed under the terms of the |
| * GNU General Public License ("GPL") version 2 as published by the Free |
| * Software Foundation. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
| * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| * POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/device.h> |
| #include <linux/hwmon.h> |
| #include <linux/hwmon-sysfs.h> |
| #include <linux/i2c.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/platform_data/mlxcpld-hotplug.h> |
| #include <linux/platform_device.h> |
| #include <linux/spinlock.h> |
| #include <linux/wait.h> |
| #include <linux/workqueue.h> |
| |
| /* Offset of event and mask registers from status register */ |
| #define MLXCPLD_HOTPLUG_EVENT_OFF 1 |
| #define MLXCPLD_HOTPLUG_MASK_OFF 2 |
| #define MLXCPLD_HOTPLUG_AGGR_MASK_OFF 1 |
| |
| #define MLXCPLD_HOTPLUG_ATTRS_NUM 8 |
| |
| /** |
| * enum mlxcpld_hotplug_attr_type - sysfs attributes for hotplug events: |
| * @MLXCPLD_HOTPLUG_ATTR_TYPE_PSU: power supply unit attribute; |
| * @MLXCPLD_HOTPLUG_ATTR_TYPE_PWR: power cable attribute; |
| * @MLXCPLD_HOTPLUG_ATTR_TYPE_FAN: FAN drawer attribute; |
| */ |
| enum mlxcpld_hotplug_attr_type { |
| MLXCPLD_HOTPLUG_ATTR_TYPE_PSU, |
| MLXCPLD_HOTPLUG_ATTR_TYPE_PWR, |
| MLXCPLD_HOTPLUG_ATTR_TYPE_FAN, |
| }; |
| |
| /** |
| * struct mlxcpld_hotplug_priv_data - platform private data: |
| * @irq: platform interrupt number; |
| * @pdev: platform device; |
| * @plat: platform data; |
| * @hwmon: hwmon device; |
| * @mlxcpld_hotplug_attr: sysfs attributes array; |
| * @mlxcpld_hotplug_dev_attr: sysfs sensor device attribute array; |
| * @group: sysfs attribute group; |
| * @groups: list of sysfs attribute group for hwmon registration; |
| * @dwork: delayed work template; |
| * @lock: spin lock; |
| * @aggr_cache: last value of aggregation register status; |
| * @psu_cache: last value of PSU register status; |
| * @pwr_cache: last value of power register status; |
| * @fan_cache: last value of FAN register status; |
| */ |
| struct mlxcpld_hotplug_priv_data { |
| int irq; |
| struct platform_device *pdev; |
| struct mlxcpld_hotplug_platform_data *plat; |
| struct device *hwmon; |
| struct attribute *mlxcpld_hotplug_attr[MLXCPLD_HOTPLUG_ATTRS_NUM + 1]; |
| struct sensor_device_attribute_2 |
| mlxcpld_hotplug_dev_attr[MLXCPLD_HOTPLUG_ATTRS_NUM]; |
| struct attribute_group group; |
| const struct attribute_group *groups[2]; |
| struct delayed_work dwork; |
| spinlock_t lock; |
| u8 aggr_cache; |
| u8 psu_cache; |
| u8 pwr_cache; |
| u8 fan_cache; |
| }; |
| |
| static ssize_t mlxcpld_hotplug_attr_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct mlxcpld_hotplug_priv_data *priv = platform_get_drvdata(pdev); |
| int index = to_sensor_dev_attr_2(attr)->index; |
| int nr = to_sensor_dev_attr_2(attr)->nr; |
| u8 reg_val = 0; |
| |
| switch (nr) { |
| case MLXCPLD_HOTPLUG_ATTR_TYPE_PSU: |
| /* Bit = 0 : PSU is present. */ |
| reg_val = !!!(inb(priv->plat->psu_reg_offset) & BIT(index)); |
| break; |
| |
| case MLXCPLD_HOTPLUG_ATTR_TYPE_PWR: |
| /* Bit = 1 : power cable is attached. */ |
| reg_val = !!(inb(priv->plat->pwr_reg_offset) & BIT(index % |
| priv->plat->pwr_count)); |
| break; |
| |
| case MLXCPLD_HOTPLUG_ATTR_TYPE_FAN: |
| /* Bit = 0 : FAN is present. */ |
| reg_val = !!!(inb(priv->plat->fan_reg_offset) & BIT(index % |
| priv->plat->fan_count)); |
| break; |
| } |
| |
| return sprintf(buf, "%u\n", reg_val); |
| } |
| |
| #define PRIV_ATTR(i) priv->mlxcpld_hotplug_attr[i] |
| #define PRIV_DEV_ATTR(i) priv->mlxcpld_hotplug_dev_attr[i] |
| static int mlxcpld_hotplug_attr_init(struct mlxcpld_hotplug_priv_data *priv) |
| { |
| int num_attrs = priv->plat->psu_count + priv->plat->pwr_count + |
| priv->plat->fan_count; |
| int i; |
| |
| priv->group.attrs = devm_kzalloc(&priv->pdev->dev, num_attrs * |
| sizeof(struct attribute *), |
| GFP_KERNEL); |
| if (!priv->group.attrs) |
| return -ENOMEM; |
| |
| for (i = 0; i < num_attrs; i++) { |
| PRIV_ATTR(i) = &PRIV_DEV_ATTR(i).dev_attr.attr; |
| |
| if (i < priv->plat->psu_count) { |
| PRIV_ATTR(i)->name = devm_kasprintf(&priv->pdev->dev, |
| GFP_KERNEL, "psu%u", i + 1); |
| PRIV_DEV_ATTR(i).nr = MLXCPLD_HOTPLUG_ATTR_TYPE_PSU; |
| } else if (i < priv->plat->psu_count + priv->plat->pwr_count) { |
| PRIV_ATTR(i)->name = devm_kasprintf(&priv->pdev->dev, |
| GFP_KERNEL, "pwr%u", i % |
| priv->plat->pwr_count + 1); |
| PRIV_DEV_ATTR(i).nr = MLXCPLD_HOTPLUG_ATTR_TYPE_PWR; |
| } else { |
| PRIV_ATTR(i)->name = devm_kasprintf(&priv->pdev->dev, |
| GFP_KERNEL, "fan%u", i % |
| priv->plat->fan_count + 1); |
| PRIV_DEV_ATTR(i).nr = MLXCPLD_HOTPLUG_ATTR_TYPE_FAN; |
| } |
| |
| if (!PRIV_ATTR(i)->name) { |
| dev_err(&priv->pdev->dev, "Memory allocation failed for sysfs attribute %d.\n", |
| i + 1); |
| return -ENOMEM; |
| } |
| |
| PRIV_DEV_ATTR(i).dev_attr.attr.name = PRIV_ATTR(i)->name; |
| PRIV_DEV_ATTR(i).dev_attr.attr.mode = S_IRUGO; |
| PRIV_DEV_ATTR(i).dev_attr.show = mlxcpld_hotplug_attr_show; |
| PRIV_DEV_ATTR(i).index = i; |
| sysfs_attr_init(&PRIV_DEV_ATTR(i).dev_attr.attr); |
| } |
| |
| priv->group.attrs = priv->mlxcpld_hotplug_attr; |
| priv->groups[0] = &priv->group; |
| priv->groups[1] = NULL; |
| |
| return 0; |
| } |
| |
| static int mlxcpld_hotplug_device_create(struct device *dev, |
| struct mlxcpld_hotplug_device *item) |
| { |
| item->adapter = i2c_get_adapter(item->bus); |
| if (!item->adapter) { |
| dev_err(dev, "Failed to get adapter for bus %d\n", |
| item->bus); |
| return -EFAULT; |
| } |
| |
| item->client = i2c_new_device(item->adapter, &item->brdinfo); |
| if (!item->client) { |
| dev_err(dev, "Failed to create client %s at bus %d at addr 0x%02x\n", |
| item->brdinfo.type, item->bus, item->brdinfo.addr); |
| i2c_put_adapter(item->adapter); |
| item->adapter = NULL; |
| return -EFAULT; |
| } |
| |
| return 0; |
| } |
| |
| static void mlxcpld_hotplug_device_destroy(struct mlxcpld_hotplug_device *item) |
| { |
| if (item->client) { |
| i2c_unregister_device(item->client); |
| item->client = NULL; |
| } |
| |
| if (item->adapter) { |
| i2c_put_adapter(item->adapter); |
| item->adapter = NULL; |
| } |
| } |
| |
| static inline void |
| mlxcpld_hotplug_work_helper(struct device *dev, |
| struct mlxcpld_hotplug_device *item, u8 is_inverse, |
| u16 offset, u8 mask, u8 *cache) |
| { |
| u8 val, asserted; |
| int bit; |
| |
| /* Mask event. */ |
| outb(0, offset + MLXCPLD_HOTPLUG_MASK_OFF); |
| /* Read status. */ |
| val = inb(offset) & mask; |
| asserted = *cache ^ val; |
| *cache = val; |
| |
| /* |
| * Validate if item related to received signal type is valid. |
| * It should never happen, excepted the situation when some |
| * piece of hardware is broken. In such situation just produce |
| * error message and return. Caller must continue to handle the |
| * signals from other devices if any. |
| */ |
| if (unlikely(!item)) { |
| dev_err(dev, "False signal is received: register at offset 0x%02x, mask 0x%02x.\n", |
| offset, mask); |
| return; |
| } |
| |
| for_each_set_bit(bit, (unsigned long *)&asserted, 8) { |
| if (val & BIT(bit)) { |
| if (is_inverse) |
| mlxcpld_hotplug_device_destroy(item + bit); |
| else |
| mlxcpld_hotplug_device_create(dev, item + bit); |
| } else { |
| if (is_inverse) |
| mlxcpld_hotplug_device_create(dev, item + bit); |
| else |
| mlxcpld_hotplug_device_destroy(item + bit); |
| } |
| } |
| |
| /* Acknowledge event. */ |
| outb(0, offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| /* Unmask event. */ |
| outb(mask, offset + MLXCPLD_HOTPLUG_MASK_OFF); |
| } |
| |
| /* |
| * mlxcpld_hotplug_work_handler - performs traversing of CPLD interrupt |
| * registers according to the below hierarchy schema: |
| * |
| * Aggregation registers (status/mask) |
| * PSU registers: *---* |
| * *-----------------* | | |
| * |status/event/mask|----->| * | |
| * *-----------------* | | |
| * Power registers: | | |
| * *-----------------* | | |
| * |status/event/mask|----->| * |---> CPU |
| * *-----------------* | | |
| * FAN registers: |
| * *-----------------* | | |
| * |status/event/mask|----->| * | |
| * *-----------------* | | |
| * *---* |
| * In case some system changed are detected: FAN in/out, PSU in/out, power |
| * cable attached/detached, relevant device is created or destroyed. |
| */ |
| static void mlxcpld_hotplug_work_handler(struct work_struct *work) |
| { |
| struct mlxcpld_hotplug_priv_data *priv = container_of(work, |
| struct mlxcpld_hotplug_priv_data, dwork.work); |
| u8 val, aggr_asserted; |
| unsigned long flags; |
| |
| /* Mask aggregation event. */ |
| outb(0, priv->plat->top_aggr_offset + MLXCPLD_HOTPLUG_AGGR_MASK_OFF); |
| /* Read aggregation status. */ |
| val = inb(priv->plat->top_aggr_offset) & priv->plat->top_aggr_mask; |
| aggr_asserted = priv->aggr_cache ^ val; |
| priv->aggr_cache = val; |
| |
| /* Handle PSU configuration changes. */ |
| if (aggr_asserted & priv->plat->top_aggr_psu_mask) |
| mlxcpld_hotplug_work_helper(&priv->pdev->dev, priv->plat->psu, |
| 1, priv->plat->psu_reg_offset, |
| priv->plat->psu_mask, |
| &priv->psu_cache); |
| |
| /* Handle power cable configuration changes. */ |
| if (aggr_asserted & priv->plat->top_aggr_pwr_mask) |
| mlxcpld_hotplug_work_helper(&priv->pdev->dev, priv->plat->pwr, |
| 0, priv->plat->pwr_reg_offset, |
| priv->plat->pwr_mask, |
| &priv->pwr_cache); |
| |
| /* Handle FAN configuration changes. */ |
| if (aggr_asserted & priv->plat->top_aggr_fan_mask) |
| mlxcpld_hotplug_work_helper(&priv->pdev->dev, priv->plat->fan, |
| 1, priv->plat->fan_reg_offset, |
| priv->plat->fan_mask, |
| &priv->fan_cache); |
| |
| if (aggr_asserted) { |
| spin_lock_irqsave(&priv->lock, flags); |
| |
| /* |
| * It is possible, that some signals have been inserted, while |
| * interrupt has been masked by mlxcpld_hotplug_work_handler. |
| * In this case such signals will be missed. In order to handle |
| * these signals delayed work is canceled and work task |
| * re-scheduled for immediate execution. It allows to handle |
| * missed signals, if any. In other case work handler just |
| * validates that no new signals have been received during |
| * masking. |
| */ |
| cancel_delayed_work(&priv->dwork); |
| schedule_delayed_work(&priv->dwork, 0); |
| |
| spin_unlock_irqrestore(&priv->lock, flags); |
| |
| return; |
| } |
| |
| /* Unmask aggregation event (no need acknowledge). */ |
| outb(priv->plat->top_aggr_mask, priv->plat->top_aggr_offset + |
| MLXCPLD_HOTPLUG_AGGR_MASK_OFF); |
| } |
| |
| static void mlxcpld_hotplug_set_irq(struct mlxcpld_hotplug_priv_data *priv) |
| { |
| /* Clear psu presense event. */ |
| outb(0, priv->plat->psu_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| /* Set psu initial status as mask and unmask psu event. */ |
| priv->psu_cache = priv->plat->psu_mask; |
| outb(priv->plat->psu_mask, priv->plat->psu_reg_offset + |
| MLXCPLD_HOTPLUG_MASK_OFF); |
| |
| /* Clear power cable event. */ |
| outb(0, priv->plat->pwr_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| /* Keep power initial status as zero and unmask power event. */ |
| outb(priv->plat->pwr_mask, priv->plat->pwr_reg_offset + |
| MLXCPLD_HOTPLUG_MASK_OFF); |
| |
| /* Clear fan presense event. */ |
| outb(0, priv->plat->fan_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| /* Set fan initial status as mask and unmask fan event. */ |
| priv->fan_cache = priv->plat->fan_mask; |
| outb(priv->plat->fan_mask, priv->plat->fan_reg_offset + |
| MLXCPLD_HOTPLUG_MASK_OFF); |
| |
| /* Keep aggregation initial status as zero and unmask events. */ |
| outb(priv->plat->top_aggr_mask, priv->plat->top_aggr_offset + |
| MLXCPLD_HOTPLUG_AGGR_MASK_OFF); |
| |
| /* Invoke work handler for initializing hot plug devices setting. */ |
| mlxcpld_hotplug_work_handler(&priv->dwork.work); |
| |
| enable_irq(priv->irq); |
| } |
| |
| static void mlxcpld_hotplug_unset_irq(struct mlxcpld_hotplug_priv_data *priv) |
| { |
| int i; |
| |
| disable_irq(priv->irq); |
| cancel_delayed_work_sync(&priv->dwork); |
| |
| /* Mask aggregation event. */ |
| outb(0, priv->plat->top_aggr_offset + MLXCPLD_HOTPLUG_AGGR_MASK_OFF); |
| |
| /* Mask psu presense event. */ |
| outb(0, priv->plat->psu_reg_offset + MLXCPLD_HOTPLUG_MASK_OFF); |
| /* Clear psu presense event. */ |
| outb(0, priv->plat->psu_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| |
| /* Mask power cable event. */ |
| outb(0, priv->plat->pwr_reg_offset + MLXCPLD_HOTPLUG_MASK_OFF); |
| /* Clear power cable event. */ |
| outb(0, priv->plat->pwr_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| |
| /* Mask fan presense event. */ |
| outb(0, priv->plat->fan_reg_offset + MLXCPLD_HOTPLUG_MASK_OFF); |
| /* Clear fan presense event. */ |
| outb(0, priv->plat->fan_reg_offset + MLXCPLD_HOTPLUG_EVENT_OFF); |
| |
| /* Remove all the attached devices. */ |
| for (i = 0; i < priv->plat->psu_count; i++) |
| mlxcpld_hotplug_device_destroy(priv->plat->psu + i); |
| |
| for (i = 0; i < priv->plat->pwr_count; i++) |
| mlxcpld_hotplug_device_destroy(priv->plat->pwr + i); |
| |
| for (i = 0; i < priv->plat->fan_count; i++) |
| mlxcpld_hotplug_device_destroy(priv->plat->fan + i); |
| } |
| |
| static irqreturn_t mlxcpld_hotplug_irq_handler(int irq, void *dev) |
| { |
| struct mlxcpld_hotplug_priv_data *priv = |
| (struct mlxcpld_hotplug_priv_data *)dev; |
| |
| /* Schedule work task for immediate execution.*/ |
| schedule_delayed_work(&priv->dwork, 0); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int mlxcpld_hotplug_probe(struct platform_device *pdev) |
| { |
| struct mlxcpld_hotplug_platform_data *pdata; |
| struct mlxcpld_hotplug_priv_data *priv; |
| int err; |
| |
| pdata = dev_get_platdata(&pdev->dev); |
| if (!pdata) { |
| dev_err(&pdev->dev, "Failed to get platform data.\n"); |
| return -EINVAL; |
| } |
| |
| priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| priv->pdev = pdev; |
| priv->plat = pdata; |
| |
| priv->irq = platform_get_irq(pdev, 0); |
| if (priv->irq < 0) { |
| dev_err(&pdev->dev, "Failed to get platform irq: %d\n", |
| priv->irq); |
| return priv->irq; |
| } |
| |
| err = devm_request_irq(&pdev->dev, priv->irq, |
| mlxcpld_hotplug_irq_handler, 0, pdev->name, |
| priv); |
| if (err) { |
| dev_err(&pdev->dev, "Failed to request irq: %d\n", err); |
| return err; |
| } |
| disable_irq(priv->irq); |
| |
| INIT_DELAYED_WORK(&priv->dwork, mlxcpld_hotplug_work_handler); |
| spin_lock_init(&priv->lock); |
| |
| err = mlxcpld_hotplug_attr_init(priv); |
| if (err) { |
| dev_err(&pdev->dev, "Failed to allocate attributes: %d\n", err); |
| return err; |
| } |
| |
| priv->hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, |
| "mlxcpld_hotplug", priv, priv->groups); |
| if (IS_ERR(priv->hwmon)) { |
| dev_err(&pdev->dev, "Failed to register hwmon device %ld\n", |
| PTR_ERR(priv->hwmon)); |
| return PTR_ERR(priv->hwmon); |
| } |
| |
| platform_set_drvdata(pdev, priv); |
| |
| /* Perform initial interrupts setup. */ |
| mlxcpld_hotplug_set_irq(priv); |
| |
| return 0; |
| } |
| |
| static int mlxcpld_hotplug_remove(struct platform_device *pdev) |
| { |
| struct mlxcpld_hotplug_priv_data *priv = platform_get_drvdata(pdev); |
| |
| /* Clean interrupts setup. */ |
| mlxcpld_hotplug_unset_irq(priv); |
| |
| return 0; |
| } |
| |
| static struct platform_driver mlxcpld_hotplug_driver = { |
| .driver = { |
| .name = "mlxcpld-hotplug", |
| }, |
| .probe = mlxcpld_hotplug_probe, |
| .remove = mlxcpld_hotplug_remove, |
| }; |
| |
| module_platform_driver(mlxcpld_hotplug_driver); |
| |
| MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); |
| MODULE_DESCRIPTION("Mellanox CPLD hotplug platform driver"); |
| MODULE_LICENSE("Dual BSD/GPL"); |
| MODULE_ALIAS("platform:mlxcpld-hotplug"); |