[PATCH v2] platform/x86: Add wmi driver for Casper Excalibur laptops. Odd line breaks was because I have used scripts/Lindent without checking, I'm sorry for that. And for my weird rgb led API: This kind of design was also used in drivers/platform/x86/dell/alienware-wmi.c:239, but mine differs as it doesn't create different attributes for different leds. That is because driver doesn't know how many leds there are, to know how many leds there are it should check processor information (whether it's 10th gen or 11th). I don't think include/linux/mod_devicetable.h supports that. If there is a way to differentiate cpus, please let me know. And even if it knew how many leds there are, having different attributes can be cumbersome because there's no way of reading leds. And also user can change led state without notifying os (with some hotkey). But I'm open to further discussion. And thanks for all of your careful reviewing. It helped me to learn more.

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

 



Adding wmi driver for Casper Excalibur Laptops:
This driver implements a ledclass_dev device for keyboard backlight
and hwmon driver to read fan speed and (also write) pwm mode. NEW_LEDS is
selected because this driver introduces new leds, and LEDS_CLASS is selected
because this driver implements a led class device. All of Casper Excalibur
Laptops are supported but fan speeds has a bug for older generations.

Signed-off-by: Mustafa Ekşi <mustafa.eskieksi@xxxxxxxxx>
---
 MAINTAINERS                       |   6 +
 drivers/platform/x86/Kconfig      |  14 ++
 drivers/platform/x86/Makefile     |   1 +
 drivers/platform/x86/casper-wmi.c | 315 ++++++++++++++++++++++++++++++
 4 files changed, 336 insertions(+)
 create mode 100644 drivers/platform/x86/casper-wmi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 9ed4d386853..d0142a75d2c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4723,6 +4723,12 @@ S:	Maintained
 W:	https://wireless.wiki.kernel.org/en/users/Drivers/carl9170
 F:	drivers/net/wireless/ath/carl9170/
 
+CASPER EXCALIBUR WMI DRIVER
+M:	Mustafa Ekşi <mustafa.eskieksi@xxxxxxxxx>
+L:	platform-driver-x86@xxxxxxxxxxxxxxx
+S:	Maintained
+F:	drivers/platform/x86/casper-wmi.c
+
 CAVIUM I2C DRIVER
 M:	Robert Richter <rric@xxxxxxxxxx>
 S:	Odd Fixes
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index bdd302274b9..ebef9c9dfb6 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -1127,6 +1127,20 @@ config SEL3350_PLATFORM
 	  To compile this driver as a module, choose M here: the module
 	  will be called sel3350-platform.
 
+config CASPER_WMI
+	tristate "Casper Excalibur Laptop WMI driver"
+	depends on ACPI_WMI
+	depends on HWMON
+	select NEW_LEDS
+	select LEDS_CLASS
+	help
+	  Say Y here if you want to support WMI-based fan speed reporting,
+	  power management and keyboard backlight support on Casper Excalibur
+	  Laptops.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called casper-wmi.
+
 endif # X86_PLATFORM_DEVICES
 
 config P2SB
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 1de432e8861..4b527dd44ad 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -14,6 +14,7 @@ obj-$(CONFIG_MXM_WMI)			+= mxm-wmi.o
 obj-$(CONFIG_NVIDIA_WMI_EC_BACKLIGHT)	+= nvidia-wmi-ec-backlight.o
 obj-$(CONFIG_XIAOMI_WMI)		+= xiaomi-wmi.o
 obj-$(CONFIG_GIGABYTE_WMI)		+= gigabyte-wmi.o
+obj-$(CONFIG_CASPER_WMI)		+= casper-wmi.o
 
 # Acer
 obj-$(CONFIG_ACERHDF)		+= acerhdf.o
diff --git a/drivers/platform/x86/casper-wmi.c b/drivers/platform/x86/casper-wmi.c
new file mode 100644
index 00000000000..012ebda195d
--- /dev/null
+++ b/drivers/platform/x86/casper-wmi.c
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <linux/bitops.h>
+#include <linux/acpi.h>
+#include <linux/leds.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/wmi.h>
+#include <linux/device.h>
+#include <linux/hwmon.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <acpi/acexcep.h>
+#include <linux/bitfield.h>
+#include <linux/sysfs.h>
+
+#define CASPER_WMI_GUID "644C5791-B7B0-4123-A90B-E93876E0DAAD"
+
+#define CASPER_READ 0xfa00
+#define CASPER_WRITE 0xfb00
+#define CASPER_GET_HARDWAREINFO 0x0200
+#define CASPER_SET_LED 0x0100
+#define CASPER_POWERPLAN 0x0300
+
+#define CASPER_KEYBOARD_LED_1 0x03
+#define CASPER_KEYBOARD_LED_2 0x04
+#define CASPER_KEYBOARD_LED_3 0x05
+#define CASPER_ALL_KEYBOARD_LEDS 0x06
+#define CASPER_CORNER_LEDS 0x07
+
+#define CASPER_LED_ID    0xF00000000
+#define CASPER_LED_MODE  0x0F0000000
+#define CASPER_LED_ALPHA 0x00F000000
+
+struct casper_wmi_args {
+	u16 a0, a1;
+	u32 a2, a3, a4, a5, a6, a7, a8;
+};
+
+static u32 casper_last_color;
+static u8 casper_last_led;
+
+static int casper_set(struct wmi_device *wdev, u16 a1, u8 led_id,
+			      u32 data)
+{
+	struct casper_wmi_args wmi_args = {
+		.a0 = CASPER_WRITE,
+		.a1 = a1,
+		.a2 = led_id,
+		.a3 = data
+	};
+	struct acpi_buffer input = {
+		(acpi_size) sizeof(struct casper_wmi_args),
+		&wmi_args
+	};
+	if (ACPI_FAILURE(wmidev_block_set(wdev, 0, &input)))
+		return -EINVAL;
+	return 0;
+}
+
+static ssize_t led_control_show(struct device *dev, struct device_attribute
+				*attr, char *buf)
+{
+	return sysfs_emit("%u%08x\n", buf, casper_last_led,
+		       casper_last_color);
+}
+
+/*
+ * Format wanted from user is a hexadecimal 36-bit integer: most significant
+ * 4 bits are led_id, next 4 bits are mode and next 4 bits are brightness,
+ * next 24 bits are rgb value. 64 bits
+ * IMARRGGBB
+ */
+static ssize_t led_control_store(struct device *dev, struct device_attribute
+				 *attr, const char *buf, size_t count)
+{
+	if (strlen(buf) != 10)
+		return -EINVAL;
+	u64 user_input;
+	/*
+	 * 16-base selected for ease of writing color codes. I chose 64 bit and
+	 * kstrtou64 because format I use determined fits into 64 bit.
+	 */
+	int ret = kstrtou64(buf, 16, &user_input);
+	if (ret)
+		return ret;
+	/*
+	 * led_id can't exceed 255 but it can vary among newer versions and
+	 * other models.
+	 */
+	u8 led_id = FIELD_GET(CASPER_LED_ID, user_input);
+	ret = casper_set(to_wmi_device(dev->parent), CASPER_SET_LED,
+			led_id, (u32) user_input);
+	if (ret)
+		return ret;
+	if (led_id != CASPER_CORNER_LEDS) {
+		casper_last_color = (u32) user_input;
+		casper_last_led = led_id;
+	}
+	return count;
+}
+
+static DEVICE_ATTR_RW(led_control);
+
+static struct attribute *casper_kbd_led_attrs[] = {
+	&dev_attr_led_control.attr,
+	NULL,
+};
+
+ATTRIBUTE_GROUPS(casper_kbd_led);
+
+static void set_casper_backlight_brightness(struct led_classdev *led_cdev,
+					    enum led_brightness brightness)
+{
+	// Setting any of the keyboard leds' brightness sets brightness of all
+	u32 bright_prep = FIELD_PREP(CASPER_LED_ALPHA, brightness);
+	u32 color_no_alpha = casper_last_color&~CASPER_LED_ALPHA;
+
+	casper_set(to_wmi_device(led_cdev->dev->parent), CASPER_SET_LED,
+		       CASPER_KEYBOARD_LED_1, color_no_alpha | bright_prep
+	);
+}
+
+static enum led_brightness get_casper_backlight_brightness(struct led_classdev
+							   *led_cdev)
+{
+	return FIELD_GET(CASPER_LED_ALPHA, casper_last_color);
+}
+
+static struct led_classdev casper_kbd_led = {
+	.name = "casper::kbd_backlight",
+	.brightness = 0,
+	.brightness_set = set_casper_backlight_brightness,
+	.brightness_get = get_casper_backlight_brightness,
+	.max_brightness = 2,
+	.groups = casper_kbd_led_groups,
+};
+
+static int casper_query(struct wmi_device *wdev, u16 a1,
+				struct casper_wmi_args *out)
+{
+	struct casper_wmi_args wmi_args = {
+		.a0 = CASPER_READ,
+		.a1 = a1
+	};
+	struct acpi_buffer input = {
+		(acpi_size) sizeof(struct casper_wmi_args),
+		&wmi_args
+	};
+
+	acpi_status ret = wmidev_block_set(wdev, 0, &input);
+	if (ACPI_FAILURE(ret))
+		return -EIO;
+
+	union acpi_object *obj = wmidev_block_query(wdev, 0);
+	if (obj->type != ACPI_TYPE_BUFFER) // obj will be int (0x10) on failure
+		return -EINVAL;
+	if (obj->buffer.length != 32)
+		return -EIO;
+
+	memcpy(out, obj->buffer.pointer, sizeof(struct casper_wmi_args));
+	kfree(obj);
+	return ret;
+}
+
+static umode_t casper_wmi_hwmon_is_visible(const void *drvdata,
+					   enum hwmon_sensor_types type,
+					   u32 attr, int channel)
+{
+	switch (type) {
+	case hwmon_fan:
+		return 0444;
+	case hwmon_pwm:
+		return 0644;
+	default:
+		return 0;
+	}
+	return 0;
+}
+
+static int casper_wmi_hwmon_read(struct device *dev,
+				 enum hwmon_sensor_types type, u32 attr,
+				 int channel, long *val)
+{
+	struct casper_wmi_args out = { 0 };
+	struct wmi_device *wdev = to_wmi_device(dev->parent);
+	int ret;
+
+	switch (type) {
+	case hwmon_fan:
+		ret = casper_query(wdev, CASPER_GET_HARDWAREINFO, &out);
+		/*
+		 * a4 and a5 is little endian in older laptops (with 10th gen
+		 * cpus or older) and big endian in newer ones. I don't think
+		 * dmi has something for cpu information. Also, defining a
+		 * dmi_list just for this seems like an overkill. This problem
+		 * can be solved in userspace too.
+		 */
+		if (channel == 0) // CPU fan
+			*val = out.a4;
+		else if (channel == 1) // GPU fan
+			*val = out.a5;
+		return 0;
+	case hwmon_pwm:
+		ret = casper_query(wdev, CASPER_POWERPLAN, &out);
+		if (ret) // power plan count varies generations.
+			return ret;
+		if (channel == 0)
+			*val = out.a2;
+		return 0;
+	default:
+		return -ENODEV;
+	}
+}
+
+static int casper_wmi_hwmon_read_string(struct device *dev,
+					enum hwmon_sensor_types type, u32 attr,
+					int channel, const char **str)
+{
+	switch (type) {
+	case hwmon_fan:
+		switch (channel) {
+		case 0:
+			*str = "cpu_fan_speed";
+			break;
+		case 1:
+			*str = "gpu_fan_speed";
+			break;
+		default:
+			return -ENODEV;
+		}
+		break;
+	default:
+		return -ENODEV;
+	}
+	return 0;
+}
+
+static int casper_wmi_hwmon_write(struct device *dev,
+				  enum hwmon_sensor_types type, u32 attr,
+				  int channel, long val)
+{
+	acpi_status ret;
+
+	switch (type) {
+	case hwmon_pwm:
+		if (val > 5 || val < 0)
+			return -EINVAL;
+		ret = casper_set(to_wmi_device(dev->parent),
+				 CASPER_POWERPLAN, val, 0);
+		if (ret)
+			return ret;
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static const struct hwmon_ops casper_wmi_hwmon_ops = {
+	.is_visible = &casper_wmi_hwmon_is_visible,
+	.read = &casper_wmi_hwmon_read,
+	.read_string = &casper_wmi_hwmon_read_string,
+	.write = &casper_wmi_hwmon_write
+};
+
+static const struct hwmon_channel_info *const casper_wmi_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(fan,
+			   HWMON_F_INPUT | HWMON_F_LABEL,
+			   HWMON_F_INPUT | HWMON_F_LABEL),
+	HWMON_CHANNEL_INFO(pwm, HWMON_PWM_MODE),
+	NULL
+};
+
+static const struct hwmon_chip_info casper_wmi_hwmon_chip_info = {
+	.ops = &casper_wmi_hwmon_ops,
+	.info = casper_wmi_hwmon_info,
+};
+
+static int casper_wmi_probe(struct wmi_device *wdev, const void *context)
+{
+	struct device *hwmon_dev;
+
+	if (ACPI_FAILURE(led_classdev_register(&wdev->dev, &casper_kbd_led)))
+		return -ENODEV;
+	hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev,
+						"casper_wmi", wdev,
+						&casper_wmi_hwmon_chip_info,
+						NULL);
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static void casper_wmi_remove(struct wmi_device *wdev)
+{
+	led_classdev_unregister(&casper_kbd_led);
+}
+
+static const struct wmi_device_id casper_wmi_id_table[] = {
+	{ CASPER_WMI_GUID, NULL },
+	{ }
+};
+
+static struct wmi_driver casper_wmi_driver = {
+	.driver = {
+		   .name = "casper-wmi",
+		    },
+	.id_table = casper_wmi_id_table,
+	.probe = casper_wmi_probe,
+	.remove = &casper_wmi_remove,
+};
+
+module_wmi_driver(casper_wmi_driver);
+MODULE_DEVICE_TABLE(wmi, casper_wmi_id_table);
+
+MODULE_AUTHOR("Mustafa Ekşi <mustafa.eskieksi@xxxxxxxxx>");
+MODULE_DESCRIPTION("Casper Excalibur Laptop WMI driver");
+MODULE_LICENSE("GPL");
-- 
2.43.2





[Index of Archives]     [Linux Kernel Development]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux