/* * drivers/platform/x86/mlxcpld-hotplug.c * Copyright (c) 2016 Mellanox Technologies. All rights reserved. * Copyright (c) 2016 Vadim Pasternak * * 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 #include #include #include #include #include #include #include #include #include #include #include #include /* 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 "); MODULE_DESCRIPTION("Mellanox CPLD hotplug platform driver"); MODULE_LICENSE("Dual BSD/GPL"); MODULE_ALIAS("platform:mlxcpld-hotplug");