[PATCH v2] leds: add NCT6795D driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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 takes advantage of the multicolor
framework.

Signed-off-by: Alexandre Courbot <gnurou@xxxxxxxxx>
---
Changes since v1 [1]:
- Use the multicolor framework

[1] https://lkml.org/lkml/2020/7/13/674 (sorry, took me some time to
    come back to this patch)

 drivers/leds/Kconfig         |  10 +
 drivers/leds/Makefile        |   1 +
 drivers/leds/leds-nct6795d.c | 442 +++++++++++++++++++++++++++++++++++
 3 files changed, 453 insertions(+)
 create mode 100644 drivers/leds/leds-nct6795d.c

diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index ed800f5da7d8..0db5986ca967 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -871,6 +871,16 @@ config LEDS_ACER_A500
 	  This option enables support for the Power Button LED of
 	  Acer Iconia Tab A500.
 
+config LEDS_NCT6795D
+	tristate "LED support for NCT6795D chipsets"
+	depends on LEDS_CLASS_MULTICOLOR
+	help
+	  Enables support for the RGB LED feature of the NCT6795D chips found
+	  on some MSI motherboards.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called leds-nct6795d.
+
 source "drivers/leds/blink/Kconfig"
 
 comment "Flash and Torch LED drivers"
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index c636ec069612..c04fdbc2a727 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -63,6 +63,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 000000000000..90d5d2a67cfa
--- /dev/null
+++ b/drivers/leds/leds-nct6795d.c
@@ -0,0 +1,442 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * NCT6795D/NCT6797D LED driver
+ *
+ * Copyright (c) 2021 Alexandre Courbot <gnurou@xxxxxxxxx>
+ *
+ * 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 colors are supported.
+ *
+ * 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/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+/* Adapted 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 adapted 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 means always on) */
+#define PARAMS_0_BLINK_DURATION(x) ((x) & 0x07)
+
+#define NCT6795D_PARAMS_1 0xfe
+/* Lower part of step duration (8 out of 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))
+/* 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 };
+
+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)");
+
+struct nct6795d_led {
+	struct device *dev;
+	u16 base_port;
+	struct led_classdev_mc mc_cdev;
+	struct mc_subled subled[NUM_COLORS];
+};
+
+enum nct679x_chip {
+	NCT6795D = 0,
+	NCT6797D,
+};
+
+static const char * const chip_names[] = {
+	"NCT6795D",
+	"NCT6797D",
+};
+
+/*
+ * Return the detected chip or an error code. If no chip was detected, -ENXIO
+ * is returned.
+ */
+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;
+}
+
+/*
+ * Setup the LEDs for use with the LED interface. I.e, no pulsing or other fancy
+ * features, only static colors.
+ */
+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;
+}
+
+/*
+ * Commit one color to the hardware.
+ */
+static void nct6795d_led_commit_color(const struct nct6795d_led *led,
+				      size_t color_cell,
+				      enum led_brightness brightness)
+{
+	int i;
+	/*
+	 * These 8 4-bit nibbles represent brightness intensity for each time
+	 * frame. We set them all to the same value to get a constant color.
+	 */
+	const u8 b = (brightness << 4) | brightness;
+
+	for (i = 0; i < 4; i++)
+		superio_outb(led->base_port, color_cell + i, b);
+}
+
+/*
+ * Commit all colors to the hardware.
+ */
+static int nct6795d_led_commit(const struct nct6795d_led *led)
+{
+	const struct mc_subled *subled = led->subled;
+	int ret;
+
+	dev_dbg(led->dev, "setting values: R=%d G=%d B=%d\n",
+		subled[RED].brightness, subled[GREEN].brightness,
+		subled[BLUE].brightness);
+
+	ret = superio_enter(led->base_port);
+	if (ret)
+		return ret;
+
+	superio_select(led->base_port, NCT6795D_RGB_BANK);
+
+	nct6795d_led_commit_color(led, NCT6795D_RED_CELL,
+				  subled[RED].brightness);
+	nct6795d_led_commit_color(led, NCT6795D_GREEN_CELL,
+				  subled[GREEN].brightness);
+	nct6795d_led_commit_color(led, NCT6795D_BLUE_CELL,
+				  subled[BLUE].brightness);
+
+	superio_exit(led->base_port);
+	return 0;
+}
+
+/*
+ * led_classdev's brightness_set hook.
+ */
+static void nct6795d_led_brightness_set(struct led_classdev *cdev,
+					enum led_brightness brightness)
+{
+	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+	struct nct6795d_led *led =
+		container_of(mc_cdev, struct nct6795d_led, mc_cdev);
+
+	led_mc_calc_color_components(mc_cdev, brightness);
+
+	nct6795d_led_commit(led);
+}
+
+static int nct6795d_led_probe(struct platform_device *pdev)
+{
+	struct nct6795d_led *led;
+	const struct resource *res;
+	int ret;
+
+	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;
+
+	led->subled[RED].color_index = LED_COLOR_ID_RED;
+	led->subled[RED].channel = 0;
+	led->subled[RED].intensity = init_vals[RED];
+	led->subled[GREEN].color_index = LED_COLOR_ID_GREEN;
+	led->subled[GREEN].channel = 1;
+	led->subled[GREEN].intensity = init_vals[GREEN];
+	led->subled[BLUE].color_index = LED_COLOR_ID_BLUE;
+	led->subled[BLUE].channel = 2;
+	led->subled[BLUE].intensity = init_vals[BLUE];
+
+	led->mc_cdev.subled_info = led->subled;
+	led->mc_cdev.num_colors = NUM_COLORS;
+	led->mc_cdev.led_cdev.name = NCT6795D_DEVICE_NAME;
+	led->mc_cdev.led_cdev.max_brightness = 0xf;
+	led->mc_cdev.led_cdev.brightness = led->mc_cdev.led_cdev.max_brightness;
+	led->mc_cdev.led_cdev.brightness_set = nct6795d_led_brightness_set;
+
+	ret = devm_led_classdev_multicolor_register_ext(&pdev->dev,
+							&led->mc_cdev, NULL);
+	if (ret)
+		return ret;
+
+	dev_set_drvdata(&pdev->dev, led);
+
+	ret = nct6795d_led_setup(led);
+	if (ret)
+		return ret;
+
+	nct6795d_led_brightness_set(&led->mc_cdev.led_cdev,
+				    led->mc_cdev.led_cdev.brightness);
+
+	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);
+}
+#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(KBUILD_MODNAME ": no supported chip detected\n");
+		return -ENXIO;
+	}
+
+	pr_info(KBUILD_MODNAME ": found %s chip at address 0x%x\n",
+		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.start = io_bases[i];
+	io_res.end = io_res.start;
+	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.34.1




[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]

  Powered by Linux