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