* [PATCH RFC 0/2] platform/x86: Add Emdoor Intel EWMI platform driver
@ 2026-11-13 18:01 ` Ziqiang Xia
0 siblings, 0 replies; 9+ messages in thread
From: Ziqiang Xia via B4 Relay @ 2026-11-13 18:01 UTC (permalink / raw)
To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia
This RFC patch series adds initial Linux support for the Intel EWMI
(\_SB.EWMI) WMI interface found on Emdoor-based Intel laptops, initially
matched strictly by DMI and BIOS version to the MECHREVO Wujie 14 2026
(WUJIE Series-Lark4-LNL, Intel Core Ultra 5 226V Lunar Lake, BIOS
EM_LNL326_V1.0.19).
Note on naming and scope:
- Community out-of-tree projects such as fewtarius/emdoor-wmi target AMD
Emdoor platforms using different WMI GUIDs (8600ACCE-...) and AMD SMU
ports. This Intel EWMI implementation (0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39,
0B30FD67-1B62-4FCA-B10B-50DC7ADE6DDD, C75665A3-4382-4646-ADC8-C81AE146E455)
is unrelated in protocol and EC register layout, so the driver is named
emdoor-intel-wmi.
- Legacy WMAA-WMCA WMI methods on this DSDT trigger AE_AML_BUFFER_LIMIT due
to an out-of-bounds CreateField() and are deliberately unused. The driver
only uses the clean WMCD, _WED(0xD0), and WMCC interfaces through modern
wmi_driver / wmidev_invoke_method().
Hardware quirks handled and verified on real hardware:
1. Shared EC5D mailbox lag and hotkey collision:
AML methods GWLS (0x03/01) and GFLS (0x03/03) time out after 20 ms on
this BIOS and return the previous mailbox byte on the call itself. After
invoking WMCD and waiting 100 ms, the driver reads the settled mailbox
register directly and retries up to 3 times if concurrent _QF6/_QF9
hotkey events overwrite EC5D in flight.
2. SPFM stale-EC5F drop race:
AML method SPFM (0x03/0a) arms ZZTT/TIZZ before Notify(IETM, 0x88) and
before writing EC5F, so an EC scan during the notify can consume a stale
EC5F and return RTNC=0 without updating PPMD (or return RTNC=1). The
driver verifies 03/09 readback and reissues the absolute mode up to 3 times.
Validation summary on Linux 7.3.4 (MECHREVO WUJIE Series-Lark4-LNL,
EM_LNL326_V1.0.19):
- 1500 readonly attribute/hwmon stress reads (battery & AC online): 0 errors
- 20 platform_profile switches + 40 idempotent writes: 0 errors
- Fn-lock, Win-lock, and auto_power_on write/readback cycles: pass
- High-frequency Fn+Esc / F8 hotkey concurrency stress & s2idle: pass
Specific RFC questions for reviewers:
1. Should fn_lock, win_lock, and auto_power_on stay as WMI device sysfs
attributes (documented in Patch 1), or would pdx86 maintainers prefer a
firmware-attributes class interface or moving any of them?
2. Event codes 0xA1 (F7 mic-mute hotkey, _QF8) and 0xA2 (Fn+R refresh rate,
_QF5) are conservatively mapped to KEY_PROG2 / KEY_PROG3 for now while
verifying whether i8042 also emits scancodes on some desktops/DEs. Is
KEY_MICMUTE / KEY_REFRESH_RATE_TOGGLE preferred once verified, or
separate input mapping?
3. Exact DMI_BIOS_VERSION matching is used because board EC offsets (0x5d,
0xf5, temp_regs {0x70, 0x73, 0x72, 0x62}) are board/BIOS-specific. Is
this strict match appropriate for initial upstreaming?
Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com>
---
Ziqiang Xia (2):
Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI
platform/x86: Add Emdoor Intel EWMI platform driver
.../ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi | 28 +
MAINTAINERS | 7 +
drivers/platform/x86/Kconfig | 18 +
drivers/platform/x86/Makefile | 1 +
drivers/platform/x86/emdoor-intel-wmi.c | 873 +++++++++++++++++++++
5 files changed, 927 insertions(+)
---
base-commit: 5680519168f65666d2c03cc6ed8fe2b7052aee12
change-id: 20261113-emdoor-intel-wmi-rfc-2173530b3df6
Best regards,
--
Ziqiang Xia <xiazq123666@gmail.com>
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH RFC 0/2] platform/x86: Add Emdoor Intel EWMI platform driver @ 2026-11-13 18:01 ` Ziqiang Xia 0 siblings, 0 replies; 9+ messages in thread From: Ziqiang Xia @ 2026-11-13 18:01 UTC (permalink / raw) To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia This RFC patch series adds initial Linux support for the Intel EWMI (\_SB.EWMI) WMI interface found on Emdoor-based Intel laptops, initially matched strictly by DMI and BIOS version to the MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, Intel Core Ultra 5 226V Lunar Lake, BIOS EM_LNL326_V1.0.19). Note on naming and scope: - Community out-of-tree projects such as fewtarius/emdoor-wmi target AMD Emdoor platforms using different WMI GUIDs (8600ACCE-...) and AMD SMU ports. This Intel EWMI implementation (0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39, 0B30FD67-1B62-4FCA-B10B-50DC7ADE6DDD, C75665A3-4382-4646-ADC8-C81AE146E455) is unrelated in protocol and EC register layout, so the driver is named emdoor-intel-wmi. - Legacy WMAA-WMCA WMI methods on this DSDT trigger AE_AML_BUFFER_LIMIT due to an out-of-bounds CreateField() and are deliberately unused. The driver only uses the clean WMCD, _WED(0xD0), and WMCC interfaces through modern wmi_driver / wmidev_invoke_method(). Hardware quirks handled and verified on real hardware: 1. Shared EC5D mailbox lag and hotkey collision: AML methods GWLS (0x03/01) and GFLS (0x03/03) time out after 20 ms on this BIOS and return the previous mailbox byte on the call itself. After invoking WMCD and waiting 100 ms, the driver reads the settled mailbox register directly and retries up to 3 times if concurrent _QF6/_QF9 hotkey events overwrite EC5D in flight. 2. SPFM stale-EC5F drop race: AML method SPFM (0x03/0a) arms ZZTT/TIZZ before Notify(IETM, 0x88) and before writing EC5F, so an EC scan during the notify can consume a stale EC5F and return RTNC=0 without updating PPMD (or return RTNC=1). The driver verifies 03/09 readback and reissues the absolute mode up to 3 times. Validation summary on Linux 7.3.4 (MECHREVO WUJIE Series-Lark4-LNL, EM_LNL326_V1.0.19): - 1500 readonly attribute/hwmon stress reads (battery & AC online): 0 errors - 20 platform_profile switches + 40 idempotent writes: 0 errors - Fn-lock, Win-lock, and auto_power_on write/readback cycles: pass - High-frequency Fn+Esc / F8 hotkey concurrency stress & s2idle: pass Specific RFC questions for reviewers: 1. Should fn_lock, win_lock, and auto_power_on stay as WMI device sysfs attributes (documented in Patch 1), or would pdx86 maintainers prefer a firmware-attributes class interface or moving any of them? 2. Event codes 0xA1 (F7 mic-mute hotkey, _QF8) and 0xA2 (Fn+R refresh rate, _QF5) are conservatively mapped to KEY_PROG2 / KEY_PROG3 for now while verifying whether i8042 also emits scancodes on some desktops/DEs. Is KEY_MICMUTE / KEY_REFRESH_RATE_TOGGLE preferred once verified, or separate input mapping? 3. Exact DMI_BIOS_VERSION matching is used because board EC offsets (0x5d, 0xf5, temp_regs {0x70, 0x73, 0x72, 0x62}) are board/BIOS-specific. Is this strict match appropriate for initial upstreaming? Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> --- Ziqiang Xia (2): Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI platform/x86: Add Emdoor Intel EWMI platform driver .../ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi | 28 + MAINTAINERS | 7 + drivers/platform/x86/Kconfig | 18 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/emdoor-intel-wmi.c | 873 +++++++++++++++++++++ 5 files changed, 927 insertions(+) --- base-commit: 5680519168f65666d2c03cc6ed8fe2b7052aee12 change-id: 20261113-emdoor-intel-wmi-rfc-2173530b3df6 Best regards, -- Ziqiang Xia <xiazq123666@gmail.com> ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH RFC 1/2] Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI 2026-11-13 18:01 ` Ziqiang Xia @ 2026-11-13 18:01 ` Ziqiang Xia -1 siblings, 0 replies; 9+ messages in thread From: Ziqiang Xia via B4 Relay @ 2026-11-13 18:01 UTC (permalink / raw) To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia From: Ziqiang Xia <xiazq123666@gmail.com> Document the fn_lock, win_lock, touchpad_enabled, and auto_power_on sysfs attributes exposed by the Emdoor Intel EWMI platform driver. Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> --- .../ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi | 28 ++++++++++++++++++++++ MAINTAINERS | 6 +++++ 2 files changed, 34 insertions(+) diff --git a/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi b/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi new file mode 100644 index 000000000..e139d2929 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi @@ -0,0 +1,28 @@ +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/fn_lock +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/win_lock +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RW) Current Fn-lock or Windows-key-lock state ("0" or "1") + queried via GFLS (0x03/0x03) / GWLS (0x03/0x01) and updated + via SFLS (0x03/0x04) / SWLS (0x03/0x02) followed by settling + delay and readback verification. + +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/touchpad_enabled +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RO) Firmware touchpad enable state ("0" or "1") queried via + GTPS (0x03/0x07). + +What: /sys/bus/wmi/devices/C75665A3-4382-4646-ADC8-C81AE146E455-*/auto_power_on +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RW) AC power-on recovery setting ("0" or "1"), backed by + board EC register 0xF5 (0x00=off, 0xa0=on) and updated + through standard WMI method WMCC followed by settling delay + and EC readback verification. diff --git a/MAINTAINERS b/MAINTAINERS index 981eb3fdb..70c976ffe 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9848,6 +9848,12 @@ T: git git://linuxtv.org/media.git F: Documentation/admin-guide/media/em28xx* F: drivers/media/usb/em28xx/ +EMDOOR INTEL EWMI DRIVER +M: Ziqiang Xia <xiazq123666@gmail.com> +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi + EMMC CMDQ HOST CONTROLLER INTERFACE (CQHCI) DRIVER M: Adrian Hunter <adrian.hunter@intel.com> M: Asutosh Das <quic_asutoshd@quicinc.com> -- 2.56.1 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH RFC 1/2] Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI @ 2026-11-13 18:01 ` Ziqiang Xia 0 siblings, 0 replies; 9+ messages in thread From: Ziqiang Xia @ 2026-11-13 18:01 UTC (permalink / raw) To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia Document the fn_lock, win_lock, touchpad_enabled, and auto_power_on sysfs attributes exposed by the Emdoor Intel EWMI platform driver. Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> --- .../ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi | 28 ++++++++++++++++++++++ MAINTAINERS | 6 +++++ 2 files changed, 34 insertions(+) diff --git a/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi b/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi new file mode 100644 index 000000000..e139d2929 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi @@ -0,0 +1,28 @@ +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/fn_lock +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/win_lock +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RW) Current Fn-lock or Windows-key-lock state ("0" or "1") + queried via GFLS (0x03/0x03) / GWLS (0x03/0x01) and updated + via SFLS (0x03/0x04) / SWLS (0x03/0x02) followed by settling + delay and readback verification. + +What: /sys/bus/wmi/devices/0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39-*/touchpad_enabled +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RO) Firmware touchpad enable state ("0" or "1") queried via + GTPS (0x03/0x07). + +What: /sys/bus/wmi/devices/C75665A3-4382-4646-ADC8-C81AE146E455-*/auto_power_on +Date: November 2026 +KernelVersion: 7.4 +Contact: platform-driver-x86@vger.kernel.org +Description: + (RW) AC power-on recovery setting ("0" or "1"), backed by + board EC register 0xF5 (0x00=off, 0xa0=on) and updated + through standard WMI method WMCC followed by settling delay + and EC readback verification. diff --git a/MAINTAINERS b/MAINTAINERS index 981eb3fdb..70c976ffe 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9848,6 +9848,12 @@ T: git git://linuxtv.org/media.git F: Documentation/admin-guide/media/em28xx* F: drivers/media/usb/em28xx/ +EMDOOR INTEL EWMI DRIVER +M: Ziqiang Xia <xiazq123666@gmail.com> +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi + EMMC CMDQ HOST CONTROLLER INTERFACE (CQHCI) DRIVER M: Adrian Hunter <adrian.hunter@intel.com> M: Asutosh Das <quic_asutoshd@quicinc.com> -- 2.56.1 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH RFC 1/2] Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI 2026-11-13 18:01 ` Ziqiang Xia (?) @ 2026-11-13 18:15 ` sashiko-bot -1 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-11-13 18:15 UTC (permalink / raw) To: Ziqiang Xia; +Cc: linux-hwmon > From: Ziqiang Xia <xiazq123666@gmail.com> > > Document the fn_lock, win_lock, touchpad_enabled, and auto_power_on > sysfs attributes exposed by the Emdoor Intel EWMI platform driver. > > Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> Sashiko has reviewed this patch and found no issues. -- Sashiko AI review · https://sashiko.dev/#/patchset/20261113-emdoor-intel-wmi-rfc-v1-0-d77ecf23693b@gmail.com?part=1 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH RFC 2/2] platform/x86: Add Emdoor Intel EWMI platform driver 2026-11-13 18:01 ` Ziqiang Xia @ 2026-11-13 18:01 ` Ziqiang Xia -1 siblings, 0 replies; 9+ messages in thread From: Ziqiang Xia via B4 Relay @ 2026-11-13 18:01 UTC (permalink / raw) To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia From: Ziqiang Xia <xiazq123666@gmail.com> Add support for the Emdoor Intel EWMI (\_SB.EWMI) WMI interface on MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, BIOS EM_LNL326_V1.0.19), exposing hwmon fan and EC temperature sensors, platform_profile (balanced/performance), Fn/Win lock and touchpad state attributes, WMI hotkey sparse keymap events, and AC auto-power-on control. Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> --- MAINTAINERS | 1 + drivers/platform/x86/Kconfig | 18 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/emdoor-intel-wmi.c | 873 ++++++++++++++++++++++++++++++++ 4 files changed, 893 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 70c976ffe..1a377dd56 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9853,6 +9853,7 @@ M: Ziqiang Xia <xiazq123666@gmail.com> L: platform-driver-x86@vger.kernel.org S: Maintained F: Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi +F: drivers/platform/x86/emdoor-intel-wmi.c EMMC CMDQ HOST CONTROLLER INTERFACE (CQHCI) DRIVER M: Adrian Hunter <adrian.hunter@intel.com> diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index b2c6cc923..4682b79e0 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -120,6 +120,24 @@ config BITLAND_MIFS_WMI To compile this driver as a module, choose M here: the module will be called bitland-mifs-wmi. +config EMDOOR_INTEL_WMI + tristate "Emdoor Intel EWMI laptop support" + depends on ACPI_EC + depends on ACPI_WMI + depends on DMI + depends on HWMON + depends on INPUT + select ACPI_PLATFORM_PROFILE + select INPUT_SPARSEKMAP + help + This is a driver for Emdoor Intel EWMI laptops, initially limited to + the MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, EM_LNL326_V1.0.19). + It exposes hwmon fan/temperature sensors, platform_profile, hotkeys, + Fn/Win lock controls, and AC auto-power-on control. + + To compile this driver as a module, choose M here: the module will be + called emdoor-intel-wmi. + config ACERHDF tristate "Acer Aspire One temperature and fan driver" depends on ACPI_EC && THERMAL diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index 88f45e03e..45adabafb 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -12,6 +12,7 @@ obj-$(CONFIG_XIAOMI_WMI) += xiaomi-wmi.o obj-$(CONFIG_REDMI_WMI) += redmi-wmi.o obj-$(CONFIG_GIGABYTE_WMI) += gigabyte-wmi.o obj-$(CONFIG_BITLAND_MIFS_WMI) += bitland-mifs-wmi.o +obj-$(CONFIG_EMDOOR_INTEL_WMI) += emdoor-intel-wmi.o # Acer obj-$(CONFIG_ACERHDF) += acerhdf.o diff --git a/drivers/platform/x86/emdoor-intel-wmi.c b/drivers/platform/x86/emdoor-intel-wmi.c new file mode 100644 index 000000000..6edd09f6f --- /dev/null +++ b/drivers/platform/x86/emdoor-intel-wmi.c @@ -0,0 +1,873 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Emdoor Intel EWMI platform driver for MECHREVO Wujie 14 2026 + * (WUJIE Series-Lark4-LNL, BIOS EM_LNL326_V1.0.19). + */ + +#include <linux/acpi.h> +#include <linux/atomic.h> +#include <linux/bits.h> +#include <linux/cleanup.h> +#include <linux/container_of.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/dmi.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/hwmon.h> +#include <linux/input.h> +#include <linux/input/sparse-keymap.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/platform_profile.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/sysfs.h> +#include <linux/types.h> +#include <linux/unaligned.h> +#include <linux/wmi.h> +#include <linux/workqueue.h> + +#define EWMI_METHOD_GUID "0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39" +#define EWMI_EVENT_GUID "0B30FD67-1B62-4FCA-B10B-50DC7ADE6DDD" +#define EWMI_AUTOPOWER_GUID "C75665A3-4382-4646-ADC8-C81AE146E455" + +#define EWMI_INPUT_SIZE 64 +#define EWMI_OUTPUT_SIZE 256 + +enum { + EWMI_FAN_COUNT = 2, + EWMI_TEMP_COUNT = 4, + EWMI_MAILBOX_RETRIES = 3, +}; + +struct ewmi_board_config { + u8 mailbox_reg; + u8 autopower_reg; + unsigned int query_settle_ms; + unsigned int setter_settle_ms; + u8 temp_regs[EWMI_TEMP_COUNT]; +}; + +struct ewmi_reply { + u8 reserved[4]; + u8 status; + u8 data[EWMI_OUTPUT_SIZE - 1]; +}; + +static_assert(sizeof(struct ewmi_reply) == 260); + +struct ewmi_method { + struct wmi_device *wdev; + const struct ewmi_board_config *board; + struct mutex lock; /* Serializes firmware calls and EC sensor reads. */ + struct device *profile; + struct notifier_block event_nb; + struct work_struct profile_work; + atomic_t mailbox_event_seq; /* EC5D hotkeys invalidate in-flight queries. */ + bool profile_dirty; /* A partial set must not become a successful no-op. */ + u8 lock_dirty; /* Uncertain lock settings must be explicitly restored. */ +}; + +struct ewmi_event { + struct input_dev *input; + struct mutex lock; /* Serializes input press/release sequences. */ +}; + +struct ewmi_autopower { + struct wmi_device *wdev; + const struct ewmi_board_config *board; + struct mutex lock; /* Serializes auto-power-on queries and settings. */ + bool dirty; /* A failed setting may have partially applied. */ +}; + +static BLOCKING_NOTIFIER_HEAD(ewmi_event_chain); + +/* Offsets apply only to the verified MECHREVO Lark4 Lunar Lake BIOS. */ +static const struct ewmi_board_config ewmi_lark4_lnl_config = { + .mailbox_reg = 0x5d, + .autopower_reg = 0xf5, + .query_settle_ms = 100, + .setter_settle_ms = 100, + .temp_regs = { 0x70, 0x73, 0x72, 0x62 }, +}; + +static const struct dmi_system_id ewmi_dmi_table[] = { + { + .ident = "MECHREVO Wujie 14 2026 Lark4 Lunar Lake", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "MECHREVO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "WUJIE Series-Lark4-LNL"), + DMI_EXACT_MATCH(DMI_BIOS_VERSION, "EM_LNL326_V1.0.19"), + }, + .driver_data = (void *)&ewmi_lark4_lnl_config, + }, + { } +}; + +static const struct ewmi_board_config *ewmi_get_board_config(void) +{ + const struct dmi_system_id *id; + + id = dmi_first_match(ewmi_dmi_table); + return id ? id->driver_data : NULL; +} + +/* + * GWLS (0x03/0x01) and GFLS (0x03/0x03) read the shared EC mailbox register + * (EC5D) after settling without sending a second overlapping command. + */ +static int ewmi_mailbox_reply(struct ewmi_method *priv, u8 command, + u8 *value) +{ + u8 raw; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ec_read(priv->board->mailbox_reg, &raw); + if (ret) + return ret; + + switch (command) { + case 0x01: + if (raw != 0 && raw != 2) + goto invalid; + *value = raw == 2; + return 0; + case 0x03: + if (raw > 1) + goto invalid; + *value = raw; + return 0; + default: + return -EOPNOTSUPP; + } + +invalid: + dev_dbg(&priv->wdev->dev, + "Settled mailbox 03/%02x transient/invalid raw 0x%02x\n", + command, raw); + return -EIO; +} + +/* + * WMCD returns Package(Buffer(4), Buffer(256)). The WMI core marshals + * both buffers into 260 bytes; the firmware status is at byte 4. + * Always check the full result size before accessing the payload. + */ +static int ewmi_call_status(struct ewmi_method *priv, u8 group, u8 command, + u8 argument, void *result, size_t result_len, + bool *firmware_failure) +{ + bool mailbox_query = group == 0x03 && (command == 0x01 || command == 0x03); + u8 request[EWMI_INPUT_SIZE] = { group, command, argument }; + struct wmi_buffer in = { .length = sizeof(request), .data = request }; + struct wmi_buffer out = { }; + const struct ewmi_reply *reply; + int attempt, ret = -EIO; + + lockdep_assert_held(&priv->lock); + if (firmware_failure) + *firmware_failure = false; + if (result_len > sizeof(reply->data)) + return -EINVAL; + if (mailbox_query && result_len != sizeof(u8)) + return -EINVAL; + + for (attempt = 0; attempt < EWMI_MAILBOX_RETRIES; attempt++) { + int seq = atomic_read(&priv->mailbox_event_seq); + + ret = wmidev_invoke_method(priv->wdev, 0, 1, &in, &out, + sizeof(*reply)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x transport error %d\n", + group, command, ret); + return ret; + } + if (!mailbox_query) + break; + + /* + * GWLS/GFLS share EC5D with each other and with _QF6/_QF9 + * hotkey notifications. Wait for the EC to settle and retry + * if an in-flight hotkey overwrites the mailbox. + */ + kfree(out.data); + out = (struct wmi_buffer) { }; + msleep(priv->board->query_settle_ms); + ret = ewmi_mailbox_reply(priv, command, result); + if (!ret && atomic_read(&priv->mailbox_event_seq) != seq) + ret = -EAGAIN; + if (!ret) + return 0; + if ((ret == -EIO || ret == -EAGAIN) && + attempt + 1 < EWMI_MAILBOX_RETRIES) + continue; + if (ret == -EAGAIN) + ret = -EIO; + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x settled mailbox error %d\n", + group, command, ret); + return ret; + } + + reply = out.data; + if (reply->status) { + if (firmware_failure && reply->status == 1) + *firmware_failure = true; + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x firmware status %u, data %8ph\n", + group, command, reply->status, reply->data); + ret = -EIO; + } else if (result_len) { + memcpy(result, reply->data, result_len); + } + kfree(out.data); + return ret; +} + +static int ewmi_call(struct ewmi_method *priv, u8 group, u8 command, + u8 argument, void *result, size_t result_len) +{ + return ewmi_call_status(priv, group, command, argument, result, + result_len, NULL); +} + +static ssize_t ewmi_switch_show(struct device *dev, u8 command, char *buf) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 value; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, command, 0, &value, sizeof(value)); + if (ret) + return ret; + if (value > 1) + return -EPROTO; + return sysfs_emit(buf, "%u\n", value); +} + +static ssize_t ewmi_switch_store(struct device *dev, u8 command, + const char *buf, size_t count) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + bool value; + u8 actual, mask; + int ret; + + if (command != 0x02 && command != 0x04) + return -EINVAL; + mask = command == 0x02 ? BIT(0) : BIT(1); + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, command - 1, 0, &actual, sizeof(actual)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x before-set query error %d\n", + command, ret); + return ret; + } + if (actual == value && !(priv->lock_dirty & mask)) + return count; + priv->lock_dirty |= mask; + ret = ewmi_call(priv, 0x03, command, value, NULL, 0); + if (ret) + return ret; + /* SFLS/SWLS do not wait: a query can overwrite the pending set command. */ + msleep(priv->board->setter_settle_ms); + /* The paired query waits for the EC reply before returning its state. */ + ret = ewmi_call(priv, 0x03, command - 1, 0, &actual, sizeof(actual)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x after-set query error %d\n", + command, ret); + return ret; + } + if (actual != value) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x requested %u, settled readback %u\n", + command, value, actual); + return -EIO; + } + priv->lock_dirty &= ~mask; + return count; +} + +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return ewmi_switch_show(dev, 0x03, buf); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return ewmi_switch_store(dev, 0x04, buf, count); +} +static DEVICE_ATTR_RW(fn_lock); + +static ssize_t win_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return ewmi_switch_show(dev, 0x01, buf); +} + +static ssize_t win_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return ewmi_switch_store(dev, 0x02, buf, count); +} +static DEVICE_ATTR_RW(win_lock); + +static ssize_t touchpad_enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return ewmi_switch_show(dev, 0x07, buf); +} +static DEVICE_ATTR_RO(touchpad_enabled); + +static struct attribute *ewmi_attrs[] = { + &dev_attr_fn_lock.attr, + &dev_attr_win_lock.attr, + &dev_attr_touchpad_enabled.attr, + NULL +}; +ATTRIBUTE_GROUPS(ewmi); + +static const char * const ewmi_temp_labels[EWMI_TEMP_COUNT] = { + "CPU", "GT VR", "Ambient", "Charger" +}; + +static const char * const ewmi_fan_labels[EWMI_FAN_COUNT] = { + "Fan 1", "Fan 2" +}; + +static int ewmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *value) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 temperature; + __le16 rpm; + int ret; + + guard(mutex)(&priv->lock); + if (type == hwmon_fan && attr == hwmon_fan_input) { + if (channel < 0 || channel >= EWMI_FAN_COUNT) + return -EOPNOTSUPP; + ret = ewmi_call(priv, 0x02, 0x01, channel, &rpm, sizeof(rpm)); + if (!ret) + *value = le16_to_cpu(rpm); + return ret; + } + if (type == hwmon_temp && attr == hwmon_temp_input) { + if (channel < 0 || channel >= EWMI_TEMP_COUNT) + return -EOPNOTSUPP; + ret = ec_read(priv->board->temp_regs[channel], &temperature); + if (!ret) { + if (temperature == 0xff) + return -ENODATA; + *value = temperature * 1000L; + } + return ret; + } + return -EOPNOTSUPP; +} + +static int ewmi_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + if (type == hwmon_fan && attr == hwmon_fan_label && + channel >= 0 && channel < EWMI_FAN_COUNT) { + *str = ewmi_fan_labels[channel]; + return 0; + } + if (type == hwmon_temp && attr == hwmon_temp_label && + channel >= 0 && channel < EWMI_TEMP_COUNT) { + *str = ewmi_temp_labels[channel]; + return 0; + } + return -EOPNOTSUPP; +} + +static const struct hwmon_ops ewmi_hwmon_ops = { + .visible = 0444, + .read = ewmi_hwmon_read, + .read_string = ewmi_hwmon_read_string, +}; + +static const struct hwmon_channel_info * const ewmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + NULL +}; + +static const struct hwmon_chip_info ewmi_hwmon_chip = { + .ops = &ewmi_hwmon_ops, + .info = ewmi_hwmon_info, +}; + +static int ewmi_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + return 0; +} + +static int ewmi_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 mode; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, 0x09, 0, &mode, sizeof(mode)); + if (ret) + return ret; + switch (mode) { + case 1: + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + case 2: + *profile = PLATFORM_PROFILE_PERFORMANCE; + return 0; + default: + return -EPROTO; + } +} + +static int ewmi_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + bool firmware_failure; + u8 mode, actual; + int attempt, ret; + + switch (profile) { + case PLATFORM_PROFILE_BALANCED: + mode = 1; + break; + case PLATFORM_PROFILE_PERFORMANCE: + mode = 2; + break; + default: + return -EOPNOTSUPP; + } + + guard(mutex)(&priv->lock); + /* Avoid a redundant EC command when userspace reapplies this profile. */ + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode && !priv->profile_dirty) + return 0; + + /* + * SPFM arms the EC (ZZTT/TIZZ) before Notify(IETM, 0x88) and before + * writing EC5F, so an EC scan during the notify can consume a stale + * EC5F and return RTNC=0 without updating PPMD; SPFM can also report + * RTNC=1 after changing mode. Reissue this absolute mode command up + * to three times when RTNC=1 or 03/09 still reads back the old mode. + */ + priv->profile_dirty = true; + for (attempt = 0; attempt < EWMI_MAILBOX_RETRIES; attempt++) { + ret = ewmi_call_status(priv, 0x03, 0x0a, mode, NULL, 0, + &firmware_failure); + if (ret) { + if (!firmware_failure || + attempt + 1 == EWMI_MAILBOX_RETRIES) + return ret; + msleep(priv->board->setter_settle_ms); + continue; + } + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode) { + priv->profile_dirty = false; + return 0; + } + msleep(priv->board->setter_settle_ms); + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode) { + priv->profile_dirty = false; + return 0; + } + if (attempt + 1 < EWMI_MAILBOX_RETRIES) + dev_dbg(dev, + "Profile 03/0a requested %u, readback %u on attempt %d/%d; retrying\n", + mode, actual, attempt + 1, + EWMI_MAILBOX_RETRIES); + } + dev_warn_ratelimited(dev, + "Profile 03/0a requested %u, settled readback %u\n", + mode, actual); + return -EIO; +} + +static const struct platform_profile_ops ewmi_profile_ops = { + .probe = ewmi_profile_probe, + .profile_get = ewmi_profile_get, + .profile_set = ewmi_profile_set, +}; + +static void ewmi_profile_work(struct work_struct *work) +{ + struct ewmi_method *priv = container_of(work, struct ewmi_method, + profile_work); + + platform_profile_notify(priv->profile); +} + +static int ewmi_method_event(struct notifier_block *nb, unsigned long code, + void *data) +{ + struct ewmi_method *priv = container_of(nb, struct ewmi_method, + event_nb); + struct wmi_device *event_wdev = data; + + if (event_wdev->dev.parent != priv->wdev->dev.parent) + return NOTIFY_DONE; + /* _QF6 (0x50/0x51) and _QF9 (0x20-0x22) share EC5D with WMCD queries. */ + if (code == 0x20 || code == 0x21 || code == 0x22 || + code == 0x50 || code == 0x51) + atomic_inc(&priv->mailbox_event_seq); + if (code == 0 || code == 0x41 || code == 0x42) + schedule_work(&priv->profile_work); + return NOTIFY_OK; +} + +static void ewmi_method_stop(void *data) +{ + struct ewmi_method *priv = data; + + blocking_notifier_chain_unregister(&ewmi_event_chain, &priv->event_nb); + cancel_work_sync(&priv->profile_work); +} + +static int ewmi_method_probe(struct wmi_device *wdev, const void *context) +{ + static const u8 expected_version[] = { 1, 0, 1, 0 }; + const struct ewmi_board_config *board; + struct ewmi_method *priv; + struct device *hwmon; + u8 version[4]; + int ret; + + board = ewmi_get_board_config(); + if (!board) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + priv->wdev = wdev; + priv->board = board; + mutex_init(&priv->lock); + INIT_WORK(&priv->profile_work, ewmi_profile_work); + dev_set_drvdata(&wdev->dev, priv); + + scoped_guard(mutex, &priv->lock) + ret = ewmi_call(priv, 0x01, 0x01, 0, version, sizeof(version)); + if (ret) + return dev_err_probe(&wdev->dev, ret, + "Cannot query protocol version\n"); + if (memcmp(version, expected_version, sizeof(version))) + return dev_err_probe(&wdev->dev, -ENODEV, + "Unsupported protocol version\n"); + + priv->profile = devm_platform_profile_register(&wdev->dev, + "emdoor-intel-wmi", + priv, &ewmi_profile_ops); + if (IS_ERR(priv->profile)) + return dev_err_probe(&wdev->dev, PTR_ERR(priv->profile), + "Cannot register platform profile\n"); + + priv->event_nb.notifier_call = ewmi_method_event; + ret = blocking_notifier_chain_register(&ewmi_event_chain, + &priv->event_nb); + if (ret) + return ret; + ret = devm_add_action_or_reset(&wdev->dev, ewmi_method_stop, priv); + if (ret) + return ret; + + hwmon = devm_hwmon_device_register_with_info(&wdev->dev, "emdoor_ewmi", + priv, &ewmi_hwmon_chip, + NULL); + if (IS_ERR(hwmon)) + return dev_err_probe(&wdev->dev, PTR_ERR(hwmon), + "Cannot register hwmon\n"); + + return 0; +} + +/* Firmware already changes these states; never request a second toggle. */ +static const struct key_entry ewmi_keymap[] = { + { KE_KEY, 0x30, { KEY_TOUCHPAD_OFF } }, + { KE_KEY, 0x31, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 0xa0, { KEY_PROG1 } }, + { KE_KEY, 0xa1, { KEY_PROG2 } }, + { KE_KEY, 0xa2, { KEY_PROG3 } }, + { KE_IGNORE, 0x00, { 0 } }, + { KE_IGNORE, 0x41, { 0 } }, + { KE_IGNORE, 0x42, { 0 } }, + { KE_IGNORE, 0x20, { 0 } }, + { KE_IGNORE, 0x21, { 0 } }, + { KE_IGNORE, 0x22, { 0 } }, + { KE_IGNORE, 0x50, { 0 } }, + { KE_IGNORE, 0x51, { 0 } }, + { KE_END, 0 } +}; + +static int ewmi_event_probe(struct wmi_device *wdev, const void *context) +{ + struct ewmi_event *priv; + int ret; + + if (!ewmi_get_board_config()) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + mutex_init(&priv->lock); + priv->input = devm_input_allocate_device(&wdev->dev); + if (!priv->input) + return -ENOMEM; + priv->input->name = "Emdoor EWMI hotkeys"; + priv->input->phys = "wmi/input0"; + priv->input->id.bustype = BUS_HOST; + ret = sparse_keymap_setup(priv->input, ewmi_keymap, NULL); + if (ret) + return ret; + dev_set_drvdata(&wdev->dev, priv); + return input_register_device(priv->input); +} + +static void ewmi_event_notify(struct wmi_device *wdev, + const struct wmi_buffer *event) +{ + struct ewmi_event *priv = dev_get_drvdata(&wdev->dev); + u32 code = get_unaligned_le32(event->data); + + blocking_notifier_call_chain(&ewmi_event_chain, code, wdev); + guard(mutex)(&priv->lock); + if (!sparse_keymap_report_event(priv->input, code, 1, true)) + dev_dbg(&wdev->dev, "Unknown event 0x%x\n", code); +} + +static const struct wmi_device_id ewmi_method_ids[] = { + { EWMI_METHOD_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_method_ids); + +static const struct wmi_device_id ewmi_event_ids[] = { + { EWMI_EVENT_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_event_ids); + +static struct wmi_driver ewmi_method_driver = { + .driver = { + .name = "emdoor-ewmi", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .dev_groups = ewmi_groups, + }, + .id_table = ewmi_method_ids, + .no_singleton = true, + .probe = ewmi_method_probe, +}; + +static struct wmi_driver ewmi_event_driver = { + .driver = { + .name = "emdoor-ewmi-events", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = ewmi_event_ids, + .no_singleton = true, + .min_event_size = sizeof(__le32), + .probe = ewmi_event_probe, + .notify_new = ewmi_event_notify, +}; + +static int ewmi_autopower_get(struct ewmi_autopower *priv, bool *enabled) +{ + u8 raw; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ec_read(priv->board->autopower_reg, &raw); + if (ret) + return ret; + if (raw != 0 && raw != 0xa0) + return -EPROTO; + *enabled = raw == 0xa0; + return 0; +} + +static int ewmi_autopower_set(struct ewmi_autopower *priv, bool enabled) +{ + __le64 argument = cpu_to_le64(enabled); + struct wmi_buffer in = { .length = sizeof(argument), .data = &argument }; + struct wmi_buffer out = { }; + bool actual; + u8 status; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ewmi_autopower_get(priv, &actual); + if (ret || (actual == enabled && !priv->dirty)) + return ret; + priv->dirty = true; + ret = wmidev_invoke_method(priv->wdev, 0, 1, &in, &out, 8); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC transport error %d\n", ret); + return ret; + } + status = ((u8 *)out.data)[0]; + ret = status ? -EIO : 0; + kfree(out.data); + msleep(priv->board->setter_settle_ms); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC firmware status %u\n", status); + return ret; + } + ret = ewmi_autopower_get(priv, &actual); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC readback error %d\n", ret); + return ret; + } + if (actual != enabled) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC requested %u, readback %u\n", + enabled, actual); + return -EIO; + } + priv->dirty = false; + return 0; +} + +static ssize_t auto_power_on_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ewmi_autopower *priv = dev_get_drvdata(dev); + bool enabled; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_autopower_get(priv, &enabled); + if (ret) + return ret; + return sysfs_emit(buf, "%u\n", enabled); +} + +static ssize_t auto_power_on_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ewmi_autopower *priv = dev_get_drvdata(dev); + bool enabled; + int ret; + + ret = kstrtobool(buf, &enabled); + if (ret) + return ret; + guard(mutex)(&priv->lock); + ret = ewmi_autopower_set(priv, enabled); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(auto_power_on); + +static struct attribute *ewmi_autopower_attrs[] = { + &dev_attr_auto_power_on.attr, + NULL +}; +ATTRIBUTE_GROUPS(ewmi_autopower); + +static int ewmi_autopower_probe(struct wmi_device *wdev, const void *context) +{ + const struct ewmi_board_config *board; + struct ewmi_autopower *priv; + + board = ewmi_get_board_config(); + if (!board) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + priv->wdev = wdev; + priv->board = board; + mutex_init(&priv->lock); + dev_set_drvdata(&wdev->dev, priv); + return 0; +} + +static const struct wmi_device_id ewmi_autopower_ids[] = { + { EWMI_AUTOPOWER_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_autopower_ids); + +static struct wmi_driver ewmi_autopower_driver = { + .driver = { + .name = "emdoor-ewmi-autopower", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .dev_groups = ewmi_autopower_groups, + }, + .id_table = ewmi_autopower_ids, + .no_singleton = true, + .probe = ewmi_autopower_probe, +}; + +static int __init ewmi_init(void) +{ + int ret; + + ret = wmi_driver_register(&ewmi_method_driver); + if (ret) + return ret; + ret = wmi_driver_register(&ewmi_event_driver); + if (ret) { + wmi_driver_unregister(&ewmi_method_driver); + return ret; + } + ret = wmi_driver_register(&ewmi_autopower_driver); + if (ret) { + wmi_driver_unregister(&ewmi_event_driver); + wmi_driver_unregister(&ewmi_method_driver); + } + return ret; +} + +static void __exit ewmi_exit(void) +{ + wmi_driver_unregister(&ewmi_autopower_driver); + wmi_driver_unregister(&ewmi_event_driver); + wmi_driver_unregister(&ewmi_method_driver); +} + +module_init(ewmi_init); +module_exit(ewmi_exit); + +MODULE_AUTHOR("Ziqiang Xia <xiazq123666@gmail.com>"); +MODULE_DESCRIPTION("Emdoor Intel EWMI platform driver"); +MODULE_LICENSE("GPL"); -- 2.56.1 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH RFC 2/2] platform/x86: Add Emdoor Intel EWMI platform driver @ 2026-11-13 18:01 ` Ziqiang Xia 0 siblings, 0 replies; 9+ messages in thread From: Ziqiang Xia @ 2026-11-13 18:01 UTC (permalink / raw) To: Armin Wolf, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon, Ziqiang Xia Add support for the Emdoor Intel EWMI (\_SB.EWMI) WMI interface on MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, BIOS EM_LNL326_V1.0.19), exposing hwmon fan and EC temperature sensors, platform_profile (balanced/performance), Fn/Win lock and touchpad state attributes, WMI hotkey sparse keymap events, and AC auto-power-on control. Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> --- MAINTAINERS | 1 + drivers/platform/x86/Kconfig | 18 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/emdoor-intel-wmi.c | 873 ++++++++++++++++++++++++++++++++ 4 files changed, 893 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 70c976ffe..1a377dd56 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9853,6 +9853,7 @@ M: Ziqiang Xia <xiazq123666@gmail.com> L: platform-driver-x86@vger.kernel.org S: Maintained F: Documentation/ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi +F: drivers/platform/x86/emdoor-intel-wmi.c EMMC CMDQ HOST CONTROLLER INTERFACE (CQHCI) DRIVER M: Adrian Hunter <adrian.hunter@intel.com> diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index b2c6cc923..4682b79e0 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -120,6 +120,24 @@ config BITLAND_MIFS_WMI To compile this driver as a module, choose M here: the module will be called bitland-mifs-wmi. +config EMDOOR_INTEL_WMI + tristate "Emdoor Intel EWMI laptop support" + depends on ACPI_EC + depends on ACPI_WMI + depends on DMI + depends on HWMON + depends on INPUT + select ACPI_PLATFORM_PROFILE + select INPUT_SPARSEKMAP + help + This is a driver for Emdoor Intel EWMI laptops, initially limited to + the MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, EM_LNL326_V1.0.19). + It exposes hwmon fan/temperature sensors, platform_profile, hotkeys, + Fn/Win lock controls, and AC auto-power-on control. + + To compile this driver as a module, choose M here: the module will be + called emdoor-intel-wmi. + config ACERHDF tristate "Acer Aspire One temperature and fan driver" depends on ACPI_EC && THERMAL diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index 88f45e03e..45adabafb 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -12,6 +12,7 @@ obj-$(CONFIG_XIAOMI_WMI) += xiaomi-wmi.o obj-$(CONFIG_REDMI_WMI) += redmi-wmi.o obj-$(CONFIG_GIGABYTE_WMI) += gigabyte-wmi.o obj-$(CONFIG_BITLAND_MIFS_WMI) += bitland-mifs-wmi.o +obj-$(CONFIG_EMDOOR_INTEL_WMI) += emdoor-intel-wmi.o # Acer obj-$(CONFIG_ACERHDF) += acerhdf.o diff --git a/drivers/platform/x86/emdoor-intel-wmi.c b/drivers/platform/x86/emdoor-intel-wmi.c new file mode 100644 index 000000000..6edd09f6f --- /dev/null +++ b/drivers/platform/x86/emdoor-intel-wmi.c @@ -0,0 +1,873 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Emdoor Intel EWMI platform driver for MECHREVO Wujie 14 2026 + * (WUJIE Series-Lark4-LNL, BIOS EM_LNL326_V1.0.19). + */ + +#include <linux/acpi.h> +#include <linux/atomic.h> +#include <linux/bits.h> +#include <linux/cleanup.h> +#include <linux/container_of.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/dmi.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/hwmon.h> +#include <linux/input.h> +#include <linux/input/sparse-keymap.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/platform_profile.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/sysfs.h> +#include <linux/types.h> +#include <linux/unaligned.h> +#include <linux/wmi.h> +#include <linux/workqueue.h> + +#define EWMI_METHOD_GUID "0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39" +#define EWMI_EVENT_GUID "0B30FD67-1B62-4FCA-B10B-50DC7ADE6DDD" +#define EWMI_AUTOPOWER_GUID "C75665A3-4382-4646-ADC8-C81AE146E455" + +#define EWMI_INPUT_SIZE 64 +#define EWMI_OUTPUT_SIZE 256 + +enum { + EWMI_FAN_COUNT = 2, + EWMI_TEMP_COUNT = 4, + EWMI_MAILBOX_RETRIES = 3, +}; + +struct ewmi_board_config { + u8 mailbox_reg; + u8 autopower_reg; + unsigned int query_settle_ms; + unsigned int setter_settle_ms; + u8 temp_regs[EWMI_TEMP_COUNT]; +}; + +struct ewmi_reply { + u8 reserved[4]; + u8 status; + u8 data[EWMI_OUTPUT_SIZE - 1]; +}; + +static_assert(sizeof(struct ewmi_reply) == 260); + +struct ewmi_method { + struct wmi_device *wdev; + const struct ewmi_board_config *board; + struct mutex lock; /* Serializes firmware calls and EC sensor reads. */ + struct device *profile; + struct notifier_block event_nb; + struct work_struct profile_work; + atomic_t mailbox_event_seq; /* EC5D hotkeys invalidate in-flight queries. */ + bool profile_dirty; /* A partial set must not become a successful no-op. */ + u8 lock_dirty; /* Uncertain lock settings must be explicitly restored. */ +}; + +struct ewmi_event { + struct input_dev *input; + struct mutex lock; /* Serializes input press/release sequences. */ +}; + +struct ewmi_autopower { + struct wmi_device *wdev; + const struct ewmi_board_config *board; + struct mutex lock; /* Serializes auto-power-on queries and settings. */ + bool dirty; /* A failed setting may have partially applied. */ +}; + +static BLOCKING_NOTIFIER_HEAD(ewmi_event_chain); + +/* Offsets apply only to the verified MECHREVO Lark4 Lunar Lake BIOS. */ +static const struct ewmi_board_config ewmi_lark4_lnl_config = { + .mailbox_reg = 0x5d, + .autopower_reg = 0xf5, + .query_settle_ms = 100, + .setter_settle_ms = 100, + .temp_regs = { 0x70, 0x73, 0x72, 0x62 }, +}; + +static const struct dmi_system_id ewmi_dmi_table[] = { + { + .ident = "MECHREVO Wujie 14 2026 Lark4 Lunar Lake", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "MECHREVO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "WUJIE Series-Lark4-LNL"), + DMI_EXACT_MATCH(DMI_BIOS_VERSION, "EM_LNL326_V1.0.19"), + }, + .driver_data = (void *)&ewmi_lark4_lnl_config, + }, + { } +}; + +static const struct ewmi_board_config *ewmi_get_board_config(void) +{ + const struct dmi_system_id *id; + + id = dmi_first_match(ewmi_dmi_table); + return id ? id->driver_data : NULL; +} + +/* + * GWLS (0x03/0x01) and GFLS (0x03/0x03) read the shared EC mailbox register + * (EC5D) after settling without sending a second overlapping command. + */ +static int ewmi_mailbox_reply(struct ewmi_method *priv, u8 command, + u8 *value) +{ + u8 raw; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ec_read(priv->board->mailbox_reg, &raw); + if (ret) + return ret; + + switch (command) { + case 0x01: + if (raw != 0 && raw != 2) + goto invalid; + *value = raw == 2; + return 0; + case 0x03: + if (raw > 1) + goto invalid; + *value = raw; + return 0; + default: + return -EOPNOTSUPP; + } + +invalid: + dev_dbg(&priv->wdev->dev, + "Settled mailbox 03/%02x transient/invalid raw 0x%02x\n", + command, raw); + return -EIO; +} + +/* + * WMCD returns Package(Buffer(4), Buffer(256)). The WMI core marshals + * both buffers into 260 bytes; the firmware status is at byte 4. + * Always check the full result size before accessing the payload. + */ +static int ewmi_call_status(struct ewmi_method *priv, u8 group, u8 command, + u8 argument, void *result, size_t result_len, + bool *firmware_failure) +{ + bool mailbox_query = group == 0x03 && (command == 0x01 || command == 0x03); + u8 request[EWMI_INPUT_SIZE] = { group, command, argument }; + struct wmi_buffer in = { .length = sizeof(request), .data = request }; + struct wmi_buffer out = { }; + const struct ewmi_reply *reply; + int attempt, ret = -EIO; + + lockdep_assert_held(&priv->lock); + if (firmware_failure) + *firmware_failure = false; + if (result_len > sizeof(reply->data)) + return -EINVAL; + if (mailbox_query && result_len != sizeof(u8)) + return -EINVAL; + + for (attempt = 0; attempt < EWMI_MAILBOX_RETRIES; attempt++) { + int seq = atomic_read(&priv->mailbox_event_seq); + + ret = wmidev_invoke_method(priv->wdev, 0, 1, &in, &out, + sizeof(*reply)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x transport error %d\n", + group, command, ret); + return ret; + } + if (!mailbox_query) + break; + + /* + * GWLS/GFLS share EC5D with each other and with _QF6/_QF9 + * hotkey notifications. Wait for the EC to settle and retry + * if an in-flight hotkey overwrites the mailbox. + */ + kfree(out.data); + out = (struct wmi_buffer) { }; + msleep(priv->board->query_settle_ms); + ret = ewmi_mailbox_reply(priv, command, result); + if (!ret && atomic_read(&priv->mailbox_event_seq) != seq) + ret = -EAGAIN; + if (!ret) + return 0; + if ((ret == -EIO || ret == -EAGAIN) && + attempt + 1 < EWMI_MAILBOX_RETRIES) + continue; + if (ret == -EAGAIN) + ret = -EIO; + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x settled mailbox error %d\n", + group, command, ret); + return ret; + } + + reply = out.data; + if (reply->status) { + if (firmware_failure && reply->status == 1) + *firmware_failure = true; + dev_warn_ratelimited(&priv->wdev->dev, + "WMCD %02x/%02x firmware status %u, data %8ph\n", + group, command, reply->status, reply->data); + ret = -EIO; + } else if (result_len) { + memcpy(result, reply->data, result_len); + } + kfree(out.data); + return ret; +} + +static int ewmi_call(struct ewmi_method *priv, u8 group, u8 command, + u8 argument, void *result, size_t result_len) +{ + return ewmi_call_status(priv, group, command, argument, result, + result_len, NULL); +} + +static ssize_t ewmi_switch_show(struct device *dev, u8 command, char *buf) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 value; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, command, 0, &value, sizeof(value)); + if (ret) + return ret; + if (value > 1) + return -EPROTO; + return sysfs_emit(buf, "%u\n", value); +} + +static ssize_t ewmi_switch_store(struct device *dev, u8 command, + const char *buf, size_t count) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + bool value; + u8 actual, mask; + int ret; + + if (command != 0x02 && command != 0x04) + return -EINVAL; + mask = command == 0x02 ? BIT(0) : BIT(1); + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, command - 1, 0, &actual, sizeof(actual)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x before-set query error %d\n", + command, ret); + return ret; + } + if (actual == value && !(priv->lock_dirty & mask)) + return count; + priv->lock_dirty |= mask; + ret = ewmi_call(priv, 0x03, command, value, NULL, 0); + if (ret) + return ret; + /* SFLS/SWLS do not wait: a query can overwrite the pending set command. */ + msleep(priv->board->setter_settle_ms); + /* The paired query waits for the EC reply before returning its state. */ + ret = ewmi_call(priv, 0x03, command - 1, 0, &actual, sizeof(actual)); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x after-set query error %d\n", + command, ret); + return ret; + } + if (actual != value) { + dev_warn_ratelimited(&priv->wdev->dev, + "Lock 03/%02x requested %u, settled readback %u\n", + command, value, actual); + return -EIO; + } + priv->lock_dirty &= ~mask; + return count; +} + +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return ewmi_switch_show(dev, 0x03, buf); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return ewmi_switch_store(dev, 0x04, buf, count); +} +static DEVICE_ATTR_RW(fn_lock); + +static ssize_t win_lock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return ewmi_switch_show(dev, 0x01, buf); +} + +static ssize_t win_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return ewmi_switch_store(dev, 0x02, buf, count); +} +static DEVICE_ATTR_RW(win_lock); + +static ssize_t touchpad_enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return ewmi_switch_show(dev, 0x07, buf); +} +static DEVICE_ATTR_RO(touchpad_enabled); + +static struct attribute *ewmi_attrs[] = { + &dev_attr_fn_lock.attr, + &dev_attr_win_lock.attr, + &dev_attr_touchpad_enabled.attr, + NULL +}; +ATTRIBUTE_GROUPS(ewmi); + +static const char * const ewmi_temp_labels[EWMI_TEMP_COUNT] = { + "CPU", "GT VR", "Ambient", "Charger" +}; + +static const char * const ewmi_fan_labels[EWMI_FAN_COUNT] = { + "Fan 1", "Fan 2" +}; + +static int ewmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *value) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 temperature; + __le16 rpm; + int ret; + + guard(mutex)(&priv->lock); + if (type == hwmon_fan && attr == hwmon_fan_input) { + if (channel < 0 || channel >= EWMI_FAN_COUNT) + return -EOPNOTSUPP; + ret = ewmi_call(priv, 0x02, 0x01, channel, &rpm, sizeof(rpm)); + if (!ret) + *value = le16_to_cpu(rpm); + return ret; + } + if (type == hwmon_temp && attr == hwmon_temp_input) { + if (channel < 0 || channel >= EWMI_TEMP_COUNT) + return -EOPNOTSUPP; + ret = ec_read(priv->board->temp_regs[channel], &temperature); + if (!ret) { + if (temperature == 0xff) + return -ENODATA; + *value = temperature * 1000L; + } + return ret; + } + return -EOPNOTSUPP; +} + +static int ewmi_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + if (type == hwmon_fan && attr == hwmon_fan_label && + channel >= 0 && channel < EWMI_FAN_COUNT) { + *str = ewmi_fan_labels[channel]; + return 0; + } + if (type == hwmon_temp && attr == hwmon_temp_label && + channel >= 0 && channel < EWMI_TEMP_COUNT) { + *str = ewmi_temp_labels[channel]; + return 0; + } + return -EOPNOTSUPP; +} + +static const struct hwmon_ops ewmi_hwmon_ops = { + .visible = 0444, + .read = ewmi_hwmon_read, + .read_string = ewmi_hwmon_read_string, +}; + +static const struct hwmon_channel_info * const ewmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + NULL +}; + +static const struct hwmon_chip_info ewmi_hwmon_chip = { + .ops = &ewmi_hwmon_ops, + .info = ewmi_hwmon_info, +}; + +static int ewmi_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + return 0; +} + +static int ewmi_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + u8 mode; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_call(priv, 0x03, 0x09, 0, &mode, sizeof(mode)); + if (ret) + return ret; + switch (mode) { + case 1: + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + case 2: + *profile = PLATFORM_PROFILE_PERFORMANCE; + return 0; + default: + return -EPROTO; + } +} + +static int ewmi_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ewmi_method *priv = dev_get_drvdata(dev); + bool firmware_failure; + u8 mode, actual; + int attempt, ret; + + switch (profile) { + case PLATFORM_PROFILE_BALANCED: + mode = 1; + break; + case PLATFORM_PROFILE_PERFORMANCE: + mode = 2; + break; + default: + return -EOPNOTSUPP; + } + + guard(mutex)(&priv->lock); + /* Avoid a redundant EC command when userspace reapplies this profile. */ + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode && !priv->profile_dirty) + return 0; + + /* + * SPFM arms the EC (ZZTT/TIZZ) before Notify(IETM, 0x88) and before + * writing EC5F, so an EC scan during the notify can consume a stale + * EC5F and return RTNC=0 without updating PPMD; SPFM can also report + * RTNC=1 after changing mode. Reissue this absolute mode command up + * to three times when RTNC=1 or 03/09 still reads back the old mode. + */ + priv->profile_dirty = true; + for (attempt = 0; attempt < EWMI_MAILBOX_RETRIES; attempt++) { + ret = ewmi_call_status(priv, 0x03, 0x0a, mode, NULL, 0, + &firmware_failure); + if (ret) { + if (!firmware_failure || + attempt + 1 == EWMI_MAILBOX_RETRIES) + return ret; + msleep(priv->board->setter_settle_ms); + continue; + } + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode) { + priv->profile_dirty = false; + return 0; + } + msleep(priv->board->setter_settle_ms); + ret = ewmi_call(priv, 0x03, 0x09, 0, &actual, sizeof(actual)); + if (ret) + return ret; + if (actual == mode) { + priv->profile_dirty = false; + return 0; + } + if (attempt + 1 < EWMI_MAILBOX_RETRIES) + dev_dbg(dev, + "Profile 03/0a requested %u, readback %u on attempt %d/%d; retrying\n", + mode, actual, attempt + 1, + EWMI_MAILBOX_RETRIES); + } + dev_warn_ratelimited(dev, + "Profile 03/0a requested %u, settled readback %u\n", + mode, actual); + return -EIO; +} + +static const struct platform_profile_ops ewmi_profile_ops = { + .probe = ewmi_profile_probe, + .profile_get = ewmi_profile_get, + .profile_set = ewmi_profile_set, +}; + +static void ewmi_profile_work(struct work_struct *work) +{ + struct ewmi_method *priv = container_of(work, struct ewmi_method, + profile_work); + + platform_profile_notify(priv->profile); +} + +static int ewmi_method_event(struct notifier_block *nb, unsigned long code, + void *data) +{ + struct ewmi_method *priv = container_of(nb, struct ewmi_method, + event_nb); + struct wmi_device *event_wdev = data; + + if (event_wdev->dev.parent != priv->wdev->dev.parent) + return NOTIFY_DONE; + /* _QF6 (0x50/0x51) and _QF9 (0x20-0x22) share EC5D with WMCD queries. */ + if (code == 0x20 || code == 0x21 || code == 0x22 || + code == 0x50 || code == 0x51) + atomic_inc(&priv->mailbox_event_seq); + if (code == 0 || code == 0x41 || code == 0x42) + schedule_work(&priv->profile_work); + return NOTIFY_OK; +} + +static void ewmi_method_stop(void *data) +{ + struct ewmi_method *priv = data; + + blocking_notifier_chain_unregister(&ewmi_event_chain, &priv->event_nb); + cancel_work_sync(&priv->profile_work); +} + +static int ewmi_method_probe(struct wmi_device *wdev, const void *context) +{ + static const u8 expected_version[] = { 1, 0, 1, 0 }; + const struct ewmi_board_config *board; + struct ewmi_method *priv; + struct device *hwmon; + u8 version[4]; + int ret; + + board = ewmi_get_board_config(); + if (!board) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + priv->wdev = wdev; + priv->board = board; + mutex_init(&priv->lock); + INIT_WORK(&priv->profile_work, ewmi_profile_work); + dev_set_drvdata(&wdev->dev, priv); + + scoped_guard(mutex, &priv->lock) + ret = ewmi_call(priv, 0x01, 0x01, 0, version, sizeof(version)); + if (ret) + return dev_err_probe(&wdev->dev, ret, + "Cannot query protocol version\n"); + if (memcmp(version, expected_version, sizeof(version))) + return dev_err_probe(&wdev->dev, -ENODEV, + "Unsupported protocol version\n"); + + priv->profile = devm_platform_profile_register(&wdev->dev, + "emdoor-intel-wmi", + priv, &ewmi_profile_ops); + if (IS_ERR(priv->profile)) + return dev_err_probe(&wdev->dev, PTR_ERR(priv->profile), + "Cannot register platform profile\n"); + + priv->event_nb.notifier_call = ewmi_method_event; + ret = blocking_notifier_chain_register(&ewmi_event_chain, + &priv->event_nb); + if (ret) + return ret; + ret = devm_add_action_or_reset(&wdev->dev, ewmi_method_stop, priv); + if (ret) + return ret; + + hwmon = devm_hwmon_device_register_with_info(&wdev->dev, "emdoor_ewmi", + priv, &ewmi_hwmon_chip, + NULL); + if (IS_ERR(hwmon)) + return dev_err_probe(&wdev->dev, PTR_ERR(hwmon), + "Cannot register hwmon\n"); + + return 0; +} + +/* Firmware already changes these states; never request a second toggle. */ +static const struct key_entry ewmi_keymap[] = { + { KE_KEY, 0x30, { KEY_TOUCHPAD_OFF } }, + { KE_KEY, 0x31, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 0xa0, { KEY_PROG1 } }, + { KE_KEY, 0xa1, { KEY_PROG2 } }, + { KE_KEY, 0xa2, { KEY_PROG3 } }, + { KE_IGNORE, 0x00, { 0 } }, + { KE_IGNORE, 0x41, { 0 } }, + { KE_IGNORE, 0x42, { 0 } }, + { KE_IGNORE, 0x20, { 0 } }, + { KE_IGNORE, 0x21, { 0 } }, + { KE_IGNORE, 0x22, { 0 } }, + { KE_IGNORE, 0x50, { 0 } }, + { KE_IGNORE, 0x51, { 0 } }, + { KE_END, 0 } +}; + +static int ewmi_event_probe(struct wmi_device *wdev, const void *context) +{ + struct ewmi_event *priv; + int ret; + + if (!ewmi_get_board_config()) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + mutex_init(&priv->lock); + priv->input = devm_input_allocate_device(&wdev->dev); + if (!priv->input) + return -ENOMEM; + priv->input->name = "Emdoor EWMI hotkeys"; + priv->input->phys = "wmi/input0"; + priv->input->id.bustype = BUS_HOST; + ret = sparse_keymap_setup(priv->input, ewmi_keymap, NULL); + if (ret) + return ret; + dev_set_drvdata(&wdev->dev, priv); + return input_register_device(priv->input); +} + +static void ewmi_event_notify(struct wmi_device *wdev, + const struct wmi_buffer *event) +{ + struct ewmi_event *priv = dev_get_drvdata(&wdev->dev); + u32 code = get_unaligned_le32(event->data); + + blocking_notifier_call_chain(&ewmi_event_chain, code, wdev); + guard(mutex)(&priv->lock); + if (!sparse_keymap_report_event(priv->input, code, 1, true)) + dev_dbg(&wdev->dev, "Unknown event 0x%x\n", code); +} + +static const struct wmi_device_id ewmi_method_ids[] = { + { EWMI_METHOD_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_method_ids); + +static const struct wmi_device_id ewmi_event_ids[] = { + { EWMI_EVENT_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_event_ids); + +static struct wmi_driver ewmi_method_driver = { + .driver = { + .name = "emdoor-ewmi", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .dev_groups = ewmi_groups, + }, + .id_table = ewmi_method_ids, + .no_singleton = true, + .probe = ewmi_method_probe, +}; + +static struct wmi_driver ewmi_event_driver = { + .driver = { + .name = "emdoor-ewmi-events", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = ewmi_event_ids, + .no_singleton = true, + .min_event_size = sizeof(__le32), + .probe = ewmi_event_probe, + .notify_new = ewmi_event_notify, +}; + +static int ewmi_autopower_get(struct ewmi_autopower *priv, bool *enabled) +{ + u8 raw; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ec_read(priv->board->autopower_reg, &raw); + if (ret) + return ret; + if (raw != 0 && raw != 0xa0) + return -EPROTO; + *enabled = raw == 0xa0; + return 0; +} + +static int ewmi_autopower_set(struct ewmi_autopower *priv, bool enabled) +{ + __le64 argument = cpu_to_le64(enabled); + struct wmi_buffer in = { .length = sizeof(argument), .data = &argument }; + struct wmi_buffer out = { }; + bool actual; + u8 status; + int ret; + + lockdep_assert_held(&priv->lock); + ret = ewmi_autopower_get(priv, &actual); + if (ret || (actual == enabled && !priv->dirty)) + return ret; + priv->dirty = true; + ret = wmidev_invoke_method(priv->wdev, 0, 1, &in, &out, 8); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC transport error %d\n", ret); + return ret; + } + status = ((u8 *)out.data)[0]; + ret = status ? -EIO : 0; + kfree(out.data); + msleep(priv->board->setter_settle_ms); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC firmware status %u\n", status); + return ret; + } + ret = ewmi_autopower_get(priv, &actual); + if (ret) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC readback error %d\n", ret); + return ret; + } + if (actual != enabled) { + dev_warn_ratelimited(&priv->wdev->dev, + "WMCC requested %u, readback %u\n", + enabled, actual); + return -EIO; + } + priv->dirty = false; + return 0; +} + +static ssize_t auto_power_on_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ewmi_autopower *priv = dev_get_drvdata(dev); + bool enabled; + int ret; + + guard(mutex)(&priv->lock); + ret = ewmi_autopower_get(priv, &enabled); + if (ret) + return ret; + return sysfs_emit(buf, "%u\n", enabled); +} + +static ssize_t auto_power_on_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ewmi_autopower *priv = dev_get_drvdata(dev); + bool enabled; + int ret; + + ret = kstrtobool(buf, &enabled); + if (ret) + return ret; + guard(mutex)(&priv->lock); + ret = ewmi_autopower_set(priv, enabled); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(auto_power_on); + +static struct attribute *ewmi_autopower_attrs[] = { + &dev_attr_auto_power_on.attr, + NULL +}; +ATTRIBUTE_GROUPS(ewmi_autopower); + +static int ewmi_autopower_probe(struct wmi_device *wdev, const void *context) +{ + const struct ewmi_board_config *board; + struct ewmi_autopower *priv; + + board = ewmi_get_board_config(); + if (!board) + return -ENODEV; + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + priv->wdev = wdev; + priv->board = board; + mutex_init(&priv->lock); + dev_set_drvdata(&wdev->dev, priv); + return 0; +} + +static const struct wmi_device_id ewmi_autopower_ids[] = { + { EWMI_AUTOPOWER_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, ewmi_autopower_ids); + +static struct wmi_driver ewmi_autopower_driver = { + .driver = { + .name = "emdoor-ewmi-autopower", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .dev_groups = ewmi_autopower_groups, + }, + .id_table = ewmi_autopower_ids, + .no_singleton = true, + .probe = ewmi_autopower_probe, +}; + +static int __init ewmi_init(void) +{ + int ret; + + ret = wmi_driver_register(&ewmi_method_driver); + if (ret) + return ret; + ret = wmi_driver_register(&ewmi_event_driver); + if (ret) { + wmi_driver_unregister(&ewmi_method_driver); + return ret; + } + ret = wmi_driver_register(&ewmi_autopower_driver); + if (ret) { + wmi_driver_unregister(&ewmi_event_driver); + wmi_driver_unregister(&ewmi_method_driver); + } + return ret; +} + +static void __exit ewmi_exit(void) +{ + wmi_driver_unregister(&ewmi_autopower_driver); + wmi_driver_unregister(&ewmi_event_driver); + wmi_driver_unregister(&ewmi_method_driver); +} + +module_init(ewmi_init); +module_exit(ewmi_exit); + +MODULE_AUTHOR("Ziqiang Xia <xiazq123666@gmail.com>"); +MODULE_DESCRIPTION("Emdoor Intel EWMI platform driver"); +MODULE_LICENSE("GPL"); -- 2.56.1 ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH RFC 2/2] platform/x86: Add Emdoor Intel EWMI platform driver 2026-11-13 18:01 ` Ziqiang Xia (?) @ 2026-11-13 18:10 ` sashiko-bot -1 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-11-13 18:10 UTC (permalink / raw) To: Ziqiang Xia; +Cc: linux-hwmon > From: Ziqiang Xia <xiazq123666@gmail.com> > > Add support for the Emdoor Intel EWMI (\_SB.EWMI) WMI interface on > MECHREVO Wujie 14 2026 (WUJIE Series-Lark4-LNL, BIOS EM_LNL326_V1.0.19), > exposing hwmon fan and EC temperature sensors, platform_profile > (balanced/performance), Fn/Win lock and touchpad state attributes, > WMI hotkey sparse keymap events, and AC auto-power-on control. > > Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> Sashiko has reviewed this patch and found no issues. -- Sashiko AI review · https://sashiko.dev/#/patchset/20261113-emdoor-intel-wmi-rfc-v1-0-d77ecf23693b@gmail.com?part=2 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH RFC 0/2] platform/x86: Add Emdoor Intel EWMI platform driver 2026-11-13 18:01 ` Ziqiang Xia ` (2 preceding siblings ...) (?) @ 2026-11-13 20:37 ` Armin Wolf -1 siblings, 0 replies; 9+ messages in thread From: Armin Wolf @ 2026-11-13 20:37 UTC (permalink / raw) To: xiazq123666, Hans de Goede, Ilpo Järvinen, Guenter Roeck Cc: platform-driver-x86, linux-kernel, linux-hwmon Am 13.11.26 um 19:01 schrieb Ziqiang Xia via B4 Relay: > This RFC patch series adds initial Linux support for the Intel EWMI > (\_SB.EWMI) WMI interface found on Emdoor-based Intel laptops, initially > matched strictly by DMI and BIOS version to the MECHREVO Wujie 14 2026 > (WUJIE Series-Lark4-LNL, Intel Core Ultra 5 226V Lunar Lake, BIOS > EM_LNL326_V1.0.19). > > Note on naming and scope: > - Community out-of-tree projects such as fewtarius/emdoor-wmi target AMD > Emdoor platforms using different WMI GUIDs (8600ACCE-...) and AMD SMU > ports. This Intel EWMI implementation (0B30FD67-1B62-4FCA-B10B-50DC7ADE6A39, > 0B30FD67-1B62-4FCA-B10B-50DC7ADE6DDD, C75665A3-4382-4646-ADC8-C81AE146E455) > is unrelated in protocol and EC register layout, so the driver is named > emdoor-intel-wmi. > - Legacy WMAA-WMCA WMI methods on this DSDT trigger AE_AML_BUFFER_LIMIT due > to an out-of-bounds CreateField() and are deliberately unused. The driver > only uses the clean WMCD, _WED(0xD0), and WMCC interfaces through modern > wmi_driver / wmidev_invoke_method(). Interesting. IMHO using direct EC addresses will eventually turn into a maintainability nightmare. I am aware of the issue with CreateField(), AFAIK you can workaround this issue by simply submitting an oversized buffer to wmidev_invoke_method(). Can you share the output of acpidump please? Thanks, Armin Wolf > > Hardware quirks handled and verified on real hardware: > 1. Shared EC5D mailbox lag and hotkey collision: > AML methods GWLS (0x03/01) and GFLS (0x03/03) time out after 20 ms on > this BIOS and return the previous mailbox byte on the call itself. After > invoking WMCD and waiting 100 ms, the driver reads the settled mailbox > register directly and retries up to 3 times if concurrent _QF6/_QF9 > hotkey events overwrite EC5D in flight. > 2. SPFM stale-EC5F drop race: > AML method SPFM (0x03/0a) arms ZZTT/TIZZ before Notify(IETM, 0x88) and > before writing EC5F, so an EC scan during the notify can consume a stale > EC5F and return RTNC=0 without updating PPMD (or return RTNC=1). The > driver verifies 03/09 readback and reissues the absolute mode up to 3 times. > > Validation summary on Linux 7.3.4 (MECHREVO WUJIE Series-Lark4-LNL, > EM_LNL326_V1.0.19): > - 1500 readonly attribute/hwmon stress reads (battery & AC online): 0 errors > - 20 platform_profile switches + 40 idempotent writes: 0 errors > - Fn-lock, Win-lock, and auto_power_on write/readback cycles: pass > - High-frequency Fn+Esc / F8 hotkey concurrency stress & s2idle: pass > > Specific RFC questions for reviewers: > 1. Should fn_lock, win_lock, and auto_power_on stay as WMI device sysfs > attributes (documented in Patch 1), or would pdx86 maintainers prefer a > firmware-attributes class interface or moving any of them? > 2. Event codes 0xA1 (F7 mic-mute hotkey, _QF8) and 0xA2 (Fn+R refresh rate, > _QF5) are conservatively mapped to KEY_PROG2 / KEY_PROG3 for now while > verifying whether i8042 also emits scancodes on some desktops/DEs. Is > KEY_MICMUTE / KEY_REFRESH_RATE_TOGGLE preferred once verified, or > separate input mapping? > 3. Exact DMI_BIOS_VERSION matching is used because board EC offsets (0x5d, > 0xf5, temp_regs {0x70, 0x73, 0x72, 0x62}) are board/BIOS-specific. Is > this strict match appropriate for initial upstreaming? > > Signed-off-by: Ziqiang Xia <xiazq123666@gmail.com> > --- > Ziqiang Xia (2): > Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI > platform/x86: Add Emdoor Intel EWMI platform driver > > .../ABI/testing/sysfs-bus-wmi-devices-emdoor-ewmi | 28 + > MAINTAINERS | 7 + > drivers/platform/x86/Kconfig | 18 + > drivers/platform/x86/Makefile | 1 + > drivers/platform/x86/emdoor-intel-wmi.c | 873 +++++++++++++++++++++ > 5 files changed, 927 insertions(+) > --- > base-commit: 5680519168f65666d2c03cc6ed8fe2b7052aee12 > change-id: 20261113-emdoor-intel-wmi-rfc-2173530b3df6 > > Best regards, > -- > Ziqiang Xia <xiazq123666@gmail.com> > > ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-11-13 20:38 UTC | newest] Thread overview: 9+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-11-13 18:01 [PATCH RFC 0/2] platform/x86: Add Emdoor Intel EWMI platform driver Ziqiang Xia via B4 Relay 2026-11-13 18:01 ` Ziqiang Xia 2026-11-13 18:01 ` [PATCH RFC 1/2] Documentation: ABI: testing: Add emdoor-intel-wmi sysfs ABI Ziqiang Xia via B4 Relay 2026-11-13 18:01 ` Ziqiang Xia 2026-11-13 18:15 ` sashiko-bot 2026-11-13 18:01 ` [PATCH RFC 2/2] platform/x86: Add Emdoor Intel EWMI platform driver Ziqiang Xia via B4 Relay 2026-11-13 18:01 ` Ziqiang Xia 2026-11-13 18:10 ` sashiko-bot 2026-11-13 20:37 ` [PATCH RFC 0/2] " Armin Wolf
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.