mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] platform/arm64: Lenovo Yoga Slim 7x Gen 11 embedded controller
@ 2026-09-08  9:17 Oleg Keri
  2026-09-08  9:17 ` [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC Oleg Keri
  2026-09-08  9:17 ` [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver Oleg Keri
  0 siblings, 2 replies; 6+ messages in thread
From: Oleg Keri @ 2026-09-08  9:17 UTC (permalink / raw)
  To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Hans de Goede,
	Ilpo Järvinen, Bryan O'Donoghue, Guenter Roeck
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	linux-hwmon

The Lenovo Yoga Slim 7x Gen 11 is a Qualcomm Snapdragon X2 Elite (glymur)
laptop. It carries a Compal EF06 embedded controller on I2C at address
0x70, which is the only source on this machine for the board thermistors,
the battery cell voltages, the fan tachometer, the keyboard backlight, and
the mic-mute and airplane-mode keys.

Patch 1 documents the binding, patch 2 adds the driver.

Why this is not qcom-hamoa-ec
-----------------------------

The Snapdragon reference designs - glymur CRD, hamoa IoT EVK, Asus Zenbook
A16 - place their embedded controller at 0x76, and
drivers/platform/arm64/qcom-hamoa-ec.c drives those. This machine is
different: only 0x70 ACKs, and 0x76 is not populated at all. That is also
why the firmware reports every host fan capability as unsupported, since
profiles, trip points, LUTs and tachometer queries are all addressed to
0x76.

Binding qcom-hamoa-ec here does not work. It speaks a different command set
(0x05/0x0e/0x42), appears to probe only because it never validates the
firmware version it reads back, then reports zero fans and leaves the
interrupt asserted at roughly 25 events/s indefinitely.

The ACPI tables describe both controllers: one at 0x76 (OperationRegion
DVUM, fields FC20-FC2A) inherited from the reference design, and the one
this driver targets at 0x70 (OperationRegion ERAM, battery and charger
fields). Only the latter responds on this board.

Notes
-----

Fan speed is reported but not controlled, deliberately - the EC enforces no
thermal floor over a manual setpoint. Patch 2 has the measurement.

The DT node for the controller is not part of this series; it lands with
the board DTS, which is being upstreamed separately.

Tested on a Lenovo Yoga Slim 7x Gen 11 (DMI 83QR, "Yoga Slim 7 14Q8Y11").

Oleg Keri (2):
  dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC
  platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver

 .../sysfs-driver-lenovo-yoga-slim7x-gen11-ec  |  21 +
 .../lenovo,yoga-slim7x-gen11-ec.yaml          |  56 ++
 MAINTAINERS                                   |   8 +
 drivers/platform/arm64/Kconfig                |  22 +
 drivers/platform/arm64/Makefile               |   1 +
 .../arm64/lenovo-yoga-slim7x-gen11-ec.c       | 639 ++++++++++++++++++
 6 files changed, 747 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec
 create mode 100644 Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml
 create mode 100644 drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c

-- 
2.55.0

base-commit: 5e036ce12de91c6fd674dad33b169c6150be2a7a

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC
  2026-09-08  9:17 [PATCH 0/2] platform/arm64: Lenovo Yoga Slim 7x Gen 11 embedded controller Oleg Keri
@ 2026-09-08  9:17 ` Oleg Keri
  2026-09-08 17:55   ` Conor Dooley
  2026-09-08  9:17 ` [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver Oleg Keri
  1 sibling, 1 reply; 6+ messages in thread
From: Oleg Keri @ 2026-09-08  9:17 UTC (permalink / raw)
  To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Hans de Goede,
	Ilpo Järvinen, Bryan O'Donoghue, Guenter Roeck
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	linux-hwmon

Document the Compal embedded controller found on the Lenovo Yoga Slim 7x
Gen 11, a Qualcomm Snapdragon X2 Elite (glymur) laptop. It sits on I2C at
address 0x70 and is distinct from the Qualcomm reference EC that the
reference designs place at 0x76.

Signed-off-by: Oleg Keri <okerixx@gmail.com>
---
 .../lenovo,yoga-slim7x-gen11-ec.yaml          | 56 +++++++++++++++++++
 MAINTAINERS                                   |  8 +++
 2 files changed, 64 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml

diff --git a/Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml b/Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml
new file mode 100644
index 000000000000..37337de86b0b
--- /dev/null
+++ b/Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Lenovo Yoga Slim 7x Gen 11 Embedded Controller
+
+maintainers:
+  - Oleg Keri <okerixx@gmail.com>
+
+description:
+  The Lenovo Yoga Slim 7x Gen 11, a Qualcomm Snapdragon X2 Elite (glymur)
+  laptop, has a Compal embedded controller on I2C. It reports board
+  thermistors, battery cell voltages and CPU fan speed, and drives the
+  keyboard backlight.
+
+  This controller is not the Qualcomm reference EC that the Snapdragon
+  reference designs place at address 0x76; it speaks a different command
+  set and answers at 0x70.
+
+properties:
+  compatible:
+    const: lenovo,yoga-slim7x-gen11-ec
+
+  reg:
+    const: 0x70
+
+  interrupts:
+    maxItems: 1
+    description:
+      The controller pulses this line while it has events queued.
+
+required:
+  - compatible
+  - reg
+  - interrupts
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        embedded-controller@70 {
+            compatible = "lenovo,yoga-slim7x-gen11-ec";
+            reg = <0x70>;
+
+            interrupts-extended = <&tlmm 66 IRQ_TYPE_EDGE_FALLING>;
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 38612eca2987..e35679264e22 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14882,6 +14882,14 @@ S:	Maintained
 F:	Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
 F:	drivers/platform/x86/lenovo/yb9-kbdock.c
 
+LENOVO YOGA SLIM 7X GEN 11 EMBEDDED CONTROLLER DRIVER
+M:	Oleg Keri <okerixx@gmail.com>
+L:	linux-arm-msm@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec
+F:	Documentation/devicetree/bindings/embedded-controller/lenovo,yoga-slim7x-gen11-ec.yaml
+F:	drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c
+
 LETSKETCH HID TABLET DRIVER
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver
  2026-09-08  9:17 [PATCH 0/2] platform/arm64: Lenovo Yoga Slim 7x Gen 11 embedded controller Oleg Keri
  2026-09-08  9:17 ` [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC Oleg Keri
@ 2026-09-08  9:17 ` Oleg Keri
  2026-09-08 15:53   ` Uwe Kleine-König
  1 sibling, 1 reply; 6+ messages in thread
From: Oleg Keri @ 2026-09-08  9:17 UTC (permalink / raw)
  To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Hans de Goede,
	Ilpo Järvinen, Bryan O'Donoghue, Guenter Roeck
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	linux-hwmon

The Lenovo Yoga Slim 7x Gen 11 (Qualcomm glymur / Snapdragon X2 Elite)
carries a Compal EF06 embedded controller on I2C at address 0x70. The
platform firmware describes it as an ACPI GenericSerialBus region, but the
machine boots from device tree, so nothing otherwise talks to it.

Commands are exchanged with two raw I2C block transfers: a 7 byte write to
register 0x02 carrying the command and up to six arguments, then a 6 byte
read of register 0x01 for the result, with a short delay in between before
the result register is valid.

Expose what the EC offers that the rest of the kernel does not already
provide:

  - five board thermistors as hwmon temperature channels. These are
    charger, CPU voltage regulator, 5 V rail and ambient sensors, not die
    sensors, and read considerably cooler than tsens
  - the four battery cell voltages, which live in a banked mailbox shared
    with other EC activity; readings are validated against a signature and
    retried, and cached briefly so that all four channels come from one
    coherent sample
  - CPU fan speed in RPM
  - the keyboard backlight as an LED, off/low/high
  - Fn-lock as a read/write sysfs attribute, matching the ideapad-laptop
    interface. The EC reports Fn+Esc as an event but keeps no state of its
    own, so the driver owns the bit and toggles it on the key
  - the mic-mute and airplane-mode keys as KEY_MICMUTE and KEY_RFKILL.
    These reach the host only as EC events; they are not on the keyboard's
    HID interface and have no GPIO of their own

The EC signals pending events on a dedicated line, and keeps pulsing it
until they are read - so the queue must be drained completely on every
interrupt, including codes this driver ignores. Leaving one behind makes
the controller pulse indefinitely.

Fan speed is reported but not controlled. The EC accepts a manual setpoint,
however it enforces no thermal floor over it: with the fan pinned at
roughly 2000 RPM under full load the SoC rose from 69 to 91 degrees in 30
seconds without the EC ever overriding. Exposing pwm1_enable or fan1_target
would therefore let userspace overheat the machine, so neither is
implemented.

Note this is the Gen 11 machine specifically. Its EC differs from the
Qualcomm reference EC that the reference designs place at address 0x76,
which is not populated here, and so EC_QCOM_HAMOA does not drive it.

Signed-off-by: Oleg Keri <okerixx@gmail.com>
---
 .../sysfs-driver-lenovo-yoga-slim7x-gen11-ec  |  21 +
 drivers/platform/arm64/Kconfig                |  22 +
 drivers/platform/arm64/Makefile               |   1 +
 .../arm64/lenovo-yoga-slim7x-gen11-ec.c       | 639 ++++++++++++++++++
 4 files changed, 683 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec
 create mode 100644 drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c

diff --git a/Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec b/Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec
new file mode 100644
index 000000000000..901a8eca4c65
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-lenovo-yoga-slim7x-gen11-ec
@@ -0,0 +1,21 @@
+What:		/sys/bus/i2c/devices/*/fn_lock
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	"Oleg Keri <okerixx@gmail.com>"
+Description:
+		Control fn-lock mode.
+
+			* 1 -> Switched On
+			* 0 -> Switched Off
+
+		When switched on, the function keys act as F1..F12 and the
+		multimedia functions require the Fn modifier.
+
+		The embedded controller reports the Fn+Esc key as an event but
+		keeps no state of its own, so this attribute is the state: the
+		driver toggles it when the key is pressed, and writing it takes
+		effect immediately.
+
+		For example::
+
+		  # echo "0" > /sys/bus/i2c/devices/9-0070/fn_lock
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig
index e32e01b2a9bd..9d8e875413fa 100644
--- a/drivers/platform/arm64/Kconfig
+++ b/drivers/platform/arm64/Kconfig
@@ -90,6 +90,28 @@ config EC_LENOVO_THINKPAD_T14S
 
 	  Say M or Y here to include this support.
 
+config EC_LENOVO_YOGA_SLIM7X_GEN11
+	tristate "Lenovo Yoga Slim 7x Gen 11 Embedded Controller driver"
+	depends on ARCH_QCOM || COMPILE_TEST
+	depends on I2C
+	depends on HWMON
+	select NEW_LEDS
+	select LEDS_CLASS
+	help
+	  Say Y here to enable the EC driver for the (Snapdragon X2 Elite,
+	  glymur-based) Lenovo Yoga Slim 7x Gen 11 laptop. The EC provides
+	  thermistors, battery cell voltages and CPU fan speed as hwmon
+	  channels, and the keyboard backlight as an LED.
+
+	  This is the Gen 11 (glymur) machine specifically. Its EC is a Compal
+	  design at i2c address 0x70 and speaks a different protocol from the
+	  Qualcomm reference EC that the reference designs place at 0x76, so
+	  EC_QCOM_HAMOA does not drive it - and conversely this driver does not
+	  cover the X1E-based Yoga Slim 7x Gen 9.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called lenovo-yoga-slim7x-gen11-ec.
+
 config EC_QCOM_HAMOA
 	tristate "Embedded Controller driver for Qualcomm Hamoa/Glymur reference devices"
 	depends on ARCH_QCOM || COMPILE_TEST
diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile
index 7681be4a46e9..605cca4d42a9 100644
--- a/drivers/platform/arm64/Makefile
+++ b/drivers/platform/arm64/Makefile
@@ -9,4 +9,5 @@ obj-$(CONFIG_EC_ACER_ASPIRE1)	+= acer-aspire1-ec.o
 obj-$(CONFIG_EC_HUAWEI_GAOKUN)	+= huawei-gaokun-ec.o
 obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o
 obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o
+obj-$(CONFIG_EC_LENOVO_YOGA_SLIM7X_GEN11) += lenovo-yoga-slim7x-gen11-ec.o
 obj-$(CONFIG_EC_QCOM_HAMOA) += qcom-hamoa-ec.o
diff --git a/drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c b/drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c
new file mode 100644
index 000000000000..521a7fb8f356
--- /dev/null
+++ b/drivers/platform/arm64/lenovo-yoga-slim7x-gen11-ec.c
@@ -0,0 +1,639 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/interrupt.h>
+#include <linux/jiffies.h>
+#include <linux/leds.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/string.h>
+
+#define YOGA_EC_BLOCK_READ	0x01
+#define YOGA_EC_BLOCK_WRITE	0x02
+#define YOGA_EC_RESP_LEN	6
+#define YOGA_EC_REQ_LEN		7
+
+#define YOGA_EC_CMD_RAM_READ	0xb0
+#define YOGA_EC_CMD_RAM_WRITE	0xb1
+#define YOGA_EC_CMD_BANK_READ	0xb4
+#define YOGA_EC_CMD_FAN		0x46
+#define YOGA_EC_FAN_GET_SPEED	0x81
+
+#define YOGA_EC_CMD_EVENT	0x84
+#define YOGA_EC_EVT_MAX_DRAIN	64
+#define YOGA_EC_EVT_FN_LOCK	0x0f
+#define YOGA_EC_EVT_MIC_MUTE	0x18
+#define YOGA_EC_EVT_FLIGHT_MODE	0x19
+
+#define YOGA_EC_SCUK		0x1c
+#define YOGA_EC_SCUK_FN_LOCK	BIT(7)
+#define YOGA_EC_SCUK_FN_LOCK_ON	0xa0
+#define YOGA_EC_SCUK_FN_LOCK_OFF	0x5f
+
+#define YOGA_EC_KBLT		0x03
+#define YOGA_EC_KBLT_LEVEL	GENMASK(1, 0)
+#define YOGA_EC_KBLT_AUTO	3
+#define YOGA_EC_KBLT_MAX	2
+
+#define YOGA_EC_TEMP_FIRST	0x12
+#define YOGA_EC_NUM_TEMP	6
+#define YOGA_EC_TEMP_MAX	120
+
+#define YOGA_EC_CELL_BANK	0x0b
+#define YOGA_EC_CELL_SIG_LO	0x02
+#define YOGA_EC_CELL_SIG_HI	0x0c
+#define YOGA_EC_CELL_FIRST	0x04
+#define YOGA_EC_NUM_CELL	4
+#define YOGA_EC_CELL_MIN_MV	2000
+#define YOGA_EC_CELL_MAX_MV	5000
+#define YOGA_EC_CELL_TRIES	30
+
+#define YOGA_EC_CACHE		(2 * HZ)
+#define YOGA_EC_STALE		(10 * HZ)
+
+struct yoga_ec {
+	struct i2c_client *client;
+	struct mutex lock;
+	unsigned long temp_present;
+	unsigned long cell_read_at;
+	u16 cell[YOGA_EC_NUM_CELL];
+	bool cell_cached;
+	struct led_classdev kbd_led;
+	u8 kblt_level;
+	bool kblt_restore;
+	struct input_dev *keys;
+};
+
+static int yoga_ec_command(struct yoga_ec *ec, u8 cmd, u8 arg0, u8 arg1,
+			   u8 resp[YOGA_EC_RESP_LEN])
+{
+	u8 req[YOGA_EC_REQ_LEN] = { cmd, arg0, arg1 };
+	int ret;
+
+	ret = i2c_smbus_write_i2c_block_data(ec->client, YOGA_EC_BLOCK_WRITE,
+					     sizeof(req), req);
+	if (ret < 0)
+		return ret;
+
+	usleep_range(2000, 3000);
+
+	ret = i2c_smbus_read_i2c_block_data(ec->client, YOGA_EC_BLOCK_READ,
+					    YOGA_EC_RESP_LEN, resp);
+	if (ret < 0)
+		return ret;
+	if (ret != YOGA_EC_RESP_LEN)
+		return -EIO;
+
+	return 0;
+}
+
+static int yoga_ec_ram_read(struct yoga_ec *ec, u8 addr, u8 *val)
+{
+	u8 resp[YOGA_EC_RESP_LEN];
+	int ret;
+
+	ret = yoga_ec_command(ec, YOGA_EC_CMD_RAM_READ, addr, 0, resp);
+	if (ret)
+		return ret;
+
+	*val = resp[0];
+	return 0;
+}
+
+static int yoga_ec_bank_read(struct yoga_ec *ec, u8 bank, u8 addr, u8 *val)
+{
+	u8 resp[YOGA_EC_RESP_LEN];
+	int ret;
+
+	ret = yoga_ec_command(ec, YOGA_EC_CMD_BANK_READ, bank, addr, resp);
+	if (ret)
+		return ret;
+
+	*val = resp[0];
+	return 0;
+}
+
+static int yoga_ec_ram_write(struct yoga_ec *ec, u8 addr, u8 val)
+{
+	u8 resp[YOGA_EC_RESP_LEN];
+
+	return yoga_ec_command(ec, YOGA_EC_CMD_RAM_WRITE, addr, val, resp);
+}
+
+static int yoga_ec_fan_read(struct yoga_ec *ec, long *rpm)
+{
+	u8 resp[YOGA_EC_RESP_LEN];
+	int ret;
+
+	ret = yoga_ec_command(ec, YOGA_EC_CMD_FAN, YOGA_EC_FAN_GET_SPEED, 0,
+			      resp);
+	if (ret)
+		return ret;
+
+	*rpm = resp[0] * 100;
+	return 0;
+}
+
+static int yoga_ec_refresh_cells(struct yoga_ec *ec)
+{
+	unsigned int try;
+	int ret;
+
+	if (ec->cell_cached &&
+	    time_before(jiffies, ec->cell_read_at + YOGA_EC_CACHE))
+		return 0;
+
+	for (try = 0; try < YOGA_EC_CELL_TRIES; try++) {
+		u16 cell[YOGA_EC_NUM_CELL];
+		u8 sig[3];
+		unsigned int i;
+		bool ok = true;
+
+		ret = yoga_ec_bank_read(ec, YOGA_EC_CELL_BANK,
+					YOGA_EC_CELL_SIG_LO, &sig[0]);
+		if (!ret)
+			ret = yoga_ec_bank_read(ec, YOGA_EC_CELL_BANK,
+						YOGA_EC_CELL_SIG_LO + 1, &sig[1]);
+		if (!ret)
+			ret = yoga_ec_bank_read(ec, YOGA_EC_CELL_BANK,
+						YOGA_EC_CELL_SIG_HI, &sig[2]);
+		if (ret)
+			return ret;
+
+		if (sig[0] != 0x01 || sig[1] != 0x01 || sig[2] != 0x03)
+			continue;
+
+		for (i = 0; i < YOGA_EC_NUM_CELL; i++) {
+			u8 lsb, msb;
+
+			ret = yoga_ec_bank_read(ec, YOGA_EC_CELL_BANK,
+						YOGA_EC_CELL_FIRST + i * 2, &lsb);
+			if (!ret)
+				ret = yoga_ec_bank_read(ec, YOGA_EC_CELL_BANK,
+							YOGA_EC_CELL_FIRST + i * 2 + 1,
+							&msb);
+			if (ret)
+				return ret;
+
+			cell[i] = lsb | (msb << 8);
+			if (cell[i] < YOGA_EC_CELL_MIN_MV ||
+			    cell[i] > YOGA_EC_CELL_MAX_MV)
+				ok = false;
+		}
+
+		if (!ok)
+			continue;
+
+		memcpy(ec->cell, cell, sizeof(cell));
+		ec->cell_cached = true;
+		ec->cell_read_at = jiffies;
+
+		return 0;
+	}
+
+	if (ec->cell_cached &&
+	    time_before(jiffies, ec->cell_read_at + YOGA_EC_STALE))
+		return 0;
+
+	return -EAGAIN;
+}
+
+static umode_t yoga_ec_is_visible(const void *drvdata,
+				  enum hwmon_sensor_types type,
+				  u32 attr, int channel)
+{
+	const struct yoga_ec *ec = drvdata;
+
+	switch (type) {
+	case hwmon_temp:
+		if (!(ec->temp_present & BIT(channel)))
+			return 0;
+		return 0444;
+	case hwmon_in:
+		return 0444;
+	case hwmon_fan:
+		return 0444;
+	default:
+		return 0;
+	}
+}
+
+static int yoga_ec_read(struct device *dev, enum hwmon_sensor_types type,
+			u32 attr, int channel, long *val)
+{
+	struct yoga_ec *ec = dev_get_drvdata(dev);
+	int ret;
+	u8 raw;
+
+	guard(mutex)(&ec->lock);
+
+	switch (type) {
+	case hwmon_temp:
+		ret = yoga_ec_ram_read(ec, YOGA_EC_TEMP_FIRST + channel, &raw);
+		if (ret)
+			return ret;
+		if (raw > YOGA_EC_TEMP_MAX)
+			return -EIO;
+		*val = raw * 1000;
+		return 0;
+	case hwmon_in:
+		ret = yoga_ec_refresh_cells(ec);
+		if (ret)
+			return ret;
+		*val = ec->cell[channel];
+		return 0;
+	case hwmon_fan:
+		return yoga_ec_fan_read(ec, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static const char * const yoga_ec_temp_labels[YOGA_EC_NUM_TEMP] = {
+	"Charger A",
+	"CPU VR",
+	"5V rail",
+	"Ambient",
+	"Charger B",
+	"Thermistor 6",
+};
+
+static const char * const yoga_ec_cell_labels[YOGA_EC_NUM_CELL] = {
+	"Cell 1", "Cell 2", "Cell 3", "Cell 4",
+};
+
+static int yoga_ec_read_string(struct device *dev, enum hwmon_sensor_types type,
+			       u32 attr, int channel, const char **str)
+{
+	switch (type) {
+	case hwmon_temp:
+		*str = yoga_ec_temp_labels[channel];
+		return 0;
+	case hwmon_in:
+		*str = yoga_ec_cell_labels[channel];
+		return 0;
+	case hwmon_fan:
+		*str = "CPU fan";
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static enum led_brightness yoga_ec_kbd_led_get(struct led_classdev *led)
+{
+	struct yoga_ec *ec = container_of(led, struct yoga_ec, kbd_led);
+	u8 raw;
+
+	guard(mutex)(&ec->lock);
+
+	if (yoga_ec_ram_read(ec, YOGA_EC_KBLT, &raw))
+		return 0;
+
+	raw = FIELD_GET(YOGA_EC_KBLT_LEVEL, raw);
+
+	if (raw == YOGA_EC_KBLT_AUTO)
+		return YOGA_EC_KBLT_MAX;
+
+	return raw;
+}
+
+static int yoga_ec_kbd_led_level(struct yoga_ec *ec, u8 level, u8 *old)
+{
+	u8 raw;
+	int ret;
+
+	ret = yoga_ec_ram_read(ec, YOGA_EC_KBLT, &raw);
+	if (ret)
+		return ret;
+
+	if (old)
+		*old = FIELD_GET(YOGA_EC_KBLT_LEVEL, raw);
+
+	raw &= ~YOGA_EC_KBLT_LEVEL;
+	raw |= FIELD_PREP(YOGA_EC_KBLT_LEVEL, level);
+
+	return yoga_ec_ram_write(ec, YOGA_EC_KBLT, raw);
+}
+
+static int yoga_ec_kbd_led_set(struct led_classdev *led,
+			       enum led_brightness brightness)
+{
+	struct yoga_ec *ec = container_of(led, struct yoga_ec, kbd_led);
+
+	guard(mutex)(&ec->lock);
+
+	return yoga_ec_kbd_led_level(ec, brightness, NULL);
+}
+
+static const struct hwmon_ops yoga_ec_hwmon_ops = {
+	.is_visible = yoga_ec_is_visible,
+	.read = yoga_ec_read,
+	.read_string = yoga_ec_read_string,
+};
+
+static const struct hwmon_channel_info * const yoga_ec_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(in,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_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,
+			   HWMON_T_INPUT | HWMON_T_LABEL,
+			   HWMON_T_INPUT | HWMON_T_LABEL),
+	HWMON_CHANNEL_INFO(fan,
+			   HWMON_F_INPUT | HWMON_F_LABEL),
+	NULL
+};
+
+static const struct hwmon_chip_info yoga_ec_chip_info = {
+	.ops = &yoga_ec_hwmon_ops,
+	.info = yoga_ec_hwmon_info,
+};
+
+static int yoga_ec_fn_lock_toggle(struct yoga_ec *ec)
+{
+	u8 raw;
+	int ret;
+
+	ret = yoga_ec_ram_read(ec, YOGA_EC_SCUK, &raw);
+	if (ret)
+		return ret;
+
+	if (raw & YOGA_EC_SCUK_FN_LOCK)
+		raw &= YOGA_EC_SCUK_FN_LOCK_OFF;
+	else
+		raw |= YOGA_EC_SCUK_FN_LOCK_ON;
+
+	return yoga_ec_ram_write(ec, YOGA_EC_SCUK, raw);
+}
+
+static void yoga_ec_report_key(struct yoga_ec *ec, unsigned int code)
+{
+	if (!ec->keys)
+		return;
+
+	input_report_key(ec->keys, code, 1);
+	input_sync(ec->keys);
+	input_report_key(ec->keys, code, 0);
+	input_sync(ec->keys);
+}
+
+static irqreturn_t yoga_ec_irq(int irq, void *data)
+{
+	struct yoga_ec *ec = data;
+	unsigned int i;
+
+	guard(mutex)(&ec->lock);
+
+	for (i = 0; i < YOGA_EC_EVT_MAX_DRAIN; i++) {
+		u8 resp[YOGA_EC_RESP_LEN];
+
+		if (yoga_ec_command(ec, YOGA_EC_CMD_EVENT, 0, 0, resp))
+			break;
+		if (!resp[0])
+			break;
+
+		switch (resp[0]) {
+		case YOGA_EC_EVT_FN_LOCK:
+			yoga_ec_fn_lock_toggle(ec);
+			break;
+		case YOGA_EC_EVT_MIC_MUTE:
+			yoga_ec_report_key(ec, KEY_MICMUTE);
+			break;
+		case YOGA_EC_EVT_FLIGHT_MODE:
+			yoga_ec_report_key(ec, KEY_RFKILL);
+			break;
+		default:
+			break;
+		}
+	}
+
+	return IRQ_HANDLED;
+}
+
+static void yoga_ec_register_keys(struct yoga_ec *ec)
+{
+	struct device *dev = &ec->client->dev;
+	struct input_dev *input;
+	int ret;
+
+	input = devm_input_allocate_device(dev);
+	if (!input) {
+		dev_warn(dev, "no memory for the hotkey device\n");
+		return;
+	}
+
+	input->name = "Lenovo Yoga Slim 7x hotkeys";
+	input->phys = "lenovo-yoga-slim7x-gen11-ec/input0";
+	input->id.bustype = BUS_I2C;
+	input_set_capability(input, EV_KEY, KEY_MICMUTE);
+	input_set_capability(input, EV_KEY, KEY_RFKILL);
+
+	ret = input_register_device(input);
+	if (ret) {
+		dev_warn(dev, "cannot register the hotkey device: %d\n", ret);
+		return;
+	}
+
+	ec->keys = input;
+}
+
+static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr,
+			    char *buf)
+{
+	struct yoga_ec *ec = dev_get_drvdata(dev);
+	u8 raw;
+	int ret;
+
+	scoped_guard(mutex, &ec->lock)
+		ret = yoga_ec_ram_read(ec, YOGA_EC_SCUK, &raw);
+
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "%u\n", !!(raw & YOGA_EC_SCUK_FN_LOCK));
+}
+
+static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr,
+			     const char *buf, size_t count)
+{
+	struct yoga_ec *ec = dev_get_drvdata(dev);
+	bool enable;
+	u8 raw;
+	int ret;
+
+	ret = kstrtobool(buf, &enable);
+	if (ret)
+		return ret;
+
+	guard(mutex)(&ec->lock);
+
+	ret = yoga_ec_ram_read(ec, YOGA_EC_SCUK, &raw);
+	if (ret)
+		return ret;
+
+	if (enable)
+		raw |= YOGA_EC_SCUK_FN_LOCK_ON;
+	else
+		raw &= YOGA_EC_SCUK_FN_LOCK_OFF;
+
+	ret = yoga_ec_ram_write(ec, YOGA_EC_SCUK, raw);
+	if (ret)
+		return ret;
+
+	return count;
+}
+static DEVICE_ATTR_RW(fn_lock);
+
+static struct attribute *yoga_ec_attrs[] = {
+	&dev_attr_fn_lock.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(yoga_ec);
+
+static int yoga_ec_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct device *hwmon;
+	struct yoga_ec *ec;
+	unsigned int i;
+	int ret;
+
+	if (!i2c_check_functionality(client->adapter,
+				     I2C_FUNC_SMBUS_I2C_BLOCK))
+		return dev_err_probe(dev, -ENODEV,
+				     "adapter does not support SMBus block transfers\n");
+
+	ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL);
+	if (!ec)
+		return -ENOMEM;
+
+	ec->client = client;
+	mutex_init(&ec->lock);
+	i2c_set_clientdata(client, ec);
+
+	for (i = 0; i < YOGA_EC_NUM_TEMP; i++) {
+		u8 raw;
+
+		ret = yoga_ec_ram_read(ec, YOGA_EC_TEMP_FIRST + i, &raw);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to read thermistor %u\n", i);
+
+		if (raw > 0 && raw <= YOGA_EC_TEMP_MAX)
+			ec->temp_present |= BIT(i);
+	}
+
+	if (!ec->temp_present)
+		return dev_err_probe(dev, -ENODEV,
+				     "no thermistor reported a usable value\n");
+
+	dev_dbg(dev, "thermistors present: %#lx\n", ec->temp_present);
+
+	hwmon = devm_hwmon_device_register_with_info(dev, "yoga_slim7x_ec", ec,
+						     &yoga_ec_chip_info, NULL);
+	if (IS_ERR(hwmon))
+		return PTR_ERR(hwmon);
+
+	ec->kbd_led.name = "platform::kbd_backlight";
+	ec->kbd_led.max_brightness = YOGA_EC_KBLT_MAX;
+	ec->kbd_led.brightness_get = yoga_ec_kbd_led_get;
+	ec->kbd_led.brightness_set_blocking = yoga_ec_kbd_led_set;
+
+	ret = devm_led_classdev_register(dev, &ec->kbd_led);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register keyboard backlight\n");
+
+	if (client->irq) {
+		yoga_ec_register_keys(ec);
+
+		ret = devm_request_threaded_irq(dev, client->irq, NULL,
+						yoga_ec_irq, IRQF_ONESHOT,
+						"lenovo-yoga-slim7x-gen11-ec", ec);
+		if (ret)
+			return dev_err_probe(dev, ret, "cannot request the EC interrupt\n");
+
+		scoped_guard(mutex, &ec->lock) {
+			unsigned int i;
+
+			for (i = 0; i < YOGA_EC_EVT_MAX_DRAIN; i++) {
+				u8 resp[YOGA_EC_RESP_LEN];
+
+				if (yoga_ec_command(ec, YOGA_EC_CMD_EVENT, 0, 0, resp))
+					break;
+				if (!resp[0])
+					break;
+			}
+		}
+	}
+
+	return 0;
+}
+
+static int yoga_ec_suspend(struct device *dev)
+{
+	struct yoga_ec *ec = dev_get_drvdata(dev);
+
+	guard(mutex)(&ec->lock);
+	ec->kblt_restore = !yoga_ec_kbd_led_level(ec, 0, &ec->kblt_level);
+
+	return 0;
+}
+
+static int yoga_ec_resume(struct device *dev)
+{
+	struct yoga_ec *ec = dev_get_drvdata(dev);
+
+	guard(mutex)(&ec->lock);
+	ec->cell_cached = false;
+
+	if (ec->kblt_restore) {
+		ec->kblt_restore = false;
+		yoga_ec_kbd_led_level(ec, ec->kblt_level, NULL);
+	}
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(yoga_ec_pm_ops, yoga_ec_suspend, yoga_ec_resume);
+
+static const struct of_device_id yoga_ec_of_match[] = {
+	{ .compatible = "lenovo,yoga-slim7x-gen11-ec" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, yoga_ec_of_match);
+
+static const struct i2c_device_id yoga_ec_i2c_id[] = {
+	{ "yoga-slim7x-ec" },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, yoga_ec_i2c_id);
+
+static struct i2c_driver yoga_ec_driver = {
+	.driver = {
+		.name = "lenovo-yoga-slim7x-ec",
+		.of_match_table = yoga_ec_of_match,
+		.dev_groups = yoga_ec_groups,
+		.pm = pm_sleep_ptr(&yoga_ec_pm_ops),
+	},
+	.probe = yoga_ec_probe,
+	.id_table = yoga_ec_i2c_id,
+};
+module_i2c_driver(yoga_ec_driver);
+
+MODULE_AUTHOR("Oleg Keri <ezhi99@gmail.com>");
+MODULE_DESCRIPTION("Lenovo Yoga Slim 7x Gen 11 embedded controller");
+MODULE_LICENSE("GPL");
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver
  2026-09-08  9:17 ` [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver Oleg Keri
@ 2026-09-08 15:53   ` Uwe Kleine-König
  0 siblings, 0 replies; 6+ messages in thread
From: Uwe Kleine-König @ 2026-09-08 15:53 UTC (permalink / raw)
  To: Oleg Keri
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Hans de Goede,
	Ilpo Järvinen, Bryan O'Donoghue, Guenter Roeck,
	linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	linux-hwmon

[-- Attachment #1: Type: text/plain, Size: 412 bytes --]

On Tue, Sep 08, 2026 at 11:17:52AM +0200, Oleg Keri wrote:
> +#include <linux/i2c.h>
> [...]
> +#include <linux/mod_devicetable.h>

Please don't include <linux/mod_devicetable.h>, this header should go
away soon. To get a definition of of_device_id and i2c_device_id
<linux/i2c.h> is enough. (Or if you want to go the full iwyu way,
include <linux/device-id/of.h> and <linux/device-id/i2c.h>.)

Best regards
Uwe

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC
  2026-09-08  9:17 ` [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC Oleg Keri
@ 2026-09-08 17:55   ` Conor Dooley
  0 siblings, 0 replies; 6+ messages in thread
From: Conor Dooley @ 2026-09-08 17:55 UTC (permalink / raw)
  To: Oleg Keri
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Hans de Goede,
	Ilpo Järvinen, Bryan O'Donoghue, Guenter Roeck,
	linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	linux-hwmon

[-- Attachment #1: Type: text/plain, Size: 75 bytes --]

Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver
       [not found] <20260908092805.E15781F00A3F@smtp.kernel.org>
@ 2026-09-08 11:25 ` Oleg Keri
  0 siblings, 0 replies; 6+ messages in thread
From: Oleg Keri @ 2026-09-08 11:25 UTC (permalink / raw)
  To: Guenter Roeck, Ilpo Järvinen, Hans de Goede, Bryan O'Donoghue
  Cc: sashiko-bot, linux-hwmon, platform-driver-x86, linux-arm-msm,
	linux-kernel

On Tue, Sep 08, 2026 at 09:28:05AM +0000, sashiko-bot@kernel.org wrote:
> - [Low] Missing dependency on CONFIG_INPUT in Kconfig.

Correct, thanks. The driver calls devm_input_allocate_device() and
input_register_device() unconditionally, so EC_LENOVO_YOGA_SLIM7X_GEN11=y
with INPUT=m would fail to link. I will add "depends on INPUT" in v2, which
is also what EC_ACER_ASPIRE1, EC_LENOVO_THINKPAD_T14S and EC_HUAWEI_GAOKUN
declare.

> - [Medium] Hardware monitoring device registered directly from a platform
>   driver outside of drivers/hwmon/.

I would prefer to keep this as it is, unless the hwmon maintainer disagrees.

The auxiliary device guidance in Documentation/hwmon/submitting-patches.rst
is aimed at chips whose hardware monitoring is a distinct sub-function
substantial enough to warrant its own driver. The embedded controller
drivers under drivers/platform/arm64/ register their hwmon device inline
instead, because the monitoring is a handful of registers reached over the
same I2C mailbox the rest of the driver already owns.

The closest precedent is huawei-gaokun-ec.c, in this same directory, which
does exactly this:

  ec->hwmon_dev = devm_hwmon_device_register_with_info(dev, "gaokun_ec_hwmon",
						       ec, &gaokun_ec_hwmon_chip_info,
						       NULL);

with "depends on HWMON" in its Kconfig entry.

Splitting five thermistors, four cell voltages and one tachometer into an
auxiliary device would add an auxiliary bus, a second driver and a
shared-state accessor for readings that are one I2C transaction each. The
cell voltages in particular have to take the same lock as the rest of the
driver: they live in a banked mailbox shared with other EC activity, are
validated against a signature and retried, and are cached so that all four
channels come from one coherent sample.

Guenter, if you would rather see the auxiliary split, please say so and I
will rework it.

Thanks,
Oleg

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-08 17:55 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-08  9:17 [PATCH 0/2] platform/arm64: Lenovo Yoga Slim 7x Gen 11 embedded controller Oleg Keri
2026-09-08  9:17 ` [PATCH 1/2] dt-bindings: embedded-controller: add Lenovo Yoga Slim 7x Gen 11 EC Oleg Keri
2026-09-08 17:55   ` Conor Dooley
2026-09-08  9:17 ` [PATCH 2/2] platform/arm64: add Lenovo Yoga Slim 7x Gen 11 EC driver Oleg Keri
2026-09-08 15:53   ` Uwe Kleine-König
     [not found] <20260908092805.E15781F00A3F@smtp.kernel.org>
2026-09-08 11:25 ` Oleg Keri

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®