[PATCH] leds: add NCT6795D driver

From: Alexandre Courbot
Date: Mon Jul 13 2020 - 09:41:22 EST


Add support for the LED feature of the NCT6795D chip found on some
motherboards, notably MSI ones. The LEDs are typically used using a
RGB connector so this driver creates one LED device for each color
component.

Also add self as maintainer.

Signed-off-by: Alexandre Courbot <gnurou@xxxxxxxxx>
---
MAINTAINERS | 6 +
drivers/leds/Kconfig | 9 +
drivers/leds/Makefile | 1 +
drivers/leds/leds-nct6795d.c | 447 +++++++++++++++++++++++++++++++++++
4 files changed, 463 insertions(+)
create mode 100644 drivers/leds/leds-nct6795d.c

diff --git a/MAINTAINERS b/MAINTAINERS
index b4a43a9e7fbc1..118c347ec2990 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11792,6 +11792,12 @@ S: Maintained
F: Documentation/hwmon/nct6775.rst
F: drivers/hwmon/nct6775.c

+NCT6795D LED CONTROLLER DRIVER
+M: Alexandre Courbot <gnurou@xxxxxxxxx>
+L: linux-leds@xxxxxxxxxxxxxxx
+S: Maintained
+F: drivers/leds/leds-nct6795d.c
+
NETDEVSIM
M: Jakub Kicinski <kuba@xxxxxxxxxx>
S: Maintained
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index ed943140e1fd4..aa41493377947 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -886,6 +886,15 @@ config LEDS_SGM3140
This option enables support for the SGM3140 500mA Buck/Boost Charge
Pump LED Driver.

+config LEDS_NCT6795D
+ tristate "LED support for NCT6795D chipsets"
+ depends on LEDS_CLASS
+ help
+ Enabled support for the leds feature of the NCT6795D chips.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-nct6795d.
+
comment "LED Triggers"
source "drivers/leds/trigger/Kconfig"

diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index d6b8a792c9367..04fcb2bb1e3c6 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -65,6 +65,7 @@ obj-$(CONFIG_LEDS_MIKROTIK_RB532) += leds-rb532.o
obj-$(CONFIG_LEDS_MLXCPLD) += leds-mlxcpld.o
obj-$(CONFIG_LEDS_MLXREG) += leds-mlxreg.o
obj-$(CONFIG_LEDS_MT6323) += leds-mt6323.o
+obj-$(CONFIG_LEDS_NCT6795D) += leds-nct6795d.o
obj-$(CONFIG_LEDS_NET48XX) += leds-net48xx.o
obj-$(CONFIG_LEDS_NETXBIG) += leds-netxbig.o
obj-$(CONFIG_LEDS_NIC78BX) += leds-nic78bx.o
diff --git a/drivers/leds/leds-nct6795d.c b/drivers/leds/leds-nct6795d.c
new file mode 100644
index 0000000000000..7d3cc7a2c8b4b
--- /dev/null
+++ b/drivers/leds/leds-nct6795d.c
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright (c) 2020 Alexandre Courbot <gnurou@xxxxxxxxx>
+/*
+ * NCT6795D/NCT6797D LED driver
+ *
+ * Driver to control the RGB interfaces found on some MSI motherboards.
+ * This is for the most part a port of the MSI-RGB user-space program
+ * by Simonas Kazlauskas (https://github.com/nagisa/msi-rgb.git) to the Linux
+ * kernel LED interface.
+ *
+ * It is more limited than the original program due to limitations in the LED
+ * interface. For now, only static displays of colors are possible.
+ *
+ * Supported motherboards (a per MSI-RGB's README):
+ * B350 MORTAR ARCTIC
+ * B350 PC MATE
+ * B350 TOMAHAWK
+ * B360M GAMING PLUS
+ * B450 GAMING PLUS AC
+ * B450 MORTAR
+ * B450 TOMAHAWK
+ * B450M GAMING PLUS
+ * H270 MORTAR ARCTIC
+ * H270 TOMAHAWK ARCTIC
+ * X470 GAMING PLUS
+ * X470 GAMING PRO
+ * Z270 GAMING M7
+ * Z270 SLI PLUS
+ * Z370 MORTAR
+ * Z370 PC PRO
+ *
+ */
+
+#include <linux/io.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/ioport.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+/* Copied from drivers/hwmon/nct6775.c */
+
+#define SIO_REG_LDSEL 0x07 /* Logical device select */
+#define SIO_REG_DEVID 0x20 /* Device ID (2 bytes) */
+
+static inline void superio_outb(int ioreg, int reg, int val)
+{
+ outb(reg, ioreg);
+ outb(val, ioreg + 1);
+}
+
+static inline int superio_inb(int ioreg, int reg)
+{
+ outb(reg, ioreg);
+ return inb(ioreg + 1);
+}
+
+static inline void superio_select(int ioreg, int ld)
+{
+ outb(SIO_REG_LDSEL, ioreg);
+ outb(ld, ioreg + 1);
+}
+
+static inline int superio_enter(int ioreg)
+{
+ if (!request_muxed_region(ioreg, 2, "NCT6795D LED"))
+ return -EBUSY;
+
+ outb(0x87, ioreg);
+ outb(0x87, ioreg);
+
+ return 0;
+}
+
+static inline void superio_exit(int ioreg)
+{
+ outb(0xaa, ioreg);
+ outb(0x02, ioreg);
+ outb(0x02, ioreg + 1);
+ release_region(ioreg, 2);
+}
+
+/* End copy from drivers/hwmon/nct6775.c */
+
+#define NCT6795D_DEVICE_NAME "nct6795d"
+#define DEFAULT_STEP_DURATION 25
+
+#define NCT6795D_RGB_BANK 0x12
+
+/* Color registers */
+#define NCT6795D_RED_CELL 0xf0
+#define NCT6795D_GREEN_CELL 0xf4
+#define NCT6795D_BLUE_CELL 0xf8
+
+#define NCT6795D_PARAMS_0 0xe4
+/* Enable/disable LED overall */
+#define PARAMS_0_LED_ENABLE(e) ((e) ? 0x0 : 0x1)
+/* Enable/disable smooth pulsing */
+#define PARAMS_0_LED_PULSE_ENABLE(e) ((e) ? 0x08 : 0x0)
+/* Duration between blinks (0 is always on) */
+#define PARAMS_0_BLINK_DURATION(x) ((x) & 0x07)
+
+#define NCT6795D_PARAMS_1 0xfe
+/* Lower part of step duration (9 bits) */
+#define PARAMS_1_STEP_DURATION_LOW(s) ((s) & 0xff)
+
+#define NCT6795D_PARAMS_2 0xff
+/* Enable fade-in effect for specified primitive */
+#define PARAMS_2_FADE_COLOR(r, g, b) (0xe0 ^ ( \
+ ((r) ? 0x80 : 0x0) | \
+ ((g) ? 0x40 : 0x0) | \
+ ((b) ? 0x20 : 0x0)))
+/* Whether the specified colors should be inverted */
+#define PARAMS_2_INVERT_COLOR(r, g, b) ( \
+ ((r) ? 0x10 : 0x0) | \
+ ((g) ? 0x08 : 0x0) | \
+ ((b) ? 0x04 : 0x0))
+// Also disable board leds if the LED_DISABLE bit is set.
+#define PARAMS_2_DISABLE_BOARD_LED 0x02
+// MSB (9th bit) of step duration
+#define PARAMS_2_STEP_DURATION_HIGH(s) (((s) >> 8) & 0x01)
+
+enum { RED = 0, GREEN, BLUE, NUM_COLORS };
+#define ALL_COLORS (BIT(RED) | BIT(GREEN) | BIT(BLUE))
+
+static u8 init_vals[NUM_COLORS];
+module_param_named(r, init_vals[RED], byte, 0);
+MODULE_PARM_DESC(r, "Initial red intensity (default 0)");
+module_param_named(g, init_vals[GREEN], byte, 0);
+MODULE_PARM_DESC(g, "Initial green intensity (default 0)");
+module_param_named(b, init_vals[BLUE], byte, 0);
+MODULE_PARM_DESC(b, "Initial blue intensity (default 0)");
+
+static const char *color_names[NUM_COLORS] = {
+ "red:",
+ "green:",
+ "blue:",
+};
+
+struct nct6795d_led {
+ struct device *dev;
+ u16 base_port;
+ struct led_classdev cdev[NUM_COLORS];
+};
+
+enum nct679x_chip {
+ NCT6795D = 0,
+ NCT6797D,
+};
+
+const char *chip_names[] = {
+ "NCT6795D",
+ "NCT6797D",
+};
+
+static enum nct679x_chip nct6795d_led_detect(u16 base_port)
+{
+ int ret;
+ u16 val;
+
+ ret = superio_enter(base_port);
+ if (ret)
+ return ret;
+
+ val = (superio_inb(base_port, SIO_REG_DEVID) << 8) |
+ superio_inb(base_port, SIO_REG_DEVID + 1);
+
+ switch (val & 0xfff0) {
+ case 0xd350:
+ ret = NCT6795D;
+ break;
+ case 0xd450:
+ ret = NCT6797D;
+ break;
+ default:
+ ret = -ENXIO;
+ break;
+ }
+
+ superio_exit(base_port);
+ return ret;
+}
+
+static int nct6795d_led_setup(const struct nct6795d_led *led)
+{
+ int ret;
+ u16 val;
+
+ ret = superio_enter(led->base_port);
+ if (ret)
+ return ret;
+
+ /* Without this pulsing does not work? */
+ superio_select(led->base_port, 0x09);
+ val = superio_inb(led->base_port, 0x2c);
+ if ((val & 0x10) != 0x10)
+ superio_outb(led->base_port, 0x2c, val | 0x10);
+
+ superio_select(led->base_port, NCT6795D_RGB_BANK);
+
+ /* Check if RGB control enabled */
+ val = superio_inb(led->base_port, 0xe0);
+ if ((val & 0xe0) != 0xe0)
+ superio_outb(led->base_port, 0xe0, val | 0xe0);
+
+ /*
+ * Set some static parameters: led enabled, no pulse, no blink,
+ * default step duration, no fading, no inversion. These fancy features
+ * are not supported by the LED API at the moment.
+ */
+ superio_outb(led->base_port, NCT6795D_PARAMS_0,
+ PARAMS_0_LED_ENABLE(true) |
+ PARAMS_0_LED_PULSE_ENABLE(false) |
+ PARAMS_0_BLINK_DURATION(0));
+
+ superio_outb(led->base_port, NCT6795D_PARAMS_1,
+ PARAMS_1_STEP_DURATION_LOW(DEFAULT_STEP_DURATION));
+
+ superio_outb(led->base_port, NCT6795D_PARAMS_2,
+ PARAMS_2_FADE_COLOR(false, false, false) |
+ PARAMS_2_INVERT_COLOR(false, false, false) |
+ PARAMS_2_DISABLE_BOARD_LED |
+ PARAMS_2_STEP_DURATION_HIGH(DEFAULT_STEP_DURATION));
+
+ superio_exit(led->base_port);
+ return 0;
+}
+
+static void nct6795d_led_commit_color(const struct nct6795d_led *led,
+ size_t color_cell,
+ enum led_brightness brightness)
+{
+ int i;
+ /*
+ * The 8 4-bit nibbles represent brightness intensity for each time
+ * frame. We set them all to the same value to get a constant color.
+ */
+ u8 b = (brightness << 4) | brightness;
+
+ for (i = 0; i < 4; i++)
+ superio_outb(led->base_port, color_cell + i, b);
+}
+
+static int nct6795d_led_commit(const struct nct6795d_led *led, u8 color_mask)
+{
+ const struct led_classdev *cdev = led->cdev;
+ int ret;
+
+ dev_dbg(led->dev, "setting values: R=%d G=%d B=%d\n",
+ cdev[RED].brightness, cdev[GREEN].brightness,
+ cdev[BLUE].brightness);
+
+ ret = superio_enter(led->base_port);
+ if (ret)
+ return ret;
+
+ superio_select(led->base_port, NCT6795D_RGB_BANK);
+
+ if (color_mask & BIT(RED))
+ nct6795d_led_commit_color(led, NCT6795D_RED_CELL,
+ cdev[RED].brightness);
+ if (color_mask & BIT(GREEN))
+ nct6795d_led_commit_color(led, NCT6795D_GREEN_CELL,
+ cdev[GREEN].brightness);
+ if (color_mask & BIT(BLUE))
+ nct6795d_led_commit_color(led, NCT6795D_BLUE_CELL,
+ cdev[BLUE].brightness);
+
+ superio_exit(led->base_port);
+ return 0;
+}
+
+static void nct6795d_led_brightness_set_red(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ const struct nct6795d_led *led =
+ container_of(cdev, struct nct6795d_led, cdev[RED]);
+ nct6795d_led_commit(led, BIT(RED));
+}
+
+static void nct6795d_led_brightness_set_green(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ const struct nct6795d_led *led =
+ container_of(cdev, struct nct6795d_led, cdev[GREEN]);
+ nct6795d_led_commit(led, BIT(GREEN));
+}
+
+static void nct6795d_led_brightness_set_blue(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ const struct nct6795d_led *led =
+ container_of(cdev, struct nct6795d_led, cdev[BLUE]);
+ nct6795d_led_commit(led, BIT(BLUE));
+}
+
+static void (*brightness_set[NUM_COLORS])(struct led_classdev *,
+ enum led_brightness) = {
+ &nct6795d_led_brightness_set_red,
+ &nct6795d_led_brightness_set_green,
+ &nct6795d_led_brightness_set_blue,
+};
+
+static int nct6795d_led_probe(struct platform_device *pdev)
+{
+ struct nct6795d_led *led;
+ const struct resource *res;
+ int ret;
+ int i;
+
+ led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL);
+ if (!led)
+ return -ENOMEM;
+
+ led->dev = &pdev->dev;
+
+ res = platform_get_resource_byname(pdev, IORESOURCE_REG, "io_base");
+ if (IS_ERR(res))
+ return PTR_ERR(res);
+
+ led->base_port = res->start;
+
+ for (i = 0; i < NUM_COLORS; i++) {
+ struct led_classdev *cdev = &led->cdev[i];
+ struct led_init_data init_data = {};
+
+ init_data.devicename = NCT6795D_DEVICE_NAME;
+ init_data.default_label = color_names[i];
+
+ cdev->brightness = init_vals[i];
+ cdev->max_brightness = 0xf;
+ cdev->brightness_set = brightness_set[i];
+ ret = devm_led_classdev_register_ext(&pdev->dev, cdev,
+ &init_data);
+ if (ret)
+ return ret;
+ }
+
+ dev_set_drvdata(&pdev->dev, led);
+
+ ret = nct6795d_led_setup(led);
+ if (ret)
+ return ret;
+
+ nct6795d_led_commit(led, ALL_COLORS);
+
+ return 0;
+}
+
+#ifdef CONFIG_PM_SLEEP
+static int nct6795d_led_suspend(struct device *dev)
+{
+ return 0;
+}
+
+static int nct6795d_led_resume(struct device *dev)
+{
+ struct nct6795d_led *led = dev_get_drvdata(dev);
+ int ret;
+
+ ret = nct6795d_led_setup(led);
+ if (ret)
+ return ret;
+
+ return nct6795d_led_commit(led, ALL_COLORS);
+}
+#endif
+
+static SIMPLE_DEV_PM_OPS(nct_6795d_led_pm_ops, nct6795d_led_suspend,
+ nct6795d_led_resume);
+
+static struct platform_driver nct6795d_led_driver = {
+ .driver = {
+ .name = "nct6795d_led",
+ .pm = &nct_6795d_led_pm_ops,
+ },
+ .probe = nct6795d_led_probe,
+};
+
+static struct platform_device *nct6795d_led_pdev;
+
+static int __init nct6795d_led_init(void)
+{
+ static const u16 io_bases[] = { 0x4e, 0x2e };
+ struct resource io_res = {
+ .name = "io_base",
+ .flags = IORESOURCE_REG,
+ };
+ enum nct679x_chip detected_chip;
+ int ret;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(io_bases); i++) {
+ detected_chip = nct6795d_led_detect(io_bases[i]);
+ if (detected_chip >= 0)
+ break;
+ }
+ if (i == ARRAY_SIZE(io_bases)) {
+ pr_err("failed to detect nct6795d chip\n");
+ return -ENXIO;
+ }
+
+ pr_info("%s: found %s chip at address 0x%x\n", KBUILD_MODNAME,
+ chip_names[detected_chip], io_bases[i]);
+
+ ret = platform_driver_register(&nct6795d_led_driver);
+ if (ret)
+ return ret;
+
+ nct6795d_led_pdev = platform_device_alloc(NCT6795D_DEVICE_NAME "_led", 0);
+ if (!nct6795d_led_pdev) {
+ ret = -ENOMEM;
+ goto error_pdev_alloc;
+ }
+
+ io_res.end = io_res.start = io_bases[i];
+ ret = platform_device_add_resources(nct6795d_led_pdev, &io_res, 1);
+ if (ret)
+ goto error_pdev_resource;
+
+ ret = platform_device_add(nct6795d_led_pdev);
+ if (ret)
+ goto error_pdev_resource;
+
+ return 0;
+
+error_pdev_resource:
+ platform_device_del(nct6795d_led_pdev);
+error_pdev_alloc:
+ platform_driver_unregister(&nct6795d_led_driver);
+ return ret;
+}
+
+static void __exit nct6795d_led_exit(void)
+{
+ platform_device_unregister(nct6795d_led_pdev);
+ platform_driver_unregister(&nct6795d_led_driver);
+}
+
+module_init(nct6795d_led_init);
+module_exit(nct6795d_led_exit);
+
+MODULE_AUTHOR("Alexandre Courbot <gnurou@xxxxxxxxx>");
+MODULE_DESCRIPTION("LED driver for NCT6795D");
+MODULE_LICENSE("GPL");
--
2.27.0