Hi, I have been thinking that the solution I went for is not very nice for the LAN LEDs in the sense that user can control them only as a group. Would it be okay if I wrote the driver in such a way that for the non-LAN LEDs, the driver would create 3 led_cdevs R, G and B for each non-LAN RGB LED, and for the LAN LEDs it would create only one led_cdev per LED? The color of the LAN LEDs would then be changeable from a attribute file, and the brightness values would be binary (so either on or off). This way the user would be able to set a sw led-trigger for each LAN LED, and for each color channel of the non-LAN LEDs. Marek On Thu, 21 Mar 2019 19:21:48 +0100 Marek Behún <marek.behun@xxxxxx> wrote: > 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");