[PATCH v5 6/6] platform/x86: bitland-mifs-wmi: Add Redmi laptop support
Mingyou Chen <[email protected]>
| Newsgroups | org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
Redmi laptops (Redmi Book and Redmi G series) use the same MIFS WMI interface but encode the performance mode values of WMI_FN_SYSTEM_PER_MODE differently, as reverse engineered from the Windows GCommandCenter application: 0 = performance, 1 = balanced, 2 = quiet, 4 = full speed Add "Redmi Book" and "Redmi G" ops entries selected via DMI: - Redmi G (board TM2135/TM2137): the full-speed mode is available - Redmi Book/Xiaomi Book: no full-speed mode - The performance modes only require AC power (any source); the barrel-jack requirement is Bitland-specific The GCommandCenter application further showed that: - the CPU power sensor (WMI_FN_CPU_POWER) is only available on Redmi machines, gate the hwmon power channel on the ops table - the refresh-rate-toggle quirk in the CPU fan speed event is Redmi-specific, gate it on the ops table instead of applying it unconditionally Also add Redmi support for Fn lock and touchpad lock control (WMI_FN_FN_LOCK / WMI_FN_TP_LOCK sysfs attributes), keyboard type detection (hide the RGB kb_mode attribute on white-only backlit keyboards) and hotkeys (airplane mode -> KEY_RFKILL, ignore the lock-state and keyboard mode indicator events). The sysfs attribute group is now registered in probe (instead of via dev_groups) so that the is_visible callback can access the probe populated driver data. Signed-off-by: Mingyou Chen <[email protected]> --- drivers/platform/x86/bitland-mifs-wmi.c | 336 ++++++++++++++++++++++-- 1 file changed, 318 insertions(+), 18 deletions(-) diff --git a/drivers/platform/x86/bitland-mifs-wmi.c b/drivers/platform/x86/bitland-mifs-wmi.c index 17792d8b7c29..1eb9f92740e0 100644 --- a/drivers/platform/x86/bitland-mifs-wmi.c +++ b/drivers/platform/x86/bitland-mifs-wmi.c @@ -14,6 +14,7 @@ #include <linux/dev_printk.h> #include <linux/device.h> #include <linux/device/devres.h> +#include <linux/dmi.h> #include <linux/err.h> #include <linux/hwmon.h> #include <linux/init.h> @@ -71,6 +72,11 @@ enum bitland_system_ac_mode { WMI_SYSTEM_AC_CIRCULARHOLE = 2, }; +enum bitland_kbd_type { + WMI_KBD_TYPE_WHITE = 0, + WMI_KBD_TYPE_RGB = 1, +}; + enum bitland_mifs_power_profile { WMI_PP_BALANCED = 0, WMI_PP_PERFORMANCE = 1, @@ -97,7 +103,15 @@ static const struct bitland_profile_mode_map bitland_mode_map = { .full_speed = WMI_PP_FULL_SPEED, }; +static const struct bitland_profile_mode_map redmi_mode_map = { + .balanced = 1, + .performance = 0, + .quiet = 2, + .full_speed = 4, +}; + static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data); +static int redmi_pp_check_capability(struct bitland_mifs_wmi_data *data); /* * Machine-specific operations. The MIFS WMI interface is shared by @@ -109,6 +123,8 @@ struct bitland_mifs_ops { const struct bitland_profile_mode_map *mode_map; int (*check_performance_capability)(struct bitland_mifs_wmi_data *data); bool has_full_speed; + bool has_cpu_power; + bool quirk_refresh_rate_toggle; }; static const struct bitland_mifs_ops bitland_ops = { @@ -118,6 +134,54 @@ static const struct bitland_mifs_ops bitland_ops = { .has_full_speed = true, }; +static const struct bitland_mifs_ops redmi_book_ops = { + .name = "Redmi Book", + .mode_map = &redmi_mode_map, + .check_performance_capability = redmi_pp_check_capability, + .has_cpu_power = true, + .quirk_refresh_rate_toggle = true, +}; + +static const struct bitland_mifs_ops redmi_g_ops = { + .name = "Redmi G", + .mode_map = &redmi_mode_map, + .check_performance_capability = redmi_pp_check_capability, + .has_full_speed = true, + .has_cpu_power = true, + .quirk_refresh_rate_toggle = true, +}; + +static const struct dmi_system_id bitland_mifs_dmi_table[] = { + { + /* Redmi G */ + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "TM2135"), + }, + .driver_data = (void *)&redmi_g_ops, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "TM2137"), + }, + .driver_data = (void *)&redmi_g_ops, + }, + { + /* Redmi Book series */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Redmi"), + }, + .driver_data = (void *)&redmi_book_ops, + }, + { + /* Xiaomi Book series */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TIMI"), + }, + .driver_data = (void *)&redmi_book_ops, + }, + {} +}; + enum bitland_mifs_event_id { WMI_EVENT_RESERVED_1 = 1, WMI_EVENT_RESERVED_2 = 2, @@ -203,6 +267,8 @@ struct bitland_mifs_wmi_data { struct input_dev *input_dev; struct device *hwmon_dev; struct device *pp_dev; + enum bitland_wmi_device_type dev_type; + u8 kbd_type; const struct bitland_mifs_ops *ops; enum platform_profile_option saved_profile; }; @@ -255,8 +321,10 @@ static int laptop_profile_get(struct device *dev, else if (mode == map->balanced) *profile = PLATFORM_PROFILE_BALANCED; else if (mode == map->performance) - *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; - else if (mode == map->full_speed) + *profile = data->ops->has_full_speed ? + PLATFORM_PROFILE_BALANCED_PERFORMANCE : + PLATFORM_PROFILE_PERFORMANCE; + else if (data->ops->has_full_speed && mode == map->full_speed) *profile = PLATFORM_PROFILE_PERFORMANCE; else return -EINVAL; @@ -273,10 +341,11 @@ static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data) struct bitland_mifs_output output; int ret; - /* Full-speed/performance mode requires DC power (not USB-C) */ + /* The performance modes require AC power */ if (!power_supply_is_system_supplied()) return -EOPNOTSUPP; + /* Additionally require the barrel-jack power supply (not USB-C) */ ret = bitland_mifs_wmi_call(data, &input, &output); if (ret) return ret; @@ -287,6 +356,15 @@ static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data) return 0; } +static int redmi_pp_check_capability(struct bitland_mifs_wmi_data *data) +{ + /* The performance modes require AC power (any source) */ + if (!power_supply_is_system_supplied()) + return -EOPNOTSUPP; + + return 0; +} + static int laptop_profile_set(struct device *dev, enum platform_profile_option profile) { @@ -309,6 +387,8 @@ static int laptop_profile_set(struct device *dev, mode = map->balanced; break; case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + if (!data->ops->has_full_speed) + return -EOPNOTSUPP; ret = data->ops->check_performance_capability(data); if (ret) return ret; @@ -318,7 +398,7 @@ static int laptop_profile_set(struct device *dev, ret = data->ops->check_performance_capability(data); if (ret) return ret; - mode = map->full_speed; + mode = data->ops->has_full_speed ? map->full_speed : map->performance; break; default: return -EOPNOTSUPP; @@ -335,10 +415,10 @@ static int platform_profile_probe(void *drvdata, unsigned long *choices) set_bit(PLATFORM_PROFILE_LOW_POWER, choices); set_bit(PLATFORM_PROFILE_BALANCED, choices); - set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); if (data->ops->has_full_speed) - set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); return 0; } @@ -418,6 +498,13 @@ static int laptop_hwmon_read(struct device *dev, enum hwmon_sensor_types type, default: return -EINVAL; } + case hwmon_power: + /* WMI_FN_CPU_POWER returns the CPU power draw in watts */ + input.function = WMI_FN_CPU_POWER; + ret = bitland_mifs_wmi_call(data, &input, &res); + if (!ret) + *val = res.data[0] * MICROWATT_PER_WATT; + return ret; default: return -EINVAL; } @@ -444,6 +531,15 @@ static const struct hwmon_channel_info *laptop_hwmon_info[] = { NULL }; +static const struct hwmon_channel_info *laptop_hwmon_info_power[] = { + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(power, HWMON_P_INPUT), + NULL +}; + static const struct hwmon_ops laptop_hwmon_ops = { .visible = 0444, .read = laptop_hwmon_read, @@ -455,6 +551,11 @@ static const struct hwmon_chip_info laptop_chip_info = { .info = laptop_hwmon_info, }; +static const struct hwmon_chip_info laptop_chip_info_power = { + .ops = &laptop_hwmon_ops, + .info = laptop_hwmon_info_power, +}; + static int laptop_kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value) { @@ -498,6 +599,96 @@ static const char *const gpu_mode_strings[] = { "uma", }; +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_FN_LOCK, + }; + struct bitland_mifs_output res; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", res.data[0]); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_FN_LOCK, + }; + bool val; + int ret; + + if (kstrtobool(buf, &val)) + return -EINVAL; + + input.payload[0] = val; + + ret = bitland_mifs_wmi_call(data, &input, NULL); + if (ret) + return ret; + + return count; +} + +static ssize_t touchpad_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_TP_LOCK, + }; + struct bitland_mifs_output res; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", res.data[0]); +} + +static ssize_t touchpad_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_TP_LOCK, + }; + bool val; + int ret; + + if (kstrtobool(buf, &val)) + return -EINVAL; + + input.payload[0] = val; + + ret = bitland_mifs_wmi_call(data, &input, NULL); + if (ret) + return ret; + + return count; +} + /* GPU Mode: 0:Hybrid, 1:Discrete, 2:UMA */ static ssize_t gpu_mode_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -637,17 +828,46 @@ static ssize_t fan_boost_store(struct device *dev, return count; } -static const DEVICE_ATTR_RW(gpu_mode); -static const DEVICE_ATTR_RW(kb_mode); -static const DEVICE_ATTR_WO(fan_boost); +static DEVICE_ATTR_RW(gpu_mode); +static DEVICE_ATTR_RW(kb_mode); +static DEVICE_ATTR_WO(fan_boost); +static DEVICE_ATTR_RW(fn_lock); +static DEVICE_ATTR_RW(touchpad_lock); -static const struct attribute *const laptop_attrs[] = { +static struct attribute *laptop_attrs[] = { &dev_attr_gpu_mode.attr, &dev_attr_kb_mode.attr, &dev_attr_fan_boost.attr, + &dev_attr_fn_lock.attr, + &dev_attr_touchpad_lock.attr, NULL, }; -ATTRIBUTE_GROUPS(laptop); + +static umode_t laptop_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + + /* The sysfs attributes are only supported on the control device */ + if (data->dev_type != BITLAND_WMI_CONTROL) + return 0; + + /* + * White-only backlit keyboards (e.g. Redmi Book series) do not + * support the RGB backlight modes. + */ + if (attr == &dev_attr_kb_mode.attr && + data->kbd_type == WMI_KBD_TYPE_WHITE) + return 0; + + return attr->mode; +} + +static const struct attribute_group laptop_group = { + .attrs = laptop_attrs, + .is_visible = laptop_attr_is_visible, +}; static const struct key_entry bitland_mifs_wmi_keymap[] = { { KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_OPEN_APP, 1, 0), { KEY_PROG1 } }, @@ -699,6 +919,34 @@ static const struct key_entry bitland_mifs_wmi_keymap[] = { { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_FNLOCK_STATE, 0, 0), {} }, { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_FNLOCK_STATE, 1, 0), {} }, + /* Airplane mode toggle (Redmi) */ + { KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_AIRPLANE_MODE, 0, 0), { KEY_RFKILL } }, + { KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_AIRPLANE_MODE, 1, 0), { KEY_RFKILL } }, + + /* + * Touchpad state change (Redmi Fn+F7). The EC performs the actual + * toggle itself; userspace can control the state via the + * touchpad_lock sysfs attribute. + */ + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_TOUCHPAD_STATE, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_TOUCHPAD_STATE, 1, 0), {} }, + + /* Keyboard backlight mode change */ + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_KBD_MODE, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_KBD_MODE, 1, 0), {} }, + + /* Lock-state indicator events */ + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_CAPSLOCK_STATE, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_CAPSLOCK_STATE, 1, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_NUMLOCK_STATE, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_NUMLOCK_STATE, 1, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_SCROLLLOCK_STATE, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_SCROLLLOCK_STATE, 1, 0), {} }, + + /* Win key lock (Redmi gaming mode) */ + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_WIN_KEY_LOCK, 0, 0), {} }, + { KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_WIN_KEY_LOCK, 1, 0), {} }, + /* Fn+`/1/2/3/4 */ { KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_FN_F, 1, 0), { KEY_F13 } }, { KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_FN_0, 1, 0), { KEY_F14 } }, @@ -744,6 +992,36 @@ static int bitland_notifier_callback(struct notifier_block *nb, return NOTIFY_OK; } +static int bitland_detect_kbd_type(struct bitland_mifs_wmi_data *data) +{ + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_KBD_TYPE, + }; + struct bitland_mifs_output res; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + data->kbd_type = res.data[0]; + return 0; +} + +static void bitland_detect_ops(struct bitland_mifs_wmi_data *data) +{ + const struct dmi_system_id *id; + + id = dmi_first_match(bitland_mifs_dmi_table); + if (id) + data->ops = id->driver_data; + + dev_dbg(&data->wdev->dev, "Detected machine ops: %s\n", data->ops->name); +} + static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context) { struct bitland_mifs_wmi_data *drv_data; @@ -761,6 +1039,9 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context) return -ENOMEM; drv_data->wdev = wdev; + drv_data->dev_type = dev_type; + /* Default to RGB so the RGB attributes stay visible if detection fails */ + drv_data->kbd_type = WMI_KBD_TYPE_RGB; drv_data->ops = &bitland_ops; ret = devm_mutex_init(&wdev->dev, &drv_data->lock); @@ -769,6 +1050,8 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context) dev_set_drvdata(&wdev->dev, drv_data); + bitland_detect_ops(drv_data); + if (dev_type == BITLAND_WMI_EVENT) { /* Register input device for hotkeys */ drv_data->input_dev = devm_input_allocate_device(&wdev->dev); @@ -794,12 +1077,28 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context) if (IS_ERR(drv_data->pp_dev)) return PTR_ERR(drv_data->pp_dev); + /* Detect the keyboard type */ + ret = bitland_detect_kbd_type(drv_data); + if (ret) + dev_dbg(&wdev->dev, "Failed to detect keyboard type, assuming RGB: %d\n", ret); + + ret = devm_device_add_group(&wdev->dev, &laptop_group); + if (ret) + return ret; + /* Register hwmon */ - drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, - "bitland_mifs", - drv_data, - &laptop_chip_info, - NULL); + if (drv_data->ops->has_cpu_power) + drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, + "bitland_mifs", + drv_data, + &laptop_chip_info_power, + NULL); + else + drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, + "bitland_mifs", + drv_data, + &laptop_chip_info, + NULL); if (IS_ERR(drv_data->hwmon_dev)) return PTR_ERR(drv_data->hwmon_dev); @@ -851,6 +1150,7 @@ static void bitland_mifs_wmi_report_key(struct wmi_device *wdev, u32 payload) static void bitland_mifs_wmi_notify(struct wmi_device *wdev, const struct wmi_buffer *buffer) { + struct bitland_mifs_wmi_data *data = dev_get_drvdata(&wdev->dev); const struct bitland_mifs_event *event = buffer->data; struct bitland_fan_notify_data fan_data; u32 payload; @@ -885,7 +1185,8 @@ static void bitland_mifs_wmi_notify(struct wmi_device *wdev, case WMI_EVENT_CPU_FAN_SPEED: case WMI_EVENT_GPU_FAN_SPEED: /* Redmi refresh rate toggle quirk */ - if (event->event_id == WMI_EVENT_CPU_FAN_SPEED && + if (data->ops->quirk_refresh_rate_toggle && + event->event_id == WMI_EVENT_CPU_FAN_SPEED && event->value_low == 0 && event->value_high == 0) { payload = BI_HOTKEY_CODE(WMI_EVENT_REFRESH_RATE, 0, 0); bitland_mifs_wmi_report_key(wdev, payload); @@ -923,7 +1224,6 @@ static struct wmi_driver bitland_mifs_wmi_driver = { .no_singleton = true, .driver = { .name = DRV_NAME, - .dev_groups = laptop_groups, .pm = pm_sleep_ptr(&bitland_mifs_wmi_pm_ops), }, .id_table = bitland_mifs_wmi_id_table, -- 2.55.0