This adds proper kernel support for the 8 RGB LEDs on the front panel of CZ.NIC's Turris 1.x router. The LEDs are controlled by a CPLD device which manages the whole router. This CPLD controls the LEDs with PWMs and exposes to CPU these functions via memory mapped registers: - enabling/disabling each RGB LED - for every LED (*) separately setting brightness for the R, G, and B color channel (and also reading this values) - setting/getting current global intensity level (there are 8 levels and the level can also be changed by pressing a button on the back side of the router) - setting/getting the intensity of each global intensity level - enabling/disabling HW trigger for each LED - changing WIFI LED to STATUS LED and back (in the sense of HW trigger) (*) The colors of the LAN LEDs, LAN1-LAN5, are controlled together. These LEDs cannot have different colors. For each LED three sysfs entries are created, with names "turris:C:N", where C is from {r, g, b} and N is from {wan, lan, wifi, power}. Although there are 8 LEDs, because of (*) we only work with them as with 4 LEDs - LAN1-LAN5 are visible as one LAN LED. Each LED has a hw_trigger attribute, which can be set according to this table: LED dis HW trigger en HW trigger wan none wan lan none lan wifi none wifi/status power none power The parent device also exposes attributes with names intensity_level_N for N from {0, ..., 7} to control the values of the intensity levels, and an attribute current_intensity_level, to get/set current intensity level. Signed-off-by: Marek Behún <marek.behun@xxxxxx> --- drivers/leds/Kconfig | 12 ++ drivers/leds/Makefile | 1 + drivers/leds/leds-turris.c | 346 +++++++++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 drivers/leds/leds-turris.c diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 2b5ae50f8c9a..8c813c0fdfd2 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -128,6 +128,18 @@ config LEDS_CR0014114 To compile this driver as a module, choose M here: the module will be called leds-cr0014114. +config LEDS_TURRIS + tristate "LED support for CZ.NIC's Turris 1.x" + depends on LEDS_CLASS + depends on OF + help + This option enables support for the RGB LEDs found on the front + side of CZ.NIC's Turris router. There are 8 RGB LEDs on the front + panel, but hardware groups 5 of them (LAN1-LAN5) into one LAN LED, + so the system sees the 8 LEDs only as four. + For each of these 4 RGB LEDs three sysfs entries are created, each + for one color channel. + config LEDS_LM3530 tristate "LCD Backlight driver for LM3530" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 4c1b0054f379..807a091adb05 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -79,6 +79,7 @@ obj-$(CONFIG_LEDS_MT6323) += leds-mt6323.o obj-$(CONFIG_LEDS_LM3692X) += leds-lm3692x.o obj-$(CONFIG_LEDS_SC27XX_BLTC) += leds-sc27xx-bltc.o obj-$(CONFIG_LEDS_LM3601X) += leds-lm3601x.o +obj-$(CONFIG_LEDS_TURRIS) += leds-turris.o # LED SPI Drivers obj-$(CONFIG_LEDS_CR0014114) += leds-cr0014114.o diff --git a/drivers/leds/leds-turris.c b/drivers/leds/leds-turris.c new file mode 100644 index 000000000000..c9c80c2c63e8 --- /dev/null +++ b/drivers/leds/leds-turris.c @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris 1.x LEDs driver + * + * 2019 by Marek Behun <marek.behun@xxxxxx> + */ +#include <linux/ctype.h> +#include <linux/io.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> + +#define CPLD_WIFI_LED_MODE 0x08 +#define CPLD_LED_BRIGHTNESS(idx) (0x13 + (idx)) +#define CPLD_LED_INTENSITY_LEVEL 0x20 +#define CPLD_LED_SW_OVERRIDE 0x22 +#define CPLD_LED_SW_ENABLE 0x23 +#define CPLD_LED_INTENSITY_LEVEL_VALUE(lvl) (0x28 + ((lvl) & 0x7)) + +#define TURRIS_BOARD_LEDS 12 + +static const struct { + u8 mask; + const char *group; + const char *alt_hw_trigger; + const char *name; +} turris_leds_info[TURRIS_BOARD_LEDS] = { + { 0x01, "wan", NULL, "turris:r:wan" }, + { 0x01, "wan", NULL, "turris:g:wan" }, + { 0x01, "wan", NULL, "turris:b:wan" }, + { 0x3e, "lan", NULL, "turris:r:lan" }, + { 0x3e, "lan", NULL, "turris:g:lan" }, + { 0x3e, "lan", NULL, "turris:b:lan" }, + { 0x40, "wifi", "status", "turris:r:wifi" }, + { 0x40, "wifi", "status", "turris:g:wifi" }, + { 0x40, "wifi", "status", "turris:b:wifi" }, + { 0x80, "power", NULL, "turris:r:power" }, + { 0x80, "power", NULL, "turris:g:power" }, + { 0x80, "power", NULL, "turris:b:power" }, +}; + +struct turris_leds { + struct device *dev; + void __iomem *regs; + struct mutex lock; + struct led_classdev leds[TURRIS_BOARD_LEDS]; +}; + +static int turris_led_idx(struct turris_leds *leds, struct led_classdev *led) +{ + int idx = led - &leds->leds[0]; + + if (idx < 0 || idx >= TURRIS_BOARD_LEDS) + return -ENXIO; + + return idx; +} + +static ssize_t hw_trigger_show(struct device *d, struct device_attribute *a, + char *buf) +{ + struct led_classdev *led = dev_get_drvdata(d); + struct turris_leds *leds = dev_get_drvdata(led->dev->parent); + int idx = turris_led_idx(leds, led); + u8 reg, hwtrig; + char *p = buf; + + if (idx < 0) + return idx; + + reg = readb(leds->regs + CPLD_LED_SW_OVERRIDE); + hwtrig = reg & turris_leds_info[idx].mask ? 0 : 1; + + if (hwtrig && turris_leds_info[idx].alt_hw_trigger) { + /* only wifi/status LED supports alternative HW trigger */ + reg = readb(leds->regs + CPLD_WIFI_LED_MODE); + if (reg & 1) + hwtrig = 2; + } + + p += sprintf(p, "%s ", hwtrig == 0 ? "[none]" : "none"); + p += sprintf(p, "%s%s%s ", hwtrig == 1 ? "[" : "", + turris_leds_info[idx].group, hwtrig == 1 ? "]" : ""); + if (turris_leds_info[idx].alt_hw_trigger) + p += sprintf(p, "%s%s%s ", hwtrig == 2 ? "[" : "", + turris_leds_info[idx].alt_hw_trigger, + hwtrig == 2 ? "]" : ""); + p[-1] = '\n'; + + return p - buf; +} + +static ssize_t hw_trigger_store(struct device *d, struct device_attribute *a, + const char *buf, size_t size) +{ + struct led_classdev *led = dev_get_drvdata(d); + struct turris_leds *leds = dev_get_drvdata(led->dev->parent); + int idx = turris_led_idx(leds, led); + char dup[16]; + u8 reg; + + if (idx < 0) + return idx; + + if (size > 15) + return -EINVAL; + + memcpy(dup, buf, size); + + if (size > 0 && dup[size-1] == '\n') + dup[size-1] = '\0'; + + mutex_lock(&leds->lock); + reg = readb(leds->regs + CPLD_LED_SW_OVERRIDE); + + if (!strcmp(dup, "none")) { + reg |= turris_leds_info[idx].mask; + } else if (!strcmp(dup, turris_leds_info[idx].group)) { + reg &= ~turris_leds_info[idx].mask; + + if (turris_leds_info[idx].alt_hw_trigger) + writel(0, leds->regs + CPLD_WIFI_LED_MODE); + } else if (turris_leds_info[idx].alt_hw_trigger && + !strcmp(dup, turris_leds_info[idx].alt_hw_trigger)) { + reg &= ~turris_leds_info[idx].mask; + + writel(1, leds->regs + CPLD_WIFI_LED_MODE); + } else { + size = -EINVAL; + } + + if (size > 0) + writeb(reg, leds->regs + CPLD_LED_SW_OVERRIDE); + mutex_unlock(&leds->lock); + + return size; +} + +static DEVICE_ATTR_RW(hw_trigger); + +static struct attribute *turris_led_attrs[] = { + &dev_attr_hw_trigger.attr, + NULL +}; +ATTRIBUTE_GROUPS(turris_led); + +static enum led_brightness turris_led_brightness_get(struct led_classdev *led) +{ + struct turris_leds *leds = dev_get_drvdata(led->dev->parent); + int idx = turris_led_idx(leds, led); + + if (idx < 0) + return idx; + + return readb(leds->regs + CPLD_LED_BRIGHTNESS(idx)); +} + +static void turris_led_brightness_set(struct led_classdev *led, + enum led_brightness brightness) +{ + struct turris_leds *leds = dev_get_drvdata(led->dev->parent); + int idx = turris_led_idx(leds, led); + + if (idx < 0) + return; + + writeb(brightness, leds->regs + CPLD_LED_BRIGHTNESS(idx)); +} + +static ssize_t intensity_level_show(struct device *d, + struct device_attribute *a, + char *buf) +{ + struct turris_leds *leds = dev_get_drvdata(d); + int idx; + u8 reg; + + if (strlen(a->attr.name) < 17 || !isdigit(a->attr.name[16])) + return -ENXIO; + + idx = a->attr.name[16] - '0'; + if (idx > 7) + return -ENXIO; + + reg = readb(leds->regs + CPLD_LED_INTENSITY_LEVEL_VALUE(idx)); + return sprintf(buf, "%u\n", reg); +} + +static ssize_t intensity_level_store(struct device *d, + struct device_attribute *a, + const char *buf, size_t size) +{ + struct turris_leds *leds = dev_get_drvdata(d); + int idx; + u8 reg; + + if (strlen(a->attr.name) < 17 || !isdigit(a->attr.name[16])) + return -ENXIO; + + idx = a->attr.name[16] - '0'; + if (idx > 7) + return -ENXIO; + + if (kstrtou8(buf, 10, ®) < 0) + return -EINVAL; + + writeb(reg, leds->regs + CPLD_LED_INTENSITY_LEVEL_VALUE(idx)); + return size; +} + +static DEVICE_ATTR(intensity_level_0, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_1, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_2, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_3, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_4, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_5, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_6, 0644, intensity_level_show, + intensity_level_store); +static DEVICE_ATTR(intensity_level_7, 0644, intensity_level_show, + intensity_level_store); + +static ssize_t current_intensity_level_show(struct device *d, + struct device_attribute *a, + char *buf) +{ + struct turris_leds *leds = dev_get_drvdata(d); + u8 reg; + + reg = readb(leds->regs + CPLD_LED_INTENSITY_LEVEL); + return sprintf(buf, "%u\n", reg); +} + +static ssize_t current_intensity_level_store(struct device *d, + struct device_attribute *a, + const char *buf, size_t size) +{ + struct turris_leds *leds = dev_get_drvdata(d); + u8 reg; + + if (kstrtou8(buf, 10, ®) < 0) + return -EINVAL; + + if (reg > 7) + return -EINVAL; + + writeb(reg, leds->regs + CPLD_LED_INTENSITY_LEVEL); + return size; +} + +static DEVICE_ATTR_RW(current_intensity_level); + +static struct attribute *turris_leds_attrs[] = { + &dev_attr_intensity_level_0.attr, + &dev_attr_intensity_level_1.attr, + &dev_attr_intensity_level_2.attr, + &dev_attr_intensity_level_3.attr, + &dev_attr_intensity_level_4.attr, + &dev_attr_intensity_level_5.attr, + &dev_attr_intensity_level_6.attr, + &dev_attr_intensity_level_7.attr, + &dev_attr_current_intensity_level.attr, + NULL +}; +ATTRIBUTE_GROUPS(turris_leds); + +static int turris_leds_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct turris_leds *leds; + struct resource *res; + void __iomem *regs; + int i, ret; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENODEV; + + regs = devm_ioremap_resource(dev, res); + if (IS_ERR(regs)) + return PTR_ERR(regs); + + leds = devm_kzalloc(dev, sizeof(*leds), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + platform_set_drvdata(pdev, leds); + + leds->dev = dev; + leds->regs = regs; + mutex_init(&leds->lock); + + for (i = 0; i < TURRIS_BOARD_LEDS; ++i) { + leds->leds[i].name = turris_leds_info[i].name; + leds->leds[i].brightness_get = turris_led_brightness_get; + leds->leds[i].brightness_set = turris_led_brightness_set; + leds->leds[i].default_trigger = "default-on"; + leds->leds[i].groups = turris_led_groups; + + ret = devm_led_classdev_register(dev, &leds->leds[i]); + if (ret < 0) { + dev_err(dev, "Cannot register LED %s: %i\n", + turris_leds_info[i].name, ret); + return ret; + } + } + + ret = devm_device_add_groups(dev, turris_leds_groups); + if (ret < 0) + dev_warn(dev, "Cannot create device attributes: %i\n", ret); + + /* enable all LEDs */ + writeb(0x00, leds->regs + CPLD_LED_SW_ENABLE); + + dev_info(dev, "Turris LEDs registered\n"); + + return 0; +} + +static const struct of_device_id of_turris_leds_match[] = { + { .compatible = "cznic,turris-leds", }, + {}, +}; + +static struct platform_driver turris_leds_driver = { + .probe = turris_leds_probe, + .driver = { + .name = "leds-turris", + .of_match_table = of_turris_leds_match, + }, +}; + +module_platform_driver(turris_leds_driver); + +MODULE_AUTHOR("Marek Behun <marek.behun@xxxxxx>"); +MODULE_DESCRIPTION("CZ.NIC's Turris 1.x LEDs"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:turris_leds"); -- 2.19.2