mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Batuhan Güvener" <tuugspeedman@gmail.com>
To: ilpo.jarvinen@linux.intel.com
Cc: hansg@kernel.org, jlee@suse.com, linux-kernel@vger.kernel.org,
	platform-driver-x86@vger.kernel.org, tuugspeedman@gmail.com
Subject: [PATCH v3] platform/x86: acer-wmi: Add 4-zone RGB keyboard support for Nitro AN515-58
Date: Sat, 14 Nov 2026 12:49:34 +0300	[thread overview]
Message-ID: <20261114094934.5792-1-tuugspeedman@gmail.com> (raw)
In-Reply-To: <2e931d55-1705-414b-9477-ce7c677d89c9@linux.intel.com>

Add support for the 4-zone RGB keyboard backlights found on Acer Nitro
AN515-58 laptops.

The hardware operates via a dual-control mechanism: global hardware
brightness is managed by the EC via RAM offset 0x31 with a range of 0 to 4
(where 0x04 is maximum intensity), while per-zone RGB color control is
handled via WMI Method 6.

Exposing max_brightness = 4 to match the EC hardware level causes the
led-class-multicolor subsystem to scale color components down, restricting
the palette to 5x5x5 (125) total color combinations. Keeping max_brightness
at 255 allows fine-grained 8-bit per-channel control from userspace, though
it creates a dual-control scenario where hardware EC brightness and
userspace multicolor brightness operate concurrently (e.g. EC brightness at
4 while userspace intensity is 0).

Additionally, when the system resumes from a shutdown or power cycle
while brightness is set to 0, the hardware initializes with 0
brightness but cannot derive valid multi-intensity values. To ensure
consistent state evaluation, it defaults to max intensity
(255, 255, 255 - white).

This implementation builds upon initial findings and reverse-engineering
efforts by Dirga Yuza and open-source community driver implementations.

Link: https://lore.kernel.org/r/20260503062131.158944-1-dirgayuza123@gmail.com/
Link: https://github.com/0x7375646F/Linuwu-Sense
Link: https://github.com/JafarAkhondali/acer-predator-turbo-and-rgb-keyboard-linux-module

Signed-off-by: Batuhan Güvener <tuugspeedman@gmail.com>

---
Changes since v2:
- Remove redundant comments
- Place new #include headers near alphabetical neighbors
- Replace deprecated wmi_evaluate_method() with WMI_gaming_execute_buf() using __free(kfree)
- Use __packed structs for WMI command payloads
- Switch to guard(mutex), devm_mutex_init(), and FIELD_MAX()
- Use dev_warn() instead of pr_warn()
- Fix indentation and reverse xmas-tree variable ordering

Changes since v1:
- Clear non-English comments
- Rebase onto linux-next
---
 drivers/platform/x86/Kconfig    |   1 +
 drivers/platform/x86/acer-wmi.c | 305 ++++++++++++++++++++++++++++++++
 2 files changed, 306 insertions(+)

diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 741653432f57..ccb1651992b5 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -169,6 +169,7 @@ config ACER_WMI
 	select LEDS_CLASS
 	select NEW_LEDS
 	select PLATFORM_PROFILE
+	select LEDS_CLASS_MULTICOLOR
 	help
 	  This is a driver for newer Acer (and Wistron) laptops. It adds
 	  wireless radio and bluetooth control, and on some laptops,
diff --git a/drivers/platform/x86/acer-wmi.c b/drivers/platform/x86/acer-wmi.c
index 3a018fdf5138..4680b99001fa 100644
--- a/drivers/platform/x86/acer-wmi.c
+++ b/drivers/platform/x86/acer-wmi.c
@@ -14,11 +14,13 @@
 #include <linux/kernel.h>
 #include <linux/minmax.h>
 #include <linux/module.h>
+#include <linux/mutex.h>
 #include <linux/init.h>
 #include <linux/types.h>
 #include <linux/dmi.h>
 #include <linux/fixp-arith.h>
 #include <linux/backlight.h>
+#include <linux/led-class-multicolor.h>
 #include <linux/leds.h>
 #include <linux/platform_device.h>
 #include <linux/platform_profile.h>
@@ -36,6 +38,10 @@
 #include <linux/unaligned.h>
 #include <linux/bitfield.h>
 #include <linux/bitmap.h>
+#include <linux/bits.h>
+#include <linux/container_of.h>
+#include <linux/compiler_attributes.h>
+#include <linux/cleanup.h>
 
 MODULE_AUTHOR("Carlos Corbacho");
 MODULE_DESCRIPTION("Acer Laptop WMI Extras Driver");
@@ -77,6 +83,21 @@ MODULE_LICENSE("GPL");
 #define ACER_WMID_SET_GAMING_MISC_SETTING_METHODID 22
 #define ACER_WMID_GET_GAMING_MISC_SETTING_METHODID 23
 
+#define ACER_WMID_SET_GAMING_STATIC_LED_METHODID 6
+#define ACER_WMID_SET_GAMING_KBD_BL_METHODID 20
+
+#define ACER_GAMING_LED_KBD_BL_ENABLE	BIT_ULL(3)
+#define ACER_GAMING_LED_KBD_BL_INTERFACE_MASK	GENMASK_ULL(43, 40)
+
+#define ACER_EC_KBD_BL_ZONE_COLOR_BASE_ADDR       0x3C
+
+#define ACER_KBD_STATIC_PREAMBLE_BRIGHTNESS 100
+#define ACER_KBD_RGB_MAX_INTENSITY         255
+#define ACER_KBD_NUM_ZONES                 4
+#define ACER_KBD_NUM_COLOR_CHANNELS        3
+
+#define ACER_KBD_MODE_STATIC               0
+
 #define ACER_GAMING_FAN_BEHAVIOR_CPU BIT(0)
 #define ACER_GAMING_FAN_BEHAVIOR_GPU BIT(3)
 
@@ -389,6 +410,136 @@ struct wmi_interface {
 /* The static interface pointer, points to the currently detected interface */
 static struct wmi_interface *interface;
 
+static acpi_status WMI_gaming_execute_u64(u32 method_id, u64 in, u64 *out);
+static int WMI_gaming_execute_buf(u32 method_id, const void *in, size_t in_size, u64 *out);
+
+struct led_information {
+	struct led_classdev_mc led_general;
+	struct mc_subled led_colors[3];
+	struct rgb_wmi *parent;
+	u8 zone_id;
+	u8 current_brightness;
+};
+
+struct rgb_wmi {
+	struct mutex led_lock; /* making sure EC receives single command at a time */
+	struct led_information zones[4];
+};
+
+struct acer_gaming_kbd_bl_input {
+	u8 mode;
+	u8 speed;
+	u8 brightness;
+	u8 reserved;
+	u8 direction;
+	u8 r;
+	u8 g;
+	u8 b;
+	u8 cmd_flags[2];
+	u8 padding[6];
+} __packed;
+
+static int acer_kbd_set_static_preamble(u8 ec_brightness)
+{
+	struct acer_gaming_kbd_bl_input gm_input = {
+		.mode = ACER_KBD_MODE_STATIC,
+		.brightness = ec_brightness,
+		.r = ACER_KBD_RGB_MAX_INTENSITY,
+		.g = ACER_KBD_RGB_MAX_INTENSITY,
+		.b = ACER_KBD_RGB_MAX_INTENSITY,
+		.cmd_flags = { 3, 1 },
+	};
+	u64 resp = 0;
+	int ret;
+
+	if (ec_read(ACER_EC_KBD_BL_ZONE_COLOR_BASE_ADDR, &gm_input.r) ||
+	    ec_read(ACER_EC_KBD_BL_ZONE_COLOR_BASE_ADDR + 1, &gm_input.g) ||
+	    ec_read(ACER_EC_KBD_BL_ZONE_COLOR_BASE_ADDR + 2, &gm_input.b)) {
+		gm_input.r = ACER_KBD_RGB_MAX_INTENSITY;
+		gm_input.g = ACER_KBD_RGB_MAX_INTENSITY;
+		gm_input.b = ACER_KBD_RGB_MAX_INTENSITY;
+	}
+
+	ret = WMI_gaming_execute_buf(ACER_WMID_SET_GAMING_KBD_BL_METHODID,
+				     &gm_input, sizeof(gm_input), &resp);
+	if (ret < 0 || resp)
+		return -EIO;
+
+	return 0;
+}
+
+struct acer_gaming_static_led_input {
+	u8 zone_bit;
+	u8 r;
+	u8 g;
+	u8 b;
+	u32 padding;
+} __packed;
+
+static int send_zone_color(u8 zone_bit, u8 r, u8 g, u8 b)
+{
+	struct acer_gaming_static_led_input input = {
+		.zone_bit = zone_bit,
+		.r = r,
+		.g = g,
+		.b = b,
+	};
+	acpi_status status;
+	u64 resp = 0;
+	u64 payload;
+
+	memcpy(&payload, &input, sizeof(payload));
+
+	status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_STATIC_LED_METHODID,
+					payload, &resp);
+	if (ACPI_FAILURE(status) || resp)
+		return -EIO;
+
+	return 0;
+}
+
+static enum led_brightness rgb_get(struct led_classdev *led_cdev)
+{
+	struct led_classdev_mc *general_mcolor = lcdev_to_mccdev(led_cdev);
+	struct led_information *led_info = container_of(general_mcolor,
+							struct led_information,
+							led_general);
+	return led_info->current_brightness;
+}
+
+static int rgb_set(struct led_classdev *led_cdev, enum led_brightness brightness)
+{
+	struct led_classdev_mc *general_mcolor = lcdev_to_mccdev(led_cdev);
+	struct led_information *led_info = container_of(general_mcolor,
+							struct led_information,
+							led_general);
+	struct rgb_wmi *data = led_info->parent;
+	u8 r, g, b;
+	int ret;
+
+	guard(mutex)(&data->led_lock);
+
+	if (brightness == 0) {
+		r = 0;
+		g = 0;
+		b = 0;
+	} else {
+		led_mc_calc_color_components(general_mcolor, brightness);
+		r = led_info->led_colors[0].brightness;
+		g = led_info->led_colors[1].brightness;
+		b = led_info->led_colors[2].brightness;
+	}
+
+	ret = send_zone_color(led_info->zone_id, r, g, b);
+	if (ret)
+		return ret;
+
+	led_info->current_brightness = brightness;
+	led_cdev->brightness = brightness;
+
+	return 0;
+}
+
 /*
  * Embedded Controller quirks
  * Some laptops require us to directly access the EC to either enable or query
@@ -406,10 +557,107 @@ struct quirk_entry {
 	bool hwmon;
 	bool platform_profile;
 	bool pwm;
+	u8 kbl_four_zone;
 };
 
 static struct quirk_entry *quirks;
 
+static int acer_kbd_rgb_init(struct platform_device *device, struct rgb_wmi *rgb_data)
+{
+	static const char * const zone_names[] = {
+		"acer-wmi:rgb:kbd_backlight-left",
+		"acer-wmi:rgb:kbd_backlight-center-left",
+		"acer-wmi:rgb:kbd_backlight-center-right",
+		"acer-wmi:rgb:kbd_backlight-right",
+	};
+	acpi_status status;
+	u64 gaming_sysinfo;
+	u64 led_mode;
+	int ret, i;
+
+	if (!quirks || !quirks->kbl_four_zone)
+		return -ENODEV;
+
+	status = WMI_gaming_execute_u64(ACER_WMID_GET_GAMING_SYS_INFO_METHODID,
+					0, &gaming_sysinfo);
+	if (ACPI_FAILURE(status))
+		return -EIO;
+
+	led_mode = ACER_GAMING_LED_KBD_BL_ENABLE |
+		   FIELD_PREP(ACER_GAMING_LED_KBD_BL_INTERFACE_MASK,
+			      FIELD_MAX(ACER_GAMING_LED_KBD_BL_INTERFACE_MASK));
+
+	status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_LED_METHODID,
+					led_mode, NULL);
+
+	if (ACPI_FAILURE(status))
+		return -EIO;
+
+	ret = acer_kbd_set_static_preamble(ACER_KBD_STATIC_PREAMBLE_BRIGHTNESS);
+	if (ret)
+		dev_warn(&device->dev, "Failed to set static preamble on init: %d\n", ret);
+
+	ret = devm_mutex_init(&device->dev, &rgb_data->led_lock);
+	if (ret)
+		return ret;
+
+	for (i = 0; i < ACER_KBD_NUM_ZONES; i++) {
+		u8 start_offset = ACER_EC_KBD_BL_ZONE_COLOR_BASE_ADDR +
+				  (i * ACER_KBD_NUM_COLOR_CHANNELS);
+		struct led_information *zone = &rgb_data->zones[i];
+		u8 r = 0, g = 0, b = 0;
+		u8 max_val;
+
+		zone->zone_id = BIT(i);
+		zone->parent = rgb_data;
+
+		if (ec_read(start_offset, &r) != 0 ||
+		    ec_read(start_offset + 1, &g) != 0 ||
+		    ec_read(start_offset + 2, &b) != 0) {
+			dev_warn(&device->dev,
+				 "Failed to read initial color for zone %d, defaulting to white\n",
+				 i);
+			r = ACER_KBD_RGB_MAX_INTENSITY;
+			g = ACER_KBD_RGB_MAX_INTENSITY;
+			b = ACER_KBD_RGB_MAX_INTENSITY;
+		}
+
+		max_val = max3(r, g, b);
+
+		if (max_val == 0) {
+			zone->led_colors[0].intensity = ACER_KBD_RGB_MAX_INTENSITY;
+			zone->led_colors[1].intensity = ACER_KBD_RGB_MAX_INTENSITY;
+			zone->led_colors[2].intensity = ACER_KBD_RGB_MAX_INTENSITY;
+			zone->current_brightness = 0;
+		} else {
+			zone->led_colors[0].intensity = (r * ACER_KBD_RGB_MAX_INTENSITY) / max_val;
+			zone->led_colors[1].intensity = (g * ACER_KBD_RGB_MAX_INTENSITY) / max_val;
+			zone->led_colors[2].intensity = (b * ACER_KBD_RGB_MAX_INTENSITY) / max_val;
+			zone->current_brightness = max_val;
+		}
+
+		zone->led_colors[0].color_index = LED_COLOR_ID_RED;
+		zone->led_colors[1].color_index = LED_COLOR_ID_GREEN;
+		zone->led_colors[2].color_index = LED_COLOR_ID_BLUE;
+
+		zone->led_general.subled_info = zone->led_colors;
+		zone->led_general.num_colors = ACER_KBD_NUM_COLOR_CHANNELS;
+
+		zone->led_general.led_cdev.name = zone_names[i];
+		zone->led_general.led_cdev.max_brightness = ACER_KBD_RGB_MAX_INTENSITY;
+		zone->led_general.led_cdev.brightness = zone->current_brightness;
+		zone->led_general.led_cdev.brightness_set_blocking = rgb_set;
+		zone->led_general.led_cdev.brightness_get = rgb_get;
+		zone->led_general.led_cdev.flags = LED_RETAIN_AT_SHUTDOWN;
+
+		ret = devm_led_classdev_multicolor_register(&device->dev,
+							    &zone->led_general);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
 static void __init set_quirks(void)
 {
 	if (quirks->mailled)
@@ -466,6 +714,7 @@ static struct quirk_entry quirk_acer_nitro_an515_58 = {
 	.hwmon = true,
 	.platform_profile = true,
 	.pwm = true,
+	.kbl_four_zone = 1,
 };
 
 static struct quirk_entry quirk_acer_predator_ph315_53 = {
@@ -1610,6 +1859,46 @@ WMI_gaming_execute_u64(u32 method_id, u64 in, u64 *out)
 	return status;
 }
 
+static int WMI_gaming_execute_buf(u32 method_id, const void *in, size_t in_size, u64 *out)
+{
+	struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL };
+	struct acpi_buffer input = {
+		.length = in_size,
+		.pointer = (void *)in,
+	};
+	acpi_status status;
+
+	status = wmi_evaluate_method(WMID_GUID4, 0, method_id, &input, &result);
+	if (ACPI_FAILURE(status))
+		return -EIO;
+
+	union acpi_object *obj __free(kfree) = result.pointer;
+
+	if (!out)
+		return 0;
+
+	if (!obj)
+		return -ENOMSG;
+
+	switch (obj->type) {
+	case ACPI_TYPE_INTEGER:
+		*out = obj->integer.value;
+		return 0;
+	case ACPI_TYPE_BUFFER:
+		if (obj->buffer.length >= 8)
+			*out = get_unaligned_le64(obj->buffer.pointer);
+		else if (obj->buffer.length >= 4)
+			*out = get_unaligned_le32(obj->buffer.pointer);
+		else if (obj->buffer.length >= 1)
+			*out = ((u8 *)obj->buffer.pointer)[0];
+		else
+			return -ENOMSG;
+		return 0;
+	default:
+		return -ENOMSG;
+	}
+}
+
 static int WMI_gaming_execute_u32_u64(u32 method_id, u32 in, u64 *out)
 {
 	struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL };
@@ -2815,6 +3104,8 @@ static int acer_wmi_hwmon_init(void);
 /*
  * Platform device
  */
+static struct rgb_wmi acer_rgb_data;
+
 static int acer_platform_probe(struct platform_device *device)
 {
 	int err;
@@ -2846,6 +3137,12 @@ static int acer_platform_probe(struct platform_device *device)
 		if (err)
 			goto error_hwmon;
 	}
+	if (quirks->kbl_four_zone) {
+		err = acer_kbd_rgb_init(device, &acer_rgb_data);
+		if (err)
+			dev_warn(&device->dev, "Failed to initialize 4-zone RGB keyboard: %d\n",
+				 err);
+	}
 
 	return 0;
 
@@ -2898,6 +3195,7 @@ static int acer_suspend(struct device *dev)
 static int acer_resume(struct device *dev)
 {
 	struct acer_data *data = &interface->data;
+	int ret;
 
 	if (!data)
 		return -ENOMEM;
@@ -2911,6 +3209,13 @@ static int acer_resume(struct device *dev)
 	if (acer_wmi_accel_dev)
 		acer_gsensor_init();
 
+	if (quirks && quirks->kbl_four_zone) {
+		ret = acer_kbd_set_static_preamble(ACER_KBD_STATIC_PREAMBLE_BRIGHTNESS);
+		if (ret)
+			dev_warn(dev, "Failed to restore keyboard static mode on resume: %d\n",
+				 ret);
+	}
+
 	return 0;
 }
 #else
-- 
2.56.1


      reply	other threads:[~2026-11-14  9:49 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-11 19:09 [RFC PATCH v2] " Batuhan
2026-11-13 11:41 ` Ilpo Järvinen
2026-11-14  9:49   ` Batuhan Güvener [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261114094934.5792-1-tuugspeedman@gmail.com \
    --to=tuugspeedman@gmail.com \
    --cc=hansg@kernel.org \
    --cc=ilpo.jarvinen@linux.intel.com \
    --cc=jlee@suse.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=platform-driver-x86@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®