/* * drivers/leds/leds-mlxcpld.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/acpi.h> #include <linux/device.h> #include <linux/dmi.h> #include <linux/hwmon.h> #include <linux/hwmon-sysfs.h> #include <linux/io.h> #include <linux/leds.h> #include <linux/module.h> #include <linux/mod_devicetable.h> #include <linux/platform_device.h> #include <linux/slab.h> #define MLXPLAT_CPLD_LPC_REG_BASE_ADRR 0x2500 /* LPC bus access */ /* Color codes for LEDs */ #define MLXCPLD_LED_OFFSET_HALF 0x01 /* Offset from solid: 3Hz blink */ #define MLXCPLD_LED_OFFSET_FULL 0x02 /* Offset from solid: 6Hz blink */ #define MLXCPLD_LED_IS_OFF 0x00 /* Off */ #define MLXCPLD_LED_RED_STATIC_ON 0x05 /* Solid red */ #define MLXCPLD_LED_RED_BLINK_HALF (MLXCPLD_LED_RED_STATIC_ON + \ MLXCPLD_LED_OFFSET_HALF) #define MLXCPLD_LED_RED_BLINK_FULL (MLXCPLD_LED_RED_STATIC_ON + \ MLXCPLD_LED_OFFSET_FULL) #define MLXCPLD_LED_GREEN_STATIC_ON 0x0D /* Solid green */ #define MLXCPLD_LED_GREEN_BLINK_HALF (MLXCPLD_LED_GREEN_STATIC_ON + \ MLXCPLD_LED_OFFSET_HALF) #define MLXCPLD_LED_GREEN_BLINK_FULL (MLXCPLD_LED_GREEN_STATIC_ON + \ MLXCPLD_LED_OFFSET_FULL) #define MLXCPLD_LED_BLINK_3HZ 167 /* ~167 msec off/on */ #define MLXCPLD_LED_BLINK_6HZ 83 /* ~83 msec off/on */ /** * mlxcpld_param - LED access parameters: * @offset - offset for LED access in CPLD device * @mask - mask for LED access in CPLD device * @base_color - base color code for LED **/ struct mlxcpld_param { u8 offset; u8 mask; u8 base_color; }; /** * mlxcpld_led_priv - LED private data: * @cled - LED class device instance * @param - LED CPLD access parameters **/ struct mlxcpld_led_priv { struct led_classdev cdev; struct mlxcpld_param param; }; #define cdev_to_priv(c) container_of(c, struct mlxcpld_led_priv, cdev) /** * mlxcpld_led_profile - system LED profile (defined per system class): * @offset - offset for LED access in CPLD device * @mask - mask for LED access in CPLD device * @base_color - base color code * @brightness - default brightness setting (on/off) * @name - LED name **/ struct mlxcpld_led_profile { u8 offset; u8 mask; u8 base_color; enum led_brightness brightness; const char *name; }; /** * mlxcpld_led_pdata - system LED private data * @pdev - platform device pointer * @pled - LED class device instance * @profile - system configuration profile * @num_led_instances - number of LED instances * @lock - device access lock **/ struct mlxcpld_led_pdata { struct platform_device *pdev; struct mlxcpld_led_priv *pled; struct mlxcpld_led_profile *profile; int num_led_instances; spinlock_t lock; }; static struct mlxcpld_led_pdata *mlxcpld_led; /* Default profile fit the next Mellanox systems: * "msx6710", "msx6720", "msb7700", "msn2700", "msx1410", * "msn2410", "msb7800", "msn2740" */ static struct mlxcpld_led_profile mlxcpld_led_default_profile[] = { { 0x21, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:fan1:green", }, { 0x21, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:fan1:red", }, { 0x21, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:fan2:green", }, { 0x21, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:fan2:red", }, { 0x22, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:fan3:green", }, { 0x22, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:fan3:red", }, { 0x22, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:fan4:green", }, { 0x22, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:fan4:red", }, { 0x20, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:psu:green", }, { 0x20, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:psu:red", }, { 0x20, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:status:green", }, { 0x20, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:status:red", }, }; /* Profile fit the Mellanox systems based on "msn2100" */ static struct mlxcpld_led_profile mlxcpld_led_msn2100_profile[] = { { 0x21, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:fan:green", }, { 0x21, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:fan:red", }, { 0x23, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:psu1:green", }, { 0x23, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:psu1:red", }, { 0x23, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:psu2:green", }, { 0x23, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:psu2:red", }, { 0x20, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, "mlxcpld:status:green", }, { 0x20, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, "mlxcpld:status:red", }, { 0x24, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, LED_OFF, "mlxcpld:uid:blue", }, }; enum mlxcpld_led_platform_types { MLXCPLD_LED_PLATFORM_DEFAULT, MLXCPLD_LED_PLATFORM_MSN2100, }; static const char *mlx_product_names[] = { "DEFAULT", "MSN2100", }; static enum mlxcpld_led_platform_types mlxcpld_led_platform_check_sys_type(void) { const char *mlx_product_name; int i; mlx_product_name = dmi_get_system_info(DMI_PRODUCT_NAME); if (!mlx_product_name) return MLXCPLD_LED_PLATFORM_DEFAULT; for (i = 1; i < ARRAY_SIZE(mlx_product_names); i++) { if (strstr(mlx_product_name, mlx_product_names[i])) return i; } return MLXCPLD_LED_PLATFORM_DEFAULT; } static void mlxcpld_led_bus_access_func(u16 base, u8 offset, u8 rw_flag, u8 *data) { u32 addr = base + offset; if (rw_flag == 0) outb(*data, addr); else *data = inb(addr); } static void mlxcpld_led_store_hw(u8 mask, u8 off, u8 vset) { u8 nib, val; /* * Each LED is controlled through low or high nibble of the relevant * CPLD register. Register offset is specified by off parameter. * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink * green. * Parameter mask specifies which nibble is used for specific LED: mask * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - * higher nibble (bits from 4 to 7). */ spin_lock(&mlxcpld_led->lock); mlxcpld_led_bus_access_func(MLXPLAT_CPLD_LPC_REG_BASE_ADRR, off, 1, &val); nib = (mask == 0xf0) ? vset : (vset << 4); val = (val & mask) | nib; mlxcpld_led_bus_access_func(MLXPLAT_CPLD_LPC_REG_BASE_ADRR, off, 0, &val); spin_unlock(&mlxcpld_led->lock); } static void mlxcpld_led_brightness_set(struct led_classdev *led, enum led_brightness value) { struct mlxcpld_led_priv *pled = cdev_to_priv(led); if (value) { mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, pled->param.base_color); return; } mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, MLXCPLD_LED_IS_OFF); } static int mlxcpld_led_blink_set(struct led_classdev *led, unsigned long *delay_on, unsigned long *delay_off) { struct mlxcpld_led_priv *pled = cdev_to_priv(led); /* * HW supports two types of blinking: full (6Hz) and half (3Hz). * For delay on/off zero default setting 3Hz is used. */ if (!(*delay_on == 0 && *delay_off == 0) && !(*delay_on == MLXCPLD_LED_BLINK_3HZ && *delay_off == MLXCPLD_LED_BLINK_3HZ) && !(*delay_on == MLXCPLD_LED_BLINK_6HZ && *delay_off == MLXCPLD_LED_BLINK_6HZ)) return -EINVAL; if (*delay_on == MLXCPLD_LED_BLINK_6HZ) mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, pled->param.base_color + MLXCPLD_LED_OFFSET_FULL); else mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, pled->param.base_color + MLXCPLD_LED_OFFSET_HALF); return 0; } static int mlxcpld_led_config(struct device *dev, struct mlxcpld_led_pdata *cpld) { int i; int err; cpld->pled = devm_kzalloc(dev, sizeof(struct mlxcpld_led_priv) * cpld->num_led_instances, GFP_KERNEL); if (!cpld->pled) return -ENOMEM; for (i = 0; i < cpld->num_led_instances; i++) { cpld->pled[i].cdev.name = cpld->profile[i].name; cpld->pled[i].cdev.brightness = cpld->profile[i].brightness; cpld->pled[i].cdev.max_brightness = 1; cpld->pled[i].cdev.brightness_set = mlxcpld_led_brightness_set; cpld->pled[i].cdev.blink_set = mlxcpld_led_blink_set; cpld->pled[i].cdev.flags = LED_CORE_SUSPENDRESUME; err = devm_led_classdev_register(dev, &cpld->pled[i].cdev); if (err) return err; cpld->pled[i].param.offset = mlxcpld_led->profile[i].offset; cpld->pled[i].param.mask = mlxcpld_led->profile[i].mask; cpld->pled[i].param.base_color = mlxcpld_led->profile[i].base_color; if (mlxcpld_led->profile[i].brightness) mlxcpld_led_brightness_set(&cpld->pled[i].cdev, mlxcpld_led->profile[i].brightness); } return 0; } static int __init mlxcpld_led_probe(struct platform_device *pdev) { enum mlxcpld_led_platform_types mlxcpld_led_plat = mlxcpld_led_platform_check_sys_type(); mlxcpld_led = devm_kzalloc(&pdev->dev, sizeof(*mlxcpld_led), GFP_KERNEL); if (!mlxcpld_led) return -ENOMEM; mlxcpld_led->pdev = pdev; switch (mlxcpld_led_plat) { case MLXCPLD_LED_PLATFORM_MSN2100: mlxcpld_led->profile = mlxcpld_led_msn2100_profile; mlxcpld_led->num_led_instances = ARRAY_SIZE(mlxcpld_led_msn2100_profile); break; default: mlxcpld_led->profile = mlxcpld_led_default_profile; mlxcpld_led->num_led_instances = ARRAY_SIZE(mlxcpld_led_default_profile); break; } spin_lock_init(&mlxcpld_led->lock); return mlxcpld_led_config(&pdev->dev, mlxcpld_led); } static struct platform_driver mlxcpld_led_driver = { .driver = { .name = KBUILD_MODNAME, }, }; static int __init mlxcpld_led_init(void) { struct platform_device *pdev; int err; if (!dmi_match(DMI_CHASSIS_VENDOR, "Mellanox Technologies Ltd.")) return -ENODEV; pdev = platform_device_register_simple(KBUILD_MODNAME, -1, NULL, 0); if (IS_ERR(pdev)) { pr_err("Device allocation failed\n"); return PTR_ERR(pdev); } err = platform_driver_probe(&mlxcpld_led_driver, mlxcpld_led_probe); if (err) { pr_err("Probe platform driver failed\n"); platform_device_unregister(pdev); } return err; } static void __exit mlxcpld_led_exit(void) { platform_device_unregister(mlxcpld_led->pdev); platform_driver_unregister(&mlxcpld_led_driver); } module_init(mlxcpld_led_init); module_exit(mlxcpld_led_exit); MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); MODULE_DESCRIPTION("Mellanox board LED driver"); MODULE_LICENSE("Dual BSD/GPL"); MODULE_ALIAS("platform:leds_mlxcpld");