Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v1 0/9] mfd: Add support for Asus Transformer embedded controller
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm

Add support for embedded controller used in Asus Transformers for
managing power and input functions.

Michał Mirosław (7):
  misc: Support Asus Transformer's EC access device
  mfd: Add driver for Asus Transformer embedded controller
  input: serio: Add driver for Asus Transformer dock keyboard and
    touchpad
  input: keyboard: Add driver for Asus Transformer dock multimedia keys
  leds: Add driver for Asus Transformer LEDs
  power: supply: Add driver for Asus Transformer battery
  power: supply: Add charger driver for Asus Transformers

Svyatoslav Ryhel (2):
  dt-bindings: misc: document ASUS Transformers EC Dockram
  dt-bindings: mfd: document ASUS Transformer EC

 .../devicetree/bindings/mfd/asus,ec.yaml      | 153 ++++++
 .../bindings/misc/asus,dockram.yaml           |  40 ++
 drivers/input/keyboard/Kconfig                |  10 +
 drivers/input/keyboard/Makefile               |   1 +
 drivers/input/keyboard/asus-ec-keys.c         | 285 +++++++++++
 drivers/input/serio/Kconfig                   |  15 +
 drivers/input/serio/Makefile                  |   1 +
 drivers/input/serio/asus-ec-kbc.c             | 162 ++++++
 drivers/leds/Kconfig                          |  11 +
 drivers/leds/Makefile                         |   1 +
 drivers/leds/leds-asus-ec.c                   | 106 ++++
 drivers/mfd/Kconfig                           |  14 +
 drivers/mfd/Makefile                          |   1 +
 drivers/mfd/asus-ec.c                         | 460 ++++++++++++++++++
 drivers/misc/Kconfig                          |   9 +
 drivers/misc/Makefile                         |   1 +
 drivers/misc/asus-dockram.c                   | 327 +++++++++++++
 drivers/power/supply/Kconfig                  |  22 +
 drivers/power/supply/Makefile                 |   2 +
 drivers/power/supply/asus-ec-battery.c        | 282 +++++++++++
 drivers/power/supply/asus-ec-charger.c        | 205 ++++++++
 include/linux/mfd/asus-ec.h                   | 113 +++++
 22 files changed, 2221 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/mfd/asus,ec.yaml
 create mode 100644 Documentation/devicetree/bindings/misc/asus,dockram.yaml
 create mode 100644 drivers/input/keyboard/asus-ec-keys.c
 create mode 100644 drivers/input/serio/asus-ec-kbc.c
 create mode 100644 drivers/leds/leds-asus-ec.c
 create mode 100644 drivers/mfd/asus-ec.c
 create mode 100644 drivers/misc/asus-dockram.c
 create mode 100644 drivers/power/supply/asus-ec-battery.c
 create mode 100644 drivers/power/supply/asus-ec-charger.c
 create mode 100644 include/linux/mfd/asus-ec.h

-- 
2.51.0


^ permalink raw reply

* [PATCH v1 1/9] dt-bindings: misc: document ASUS Transformers EC Dockram
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

Documenting an I2C device used in conjunction with the EC on ASUS
Transformers. The main function of Dockram (the name used by downstream
ASUS sources) is to provide power-related functions, such as battery and
charger communication. The device is exposed as an individual entity
because multiple embedded controllers can utilize the same Dockram
instance.

Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 .../bindings/misc/asus,dockram.yaml           | 40 +++++++++++++++++++
 1 file changed, 40 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/misc/asus,dockram.yaml

diff --git a/Documentation/devicetree/bindings/misc/asus,dockram.yaml b/Documentation/devicetree/bindings/misc/asus,dockram.yaml
new file mode 100644
index 000000000000..3c690b83dffe
--- /dev/null
+++ b/Documentation/devicetree/bindings/misc/asus,dockram.yaml
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/misc/asus,dockram.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Asus Transformer EC Dockram
+
+maintainers:
+  - Svyatoslav Ryhel <clamor95@gmail.com>
+
+description:
+  Dedicated i2c device used to provide power related functions of the
+  embedded controller used in ASUS Transformer device family.
+
+properties:
+  compatible:
+    const: asus,dockram
+
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    i2c {
+      #address-cells = <1>;
+      #size-cells = <0>;
+
+      dockram@17 {
+        compatible = "asus,dockram";
+        reg = <0x17>;
+      };
+    };
+...
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 2/9] misc: Support Asus Transformer's EC access device
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Add support for accessing Embedded Controller of Asus Transformer devices.
This will be used by the EC MFD drivers.

Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 drivers/misc/Kconfig        |   9 +
 drivers/misc/Makefile       |   1 +
 drivers/misc/asus-dockram.c | 327 ++++++++++++++++++++++++++++++++++++
 include/linux/mfd/asus-ec.h |  18 ++
 4 files changed, 355 insertions(+)
 create mode 100644 drivers/misc/asus-dockram.c
 create mode 100644 include/linux/mfd/asus-ec.h

diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index dd1ab7e445ac..e7faa7ab4199 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -50,6 +50,15 @@ config AD525X_DPOT_SPI
 	  To compile this driver as a module, choose M here: the
 	  module will be called ad525x_dpot-spi.
 
+config ASUS_DOCKRAM
+	tristate "Asus Transformer's EC DockRAM"
+	depends on I2C
+	help
+	  Select this if you are building for Asus Transformer's.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called asus-dockram.
+
 config DUMMY_IRQ
 	tristate "Dummy IRQ handler"
 	help
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index bfad6982591c..d2287e912d59 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -8,6 +8,7 @@ obj-$(CONFIG_IBMVMC)		+= ibmvmc.o
 obj-$(CONFIG_AD525X_DPOT)	+= ad525x_dpot.o
 obj-$(CONFIG_AD525X_DPOT_I2C)	+= ad525x_dpot-i2c.o
 obj-$(CONFIG_AD525X_DPOT_SPI)	+= ad525x_dpot-spi.o
+obj-$(CONFIG_ASUS_DOCKRAM)	+= asus-dockram.o
 obj-$(CONFIG_ATMEL_SSC)		+= atmel-ssc.o
 obj-$(CONFIG_DUMMY_IRQ)		+= dummy-irq.o
 obj-$(CONFIG_ICS932S401)	+= ics932s401.o
diff --git a/drivers/misc/asus-dockram.c b/drivers/misc/asus-dockram.c
new file mode 100644
index 000000000000..d98dcf5ef2d4
--- /dev/null
+++ b/drivers/misc/asus-dockram.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS EC: DockRAM
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+
+struct dockram_ec_data {
+	struct mutex ctl_lock; /* prevent simultaneous access */
+	char ctl_data[DOCKRAM_ENTRY_BUFSIZE];
+};
+
+int asus_dockram_read(struct i2c_client *client, int reg, char *buf)
+{
+	int rc;
+
+	memset(buf, 0, DOCKRAM_ENTRY_BUFSIZE);
+	rc = i2c_smbus_read_i2c_block_data(client, reg, DOCKRAM_ENTRY_BUFSIZE, buf);
+	if (rc < 0)
+		return rc;
+
+	if (buf[0] > DOCKRAM_ENTRY_SIZE) {
+		dev_err(&client->dev, "bad data len; buffer: %*ph; rc: %d\n",
+			DOCKRAM_ENTRY_BUFSIZE, buf, rc);
+		return -EPROTO;
+	}
+
+	dev_dbg(&client->dev, "got data; buffer: %*ph; rc: %d\n",
+		DOCKRAM_ENTRY_BUFSIZE, buf, rc);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(asus_dockram_read);
+
+int asus_dockram_write(struct i2c_client *client, int reg, const char *buf)
+{
+	if (buf[0] > DOCKRAM_ENTRY_SIZE)
+		return -EINVAL;
+
+	dev_dbg(&client->dev, "sending data; buffer: %*ph\n", buf[0] + 1, buf);
+
+	return i2c_smbus_write_i2c_block_data(client, reg, buf[0] + 1, buf);
+}
+EXPORT_SYMBOL_GPL(asus_dockram_write);
+
+int asus_dockram_access_ctl(struct i2c_client *client,
+			    u64 *out, u64 mask, u64 xor)
+{
+	struct dockram_ec_data *priv = i2c_get_clientdata(client);
+	char *buf = priv->ctl_data;
+	u64 val;
+	int ret = 0;
+
+	guard(mutex)(&priv->ctl_lock);
+
+	ret = asus_dockram_read(client, ASUSEC_DOCKRAM_CONTROL, buf);
+	if (ret < 0)
+		goto exit;
+
+	if (buf[0] != ASUSEC_CTL_SIZE) {
+		ret = -EPROTO;
+		goto exit;
+	}
+
+	val = get_unaligned_le64(buf + 1);
+
+	if (out)
+		*out = val;
+
+	if (mask || xor) {
+		put_unaligned_le64((val & ~mask) ^ xor, buf + 1);
+		ret = asus_dockram_write(client, ASUSEC_DOCKRAM_CONTROL, buf);
+	}
+
+exit:
+	if (ret < 0)
+		dev_err(&client->dev, "Failed to access control flags: %d\n",
+			ret);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(asus_dockram_access_ctl);
+
+static ssize_t dockram_read(struct file *filp, struct kobject *kobj,
+			    const struct bin_attribute *attr,
+			    char *buf, loff_t off, size_t count)
+{
+	struct i2c_client *client = kobj_to_i2c_client(kobj);
+	unsigned int reg;
+	ssize_t n_read = 0;
+	char *data;
+	int ret;
+
+	reg = off / DOCKRAM_ENTRY_SIZE;
+	off %= DOCKRAM_ENTRY_SIZE;
+
+	if (!count)
+		return 0;
+
+	data = kmalloc(DOCKRAM_ENTRY_BUFSIZE, GFP_KERNEL);
+
+	while (reg < DOCKRAM_ENTRIES) {
+		unsigned int len = DOCKRAM_ENTRY_SIZE - off;
+
+		if (len > count)
+			len = count;
+
+		ret = asus_dockram_read(client, reg, data);
+		if (ret < 0) {
+			if (!n_read)
+				n_read = ret;
+			break;
+		}
+
+		memcpy(buf, data + 1 + off, len);
+		n_read += len;
+
+		if (len == count)
+			break;
+
+		count -= len;
+		buf += len;
+		off = 0;
+		++reg;
+	}
+
+	kfree(data);
+
+	return n_read;
+}
+
+static int dockram_write_one(struct i2c_client *client, int reg,
+			     const char *buf, size_t count)
+{
+	struct dockram_ec_data *priv = i2c_get_clientdata(client);
+	int ret;
+
+	if (!count || count > DOCKRAM_ENTRY_SIZE)
+		return -EINVAL;
+	if (buf[0] != count - 1)
+		return -EINVAL;
+
+	guard(mutex)(&priv->ctl_lock);
+
+	priv->ctl_data[0] = (u8)count;
+	memcpy(priv->ctl_data + 1, buf, count);
+	ret = asus_dockram_write(client, reg, priv->ctl_data);
+
+	return ret;
+}
+
+static ssize_t dockram_write(struct file *filp, struct kobject *kobj,
+			     const struct bin_attribute *attr,
+			     char *buf, loff_t off, size_t count)
+{
+	struct i2c_client *client = kobj_to_i2c_client(kobj);
+	unsigned int reg;
+	int ret;
+
+	if (off % DOCKRAM_ENTRY_SIZE != 0)
+		return -EINVAL;
+
+	reg = off / DOCKRAM_ENTRY_SIZE;
+	if (reg >= DOCKRAM_ENTRIES)
+		return -EINVAL;
+
+	ret = dockram_write_one(client, reg, buf, count);
+
+	return ret < 0 ? ret : count;
+}
+
+static ssize_t control_reg_show(struct device *dev,
+				struct device_attribute *attr,
+				char *buf)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	u64 val;
+	int ret;
+
+	ret = asus_dockram_access_ctl(client, &val, 0, 0);
+	if (ret < 0)
+		return ret;
+
+	return sysfs_emit(buf, "%016llx\n", val);
+}
+
+static ssize_t control_reg_store(struct device *dev,
+				 struct device_attribute *attr,
+				 const char *buf, size_t count)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	u64 val;
+	int ret;
+
+	ret = kstrtoull(buf, 16, &val);
+	if (ret < 0)
+		return ret;
+
+	ret = asus_dockram_access_ctl(client, NULL, ~0ull, val);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static BIN_ATTR_RW(dockram, DOCKRAM_ENTRIES * DOCKRAM_ENTRY_SIZE);
+static DEVICE_ATTR_RW(control_reg);
+
+static struct attribute *dockram_attrs[] = {
+	&dev_attr_control_reg.attr,
+	NULL
+};
+
+static const struct bin_attribute *dockram_bin_attrs[] = {
+	&bin_attr_dockram,
+	NULL
+};
+
+static const struct attribute_group dockram_group = {
+	.attrs = dockram_attrs,
+	.bin_attrs = dockram_bin_attrs,
+};
+
+static int asus_dockram_probe(struct i2c_client *client)
+{
+	struct dockram_ec_data *priv;
+
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_I2C_BLOCK))
+		return dev_err_probe(&client->dev, -ENXIO,
+			"I2C bus is missing required SMBus block mode support\n");
+
+	priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	i2c_set_clientdata(client, priv);
+	mutex_init(&priv->ctl_lock);
+
+	return devm_device_add_group(&client->dev, &dockram_group);
+}
+
+static const struct of_device_id asus_dockram_ids[] = {
+	{ .compatible = "asus,dockram" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_dockram_ids);
+
+static struct i2c_driver asus_dockram_driver = {
+	.driver	= {
+		.name = "asus-dockram",
+		.of_match_table = of_match_ptr(asus_dockram_ids),
+	},
+	.probe = asus_dockram_probe,
+};
+module_i2c_driver(asus_dockram_driver);
+
+static void devm_i2c_device_release(struct device *dev, void *res)
+{
+	struct i2c_client **pdev = res;
+	struct i2c_client *child = *pdev;
+
+	if (child)
+		put_device(&child->dev);
+}
+
+static struct i2c_client *devm_i2c_device_get_by_phandle(struct device *dev,
+							 const char *name,
+							 int index)
+{
+	struct device_node *np;
+	struct i2c_client **pdev;
+
+	pdev = devres_alloc(devm_i2c_device_release, sizeof(*pdev),
+			    GFP_KERNEL);
+	if (!pdev)
+		return ERR_PTR(-ENOMEM);
+
+	np = of_parse_phandle(dev_of_node(dev), name, index);
+	if (!np) {
+		devres_free(pdev);
+		dev_err(dev, "can't resolve phandle %s: %d\n", name, index);
+		return ERR_PTR(-ENODEV);
+	}
+
+	*pdev = of_find_i2c_device_by_node(np);
+	of_node_put(np);
+
+	if (!*pdev) {
+		devres_free(pdev);
+		return ERR_PTR(-EPROBE_DEFER);
+	}
+
+	devres_add(dev, pdev);
+
+	return *pdev;
+}
+
+struct i2c_client *devm_asus_dockram_get(struct device *parent)
+{
+	struct i2c_client *dockram =
+		devm_i2c_device_get_by_phandle(parent, "asus,dockram", 0);
+
+	if (IS_ERR(dockram))
+		return dockram;
+	if (!dockram->dev.driver)
+		return ERR_PTR(-EPROBE_DEFER);
+	if (dockram->dev.driver != &asus_dockram_driver.driver)
+		return ERR_PTR(-EBUSY);
+
+	return dockram;
+}
+EXPORT_SYMBOL_GPL(devm_asus_dockram_get);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_DESCRIPTION("ASUS Transformer's dockram driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/asus-ec.h b/include/linux/mfd/asus-ec.h
new file mode 100644
index 000000000000..bc4efa37f5ba
--- /dev/null
+++ b/include/linux/mfd/asus-ec.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __MISC_ASUS_EC_H
+#define __MISC_ASUS_EC_H
+
+struct i2c_client;
+
+/* dockram comm */
+int asus_dockram_read(struct i2c_client *client, int reg, char *buf);
+int asus_dockram_write(struct i2c_client *client, int reg, const char *buf);
+int asus_dockram_access_ctl(struct i2c_client *client,
+			    u64 *out, u64 mask, u64 xor);
+struct i2c_client *devm_asus_dockram_get(struct device *parent);
+
+#define DOCKRAM_ENTRIES		0x100
+#define DOCKRAM_ENTRY_SIZE	32
+#define DOCKRAM_ENTRY_BUFSIZE	(DOCKRAM_ENTRY_SIZE + 1)
+
+#endif /* __MISC_ASUS_EC_H */
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 3/9] dt-bindings: mfd: document ASUS Transformer EC
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

Document embedded controller used in ASUS Transformer device series.

Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 .../devicetree/bindings/mfd/asus,ec.yaml      | 153 ++++++++++++++++++
 1 file changed, 153 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/mfd/asus,ec.yaml

diff --git a/Documentation/devicetree/bindings/mfd/asus,ec.yaml b/Documentation/devicetree/bindings/mfd/asus,ec.yaml
new file mode 100644
index 000000000000..5c28deebce7b
--- /dev/null
+++ b/Documentation/devicetree/bindings/mfd/asus,ec.yaml
@@ -0,0 +1,153 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mfd/asus,ec.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ASUS Transformer's Embedded Controller
+
+description:
+  Several Nuvoton based Embedded Controller attached to an I2C bus,
+  running a custom ASUS firmware, specific to the Asus Transformer
+  device series.
+
+maintainers:
+  - Svyatoslav Ryhel <clamor95@gmail.com>
+
+properties:
+  compatible:
+    oneOf:
+      - enum:
+          - asus,ec-pad  # Pad part of Asus Transformer
+          - asus,ec-dock # Dock part of Asus Transformer
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  request-gpio:
+    maxItems: 1
+
+  asus,dockram:
+    $ref: /schemas/types.yaml#/definitions/phandle
+    description: I2C device used to access power related functions.
+
+  battery:
+    type: object
+    $ref: /schemas/power/supply/power-supply.yaml
+    unevaluatedProperties: false
+
+    properties:
+      compatible:
+        const: asus,ec-battery
+
+    required:
+      - compatible
+
+  charger:
+    type: object
+    $ref: /schemas/power/supply/power-supply.yaml
+    additionalProperties: false
+
+    properties:
+      compatible:
+        const: asus,ec-charger
+
+      monitored-battery: true
+
+    required:
+      - compatible
+
+  keyboard-ext:
+    type: object
+    description: top row of multimedia keys
+    additionalProperties: false
+
+    properties:
+      compatible:
+        const: asus,ec-keys
+
+    required:
+      - compatible
+
+  led:
+    type: object
+    additionalProperties: false
+
+    properties:
+      compatible:
+        const: asus,ec-led
+
+    required:
+      - compatible
+
+  serio:
+    type: object
+    description: keyboard and touchpad
+    additionalProperties: false
+
+    properties:
+      compatible:
+        const: asus,ec-kbc
+
+    required:
+      - compatible
+
+  asus,clear-factory-mode:
+    type: boolean
+    description: clear Factory Mode bit in EC control register
+    additionalProperties: false
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - request-gpio
+  - asus,dockram
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+      #address-cells = <1>;
+      #size-cells = <0>;
+
+      embedded-controller@19 {
+        compatible = "asus,ec-dock";
+        reg = <0x19>;
+
+        interrupt-parent = <&gpio>;
+        interrupts = <151 IRQ_TYPE_LEVEL_LOW>;
+
+        request-gpio = <&gpio 134 GPIO_ACTIVE_LOW>;
+        asus,dockram = <&dockram_ec>;
+
+        battery {
+          compatible = "asus,ec-battery";
+          monitored-battery = <&dock_battery>;
+        };
+
+        charger {
+          compatible = "asus,ec-charger";
+          monitored-battery = <&dock_battery>;
+        };
+
+        keyboard-ext {
+          compatible = "asus,ec-keys";
+        };
+
+        led {
+          compatible = "asus,ec-led";
+        };
+
+        serio {
+          compatible = "asus,ec-kbc";
+        };
+      };
+    };
+...
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 4/9] mfd: Add driver for Asus Transformer embedded controller
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Support Nuvoton NPCE795-based ECs as used in Asus Transformer TF201,
TF300T, TF300TG, TF300TL and TF700T pad and dock, as well as TF101 dock
and TF600T, P1801-T and TF701T pad. This is a glue driver handling
detection and common operations for EC's functions.

Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
---
 drivers/mfd/Kconfig         |  14 ++
 drivers/mfd/Makefile        |   1 +
 drivers/mfd/asus-ec.c       | 460 ++++++++++++++++++++++++++++++++++++
 include/linux/mfd/asus-ec.h | 101 +++++++-
 4 files changed, 573 insertions(+), 3 deletions(-)
 create mode 100644 drivers/mfd/asus-ec.c

diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..c7b32a4e65fa 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -137,6 +137,20 @@ config MFD_AAT2870_CORE
 	  additional drivers must be enabled in order to use the
 	  functionality of the device.
 
+config MFD_ASUSEC
+	tristate "ASUS embedded controller"
+	depends on I2C && OF
+	select SYSFS
+	select ASUS_DOCKRAM
+	help
+	  Support ECs as found in ASUS Transformer's Pad and Mobile Dock.
+
+	  This provides shared glue for functional part drivers:
+	    leds-asusec, serio-asusec, asusec-ext-keys, asusec-battery.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called asus-ec.
+
 config MFD_AT91_USART
 	tristate "AT91 USART Driver"
 	select MFD_CORE
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..b676922601ba 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -10,6 +10,7 @@ obj-$(CONFIG_MFD_88PM805)	+= 88pm805.o 88pm80x.o
 obj-$(CONFIG_MFD_88PM886_PMIC)	+= 88pm886.o
 obj-$(CONFIG_MFD_ACT8945A)	+= act8945a.o
 obj-$(CONFIG_MFD_SM501)		+= sm501.o
+obj-$(CONFIG_MFD_ASUSEC)	+= asus-ec.o
 obj-$(CONFIG_ARCH_BCM2835)	+= bcm2835-pm.o
 obj-$(CONFIG_MFD_BCM590XX)	+= bcm590xx.o
 obj-$(CONFIG_MFD_BD9571MWV)	+= bd9571mwv.o
diff --git a/drivers/mfd/asus-ec.c b/drivers/mfd/asus-ec.c
new file mode 100644
index 000000000000..09a914a963fe
--- /dev/null
+++ b/drivers/mfd/asus-ec.c
@@ -0,0 +1,460 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS EC driver
+ */
+
+#include <linux/array_size.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mfd/core.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+
+#define ASUSEC_RSP_BUFFER_SIZE		8
+
+struct asus_ec_chip_data {
+	const char *name;
+	const struct mfd_cell *mfd_devices;
+	unsigned int num_devices;
+};
+
+struct asus_ec_data {
+	struct asusec_info info;
+	struct mutex ecreq_lock; /* prevent simultaneous access */
+	struct gpio_desc *ecreq;
+	struct i2c_client *self;
+	const struct asus_ec_chip_data *data;
+	u8 ec_data[DOCKRAM_ENTRY_BUFSIZE];
+	bool logging_disabled;
+};
+
+#define to_ec_data(ec) \
+	container_of(ec, struct asus_ec_data, info)
+
+static void asus_ec_remove_notifier(struct device *dev, void *res)
+{
+	struct asusec_info *ec = dev_get_drvdata(dev->parent);
+	struct notifier_block **nb = res;
+
+	blocking_notifier_chain_unregister(&ec->notify_list, *nb);
+}
+
+int devm_asus_ec_register_notifier(struct platform_device *pdev,
+				   struct notifier_block *nb)
+{
+	struct asusec_info *ec = dev_get_drvdata(pdev->dev.parent);
+	struct notifier_block **res;
+	int ret;
+
+	res = devres_alloc(asus_ec_remove_notifier, sizeof(*res), GFP_KERNEL);
+	if (!res)
+		return -ENOMEM;
+
+	*res = nb;
+	ret = blocking_notifier_chain_register(&ec->notify_list, nb);
+	if (ret) {
+		devres_free(res);
+		return ret;
+	}
+
+	devres_add(&pdev->dev, res);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(devm_asus_ec_register_notifier);
+
+static int asus_ec_signal_request(const struct asusec_info *ec)
+{
+	struct asus_ec_data *priv = to_ec_data(ec);
+
+	guard(mutex)(&priv->ecreq_lock);
+
+	dev_dbg(&priv->self->dev, "EC request\n");
+
+	gpiod_set_value_cansleep(priv->ecreq, 1);
+	msleep(50);
+
+	gpiod_set_value_cansleep(priv->ecreq, 0);
+	msleep(200);
+
+	return 0;
+}
+
+static int asus_ec_write(struct asus_ec_data *priv, u16 data)
+{
+	int ret = i2c_smbus_write_word_data(priv->self, 0x64, data);
+
+	dev_dbg(&priv->self->dev, "EC write: %04x, ret = %d\n", data, ret);
+	return ret;
+}
+
+static int asus_ec_read(struct asus_ec_data *priv, bool in_irq)
+{
+	int ret = i2c_smbus_read_i2c_block_data(priv->self, 0x6A,
+						sizeof(priv->ec_data),
+						priv->ec_data);
+
+	dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
+		sizeof(priv->ec_data), priv->ec_data,
+		ret, in_irq ? "; in irq" : "");
+
+	return ret;
+}
+
+int asus_ec_i2c_command(const struct asusec_info *ec, u16 data)
+{
+	return asus_ec_write(to_ec_data(ec), data);
+}
+EXPORT_SYMBOL_GPL(asus_ec_i2c_command);
+
+static void asus_ec_clear_buffer(struct asus_ec_data *priv)
+{
+	int retry = ASUSEC_RSP_BUFFER_SIZE;
+
+	while (retry--) {
+		if (asus_ec_read(priv, false) < 0)
+			continue;
+
+		if (priv->ec_data[1] & ASUSEC_OBF_MASK)
+			continue;
+
+		break;
+	}
+}
+
+static int asus_ec_log_info(struct asus_ec_data *priv, unsigned int reg,
+			    const char *name, char **out)
+{
+	char buf[DOCKRAM_ENTRY_BUFSIZE];
+	int ret;
+
+	ret = asus_dockram_read(priv->info.dockram, reg, buf);
+	if (ret < 0)
+		return ret;
+
+	if (!priv->logging_disabled)
+		dev_info(&priv->self->dev, "%-14s: %.*s\n", name, buf[0], buf + 1);
+
+	if (out)
+		*out = kstrndup(buf + 1, buf[0], GFP_KERNEL);
+
+	return 0;
+}
+
+static int asus_ec_reset(struct asus_ec_data *priv)
+{
+	int retry, ret;
+
+	for (retry = 0; retry < 3; retry++) {
+		ret = asus_ec_write(priv, 0);
+		if (!ret)
+			return 0;
+
+		msleep(300);
+	}
+
+	return ret;
+}
+
+static int asus_ec_magic_debug(struct asus_ec_data *priv)
+{
+	u64 flag;
+	int ret;
+
+	ret = asus_ec_get_ctl(&priv->info, &flag);
+	if (ret < 0)
+		return ret;
+
+	flag &= ASUSEC_CTL_SUSB_MODE;
+	dev_info(&priv->self->dev, "EC FW behaviour: %s\n",
+		 flag ? "susb on when receive ec_req" : "susb on when system wakeup");
+
+	return 0;
+}
+
+static int asus_ec_set_factory_mode(struct asus_ec_data *priv, bool on)
+{
+	dev_info(&priv->self->dev, "Entering %s mode.\n", on ? "factory" : "normal");
+	return asus_ec_update_ctl(&priv->info, ASUSEC_CTL_FACTORY_MODE,
+				 on ? ASUSEC_CTL_FACTORY_MODE : 0);
+}
+
+static void asus_ec_handle_smi(struct asus_ec_data *priv, unsigned int code);
+
+static irqreturn_t asus_ec_interrupt(int irq, void *dev_id)
+{
+	struct asus_ec_data *priv = dev_id;
+	unsigned long notify_action;
+	int ret;
+
+	ret = asus_ec_read(priv, true);
+	if (ret <= 0 || !(priv->ec_data[1] & ASUSEC_OBF_MASK))
+		return IRQ_NONE;
+
+	notify_action = priv->ec_data[1];
+	if (notify_action & ASUSEC_SMI_MASK) {
+		unsigned int code = priv->ec_data[2];
+
+		asus_ec_handle_smi(priv, code);
+
+		notify_action |= code << 8;
+		dev_dbg(&priv->self->dev, "SMI code: 0x%02x\n", code);
+	}
+
+	blocking_notifier_call_chain(&priv->info.notify_list,
+				     notify_action, priv->ec_data);
+
+	return IRQ_HANDLED;
+}
+
+static int asus_ec_detect(struct asus_ec_data *priv)
+{
+	char *model = NULL;
+	int ret;
+
+	ret = asus_ec_reset(priv);
+	if (ret)
+		goto err_exit;
+
+	asus_ec_clear_buffer(priv);
+
+	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_MODEL, "model", &model);
+	if (ret)
+		goto err_exit;
+
+	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_FW, "FW version", NULL);
+	if (ret)
+		goto err_exit;
+
+	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_CFGFMT, "Config format", NULL);
+	if (ret)
+		goto err_exit;
+
+	ret = asus_ec_log_info(priv, ASUSEC_DOCKRAM_INFO_HW, "HW version", NULL);
+	if (ret)
+		goto err_exit;
+
+	priv->logging_disabled = true;
+
+	ret = asus_ec_magic_debug(priv);
+	if (ret)
+		goto err_exit;
+
+	priv->info.model = model;
+	priv->info.name = priv->data->name;
+
+	if (device_property_read_bool(&priv->self->dev, "asus,clear-factory-mode"))
+		asus_ec_set_factory_mode(priv, false);
+
+err_exit:
+	if (ret)
+		dev_err(&priv->self->dev, "failed to access EC: %d\n", ret);
+
+	return ret;
+}
+
+static void asus_ec_handle_smi(struct asus_ec_data *priv, unsigned int code)
+{
+	dev_dbg(&priv->self->dev, "SMI interrupt: 0x%02x\n", code);
+
+	switch (code) {
+	case ASUSEC_SMI_HANDSHAKE:
+	case ASUSEC_SMI_RESET:
+		asus_ec_detect(priv);
+		break;
+	}
+}
+
+static ssize_t ec_request_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	struct asusec_info *ec = dev_get_drvdata(dev);
+
+	asus_ec_signal_request(ec);
+
+	return count;
+}
+
+static ssize_t ec_irq_store(struct device *dev,
+			    struct device_attribute *attr,
+			    const char *buf, size_t count)
+{
+	struct asusec_info *ec = dev_get_drvdata(dev);
+	struct asus_ec_data *priv = to_ec_data(ec);
+
+	irq_wake_thread(priv->self->irq, priv);
+
+	return count;
+}
+
+static DEVICE_ATTR_WO(ec_request);
+static DEVICE_ATTR_WO(ec_irq);
+
+static struct attribute *asus_ec_attributes[] = {
+	&dev_attr_ec_request.attr,
+	&dev_attr_ec_irq.attr,
+	NULL
+};
+
+static const struct attribute_group asus_ec_attr_group = {
+	.attrs = asus_ec_attributes,
+};
+
+static void asus_ec_sysfs_release(void *data)
+{
+	struct i2c_client *client = data;
+
+	sysfs_remove_link(&client->dev.kobj, "dockram");
+}
+
+static int asus_ec_probe(struct i2c_client *client)
+{
+	struct asus_ec_data *priv;
+	int ret;
+
+	priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->data = device_get_match_data(&client->dev);
+	if (!priv->data)
+		return -ENODEV;
+
+	i2c_set_clientdata(client, priv);
+	priv->self = client;
+
+	priv->info.dockram = devm_asus_dockram_get(&client->dev);
+	if (IS_ERR(priv->info.dockram))
+		return dev_err_probe(&client->dev, PTR_ERR(priv->info.dockram),
+				     "failed to get dockram\n");
+
+	priv->ecreq = devm_gpiod_get(&client->dev, "request", GPIOD_OUT_LOW);
+	if (IS_ERR(priv->ecreq))
+		return dev_err_probe(&client->dev, PTR_ERR(priv->ecreq),
+				     "failed to get request GPIO\n");
+
+	BLOCKING_INIT_NOTIFIER_HEAD(&priv->info.notify_list);
+	mutex_init(&priv->ecreq_lock);
+
+	ret = devm_device_add_group(&client->dev, &asus_ec_attr_group);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "failed to create sysfs attributes\n");
+
+	ret = sysfs_create_link(&client->dev.kobj,
+				&priv->info.dockram->dev.kobj,
+				"dockram");
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "failed to create sysfs link 'dockram'\n");
+
+	ret = devm_add_action_or_reset(&client->dev, asus_ec_sysfs_release,
+				       client);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "failed to register sysfs release\n");
+
+	asus_ec_signal_request(&priv->info);
+
+	ret = asus_ec_detect(priv);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "failed to detect EC version\n");
+
+	ret = devm_request_threaded_irq(&client->dev, client->irq,
+					NULL, &asus_ec_interrupt,
+					IRQF_ONESHOT | IRQF_SHARED,
+					client->name, priv);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "failed to register IRQ\n");
+
+	/* Parent I2C controller uses DMA, ASUS EC and child devices do not */
+	client->dev.coherent_dma_mask = 0;
+	client->dev.dma_mask = &client->dev.coherent_dma_mask;
+
+	return devm_mfd_add_devices(&client->dev, 0, priv->data->mfd_devices,
+				    priv->data->num_devices, NULL, 0, NULL);
+}
+
+static const struct mfd_cell asus_ec_pad_mfd_devices[] = {
+	{
+		.name = "asus-ec-battery",
+		.id = 0,
+		.of_compatible = "asus,ec-battery",
+	}, {
+		.name = "asus-ec-charger",
+		.id = 0,
+		.of_compatible = "asus,ec-charger",
+	}, {
+		.name = "asus-ec-led",
+		.id = 0,
+		.of_compatible = "asus,ec-led",
+	},
+};
+
+static const struct mfd_cell asus_ec_dock_mfd_devices[] = {
+	{
+		.name = "asus-ec-battery",
+		.id = 1,
+		.of_compatible = "asus,ec-battery",
+	}, {
+		.name = "asus-ec-charger",
+		.id = 1,
+		.of_compatible = "asus,ec-charger",
+	}, {
+		.name = "asus-ec-led",
+		.id = 1,
+		.of_compatible = "asus,ec-led",
+	}, {
+		.name = "asus-ec-keys",
+		.of_compatible = "asus,ec-keys",
+	}, {
+		.name = "asus-ec-kbc",
+		.of_compatible = "asus,ec-kbc",
+	},
+};
+
+static const struct asus_ec_chip_data asus_ec_pad_data = {
+	.name = "pad",
+	.mfd_devices = asus_ec_pad_mfd_devices,
+	.num_devices = ARRAY_SIZE(asus_ec_pad_mfd_devices),
+};
+
+static const struct asus_ec_chip_data asus_ec_dock_data = {
+	.name = "dock",
+	.mfd_devices = asus_ec_dock_mfd_devices,
+	.num_devices = ARRAY_SIZE(asus_ec_dock_mfd_devices),
+};
+
+static const struct of_device_id asus_ec_match[] = {
+	{ .compatible = "asus,ec-pad", .data = &asus_ec_pad_data },
+	{ .compatible = "asus,ec-dock", .data = &asus_ec_dock_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_match);
+
+static struct i2c_driver asus_ec_driver = {
+	.driver	= {
+		.name = "asus-ec",
+		.of_match_table = asus_ec_match,
+	},
+	.probe = asus_ec_probe,
+};
+module_i2c_driver(asus_ec_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>");
+MODULE_DESCRIPTION("ASUS Transformer's EC driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/asus-ec.h b/include/linux/mfd/asus-ec.h
index bc4efa37f5ba..11b16295afcd 100644
--- a/include/linux/mfd/asus-ec.h
+++ b/include/linux/mfd/asus-ec.h
@@ -2,8 +2,76 @@
 #ifndef __MISC_ASUS_EC_H
 #define __MISC_ASUS_EC_H
 
+#include <linux/notifier.h>
+#include <linux/platform_device.h>
+#include <linux/workqueue.h>
+
 struct i2c_client;
 
+struct asusec_info {
+	const char *model;
+	const char *name;
+	struct i2c_client *dockram;
+	struct workqueue_struct *wq;
+	struct blocking_notifier_head notify_list;
+};
+
+#define DOCKRAM_ENTRIES			0x100
+#define DOCKRAM_ENTRY_SIZE		32
+#define DOCKRAM_ENTRY_BUFSIZE		(DOCKRAM_ENTRY_SIZE + 1)
+
+/* interrupt sources */
+#define ASUSEC_OBF_MASK			BIT(0)
+#define ASUSEC_KEY_MASK			BIT(2)
+#define ASUSEC_KBC_MASK			BIT(3)
+#define ASUSEC_AUX_MASK			BIT(5)
+#define ASUSEC_SCI_MASK			BIT(6)
+#define ASUSEC_SMI_MASK			BIT(7)
+
+/* SMI notification codes */
+#define ASUSEC_SMI_POWER_NOTIFY		0x31	/* [un]plugging USB cable */
+#define ASUSEC_SMI_HANDSHAKE		0x50	/* response to ec_req edge */
+#define ASUSEC_SMI_WAKE			0x53
+#define ASUSEC_SMI_RESET		0x5f
+#define ASUSEC_SMI_ADAPTER_EVENT	0x60	/* [un]plugging charger to dock */
+#define ASUSEC_SMI_BACKLIGHT_ON		0x63
+#define ASUSEC_SMI_AUDIO_DOCK_IN	0x70
+
+#define ASUSEC_SMI_ACTION(code)		(ASUSEC_SMI_MASK | ASUSEC_OBF_MASK | \
+					(ASUSEC_SMI_##code << 8))
+
+/* control register [0x0A] layout */
+#define ASUSEC_CTL_SIZE			8
+
+/*
+ * EC reports power from 40-pin connector in the LSB of the control
+ * register.  The following values have been observed (xor 0x02):
+ *
+ * PAD-ec no-plug  0x40 / PAD-ec DOCK     0x20 / DOCK-ec no-plug 0x40
+ * PAD-ec AC       0x25 / PAD-ec DOCK+AC  0x24 / DOCK-ec AC      0x25
+ * PAD-ec USB      0x45 / PAD-ec DOCK+USB 0x24 / DOCK-ec USB     0x41
+ */
+
+#define ASUSEC_CTL_DIRECT_POWER_SOURCE	BIT_ULL(0)
+#define ASUSEC_STAT_CHARGING		BIT_ULL(2)
+#define ASUSEC_CTL_FULL_POWER_SOURCE	BIT_ULL(5)
+#define ASUSEC_CTL_SUSB_MODE		BIT_ULL(11)
+#define ASUSEC_CMD_SUSPEND_S3		BIT_ULL(41)
+#define ASUSEC_CTL_TEST_DISCHARGE	BIT_ULL(43)
+#define ASUSEC_CMD_SUSPEND_INHIBIT	BIT_ULL(45)
+#define ASUSEC_CTL_FACTORY_MODE		BIT_ULL(46)
+#define ASUSEC_CTL_KEEP_AWAKE		BIT_ULL(47)
+#define ASUSEC_CTL_USB_CHARGE		BIT_ULL(50)
+#define ASUSEC_CMD_SWITCH_HDMI		BIT_ULL(70)
+#define ASUSEC_CMD_WIN_SHUTDOWN		BIT_ULL(76)
+
+#define ASUSEC_DOCKRAM_INFO_MODEL	0x01
+#define ASUSEC_DOCKRAM_INFO_FW		0x02
+#define ASUSEC_DOCKRAM_INFO_CFGFMT	0x03
+#define ASUSEC_DOCKRAM_INFO_HW		0x04
+#define ASUSEC_DOCKRAM_CONTROL		0x0a
+#define ASUSEC_DOCKRAM_BATT_CTL		0x14
+
 /* dockram comm */
 int asus_dockram_read(struct i2c_client *client, int reg, char *buf);
 int asus_dockram_write(struct i2c_client *client, int reg, const char *buf);
@@ -11,8 +79,35 @@ int asus_dockram_access_ctl(struct i2c_client *client,
 			    u64 *out, u64 mask, u64 xor);
 struct i2c_client *devm_asus_dockram_get(struct device *parent);
 
-#define DOCKRAM_ENTRIES		0x100
-#define DOCKRAM_ENTRY_SIZE	32
-#define DOCKRAM_ENTRY_BUFSIZE	(DOCKRAM_ENTRY_SIZE + 1)
+/* EC public API */
+static inline struct asusec_info *cell_to_ec(struct platform_device *pdev)
+{
+	return dev_get_drvdata(pdev->dev.parent);
+}
+
+static inline int asus_ec_get_ctl(const struct asusec_info *ec, u64 *out)
+{
+	return asus_dockram_access_ctl(ec->dockram, out, 0, 0);
+}
+
+static inline int asus_ec_update_ctl(const struct asusec_info *ec,
+				     u64 mask, u64 xor)
+{
+	return asus_dockram_access_ctl(ec->dockram, NULL, mask, xor);
+}
+
+static inline int asus_ec_set_ctl_bits(const struct asusec_info *ec, u64 mask)
+{
+	return asus_dockram_access_ctl(ec->dockram, NULL, mask, mask);
+}
+
+static inline int asus_ec_clear_ctl_bits(const struct asusec_info *ec, u64 mask)
+{
+	return asus_dockram_access_ctl(ec->dockram, NULL, mask, 0);
+}
+
+int asus_ec_i2c_command(const struct asusec_info *ec, u16 data);
+int devm_asus_ec_register_notifier(struct platform_device *dev,
+				   struct notifier_block *nb);
 
 #endif /* __MISC_ASUS_EC_H */
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 5/9] input: serio: Add driver for Asus Transformer dock keyboard and touchpad
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Add input driver for Asus Transformer dock keyboard and touchpad.

Some keys in Asus Dock report keycodes that don't make sense according to
their position, this patch modifies the incoming data that is sent to
serio to send proper scancodes.

Co-developed-by: Ion Agorria <ion@agorria.com>
Signed-off-by: Ion Agorria <ion@agorria.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 drivers/input/serio/Kconfig       |  15 +++
 drivers/input/serio/Makefile      |   1 +
 drivers/input/serio/asus-ec-kbc.c | 162 ++++++++++++++++++++++++++++++
 3 files changed, 178 insertions(+)
 create mode 100644 drivers/input/serio/asus-ec-kbc.c

diff --git a/drivers/input/serio/Kconfig b/drivers/input/serio/Kconfig
index c7ef347a4dff..c4e17dcfc98a 100644
--- a/drivers/input/serio/Kconfig
+++ b/drivers/input/serio/Kconfig
@@ -97,6 +97,21 @@ config SERIO_RPCKBD
 	  To compile this driver as a module, choose M here: the
 	  module will be called rpckbd.
 
+config SERIO_ASUSEC
+	tristate "Asus Transformer's Dock keyboard and touchpad controller"
+	depends on MFD_ASUSEC
+	help
+	  Say Y here if you want to use the keyboard and/or touchpad on
+	  Asus Transformed's Mobile Dock.
+
+	  For keyboard support you also need atkbd driver.
+
+	  For touchpad support you also need psmouse driver with Elantech
+	  touchpad option enabled.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called asusec-kbc.
+
 config SERIO_AMBAKMI
 	tristate "AMBA KMI keyboard controller"
 	depends on ARM_AMBA
diff --git a/drivers/input/serio/Makefile b/drivers/input/serio/Makefile
index 6d97bad7b844..444e3ea70e37 100644
--- a/drivers/input/serio/Makefile
+++ b/drivers/input/serio/Makefile
@@ -13,6 +13,7 @@ obj-$(CONFIG_SERIO_CT82C710)	+= ct82c710.o
 obj-$(CONFIG_SERIO_RPCKBD)	+= rpckbd.o
 obj-$(CONFIG_SERIO_SA1111)	+= sa1111ps2.o
 obj-$(CONFIG_SERIO_AMBAKMI)	+= ambakmi.o
+obj-$(CONFIG_SERIO_ASUSEC)	+= asus-ec-kbc.o
 obj-$(CONFIG_SERIO_Q40KBD)	+= q40kbd.o
 obj-$(CONFIG_SERIO_GSCPS2)	+= gscps2.o
 obj-$(CONFIG_HP_SDC)		+= hp_sdc.o
diff --git a/drivers/input/serio/asus-ec-kbc.c b/drivers/input/serio/asus-ec-kbc.c
new file mode 100644
index 000000000000..796ce0de38c8
--- /dev/null
+++ b/drivers/input/serio/asus-ec-kbc.c
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS EC - keyboard and touchpad
+ *
+ */
+
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/i8042.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/serio.h>
+
+struct asus_ec_kbc_data {
+	struct notifier_block nb;
+	struct asusec_info *ec;
+	struct serio *sdev[2];
+};
+
+static int asus_ec_kbc_notify(struct notifier_block *nb,
+			      unsigned long action, void *data_)
+{
+	struct asus_ec_kbc_data *priv = container_of(nb, struct asus_ec_kbc_data, nb);
+	unsigned int port_idx, n;
+	u8 *data = data_;
+
+	if (action & (ASUSEC_SMI_MASK | ASUSEC_SCI_MASK))
+		return NOTIFY_DONE;
+	else if (action & ASUSEC_AUX_MASK)
+		port_idx = 1;
+	else if (action & (ASUSEC_KBC_MASK | ASUSEC_KEY_MASK))
+		port_idx = 0;
+	else
+		return NOTIFY_DONE;
+
+	n = data[0] - 1;
+	data += 2;
+
+	/*
+	 * We need to replace these incoming data for keys:
+	 * RIGHT_META Press   0xE0 0x27      -> LEFT_ALT   Press   0x11
+	 * RIGHT_META Release 0xE0 0xF0 0x27 -> LEFT_ALT   Release 0xF0 0x11
+	 * COMPOSE    Press   0xE0 0x2F      -> RIGHT_META Press   0xE0 0x27
+	 * COMPOSE    Release 0xE0 0xF0 0x2F -> RIGHT_META Release 0xE0 0xF0 0x27
+	 */
+
+	if (port_idx == 0 && n >= 2 && data[0] == 0xE0) {
+		if (n == 3 && data[1] == 0xF0) {
+			switch (data[2]) {
+			case 0x27:
+				data[0] = 0xF0;
+				data[1] = 0x11;
+				n = 2;
+				break;
+			case 0x2F:
+				data[2] = 0x27;
+				break;
+			}
+		} else if (n == 2) {
+			switch (data[1]) {
+			case 0x27:
+				data[0] = 0x11;
+				n = 1;
+				break;
+			case 0x2F:
+				data[1] = 0x27;
+				break;
+			}
+		}
+	}
+
+	while (n--)
+		serio_interrupt(priv->sdev[port_idx], *data++, 0);
+
+	return NOTIFY_OK;
+}
+
+static int asus_ec_serio_write(struct serio *port, unsigned char data)
+{
+	const struct asusec_info *ec = port->port_data;
+
+	return asus_ec_i2c_command(ec, (data << 8) | port->id.extra);
+}
+
+static void asus_ec_serio_remove(void *data)
+{
+	serio_unregister_port(data);
+}
+
+static int asus_ec_register_serio(struct platform_device *pdev, int idx,
+				  const char *name, int cmd)
+{
+	struct asus_ec_kbc_data *priv = platform_get_drvdata(pdev);
+	struct i2c_client *parent = to_i2c_client(pdev->dev.parent);
+	struct serio *port = kzalloc(sizeof(*port), GFP_KERNEL);
+
+	if (!port)
+		dev_err_probe(&pdev->dev, -ENOMEM,
+			      "No memory for serio%d\n", idx);
+
+	priv->sdev[idx] = port;
+	port->dev.parent = &pdev->dev;
+	port->id.type = SERIO_8042;
+	port->id.extra = cmd & 0xFF;
+	port->write = asus_ec_serio_write;
+	port->port_data = (void *)priv->ec;
+	snprintf(port->name, sizeof(port->name), "%s %s",
+		 priv->ec->model, name);
+	snprintf(port->phys, sizeof(port->phys), "i2c-%u-%04x/serio%d",
+		 i2c_adapter_id(parent->adapter), parent->addr, idx);
+
+	serio_register_port(port);
+
+	return devm_add_action_or_reset(&pdev->dev, asus_ec_serio_remove, port);
+}
+
+static int asus_ec_kbc_probe(struct platform_device *pdev)
+{
+	struct asusec_info *ec = cell_to_ec(pdev);
+	struct asus_ec_kbc_data *priv;
+	int ret;
+
+	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+	priv->ec = ec;
+
+	ret = asus_ec_register_serio(pdev, 0, "Keyboard", 0);
+	if (ret < 0)
+		return ret;
+
+	ret = asus_ec_register_serio(pdev, 1, "Touchpad", I8042_CMD_AUX_SEND);
+	if (ret < 0)
+		return ret;
+
+	priv->nb.notifier_call = asus_ec_kbc_notify;
+
+	return devm_asus_ec_register_notifier(pdev, &priv->nb);
+}
+
+static const struct of_device_id asus_ec_kbc_match[] = {
+	{ .compatible = "asus,ec-kbc" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_kbc_match);
+
+static struct platform_driver asus_ec_kbc_driver = {
+	.driver = {
+		.name = "asus-ec-kbc",
+		.of_match_table = asus_ec_kbc_match,
+	},
+	.probe = asus_ec_kbc_probe,
+};
+module_platform_driver(asus_ec_kbc_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_DESCRIPTION("ASUS Transformer's Dock keyboard and touchpad controller driver");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 6/9] input: keyboard: Add driver for Asus Transformer dock multimedia keys
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Add support for multimedia top button row of ASUS Transformer's Mobile
Dock keyboard. Driver is made that function keys (F1-F12) are used by
default which suits average Linux use better and with pressing
ScreenLock + AltGr function keys layout is switched to multimedia keys.
Since this only modifies codes sent by asus-ec-keys it doesn't affect
normal keyboards at all.

Co-developed-by: Ion Agorria <ion@agorria.com>
Signed-off-by: Ion Agorria <ion@agorria.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 drivers/input/keyboard/Kconfig        |  10 +
 drivers/input/keyboard/Makefile       |   1 +
 drivers/input/keyboard/asus-ec-keys.c | 285 ++++++++++++++++++++++++++
 3 files changed, 296 insertions(+)
 create mode 100644 drivers/input/keyboard/asus-ec-keys.c

diff --git a/drivers/input/keyboard/Kconfig b/drivers/input/keyboard/Kconfig
index 2ff4fef322c2..9eef1ff279c3 100644
--- a/drivers/input/keyboard/Kconfig
+++ b/drivers/input/keyboard/Kconfig
@@ -89,6 +89,16 @@ config KEYBOARD_APPLESPI
 	  To compile this driver as a module, choose M here: the
 	  module will be called applespi.
 
+config KEYBOARD_ASUSEC
+	tristate "Asus Transformer Mobile Dock multimedia keys"
+	depends on MFD_ASUSEC
+	help
+	  Say Y here if you want to use multimedia keys present on Asus
+	  Transformer's Mobile Dock.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called asusec-ext-keys.
+
 config KEYBOARD_ATARI
 	tristate "Atari keyboard"
 	depends on ATARI
diff --git a/drivers/input/keyboard/Makefile b/drivers/input/keyboard/Makefile
index 2d906e14f3e2..7226aafddf7a 100644
--- a/drivers/input/keyboard/Makefile
+++ b/drivers/input/keyboard/Makefile
@@ -11,6 +11,7 @@ obj-$(CONFIG_KEYBOARD_ADP5585)		+= adp5585-keys.o
 obj-$(CONFIG_KEYBOARD_ADP5588)		+= adp5588-keys.o
 obj-$(CONFIG_KEYBOARD_AMIGA)		+= amikbd.o
 obj-$(CONFIG_KEYBOARD_APPLESPI)		+= applespi.o
+obj-$(CONFIG_KEYBOARD_ASUSEC)		+= asus-ec-keys.o
 obj-$(CONFIG_KEYBOARD_ATARI)		+= atakbd.o
 obj-$(CONFIG_KEYBOARD_ATKBD)		+= atkbd.o
 obj-$(CONFIG_KEYBOARD_BCM)		+= bcm-keypad.o
diff --git a/drivers/input/keyboard/asus-ec-keys.c b/drivers/input/keyboard/asus-ec-keys.c
new file mode 100644
index 000000000000..42365db63bdf
--- /dev/null
+++ b/drivers/input/keyboard/asus-ec-keys.c
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS Transformer Pad - multimedia keys
+ */
+
+#include <linux/array_size.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+
+#define ASUSEC_EXT_KEY_CODES		0x20
+
+struct asus_ec_keys_data {
+	struct notifier_block nb;
+	struct asusec_info *ec;
+	struct input_dev *xidev;
+	bool special_key_pressed;
+	bool special_key_mode;
+	unsigned short keymap[ASUSEC_EXT_KEY_CODES * 2];
+};
+
+static void asus_ec_input_event(struct input_handle *handle,
+				unsigned int event_type,
+				unsigned int event_code, int value)
+{
+	struct asus_ec_keys_data *priv = handle->handler->private;
+
+	/* Store special key state */
+	if (event_type == EV_KEY && event_code == KEY_RIGHTALT)
+		priv->special_key_pressed = !!value;
+}
+
+static int asus_ec_input_connect(struct input_handler *handler, struct input_dev *dev,
+				 const struct input_device_id *id)
+{
+	struct input_handle *handle;
+	int error;
+
+	handle = kzalloc(sizeof(*handle), GFP_KERNEL);
+	if (!handle)
+		return -ENOMEM;
+
+	handle->dev = dev;
+	handle->handler = handler;
+	handle->name = "asusec-media-handler";
+
+	error = input_register_handle(handle);
+	if (error)
+		goto err_free_handle;
+
+	error = input_open_device(handle);
+	if (error)
+		goto err_unregister_handle;
+
+	return 0;
+
+ err_unregister_handle:
+	input_unregister_handle(handle);
+ err_free_handle:
+	kfree(handle);
+
+	return error;
+}
+
+static void asus_ec_input_disconnect(struct input_handle *handle)
+{
+	input_close_device(handle);
+	input_unregister_handle(handle);
+	kfree(handle);
+}
+
+static const struct input_device_id asus_ec_input_ids[] = {
+	{
+		.flags = INPUT_DEVICE_ID_MATCH_EVBIT,
+		.evbit = { BIT_MASK(EV_KEY) },
+	},
+	{ }
+};
+
+static struct input_handler asus_ec_input_handler = {
+	.name =	"asusec-media-handler",
+	.event = asus_ec_input_event,
+	.connect = asus_ec_input_connect,
+	.disconnect = asus_ec_input_disconnect,
+	.id_table = asus_ec_input_ids,
+};
+
+static const unsigned short asus_ec_dock_ext_keys[] = {
+	/* Function keys [0x00 - 0x19] */
+	[0x01] = KEY_DELETE,
+	[0x02] = KEY_F1,
+	[0x03] = KEY_F2,
+	[0x04] = KEY_F3,
+	[0x05] = KEY_F4,
+	[0x06] = KEY_F5,
+	[0x07] = KEY_F6,
+	[0x08] = KEY_F7,
+	[0x10] = KEY_F8,
+	[0x11] = KEY_F9,
+	[0x12] = KEY_F10,
+	[0x13] = KEY_F11,
+	[0x14] = KEY_F12,
+	[0x15] = KEY_MUTE,
+	[0x16] = KEY_VOLUMEDOWN,
+	[0x17] = KEY_VOLUMEUP,
+	/* Multimedia keys [0x20 - 0x39] */
+	[0x21] = KEY_SCREENLOCK,
+	[0x22] = KEY_WLAN,
+	[0x23] = KEY_BLUETOOTH,
+	[0x24] = KEY_TOUCHPAD_TOGGLE,
+	[0x25] = KEY_BRIGHTNESSDOWN,
+	[0x26] = KEY_BRIGHTNESSUP,
+	[0x27] = KEY_BRIGHTNESS_AUTO,
+	[0x28] = KEY_PRINT,
+	[0x30] = KEY_WWW,
+	[0x31] = KEY_CONFIG,
+	[0x32] = KEY_PREVIOUSSONG,
+	[0x33] = KEY_PLAYPAUSE,
+	[0x34] = KEY_NEXTSONG,
+	[0x35] = KEY_MUTE,
+	[0x36] = KEY_VOLUMEDOWN,
+	[0x37] = KEY_VOLUMEUP,
+};
+
+static void asus_ec_keys_report_key(struct input_dev *dev, unsigned int code,
+				    unsigned int key, bool value)
+{
+	input_event(dev, EV_MSC, MSC_SCAN, code);
+	input_report_key(dev, key, value);
+	input_sync(dev);
+}
+
+static int asus_ec_keys_process_key(struct input_dev *dev, u8 code)
+{
+	struct asus_ec_keys_data *priv = dev_get_drvdata(dev->dev.parent);
+	unsigned int key = 0;
+
+	if (code == 0)
+		return NOTIFY_DONE;
+
+	/* Flip special key mode state when pressing key 1 with special key pressed */
+	if (priv->special_key_pressed && code == 1) {
+		priv->special_key_mode = !priv->special_key_mode;
+		return NOTIFY_DONE;
+	}
+
+	/*
+	 * Relocate code to second "page" if pressed state XOR's mode state
+	 * This way special key will invert the current mode
+	 */
+	if (priv->special_key_mode ^ priv->special_key_pressed)
+		code += ASUSEC_EXT_KEY_CODES;
+
+	if (code < dev->keycodemax) {
+		unsigned short *map = dev->keycode;
+
+		key = map[code];
+	}
+
+	if (!key)
+		key = KEY_UNKNOWN;
+
+	asus_ec_keys_report_key(dev, code, key, 1);
+	asus_ec_keys_report_key(dev, code, key, 0);
+
+	return NOTIFY_OK;
+}
+
+static int asus_ec_keys_notify(struct notifier_block *nb,
+			       unsigned long action, void *data_)
+{
+	struct asus_ec_keys_data *priv = container_of(nb, struct asus_ec_keys_data, nb);
+	u8 *data = data_;
+
+	if (action & ASUSEC_SMI_MASK)
+		return NOTIFY_DONE;
+
+	if (action & ASUSEC_SCI_MASK)
+		return asus_ec_keys_process_key(priv->xidev, data[2]);
+
+	return NOTIFY_DONE;
+}
+
+static void asus_ec_keys_setup_keymap(struct asus_ec_keys_data *priv)
+{
+	struct input_dev *dev = priv->xidev;
+	unsigned int i;
+
+	BUILD_BUG_ON(ARRAY_SIZE(priv->keymap) < ARRAY_SIZE(asus_ec_dock_ext_keys));
+
+	dev->keycode = priv->keymap;
+	dev->keycodesize = sizeof(*priv->keymap);
+	dev->keycodemax = ARRAY_SIZE(priv->keymap);
+
+	input_set_capability(dev, EV_MSC, MSC_SCAN);
+	input_set_capability(dev, EV_KEY, KEY_UNKNOWN);
+
+	for (i = 0; i < ARRAY_SIZE(asus_ec_dock_ext_keys); i++) {
+		unsigned int code = asus_ec_dock_ext_keys[i];
+
+		if (!code)
+			continue;
+
+		__set_bit(code, dev->keybit);
+		priv->keymap[i] = code;
+	}
+}
+
+static void asus_ec_input_handler_deregister(void *priv)
+{
+	input_unregister_handler(&asus_ec_input_handler);
+}
+
+static int asus_ec_keys_probe(struct platform_device *pdev)
+{
+	struct asusec_info *ec = cell_to_ec(pdev);
+	struct i2c_client *parent = to_i2c_client(pdev->dev.parent);
+	struct asus_ec_keys_data *priv;
+	int ret;
+
+	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+	priv->ec = ec;
+
+	priv->xidev = devm_input_allocate_device(&pdev->dev);
+	if (!priv->xidev)
+		return -ENOMEM;
+
+	priv->xidev->name = devm_kasprintf(&pdev->dev, GFP_KERNEL,
+					   "%s Keyboard Ext", ec->model);
+	priv->xidev->phys = devm_kasprintf(&pdev->dev, GFP_KERNEL,
+					   "i2c-%u-%04x",
+					   i2c_adapter_id(parent->adapter),
+					   parent->addr);
+	asus_ec_keys_setup_keymap(priv);
+
+	ret = input_register_device(priv->xidev);
+	if (ret < 0) {
+		dev_err(&pdev->dev, "failed to register extension keys: %d\n",
+			ret);
+		return ret;
+	}
+
+	asus_ec_input_handler.private = priv;
+
+	ret = input_register_handler(&asus_ec_input_handler);
+	if (ret)
+		return ret;
+
+	ret = devm_add_action_or_reset(&pdev->dev, asus_ec_input_handler_deregister,
+				       priv);
+	if (ret)
+		return ret;
+
+	priv->nb.notifier_call = asus_ec_keys_notify;
+
+	return devm_asus_ec_register_notifier(pdev, &priv->nb);
+}
+
+static const struct of_device_id asus_ec_keys_match[] = {
+	{ .compatible = "asus,ec-keys" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_keys_match);
+
+static struct platform_driver asus_ec_keys_driver = {
+	.driver = {
+		.name = "asus-ec-keys",
+		.of_match_table = asus_ec_keys_match,
+	},
+	.probe = asus_ec_keys_probe,
+};
+module_platform_driver(asus_ec_keys_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_DESCRIPTION("ASUS Transformer's multimedia keys driver");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 7/9] leds: Add driver for Asus Transformer LEDs
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Asus Transformer tablets have a green and an amber LED on both the pad
and the dock. If both LEDs are enabled simultaneously, the emitted light
will be yellow.

Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
---
 drivers/leds/Kconfig        |  11 ++++
 drivers/leds/Makefile       |   1 +
 drivers/leds/leds-asus-ec.c | 106 ++++++++++++++++++++++++++++++++++++
 3 files changed, 118 insertions(+)
 create mode 100644 drivers/leds/leds-asus-ec.c

diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 597d7a79c988..96dab210f6ca 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -120,6 +120,17 @@ config LEDS_OSRAM_AMS_AS3668
 	  To compile this driver as a module, choose M here: the module
 	  will be called leds-as3668.
 
+config LEDS_ASUSEC
+	tristate "LED Support for Asus Transformer charging LED"
+	depends on LEDS_CLASS
+	depends on MFD_ASUSEC
+	help
+	  This option enables support for charging indicator on
+	  Asus Transformer's Pad and it's Dock.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called leds-asus-ec.
+
 config LEDS_AW200XX
 	tristate "LED support for Awinic AW20036/AW20054/AW20072/AW20108"
 	depends on LEDS_CLASS
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 8fdb45d5b439..1117304dfdf4 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_LEDS_AN30259A)		+= leds-an30259a.o
 obj-$(CONFIG_LEDS_APU)			+= leds-apu.o
 obj-$(CONFIG_LEDS_ARIEL)		+= leds-ariel.o
 obj-$(CONFIG_LEDS_AS3668)		+= leds-as3668.o
+obj-$(CONFIG_LEDS_ASUSEC)		+= leds-asus-ec.o
 obj-$(CONFIG_LEDS_AW200XX)		+= leds-aw200xx.o
 obj-$(CONFIG_LEDS_AW2013)		+= leds-aw2013.o
 obj-$(CONFIG_LEDS_BCM6328)		+= leds-bcm6328.o
diff --git a/drivers/leds/leds-asus-ec.c b/drivers/leds/leds-asus-ec.c
new file mode 100644
index 000000000000..2fae62ddb936
--- /dev/null
+++ b/drivers/leds/leds-asus-ec.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ASUS EC driver - battery LED
+ */
+
+#include <linux/err.h>
+#include <linux/leds.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+
+/*
+ * F[5] & 0x07
+ *  auto: brightness == 0
+ *  bit 0: blink / charger on
+ *  bit 1: amber on
+ *  bit 2: green on
+ */
+
+#define ASUSEC_CTL_LED_BLINK		BIT_ULL(40)
+#define ASUSEC_CTL_LED_AMBER		BIT_ULL(41)
+#define ASUSEC_CTL_LED_GREEN		BIT_ULL(42)
+
+static void asus_ec_led_set_brightness_amber(struct led_classdev *led,
+					     enum led_brightness brightness)
+{
+	const struct asusec_info *ec = dev_get_drvdata(led->dev->parent);
+
+	if (brightness)
+		asus_ec_set_ctl_bits(ec, ASUSEC_CTL_LED_AMBER);
+	else
+		asus_ec_clear_ctl_bits(ec, ASUSEC_CTL_LED_AMBER);
+}
+
+static void asus_ec_led_set_brightness_green(struct led_classdev *led,
+					     enum led_brightness brightness)
+{
+	const struct asusec_info *ec =
+				dev_get_drvdata(led->dev->parent);
+
+	if (brightness)
+		asus_ec_set_ctl_bits(ec, ASUSEC_CTL_LED_GREEN);
+	else
+		asus_ec_clear_ctl_bits(ec, ASUSEC_CTL_LED_GREEN);
+}
+
+static int asus_ec_led_probe(struct platform_device *pdev)
+{
+	struct asusec_info *ec = cell_to_ec(pdev);
+	struct led_classdev *amber_led, *green_led;
+	int ret;
+
+	platform_set_drvdata(pdev, ec);
+
+	amber_led = devm_kzalloc(&pdev->dev, sizeof(*amber_led), GFP_KERNEL);
+	if (!amber_led)
+		return -ENOMEM;
+
+	amber_led->name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s::amber", ec->name);
+	amber_led->max_brightness = 1;
+	amber_led->flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
+	amber_led->brightness_set = asus_ec_led_set_brightness_amber;
+
+	ret = devm_led_classdev_register(&pdev->dev, amber_led);
+	if (ret)
+		return dev_err_probe(&pdev->dev, ret,
+				     "failed to register amber LED\n");
+
+	green_led = devm_kzalloc(&pdev->dev, sizeof(*green_led), GFP_KERNEL);
+	if (!green_led)
+		return -ENOMEM;
+
+	green_led->name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s::green", ec->name);
+	green_led->max_brightness = 1;
+	green_led->flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
+	green_led->brightness_set = asus_ec_led_set_brightness_green;
+
+	ret = devm_led_classdev_register(&pdev->dev, green_led);
+	if (ret)
+		return dev_err_probe(&pdev->dev, ret,
+				     "failed to register green LED\n");
+
+	return 0;
+}
+
+static const struct of_device_id asus_ec_led_match[] = {
+	{ .compatible = "asus,ec-led" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_led_match);
+
+static struct platform_driver asus_ec_led_driver = {
+	.driver = {
+		.name = "asus-ec-led",
+		.of_match_table = asus_ec_led_match,
+	},
+	.probe = asus_ec_led_probe,
+};
+module_platform_driver(asus_ec_led_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>");
+MODULE_DESCRIPTION("ASUS Transformer's charging LED driver");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 8/9] power: supply: Add driver for Asus Transformer battery
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Driver implements one battery cell per EC controller and supports reading
of battery status for Asus Transformer's pad and mobile dock.

Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
---
 drivers/power/supply/Kconfig           |  11 +
 drivers/power/supply/Makefile          |   1 +
 drivers/power/supply/asus-ec-battery.c | 282 +++++++++++++++++++++++++
 3 files changed, 294 insertions(+)
 create mode 100644 drivers/power/supply/asus-ec-battery.c

diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index 81fadb0695a9..bcf6a23858be 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -122,6 +122,17 @@ config BATTERY_CHAGALL
 	  This driver can also be built as a module. If so, the module will be
 	  called chagall-battery.
 
+config BATTERY_ASUSEC
+	tristate "Asus Transformer's battery driver"
+	depends on MFD_ASUSEC
+	help
+	  Say Y here to enable support APM status emulation using
+	  battery class devices.
+
+	  This sub-driver supports battery cells found in Asus Transformer
+	  tablets and mobile docks and controlled by special embedded
+	  controller.
+
 config BATTERY_CPCAP
 	tristate "Motorola CPCAP PMIC battery driver"
 	depends on MFD_CPCAP && IIO
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index 41c400bbf022..0a2cbfa96ed9 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -22,6 +22,7 @@ obj-$(CONFIG_TEST_POWER)	+= test_power.o
 obj-$(CONFIG_BATTERY_88PM860X)	+= 88pm860x_battery.o
 obj-$(CONFIG_CHARGER_ADP5061)	+= adp5061.o
 obj-$(CONFIG_BATTERY_ACT8945A)	+= act8945a_charger.o
+obj-$(CONFIG_BATTERY_ASUSEC)	+= asus-ec-battery.o
 obj-$(CONFIG_BATTERY_AXP20X)	+= axp20x_battery.o
 obj-$(CONFIG_CHARGER_AXP20X)	+= axp20x_ac_power.o
 obj-$(CONFIG_BATTERY_CHAGALL)	+= chagall-battery.o
diff --git a/drivers/power/supply/asus-ec-battery.c b/drivers/power/supply/asus-ec-battery.c
new file mode 100644
index 000000000000..7e6cb6017339
--- /dev/null
+++ b/drivers/power/supply/asus-ec-battery.c
@@ -0,0 +1,282 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS EC driver - battery monitoring
+ */
+
+#include <linux/array_size.h>
+#include <linux/devm-helpers.h>
+#include <linux/err.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/power_supply.h>
+#include <linux/property.h>
+#include <linux/unaligned.h>
+
+#define ASUSEC_BATTERY_DATA_FRESH_MSEC		5000
+
+#define ASUSEC_BATTERY_DISCHARGING		0x40
+#define ASUSEC_BATTERY_FULL_CHARGED		0x20
+#define ASUSEC_BATTERY_NOT_CHARGING		0x10
+
+#define TEMP_CELSIUS_OFFSET			2731
+
+struct asus_ec_battery_data {
+	const struct asusec_info *ec;
+	struct power_supply *battery;
+	struct power_supply_desc psy_desc;
+	struct delayed_work poll_work;
+	struct mutex battery_lock; /* for data refresh */
+	unsigned long batt_data_ts;
+	int last_state;
+	u8 batt_data[DOCKRAM_ENTRY_BUFSIZE];
+};
+
+static int asus_ec_battery_refresh(struct asus_ec_battery_data *priv)
+{
+	int ret = 0;
+
+	guard(mutex)(&priv->battery_lock);
+
+	if (time_before(jiffies, priv->batt_data_ts))
+		return ret;
+
+	ret = asus_dockram_read(priv->ec->dockram, ASUSEC_DOCKRAM_BATT_CTL,
+				priv->batt_data);
+	if (ret < 0)
+		return ret;
+
+	priv->batt_data_ts = jiffies +
+		msecs_to_jiffies(ASUSEC_BATTERY_DATA_FRESH_MSEC);
+
+	return ret;
+}
+
+static enum power_supply_property asus_ec_battery_properties[] = {
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_VOLTAGE_MAX,
+	POWER_SUPPLY_PROP_CURRENT_MAX,
+	POWER_SUPPLY_PROP_TEMP,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW,
+	POWER_SUPPLY_PROP_CURRENT_NOW,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_CHARGE_NOW,
+	POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW,
+	POWER_SUPPLY_PROP_TIME_TO_FULL_NOW,
+	POWER_SUPPLY_PROP_PRESENT,
+};
+
+static const unsigned int asus_ec_battery_prop_offs[] = {
+	[POWER_SUPPLY_PROP_STATUS] = 1,
+	[POWER_SUPPLY_PROP_VOLTAGE_MAX] = 3,
+	[POWER_SUPPLY_PROP_CURRENT_MAX] = 5,
+	[POWER_SUPPLY_PROP_TEMP] = 7,
+	[POWER_SUPPLY_PROP_VOLTAGE_NOW] = 9,
+	[POWER_SUPPLY_PROP_CURRENT_NOW] = 11,
+	[POWER_SUPPLY_PROP_CAPACITY] = 13,
+	[POWER_SUPPLY_PROP_CHARGE_NOW] = 15,
+	[POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW] = 17,
+	[POWER_SUPPLY_PROP_TIME_TO_FULL_NOW] = 19,
+};
+
+static int asus_ec_battery_get_value(struct asus_ec_battery_data *priv,
+				     enum power_supply_property psp)
+{
+	int ret, offs;
+
+	if (psp >= ARRAY_SIZE(asus_ec_battery_prop_offs))
+		return -EINVAL;
+
+	offs = asus_ec_battery_prop_offs[psp];
+	if (!offs)
+		return -EINVAL;
+
+	ret = asus_ec_battery_refresh(priv);
+	if (ret < 0)
+		return ret;
+
+	if (offs >= priv->batt_data[0])
+		return -ENODATA;
+
+	return get_unaligned_le16(priv->batt_data + offs);
+}
+
+static int asus_ec_battery_get_property(struct power_supply *psy,
+					enum power_supply_property psp,
+					union power_supply_propval *val)
+{
+	struct asus_ec_battery_data *priv = power_supply_get_drvdata(psy);
+	int ret;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_PRESENT:
+		val->intval = 1;
+		break;
+
+	default:
+		ret = asus_ec_battery_get_value(priv, psp);
+		if (ret < 0)
+			return ret;
+
+		val->intval = (s16)ret;
+
+		switch (psp) {
+		case POWER_SUPPLY_PROP_STATUS:
+			if (ret & ASUSEC_BATTERY_FULL_CHARGED)
+				val->intval = POWER_SUPPLY_STATUS_FULL;
+			else if (ret & ASUSEC_BATTERY_NOT_CHARGING)
+				val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
+			else if (ret & ASUSEC_BATTERY_DISCHARGING)
+				val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+			else
+				val->intval = POWER_SUPPLY_STATUS_CHARGING;
+			break;
+
+		case POWER_SUPPLY_PROP_TEMP:
+			val->intval -= TEMP_CELSIUS_OFFSET;
+			break;
+
+		case POWER_SUPPLY_PROP_CHARGE_NOW:
+		case POWER_SUPPLY_PROP_CURRENT_NOW:
+		case POWER_SUPPLY_PROP_CURRENT_MAX:
+		case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+		case POWER_SUPPLY_PROP_VOLTAGE_MAX:
+			val->intval *= 1000;
+			break;
+
+		case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW:
+		case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW:
+			val->intval *= 60;
+			break;
+
+		default:
+			break;
+		}
+
+		break;
+	}
+
+	return 0;
+}
+
+static void asus_ec_battery_poll_work(struct work_struct *work)
+{
+	struct asus_ec_battery_data *priv =
+		container_of(work, struct asus_ec_battery_data, poll_work.work);
+	int state;
+
+	state = asus_ec_battery_get_value(priv, POWER_SUPPLY_PROP_STATUS);
+	if (state < 0)
+		return;
+
+	if (state & ASUSEC_BATTERY_FULL_CHARGED)
+		state = POWER_SUPPLY_STATUS_FULL;
+	else if (state & ASUSEC_BATTERY_DISCHARGING)
+		state = POWER_SUPPLY_STATUS_DISCHARGING;
+	else
+		state = POWER_SUPPLY_STATUS_CHARGING;
+
+	if (priv->last_state != state) {
+		priv->last_state = state;
+		power_supply_changed(priv->battery);
+	}
+
+	/* continuously send uevent notification */
+	schedule_delayed_work(&priv->poll_work,
+			      msecs_to_jiffies(ASUSEC_BATTERY_DATA_FRESH_MSEC));
+}
+
+static const struct power_supply_desc asus_ec_battery_desc = {
+	.name = "asus-ec-battery",
+	.type = POWER_SUPPLY_TYPE_BATTERY,
+	.properties = asus_ec_battery_properties,
+	.num_properties = ARRAY_SIZE(asus_ec_battery_properties),
+	.get_property = asus_ec_battery_get_property,
+	.external_power_changed = power_supply_changed,
+};
+
+static int asus_ec_battery_probe(struct platform_device *pdev)
+{
+	struct asus_ec_battery_data *priv;
+	struct power_supply_config cfg = { };
+	int ret;
+
+	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+
+	mutex_init(&priv->battery_lock);
+
+	priv->ec = cell_to_ec(pdev);
+	priv->batt_data_ts = jiffies - 1;
+	priv->last_state = POWER_SUPPLY_STATUS_UNKNOWN;
+
+	cfg.fwnode = dev_fwnode(&pdev->dev);
+	cfg.drv_data = priv;
+
+	memcpy(&priv->psy_desc, &asus_ec_battery_desc, sizeof(priv->psy_desc));
+	priv->psy_desc.name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s-battery",
+					     priv->ec->name);
+
+	priv->battery = devm_power_supply_register(&pdev->dev, &priv->psy_desc, &cfg);
+	if (IS_ERR(priv->battery))
+		return dev_err_probe(&pdev->dev, PTR_ERR(priv->battery),
+				     "Failed to register power supply\n");
+
+	ret = devm_delayed_work_autocancel(&pdev->dev, &priv->poll_work,
+					   asus_ec_battery_poll_work);
+	if (ret)
+		return ret;
+
+	schedule_delayed_work(&priv->poll_work,
+			      msecs_to_jiffies(ASUSEC_BATTERY_DATA_FRESH_MSEC));
+
+	return 0;
+}
+
+static int __maybe_unused asus_ec_battery_suspend(struct device *dev)
+{
+	struct asus_ec_battery_data *priv = dev_get_drvdata(dev);
+
+	cancel_delayed_work_sync(&priv->poll_work);
+
+	return 0;
+}
+
+static int __maybe_unused asus_ec_battery_resume(struct device *dev)
+{
+	struct asus_ec_battery_data *priv = dev_get_drvdata(dev);
+
+	schedule_delayed_work(&priv->poll_work,
+			      msecs_to_jiffies(ASUSEC_BATTERY_DATA_FRESH_MSEC));
+
+	return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(asus_ec_battery_pm_ops,
+			 asus_ec_battery_suspend, asus_ec_battery_resume);
+
+static const struct of_device_id asus_ec_battery_match[] = {
+	{ .compatible = "asus,ec-battery" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_battery_match);
+
+static struct platform_driver asus_ec_battery_driver = {
+	.driver = {
+		.name = "asus-ec-battery",
+		.of_match_table = asus_ec_battery_match,
+		.pm = &asus_ec_battery_pm_ops,
+	},
+	.probe = asus_ec_battery_probe,
+};
+module_platform_driver(asus_ec_battery_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>");
+MODULE_DESCRIPTION("ASUS Transformer's battery driver");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* [PATCH v1 9/9] power: supply: Add charger driver for Asus Transformers
From: Svyatoslav Ryhel @ 2026-02-01 10:43 UTC (permalink / raw)
  To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Svyatoslav Ryhel, Michał Mirosław,
	Ion Agorria
  Cc: devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-1-clamor95@gmail.com>

From: Michał Mirosław <mirq-linux@rere.qmqm.pl>

Add support for charger detection capabilities found in the embedded
controller of ASUS Transformer devices.

Suggested-by: Maxim Schwalm <maxim.schwalm@gmail.com>
Suggested-by: Svyatoslav Ryhel <clamor95@gmail.com>
Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
---
 drivers/power/supply/Kconfig           |  11 ++
 drivers/power/supply/Makefile          |   1 +
 drivers/power/supply/asus-ec-charger.c | 205 +++++++++++++++++++++++++
 3 files changed, 217 insertions(+)
 create mode 100644 drivers/power/supply/asus-ec-charger.c

diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index bcf6a23858be..6d7e115f5d2f 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -133,6 +133,17 @@ config BATTERY_ASUSEC
 	  tablets and mobile docks and controlled by special embedded
 	  controller.
 
+config CHARGER_ASUSEC
+	tristate "Asus Transformer's charger driver"
+	depends on MFD_ASUSEC
+	help
+	  Say Y here to enable support AC plug detection on Asus Transformer
+	  Dock.
+
+	  This sub-driver supports charger detection mechanism found in Asus
+	  Transformer tablets and mobile docks and controlled by special
+	  embedded controller.
+
 config BATTERY_CPCAP
 	tristate "Motorola CPCAP PMIC battery driver"
 	depends on MFD_CPCAP && IIO
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index 0a2cbfa96ed9..b93848227f50 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -23,6 +23,7 @@ obj-$(CONFIG_BATTERY_88PM860X)	+= 88pm860x_battery.o
 obj-$(CONFIG_CHARGER_ADP5061)	+= adp5061.o
 obj-$(CONFIG_BATTERY_ACT8945A)	+= act8945a_charger.o
 obj-$(CONFIG_BATTERY_ASUSEC)	+= asus-ec-battery.o
+obj-$(CONFIG_CHARGER_ASUSEC)	+= asus-ec-charger.o
 obj-$(CONFIG_BATTERY_AXP20X)	+= axp20x_battery.o
 obj-$(CONFIG_CHARGER_AXP20X)	+= axp20x_ac_power.o
 obj-$(CONFIG_BATTERY_CHAGALL)	+= chagall-battery.o
diff --git a/drivers/power/supply/asus-ec-charger.c b/drivers/power/supply/asus-ec-charger.c
new file mode 100644
index 000000000000..9d98e5014915
--- /dev/null
+++ b/drivers/power/supply/asus-ec-charger.c
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ASUS EC driver - charger monitoring
+ */
+
+#include <linux/err.h>
+#include <linux/mfd/asus-ec.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/power_supply.h>
+#include <linux/property.h>
+
+struct asus_ec_charger_data {
+	struct notifier_block nb;
+	const struct asusec_info *ec;
+	struct power_supply *psy;
+	struct power_supply_desc psy_desc;
+};
+
+static enum power_supply_property asus_ec_charger_properties[] = {
+	POWER_SUPPLY_PROP_USB_TYPE,
+	POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR,
+	POWER_SUPPLY_PROP_ONLINE,
+	POWER_SUPPLY_PROP_MODEL_NAME,
+};
+
+static int asus_ec_charger_get_property(struct power_supply *psy,
+					enum power_supply_property psp,
+					union power_supply_propval *val)
+{
+	struct asus_ec_charger_data *priv = power_supply_get_drvdata(psy);
+	enum power_supply_usb_type psu;
+	int ret;
+	u64 ctl;
+
+	ret = asus_ec_get_ctl(priv->ec, &ctl);
+	if (ret)
+		return ret;
+
+	switch (ctl & (ASUSEC_CTL_FULL_POWER_SOURCE | ASUSEC_CTL_DIRECT_POWER_SOURCE)) {
+	case ASUSEC_CTL_FULL_POWER_SOURCE:
+		psu = POWER_SUPPLY_USB_TYPE_CDP;	// DOCK
+		break;
+	case ASUSEC_CTL_DIRECT_POWER_SOURCE:
+		psu = POWER_SUPPLY_USB_TYPE_SDP;	// USB
+		break;
+	case 0:
+		psu = POWER_SUPPLY_USB_TYPE_UNKNOWN;	// no power source connected
+		break;
+	default:
+		psu = POWER_SUPPLY_USB_TYPE_ACA;	// power adapter
+		break;
+	}
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_ONLINE:
+		val->intval = psu != POWER_SUPPLY_USB_TYPE_UNKNOWN;
+		return 0;
+
+	case POWER_SUPPLY_PROP_USB_TYPE:
+		val->intval = psu;
+		return 0;
+
+	case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
+		if (ctl & ASUSEC_CTL_TEST_DISCHARGE)
+			val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE;
+		else if (ctl & ASUSEC_CTL_USB_CHARGE)
+			val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
+		else
+			val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
+		return 0;
+
+	case POWER_SUPPLY_PROP_MODEL_NAME:
+		val->strval = priv->ec->model;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int asus_ec_charger_set_property(struct power_supply *psy,
+					enum power_supply_property psp,
+					const union power_supply_propval *val)
+{
+	struct asus_ec_charger_data *priv = power_supply_get_drvdata(psy);
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
+		switch ((enum power_supply_charge_behaviour)val->intval) {
+		case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
+			return asus_ec_update_ctl(priv->ec,
+				ASUSEC_CTL_TEST_DISCHARGE | ASUSEC_CTL_USB_CHARGE,
+				ASUSEC_CTL_USB_CHARGE);
+
+		case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
+			return asus_ec_clear_ctl_bits(priv->ec,
+				ASUSEC_CTL_TEST_DISCHARGE | ASUSEC_CTL_USB_CHARGE);
+
+		case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE:
+			return asus_ec_update_ctl(priv->ec,
+				ASUSEC_CTL_TEST_DISCHARGE | ASUSEC_CTL_USB_CHARGE,
+				ASUSEC_CTL_TEST_DISCHARGE);
+		default:
+			return -EINVAL;
+		}
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int asus_ec_charger_property_is_writeable(struct power_supply *psy,
+						 enum power_supply_property psp)
+{
+	switch (psp) {
+	case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
+		return true;
+	default:
+		return false;
+	}
+}
+
+static const struct power_supply_desc asus_ec_charger_desc = {
+	.name = "asus-ec-charger",
+	.type = POWER_SUPPLY_TYPE_USB,
+	.charge_behaviours = BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) |
+			     BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE) |
+			     BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE),
+	.usb_types = BIT(POWER_SUPPLY_USB_TYPE_UNKNOWN) |
+		     BIT(POWER_SUPPLY_USB_TYPE_SDP) |
+		     BIT(POWER_SUPPLY_USB_TYPE_CDP) |
+		     BIT(POWER_SUPPLY_USB_TYPE_ACA),
+	.properties = asus_ec_charger_properties,
+	.num_properties = ARRAY_SIZE(asus_ec_charger_properties),
+	.get_property = asus_ec_charger_get_property,
+	.set_property = asus_ec_charger_set_property,
+	.property_is_writeable = asus_ec_charger_property_is_writeable,
+	.no_thermal = true,
+};
+
+static int asus_ec_charger_notify(struct notifier_block *nb,
+				  unsigned long action, void *data)
+{
+	struct asus_ec_charger_data *priv =
+		container_of(nb, struct asus_ec_charger_data, nb);
+
+	switch (action) {
+	case ASUSEC_SMI_ACTION(POWER_NOTIFY):
+	case ASUSEC_SMI_ACTION(ADAPTER_EVENT):
+		power_supply_changed(priv->psy);
+		break;
+	}
+
+	return NOTIFY_DONE;
+}
+
+static int asus_ec_charger_probe(struct platform_device *pdev)
+{
+	struct asus_ec_charger_data *priv;
+	struct power_supply_config cfg = { };
+
+	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+	priv->ec = cell_to_ec(pdev);
+
+	cfg.fwnode = dev_fwnode(&pdev->dev);
+	cfg.drv_data = priv;
+
+	memcpy(&priv->psy_desc, &asus_ec_charger_desc, sizeof(priv->psy_desc));
+	priv->psy_desc.name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s-charger",
+					     priv->ec->name);
+
+	priv->psy = devm_power_supply_register(&pdev->dev, &priv->psy_desc, &cfg);
+	if (IS_ERR(priv->psy))
+		return dev_err_probe(&pdev->dev, PTR_ERR(priv->psy),
+				     "Failed to register power supply\n");
+
+	priv->nb.notifier_call = asus_ec_charger_notify;
+
+	return devm_asus_ec_register_notifier(pdev, &priv->nb);
+}
+
+static const struct of_device_id asus_ec_charger_match[] = {
+	{ .compatible = "asus,ec-charger" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, asus_ec_charger_match);
+
+static struct platform_driver asus_ec_charger_driver = {
+	.driver = {
+		.name = "asus-ec-charger",
+		.of_match_table = asus_ec_charger_match,
+	},
+	.probe = asus_ec_charger_probe,
+};
+module_platform_driver(asus_ec_charger_driver);
+
+MODULE_AUTHOR("Michał Mirosław <mirq-linux@rere.qmqm.pl>");
+MODULE_DESCRIPTION("ASUS Transformer Pad battery charger driver");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* Re: [PATCH v1 4/9] mfd: Add driver for Asus Transformer embedded controller
From: kernel test robot @ 2026-02-01 17:51 UTC (permalink / raw)
  To: Svyatoslav Ryhel, Lee Jones, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dmitry Torokhov, Pavel Machek, Arnd Bergmann,
	Greg Kroah-Hartman, Sebastian Reichel, Michał Mirosław,
	Ion Agorria
  Cc: llvm, oe-kbuild-all, devicetree, linux-kernel, linux-input,
	linux-leds, linux-pm
In-Reply-To: <20260201104343.79231-5-clamor95@gmail.com>

Hi Svyatoslav,

kernel test robot noticed the following build warnings:

[auto build test WARNING on next-20260130]
[also build test WARNING on linus/master v6.19-rc7]
[cannot apply to lee-mfd/for-mfd-next lee-mfd/for-mfd-fixes dtor-input/next dtor-input/for-linus sre-power-supply/for-next v6.19-rc7 v6.19-rc6 v6.19-rc5]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Svyatoslav-Ryhel/dt-bindings-misc-document-ASUS-Transformers-EC-Dockram/20260201-184740
base:   next-20260130
patch link:    https://lore.kernel.org/r/20260201104343.79231-5-clamor95%40gmail.com
patch subject: [PATCH v1 4/9] mfd: Add driver for Asus Transformer embedded controller
config: s390-allmodconfig (https://download.01.org/0day-ci/archive/20260202/202602020110.y4aJbWNb-lkp@intel.com/config)
compiler: clang version 18.1.8 (https://github.com/llvm/llvm-project 3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260202/202602020110.y4aJbWNb-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202602020110.y4aJbWNb-lkp@intel.com/

All warnings (new ones prefixed by >>):

>> drivers/mfd/asus-ec.c:107:40: warning: field width should have type 'int', but argument has type 'unsigned long' [-Wformat]
     107 |         dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
         |                                             ~~^
     108 |                 sizeof(priv->ec_data), priv->ec_data,
         |                 ~~~~~~~~~~~~~~~~~~~~~
   include/linux/dev_printk.h:165:31: note: expanded from macro 'dev_dbg'
     165 |         dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
         |                                      ^~~     ~~~~~~~~~~~
   include/linux/dev_printk.h:19:22: note: expanded from macro 'dev_fmt'
      19 | #define dev_fmt(fmt) fmt
         |                      ^~~
   include/linux/dynamic_debug.h:285:12: note: expanded from macro 'dynamic_dev_dbg'
     285 |                            dev, fmt, ##__VA_ARGS__)
         |                                 ^~~    ~~~~~~~~~~~
   include/linux/dynamic_debug.h:261:59: note: expanded from macro '_dynamic_func_call'
     261 |         _dynamic_func_call_cls(_DPRINTK_CLASS_DFLT, fmt, func, ##__VA_ARGS__)
         |                                                                  ^~~~~~~~~~~
   include/linux/dynamic_debug.h:259:65: note: expanded from macro '_dynamic_func_call_cls'
     259 |         __dynamic_func_call_cls(__UNIQUE_ID(ddebug), cls, fmt, func, ##__VA_ARGS__)
         |                                                                        ^~~~~~~~~~~
   include/linux/dynamic_debug.h:231:15: note: expanded from macro '__dynamic_func_call_cls'
     231 |                 func(&id, ##__VA_ARGS__);                       \
         |                             ^~~~~~~~~~~
   1 warning generated.


vim +107 drivers/mfd/asus-ec.c

   100	
   101	static int asus_ec_read(struct asus_ec_data *priv, bool in_irq)
   102	{
   103		int ret = i2c_smbus_read_i2c_block_data(priv->self, 0x6A,
   104							sizeof(priv->ec_data),
   105							priv->ec_data);
   106	
 > 107		dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
   108			sizeof(priv->ec_data), priv->ec_data,
   109			ret, in_irq ? "; in irq" : "");
   110	
   111		return ret;
   112	}
   113	

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* usbhid: Intermittent EPROTO errors trigger USB reset and interrupt user input
From: Liam Mitchell @ 2026-02-01 17:57 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-usb, linux-input, linux-kernel

Hi,

I'm trying to understand and fix intermittent keyboard/trackpad issues
on my 2013 MacBook Pro running Linux v6.18.4:
- missed/repeated/sticky keys while typing (this thread)
- trackpad stops working (see "bcm5974 trackpad broken after USB
reset" in linux-input)

The keyboard (usbhid) and trackpad (bcm5974) are interfaces of a
single full-speed device behind a high-speed hub:

/:  Bus 003.Port 001: Dev 001, Class=root_hub, Driver=ehci-pci/2p, 480M
    ID 1d6b:0002 Linux Foundation 2.0 root hub
    |__ Port 001: Dev 002, If 0, Class=Hub, Driver=hub/8p, 480M
        ID 8087:0024 Intel Corp. Integrated Rate Matching Hub
        |__ Port 008: Dev 003, If 0, Class=Hub, Driver=hub/2p, 480M
            ID 0424:2512 Microchip Technology, Inc. (formerly SMSC) USB 2.0 Hub
            |__ Port 002: Dev 005, If 0, Class=Human Interface Device,
Driver=usbhid, 12M
                ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
            |__ Port 002: Dev 005, If 1, Class=Human Interface Device,
Driver=usbhid, 12M
                ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
            |__ Port 002: Dev 005, If 2, Class=Human Interface Device,
Driver=bcm5974, 12M
                ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad

Logs preceding one of these events:

[19607.563871] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19611.523681] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19614.550685] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19614.563878] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
[19615.050802] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19615.067673] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19616.822636] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19616.835934] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
[19618.126656] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
[19618.126711] usbhid: usbhid 3-1.8.2:1.0: resetting device
[19618.126728] usbcore: bcm5974 3-1.8.2:1.2: forced unbind
[19618.129942] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb shutting down: -2
[19618.155292] usbcore: usb 3-1.8-port2: not reset yet, waiting 10ms
[19618.217146] usb 3-1.8.2: reset full-speed USB device number 5 using ehci-pci

Both interfaces receive frequent EPROTO errors. I collected statistics
on the error interval over a 30 min idle period:
Interface, Count, Mean (s), Std Dev (s), Median (s), Min (s), Max (s)
Trackpad (bcm5974), 631, 3.21, 4.75, 1.74, 0.01, 35.62
Keyboard (usbhid), 71, 26.82, 24.90, 19.34, 0.92, 107.74

From debugging I understand these to be missed micro-frame (MMF)
timing errors in the host controller. They require URB reset but not a
reset of the whole device.

The bcm5974 driver simply re-submits the interrupt URB. This is the
best behavior for these errors on my machine.

The more "correct" usbhid driver will delay URB re-submission and even
reset the device if a second error is received within a 1.5 second
timeout. It's these periods between EPROTO error and re-submission of
URB where keyboard events are missed (missed keyup == key stays down
and repeats). The default error retry delay of 13ms is relatively
small but a full device reset takes about 200ms. With the frequency of
errors I'm seeing, every ms counts.

Can we improve the usbhid error handling for these cases?
1. Reduce first retry delay from 13ms to 1ms?
2. Reduce the stop retry (reset) timeout from 1.5s to 500ms? 100ms?
3. Require 3 or more errors in the timeout before resetting?
4. Ignore EPROTO errors for known hardware/quirks?

Are there ways to differentiate these safe-to-retry MMF/EPROTO errors
from others that may really need a full device reset?

Appreciate your thoughts.
Thanks,
Liam

^ permalink raw reply

* Re: [PATCH v1 4/9] mfd: Add driver for Asus Transformer embedded controller
From: kernel test robot @ 2026-02-01 19:06 UTC (permalink / raw)
  To: Svyatoslav Ryhel, Lee Jones, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dmitry Torokhov, Pavel Machek, Arnd Bergmann,
	Greg Kroah-Hartman, Sebastian Reichel, Michał Mirosław,
	Ion Agorria
  Cc: oe-kbuild-all, devicetree, linux-kernel, linux-input, linux-leds,
	linux-pm
In-Reply-To: <20260201104343.79231-5-clamor95@gmail.com>

Hi Svyatoslav,

kernel test robot noticed the following build warnings:

[auto build test WARNING on next-20260130]
[also build test WARNING on linus/master v6.19-rc7]
[cannot apply to lee-mfd/for-mfd-next lee-mfd/for-mfd-fixes dtor-input/next dtor-input/for-linus sre-power-supply/for-next v6.19-rc7 v6.19-rc6 v6.19-rc5]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Svyatoslav-Ryhel/dt-bindings-misc-document-ASUS-Transformers-EC-Dockram/20260201-184740
base:   next-20260130
patch link:    https://lore.kernel.org/r/20260201104343.79231-5-clamor95%40gmail.com
patch subject: [PATCH v1 4/9] mfd: Add driver for Asus Transformer embedded controller
config: alpha-allyesconfig (https://download.01.org/0day-ci/archive/20260202/202602020344.ZGXToDMx-lkp@intel.com/config)
compiler: alpha-linux-gcc (GCC) 15.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260202/202602020344.ZGXToDMx-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202602020344.ZGXToDMx-lkp@intel.com/

All warnings (new ones prefixed by >>):

   In file included from include/linux/printk.h:620,
                    from include/asm-generic/bug.h:31,
                    from arch/alpha/include/asm/bug.h:23,
                    from include/linux/bug.h:5,
                    from include/linux/thread_info.h:13,
                    from include/asm-generic/current.h:6,
                    from ./arch/alpha/include/generated/asm/current.h:1,
                    from include/linux/sched.h:12,
                    from include/linux/delay.h:13,
                    from drivers/mfd/asus-ec.c:7:
   drivers/mfd/asus-ec.c: In function 'asus_ec_read':
>> drivers/mfd/asus-ec.c:107:35: warning: field width specifier '*' expects argument of type 'int', but argument 4 has type 'long unsigned int' [-Wformat=]
     107 |         dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
         |                                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/linux/dynamic_debug.h:231:29: note: in definition of macro '__dynamic_func_call_cls'
     231 |                 func(&id, ##__VA_ARGS__);                       \
         |                             ^~~~~~~~~~~
   include/linux/dynamic_debug.h:261:9: note: in expansion of macro '_dynamic_func_call_cls'
     261 |         _dynamic_func_call_cls(_DPRINTK_CLASS_DFLT, fmt, func, ##__VA_ARGS__)
         |         ^~~~~~~~~~~~~~~~~~~~~~
   include/linux/dynamic_debug.h:284:9: note: in expansion of macro '_dynamic_func_call'
     284 |         _dynamic_func_call(fmt, __dynamic_dev_dbg,              \
         |         ^~~~~~~~~~~~~~~~~~
   include/linux/dev_printk.h:165:9: note: in expansion of macro 'dynamic_dev_dbg'
     165 |         dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
         |         ^~~~~~~~~~~~~~~
   include/linux/dev_printk.h:165:30: note: in expansion of macro 'dev_fmt'
     165 |         dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
         |                              ^~~~~~~
   drivers/mfd/asus-ec.c:107:9: note: in expansion of macro 'dev_dbg'
     107 |         dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
         |         ^~~~~~~
   drivers/mfd/asus-ec.c:107:46: note: format string is defined here
     107 |         dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
         |                                             ~^~
         |                                              |
         |                                              int


vim +107 drivers/mfd/asus-ec.c

   100	
   101	static int asus_ec_read(struct asus_ec_data *priv, bool in_irq)
   102	{
   103		int ret = i2c_smbus_read_i2c_block_data(priv->self, 0x6A,
   104							sizeof(priv->ec_data),
   105							priv->ec_data);
   106	
 > 107		dev_dbg(&priv->self->dev, "EC read: %*ph, ret = %d%s\n",
   108			sizeof(priv->ec_data), priv->ec_data,
   109			ret, in_irq ? "; in irq" : "");
   110	
   111		return ret;
   112	}
   113	

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* [dtor-input:next] BUILD SUCCESS ed8a4ef29da3821ee3155d3b1925fa67fc92aae2
From: kernel test robot @ 2026-02-01 20:25 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
branch HEAD: ed8a4ef29da3821ee3155d3b1925fa67fc92aae2  Input: gpio_keys - fall back to platform_get_irq() for interrupt-only keys

elapsed time: 726m

configs tested: 293
configs skipped: 3

The following configs have been built successfully.
More configs may be tested in the coming days.

tested configs:
alpha                             allnoconfig    gcc-15.2.0
alpha                            allyesconfig    gcc-15.2.0
alpha                               defconfig    gcc-15.2.0
arc                              allmodconfig    clang-16
arc                              allmodconfig    gcc-15.2.0
arc                               allnoconfig    gcc-15.2.0
arc                              allyesconfig    clang-22
arc                              allyesconfig    gcc-15.2.0
arc                                 defconfig    gcc-15.2.0
arc                   randconfig-001-20260201    gcc-10.5.0
arc                   randconfig-001-20260201    gcc-14.3.0
arc                   randconfig-001-20260202    gcc-14.3.0
arc                   randconfig-002-20260201    gcc-10.5.0
arc                   randconfig-002-20260202    gcc-14.3.0
arc                           tb10x_defconfig    clang-22
arm                               allnoconfig    clang-22
arm                               allnoconfig    gcc-15.2.0
arm                              allyesconfig    clang-16
arm                              allyesconfig    gcc-15.2.0
arm                                 defconfig    gcc-15.2.0
arm                         lpc18xx_defconfig    clang-22
arm                         mv78xx0_defconfig    clang-22
arm                        mvebu_v5_defconfig    clang-22
arm                        mvebu_v7_defconfig    gcc-15.2.0
arm                   randconfig-001-20260201    gcc-10.5.0
arm                   randconfig-001-20260201    gcc-8.5.0
arm                   randconfig-001-20260202    gcc-14.3.0
arm                   randconfig-002-20260201    clang-22
arm                   randconfig-002-20260201    gcc-10.5.0
arm                   randconfig-002-20260202    gcc-14.3.0
arm                   randconfig-003-20260201    gcc-10.5.0
arm                   randconfig-003-20260201    gcc-8.5.0
arm                   randconfig-003-20260202    gcc-14.3.0
arm                   randconfig-004-20260201    clang-22
arm                   randconfig-004-20260201    gcc-10.5.0
arm                   randconfig-004-20260202    gcc-14.3.0
arm64                            allmodconfig    clang-19
arm64                            allmodconfig    clang-22
arm64                             allnoconfig    gcc-15.2.0
arm64                               defconfig    gcc-15.2.0
arm64                 randconfig-001-20260201    gcc-13.4.0
arm64                 randconfig-001-20260201    gcc-15.2.0
arm64                 randconfig-001-20260202    gcc-9.5.0
arm64                 randconfig-002-20260201    clang-20
arm64                 randconfig-002-20260201    gcc-15.2.0
arm64                 randconfig-002-20260202    gcc-9.5.0
arm64                 randconfig-003-20260201    gcc-15.2.0
arm64                 randconfig-003-20260201    gcc-8.5.0
arm64                 randconfig-003-20260202    gcc-9.5.0
arm64                 randconfig-004-20260201    gcc-12.5.0
arm64                 randconfig-004-20260201    gcc-15.2.0
arm64                 randconfig-004-20260202    gcc-9.5.0
csky                             allmodconfig    gcc-15.2.0
csky                              allnoconfig    gcc-15.2.0
csky                                defconfig    gcc-15.2.0
csky                  randconfig-001-20260201    gcc-11.5.0
csky                  randconfig-001-20260201    gcc-15.2.0
csky                  randconfig-001-20260202    gcc-9.5.0
csky                  randconfig-002-20260201    gcc-15.2.0
csky                  randconfig-002-20260202    gcc-9.5.0
hexagon                          alldefconfig    gcc-15.2.0
hexagon                          allmodconfig    clang-17
hexagon                          allmodconfig    gcc-15.2.0
hexagon                           allnoconfig    clang-22
hexagon                           allnoconfig    gcc-15.2.0
hexagon                             defconfig    gcc-15.2.0
hexagon               randconfig-001-20260201    clang-22
hexagon               randconfig-001-20260201    gcc-15.2.0
hexagon               randconfig-001-20260202    clang-19
hexagon               randconfig-002-20260201    clang-22
hexagon               randconfig-002-20260201    gcc-15.2.0
hexagon               randconfig-002-20260202    clang-19
i386                             allmodconfig    clang-20
i386                             allmodconfig    gcc-14
i386                              allnoconfig    gcc-14
i386                              allnoconfig    gcc-15.2.0
i386                             allyesconfig    clang-20
i386                             allyesconfig    gcc-14
i386        buildonly-randconfig-001-20260201    clang-20
i386        buildonly-randconfig-001-20260201    gcc-14
i386        buildonly-randconfig-002-20260201    clang-20
i386        buildonly-randconfig-003-20260201    clang-20
i386        buildonly-randconfig-003-20260201    gcc-14
i386        buildonly-randconfig-004-20260201    clang-20
i386        buildonly-randconfig-005-20260201    clang-20
i386        buildonly-randconfig-005-20260201    gcc-13
i386        buildonly-randconfig-006-20260201    clang-20
i386                                defconfig    gcc-15.2.0
i386                  randconfig-001-20260201    clang-20
i386                  randconfig-002-20260201    clang-20
i386                  randconfig-003-20260201    clang-20
i386                  randconfig-004-20260201    clang-20
i386                  randconfig-005-20260201    clang-20
i386                  randconfig-006-20260201    clang-20
i386                  randconfig-007-20260201    clang-20
i386                  randconfig-011-20260201    gcc-14
i386                  randconfig-012-20260201    gcc-14
i386                  randconfig-013-20260201    gcc-14
i386                  randconfig-014-20260201    gcc-14
i386                  randconfig-015-20260201    gcc-14
i386                  randconfig-016-20260201    gcc-14
i386                  randconfig-017-20260201    gcc-14
loongarch                        allmodconfig    clang-22
loongarch                         allnoconfig    clang-22
loongarch                         allnoconfig    gcc-15.2.0
loongarch                           defconfig    clang-19
loongarch             randconfig-001-20260201    clang-22
loongarch             randconfig-001-20260201    gcc-15.2.0
loongarch             randconfig-001-20260202    clang-19
loongarch             randconfig-002-20260201    gcc-15.2.0
loongarch             randconfig-002-20260202    clang-19
m68k                             allmodconfig    gcc-15.2.0
m68k                              allnoconfig    gcc-15.2.0
m68k                             allyesconfig    clang-16
m68k                             allyesconfig    gcc-15.2.0
m68k                                defconfig    clang-19
m68k                                defconfig    gcc-15.2.0
m68k                        m5407c3_defconfig    clang-22
m68k                       m5475evb_defconfig    clang-22
microblaze                       alldefconfig    gcc-15.2.0
microblaze                        allnoconfig    gcc-15.2.0
microblaze                       allyesconfig    gcc-15.2.0
microblaze                          defconfig    clang-19
microblaze                          defconfig    gcc-15.2.0
mips                             allmodconfig    gcc-15.2.0
mips                              allnoconfig    gcc-15.2.0
mips                             allyesconfig    gcc-15.2.0
mips                          ath25_defconfig    clang-22
mips                  cavium_octeon_defconfig    gcc-15.2.0
mips                         db1xxx_defconfig    gcc-15.2.0
mips                  decstation_64_defconfig    gcc-15.2.0
mips                           ip30_defconfig    clang-22
mips                malta_qemu_32r6_defconfig    gcc-15.2.0
mips                      maltaaprp_defconfig    clang-22
mips                      maltasmvp_defconfig    gcc-15.2.0
mips                        maltaup_defconfig    clang-22
mips                        vocore2_defconfig    clang-22
nios2                            allmodconfig    clang-22
nios2                            allmodconfig    gcc-11.5.0
nios2                             allnoconfig    clang-22
nios2                             allnoconfig    gcc-11.5.0
nios2                               defconfig    clang-19
nios2                               defconfig    gcc-11.5.0
nios2                 randconfig-001-20260201    gcc-10.5.0
nios2                 randconfig-001-20260201    gcc-15.2.0
nios2                 randconfig-001-20260202    clang-19
nios2                 randconfig-002-20260201    gcc-15.2.0
nios2                 randconfig-002-20260201    gcc-9.5.0
nios2                 randconfig-002-20260202    clang-19
openrisc                         allmodconfig    clang-22
openrisc                         allmodconfig    gcc-15.2.0
openrisc                          allnoconfig    clang-22
openrisc                          allnoconfig    gcc-15.2.0
openrisc                            defconfig    gcc-15.2.0
parisc                           allmodconfig    gcc-15.2.0
parisc                            allnoconfig    clang-22
parisc                            allnoconfig    gcc-15.2.0
parisc                           allyesconfig    clang-19
parisc                              defconfig    gcc-15.2.0
parisc                randconfig-001-20260201    gcc-14.3.0
parisc                randconfig-001-20260202    gcc-8.5.0
parisc                randconfig-002-20260201    gcc-14.3.0
parisc                randconfig-002-20260202    gcc-8.5.0
parisc64                            defconfig    clang-19
parisc64                            defconfig    gcc-15.2.0
powerpc                          allmodconfig    gcc-15.2.0
powerpc                           allnoconfig    clang-22
powerpc                           allnoconfig    gcc-15.2.0
powerpc                      ep88xc_defconfig    clang-22
powerpc                   lite5200b_defconfig    gcc-15.2.0
powerpc                     powernv_defconfig    gcc-15.2.0
powerpc                      ppc6xx_defconfig    gcc-15.2.0
powerpc               randconfig-001-20260201    gcc-14.3.0
powerpc               randconfig-001-20260202    gcc-8.5.0
powerpc               randconfig-002-20260201    gcc-14.3.0
powerpc               randconfig-002-20260202    gcc-8.5.0
powerpc                     skiroot_defconfig    gcc-15.2.0
powerpc                     tqm8560_defconfig    clang-22
powerpc64             randconfig-001-20260201    gcc-14.3.0
powerpc64             randconfig-001-20260202    gcc-8.5.0
powerpc64             randconfig-002-20260201    gcc-14.3.0
powerpc64             randconfig-002-20260202    gcc-8.5.0
riscv                            allmodconfig    clang-22
riscv                             allnoconfig    clang-22
riscv                             allnoconfig    gcc-15.2.0
riscv                            allyesconfig    clang-16
riscv                               defconfig    gcc-15.2.0
riscv                 randconfig-001-20260201    clang-22
riscv                 randconfig-001-20260201    gcc-13.4.0
riscv                 randconfig-002-20260201    gcc-13.4.0
riscv                 randconfig-002-20260201    gcc-8.5.0
s390                             allmodconfig    clang-19
s390                              allnoconfig    clang-22
s390                             allyesconfig    gcc-15.2.0
s390                                defconfig    gcc-15.2.0
s390                  randconfig-001-20260201    gcc-13.4.0
s390                  randconfig-002-20260201    gcc-12.5.0
s390                  randconfig-002-20260201    gcc-13.4.0
s390                       zfcpdump_defconfig    clang-22
sh                               allmodconfig    gcc-15.2.0
sh                                allnoconfig    clang-22
sh                                allnoconfig    gcc-15.2.0
sh                               allyesconfig    clang-19
sh                                  defconfig    gcc-14
sh                                  defconfig    gcc-15.2.0
sh                            migor_defconfig    clang-22
sh                    randconfig-001-20260201    gcc-11.5.0
sh                    randconfig-001-20260201    gcc-13.4.0
sh                    randconfig-002-20260201    gcc-13.4.0
sh                          sdk7780_defconfig    gcc-15.2.0
sh                     sh7710voipgw_defconfig    clang-22
sh                        sh7763rdp_defconfig    gcc-15.2.0
sparc                             allnoconfig    clang-22
sparc                             allnoconfig    gcc-15.2.0
sparc                               defconfig    gcc-15.2.0
sparc                 randconfig-001-20260201    gcc-8.5.0
sparc                 randconfig-001-20260202    gcc-12.5.0
sparc                 randconfig-002-20260201    gcc-8.5.0
sparc                 randconfig-002-20260202    gcc-12.5.0
sparc64                          allmodconfig    clang-22
sparc64                             defconfig    clang-20
sparc64                             defconfig    gcc-14
sparc64               randconfig-001-20260201    gcc-8.5.0
sparc64               randconfig-001-20260202    gcc-12.5.0
sparc64               randconfig-002-20260201    gcc-8.5.0
sparc64               randconfig-002-20260202    gcc-12.5.0
um                               allmodconfig    clang-19
um                                allnoconfig    clang-22
um                               allyesconfig    gcc-14
um                               allyesconfig    gcc-15.2.0
um                                  defconfig    clang-22
um                                  defconfig    gcc-14
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260201    gcc-8.5.0
um                    randconfig-001-20260202    gcc-12.5.0
um                    randconfig-002-20260201    gcc-8.5.0
um                    randconfig-002-20260202    gcc-12.5.0
um                           x86_64_defconfig    clang-22
um                           x86_64_defconfig    gcc-14
x86_64                           allmodconfig    clang-20
x86_64                            allnoconfig    clang-20
x86_64                            allnoconfig    clang-22
x86_64                           allyesconfig    clang-20
x86_64      buildonly-randconfig-001-20260201    clang-20
x86_64      buildonly-randconfig-002-20260201    clang-20
x86_64      buildonly-randconfig-003-20260201    clang-20
x86_64      buildonly-randconfig-004-20260201    clang-20
x86_64      buildonly-randconfig-004-20260201    gcc-14
x86_64      buildonly-randconfig-005-20260201    clang-20
x86_64      buildonly-randconfig-006-20260201    clang-20
x86_64      buildonly-randconfig-006-20260201    gcc-14
x86_64                              defconfig    gcc-14
x86_64                                  kexec    clang-20
x86_64                randconfig-001-20260201    gcc-14
x86_64                randconfig-002-20260201    gcc-14
x86_64                randconfig-003-20260201    gcc-14
x86_64                randconfig-004-20260201    gcc-14
x86_64                randconfig-005-20260201    gcc-14
x86_64                randconfig-006-20260201    gcc-14
x86_64                randconfig-011-20260201    gcc-14
x86_64                randconfig-011-20260202    gcc-14
x86_64                randconfig-012-20260201    gcc-14
x86_64                randconfig-012-20260202    gcc-14
x86_64                randconfig-013-20260201    gcc-14
x86_64                randconfig-013-20260202    gcc-14
x86_64                randconfig-014-20260201    gcc-14
x86_64                randconfig-014-20260202    gcc-14
x86_64                randconfig-015-20260201    gcc-14
x86_64                randconfig-015-20260202    gcc-14
x86_64                randconfig-016-20260201    gcc-14
x86_64                randconfig-016-20260202    gcc-14
x86_64                randconfig-071-20260201    gcc-14
x86_64                randconfig-072-20260201    gcc-14
x86_64                randconfig-073-20260201    gcc-14
x86_64                randconfig-074-20260201    gcc-14
x86_64                randconfig-075-20260201    gcc-14
x86_64                randconfig-076-20260201    gcc-14
x86_64                               rhel-9.4    clang-20
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    clang-20
x86_64                    rhel-9.4-kselftests    clang-20
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-20
xtensa                            allnoconfig    clang-22
xtensa                            allnoconfig    gcc-15.2.0
xtensa                           allyesconfig    clang-22
xtensa                           allyesconfig    gcc-15.2.0
xtensa                generic_kc705_defconfig    gcc-15.2.0
xtensa                randconfig-001-20260201    gcc-8.5.0
xtensa                randconfig-001-20260202    gcc-12.5.0
xtensa                randconfig-002-20260201    gcc-8.5.0
xtensa                randconfig-002-20260202    gcc-12.5.0

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* Re: usbhid: Intermittent EPROTO errors trigger USB reset and interrupt user input
From: Alan Stern @ 2026-02-01 20:40 UTC (permalink / raw)
  To: Liam Mitchell
  Cc: Jiri Kosina, Benjamin Tissoires, linux-usb, linux-input,
	linux-kernel
In-Reply-To: <CAOQ1CL6Q+4GNy=kgisLzs0UBXFT3b02PG8t-0rPuW-Wf6NhQ6g@mail.gmail.com>

On Sun, Feb 01, 2026 at 06:57:06PM +0100, Liam Mitchell wrote:
> Hi,
> 
> I'm trying to understand and fix intermittent keyboard/trackpad issues
> on my 2013 MacBook Pro running Linux v6.18.4:
> - missed/repeated/sticky keys while typing (this thread)
> - trackpad stops working (see "bcm5974 trackpad broken after USB
> reset" in linux-input)
> 
> The keyboard (usbhid) and trackpad (bcm5974) are interfaces of a
> single full-speed device behind a high-speed hub:
> 
> /:  Bus 003.Port 001: Dev 001, Class=root_hub, Driver=ehci-pci/2p, 480M
>     ID 1d6b:0002 Linux Foundation 2.0 root hub
>     |__ Port 001: Dev 002, If 0, Class=Hub, Driver=hub/8p, 480M
>         ID 8087:0024 Intel Corp. Integrated Rate Matching Hub
>         |__ Port 008: Dev 003, If 0, Class=Hub, Driver=hub/2p, 480M
>             ID 0424:2512 Microchip Technology, Inc. (formerly SMSC) USB 2.0 Hub
>             |__ Port 002: Dev 005, If 0, Class=Human Interface Device,
> Driver=usbhid, 12M
>                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
>             |__ Port 002: Dev 005, If 1, Class=Human Interface Device,
> Driver=usbhid, 12M
>                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
>             |__ Port 002: Dev 005, If 2, Class=Human Interface Device,
> Driver=bcm5974, 12M
>                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
> 
> Logs preceding one of these events:
> 
> [19607.563871] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19611.523681] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19614.550685] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19614.563878] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
> [19615.050802] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19615.067673] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19616.822636] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19616.835934] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
> [19618.126656] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> [19618.126711] usbhid: usbhid 3-1.8.2:1.0: resetting device
> [19618.126728] usbcore: bcm5974 3-1.8.2:1.2: forced unbind
> [19618.129942] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb shutting down: -2
> [19618.155292] usbcore: usb 3-1.8-port2: not reset yet, waiting 10ms
> [19618.217146] usb 3-1.8.2: reset full-speed USB device number 5 using ehci-pci
> 
> Both interfaces receive frequent EPROTO errors. I collected statistics
> on the error interval over a 30 min idle period:
> Interface, Count, Mean (s), Std Dev (s), Median (s), Min (s), Max (s)
> Trackpad (bcm5974), 631, 3.21, 4.75, 1.74, 0.01, 35.62
> Keyboard (usbhid), 71, 26.82, 24.90, 19.34, 0.92, 107.74

There should not be this many EPROTO errors.  The fact that you get them 
indicates some sort of hardware problem.  Have you tried swapping the 
cables, hub, keyboard, and trackpad for different ones to see if that 
makes any difference?

> From debugging I understand these to be missed micro-frame (MMF)
> timing errors in the host controller. They require URB reset but not a
> reset of the whole device.
> 
> The bcm5974 driver simply re-submits the interrupt URB. This is the
> best behavior for these errors on my machine.
> 
> The more "correct" usbhid driver will delay URB re-submission and even
> reset the device if a second error is received within a 1.5 second
> timeout. It's these periods between EPROTO error and re-submission of
> URB where keyboard events are missed (missed keyup == key stays down
> and repeats). The default error retry delay of 13ms is relatively
> small but a full device reset takes about 200ms. With the frequency of
> errors I'm seeing, every ms counts.
> 
> Can we improve the usbhid error handling for these cases?
> 1. Reduce first retry delay from 13ms to 1ms?
> 2. Reduce the stop retry (reset) timeout from 1.5s to 500ms? 100ms?
> 3. Require 3 or more errors in the timeout before resetting?
> 4. Ignore EPROTO errors for known hardware/quirks?

It's possible to do all these things, as far as I know.  However, it 
would be better to eliminate the errors completely.

> Are there ways to differentiate these safe-to-retry MMF/EPROTO errors
> from others that may really need a full device reset?

Not that I'm aware of, other than just by retrying.

Alan Stern

^ permalink raw reply

* Re: usbhid: Intermittent EPROTO errors trigger USB reset and interrupt user input
From: Liam Mitchell @ 2026-02-01 21:51 UTC (permalink / raw)
  To: Alan Stern
  Cc: Jiri Kosina, Benjamin Tissoires, linux-usb, linux-input,
	linux-kernel
In-Reply-To: <1ebf9d19-7293-4902-857b-164fd4d21f25@rowland.harvard.edu>

On Sun, 1 Feb 2026 at 21:40, Alan Stern <stern@rowland.harvard.edu> wrote:
>
> On Sun, Feb 01, 2026 at 06:57:06PM +0100, Liam Mitchell wrote:
> > Hi,
> >
> > I'm trying to understand and fix intermittent keyboard/trackpad issues
> > on my 2013 MacBook Pro running Linux v6.18.4:
> > - missed/repeated/sticky keys while typing (this thread)
> > - trackpad stops working (see "bcm5974 trackpad broken after USB
> > reset" in linux-input)
> >
> > The keyboard (usbhid) and trackpad (bcm5974) are interfaces of a
> > single full-speed device behind a high-speed hub:
> >
> > /:  Bus 003.Port 001: Dev 001, Class=root_hub, Driver=ehci-pci/2p, 480M
> >     ID 1d6b:0002 Linux Foundation 2.0 root hub
> >     |__ Port 001: Dev 002, If 0, Class=Hub, Driver=hub/8p, 480M
> >         ID 8087:0024 Intel Corp. Integrated Rate Matching Hub
> >         |__ Port 008: Dev 003, If 0, Class=Hub, Driver=hub/2p, 480M
> >             ID 0424:2512 Microchip Technology, Inc. (formerly SMSC) USB 2.0 Hub
> >             |__ Port 002: Dev 005, If 0, Class=Human Interface Device,
> > Driver=usbhid, 12M
> >                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
> >             |__ Port 002: Dev 005, If 1, Class=Human Interface Device,
> > Driver=usbhid, 12M
> >                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
> >             |__ Port 002: Dev 005, If 2, Class=Human Interface Device,
> > Driver=bcm5974, 12M
> >                 ID 05ac:0259 Apple, Inc. Internal Keyboard/Trackpad
> >
> > Logs preceding one of these events:
> >
> > [19607.563871] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19611.523681] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19614.550685] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19614.563878] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
> > [19615.050802] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19615.067673] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19616.822636] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19616.835934] usbhid: usbhid 3-1.8.2:1.0: retrying intr urb
> > [19618.126656] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb status: -71
> > [19618.126711] usbhid: usbhid 3-1.8.2:1.0: resetting device
> > [19618.126728] usbcore: bcm5974 3-1.8.2:1.2: forced unbind
> > [19618.129942] bcm5974: bcm5974 3-1.8.2:1.2: trackpad urb shutting down: -2
> > [19618.155292] usbcore: usb 3-1.8-port2: not reset yet, waiting 10ms
> > [19618.217146] usb 3-1.8.2: reset full-speed USB device number 5 using ehci-pci
> >
> > Both interfaces receive frequent EPROTO errors. I collected statistics
> > on the error interval over a 30 min idle period:
> > Interface, Count, Mean (s), Std Dev (s), Median (s), Min (s), Max (s)
> > Trackpad (bcm5974), 631, 3.21, 4.75, 1.74, 0.01, 35.62
> > Keyboard (usbhid), 71, 26.82, 24.90, 19.34, 0.92, 107.74
>
> There should not be this many EPROTO errors.  The fact that you get them
> indicates some sort of hardware problem.  Have you tried swapping the
> cables, hub, keyboard, and trackpad for different ones to see if that
> makes any difference?

Thanks for the reply,

These are the laptops internal keyboard/trackpad/hub. I would have to
remove the glued-in battery to even unplug the internal cables. If
it's a hardware issue, I'm assuming it was like this out of the
factory.

I haven't had problems with external devices, they go through a
different USB controller.

> > From debugging I understand these to be missed micro-frame (MMF)
> > timing errors in the host controller. They require URB reset but not a
> > reset of the whole device.
> >
> > The bcm5974 driver simply re-submits the interrupt URB. This is the
> > best behavior for these errors on my machine.
> >
> > The more "correct" usbhid driver will delay URB re-submission and even
> > reset the device if a second error is received within a 1.5 second
> > timeout. It's these periods between EPROTO error and re-submission of
> > URB where keyboard events are missed (missed keyup == key stays down
> > and repeats). The default error retry delay of 13ms is relatively
> > small but a full device reset takes about 200ms. With the frequency of
> > errors I'm seeing, every ms counts.
> >
> > Can we improve the usbhid error handling for these cases?
> > 1. Reduce first retry delay from 13ms to 1ms?
> > 2. Reduce the stop retry (reset) timeout from 1.5s to 500ms? 100ms?
> > 3. Require 3 or more errors in the timeout before resetting?
> > 4. Ignore EPROTO errors for known hardware/quirks?
>
> It's possible to do all these things, as far as I know.  However, it
> would be better to eliminate the errors completely.
>
> > Are there ways to differentiate these safe-to-retry MMF/EPROTO errors
> > from others that may really need a full device reset?
>
> Not that I'm aware of, other than just by retrying.
>
> Alan Stern

Liam

^ permalink raw reply

* Re: [PATCH v5 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-02-01 23:46 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
	Rafael J. Wysocki, Andrew Morton, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Kamel Bouhara, Marco Felsch,
	Henrik Rydberg, Danilo Krummrich, linux-kernel, devicetree,
	linux-input
In-Reply-To: <jp3m5acvoxk7zadzv5h6imqntpyceoaliqw27wlvkydfb2ennz@dbfc4mh44yn6>

Hi Dmitry,

On 26-01-20, Dmitry Torokhov wrote:
> Hi Marco,
> 
> On Mon, Jan 19, 2026 at 10:17:50PM +0100, Marco Felsch wrote:
> > Hi Dmitry,
> > 
> > gentle ping since we never received feedback from one of the input
> > maintainers.
> 
> Some time ago I did few cleanup/fixes to this driver but did not
> actually post them. The patch is below, please pick what still makes
> sense and then I'll do the full review.

Thanks a lot, this was a really nice cleanup. I applied all your
changes and will send a new version soon.

Regards,
  Marco


> 
> Thanks!
> 
> -- 
> Dmitry
> 
> ---
>  drivers/input/touchscreen/touchnetix_axiom.c |  989 +++++++++++++-------------
>  1 file changed, 495 insertions(+), 494 deletions(-)
> 
> diff --git a/drivers/input/touchscreen/touchnetix_axiom.c b/drivers/input/touchscreen/touchnetix_axiom.c
> index e8f56a8f7e8a..300e7b76c3b3 100644
> --- a/drivers/input/touchscreen/touchnetix_axiom.c
> +++ b/drivers/input/touchscreen/touchnetix_axiom.c
> @@ -396,24 +396,24 @@ static int axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf,
>  static int axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf,
>  					 size_t bufsize);
>  
> -#define AXIOM_USAGE(num, rev)		\
> -	{				\
> -		.usage_num = num,	\
> -		.rev_num = rev,		\
> +#define AXIOM_USAGE(num, rev)			\
> +	{					\
> +		.usage_num = num,		\
> +		.rev_num = rev,			\
>  	}
>  
> -#define AXIOM_RO_USAGE(num, rev)	\
> -	{				\
> -		.usage_num = num,	\
> -		.rev_num = rev,		\
> -		.is_ro = true,		\
> +#define AXIOM_RO_USAGE(num, rev)		\
> +	{					\
> +		.usage_num = num,		\
> +		.rev_num = rev,			\
> +		.is_ro = true,			\
>  	}
>  
> -#define AXIOM_CDU_USAGE(num, rev)	\
> -	{				\
> -		.usage_num = num,	\
> -		.rev_num = rev,		\
> -		.is_cdu = true,		\
> +#define AXIOM_CDU_USAGE(num, rev)		\
> +	{					\
> +		.usage_num = num,		\
> +		.rev_num = rev,			\
> +		.is_cdu = true,			\
>  	}
>  
>  #define AXIOM_REPORT_USAGE(num, rev, func)	\
> @@ -511,20 +511,19 @@ static bool axiom_skip_usage_check(struct axiom_data *ts)
>  	}
>  }
>  
> -static unsigned int
> -axiom_usage_baseaddr(struct axiom_data *ts, unsigned char usage_num)
> +static unsigned int axiom_usage_baseaddr(struct axiom_data *ts,
> +					 unsigned char usage_num)
>  {
>  	return ts->usage_table[usage_num].baseaddr;
>  }
>  
> -static unsigned int
> -axiom_usage_size(struct axiom_data *ts, unsigned char usage_num)
> +static unsigned int axiom_usage_size(struct axiom_data *ts,
> +				     unsigned char usage_num)
>  {
>  	return ts->usage_table[usage_num].size_bytes;
>  }
>  
> -static int
> -axiom_usage_rev(struct axiom_data *ts, unsigned char usage_num)
> +static int axiom_usage_rev(struct axiom_data *ts, unsigned char usage_num)
>  {
>  	struct axiom_usage_table_entry *entry = &ts->usage_table[usage_num];
>  
> @@ -534,8 +533,8 @@ axiom_usage_rev(struct axiom_data *ts, unsigned char usage_num)
>  	return entry->info->rev_num;
>  }
>  
> -static bool
> -axiom_driver_supports_usage(struct axiom_data *ts, unsigned char usage_num)
> +static bool axiom_driver_supports_usage(struct axiom_data *ts,
> +					unsigned char usage_num)
>  {
>  	const struct axiom_usage_info *iter = driver_required_usages;
>  	struct device *dev = ts->dev;
> @@ -571,14 +570,12 @@ axiom_driver_supports_usage(struct axiom_data *ts, unsigned char usage_num)
>  	return false;
>  }
>  
> -static bool
> -axiom_usage_entry_is_report(struct axiom_u31_usage_table_entry *entry)
> +static bool axiom_usage_entry_is_report(struct axiom_u31_usage_table_entry *entry)
>  {
>  	return entry->num_pages == 0;
>  }
>  
> -static unsigned int
> -axiom_get_usage_size_bytes(struct axiom_u31_usage_table_entry *entry)
> +static unsigned int axiom_get_usage_size_bytes(struct axiom_u31_usage_table_entry *entry)
>  {
>  	unsigned char max_offset;
>  
> @@ -699,9 +696,9 @@ static bool axiom_usage_supported(struct axiom_data *ts, unsigned int baseaddr)
>  
>  static void axiom_poll(struct input_dev *input);
>  
> -static unsigned long
> -axiom_wait_for_completion_timeout(struct axiom_data *ts, struct axiom_completion *x,
> -				  long timeout)
> +static unsigned long axiom_wait_for_completion_timeout(struct axiom_data *ts,
> +						       struct axiom_completion *x,
> +						       long timeout)
>  {
>  	struct i2c_client *client = to_i2c_client(ts->dev);
>  	unsigned long poll_timeout;
> @@ -751,7 +748,7 @@ static void axiom_complete(struct axiom_data *ts, struct axiom_completion *x)
>  static int axiom_u02_wait_idle(struct axiom_data *ts)
>  {
>  	unsigned int reg;
> -	int ret, _ret;
> +	int error, ret;
>  	u16 cmd;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> @@ -764,23 +761,24 @@ static int axiom_u02_wait_idle(struct axiom_data *ts)
>  	 * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
>  	 * the last command successfully completed and the device is idle.
>  	 */
> -	ret = read_poll_timeout(regmap_raw_read, _ret,
> -				_ret || cmd == AXIOM_U02_REV1_RESP_SUCCESS,
> -				10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> -				ts->regmap, reg, &cmd, 2);
> -	if (ret)
> +	error = read_poll_timeout(regmap_raw_read, ret,
> +				  ret || cmd == AXIOM_U02_REV1_RESP_SUCCESS,
> +				  10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> +				  ts->regmap, reg, &cmd, 2);
> +	if (error) {
>  		dev_err(ts->dev, "Poll u02 timedout with: %#x\n", cmd);
> +		return error;
> +	}
>  
> -	return ret;
> +	return 0;
>  }
>  
> -static int
> -axiom_u02_send_msg(struct axiom_data *ts,
> -		   const struct axiom_u02_rev1_system_manager_msg *msg,
> -		   bool validate_response)
> +static int axiom_u02_send_msg(struct axiom_data *ts,
> +			      const struct axiom_u02_rev1_system_manager_msg *msg,
> +			      bool validate_response)
>  {
>  	unsigned int reg;
> -	int ret;
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U02))
>  		return -EINVAL;
> @@ -788,9 +786,9 @@ axiom_u02_send_msg(struct axiom_data *ts,
>  	reg = axiom_usage_baseaddr(ts, AXIOM_U02);
>  	reg += AXIOM_U02_REV1_COMMAND_REG;
>  
> -	ret = regmap_raw_write(ts->regmap, reg, msg, sizeof(*msg));
> -	if (ret)
> -		return ret;
> +	error = regmap_raw_write(ts->regmap, reg, msg, sizeof(*msg));
> +	if (error)
> +		return error;
>  
>  	if (!validate_response)
>  		return 0;
> @@ -798,8 +796,7 @@ axiom_u02_send_msg(struct axiom_data *ts,
>  	return axiom_u02_wait_idle(ts);
>  }
>  
> -static int
> -axiom_u02_rev1_send_single_cmd(struct axiom_data *ts, u16 cmd)
> +static int axiom_u02_rev1_send_single_cmd(struct axiom_data *ts, u16 cmd)
>  {
>  	struct axiom_u02_rev1_system_manager_msg msg = {
>  		.command = cpu_to_le16(cmd)
> @@ -826,7 +823,7 @@ static int axiom_u02_stop(struct axiom_data *ts)
>  static int axiom_u02_save_config(struct axiom_data *ts)
>  {
>  	struct axiom_u02_rev1_system_manager_msg msg;
> -	int ret;
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U02))
>  		return -EINVAL;
> @@ -836,45 +833,48 @@ static int axiom_u02_save_config(struct axiom_data *ts)
>  	msg.parameters[1] = cpu_to_le16(AXIOM_U02_REV1_PARAM1_SAVEVLTLCFG2NVM);
>  	msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM);
>  
> -	ret = axiom_u02_send_msg(ts, &msg, false);
> -	if (ret)
> -		return ret;
> +	error = axiom_u02_send_msg(ts, &msg, false);
> +	if (error)
> +		return error;
>  
>  	/* Downstream axcfg.py waits for 2sec without checking U01 response */
> -	ret = axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> -					msecs_to_jiffies(2 * MSEC_PER_SEC));
> -	if (!ret)
> +	if (!axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> +					       msecs_to_jiffies(2 * MSEC_PER_SEC))) {
>  		dev_err(ts->dev, "Error save volatile config timedout\n");
> +		return -ETIMEDOUT;
> +	}
>  
> -	return ret ? 0 : -ETIMEDOUT;
> +	return 0;
>  }
>  
>  static int axiom_u02_swreset(struct axiom_data *ts)
>  {
> -	struct axiom_u02_rev1_system_manager_msg msg = { };
> -	int ret;
> +	struct axiom_u02_rev1_system_manager_msg msg = {
> +		.command = cpu_to_le16(AXIOM_U02_REV1_CMD_SOFTRESET),
> +	};
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U02))
>  		return -EINVAL;
>  
> -	msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_SOFTRESET);
> -	ret = axiom_u02_send_msg(ts, &msg, false);
> -	if (ret)
> -		return ret;
> +	error = axiom_u02_send_msg(ts, &msg, false);
> +	if (error)
> +		return error;
>  
>  	/*
>  	 * Downstream axcfg.py waits for 1sec without checking U01 hello. Tests
>  	 * showed that waiting for the hello message isn't enough therefore we
> -	 * need both to make it robuster.
> +	 * need both to make it more robust.
>  	 */
> -	ret = axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
> -					msecs_to_jiffies(1 * MSEC_PER_SEC));
> -	if (!ret)
> +	if (!axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
> +					       msecs_to_jiffies(1 * MSEC_PER_SEC))) {
>  		dev_err(ts->dev, "Error swreset timedout\n");
> +		error = -ETIMEDOUT;
> +	}
>  
>  	fsleep(USEC_PER_SEC);
>  
> -	return ret ? 0 : -ETIMEDOUT;
> +	return error;
>  }
>  
>  static int axiom_u02_fillconfig(struct axiom_data *ts)
> @@ -897,7 +897,7 @@ static int axiom_u02_enter_bootloader(struct axiom_data *ts)
>  	struct axiom_u02_rev1_system_manager_msg msg = { };
>  	struct device *dev = ts->dev;
>  	unsigned int val;
> -	int ret;
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U02))
>  		return -EINVAL;
> @@ -908,57 +908,62 @@ static int axiom_u02_enter_bootloader(struct axiom_data *ts)
>  	 */
>  	msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_ENTERBOOTLOADER);
>  	msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1);
> -	ret = axiom_u02_send_msg(ts, &msg, false);
> -	if (ret) {
> -		dev_err(dev, "Failed to send bootloader-key1: %d\n", ret);
> -		return ret;
> +	error = axiom_u02_send_msg(ts, &msg, false);
> +	if (error) {
> +		dev_err(dev, "Failed to send bootloader-key1: %d\n", error);
> +		return error;
>  	}
>  
>  	msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2);
> -	ret = axiom_u02_send_msg(ts, &msg, false);
> -	if (ret) {
> -		dev_err(dev, "Failed to send bootloader-key2: %d\n", ret);
> -		return ret;
> +	error = axiom_u02_send_msg(ts, &msg, false);
> +	if (error) {
> +		dev_err(dev, "Failed to send bootloader-key2: %d\n", error);
> +		return error;
>  	}
>  
>  	msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3);
> -	ret = axiom_u02_send_msg(ts, &msg, false);
> -	if (ret) {
> -		dev_err(dev, "Failed to send bootloader-key3: %d\n", ret);
> -		return ret;
> +	error = axiom_u02_send_msg(ts, &msg, false);
> +	if (error) {
> +		dev_err(dev, "Failed to send bootloader-key3: %d\n", error);
> +		return error;
>  	}
>  
>  	/* Sleep before the first read to give the device time */
>  	fsleep(250 * USEC_PER_MSEC);
>  
>  	/* Wait till the device reports it is in bootloader mode */
> -	return regmap_read_poll_timeout(ts->regmap,
> -			AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, val,
> -			FIELD_GET(AXIOM_U31_REV1_MODE_MASK, val) ==
> -			AXIOM_U31_REV1_MODE_BLP, 250 * USEC_PER_MSEC,
> -			USEC_PER_SEC);
> +	error = regmap_read_poll_timeout(ts->regmap,
> +					 AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, val,
> +					 FIELD_GET(AXIOM_U31_REV1_MODE_MASK, val) ==
> +						AXIOM_U31_REV1_MODE_BLP,
> +					 250 * USEC_PER_MSEC, USEC_PER_SEC);
> +	if (error)
> +		return error;
> +
> +	return 0;
>  }
>  
> -static int axiom_u04_get(struct axiom_data *ts, u8 **_buf)
> +static u8 *axiom_u04_get(struct axiom_data *ts)
>  {
> -	u8 buf[AXIOM_U04_REV1_SIZE_BYTES];
>  	unsigned int reg;
> -	int ret;
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U04))
> -		return -EINVAL;
> +		return ERR_PTR(-EINVAL);
>  
> -	reg = axiom_usage_baseaddr(ts, AXIOM_U04);
> -	ret = regmap_raw_read(ts->regmap, reg, buf, sizeof(buf));
> -	if (ret)
> -		return ret;
> +	u8 *buf __free(kfree) = kzalloc(AXIOM_U04_REV1_SIZE_BYTES, GFP_KERNEL);
> +	if (!buf)
> +		return ERR_PTR(-ENOMEM);
>  
> -	*_buf = kmemdup(buf, sizeof(buf), GFP_KERNEL);
> +	reg = axiom_usage_baseaddr(ts, AXIOM_U04);
> +	error = regmap_raw_read(ts->regmap, reg, buf, sizeof(buf));
> +	if (error)
> +		return ERR_PTR(error);
>  
> -	return sizeof(buf);
> +	return_ptr(buf);
>  }
>  
> -static int axiom_u04_set(struct axiom_data *ts, u8 *buf, unsigned int bufsize)
> +static int axiom_u04_set(struct axiom_data *ts, u8 *buf)
>  {
>  	unsigned int reg;
>  
> @@ -966,7 +971,7 @@ static int axiom_u04_set(struct axiom_data *ts, u8 *buf, unsigned int bufsize)
>  		return -EINVAL;
>  
>  	reg = axiom_usage_baseaddr(ts, AXIOM_U04);
> -	return regmap_raw_write(ts->regmap, reg, buf, bufsize);
> +	return regmap_raw_write(ts->regmap, reg, buf, AXIOM_U04_REV1_SIZE_BYTES);
>  }
>  
>  /*
> @@ -977,52 +982,52 @@ static int axiom_u31_parse_device_info(struct axiom_data *ts)
>  {
>  	struct regmap *regmap = ts->regmap;
>  	unsigned int id_low, id_high, val;
> -	int ret;
> +	int error;
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &id_high);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &id_high);
> +	if (error)
> +		return error;
>  	id_high = FIELD_GET(AXIOM_U31_REV1_DEVICE_ID_HIGH_MASK, id_high);
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_LOW_REG, &id_low);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_LOW_REG, &id_low);
> +	if (error)
> +		return error;
>  	ts->device_id = id_high << 8 | id_low;
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG, &val);
> +	if (error)
> +		return error;
>  	ts->fw_major = val;
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MIN_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MIN_REG, &val);
> +	if (error)
> +		return error;
>  	ts->fw_minor = val;
>  
>  	/* All other fields are not allowed to be read in BLP mode */
>  	if (axiom_get_runmode(ts) == AXIOM_BLP_MODE)
>  		return 0;
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_RC_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_RC_REG, &val);
> +	if (error)
> +		return error;
>  	ts->fw_rc = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_RC_MASK, val);
>  	ts->silicon_rev = FIELD_GET(AXIOM_U31_REV1_SILICON_REV_MASK, val);
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG, &val);
> +	if (error)
> +		return error;
>  	ts->fw_status = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_STATUS, val);
>  	ts->fw_variant = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_VARIANT, val);
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_HIGH_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_HIGH_REG, &val);
> +	if (error)
> +		return error;
>  	ts->jedec_id = val << 8;
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_LOW_REG, &val);
> -	if (ret)
> -		return ret;
> +	error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_LOW_REG, &val);
> +	if (error)
> +		return error;
>  	ts->jedec_id |= val;
>  
>  	return 0;
> @@ -1032,20 +1037,19 @@ static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc);
>  
>  static int axiom_u31_device_discover(struct axiom_data *ts)
>  {
> -	struct axiom_u31_usage_table_entry *u31_usage_table __free(kfree) = NULL;
>  	struct axiom_u31_usage_table_entry *entry;
>  	struct regmap *regmap = ts->regmap;
>  	unsigned int mode, num_usages;
>  	struct device *dev = ts->dev;
>  	unsigned int i;
> -	int ret;
> +	int error;
>  
>  	axiom_set_runmode(ts, AXIOM_DISCOVERY_MODE);
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &mode);
> -	if (ret) {
> +	error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &mode);
> +	if (error) {
>  		dev_err(dev, "Failed to read MODE\n");
> -		return ret;
> +		return error;
>  	}
>  
>  	/* Abort if the device is in bootloader protocol mode */
> @@ -1054,10 +1058,10 @@ static int axiom_u31_device_discover(struct axiom_data *ts)
>  		axiom_set_runmode(ts, AXIOM_BLP_MODE);
>  
>  	/* Since we are not in bootloader mode we can parse the device info */
> -	ret = axiom_u31_parse_device_info(ts);
> -	if (ret) {
> +	error = axiom_u31_parse_device_info(ts);
> +	if (error) {
>  		dev_err(dev, "Failed to parse device info\n");
> -		return ret;
> +		return error;
>  	}
>  
>  	/* All other fields are not allowed to be read in BLP mode */
> @@ -1066,22 +1070,22 @@ static int axiom_u31_device_discover(struct axiom_data *ts)
>  		return -EACCES;
>  	}
>  
> -	ret = regmap_read(regmap, AXIOM_U31_REV1_NUM_USAGES_REG, &num_usages);
> -	if (ret) {
> +	error = regmap_read(regmap, AXIOM_U31_REV1_NUM_USAGES_REG, &num_usages);
> +	if (error) {
>  		dev_err(dev, "Failed to read NUM_USAGES\n");
> -		return ret;
> +		return error;
>  	}
>  
> -	u31_usage_table = kcalloc(num_usages, sizeof(*u31_usage_table),
> -				  GFP_KERNEL);
> +	struct axiom_u31_usage_table_entry *u31_usage_table __free(kfree) =
> +		kcalloc(num_usages, sizeof(*u31_usage_table), GFP_KERNEL);
>  	if (!u31_usage_table)
>  		return -ENOMEM;
>  
> -	ret = regmap_raw_read(regmap, AXIOM_U31_REV1_PAGE1, u31_usage_table,
> -			      array_size(num_usages, sizeof(*u31_usage_table)));
> -	if (ret) {
> +	error = regmap_raw_read(regmap, AXIOM_U31_REV1_PAGE1, u31_usage_table,
> +				array_size(num_usages, sizeof(*u31_usage_table)));
> +	if (error) {
>  		dev_err(dev, "Failed to read NUM_USAGES\n");
> -		return ret;
> +		return error;
>  	}
>  
>  	/*
> @@ -1106,7 +1110,7 @@ static int axiom_u31_device_discover(struct axiom_data *ts)
>  		if (IS_ERR(info)) {
>  			dev_info(dev, "Required usage u%02X isn't supported for rev.%u\n",
>  				 entry->usage_num, entry->uifrevision);
> -			ret = -EACCES;
> +			error = -EACCES;
>  		}
>  
>  		size_bytes = axiom_get_usage_size_bytes(entry);
> @@ -1121,20 +1125,24 @@ static int axiom_u31_device_discover(struct axiom_data *ts)
>  			ts->max_report_byte_len = size_bytes;
>  	}
>  
> -	if (ret)
> -		return ret;
> +	if (error)
> +		return error;
>  
>  	/* From now on we are in TCP mode to include usage revision checks */
>  	axiom_set_runmode(ts, AXIOM_TCP_MODE);
>  
> -	return axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> +	error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> +	if (error)
> +		return error;
> +
> +	return 0;
>  }
>  
>  static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc)
>  {
>  	struct device *dev = ts->dev;
>  	unsigned int reg;
> -	int ret;
> +	int error;
>  
>  	if (!axiom_driver_supports_usage(ts, AXIOM_U33))
>  		return -EINVAL;
> @@ -1143,10 +1151,10 @@ static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc)
>  		struct axiom_u33_rev2 val;
>  
>  		reg = axiom_usage_baseaddr(ts, AXIOM_U33);
> -		ret = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> -		if (ret) {
> +		error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> +		if (error) {
>  			dev_err(dev, "Failed to read u33\n");
> -			return ret;
> +			return error;
>  		}
>  
>  		crc->runtime = le32_to_cpu(val.runtime_crc);
> @@ -1160,10 +1168,10 @@ static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc)
>  		struct axiom_u33_rev3 val;
>  
>  		reg = axiom_usage_baseaddr(ts, AXIOM_U33);
> -		ret = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> -		if (ret) {
> +		error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> +		if (error) {
>  			dev_err(dev, "Failed to read u33\n");
> -			return ret;
> +			return error;
>  		}
>  
>  		crc->runtime = le32_to_cpu(val.runtime_crc);
> @@ -1195,31 +1203,22 @@ static bool axiom_u42_touch_enabled(struct axiom_data *ts, const u8 *buf,
>  	}
>  }
>  
> -static void axiom_u42_get_touchslots(struct axiom_data *ts)
> +static bool axiom_u42_get_touchslots(struct axiom_data *ts)
>  {
> -	u8 *buf __free(kfree) = NULL;
> -	struct device *dev = ts->dev;
>  	unsigned int bufsize;
>  	unsigned int reg;
> -	int ret, i;
> -
> -	if (!axiom_driver_supports_usage(ts, AXIOM_U42)) {
> -		dev_warn(dev, "Use default touchslots num\n");
> -		goto fallback;
> -	}
> +	int error, i;
>  
>  	bufsize = axiom_usage_size(ts, AXIOM_U42);
> -	buf = kzalloc(bufsize, GFP_KERNEL);
> -	if (!buf) {
> -		dev_warn(dev, "Failed to alloc u42 read buffer, use default value\n");
> -		goto fallback;
> -	}
> +	u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
> +	if (!buf)
> +		return false;
>  
>  	reg = axiom_usage_baseaddr(ts, AXIOM_U42);
> -	ret = regmap_raw_read(ts->regmap, reg, buf, bufsize);
> -	if (ret) {
> -		dev_warn(dev, "Failed to read u42, use default value\n");
> -		goto fallback;
> +	error = regmap_raw_read(ts->regmap, reg, buf, bufsize);
> +	if (error) {
> +		dev_warn(ts->dev, "Failed to read u42\n");
> +		return false;
>  	}
>  
>  	ts->enabled_slots = 0;
> @@ -1232,41 +1231,42 @@ static void axiom_u42_get_touchslots(struct axiom_data *ts)
>  		}
>  	}
>  
> -	return;
> +	return true;
> +}
>  
> -fallback:
> -	ts->enabled_slots = AXIOM_MAX_TOUCHSLOTS_MASK;
> -	ts->num_slots = AXIOM_MAX_TOUCHSLOTS;
> +static void axiom_get_touchslots(struct axiom_data *ts)
> +{
> +	if (!axiom_driver_supports_usage(ts, AXIOM_U42) ||
> +	    !axiom_u42_get_touchslots(ts)) {
> +		dev_warn(ts->dev, "Use default touchslots num\n");
> +		ts->enabled_slots = AXIOM_MAX_TOUCHSLOTS_MASK;
> +		ts->num_slots = AXIOM_MAX_TOUCHSLOTS;
> +	}
>  }
>  
>  static void axiom_u64_cds_enabled(struct axiom_data *ts)
>  {
>  	unsigned int reg, val;
> -	int ret;
> -
> -	if (!axiom_driver_supports_usage(ts, AXIOM_U64))
> -		goto fallback_out;
> -
> -	reg = axiom_usage_baseaddr(ts, AXIOM_U64);
> -	reg += AXIOM_U64_REV2_ENABLECDSPROCESSING_REG;
> -
> -	ret = regmap_read(ts->regmap, reg, &val);
> -	if (ret)
> -		goto fallback_out;
> +	int error;
>  
> -	val = FIELD_GET(AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK, val);
> -	ts->cds_enabled = val ? true : false;
> +	ts->cds_enabled = false;
>  
> -	return;
> +	if (axiom_driver_supports_usage(ts, AXIOM_U64)) {
> +		reg = axiom_usage_baseaddr(ts, AXIOM_U64);
> +		reg += AXIOM_U64_REV2_ENABLECDSPROCESSING_REG;
>  
> -fallback_out:
> -	ts->cds_enabled = false;
> +		error = regmap_read(ts->regmap, reg, &val);
> +		if (!error) {
> +			val = FIELD_GET(AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK, val);
> +			ts->cds_enabled = val ? true : false;
> +		}
> +	}
>  }
>  
>  static int axiom_cdu_wait_idle(struct axiom_data *ts, u8 cdu_usage_num)
>  {
>  	unsigned int reg;
> -	int ret, _ret;
> +	int error, ret;
>  	u16 cmd;
>  
>  	reg = axiom_usage_baseaddr(ts, cdu_usage_num);
> @@ -1275,15 +1275,17 @@ static int axiom_cdu_wait_idle(struct axiom_data *ts, u8 cdu_usage_num)
>  	 * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
>  	 * the last command successfully completed and the device is idle.
>  	 */
> -	ret = read_poll_timeout(regmap_raw_read, _ret,
> -				_ret || cmd == AXIOM_CDU_RESP_SUCCESS,
> -				10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> -				ts->regmap, reg, &cmd, 2);
> -	if (ret)
> +	error = read_poll_timeout(regmap_raw_read, ret,
> +				  ret || cmd == AXIOM_CDU_RESP_SUCCESS,
> +				  10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> +				  ts->regmap, reg, &cmd, 2);
> +	if (error) {
>  		dev_err(ts->dev, "Poll CDU u%02X timedout with: %#x\n",
>  			cdu_usage_num, cmd);
> +		return error;
> +	}
>  
> -	return ret;
> +	return 0;
>  }
>  
>  /*********************** Report usage handling ********************************/
> @@ -1312,8 +1314,8 @@ static int axiom_process_report(struct axiom_data *ts, unsigned char usage_num,
>  }
>  
>  /* Make use of datasheet method 1 - single transfer read */
> -static int
> -axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf, size_t bufsize)
> +static int axiom_u34_rev1_process_report(struct axiom_data *ts,
> +					 const u8 *_buf, size_t bufsize)
>  {
>  	unsigned int reg = axiom_usage_baseaddr(ts, AXIOM_U34);
>  	struct regmap *regmap = ts->regmap;
> @@ -1323,11 +1325,11 @@ axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf, size_t bufs
>  	u16 crc_report, crc_calc;
>  	unsigned int len;
>  	u8 *payload;
> -	int ret;
> +	int error;
>  
> -	ret = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
> -	if (ret)
> -		return ret;
> +	error = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
> +	if (error)
> +		return error;
>  
>  	/* TODO: Add overflow statistics */
>  
> @@ -1373,8 +1375,8 @@ axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf, size_t bufs
>  	return 0;
>  }
>  
> -static void
> -axiom_u41_rev2_decode_target(const u8 *buf, u8 id, u16 *x, u16 *y, s8 *z)
> +static void axiom_u41_rev2_decode_target(const u8 *buf, u8 id,
> +					 u16 *x, u16 *y, s8 *z)
>  {
>  	u16 val;
>  
> @@ -1389,8 +1391,8 @@ axiom_u41_rev2_decode_target(const u8 *buf, u8 id, u16 *x, u16 *y, s8 *z)
>  	*z = buf[AXIOM_U41_REV2_Z_REG(id)];
>  }
>  
> -static int
> -axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf, size_t bufsize)
> +static int axiom_u41_rev2_process_report(struct axiom_data *ts,
> +					 const u8 *buf, size_t bufsize)
>  {
>  	struct input_dev *input = ts->input;
>  	unsigned char id;
> @@ -1437,8 +1439,8 @@ axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf, size_t bufsi
>  	return 0;
>  }
>  
> -static int
> -axiom_u01_rev1_process_report(struct axiom_data *ts, const u8 *buf, size_t bufsize)
> +static int axiom_u01_rev1_process_report(struct axiom_data *ts,
> +					 const u8 *buf, size_t bufsize)
>  {
>  	switch (buf[AXIOM_U01_REV1_REPORTTYPE_REG]) {
>  	case AXIOM_U01_REV1_REPORTTYPE_HELLO:
> @@ -1509,13 +1511,11 @@ static int axiom_regmap_read(void *context, const void *reg_buf, size_t reg_size
>  	xfer[1].len = val_size;
>  	xfer[1].buf = val_buf;
>  
> -	ret = i2c_transfer(i2c->adapter, xfer, 2);
> -	if (ret == 2)
> +	ret = i2c_transfer(i2c->adapter, xfer, ARRAY_SIZE(xfer));
> +	if (likely(ret == ARRAY_SIZE(xfer)))
>  		return 0;
> -	else if (ret < 0)
> -		return ret;
> -	else
> -		return -EIO;
> +
> +	return ret < 0 ? ret : -EIO;
>  }
>  
>  static int axiom_regmap_write(void *context, const void *data, size_t count)
> @@ -1523,7 +1523,6 @@ static int axiom_regmap_write(void *context, const void *data, size_t count)
>  	struct device *dev = context;
>  	struct i2c_client *i2c = to_i2c_client(dev);
>  	struct axiom_data *ts = i2c_get_clientdata(i2c);
> -	char *buf __free(kfree) = NULL;
>  	struct axiom_cmd_header hdr;
>  	u16 xferlen, addr, baseaddr;
>  	size_t val_size, msg_size;
> @@ -1548,7 +1547,7 @@ static int axiom_regmap_write(void *context, const void *data, size_t count)
>  		return -EINVAL;
>  
>  	msg_size = sizeof(hdr) + val_size;
> -	buf = kzalloc(msg_size, GFP_KERNEL);
> +	u8 *buf __free(kfree) = kzalloc(msg_size, GFP_KERNEL);
>  	if (!buf)
>  		return -ENOMEM;
>  
> @@ -1556,8 +1555,10 @@ static int axiom_regmap_write(void *context, const void *data, size_t count)
>  	memcpy(&buf[sizeof(hdr)], &((char *)data)[2], val_size);
>  
>  	ret = i2c_master_send(i2c, buf, msg_size);
> +	if (likely(ret == msg_size))
> +		return 0;
>  
> -	return ret == msg_size ? 0 : ret;
> +	return ret < 0 ? ret : -EIO;
>  }
>  
>  static const struct regmap_config axiom_i2c_regmap_config = {
> @@ -1571,29 +1572,20 @@ static const struct regmap_config axiom_i2c_regmap_config = {
>  
>  static int axiom_update_input_dev(struct axiom_data *ts);
>  
> -static enum fw_upload_err
> -axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
> +static enum fw_upload_err __axiom_axfw_fw_prepare(struct axiom_data *ts,
> +						  struct axiom_firmware *afw,
> +						  const u8 *data, u32 size)
>  {
> -	struct axiom_data *ts = fw_upload->dd_handle;
> -	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
>  	u8 major_ver, minor_ver, rc_ver, status, variant;
>  	u32 fw_file_crc32, crc32_calc;
>  	struct device *dev = ts->dev;
>  	unsigned int signature_len;
> -	enum fw_upload_err ret;
>  	u16 fw_file_format_ver;
>  	u16 fw_file_device_id;
>  
> -	mutex_lock(&afw->lock);
> -	afw->cancel = false;
> -	mutex_unlock(&afw->lock);
> -
> -	mutex_lock(&ts->fwupdate_lock);
> -
>  	if (size < sizeof(struct axiom_fw_axfw_hdr)) {
>  		dev_err(dev, "Invalid AXFW file size\n");
> -		ret = FW_UPLOAD_ERR_INVALID_SIZE;
> -		goto out;
> +		return FW_UPLOAD_ERR_INVALID_SIZE;
>  	}
>  
>  	signature_len = strlen(AXIOM_FW_AXFW_SIGNATURE);
> @@ -1603,8 +1595,7 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  		 * ALC don't. Therefore the AXFW format is preferred.
>  		 */
>  		dev_warn(dev, "No AXFW signature, assume ALC firmware\n");
> -		ret = FW_UPLOAD_ERR_NONE;
> -		goto out;
> +		return FW_UPLOAD_ERR_NONE;
>  	}
>  
>  	fw_file_crc32 = get_unaligned_le32(&data[signature_len]);
> @@ -1612,8 +1603,7 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  	if (fw_file_crc32 != crc32_calc) {
>  		dev_err(dev, "AXFW CRC32 doesn't match (fw:%#x calc:%#x)\n",
>  			fw_file_crc32, crc32_calc);
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  	}
>  
>  	data += signature_len + sizeof(fw_file_crc32);
> @@ -1621,8 +1611,7 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  	if (fw_file_format_ver != AXIOM_FW_AXFW_FILE_FMT_VER) {
>  		dev_err(dev, "Invalid AXFW file format version: %04x",
>  			fw_file_format_ver);
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  	}
>  
>  	data += sizeof(fw_file_format_ver);
> @@ -1630,8 +1619,7 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  	if (fw_file_device_id != ts->device_id) {
>  		dev_err(dev, "Invalid AXFW target device (fw:%#04x dev:%#04x)\n",
>  			fw_file_device_id, ts->device_id);
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  	}
>  
>  	/*
> @@ -1640,7 +1628,7 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  	 *  * downloading the firmware failed in between, or
>  	 *  * the following usage discovery failed.
>  	 *
> -	 *  All cases are crcitical and we need to use any firmware to
> +	 *  All cases are critical and we need to use any firmware to
>  	 *  bring the device back into a working state which is supported by the
>  	 *  host.
>  	 */
> @@ -1657,19 +1645,32 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  	if (major_ver == ts->fw_major && minor_ver == ts->fw_minor &&
>  	    rc_ver == ts->fw_rc && status == ts->fw_status &&
>  	    variant == ts->fw_variant) {
> -		ret = FW_UPLOAD_ERR_DUPLICATE;
> -		goto out;
> +		return FW_UPLOAD_ERR_DUPLICATE;
>  	}
>  
>  	dev_info(dev, "Detected AXFW %02u.%02u.%02u (%s)\n",
>  		 major_ver, minor_ver, rc_ver,
>  		 status ? "production" : "engineering");
>  
> -	mutex_lock(&afw->lock);
> -	ret = afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> -	mutex_unlock(&afw->lock);
> +	guard(mutex)(&afw->lock);
> +	return afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> +}
> +
> +static enum fw_upload_err axiom_axfw_fw_prepare(struct fw_upload *fw_upload,
> +						const u8 *data, u32 size)
> +{
> +	struct axiom_data *ts = fw_upload->dd_handle;
> +	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
> +	enum fw_upload_err ret;
> +
> +	scoped_guard(mutex, &afw->lock) {
> +		afw->cancel = false;
> +	}
> +
> +	mutex_lock(&ts->fwupdate_lock);
> +
> +	ret = __axiom_axfw_fw_prepare(ts, afw, data, size);
>  
> -out:
>  	/*
>  	 * In FW_UPLOAD_ERR_NONE case the complete handler will release the
>  	 * lock.
> @@ -1683,28 +1684,26 @@ axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  static int axiom_enter_bootloader_mode(struct axiom_data *ts)
>  {
>  	struct device *dev = ts->dev;
> -	int ret;
> +	int error;
>  
>  	axiom_set_runmode(ts, AXIOM_BLP_PRE_MODE);
>  
> -	ret = axiom_u02_wait_idle(ts);
> -	if (ret)
> +	error = axiom_u02_wait_idle(ts);
> +	if (error)
>  		goto err_out;
>  
> -	ret = axiom_u02_enter_bootloader(ts);
> -	if (ret) {
> +	error = axiom_u02_enter_bootloader(ts);
> +	if (error) {
>  		dev_err(dev, "Failed to enter bootloader mode\n");
>  		goto err_out;
>  	}
>  
>  	axiom_set_runmode(ts, AXIOM_BLP_MODE);
> -
>  	return 0;
>  
>  err_out:
>  	axiom_set_runmode(ts, AXIOM_TCP_MODE);
> -
> -	return ret;
> +	return error;
>  }
>  
>  static int axoim_blp_wait_ready(struct axiom_data *ts)
> @@ -1727,18 +1726,18 @@ static int axoim_blp_wait_ready(struct axiom_data *ts)
>  	return ret;
>  }
>  
> -static int
> -axiom_blp_write_chunk(struct axiom_data *ts, const u8 *data, u16 length)
> +static int axiom_blp_write_chunk(struct axiom_data *ts,
> +				 const u8 *data, u16 length)
>  {
>  	unsigned int chunk_size = AXIOM_U01_BLP_FIFO_CHK_SIZE_BYTES;
>  	unsigned int reg = AXIOM_U01_BLP_FIFO_REG;
>  	struct device *dev = ts->dev;
>  	unsigned int pos = 0;
> -	int ret;
> +	int error;
>  
> -	ret = axoim_blp_wait_ready(ts);
> -	if (ret)
> -		return ret;
> +	error = axoim_blp_wait_ready(ts);
> +	if (error)
> +		return error;
>  
>  	/*
>  	 * TODO: Downstream does this chunk transfers. Verify if this is
> @@ -1751,16 +1750,16 @@ axiom_blp_write_chunk(struct axiom_data *ts, const u8 *data, u16 length)
>  		if ((pos + chunk_size) > length)
>  			len = length - pos;
>  
> -		ret = regmap_raw_write(ts->regmap, reg, &data[pos], len);
> -		if (ret) {
> -			dev_err(dev, "Bootloader download AXFW chunk failed %d\n", ret);
> -			return ret;
> +		error = regmap_raw_write(ts->regmap, reg, &data[pos], len);
> +		if (error) {
> +			dev_err(dev, "Bootloader download AXFW chunk failed %d\n", error);
> +			return error;
>  		}
>  
>  		pos += len;
> -		ret = axoim_blp_wait_ready(ts);
> -		if (ret)
> -			return ret;
> +		error = axoim_blp_wait_ready(ts);
> +		if (error)
> +			return error;
>  	}
>  
>  	return 0;
> @@ -1773,11 +1772,11 @@ static int axiom_blp_reset(struct axiom_data *ts)
>  	struct device *dev = ts->dev;
>  	unsigned int attempts = 20;
>  	unsigned int mode;
> -	int ret;
> +	int error;
>  
> -	ret = axoim_blp_wait_ready(ts);
> -	if (ret)
> -		return ret;
> +	error = axoim_blp_wait_ready(ts);
> +	if (error)
> +		return error;
>  
>  	/*
>  	 * For some reason this write fail with -ENXIO. Skip checking the return
> @@ -1787,17 +1786,17 @@ static int axiom_blp_reset(struct axiom_data *ts)
>  	regmap_raw_write(ts->regmap, reg, &reset_cmd, sizeof(reset_cmd));
>  
>  	do {
> -		ret = regmap_read(ts->regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG,
> -				  &mode);
> -		if (!ret)
> +		error = regmap_read(ts->regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG,
> +				    &mode);
> +		if (!error)
>  			break;
>  
>  		fsleep(250 * USEC_PER_MSEC);
>  	} while (attempts--);
>  
> -	if (ret) {
> -		dev_err(dev, "Failed to read MODE after BLP reset: %d\n", ret);
> -		return ret;
> +	if (error) {
> +		dev_err(dev, "Failed to read MODE after BLP reset: %d\n", error);
> +		return error;
>  	}
>  
>  	mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
> @@ -1833,27 +1832,24 @@ static void axiom_unregister_input_dev(struct axiom_data *ts)
>  	ts->input = NULL;
>  }
>  
> -static enum fw_upload_err
> -axiom_axfw_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
> -		    u32 size, u32 *written)
> +static enum fw_upload_err axiom_axfw_fw_write(struct fw_upload *fw_upload,
> +					      const u8 *data, u32 offset,
> +					      u32 size, u32 *written)
>  {
>  	struct axiom_data *ts = fw_upload->dd_handle;
>  	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
>  	struct device *dev = ts->dev;
> -	bool cancel;
> -	int ret;
> +	int error;
>  
>  	/* Done before cancel check due to cleanup based put */
> -	ret = pm_runtime_resume_and_get(ts->dev);
> -	if (ret)
> +	error = pm_runtime_resume_and_get(ts->dev);
> +	if (error)
>  		return FW_UPLOAD_ERR_HW_ERROR;
>  
> -	mutex_lock(&afw->lock);
> -	cancel = afw->cancel;
> -	mutex_unlock(&afw->lock);
> -
> -	if (cancel)
> -		return FW_UPLOAD_ERR_CANCELED;
> +	scoped_guard(mutex, &afw->lock) {
> +		if (afw->cancel)
> +			return FW_UPLOAD_ERR_CANCELED;
> +	}
>  
>  	axiom_lock_input_device(ts);
>  
> @@ -1883,8 +1879,8 @@ axiom_axfw_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  		 * The bootlaoder FW can handle the complete chunk incl. the
>  		 * header.
>  		 */
> -		ret = axiom_blp_write_chunk(ts, data, len);
> -		if (ret)
> +		error = axiom_blp_write_chunk(ts, data, len);
> +		if (error)
>  			goto err;
>  
>  		size -= len;
> @@ -1892,12 +1888,12 @@ axiom_axfw_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  		data += len;
>  	}
>  
> -	ret = axiom_blp_reset(ts);
> -	if (ret)
> +	error = axiom_blp_reset(ts);
> +	if (error)
>  		dev_warn(dev, "BLP reset failed\n");
>  
> -	ret = axiom_u31_device_discover(ts);
> -	if (ret) {
> +	error = axiom_u31_device_discover(ts);
> +	if (error) {
>  		/*
>  		 * This is critical and we need to avoid that the user-space can
>  		 * still use the input-dev.
> @@ -1911,8 +1907,8 @@ axiom_axfw_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  	/* Unlock before the input device gets unregistered */
>  	axiom_unlock_input_device(ts);
>  
> -	ret = axiom_update_input_dev(ts);
> -	if (ret) {
> +	error = axiom_update_input_dev(ts);
> +	if (error) {
>  		dev_err(dev, "Input device update failed after AXFW/ALC firmware update\n");
>  		return FW_UPLOAD_ERR_HW_ERROR;
>  	}
> @@ -1936,9 +1932,8 @@ static void axiom_axfw_fw_cancel(struct fw_upload *fw_upload)
>  	struct axiom_data *ts = fw_upload->dd_handle;
>  	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
>  
> -	mutex_lock(&afw->lock);
> +	guard(mutex)(&afw->lock);
>  	afw->cancel = true;
> -	mutex_unlock(&afw->lock);
>  }
>  
>  static void axiom_axfw_fw_cleanup(struct fw_upload *fw_upload)
> @@ -1958,8 +1953,8 @@ static const struct fw_upload_ops axiom_axfw_fw_upload_ops = {
>  	.cleanup = axiom_axfw_fw_cleanup,
>  };
>  
> -static int
> -axiom_set_new_crcs(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *cfg)
> +static int axiom_set_new_crcs(struct axiom_data *ts,
> +			      const struct axiom_fw_cfg_chunk *cfg)
>  {
>  	struct axiom_crc *crc = &ts->crc[AXIOM_CRC_NEW];
>  	const u32 *u33_data = (const u32 *)cfg->usage_content;
> @@ -1987,8 +1982,8 @@ axiom_set_new_crcs(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *cfg)
>  	return 0;
>  }
>  
> -static unsigned int
> -axiom_cfg_fw_prepare_chunk(struct axiom_fw_cfg_chunk *chunk, const u8 *data)
> +static unsigned int axiom_cfg_fw_prepare_chunk(struct axiom_fw_cfg_chunk *chunk,
> +					       const u8 *data)
>  {
>  	chunk->usage_num = data[0];
>  	chunk->usage_rev = data[1];
> @@ -2017,46 +2012,37 @@ static bool axiom_cfg_fw_update_required(struct axiom_data *ts)
>  	    !axiom_usage_crc_match(ts, AXIOM_U22, cur, new, u22_sequencedata) ||
>  	    !axiom_usage_crc_match(ts, AXIOM_U43, cur, new, u43_hotspots) ||
>  	    !axiom_usage_crc_match(ts, AXIOM_U93, cur, new, u93_profiles) ||
> -	    !axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap))
> +	    !axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap)) {
>  		return true;
> +	}
>  
>  	return false;
>  }
>  
> -static enum fw_upload_err
> -axiom_cfg_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
> +static enum fw_upload_err __axiom_cfg_fw_prepare(struct axiom_data *ts,
> +						 struct axiom_firmware *afw,
> +						 const u8 *data, u32 size)
>  {
> -	struct axiom_data *ts = fw_upload->dd_handle;
> -	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
>  	u32 cur_runtime_crc, fw_runtime_crc;
>  	struct axiom_fw_cfg_chunk chunk;
>  	struct device *dev = ts->dev;
> -	enum fw_upload_err ret;
>  	u32 signature;
> -
> -	mutex_lock(&afw->lock);
> -	afw->cancel = false;
> -	mutex_unlock(&afw->lock);
> -
> -	mutex_lock(&ts->fwupdate_lock);
> +	int error;
>  
>  	if (axiom_get_runmode(ts) != AXIOM_TCP_MODE) {
>  		dev_err(dev, "Device not in TCP mode, abort TH2CFG update\n");
> -		ret = FW_UPLOAD_ERR_HW_ERROR;
> -		goto out;
> +		return FW_UPLOAD_ERR_HW_ERROR;
>  	}
>  
>  	if (size < sizeof(struct axiom_fw_cfg_hdr)) {
>  		dev_err(dev, "Invalid TH2CFG file size\n");
> -		ret = FW_UPLOAD_ERR_INVALID_SIZE;
> -		goto out;
> +		return FW_UPLOAD_ERR_INVALID_SIZE;
>  	}
>  
>  	signature = get_unaligned_be32(data);
>  	if (signature != AXIOM_FW_CFG_SIGNATURE) {
>  		dev_err(dev, "Invalid TH2CFG signature\n");
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  	}
>  
>  	/* Skip to the first fw chunk */
> @@ -2080,39 +2066,47 @@ axiom_cfg_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  
>  	if (size == 0) {
>  		dev_err(dev, "Failed to find the u33 entry in TH2CFG\n");
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  	}
>  
> -	ret = axiom_set_new_crcs(ts, &chunk);
> -	if (ret) {
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> -	}
> +	error = axiom_set_new_crcs(ts, &chunk);
> +	if (error)
> +		return FW_UPLOAD_ERR_FW_INVALID;
>  
>  	/*
>  	 * Nothing to do if the CRCs are the same. TODO: Must be extended once
>  	 * the CDU update is added.
>  	 */
> -	if (!axiom_cfg_fw_update_required(ts)) {
> -		ret = FW_UPLOAD_ERR_DUPLICATE;
> -		goto out;
> -	}
> +	if (!axiom_cfg_fw_update_required(ts))
> +		return FW_UPLOAD_ERR_DUPLICATE;
>  
>  	cur_runtime_crc = ts->crc[AXIOM_CRC_CUR].runtime;
>  	fw_runtime_crc = ts->crc[AXIOM_CRC_NEW].runtime;
>  	if (cur_runtime_crc != fw_runtime_crc) {
>  		dev_err(dev, "TH2CFG and device runtime CRC doesn't match: %#x != %#x\n",
>  			fw_runtime_crc, cur_runtime_crc);
> -		ret = FW_UPLOAD_ERR_FW_INVALID;
> -		goto out;
> +		return FW_UPLOAD_ERR_FW_INVALID;
> +	}
> +
> +	guard(mutex)(&afw->lock);
> +	return afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> +}
> +
> +static enum fw_upload_err axiom_cfg_fw_prepare(struct fw_upload *fw_upload,
> +					       const u8 *data, u32 size)
> +{
> +	struct axiom_data *ts = fw_upload->dd_handle;
> +	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
> +	enum fw_upload_err ret;
> +
> +	scoped_guard(mutex, &afw->lock) {
> +		afw->cancel = false;
>  	}
>  
> -	mutex_lock(&afw->lock);
> -	ret = afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> -	mutex_unlock(&afw->lock);
> +	mutex_lock(&ts->fwupdate_lock);
> +
> +	ret = __axiom_cfg_fw_prepare(ts, afw, data, size);
>  
> -out:
>  	/*
>  	 * In FW_UPLOAD_ERR_NONE case the complete handler will release the
>  	 * lock.
> @@ -2125,27 +2119,26 @@ axiom_cfg_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
>  
>  static int axiom_zero_volatile_mem(struct axiom_data *ts)
>  {
> -	int ret, size;
> -	u8 *buf;
> +	int error;
>  
>  	/* Zero out the volatile memory except for the user content in u04 */
> -	ret = axiom_u04_get(ts, &buf);
> -	if (ret < 0)
> -		return ret;
> -	size = ret;
> +	u8 *buf __free(kfree) = axiom_u04_get(ts);
> +	if (IS_ERR(buf))
> +		return PTR_ERR(buf);
>  
> -	ret = axiom_u02_fillconfig(ts);
> -	if (ret)
> -		goto out;
> +	error = axiom_u02_fillconfig(ts);
> +	if (error)
> +		return error;
>  
> -	ret = axiom_u04_set(ts, buf, size);
> -out:
> -	kfree(buf);
> -	return ret;
> +	error = axiom_u04_set(ts, buf);
> +	if (error)
> +		return error;
> +
> +	return 0;
>  }
>  
> -static bool
> -axiom_skip_cfg_chunk(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +static bool axiom_skip_cfg_chunk(struct axiom_data *ts,
> +				 const struct axiom_fw_cfg_chunk *chunk)
>  {
>  	u8 usage_num = chunk->usage_num;
>  
> @@ -2156,21 +2149,22 @@ axiom_skip_cfg_chunk(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chu
>  
>  	/* Skip read-only usages */
>  	if (ts->usage_table[usage_num].info &&
> -	    ts->usage_table[usage_num].info->is_ro)
> +	    ts->usage_table[usage_num].info->is_ro) {
>  		return true;
> +	}
>  
>  	return false;
>  }
>  
> -static int
> -axiom_write_cdu_usage(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +static int axiom_write_cdu_usage(struct axiom_data *ts,
> +				 const struct axiom_fw_cfg_chunk *chunk)
>  {
>  	struct axiom_cdu_usage cdu = { };
>  	struct device *dev = ts->dev;
>  	unsigned int remaining;
>  	unsigned int reg;
>  	unsigned int pos;
> -	int ret;
> +	int error;
>  
>  	pos = 0;
>  	remaining = chunk->usage_length;
> @@ -2189,17 +2183,17 @@ axiom_write_cdu_usage(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *ch
>  		memset(cdu.data, 0, sizeof(cdu.data));
>  		memcpy(cdu.data, &chunk->usage_content[pos], size);
>  
> -		ret = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> -		if (ret) {
> +		error = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> +		if (error) {
>  			dev_err(dev, "Failed to write CDU u%02X\n",
>  				chunk->usage_num);
> -			return ret;
> +			return error;
>  		}
>  
> -		ret = axiom_cdu_wait_idle(ts, chunk->usage_num);
> -		if (ret) {
> +		error = axiom_cdu_wait_idle(ts, chunk->usage_num);
> +		if (error) {
>  			dev_err(dev, "CDU write wait-idle failed\n");
> -			return ret;
> +			return error;
>  		}
>  
>  		remaining -= size;
> @@ -2215,59 +2209,65 @@ axiom_write_cdu_usage(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *ch
>  	cdu.parameters[0] = cpu_to_le16(AXIOM_CDU_PARAM0_COMMIT);
>  	cdu.parameters[1] = cpu_to_le16(AXIOM_CDU_PARAM1_COMMIT);
>  
> -	ret = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> -	if (ret) {
> +	error = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> +	if (error) {
>  		dev_err(dev, "Failed to commit CDU u%02X to NVM\n",
>  			chunk->usage_num);
> -		return ret;
> +		return error;
>  	}
>  
> -	ret = axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> -					msecs_to_jiffies(5 * MSEC_PER_SEC));
> -	if (!ret) {
> +	if (!axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> +					       msecs_to_jiffies(5 * MSEC_PER_SEC))) {
>  		dev_err(ts->dev, "Error CDU u%02X commit timedout\n",
>  			chunk->usage_num);
>  		return -ETIMEDOUT;
>  	}
>  
> -	return axiom_cdu_wait_idle(ts, chunk->usage_num);
> +	error = axiom_cdu_wait_idle(ts, chunk->usage_num);
> +	if (error)
> +		return error;
> +
> +	return 0;
>  }
>  
> -static int
> -axiom_write_cfg_chunk(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +static int axiom_write_cfg_chunk(struct axiom_data *ts,
> +				 const struct axiom_fw_cfg_chunk *chunk)
>  {
>  	unsigned int reg;
> -	int ret;
> +	int error;
>  
>  	if (ts->usage_table[chunk->usage_num].info &&
>  	    ts->usage_table[chunk->usage_num].info->is_cdu) {
> -		ret = axiom_write_cdu_usage(ts, chunk);
> -		if (ret)
> -			return ret;
> -		goto out;
> +		error = axiom_write_cdu_usage(ts, chunk);
> +		if (error)
> +			return error;
> +	} else {
> +		reg = axiom_usage_baseaddr(ts, chunk->usage_num);
> +		error = regmap_raw_write(ts->regmap, reg, chunk->usage_content,
> +					 chunk->usage_length);
> +		if (error)
> +			return error;
>  	}
>  
> -	reg = axiom_usage_baseaddr(ts, chunk->usage_num);
> -	ret = regmap_raw_write(ts->regmap, reg, chunk->usage_content, chunk->usage_length);
> -	if (ret)
> -		return ret;
> +	error = axiom_u02_wait_idle(ts);
> +	if (error)
> +		return error;
>  
> -out:
> -	return axiom_u02_wait_idle(ts);
> +	return 0;
>  }
>  
>  static int axiom_verify_volatile_mem(struct axiom_data *ts)
>  {
> -	int ret;
> +	int error;
>  
> -	ret = axiom_u02_computecrc(ts);
> -	if (ret)
> -		return ret;
> +	error = axiom_u02_computecrc(ts);
> +	if (error)
> +		return error;
>  
>  	/* Query the new CRCs after they are re-computed */
> -	ret = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> -	if (ret)
> -		return ret;
> +	error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> +	if (error)
> +		return error;
>  
>  	return ts->crc[AXIOM_CRC_CUR].vltusageconfig ==
>  	       ts->crc[AXIOM_CRC_NEW].vltusageconfig ? 0 : -EINVAL;
> @@ -2310,27 +2310,24 @@ static int axiom_verify_crcs(struct axiom_data *ts)
>  	return 0;
>  }
>  
> -static enum fw_upload_err
> -axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
> -		   u32 size, u32 *written)
> +static enum fw_upload_err axiom_cfg_fw_write(struct fw_upload *fw_upload,
> +					     const u8 *data, u32 offset,
> +					     u32 size, u32 *written)
>  {
>  	struct axiom_data *ts = fw_upload->dd_handle;
>  	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
>  	struct device *dev = ts->dev;
> -	bool cancel;
> -	int ret;
> +	int error;
>  
>  	/* Done before cancel check due to cleanup based put */
> -	ret = pm_runtime_resume_and_get(ts->dev);
> -	if (ret)
> +	error = pm_runtime_resume_and_get(ts->dev);
> +	if (error)
>  		return FW_UPLOAD_ERR_HW_ERROR;
>  
> -	mutex_lock(&afw->lock);
> -	cancel = afw->cancel;
> -	mutex_unlock(&afw->lock);
> -
> -	if (cancel)
> -		return FW_UPLOAD_ERR_CANCELED;
> +	scoped_guard(mutex, &afw->lock) {
> +		if (afw->cancel)
> +			return FW_UPLOAD_ERR_CANCELED;
> +	}
>  
>  	axiom_lock_input_device(ts);
>  
> @@ -2340,12 +2337,12 @@ axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  		return FW_UPLOAD_ERR_HW_ERROR;
>  	}
>  
> -	ret = axiom_u02_stop(ts);
> -	if (ret)
> +	error = axiom_u02_stop(ts);
> +	if (error)
>  		goto err_swreset;
>  
> -	ret = axiom_zero_volatile_mem(ts);
> -	if (ret)
> +	error = axiom_zero_volatile_mem(ts);
> +	if (error)
>  		goto err_swreset;
>  
>  	/* Skip to the first fw chunk */
> @@ -2362,16 +2359,14 @@ axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  		chunk_len = axiom_cfg_fw_prepare_chunk(&chunk, data);
>  		if (axiom_skip_cfg_chunk(ts, &chunk)) {
>  			dev_dbg(dev, "Skip TH2CFG usage u%02X\n", chunk.usage_num);
> -			goto next_chunk;
> -		}
> -
> -		ret = axiom_write_cfg_chunk(ts, &chunk);
> -		if (ret) {
> -			axiom_set_runmode(ts, AXIOM_TCP_MODE);
> -			goto err_swreset;
> +		} else {
> +			error = axiom_write_cfg_chunk(ts, &chunk);
> +			if (error) {
> +				axiom_set_runmode(ts, AXIOM_TCP_MODE);
> +				goto err_swreset;
> +			}
>  		}
>  
> -next_chunk:
>  		data += chunk_len;
>  		size -= chunk_len;
>  		*written += chunk_len;
> @@ -2380,28 +2375,28 @@ axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  	axiom_set_runmode(ts, AXIOM_TCP_MODE);
>  
>  	/* Ensure that the chunks are written correctly */
> -	ret = axiom_verify_volatile_mem(ts);
> -	if (ret) {
> +	error = axiom_verify_volatile_mem(ts);
> +	if (error) {
>  		dev_err(dev, "Failed to verify written config, abort\n");
>  		goto err_swreset;
>  	}
>  
> -	ret = axiom_u02_save_config(ts);
> -	if (ret)
> +	error = axiom_u02_save_config(ts);
> +	if (error)
>  		goto err_swreset;
>  
>  	/*
>  	 * TODO: Check if u02 start would be sufficient to load the new config
>  	 * values
>  	 */
> -	ret = axiom_u02_swreset(ts);
> -	if (ret) {
> +	error = axiom_u02_swreset(ts);
> +	if (error) {
>  		dev_err(dev, "Soft reset failed\n");
>  		goto err_unlock;
>  	}
>  
> -	ret = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> -	if (ret)
> +	error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> +	if (error)
>  		goto err_unlock;
>  
>  	if (axiom_verify_crcs(ts))
> @@ -2410,8 +2405,8 @@ axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  	/* Unlock before the input device gets unregistered */
>  	axiom_unlock_input_device(ts);
>  
> -	ret = axiom_update_input_dev(ts);
> -	if (ret) {
> +	error = axiom_update_input_dev(ts);
> +	if (error) {
>  		dev_err(dev, "Input device update failed after TH2CFG firmware update\n");
>  		goto err_out;
>  	}
> @@ -2425,7 +2420,7 @@ axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
>  err_unlock:
>  	axiom_unlock_input_device(ts);
>  err_out:
> -	return ret == -ETIMEDOUT ? FW_UPLOAD_ERR_TIMEOUT : FW_UPLOAD_ERR_HW_ERROR;
> +	return error == -ETIMEDOUT ? FW_UPLOAD_ERR_TIMEOUT : FW_UPLOAD_ERR_HW_ERROR;
>  }
>  
>  static void axiom_cfg_fw_cancel(struct fw_upload *fw_upload)
> @@ -2433,9 +2428,8 @@ static void axiom_cfg_fw_cancel(struct fw_upload *fw_upload)
>  	struct axiom_data *ts = fw_upload->dd_handle;
>  	struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
>  
> -	mutex_lock(&afw->lock);
> +	guard(mutex)(&afw->lock);
>  	afw->cancel = true;
> -	mutex_unlock(&afw->lock);
>  }
>  
>  static void axiom_cfg_fw_cleanup(struct fw_upload *fw_upload)
> @@ -2474,7 +2468,7 @@ static int axiom_register_fwl(struct axiom_data *ts)
>  	struct device *dev = ts->dev;
>  	struct fw_upload *fwl;
>  	char *fw_name;
> -	int ret;
> +	int error;
>  
>  	if (!IS_ENABLED(CONFIG_FW_UPLOAD)) {
>  		dev_dbg(dev, "axfw and th2cfgbin update disabled\n");
> @@ -2490,9 +2484,9 @@ static int axiom_register_fwl(struct axiom_data *ts)
>  		return dev_err_probe(dev, PTR_ERR(fwl),
>  				     "Failed to register firmware upload\n");
>  
> -	ret = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
> -	if (ret)
> -		return ret;
> +	error = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
> +	if (error)
> +		return error;
>  
>  	ts->fw[AXIOM_FW_AXFW].fwl = fwl;
>  
> @@ -2505,9 +2499,9 @@ static int axiom_register_fwl(struct axiom_data *ts)
>  		return dev_err_probe(dev, PTR_ERR(fwl),
>  				     "Failed to register cfg firmware upload\n");
>  
> -	ret = devm_add_action_or_reset(dev, axiom_remove_cfg_fwl_action, ts);
> -	if (ret)
> -		return ret;
> +	error = devm_add_action_or_reset(dev, axiom_remove_cfg_fwl_action, ts);
> +	if (error)
> +		return error;
>  
>  	ts->fw[AXIOM_FW_CFG].fwl = fwl;
>  
> @@ -2643,7 +2637,7 @@ static int axiom_register_input_dev(struct axiom_data *ts,
>  	struct device *dev = ts->dev;
>  	struct i2c_client *client = to_i2c_client(dev);
>  	struct input_dev *input;
> -	int ret;
> +	int error;
>  
>  	input = input_allocate_device();
>  	if (!input) {
> @@ -2673,7 +2667,7 @@ static int axiom_register_input_dev(struct axiom_data *ts,
>  
>  	touchscreen_parse_properties(input, true, &ts->prop);
>  
> -	axiom_u42_get_touchslots(ts);
> +	axiom_get_touchslots(ts);
>  	if (!ts->num_slots && update_in_process) {
>  		input_free_device(input);
>  		/*
> @@ -2690,11 +2684,11 @@ static int axiom_register_input_dev(struct axiom_data *ts,
>  		return -EINVAL;
>  	}
>  
> -	ret = input_mt_init_slots(input, ts->num_slots, INPUT_MT_DIRECT);
> -	if (ret) {
> +	error = input_mt_init_slots(input, ts->num_slots, INPUT_MT_DIRECT);
> +	if (error) {
>  		input_free_device(input);
>  		dev_err(dev, "Failed to init mt slots\n");
> -		return ret;
> +		return error;
>  	}
>  
>  	/*
> @@ -2705,19 +2699,21 @@ static int axiom_register_input_dev(struct axiom_data *ts,
>  	 * always.
>  	 */
>  	if (!ts->irq_setup_done && client->irq) {
> -		ret = devm_request_threaded_irq(dev, client->irq, NULL, axiom_irq,
> -						IRQF_ONESHOT, dev_name(dev), ts);
> -		if (ret) {
> +		error = devm_request_threaded_irq(dev, client->irq,
> +						  NULL, axiom_irq,
> +						  IRQF_ONESHOT,
> +						  dev_name(dev), ts);
> +		if (error) {
>  			dev_err(dev, "Failed to request IRQ\n");
> -			return ret;
> +			return error;
>  		}
>  		ts->irq_setup_done = true;
>  	} else {
> -		ret = input_setup_polling(input, axiom_poll);
> -		if (ret) {
> +		error = input_setup_polling(input, axiom_poll);
> +		if (error) {
>  			input_free_device(input);
>  			dev_err(dev, "Setup polling mode failed\n");
> -			return ret;
> +			return error;
>  		}
>  
>  		input_set_poll_interval(input, ts->poll_interval);
> @@ -2726,14 +2722,14 @@ static int axiom_register_input_dev(struct axiom_data *ts,
>  	input_set_drvdata(input, ts);
>  	ts->input = input;
>  
> -	ret = input_register_device(input);
> -	if (ret) {
> +	error = input_register_device(input);
> +	if (error) {
>  		input_free_device(input);
>  		ts->input = NULL;
>  		dev_err(dev, "Failed to register input device\n");
>  	};
>  
> -	return ret;
> +	return error;
>  }
>  
>  static int axiom_update_input_dev(struct axiom_data *ts)
> @@ -2747,15 +2743,15 @@ static int axiom_parse_firmware(struct axiom_data *ts)
>  {
>  	struct device *dev = ts->dev;
>  	struct gpio_desc *gpio;
> -	int ret;
> +	int error;
>  
>  	ts->supplies[0].supply = "vddi";
>  	ts->supplies[1].supply = "vdda";
>  	ts->num_supplies = ARRAY_SIZE(ts->supplies);
>  
> -	ret = devm_regulator_bulk_get(dev, ts->num_supplies, ts->supplies);
> -	if (ret)
> -		return dev_err_probe(dev, ret,
> +	error = devm_regulator_bulk_get(dev, ts->num_supplies, ts->supplies);
> +	if (error)
> +		return dev_err_probe(dev, error,
>  				     "Failed to get power supplies\n");
>  
>  	gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
> @@ -2770,20 +2766,15 @@ static int axiom_parse_firmware(struct axiom_data *ts)
>  	return 0;
>  }
>  
> -static int axiom_power_device(struct axiom_data *ts, unsigned int enable)
> +static int axiom_power_up_device(struct axiom_data *ts)
>  {
>  	struct device *dev = ts->dev;
> -	int ret;
> -
> -	if (!enable) {
> -		regulator_bulk_disable(ts->num_supplies, ts->supplies);
> -		return 0;
> -	}
> +	int error;
>  
> -	ret = regulator_bulk_enable(ts->num_supplies, ts->supplies);
> -	if (ret) {
> +	error = regulator_bulk_enable(ts->num_supplies, ts->supplies);
> +	if (error) {
>  		dev_err(dev, "Failed to enable power supplies\n");
> -		return ret;
> +		return error;
>  	}
>  
>  	gpiod_set_value_cansleep(ts->reset_gpio, 1);
> @@ -2795,6 +2786,11 @@ static int axiom_power_device(struct axiom_data *ts, unsigned int enable)
>  	return 0;
>  }
>  
> +static void axiom_power_down_device(struct axiom_data *ts)
> +{
> +	regulator_bulk_disable(ts->num_supplies, ts->supplies);
> +}
> +
>  static int axiom_panel_prepared(struct drm_panel_follower *follower)
>  {
>  	struct axiom_data *ts = container_of(follower, struct axiom_data,
> @@ -2838,7 +2834,7 @@ static int axiom_i2c_probe(struct i2c_client *client)
>  {
>  	struct device *dev = &client->dev;
>  	struct axiom_data *ts;
> -	int ret;
> +	int error;
>  
>  	ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
>  	if (!ts)
> @@ -2857,53 +2853,56 @@ static int axiom_i2c_probe(struct i2c_client *client)
>  	init_completion(&ts->nvm_write.completion);
>  	mutex_init(&ts->fwupdate_lock);
>  
> -	ret = axiom_register_fwl(ts);
> -	if (ret)
> -		return ret;
> +	error = axiom_register_fwl(ts);
> +	if (error)
> +		return error;
>  
> -	ret = axiom_parse_firmware(ts);
> -	if (ret)
> -		return ret;
> +	error = axiom_parse_firmware(ts);
> +	if (error)
> +		return error;
>  
> -	ret = axiom_power_device(ts, 1);
> -	if (ret)
> -		return dev_err_probe(dev, ret, "Failed to power-on device\n");
> +	error = axiom_power_up_device(ts);
> +	if (error)
> +		return dev_err_probe(dev, error, "Failed to power-on device\n");
>  
>  	pm_runtime_set_autosuspend_delay(dev, 10 * MSEC_PER_SEC);
>  	pm_runtime_use_autosuspend(dev);
>  	pm_runtime_set_active(dev);
>  	pm_runtime_get_noresume(dev);
> -	ret = devm_pm_runtime_enable(dev);
> -	if (ret)
> -		return dev_err_probe(dev, ret, "Failed to enable pm-runtime\n");
> +	error = devm_pm_runtime_enable(dev);
> +	if (error)
> +		return dev_err_probe(dev, error, "Failed to enable pm-runtime\n");
>  
> -	ret = axiom_register_panel_follower(ts);
> -	if (ret)
> -		return dev_err_probe(dev, ret, "Failed to register panel follower\n");
> +	error = axiom_register_panel_follower(ts);
> +	if (error)
> +		return dev_err_probe(dev, error, "Failed to register panel follower\n");
>  
> -	ret = axiom_u31_device_discover(ts);
> +	error = axiom_u31_device_discover(ts);
>  	/*
>  	 * Register the device to allow FW updates in case that the current FW
>  	 * doesn't support the required driver usages or if the device is in
>  	 * bootloader mode.
>  	 */
> -	if (ret && ret == -EACCES && IS_ENABLED(CONFIG_FW_UPLOAD)) {
> -		dev_warn(dev, "Device discovery failed, wait for user fw update\n");
> -		pm_runtime_mark_last_busy(dev);
> -		pm_runtime_put_sync_autosuspend(dev);
> -		return 0;
> -	} else if (ret) {
> +	if (error) {
> +		if (error == -EACCES && IS_ENABLED(CONFIG_FW_UPLOAD)) {
> +			dev_warn(dev, "Device discovery failed, wait for user fw update\n");
> +			pm_runtime_mark_last_busy(dev);
> +			pm_runtime_put_sync_autosuspend(dev);
> +			return 0;
> +		}
>  		pm_runtime_put_sync(dev);
> -		return dev_err_probe(dev, ret, "Device discovery failed\n");
> +		return dev_err_probe(dev, error, "Device discovery failed\n");
>  	}
>  
> -	ret = axiom_register_input_dev(ts, false);
> +	error = axiom_register_input_dev(ts, false);
>  	pm_runtime_mark_last_busy(dev);
>  	pm_runtime_put_sync_autosuspend(dev);
> -	if (ret && IS_ENABLED(CONFIG_FW_UPLOAD))
> +	if (error) {
> +		if (!IS_ENABLED(CONFIG_FW_UPLOAD))
> +			return dev_err_probe(dev, error, "Failed to register input device\n");
> +
>  		dev_warn(dev, "Failed to register the input device, wait for user fw update\n");
> -	else if (ret)
> -		return dev_err_probe(dev, ret, "Failed to register input device\n");
> +	}
>  
>  	return 0;
>  }
> @@ -2923,18 +2922,20 @@ static int axiom_runtime_suspend(struct device *dev)
>  	if (client->irq && ts->irq_setup_done)
>  		disable_irq(client->irq);
>  
> -	return axiom_power_device(ts, 0);
> +	axiom_power_down_device(ts);
> +
> +	return 0;
>  }
>  
>  static int axiom_runtime_resume(struct device *dev)
>  {
>  	struct axiom_data *ts = dev_get_drvdata(dev);
>  	struct i2c_client *client = to_i2c_client(dev);
> -	int ret;
> +	int error;
>  
> -	ret = axiom_power_device(ts, 1);
> -	if (ret)
> -		return ret;
> +	error = axiom_power_up_device(ts);
> +	if (error)
> +		return error;
>  
>  	if (client->irq && ts->irq_setup_done)
>  		enable_irq(client->irq);
> 

-- 
#gernperDu 
#CallMeByMyFirstName

Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply

* Re: [PATCH v5 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-02-02  0:58 UTC (permalink / raw)
  To: Andrew Thomas
  Cc: linux-kernel@vger.kernel.org, devicetree@vger.kernel.org,
	linux-input@vger.kernel.org, Luis Chamberlain, Russ Weight,
	Greg Kroah-Hartman, Rafael J. Wysocki, Andrew Morton, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov,
	kamel.bouhara@bootlin.com, kernel@pengutronix.de,
	rydberg@bitmath.org, Danilo Krummrich
In-Reply-To: <LOYP123MB2701885B266AE2CC319CD9D49791A@LOYP123MB2701.GBRP123.PROD.OUTLOOK.COM>

Hi Andrew,

On 26-01-28, Andrew Thomas wrote:
> Hi Marco,
> 
> Thank you for your work on this patch in helping support TouchNetix
> devices.
> Currently we have quite similar patches, yours supporting firmware and
> config download, while mine supports SPI.
> Would you be happy if I went ahead and started implementing your work
> on top of mine to support firmware download over SPI?

Sorry but I don't get what you mean. IMHO the correct approach would be,
to take the most recent patchset and do some refactoring to add the SPI
communication support. The rest is already in place. You can use the
goodix_berlin_*.c as example.

Since Dimitry provided a very good review feedback and I touched many
code parts, I would advise to wait for my new series before starting
with the SPI refactoring work.

> I would strive to maintain all the features you have included since it
> is clear much of your driver is well thought out.

NACK, this is not how upstream works. We and the community already spend
much effort to review all parts. The way to add your SPI use-case is to
pick the patchset and add the SPI regmap read/write accessors.

> My hope was to include basic support for I2C and SPI then move onto
> the more substantial task of firmware and config download.

Sry. but again NACK. This driver supports already all use-cases incl.
many firmware update conor-cases. Please stop making the SPI use-case
such special, you need to add two accessors (read/write) and some code
refactoring.

> Since I have most of the aXiom range of devices available to me I will
> be able to test the firmware/config download with many of them.

It would be nice if you could test _this_ driver :) and provide your
feedback on this patchset.

> I have done a brief review below, however I have not yet tried
> firmware and config download through your driver, which I shall get
> onto soon.

Testing is always welcome if we speak about _this_ patchset.

> > #define AXIOM_U34                             0x34
> > #define   AXIOM_U34_REV1_OVERFLOW_MASK                BIT(7)
> > #define   AXIOM_U34_REV1_REPORTLENGTH_MASK    GENMASK(6, 0)
> > #define   AXIOM_U34_REV1_PREAMBLE_BYTES               2
> > #define   AXIOM_U34_REV1_POSTAMBLE_BYTES      4
> 
> The indentation is not aligned on all the defines. And below.

Please retrieve the patchset via b4 and open it in your editor. You will
see that the alignment is correct.

> > #define AXIOM_U42                             0x42
> > #define AXIOM_U42_REV1_REPORT_ID_CONTAINS(id) ((id) + 2)
> > #define   AXIOM_U42_REV1_REPORT_ID_TOUCH      1       /* Touch, Proximity, Hover */
> >
> > #define AXIOM_U42_REV4_REPORT_ID_CONTAINS(id)   ((id) + 8)
> > #define   AXIOM_U42_REV4_REPORT_ID_TOUCH      1       /* Touch, Proximity, Hover */
> 
> Could this be done with BIT and GENMASK? Since there may be many
> revisions to support, is there a cleaner way we can generalise
> revision handling?

This is a single 8-bit register, no fields, according the "Programmer's
Model" definition of REPORT_ID_CONTAINS[x] I have. So no, neither
the GENMASK() nor the BIT() macro is valid.

If TouchNetix shuffels registers or redefines register values from
revision to revision, we need to add the new revision meaning exactly
like we do here.

The driver is already multi-revision capable and new revisions can be
added easily.

> >       /* Skip processing if not in TCP mode */
> >       if ((axiom_get_runmode(ts) != AXIOM_TCP_MODE) &&
> >           (axiom_get_runmode(ts) != AXIOM_TCP_CFG_UPDATE_MODE))
> >               return 0;
> 
> Should the IRQ/POLL not be completely disabled if axiom is in either
> of these modes?

Neither of these modes == BOOTLOADER_MODE. This mode is used to update
the runtime FW. IMHO for such a rare case, the IRQ could stay on and we
leave the enable/disable to the device-core. Did you encountered IRQ
storms?

The Poll shouldn't be active, since the bootloader mode requires that
the input-device is idle (no active user). This means that the
poll-routine should be inactive.

Regards,
  Marco


> 
> Many Thanks,
> Andrew
> 
> ________________________________________
> From: Marco Felsch <m.felsch@pengutronix.de>
> Sent: 11 January 2026 3:05 PM
> To: Luis Chamberlain; Russ Weight; Greg Kroah-Hartman; Rafael J. Wysocki; Andrew Morton; Rob Herring; Krzysztof Kozlowski; Conor Dooley; Dmitry Torokhov; Kamel Bouhara; Marco Felsch; Henrik Rydberg; Danilo Krummrich; Danilo Krummrich
> Cc: linux-kernel@vger.kernel.org; devicetree@vger.kernel.org; linux-input@vger.kernel.org; kernel@pengutronix.de; Marco Felsch
> Subject: [PATCH v5 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
> 
> This adds the initial support for the TouchNetix AX54A touchcontroller
> which is part of TouchNetix's aXiom touchscreen controller family.
> 
> The TouchNetix aXiom family provides two physical interfaces: SPI and
> I2C. This patch covers only the I2C interface.
> 
> Apart the input event handling the driver supports firmware updates too.
> One firmware interface handles the touchcontroller firmware (AXFW and
> ALC) update the other handles the touchcontroller configuration
> (TH2CFGBIN) update.
> 
> Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
> ---
> .../testing/sysfs-driver-input-touchnetix-axiom | 81 +
> drivers/input/touchscreen/Kconfig | 17 +
> drivers/input/touchscreen/Makefile | 1 +
> drivers/input/touchscreen/touchnetix_axiom.c | 2974 ++++++++++++++++++++
> 4 files changed, 3073 insertions(+)
> 
> diff --git a/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom b/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom
> new file mode 100644
> index 0000000000000000000000000000000000000000..31c1c6510c55da80659ddf7bea2d0ce681fde323
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom
> @@ -0,0 +1,81 @@
> +What: /sys/bus/i2c/devices/xxx/fw_major
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the firmware major version provided by the touchscreen.
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/fw_minor
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the firmware minor version provided by the touchscreen.
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/fw_rc
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the firmware release canidate version provided by the touchscreen.
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/fw_status
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the firmware status provided by the touchscreen. It may
> + be either "release" or "engineering".
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/fw_variant
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the firmware variant provided by the touchscreen. It may
> + be either: "3d", "2d", "force" or "unknown".
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/device_id
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the touchscreen device id, for example: "54" for the AX54A.
> +
> + Access: Read
> +
> + Valid values: Represented as string
> +
> +What: /sys/bus/i2c/devices/xxx/device_state
> +Date: Jan 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Reports the touchscreen device current runtime state. The
> + following values are reported:
> +
> + discovery: Device is in discovery mode.
> + tcp: Device is in touch-control-protocol (tcp) mode. This is
> + the normal working mode.
> + th2cfg-update: Device is in configuration update mode.
> + bootloader-pre: Device bootloader mode enter was triggered
> + bootloader: Device is in bootloader mode, used for firmware
> + updates.
> + unknown: Device mode is unknown.
> +
> + Access: Read
> +
> + Valid values: Represented as string
> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
> index 7d5b72ee07fa1313da39a625b5129a0459720865..449ae5e29cb4bb1f5335afdee82e91f0aa30a209 100644
> --- a/drivers/input/touchscreen/Kconfig
> +++ b/drivers/input/touchscreen/Kconfig
> @@ -828,6 +828,23 @@ config TOUCHSCREEN_MIGOR
> To compile this driver as a module, choose M here: the
> module will be called migor_ts.
> 
> +config TOUCHSCREEN_TOUCHNETIX_AXIOM
> + tristate "TouchNetix aXiom based touchscreen controllers"
> + # We need to call into panel code so if DRM=m, this can't be 'y'
> + depends on DRM || !DRM
> + depends on I2C
> + select CRC16
> + select CRC32
> + select REGMAP_I2C
> + help
> + Say Y here if you have a axiom touchscreen connected to
> + your system.
> +
> + If unsure, say N.
> +
> + To compile this driver as a module, choose M here: the
> + module will be called touchnetix_axiom.
> +
> config TOUCHSCREEN_TOUCHRIGHT
> tristate "Touchright serial touchscreen"
> select SERIO
> diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
> index ab9abd151078831a4b22d6998e00ef74fe01c356..540df3ada4b2b6ad05ffeba67f44ff262f93c11f 100644
> --- a/drivers/input/touchscreen/Makefile
> +++ b/drivers/input/touchscreen/Makefile
> @@ -90,6 +90,7 @@ obj-$(CONFIG_TOUCHSCREEN_SUR40) += sur40.o
> obj-$(CONFIG_TOUCHSCREEN_SURFACE3_SPI) += surface3_spi.o
> obj-$(CONFIG_TOUCHSCREEN_TI_AM335X_TSC) += ti_am335x_tsc.o
> obj-$(CONFIG_TOUCHSCREEN_TOUCHIT213) += touchit213.o
> +obj-$(CONFIG_TOUCHSCREEN_TOUCHNETIX_AXIOM) += touchnetix_axiom.o
> obj-$(CONFIG_TOUCHSCREEN_TOUCHRIGHT) += touchright.o
> obj-$(CONFIG_TOUCHSCREEN_TOUCHWIN) += touchwin.o
> obj-$(CONFIG_TOUCHSCREEN_TS4800) += ts4800-ts.o
> diff --git a/drivers/input/touchscreen/touchnetix_axiom.c b/drivers/input/touchscreen/touchnetix_axiom.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..e8f56a8f7e8a83361b04bb858caadf9658fb7e05
> --- /dev/null
> +++ b/drivers/input/touchscreen/touchnetix_axiom.c
> @@ -0,0 +1,2974 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * TouchNetix aXiom Touchscreen Driver
> + *
> + * Copyright (C) 2024 Pengutronix
> + *
> + * Marco Felsch <kernel@pengutronix.de>
> + */
> +
> +#include <drm/drm_panel.h>
> +#include <linux/bitfield.h>
> +#include <linux/bits.h>
> +#include <linux/completion.h>
> +#include <linux/crc16.h>
> +#include <linux/crc32.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
> +#include <linux/firmware.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/i2c.h>
> +#include <linux/input.h>
> +#include <linux/input/mt.h>
> +#include <linux/input/touchscreen.h>
> +#include <linux/interrupt.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/time.h>
> +#include <linux/unaligned.h>
> +
> +/*
> + * Short introduction for developers:
> + * The programming manual is written based on u(sages):
> + * - Max. 0xff usages possible
> + * - A usage is a group of registers (0x00 ... 0xff)
> + * - The usage base address must be discovered (FW dependent)
> + * - Partial RW usage access is allowed
> + * - Each usage has a revision (FW dependent)
> + * - Only u31 is always at address 0x0 (used for discovery)
> + *
> + * E.x. Reading register 0x01 for usage u03 with baseaddr 0x20 results in the
> + * following physical 16bit I2C address: 0x2001.
> + *
> + * Note the datasheet specifies the usage numbers in hex and the internal
> + * offsets in decimal. Keep it that way to make it more developer friendly.
> + */
> +#define AXIOM_U01 0x01
> +#define AXIOM_U01_REV1_REPORTTYPE_REG 0
> +#define AXIOM_U01_REV1_REPORTTYPE_HELLO 0
> +#define AXIOM_U01_REV1_REPORTTYPE_HEARTBEAT 1
> +#define AXIOM_U01_REV1_REPORTTYPE_OPCOMPLETE 3
> +
> +#define AXIOM_U02 0x02
> +#define AXIOM_U02_REV1_COMMAND_REG 0
> +#define AXIOM_U02_REV1_CMD_HARDRESET 0x0001
> +#define AXIOM_U02_REV1_CMD_SOFTRESET 0x0002
> +#define AXIOM_U02_REV1_CMD_STOP 0x0005
> +#define AXIOM_U02_REV1_CMD_SAVEVLTLCFG2NVM 0x0007
> +#define AXIOM_U02_REV1_PARAM1_SAVEVLTLCFG2NVM 0xb10c
> +#define AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM 0xc0de
> +#define AXIOM_U02_REV1_CMD_HANDSHAKENVM 0x0008
> +#define AXIOM_U02_REV1_CMD_COMPUTECRCS 0x0009
> +#define AXIOM_U02_REV1_CMD_FILLCONFIG 0x000a
> +#define AXIOM_U02_REV1_PARAM0_FILLCONFIG 0x5555
> +#define AXIOM_U02_REV1_PARAM1_FILLCONFIG 0xaaaa
> +#define AXIOM_U02_REV1_PARAM2_FILLCONFIG_ZERO 0xa55a
> +#define AXIOM_U02_REV1_CMD_ENTERBOOTLOADER 0x000b
> +#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1 0x5555
> +#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2 0xaaaa
> +#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3 0xa55a
> +#define AXIOM_U02_REV1_RESP_SUCCESS 0x0000
> +
> +struct axiom_u02_rev1_system_manager_msg {
> + union {
> + __le16 command;
> + __le16 response;
> + };
> + __le16 parameters[3];
> +};
> +
> +#define AXIOM_U04 0x04
> +#define AXIOM_U04_REV1_SIZE_BYTES 128
> +
> +#define AXIOM_U05 0x05 /* CDU */
> +
> +#define AXIOM_U22 0x22 /* CDU */
> +
> +#define AXIOM_U31 0x31
> +#define AXIOM_U31_REV1_PAGE0 0x0000
> +#define AXIOM_U31_REV1_DEVICE_ID_LOW_REG (AXIOM_U31_REV1_PAGE0 + 0)
> +#define AXIOM_U31_REV1_DEVICE_ID_HIGH_REG (AXIOM_U31_REV1_PAGE0 + 1)
> +#define AXIOM_U31_REV1_MODE_MASK BIT(7)
> +#define AXIOM_U31_REV1_MODE_BLP 1
> +#define AXIOM_U31_REV1_DEVICE_ID_HIGH_MASK GENMASK(6, 0)
> +#define AXIOM_U31_REV1_RUNTIME_FW_MIN_REG (AXIOM_U31_REV1_PAGE0 + 2)
> +#define AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG (AXIOM_U31_REV1_PAGE0 + 3)
> +#define AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG (AXIOM_U31_REV1_PAGE0 + 4)
> +#define AXIOM_U31_REV1_RUNTIME_FW_STATUS BIT(7)
> +#define AXIOM_U31_REV1_RUNTIME_FW_VARIANT GENMASK(6, 0)
> +#define AXIOM_U31_REV1_JEDEC_ID_LOW_REG (AXIOM_U31_REV1_PAGE0 + 8)
> +#define AXIOM_U31_REV1_JEDEC_ID_HIGH_REG (AXIOM_U31_REV1_PAGE0 + 9)
> +#define AXIOM_U31_REV1_NUM_USAGES_REG (AXIOM_U31_REV1_PAGE0 + 10)
> +#define AXIOM_U31_REV1_RUNTIME_FW_RC_REG (AXIOM_U31_REV1_PAGE0 + 11)
> +#define AXIOM_U31_REV1_RUNTIME_FW_RC_MASK GENMASK(7, 4)
> +#define AXIOM_U31_REV1_SILICON_REV_MASK GENMASK(3, 0)
> +
> +#define AXIOM_U31_REV1_PAGE1 0x0100
> +#define AXIOM_U31_REV1_OFFSET_TYPE_MASK BIT(7)
> +#define AXIOM_U31_REV1_MAX_OFFSET_MASK GENMASK(6, 0)
> +
> +#define AXIOM_U32 0x32
> +
> +struct axiom_u31_usage_table_entry {
> + u8 usage_num;
> + u8 start_page;
> + u8 num_pages;
> + u8 max_offset;
> + u8 uifrevision;
> + u8 reserved;
> +} __packed;
> +
> +#define AXIOM_U33 0x33
> +
> +struct axiom_u33_rev2 {
> + __le32 runtime_crc;
> + __le32 runtime_nvm_crc;
> + __le32 bootloader_crc;
> + __le32 nvltlusageconfig_crc;
> + __le32 vltusageconfig_crc;
> + __le32 u22_sequencedata_crc;
> + __le32 u43_hotspots_crc;
> + __le32 u93_profiles_crc;
> + __le32 u94_deltascalemap_crc;
> + __le32 runtimehash_crc;
> +};
> +
> +struct axiom_u33_rev3 {
> + __le32 runtime_crc;
> + __le32 runtime_nvm_crc;
> + __le32 bootloader_crc;
> + __le32 nvltlusageconfig_crc;
> + __le32 vltusageconfig_crc;
> + __le32 u22_sequencedata_crc;
> + __le32 u43_hotspots_crc;
> + __le32 u77_dod_data_crc;
> + __le32 u93_profiles_crc;
> + __le32 u94_deltascalemap_crc;
> + __le32 runtimehash_crc;
> +};
> +
> +#define AXIOM_U34 0x34
> +#define AXIOM_U34_REV1_OVERFLOW_MASK BIT(7)
> +#define AXIOM_U34_REV1_REPORTLENGTH_MASK GENMASK(6, 0)
> +#define AXIOM_U34_REV1_PREAMBLE_BYTES 2
> +#define AXIOM_U34_REV1_POSTAMBLE_BYTES 4
> +
> +#define AXIOM_U36 0x36
> +
> +#define AXIOM_U41 0x41
> +#define AXIOM_U41_REV2_TARGETSTATUS_REG 0
> +#define AXIOM_U41_REV2_X_REG(id) ((4 * (id)) + 2)
> +#define AXIOM_U41_REV2_Y_REG(id) ((4 * (id)) + 4)
> +#define AXIOM_U41_REV2_Z_REG(id) ((id) + 42)
> +
> +#define AXIOM_U42 0x42
> +#define AXIOM_U42_REV1_REPORT_ID_CONTAINS(id) ((id) + 2)
> +#define AXIOM_U42_REV1_REPORT_ID_TOUCH 1 /* Touch, Proximity, Hover */
> +
> +#define AXIOM_U42_REV4_REPORT_ID_CONTAINS(id) ((id) + 8)
> +#define AXIOM_U42_REV4_REPORT_ID_TOUCH 1 /* Touch, Proximity, Hover */
> +
> +#define AXIOM_U43 0x43 /* CDU */
> +
> +#define AXIOM_U64 0x64
> +#define AXIOM_U64_REV2_ENABLECDSPROCESSING_REG 0
> +#define AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK BIT(0)
> +
> +#define AXIOM_U77 0x77 /* CDU */
> +#define AXIOM_U82 0x82
> +#define AXIOM_U93 0x93 /* CDU */
> +#define AXIOM_U94 0x94 /* CDU */
> +
> +/*
> + * Axiom CDU usage structure copied from downstream CDU_Common.py. Downstream
> + * doesn't mention any revision. According downstream all CDU register windows
> + * are 56 byte wide (8 byte header + 48 byte data).
> + */
> +#define AXIOM_CDU_CMD_STORE 0x0002
> +#define AXIOM_CDU_CMD_COMMIT 0x0003
> +#define AXIOM_CDU_PARAM0_COMMIT 0xb10c
> +#define AXIOM_CDU_PARAM1_COMMIT 0xc0de
> +
> +#define AXIOM_CDU_RESP_SUCCESS 0x0000
> +#define AXIOM_CDU_MAX_DATA_BYTES 48
> +
> +struct axiom_cdu_usage {
> + union {
> + __le16 command;
> + __le16 response;
> + };
> + __le16 parameters[3];
> + u8 data[AXIOM_CDU_MAX_DATA_BYTES];
> +};
> +
> +/*
> + * u01 for the bootloader protocol (BLP)
> + *
> + * Values taken from Bootloader.py<http://Bootloader.py> [1] which had a comment that documentation
> + * values are out dated. The BLP does not have different versions according the
> + * documentation python helper.
> + *
> + * [1] https://github.com/TouchNetix/axiom_pylib<https://github.com/TouchNetix/axiom_pylib>
> + */
> +#define AXIOM_U01_BLP_COMMAND_REG 0x0100
> +#define AXIOM_U01_BLP_COMMAND_RESET BIT(1)
> +#define AXIOM_U01_BLP_SATUS_REG 0x0100
> +#define AXIOM_U01_BLP_STATUS_BUSY BIT(0)
> +#define AXIOM_U01_BLP_FIFO_REG 0x0102
> +#define AXIOM_U01_BLP_FIFO_CHK_SIZE_BYTES 255
> +
> +#define AXIOM_PROX_LEVEL -128
> +#define AXIOM_STARTUP_TIME_MS 110
> +
> +#define AXIOM_USAGE_BASEADDR_MASK GENMASK(15, 8)
> +#define AXIOM_MAX_USAGES 256 /* u00 - uFF */
> +/*
> + * The devices have a 16bit ADC but Touchnetix used the lower two bits for other
> + * information.
> + */
> +#define AXIOM_MAX_XY (65535 - 3)
> +#define AXIOM_DEFAULT_POLL_INTERVAL_MS 10
> +#define AXIOM_PAGE_BYTE_LEN 256
> +#define AXIOM_MAX_XFERLEN 0x7fff
> +#define AXIOM_MAX_TOUCHSLOTS 10
> +#define AXIOM_MAX_TOUCHSLOTS_MASK GENMASK(9, 0)
> +
> +/* aXiom firmware (.axfw) */
> +#define AXIOM_FW_AXFW_SIGNATURE "AXFW"
> +#define AXIOM_FW_AXFW_FILE_FMT_VER 0x0200
> +
> +struct axiom_fw_axfw_hdr {
> + u8 signature[4];
> + __le32 file_crc32;
> + __le16 file_format_ver;
> + __le16 device_id;
> + u8 variant;
> + u8 minor_ver;
> + u8 major_ver;
> + u8 rc_ver;
> + u8 status;
> + __le16 silicon_ver;
> + u8 silicon_rev;
> + __le32 fw_crc32;
> +} __packed;
> +
> +struct axiom_fw_axfw_chunk_hdr {
> + u8 internal[6]; /* no description */
> + __be16 payload_length;
> +};
> +
> +/* aXiom config (.th2cfgbin) */
> +#define AXIOM_FW_CFG_SIGNATURE 0x20071969
> +
> +struct axiom_fw_cfg_hdr {
> + __be32 signature;
> + __le16 file_format_ver;
> + __le16 tcp_file_rev_major;
> + __le16 tcp_file_rev_minor;
> + __le16 tcp_file_rev_patch;
> + u8 tcp_version;
> +} __packed;
> +
> +struct axiom_fw_cfg_chunk_hdr {
> + u8 usage_num;
> + u8 usage_rev;
> + u8 reserved;
> + __le16 usage_length;
> +} __packed;
> +
> +struct axiom_fw_cfg_chunk {
> + u8 usage_num;
> + u8 usage_rev;
> + u16 usage_length;
> + const u8 *usage_content;
> +};
> +
> +enum axiom_fw_type {
> + AXIOM_FW_AXFW,
> + AXIOM_FW_CFG,
> + AXIOM_FW_NUM
> +};
> +
> +enum axiom_crc_type {
> + AXIOM_CRC_CUR,
> + AXIOM_CRC_NEW,
> + AXIOM_CRC_NUM
> +};
> +
> +struct axiom_data;
> +
> +struct axiom_usage_info {
> + unsigned char usage_num; /* uXX number (XX in hex) */
> + unsigned int rev_num; /* rev.X (X in dec) */
> + bool is_cdu;
> + bool is_ro;
> +
> + /* Optional hooks */
> + int (*process_report)(struct axiom_data *ts, const u8 *buf, size_t bufsize);
> +};
> +
> +enum axiom_runmode {
> + AXIOM_DISCOVERY_MODE,
> + AXIOM_TCP_MODE,
> + AXIOM_TCP_CFG_UPDATE_MODE,
> + AXIOM_BLP_PRE_MODE,
> + AXIOM_BLP_MODE,
> +};
> +
> +struct axiom_data {
> + struct input_dev *input;
> + struct device *dev;
> +
> + struct gpio_desc *reset_gpio;
> + struct regulator_bulk_data supplies[2];
> + unsigned int num_supplies;
> +
> + struct regmap *regmap;
> + struct touchscreen_properties prop;
> + bool irq_setup_done;
> + u32 poll_interval;
> +
> + struct drm_panel_follower panel_follower;
> + bool is_panel_follower;
> +
> + enum axiom_runmode mode;
> + /*
> + * Two completion types to support firmware updates
> + * in irq and poll mode.
> + */
> + struct axiom_completion {
> + struct completion completion;
> + bool poll_done;
> + } nvm_write, boot_complete;
> +
> + /* Lock to protect both firmware interfaces */
> + struct mutex fwupdate_lock;
> + struct axiom_firmware {
> + /* Lock to protect cancel */
> + struct mutex lock;
> + bool cancel;
> + struct fw_upload *fwl;
> + } fw[AXIOM_FW_NUM];
> +
> + unsigned int fw_major;
> + unsigned int fw_minor;
> + unsigned int fw_rc;
> + unsigned int fw_status;
> + unsigned int fw_variant;
> + u16 device_id;
> + u16 jedec_id;
> + u8 silicon_rev;
> +
> + /* CRCs we need to check during a config update */
> + struct axiom_crc {
> + u32 runtime;
> + u32 vltusageconfig;
> + u32 nvltlusageconfig;
> + u32 u22_sequencedata;
> + u32 u43_hotspots;
> + u32 u77_dod_data;
> + u32 u93_profiles;
> + u32 u94_deltascalemap;
> + } crc[AXIOM_CRC_NUM];
> +
> + bool cds_enabled;
> + unsigned long enabled_slots;
> + unsigned int num_slots;
> +
> + unsigned int max_report_byte_len;
> + struct axiom_usage_table_entry {
> + bool populated;
> + unsigned int baseaddr;
> + unsigned int size_bytes;
> + const struct axiom_usage_info *info;
> + } usage_table[AXIOM_MAX_USAGES];
> +};
> +
> +static int axiom_u01_rev1_process_report(struct axiom_data *ts, const u8 *buf,
> + size_t bufsize);
> +static int axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf,
> + size_t bufsize);
> +static int axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf,
> + size_t bufsize);
> +
> +#define AXIOM_USAGE(num, rev) \
> + { \
> + .usage_num = num, \
> + .rev_num = rev, \
> + }
> +
> +#define AXIOM_RO_USAGE(num, rev) \
> + { \
> + .usage_num = num, \
> + .rev_num = rev, \
> + .is_ro = true, \
> + }
> +
> +#define AXIOM_CDU_USAGE(num, rev) \
> + { \
> + .usage_num = num, \
> + .rev_num = rev, \
> + .is_cdu = true, \
> + }
> +
> +#define AXIOM_REPORT_USAGE(num, rev, func) \
> + { \
> + .usage_num = num, \
> + .rev_num = rev, \
> + .process_report = func, \
> + }
> +
> +#define AXIOM_USAGE_REV_UNUSED (-1)
> +
> +/*
> + * All usages used by driver must be added to this list to ensure the correct
> + * communictation with the devices. The list can contain multiple entries of the
> + * same usage to handle different usage revisions.
> + *
> + * Note:
> + * During a th2cfgbin update the driver may use usages not listed here.
> + * Therefore the th2cfgbin update compares the current running FW again the
> + * th2cfgbin targets FW.
> + */
> +static const struct axiom_usage_info driver_required_usages[] = {
> + AXIOM_REPORT_USAGE(AXIOM_U01, 1, axiom_u01_rev1_process_report),
> + AXIOM_REPORT_USAGE(AXIOM_U01, 3, axiom_u01_rev1_process_report),
> + AXIOM_USAGE(AXIOM_U02, 1),
> + AXIOM_USAGE(AXIOM_U02, 2),
> + AXIOM_USAGE(AXIOM_U04, 1),
> + AXIOM_RO_USAGE(AXIOM_U33, 2),
> + AXIOM_RO_USAGE(AXIOM_U33, 3),
> + AXIOM_REPORT_USAGE(AXIOM_U34, 1, axiom_u34_rev1_process_report),
> + AXIOM_REPORT_USAGE(AXIOM_U41, 2, axiom_u41_rev2_process_report),
> + AXIOM_REPORT_USAGE(AXIOM_U41, 4, axiom_u41_rev2_process_report),
> + AXIOM_USAGE(AXIOM_U42, 1),
> + AXIOM_USAGE(AXIOM_U42, 4),
> + AXIOM_USAGE(AXIOM_U64, 2),
> + AXIOM_USAGE(AXIOM_U64, 4),
> + { /* sentinel */ }
> +};
> +
> +/*
> + * All usages below are unused but the driver needs to know the type (ro, cdu)
> + * to handle them correctly. Unfortunately the type is not discoverable. Once
> + * a usage is actually used, it must be shifted to driver_required_usages and
> + * the revision must be set accordingly.
> + */
> +static const struct axiom_usage_info driver_additional_usages[] = {
> + AXIOM_CDU_USAGE(AXIOM_U05, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_CDU_USAGE(AXIOM_U22, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_RO_USAGE(AXIOM_U31, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_RO_USAGE(AXIOM_U32, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_RO_USAGE(AXIOM_U36, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_CDU_USAGE(AXIOM_U43, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_CDU_USAGE(AXIOM_U77, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_RO_USAGE(AXIOM_U82, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_CDU_USAGE(AXIOM_U93, AXIOM_USAGE_REV_UNUSED),
> + AXIOM_CDU_USAGE(AXIOM_U94, AXIOM_USAGE_REV_UNUSED),
> + { /* sentinel */ }
> +};
> +
> +/************************ Common helpers **************************************/
> +
> +static void axiom_set_runmode(struct axiom_data *ts, enum axiom_runmode mode)
> +{
> + ts->mode = mode;
> +}
> +
> +static enum axiom_runmode axiom_get_runmode(struct axiom_data *ts)
> +{
> + return ts->mode;
> +}
> +
> +static const char *axiom_runmode_to_string(struct axiom_data *ts)
> +{
> + switch (ts->mode) {
> + case AXIOM_DISCOVERY_MODE: return "discovery";
> + case AXIOM_TCP_MODE: return "tcp";
> + case AXIOM_TCP_CFG_UPDATE_MODE: return "th2cfg-update";
> + case AXIOM_BLP_PRE_MODE: return "bootloader-pre";
> + case AXIOM_BLP_MODE: return "bootlaoder";
> + default: return "unknown";
> + }
> +}
> +
> +static bool axiom_skip_usage_check(struct axiom_data *ts)
> +{
> + switch (ts->mode) {
> + case AXIOM_TCP_CFG_UPDATE_MODE:
> + case AXIOM_DISCOVERY_MODE:
> + case AXIOM_BLP_MODE:
> + return true;
> + case AXIOM_BLP_PRE_MODE:
> + case AXIOM_TCP_MODE:
> + default:
> + return false;
> + }
> +}
> +
> +static unsigned int
> +axiom_usage_baseaddr(struct axiom_data *ts, unsigned char usage_num)
> +{
> + return ts->usage_table[usage_num].baseaddr;
> +}
> +
> +static unsigned int
> +axiom_usage_size(struct axiom_data *ts, unsigned char usage_num)
> +{
> + return ts->usage_table[usage_num].size_bytes;
> +}
> +
> +static int
> +axiom_usage_rev(struct axiom_data *ts, unsigned char usage_num)
> +{
> + struct axiom_usage_table_entry *entry = &ts->usage_table[usage_num];
> +
> + if (!entry->info)
> + return -EINVAL;
> +
> + return entry->info->rev_num;
> +}
> +
> +static bool
> +axiom_driver_supports_usage(struct axiom_data *ts, unsigned char usage_num)
> +{
> + const struct axiom_usage_info *iter = driver_required_usages;
> + struct device *dev = ts->dev;
> + int rev;
> +
> + /*
> + * Some features depend on the current running firmware. Don't print an
> + * error if the usage for an optional feature is missing.
> + */
> + if (!ts->usage_table[usage_num].populated) {
> + dev_dbg(dev, "u%02X is not supported by the current firmware\n",
> + usage_num);
> + return false;
> + }
> +
> + rev = axiom_usage_rev(ts, usage_num);
> + if (rev < 0) {
> + dev_warn(dev, "Driver doesn't support u%02X yet\n", usage_num);
> + return false;
> + }
> +
> + for (; iter; iter++) {
> + if (iter->usage_num != usage_num)
> + continue;
> +
> + if (iter->rev_num == rev)
> + return true;
> + }
> +
> + dev_warn(dev, "Driver doesn't support u%02X rev.%d yet\n",
> + usage_num, rev);
> +
> + return false;
> +}
> +
> +static bool
> +axiom_usage_entry_is_report(struct axiom_u31_usage_table_entry *entry)
> +{
> + return entry->num_pages == 0;
> +}
> +
> +static unsigned int
> +axiom_get_usage_size_bytes(struct axiom_u31_usage_table_entry *entry)
> +{
> + unsigned char max_offset;
> +
> + max_offset = FIELD_GET(AXIOM_U31_REV1_MAX_OFFSET_MASK,
> + entry->max_offset) + 1;
> + max_offset *= 2;
> +
> + if (axiom_usage_entry_is_report(entry))
> + return max_offset;
> +
> + if (FIELD_GET(AXIOM_U31_REV1_OFFSET_TYPE_MASK, entry->max_offset))
> + return (entry->num_pages - 1) * AXIOM_PAGE_BYTE_LEN + max_offset;
> +
> + return max_offset;
> +}
> +
> +static void axiom_dump_usage_entry(struct device *dev,
> + struct axiom_u31_usage_table_entry *entry)
> +{
> + unsigned int page_len, total_len;
> +
> + total_len = axiom_get_usage_size_bytes(entry);
> +
> + if (total_len > AXIOM_PAGE_BYTE_LEN)
> + page_len = AXIOM_PAGE_BYTE_LEN;
> + else
> + page_len = total_len;
> +
> + if (axiom_usage_entry_is_report(entry))
> + dev_dbg(dev,
> + "u%02X rev.%d total-len:%u [REPORT]\n",
> + entry->usage_num, entry->uifrevision, total_len);
> + else
> + dev_dbg(dev,
> + "u%02X rev.%d first-page:%#02x page-len:%u num-pages:%u total-len:%u\n",
> + entry->usage_num, entry->uifrevision, entry->start_page, page_len,
> + entry->num_pages, total_len);
> +}
> +
> +static const struct axiom_usage_info *
> +axiom_get_usage_info(struct axiom_u31_usage_table_entry *query)
> +{
> + const struct axiom_usage_info *info = driver_required_usages;
> + bool required = false;
> + bool found = false;
> +
> + for (; info->usage_num; info++) {
> + /* Skip all usages not used by the driver */
> + if (query->usage_num != info->usage_num)
> + continue;
> +
> + /* The usage is used so we need to mark it as required */
> + required = true;
> +
> + /* Continue with the next usage if the revision doesn't match */
> + if (query->uifrevision != info->rev_num)
> + continue;
> +
> + found = true;
> + break;
> + }
> +
> + if (found)
> + return info;
> +
> + /* Return an error if not found but required */
> + if (required)
> + return ERR_PTR(-EINVAL);
> +
> + info = driver_additional_usages;
> + for (; info->usage_num; info++) {
> + if (query->usage_num != info->usage_num)
> + continue;
> +
> + /*
> + * No need to check the revision since these usages are not
> + * used actually but the driver needs the type information.
> + */
> + return info;
> + }
> +
> + /* No info found */
> + return NULL;
> +}
> +
> +static bool axiom_usage_supported(struct axiom_data *ts, unsigned int baseaddr)
> +{
> + struct axiom_usage_table_entry *entry;
> + unsigned int i;
> +
> + if (axiom_skip_usage_check(ts))
> + return true;
> +
> + dev_dbg(ts->dev, "Checking support for baseaddr: %#x\n", baseaddr);
> +
> + for (i = 0; i < ARRAY_SIZE(ts->usage_table); i++) {
> + entry = &ts->usage_table[i];
> +
> + if (!entry->populated)
> + continue;
> +
> + if (entry->baseaddr != baseaddr)
> + continue;
> +
> + break;
> + }
> +
> + if (i == ARRAY_SIZE(ts->usage_table)) {
> + dev_warn(ts->dev, "Usage not found\n");
> + return false;
> + }
> +
> + if (!entry->info)
> + dev_warn(ts->dev, "Unsupported usage u%02X used, driver bug!", i);
> +
> + return !!entry->info;
> +}
> +
> +static void axiom_poll(struct input_dev *input);
> +
> +static unsigned long
> +axiom_wait_for_completion_timeout(struct axiom_data *ts, struct axiom_completion *x,
> + long timeout)
> +{
> + struct i2c_client *client = to_i2c_client(ts->dev);
> + unsigned long poll_timeout;
> +
> + if (client->irq)
> + return wait_for_completion_timeout(&x->completion, timeout);
> +
> + /*
> + * Only firmware update cases do wait for completion. Since they require
> + * the input device to be closed, the poller is not running. So we need
> + * to do the polling manually.
> + */
> + poll_timeout = timeout / 10;
> +
> + /*
> + * Very basic and not very accurate but it does the job because there
> + * are no known timeout constraints.
> + */
> + do {
> + axiom_poll(ts->input);
> + fsleep(jiffies_to_usecs(poll_timeout));
> + if (x->poll_done)
> + break;
> + timeout -= poll_timeout;
> + } while (timeout > 0);
> +
> + x->poll_done = false;
> +
> + return timeout > 0 ? timeout : 0;
> +}
> +
> +static void axiom_complete(struct axiom_data *ts, struct axiom_completion *x)
> +{
> + struct i2c_client *client = to_i2c_client(ts->dev);
> +
> + if (client->irq)
> + complete(&x->completion);
> + else
> + x->poll_done = true;
> +}
> +
> +/*************************** Usage handling ***********************************/
> +/*
> + * Wrapper functions to handle the usage access. Wrappers are used to add
> + * different revision handling later on more easily.
> + */
> +static int axiom_u02_wait_idle(struct axiom_data *ts)
> +{
> + unsigned int reg;
> + int ret, _ret;
> + u16 cmd;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U02);
> + reg += AXIOM_U02_REV1_COMMAND_REG;
> +
> + /*
> + * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
> + * the last command successfully completed and the device is idle.
> + */
> + ret = read_poll_timeout(regmap_raw_read, _ret,
> + _ret || cmd == AXIOM_U02_REV1_RESP_SUCCESS,
> + 10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> + ts->regmap, reg, &cmd, 2);
> + if (ret)
> + dev_err(ts->dev, "Poll u02 timedout with: %#x\n", cmd);
> +
> + return ret;
> +}
> +
> +static int
> +axiom_u02_send_msg(struct axiom_data *ts,
> + const struct axiom_u02_rev1_system_manager_msg *msg,
> + bool validate_response)
> +{
> + unsigned int reg;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U02);
> + reg += AXIOM_U02_REV1_COMMAND_REG;
> +
> + ret = regmap_raw_write(ts->regmap, reg, msg, sizeof(*msg));
> + if (ret)
> + return ret;
> +
> + if (!validate_response)
> + return 0;
> +
> + return axiom_u02_wait_idle(ts);
> +}
> +
> +static int
> +axiom_u02_rev1_send_single_cmd(struct axiom_data *ts, u16 cmd)
> +{
> + struct axiom_u02_rev1_system_manager_msg msg = {
> + .command = cpu_to_le16(cmd)
> + };
> +
> + return axiom_u02_send_msg(ts, &msg, true);
> +}
> +
> +static int axiom_u02_handshakenvm(struct axiom_data *ts)
> +{
> + return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_HANDSHAKENVM);
> +}
> +
> +static int axiom_u02_computecrc(struct axiom_data *ts)
> +{
> + return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_COMPUTECRCS);
> +}
> +
> +static int axiom_u02_stop(struct axiom_data *ts)
> +{
> + return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_STOP);
> +}
> +
> +static int axiom_u02_save_config(struct axiom_data *ts)
> +{
> + struct axiom_u02_rev1_system_manager_msg msg;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_SAVEVLTLCFG2NVM);
> + msg.parameters[0] = 0; /* Don't care */
> + msg.parameters[1] = cpu_to_le16(AXIOM_U02_REV1_PARAM1_SAVEVLTLCFG2NVM);
> + msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM);
> +
> + ret = axiom_u02_send_msg(ts, &msg, false);
> + if (ret)
> + return ret;
> +
> + /* Downstream axcfg.py<http://axcfg.py> waits for 2sec without checking U01 response */
> + ret = axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> + msecs_to_jiffies(2 * MSEC_PER_SEC));
> + if (!ret)
> + dev_err(ts->dev, "Error save volatile config timedout\n");
> +
> + return ret ? 0 : -ETIMEDOUT;
> +}
> +
> +static int axiom_u02_swreset(struct axiom_data *ts)
> +{
> + struct axiom_u02_rev1_system_manager_msg msg = { };
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_SOFTRESET);
> + ret = axiom_u02_send_msg(ts, &msg, false);
> + if (ret)
> + return ret;
> +
> + /*
> + * Downstream axcfg.py<http://axcfg.py> waits for 1sec without checking U01 hello. Tests
> + * showed that waiting for the hello message isn't enough therefore we
> + * need both to make it robuster.
> + */
> + ret = axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
> + msecs_to_jiffies(1 * MSEC_PER_SEC));
> + if (!ret)
> + dev_err(ts->dev, "Error swreset timedout\n");
> +
> + fsleep(USEC_PER_SEC);
> +
> + return ret ? 0 : -ETIMEDOUT;
> +}
> +
> +static int axiom_u02_fillconfig(struct axiom_data *ts)
> +{
> + struct axiom_u02_rev1_system_manager_msg msg;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_FILLCONFIG);
> + msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_FILLCONFIG);
> + msg.parameters[1] = cpu_to_le16(AXIOM_U02_REV1_PARAM1_FILLCONFIG);
> + msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_FILLCONFIG_ZERO);
> +
> + return axiom_u02_send_msg(ts, &msg, true);
> +}
> +
> +static int axiom_u02_enter_bootloader(struct axiom_data *ts)
> +{
> + struct axiom_u02_rev1_system_manager_msg msg = { };
> + struct device *dev = ts->dev;
> + unsigned int val;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> + return -EINVAL;
> +
> + /*
> + * Enter the bootloader mode requires 3 consecutive messages so we can't
> + * check for the response.
> + */
> + msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_ENTERBOOTLOADER);
> + msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1);
> + ret = axiom_u02_send_msg(ts, &msg, false);
> + if (ret) {
> + dev_err(dev, "Failed to send bootloader-key1: %d\n", ret);
> + return ret;
> + }
> +
> + msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2);
> + ret = axiom_u02_send_msg(ts, &msg, false);
> + if (ret) {
> + dev_err(dev, "Failed to send bootloader-key2: %d\n", ret);
> + return ret;
> + }
> +
> + msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3);
> + ret = axiom_u02_send_msg(ts, &msg, false);
> + if (ret) {
> + dev_err(dev, "Failed to send bootloader-key3: %d\n", ret);
> + return ret;
> + }
> +
> + /* Sleep before the first read to give the device time */
> + fsleep(250 * USEC_PER_MSEC);
> +
> + /* Wait till the device reports it is in bootloader mode */
> + return regmap_read_poll_timeout(ts->regmap,
> + AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, val,
> + FIELD_GET(AXIOM_U31_REV1_MODE_MASK, val) ==
> + AXIOM_U31_REV1_MODE_BLP, 250 * USEC_PER_MSEC,
> + USEC_PER_SEC);
> +}
> +
> +static int axiom_u04_get(struct axiom_data *ts, u8 **_buf)
> +{
> + u8 buf[AXIOM_U04_REV1_SIZE_BYTES];
> + unsigned int reg;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U04))
> + return -EINVAL;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U04);
> + ret = regmap_raw_read(ts->regmap, reg, buf, sizeof(buf));
> + if (ret)
> + return ret;
> +
> + *_buf = kmemdup(buf, sizeof(buf), GFP_KERNEL);
> +
> + return sizeof(buf);
> +}
> +
> +static int axiom_u04_set(struct axiom_data *ts, u8 *buf, unsigned int bufsize)
> +{
> + unsigned int reg;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U04))
> + return -EINVAL;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U04);
> + return regmap_raw_write(ts->regmap, reg, buf, bufsize);
> +}
> +
> +/*
> + * U31 revision must be always rev.1 else the whole self discovery mechanism
> + * fall apart.
> + */
> +static int axiom_u31_parse_device_info(struct axiom_data *ts)
> +{
> + struct regmap *regmap = ts->regmap;
> + unsigned int id_low, id_high, val;
> + int ret;
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &id_high);
> + if (ret)
> + return ret;
> + id_high = FIELD_GET(AXIOM_U31_REV1_DEVICE_ID_HIGH_MASK, id_high);
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_LOW_REG, &id_low);
> + if (ret)
> + return ret;
> + ts->device_id = id_high << 8 | id_low;
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG, &val);
> + if (ret)
> + return ret;
> + ts->fw_major = val;
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MIN_REG, &val);
> + if (ret)
> + return ret;
> + ts->fw_minor = val;
> +
> + /* All other fields are not allowed to be read in BLP mode */
> + if (axiom_get_runmode(ts) == AXIOM_BLP_MODE)
> + return 0;
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_RC_REG, &val);
> + if (ret)
> + return ret;
> + ts->fw_rc = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_RC_MASK, val);
> + ts->silicon_rev = FIELD_GET(AXIOM_U31_REV1_SILICON_REV_MASK, val);
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG, &val);
> + if (ret)
> + return ret;
> + ts->fw_status = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_STATUS, val);
> + ts->fw_variant = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_VARIANT, val);
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_HIGH_REG, &val);
> + if (ret)
> + return ret;
> + ts->jedec_id = val << 8;
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_LOW_REG, &val);
> + if (ret)
> + return ret;
> + ts->jedec_id |= val;
> +
> + return 0;
> +}
> +
> +static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc);
> +
> +static int axiom_u31_device_discover(struct axiom_data *ts)
> +{
> + struct axiom_u31_usage_table_entry *u31_usage_table __free(kfree) = NULL;
> + struct axiom_u31_usage_table_entry *entry;
> + struct regmap *regmap = ts->regmap;
> + unsigned int mode, num_usages;
> + struct device *dev = ts->dev;
> + unsigned int i;
> + int ret;
> +
> + axiom_set_runmode(ts, AXIOM_DISCOVERY_MODE);
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &mode);
> + if (ret) {
> + dev_err(dev, "Failed to read MODE\n");
> + return ret;
> + }
> +
> + /* Abort if the device is in bootloader protocol mode */
> + mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
> + if (mode == AXIOM_U31_REV1_MODE_BLP)
> + axiom_set_runmode(ts, AXIOM_BLP_MODE);
> +
> + /* Since we are not in bootloader mode we can parse the device info */
> + ret = axiom_u31_parse_device_info(ts);
> + if (ret) {
> + dev_err(dev, "Failed to parse device info\n");
> + return ret;
> + }
> +
> + /* All other fields are not allowed to be read in BLP mode */
> + if (axiom_get_runmode(ts) == AXIOM_BLP_MODE) {
> + dev_info(dev, "Device in Bootloader mode, firmware upload required\n");
> + return -EACCES;
> + }
> +
> + ret = regmap_read(regmap, AXIOM_U31_REV1_NUM_USAGES_REG, &num_usages);
> + if (ret) {
> + dev_err(dev, "Failed to read NUM_USAGES\n");
> + return ret;
> + }
> +
> + u31_usage_table = kcalloc(num_usages, sizeof(*u31_usage_table),
> + GFP_KERNEL);
> + if (!u31_usage_table)
> + return -ENOMEM;
> +
> + ret = regmap_raw_read(regmap, AXIOM_U31_REV1_PAGE1, u31_usage_table,
> + array_size(num_usages, sizeof(*u31_usage_table)));
> + if (ret) {
> + dev_err(dev, "Failed to read NUM_USAGES\n");
> + return ret;
> + }
> +
> + /*
> + * axiom_u31_device_discover() is call after fw update too, so ensure
> + * that the usage_table is cleared.
> + */
> + memset(ts->usage_table, 0, sizeof(ts->usage_table));
> +
> + for (i = 0, entry = u31_usage_table; i < num_usages; i++, entry++) {
> + unsigned char idx = entry->usage_num;
> + const struct axiom_usage_info *info;
> + unsigned int size_bytes;
> +
> + axiom_dump_usage_entry(dev, entry);
> +
> + /*
> + * Verify that the driver used usages are supported. Don't abort
> + * yet if a usage isn't supported to allow the user to dump the
> + * actual usage table.
> + */
> + info = axiom_get_usage_info(entry);
> + if (IS_ERR(info)) {
> + dev_info(dev, "Required usage u%02X isn't supported for rev.%u\n",
> + entry->usage_num, entry->uifrevision);
> + ret = -EACCES;
> + }
> +
> + size_bytes = axiom_get_usage_size_bytes(entry);
> +
> + ts->usage_table[idx].baseaddr = entry->start_page << 8;
> + ts->usage_table[idx].size_bytes = size_bytes;
> + ts->usage_table[idx].populated = true;
> + ts->usage_table[idx].info = info;
> +
> + if (axiom_usage_entry_is_report(entry) &&
> + ts->max_report_byte_len < size_bytes)
> + ts->max_report_byte_len = size_bytes;
> + }
> +
> + if (ret)
> + return ret;
> +
> + /* From now on we are in TCP mode to include usage revision checks */
> + axiom_set_runmode(ts, AXIOM_TCP_MODE);
> +
> + return axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> +}
> +
> +static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc)
> +{
> + struct device *dev = ts->dev;
> + unsigned int reg;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U33))
> + return -EINVAL;
> +
> + if (axiom_usage_rev(ts, AXIOM_U33) == 2) {
> + struct axiom_u33_rev2 val;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U33);
> + ret = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> + if (ret) {
> + dev_err(dev, "Failed to read u33\n");
> + return ret;
> + }
> +
> + crc->runtime = le32_to_cpu(val.runtime_crc);
> + crc->vltusageconfig = le32_to_cpu(val.vltusageconfig_crc);
> + crc->nvltlusageconfig = le32_to_cpu(val.nvltlusageconfig_crc);
> + crc->u22_sequencedata = le32_to_cpu(val.u22_sequencedata_crc);
> + crc->u43_hotspots = le32_to_cpu(val.u43_hotspots_crc);
> + crc->u93_profiles = le32_to_cpu(val.u93_profiles_crc);
> + crc->u94_deltascalemap = le32_to_cpu(val.u94_deltascalemap_crc);
> + } else if (axiom_usage_rev(ts, AXIOM_U33) == 3) {
> + struct axiom_u33_rev3 val;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U33);
> + ret = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
> + if (ret) {
> + dev_err(dev, "Failed to read u33\n");
> + return ret;
> + }
> +
> + crc->runtime = le32_to_cpu(val.runtime_crc);
> + crc->vltusageconfig = le32_to_cpu(val.vltusageconfig_crc);
> + crc->nvltlusageconfig = le32_to_cpu(val.nvltlusageconfig_crc);
> + crc->u22_sequencedata = le32_to_cpu(val.u22_sequencedata_crc);
> + crc->u43_hotspots = le32_to_cpu(val.u43_hotspots_crc);
> + crc->u77_dod_data = le32_to_cpu(val.u77_dod_data_crc);
> + crc->u93_profiles = le32_to_cpu(val.u93_profiles_crc);
> + crc->u94_deltascalemap = le32_to_cpu(val.u94_deltascalemap_crc);
> + }
> +
> + return 0;
> +}
> +
> +static bool axiom_u42_touch_enabled(struct axiom_data *ts, const u8 *buf,
> + unsigned int touch_num)
> +{
> + switch (axiom_usage_rev(ts, AXIOM_U42)) {
> + case 1:
> + return buf[AXIOM_U42_REV1_REPORT_ID_CONTAINS(touch_num)] ==
> + AXIOM_U42_REV1_REPORT_ID_TOUCH;
> + case 4:
> + return buf[AXIOM_U42_REV4_REPORT_ID_CONTAINS(touch_num)] ==
> + AXIOM_U42_REV4_REPORT_ID_TOUCH;
> + default:
> + /* Should never happen */
> + return false;
> + }
> +}
> +
> +static void axiom_u42_get_touchslots(struct axiom_data *ts)
> +{
> + u8 *buf __free(kfree) = NULL;
> + struct device *dev = ts->dev;
> + unsigned int bufsize;
> + unsigned int reg;
> + int ret, i;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U42)) {
> + dev_warn(dev, "Use default touchslots num\n");
> + goto fallback;
> + }
> +
> + bufsize = axiom_usage_size(ts, AXIOM_U42);
> + buf = kzalloc(bufsize, GFP_KERNEL);
> + if (!buf) {
> + dev_warn(dev, "Failed to alloc u42 read buffer, use default value\n");
> + goto fallback;
> + }
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U42);
> + ret = regmap_raw_read(ts->regmap, reg, buf, bufsize);
> + if (ret) {
> + dev_warn(dev, "Failed to read u42, use default value\n");
> + goto fallback;
> + }
> +
> + ts->enabled_slots = 0;
> + ts->num_slots = 0;
> +
> + for (i = 0; i < AXIOM_MAX_TOUCHSLOTS; i++) {
> + if (axiom_u42_touch_enabled(ts, buf, i)) {
> + ts->enabled_slots |= BIT(i);
> + ts->num_slots++;
> + }
> + }
> +
> + return;
> +
> +fallback:
> + ts->enabled_slots = AXIOM_MAX_TOUCHSLOTS_MASK;
> + ts->num_slots = AXIOM_MAX_TOUCHSLOTS;
> +}
> +
> +static void axiom_u64_cds_enabled(struct axiom_data *ts)
> +{
> + unsigned int reg, val;
> + int ret;
> +
> + if (!axiom_driver_supports_usage(ts, AXIOM_U64))
> + goto fallback_out;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U64);
> + reg += AXIOM_U64_REV2_ENABLECDSPROCESSING_REG;
> +
> + ret = regmap_read(ts->regmap, reg, &val);
> + if (ret)
> + goto fallback_out;
> +
> + val = FIELD_GET(AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK, val);
> + ts->cds_enabled = val ? true : false;
> +
> + return;
> +
> +fallback_out:
> + ts->cds_enabled = false;
> +}
> +
> +static int axiom_cdu_wait_idle(struct axiom_data *ts, u8 cdu_usage_num)
> +{
> + unsigned int reg;
> + int ret, _ret;
> + u16 cmd;
> +
> + reg = axiom_usage_baseaddr(ts, cdu_usage_num);
> +
> + /*
> + * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
> + * the last command successfully completed and the device is idle.
> + */
> + ret = read_poll_timeout(regmap_raw_read, _ret,
> + _ret || cmd == AXIOM_CDU_RESP_SUCCESS,
> + 10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
> + ts->regmap, reg, &cmd, 2);
> + if (ret)
> + dev_err(ts->dev, "Poll CDU u%02X timedout with: %#x\n",
> + cdu_usage_num, cmd);
> +
> + return ret;
> +}
> +
> +/*********************** Report usage handling ********************************/
> +
> +static int axiom_process_report(struct axiom_data *ts, unsigned char usage_num,
> + const u8 *buf, size_t buflen)
> +{
> + struct axiom_usage_table_entry *entry = &ts->usage_table[usage_num];
> +
> + /* Skip processing if not in TCP mode */
> + if ((axiom_get_runmode(ts) != AXIOM_TCP_MODE) &&
> + (axiom_get_runmode(ts) != AXIOM_TCP_CFG_UPDATE_MODE))
> + return 0;
> +
> + /* May happen if an unsupported usage was requested */
> + if (!entry) {
> + dev_info(ts->dev, "Unsupported usage U%x request\n", usage_num);
> + return 0;
> + }
> +
> + /* Supported report usages need to have a process_report hook */
> + if (!entry->info || !entry->info->process_report)
> + return -EINVAL;
> +
> + return entry->info->process_report(ts, buf, buflen);
> +}
> +
> +/* Make use of datasheet method 1 - single transfer read */
> +static int
> +axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf, size_t bufsize)
> +{
> + unsigned int reg = axiom_usage_baseaddr(ts, AXIOM_U34);
> + struct regmap *regmap = ts->regmap;
> + u8 buf[AXIOM_PAGE_BYTE_LEN] = { };
> + struct device *dev = ts->dev;
> + unsigned char report_usage;
> + u16 crc_report, crc_calc;
> + unsigned int len;
> + u8 *payload;
> + int ret;
> +
> + ret = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
> + if (ret)
> + return ret;
> +
> + /* TODO: Add overflow statistics */
> +
> + /* REPORTLENGTH is in uint16 */
> + len = FIELD_GET(AXIOM_U34_REV1_REPORTLENGTH_MASK, buf[0]);
> + len *= 2;
> +
> + /*
> + * Downstream ignores zero length reports, extend the check to validate
> + * the upper bound too.
> + */
> + if (len == 0 || len > AXIOM_PAGE_BYTE_LEN) {
> + dev_dbg_ratelimited(dev, "Invalid report length: %u\n", len);
> + return -EINVAL;
> + }
> +
> + /*
> + * The CRC16 value can be queried at the last two bytes of the report.
> + * The value itself is covering the complete report excluding the CRC16
> + * value at the end.
> + */
> + crc_report = get_unaligned_le16(&buf[len - 2]);
> + crc_calc = crc16(0, buf, (len - 2));
> +
> + if (crc_calc != crc_report) {
> + dev_err_ratelimited(dev, "CRC16 mismatch!\n");
> + return -EINVAL;
> + }
> +
> + report_usage = buf[1];
> + payload = &buf[AXIOM_U34_REV1_PREAMBLE_BYTES];
> + len -= AXIOM_U34_REV1_PREAMBLE_BYTES - AXIOM_U34_REV1_POSTAMBLE_BYTES;
> +
> + switch (report_usage) {
> + case AXIOM_U01:
> + case AXIOM_U41:
> + return axiom_process_report(ts, report_usage, payload, len);
> + default:
> + dev_dbg(dev, "Unsupported report u%02X received\n",
> + report_usage);
> + }
> +
> + return 0;
> +}
> +
> +static void
> +axiom_u41_rev2_decode_target(const u8 *buf, u8 id, u16 *x, u16 *y, s8 *z)
> +{
> + u16 val;
> +
> + val = get_unaligned_le16(&buf[AXIOM_U41_REV2_X_REG(id)]);
> + val &= AXIOM_MAX_XY;
> + *x = val;
> +
> + val = get_unaligned_le16(&buf[AXIOM_U41_REV2_Y_REG(id)]);
> + val &= AXIOM_MAX_XY;
> + *y = val;
> +
> + *z = buf[AXIOM_U41_REV2_Z_REG(id)];
> +}
> +
> +static int
> +axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf, size_t bufsize)
> +{
> + struct input_dev *input = ts->input;
> + unsigned char id;
> + u16 targets;
> +
> + /*
> + * The input registration can be postponed but the touchscreen FW is
> + * sending u41 reports regardless.
> + */
> + if (!input)
> + return 0;
> +
> + targets = get_unaligned_le16(&buf[AXIOM_U41_REV2_TARGETSTATUS_REG]);
> +
> + for_each_set_bit(id, &ts->enabled_slots, AXIOM_MAX_TOUCHSLOTS) {
> + bool present;
> + u16 x, y;
> + s8 z;
> +
> + axiom_u41_rev2_decode_target(buf, id, &x, &y, &z);
> +
> + present = targets & BIT(id);
> + /* Ignore possible jitters */
> + if (z == AXIOM_PROX_LEVEL)
> + present = false;
> +
> + dev_dbg(ts->dev, "id:%u x:%u y:%u z:%d present:%u",
> + id, x, y, z, present);
> +
> + input_mt_slot(input, id);
> + if (input_mt_report_slot_state(input, MT_TOOL_FINGER, present))
> + touchscreen_report_pos(input, &ts->prop, x, y, true);
> +
> + if (!present)
> + continue;
> +
> + input_report_abs(input, ABS_MT_DISTANCE, z < 0 ? -z : 0);
> + if (ts->cds_enabled)
> + input_report_abs(input, ABS_MT_PRESSURE, z >= 0 ? z : 0);
> + }
> +
> + input_sync(input);
> +
> + return 0;
> +}
> +
> +static int
> +axiom_u01_rev1_process_report(struct axiom_data *ts, const u8 *buf, size_t bufsize)
> +{
> + switch (buf[AXIOM_U01_REV1_REPORTTYPE_REG]) {
> + case AXIOM_U01_REV1_REPORTTYPE_HELLO:
> + dev_dbg(ts->dev, "u01 HELLO received\n");
> + axiom_complete(ts, &ts->boot_complete);
> + return 0;
> + case AXIOM_U01_REV1_REPORTTYPE_HEARTBEAT:
> + dev_dbg_ratelimited(ts->dev, "u01 HEARTBEAT received\n");
> + return 0;
> + case AXIOM_U01_REV1_REPORTTYPE_OPCOMPLETE:
> + dev_dbg(ts->dev, "u01 OPCOMPLETE received\n");
> + axiom_u02_handshakenvm(ts);
> + axiom_complete(ts, &ts->nvm_write);
> + return 0;
> + default:
> + return -EINVAL;
> + }
> +}
> +
> +/**************************** Regmap handling *********************************/
> +
> +#define AXIOM_CMD_HDR_DIR_MASK BIT(15)
> +#define AXIOM_CMD_HDR_READ 1
> +#define AXIOM_CMD_HDR_WRITE 0
> +#define AXIOM_CMD_HDR_LEN_MASK GENMASK(14, 0)
> +
> +struct axiom_cmd_header {
> + __le16 target_address;
> + __le16 xferlen;
> +};
> +
> +/* Custom regmap read/write handling is required due to the aXiom protocol */
> +static int axiom_regmap_read(void *context, const void *reg_buf, size_t reg_size,
> + void *val_buf, size_t val_size)
> +{
> + struct device *dev = context;
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> + struct axiom_cmd_header hdr;
> + u16 xferlen, addr, baseaddr;
> + struct i2c_msg xfer[2];
> + int ret;
> +
> + if (val_size > AXIOM_MAX_XFERLEN) {
> + dev_err(ts->dev, "Exceed max xferlen: %zu > %u\n",
> + val_size, AXIOM_MAX_XFERLEN);
> + return -EINVAL;
> + }
> +
> + addr = *((u16 *)reg_buf);
> + hdr.target_address = cpu_to_le16(addr);
> + xferlen = FIELD_PREP(AXIOM_CMD_HDR_DIR_MASK, AXIOM_CMD_HDR_READ) |
> + FIELD_PREP(AXIOM_CMD_HDR_LEN_MASK, val_size);
> + hdr.xferlen = cpu_to_le16(xferlen);
> +
> + /* Verify that usage including the usage rev is supported */
> + baseaddr = addr & AXIOM_USAGE_BASEADDR_MASK;
> + if (!axiom_usage_supported(ts, baseaddr))
> + return -EINVAL;
> +
> + xfer[0].addr = i2c->addr;
> + xfer[0].flags = 0;
> + xfer[0].len = sizeof(hdr);
> + xfer[0].buf = (u8 *)&hdr;
> +
> + xfer[1].addr = i2c->addr;
> + xfer[1].flags = I2C_M_RD;
> + xfer[1].len = val_size;
> + xfer[1].buf = val_buf;
> +
> + ret = i2c_transfer(i2c->adapter, xfer, 2);
> + if (ret == 2)
> + return 0;
> + else if (ret < 0)
> + return ret;
> + else
> + return -EIO;
> +}
> +
> +static int axiom_regmap_write(void *context, const void *data, size_t count)
> +{
> + struct device *dev = context;
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> + char *buf __free(kfree) = NULL;
> + struct axiom_cmd_header hdr;
> + u16 xferlen, addr, baseaddr;
> + size_t val_size, msg_size;
> + int ret;
> +
> + val_size = count - sizeof(addr);
> + if (val_size > AXIOM_MAX_XFERLEN) {
> + dev_err(ts->dev, "Exceed max xferlen: %zu > %u\n",
> + val_size, AXIOM_MAX_XFERLEN);
> + return -EINVAL;
> + }
> +
> + addr = *((u16 *)data);
> + hdr.target_address = cpu_to_le16(addr);
> + xferlen = FIELD_PREP(AXIOM_CMD_HDR_DIR_MASK, AXIOM_CMD_HDR_WRITE) |
> + FIELD_PREP(AXIOM_CMD_HDR_LEN_MASK, val_size);
> + hdr.xferlen = cpu_to_le16(xferlen);
> +
> + /* Verify that usage including the usage rev is supported */
> + baseaddr = addr & AXIOM_USAGE_BASEADDR_MASK;
> + if (!axiom_usage_supported(ts, baseaddr))
> + return -EINVAL;
> +
> + msg_size = sizeof(hdr) + val_size;
> + buf = kzalloc(msg_size, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> +
> + memcpy(buf, &hdr, sizeof(hdr));
> + memcpy(&buf[sizeof(hdr)], &((char *)data)[2], val_size);
> +
> + ret = i2c_master_send(i2c, buf, msg_size);
> +
> + return ret == msg_size ? 0 : ret;
> +}
> +
> +static const struct regmap_config axiom_i2c_regmap_config = {
> + .reg_bits = 16,
> + .val_bits = 8,
> + .read = axiom_regmap_read,
> + .write = axiom_regmap_write,
> +};
> +
> +/************************ FW update handling **********************************/
> +
> +static int axiom_update_input_dev(struct axiom_data *ts);
> +
> +static enum fw_upload_err
> +axiom_axfw_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
> + u8 major_ver, minor_ver, rc_ver, status, variant;
> + u32 fw_file_crc32, crc32_calc;
> + struct device *dev = ts->dev;
> + unsigned int signature_len;
> + enum fw_upload_err ret;
> + u16 fw_file_format_ver;
> + u16 fw_file_device_id;
> +
> + mutex_lock(&afw->lock);
> + afw->cancel = false;
> + mutex_unlock(&afw->lock);
> +
> + mutex_lock(&ts->fwupdate_lock);
> +
> + if (size < sizeof(struct axiom_fw_axfw_hdr)) {
> + dev_err(dev, "Invalid AXFW file size\n");
> + ret = FW_UPLOAD_ERR_INVALID_SIZE;
> + goto out;
> + }
> +
> + signature_len = strlen(AXIOM_FW_AXFW_SIGNATURE);
> + if (strncmp(data, AXIOM_FW_AXFW_SIGNATURE, signature_len)) {
> + /*
> + * AXFW has a header which can be used to perform validations,
> + * ALC don't. Therefore the AXFW format is preferred.
> + */
> + dev_warn(dev, "No AXFW signature, assume ALC firmware\n");
> + ret = FW_UPLOAD_ERR_NONE;
> + goto out;
> + }
> +
> + fw_file_crc32 = get_unaligned_le32(&data[signature_len]);
> + crc32_calc = crc32(~0, &data[8], size - 8) ^ 0xffffffff;
> + if (fw_file_crc32 != crc32_calc) {
> + dev_err(dev, "AXFW CRC32 doesn't match (fw:%#x calc:%#x)\n",
> + fw_file_crc32, crc32_calc);
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + data += signature_len + sizeof(fw_file_crc32);
> + fw_file_format_ver = get_unaligned_le16(data);
> + if (fw_file_format_ver != AXIOM_FW_AXFW_FILE_FMT_VER) {
> + dev_err(dev, "Invalid AXFW file format version: %04x",
> + fw_file_format_ver);
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + data += sizeof(fw_file_format_ver);
> + fw_file_device_id = get_unaligned_le16(data);
> + if (fw_file_device_id != ts->device_id) {
> + dev_err(dev, "Invalid AXFW target device (fw:%#04x dev:%#04x)\n",
> + fw_file_device_id, ts->device_id);
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + /*
> + * This can happen if:
> + * * the device came up in bootloader mode, or
> + * * downloading the firmware failed in between, or
> + * * the following usage discovery failed.
> + *
> + * All cases are crcitical and we need to use any firmware to
> + * bring the device back into a working state which is supported by the
> + * host.
> + */
> + if (axiom_get_runmode(ts) != AXIOM_TCP_MODE)
> + return FW_UPLOAD_ERR_NONE;
> +
> + data += sizeof(fw_file_device_id);
> + variant = *data++;
> + minor_ver = *data++;
> + major_ver = *data++;
> + rc_ver = *data++;
> + status = *data++;
> +
> + if (major_ver == ts->fw_major && minor_ver == ts->fw_minor &&
> + rc_ver == ts->fw_rc && status == ts->fw_status &&
> + variant == ts->fw_variant) {
> + ret = FW_UPLOAD_ERR_DUPLICATE;
> + goto out;
> + }
> +
> + dev_info(dev, "Detected AXFW %02u.%02u.%02u (%s)\n",
> + major_ver, minor_ver, rc_ver,
> + status ? "production" : "engineering");
> +
> + mutex_lock(&afw->lock);
> + ret = afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> + mutex_unlock(&afw->lock);
> +
> +out:
> + /*
> + * In FW_UPLOAD_ERR_NONE case the complete handler will release the
> + * lock.
> + */
> + if (ret != FW_UPLOAD_ERR_NONE)
> + mutex_unlock(&ts->fwupdate_lock);
> +
> + return ret;
> +}
> +
> +static int axiom_enter_bootloader_mode(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + int ret;
> +
> + axiom_set_runmode(ts, AXIOM_BLP_PRE_MODE);
> +
> + ret = axiom_u02_wait_idle(ts);
> + if (ret)
> + goto err_out;
> +
> + ret = axiom_u02_enter_bootloader(ts);
> + if (ret) {
> + dev_err(dev, "Failed to enter bootloader mode\n");
> + goto err_out;
> + }
> +
> + axiom_set_runmode(ts, AXIOM_BLP_MODE);
> +
> + return 0;
> +
> +err_out:
> + axiom_set_runmode(ts, AXIOM_TCP_MODE);
> +
> + return ret;
> +}
> +
> +static int axoim_blp_wait_ready(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + unsigned int reg;
> + int tmp, ret;
> + u8 buf[4];
> +
> + reg = AXIOM_U01_BLP_SATUS_REG;
> +
> + /* BLP busy poll requires to read 4 bytes! */
> + ret = read_poll_timeout(regmap_raw_read, tmp,
> + tmp || !(buf[2] & AXIOM_U01_BLP_STATUS_BUSY),
> + 10 * USEC_PER_MSEC, 5 * USEC_PER_SEC, false,
> + ts->regmap, reg, &buf, 4);
> + if (ret)
> + dev_err(dev, "Bootloader wait processing packets failed %d\n", ret);
> +
> + return ret;
> +}
> +
> +static int
> +axiom_blp_write_chunk(struct axiom_data *ts, const u8 *data, u16 length)
> +{
> + unsigned int chunk_size = AXIOM_U01_BLP_FIFO_CHK_SIZE_BYTES;
> + unsigned int reg = AXIOM_U01_BLP_FIFO_REG;
> + struct device *dev = ts->dev;
> + unsigned int pos = 0;
> + int ret;
> +
> + ret = axoim_blp_wait_ready(ts);
> + if (ret)
> + return ret;
> +
> + /*
> + * TODO: Downstream does this chunk transfers. Verify if this is
> + * required if one fw-chunk <= AXIOM_MAX_XFERLEN
> + */
> + while (pos < length) {
> + u16 len;
> +
> + len = chunk_size;
> + if ((pos + chunk_size) > length)
> + len = length - pos;
> +
> + ret = regmap_raw_write(ts->regmap, reg, &data[pos], len);
> + if (ret) {
> + dev_err(dev, "Bootloader download AXFW chunk failed %d\n", ret);
> + return ret;
> + }
> +
> + pos += len;
> + ret = axoim_blp_wait_ready(ts);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static int axiom_blp_reset(struct axiom_data *ts)
> +{
> + __le16 reset_cmd = cpu_to_le16(AXIOM_U01_BLP_COMMAND_RESET);
> + unsigned int reg = AXIOM_U01_BLP_COMMAND_REG;
> + struct device *dev = ts->dev;
> + unsigned int attempts = 20;
> + unsigned int mode;
> + int ret;
> +
> + ret = axoim_blp_wait_ready(ts);
> + if (ret)
> + return ret;
> +
> + /*
> + * For some reason this write fail with -ENXIO. Skip checking the return
> + * code (which is also done by the downstream axfw.py<http://axfw.py> tool and poll u31
> + * instead.
> + */
> + regmap_raw_write(ts->regmap, reg, &reset_cmd, sizeof(reset_cmd));
> +
> + do {
> + ret = regmap_read(ts->regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG,
> + &mode);
> + if (!ret)
> + break;
> +
> + fsleep(250 * USEC_PER_MSEC);
> + } while (attempts--);
> +
> + if (ret) {
> + dev_err(dev, "Failed to read MODE after BLP reset: %d\n", ret);
> + return ret;
> + }
> +
> + mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
> + if (mode == AXIOM_U31_REV1_MODE_BLP) {
> + dev_err(dev, "Device still in BLP mode, abort\n");
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static void axiom_lock_input_device(struct axiom_data *ts)
> +{
> + if (!ts->input)
> + return;
> +
> + mutex_lock(&ts->input->mutex);
> +}
> +
> +static void axiom_unlock_input_device(struct axiom_data *ts)
> +{
> + if (!ts->input)
> + return;
> +
> + mutex_unlock(&ts->input->mutex);
> +}
> +
> +static void axiom_unregister_input_dev(struct axiom_data *ts)
> +{
> + if (ts->input)
> + input_unregister_device(ts->input);
> +
> + ts->input = NULL;
> +}
> +
> +static enum fw_upload_err
> +axiom_axfw_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
> + u32 size, u32 *written)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
> + struct device *dev = ts->dev;
> + bool cancel;
> + int ret;
> +
> + /* Done before cancel check due to cleanup based put */
> + ret = pm_runtime_resume_and_get(ts->dev);
> + if (ret)
> + return FW_UPLOAD_ERR_HW_ERROR;
> +
> + mutex_lock(&afw->lock);
> + cancel = afw->cancel;
> + mutex_unlock(&afw->lock);
> +
> + if (cancel)
> + return FW_UPLOAD_ERR_CANCELED;
> +
> + axiom_lock_input_device(ts);
> +
> + if (ts->input && input_device_enabled(ts->input)) {
> + dev_err(dev, "Input device not idle, abort AXFW/ALC update\n");
> + goto err;
> + }
> +
> + if (!strncmp(data, AXIOM_FW_AXFW_SIGNATURE,
> + strlen(AXIOM_FW_AXFW_SIGNATURE))) {
> + /* Set the pointer to the first fw chunk */
> + data += sizeof(struct axiom_fw_axfw_hdr);
> + size -= sizeof(struct axiom_fw_axfw_hdr);
> + *written += sizeof(struct axiom_fw_axfw_hdr);
> + }
> +
> + if (axiom_enter_bootloader_mode(ts))
> + goto err;
> +
> + while (size) {
> + u16 chunk_len, len;
> +
> + chunk_len = get_unaligned_be16(&data[6]);
> + len = chunk_len + sizeof(struct axiom_fw_axfw_chunk_hdr);
> +
> + /*
> + * The bootlaoder FW can handle the complete chunk incl. the
> + * header.
> + */
> + ret = axiom_blp_write_chunk(ts, data, len);
> + if (ret)
> + goto err;
> +
> + size -= len;
> + *written += len;
> + data += len;
> + }
> +
> + ret = axiom_blp_reset(ts);
> + if (ret)
> + dev_warn(dev, "BLP reset failed\n");
> +
> + ret = axiom_u31_device_discover(ts);
> + if (ret) {
> + /*
> + * This is critical and we need to avoid that the user-space can
> + * still use the input-dev.
> + */
> + axiom_unlock_input_device(ts);
> + axiom_unregister_input_dev(ts);
> + dev_err(dev, "Device discovery failed after AXFW/ALC firmware update\n");
> + goto err;
> + }
> +
> + /* Unlock before the input device gets unregistered */
> + axiom_unlock_input_device(ts);
> +
> + ret = axiom_update_input_dev(ts);
> + if (ret) {
> + dev_err(dev, "Input device update failed after AXFW/ALC firmware update\n");
> + return FW_UPLOAD_ERR_HW_ERROR;
> + }
> +
> + dev_info(dev, "AXFW update successful\n");
> +
> + return FW_UPLOAD_ERR_NONE;
> +
> +err:
> + axiom_unlock_input_device(ts);
> + return FW_UPLOAD_ERR_HW_ERROR;
> +}
> +
> +static enum fw_upload_err axiom_fw_poll_complete(struct fw_upload *fw_upload)
> +{
> + return FW_UPLOAD_ERR_NONE;
> +}
> +
> +static void axiom_axfw_fw_cancel(struct fw_upload *fw_upload)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
> +
> + mutex_lock(&afw->lock);
> + afw->cancel = true;
> + mutex_unlock(&afw->lock);
> +}
> +
> +static void axiom_axfw_fw_cleanup(struct fw_upload *fw_upload)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> +
> + mutex_unlock(&ts->fwupdate_lock);
> + pm_runtime_mark_last_busy(ts->dev);
> + pm_runtime_put_sync_autosuspend(ts->dev);
> +}
> +
> +static const struct fw_upload_ops axiom_axfw_fw_upload_ops = {
> + .prepare = axiom_axfw_fw_prepare,
> + .write = axiom_axfw_fw_write,
> + .poll_complete = axiom_fw_poll_complete,
> + .cancel = axiom_axfw_fw_cancel,
> + .cleanup = axiom_axfw_fw_cleanup,
> +};
> +
> +static int
> +axiom_set_new_crcs(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *cfg)
> +{
> + struct axiom_crc *crc = &ts->crc[AXIOM_CRC_NEW];
> + const u32 *u33_data = (const u32 *)cfg->usage_content;
> +
> + if (cfg->usage_rev != 2 && cfg->usage_rev != 3) {
> + dev_err(ts->dev, "The driver doesn't support u33 revision %u\n",
> + cfg->usage_rev);
> + return -EINVAL;
> + }
> +
> + crc->runtime = get_unaligned_le32(u33_data);
> + crc->nvltlusageconfig = get_unaligned_le32(&u33_data[3]);
> + crc->vltusageconfig = get_unaligned_le32(&u33_data[4]);
> + crc->u22_sequencedata = get_unaligned_le32(&u33_data[5]);
> + crc->u43_hotspots = get_unaligned_le32(&u33_data[6]);
> + if (cfg->usage_rev == 2) {
> + crc->u93_profiles = get_unaligned_le32(&u33_data[7]);
> + crc->u94_deltascalemap = get_unaligned_le32(&u33_data[8]);
> + } else if (cfg->usage_rev == 3) {
> + crc->u77_dod_data = get_unaligned_le32(&u33_data[7]);
> + crc->u93_profiles = get_unaligned_le32(&u33_data[8]);
> + crc->u94_deltascalemap = get_unaligned_le32(&u33_data[9]);
> + }
> +
> + return 0;
> +}
> +
> +static unsigned int
> +axiom_cfg_fw_prepare_chunk(struct axiom_fw_cfg_chunk *chunk, const u8 *data)
> +{
> + chunk->usage_num = data[0];
> + chunk->usage_rev = data[1];
> + chunk->usage_length = get_unaligned_le16(&data[3]);
> + chunk->usage_content = &data[5];
> +
> + return chunk->usage_length + sizeof(struct axiom_fw_cfg_chunk_hdr);
> +}
> +
> +/*
> + * To overcome buggy firmware we need to check if a given usage is used by the
> + * current running firmware. Return true if the usage is unused/not populated
> + * by the firmware since we can't perform the actual check.
> + */
> +#define axiom_usage_crc_match(_ts, _usage_num, _cur, _new, _field) \
> + (!_ts->usage_table[_usage_num].populated || (_cur->_field == _new->_field))
> +
> +static bool axiom_cfg_fw_update_required(struct axiom_data *ts)
> +{
> + struct axiom_crc *cur, *new;
> +
> + cur = &ts->crc[AXIOM_CRC_CUR];
> + new = &ts->crc[AXIOM_CRC_NEW];
> +
> + if (cur->nvltlusageconfig != new->nvltlusageconfig ||
> + !axiom_usage_crc_match(ts, AXIOM_U22, cur, new, u22_sequencedata) ||
> + !axiom_usage_crc_match(ts, AXIOM_U43, cur, new, u43_hotspots) ||
> + !axiom_usage_crc_match(ts, AXIOM_U93, cur, new, u93_profiles) ||
> + !axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap))
> + return true;
> +
> + return false;
> +}
> +
> +static enum fw_upload_err
> +axiom_cfg_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
> + u32 cur_runtime_crc, fw_runtime_crc;
> + struct axiom_fw_cfg_chunk chunk;
> + struct device *dev = ts->dev;
> + enum fw_upload_err ret;
> + u32 signature;
> +
> + mutex_lock(&afw->lock);
> + afw->cancel = false;
> + mutex_unlock(&afw->lock);
> +
> + mutex_lock(&ts->fwupdate_lock);
> +
> + if (axiom_get_runmode(ts) != AXIOM_TCP_MODE) {
> + dev_err(dev, "Device not in TCP mode, abort TH2CFG update\n");
> + ret = FW_UPLOAD_ERR_HW_ERROR;
> + goto out;
> + }
> +
> + if (size < sizeof(struct axiom_fw_cfg_hdr)) {
> + dev_err(dev, "Invalid TH2CFG file size\n");
> + ret = FW_UPLOAD_ERR_INVALID_SIZE;
> + goto out;
> + }
> +
> + signature = get_unaligned_be32(data);
> + if (signature != AXIOM_FW_CFG_SIGNATURE) {
> + dev_err(dev, "Invalid TH2CFG signature\n");
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + /* Skip to the first fw chunk */
> + data += sizeof(struct axiom_fw_cfg_hdr);
> + size -= sizeof(struct axiom_fw_cfg_hdr);
> +
> + /*
> + * Search for u33 which contains the CRC information and perform only
> + * the runtime-crc check.
> + */
> + while (size) {
> + unsigned int chunk_len;
> +
> + chunk_len = axiom_cfg_fw_prepare_chunk(&chunk, data);
> + if (chunk.usage_num == AXIOM_U33)
> + break;
> +
> + data += chunk_len;
> + size -= chunk_len;
> + }
> +
> + if (size == 0) {
> + dev_err(dev, "Failed to find the u33 entry in TH2CFG\n");
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + ret = axiom_set_new_crcs(ts, &chunk);
> + if (ret) {
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + /*
> + * Nothing to do if the CRCs are the same. TODO: Must be extended once
> + * the CDU update is added.
> + */
> + if (!axiom_cfg_fw_update_required(ts)) {
> + ret = FW_UPLOAD_ERR_DUPLICATE;
> + goto out;
> + }
> +
> + cur_runtime_crc = ts->crc[AXIOM_CRC_CUR].runtime;
> + fw_runtime_crc = ts->crc[AXIOM_CRC_NEW].runtime;
> + if (cur_runtime_crc != fw_runtime_crc) {
> + dev_err(dev, "TH2CFG and device runtime CRC doesn't match: %#x != %#x\n",
> + fw_runtime_crc, cur_runtime_crc);
> + ret = FW_UPLOAD_ERR_FW_INVALID;
> + goto out;
> + }
> +
> + mutex_lock(&afw->lock);
> + ret = afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
> + mutex_unlock(&afw->lock);
> +
> +out:
> + /*
> + * In FW_UPLOAD_ERR_NONE case the complete handler will release the
> + * lock.
> + */
> + if (ret != FW_UPLOAD_ERR_NONE)
> + mutex_unlock(&ts->fwupdate_lock);
> +
> + return ret;
> +}
> +
> +static int axiom_zero_volatile_mem(struct axiom_data *ts)
> +{
> + int ret, size;
> + u8 *buf;
> +
> + /* Zero out the volatile memory except for the user content in u04 */
> + ret = axiom_u04_get(ts, &buf);
> + if (ret < 0)
> + return ret;
> + size = ret;
> +
> + ret = axiom_u02_fillconfig(ts);
> + if (ret)
> + goto out;
> +
> + ret = axiom_u04_set(ts, buf, size);
> +out:
> + kfree(buf);
> + return ret;
> +}
> +
> +static bool
> +axiom_skip_cfg_chunk(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +{
> + u8 usage_num = chunk->usage_num;
> +
> + if (!ts->usage_table[usage_num].populated) {
> + dev_warn(ts->dev, "Unknown usage chunk for u%02X\n", usage_num);
> + return true;
> + }
> +
> + /* Skip read-only usages */
> + if (ts->usage_table[usage_num].info &&
> + ts->usage_table[usage_num].info->is_ro)
> + return true;
> +
> + return false;
> +}
> +
> +static int
> +axiom_write_cdu_usage(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +{
> + struct axiom_cdu_usage cdu = { };
> + struct device *dev = ts->dev;
> + unsigned int remaining;
> + unsigned int reg;
> + unsigned int pos;
> + int ret;
> +
> + pos = 0;
> + remaining = chunk->usage_length;
> + cdu.command = cpu_to_le16(AXIOM_CDU_CMD_STORE);
> + reg = axiom_usage_baseaddr(ts, chunk->usage_num);
> +
> + while (remaining) {
> + unsigned int size;
> +
> + cdu.parameters[1] = cpu_to_le16(pos);
> +
> + size = remaining;
> + if (size > AXIOM_CDU_MAX_DATA_BYTES)
> + size = AXIOM_CDU_MAX_DATA_BYTES;
> +
> + memset(cdu.data<http://cdu.data>, 0, sizeof(cdu.data));
> + memcpy(cdu.data<http://cdu.data>, &chunk->usage_content[pos], size);
> +
> + ret = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> + if (ret) {
> + dev_err(dev, "Failed to write CDU u%02X\n",
> + chunk->usage_num);
> + return ret;
> + }
> +
> + ret = axiom_cdu_wait_idle(ts, chunk->usage_num);
> + if (ret) {
> + dev_err(dev, "CDU write wait-idle failed\n");
> + return ret;
> + }
> +
> + remaining -= size;
> + pos += size;
> + }
> +
> + /*
> + * TODO: Check if we really need to send 48 zero bytes of data like
> + * downstream does.
> + */
> + memset(&cdu, 0, sizeof(cdu));
> + cdu.command = cpu_to_le16(AXIOM_CDU_CMD_COMMIT);
> + cdu.parameters[0] = cpu_to_le16(AXIOM_CDU_PARAM0_COMMIT);
> + cdu.parameters[1] = cpu_to_le16(AXIOM_CDU_PARAM1_COMMIT);
> +
> + ret = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
> + if (ret) {
> + dev_err(dev, "Failed to commit CDU u%02X to NVM\n",
> + chunk->usage_num);
> + return ret;
> + }
> +
> + ret = axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> + msecs_to_jiffies(5 * MSEC_PER_SEC));
> + if (!ret) {
> + dev_err(ts->dev, "Error CDU u%02X commit timedout\n",
> + chunk->usage_num);
> + return -ETIMEDOUT;
> + }
> +
> + return axiom_cdu_wait_idle(ts, chunk->usage_num);
> +}
> +
> +static int
> +axiom_write_cfg_chunk(struct axiom_data *ts, const struct axiom_fw_cfg_chunk *chunk)
> +{
> + unsigned int reg;
> + int ret;
> +
> + if (ts->usage_table[chunk->usage_num].info &&
> + ts->usage_table[chunk->usage_num].info->is_cdu) {
> + ret = axiom_write_cdu_usage(ts, chunk);
> + if (ret)
> + return ret;
> + goto out;
> + }
> +
> + reg = axiom_usage_baseaddr(ts, chunk->usage_num);
> + ret = regmap_raw_write(ts->regmap, reg, chunk->usage_content, chunk->usage_length);
> + if (ret)
> + return ret;
> +
> +out:
> + return axiom_u02_wait_idle(ts);
> +}
> +
> +static int axiom_verify_volatile_mem(struct axiom_data *ts)
> +{
> + int ret;
> +
> + ret = axiom_u02_computecrc(ts);
> + if (ret)
> + return ret;
> +
> + /* Query the new CRCs after they are re-computed */
> + ret = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> + if (ret)
> + return ret;
> +
> + return ts->crc[AXIOM_CRC_CUR].vltusageconfig ==
> + ts->crc[AXIOM_CRC_NEW].vltusageconfig ? 0 : -EINVAL;
> +}
> +
> +static int axiom_verify_crcs(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + struct axiom_crc *cur, *new;
> +
> + cur = &ts->crc[AXIOM_CRC_CUR];
> + new = &ts->crc[AXIOM_CRC_NEW];
> +
> + if (new->vltusageconfig != cur->vltusageconfig) {
> + dev_err(dev, "VLTUSAGECONFIG CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->vltusageconfig, new->vltusageconfig);
> + return -EINVAL;
> + } else if (new->nvltlusageconfig != cur->nvltlusageconfig) {
> + dev_err(dev, "NVLTUSAGECONFIG CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->nvltlusageconfig, new->nvltlusageconfig);
> + return -EINVAL;
> + } else if (!axiom_usage_crc_match(ts, AXIOM_U22, cur, new, u22_sequencedata)) {
> + dev_err(dev, "U22_SEQUENCEDATA CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->u22_sequencedata, new->u22_sequencedata);
> + return -EINVAL;
> + } else if (!axiom_usage_crc_match(ts, AXIOM_U43, cur, new, u43_hotspots)) {
> + dev_err(dev, "U43_HOTSPOTS CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->u43_hotspots, new->u43_hotspots);
> + return -EINVAL;
> + } else if (!axiom_usage_crc_match(ts, AXIOM_U93, cur, new, u93_profiles)) {
> + dev_err(dev, "U93_PROFILES CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->u93_profiles, new->u93_profiles);
> + return -EINVAL;
> + } else if (!axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap)) {
> + dev_err(dev, "U94_DELTASCALEMAP CRC32 mismatch (dev:%#x != fw:%#x)\n",
> + cur->u94_deltascalemap, new->u94_deltascalemap);
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static enum fw_upload_err
> +axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
> + u32 size, u32 *written)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
> + struct device *dev = ts->dev;
> + bool cancel;
> + int ret;
> +
> + /* Done before cancel check due to cleanup based put */
> + ret = pm_runtime_resume_and_get(ts->dev);
> + if (ret)
> + return FW_UPLOAD_ERR_HW_ERROR;
> +
> + mutex_lock(&afw->lock);
> + cancel = afw->cancel;
> + mutex_unlock(&afw->lock);
> +
> + if (cancel)
> + return FW_UPLOAD_ERR_CANCELED;
> +
> + axiom_lock_input_device(ts);
> +
> + if (ts->input && input_device_enabled(ts->input)) {
> + dev_err(dev, "Input device not idle, abort TH2CFG update\n");
> + axiom_unlock_input_device(ts);
> + return FW_UPLOAD_ERR_HW_ERROR;
> + }
> +
> + ret = axiom_u02_stop(ts);
> + if (ret)
> + goto err_swreset;
> +
> + ret = axiom_zero_volatile_mem(ts);
> + if (ret)
> + goto err_swreset;
> +
> + /* Skip to the first fw chunk */
> + data += sizeof(struct axiom_fw_cfg_hdr);
> + size -= sizeof(struct axiom_fw_cfg_hdr);
> + *written += sizeof(struct axiom_fw_cfg_hdr);
> +
> + axiom_set_runmode(ts, AXIOM_TCP_CFG_UPDATE_MODE);
> +
> + while (size) {
> + struct axiom_fw_cfg_chunk chunk;
> + unsigned int chunk_len;
> +
> + chunk_len = axiom_cfg_fw_prepare_chunk(&chunk, data);
> + if (axiom_skip_cfg_chunk(ts, &chunk)) {
> + dev_dbg(dev, "Skip TH2CFG usage u%02X\n", chunk.usage_num);
> + goto next_chunk;
> + }
> +
> + ret = axiom_write_cfg_chunk(ts, &chunk);
> + if (ret) {
> + axiom_set_runmode(ts, AXIOM_TCP_MODE);
> + goto err_swreset;
> + }
> +
> +next_chunk:
> + data += chunk_len;
> + size -= chunk_len;
> + *written += chunk_len;
> + }
> +
> + axiom_set_runmode(ts, AXIOM_TCP_MODE);
> +
> + /* Ensure that the chunks are written correctly */
> + ret = axiom_verify_volatile_mem(ts);
> + if (ret) {
> + dev_err(dev, "Failed to verify written config, abort\n");
> + goto err_swreset;
> + }
> +
> + ret = axiom_u02_save_config(ts);
> + if (ret)
> + goto err_swreset;
> +
> + /*
> + * TODO: Check if u02 start would be sufficient to load the new config
> + * values
> + */
> + ret = axiom_u02_swreset(ts);
> + if (ret) {
> + dev_err(dev, "Soft reset failed\n");
> + goto err_unlock;
> + }
> +
> + ret = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> + if (ret)
> + goto err_unlock;
> +
> + if (axiom_verify_crcs(ts))
> + goto err_unlock;
> +
> + /* Unlock before the input device gets unregistered */
> + axiom_unlock_input_device(ts);
> +
> + ret = axiom_update_input_dev(ts);
> + if (ret) {
> + dev_err(dev, "Input device update failed after TH2CFG firmware update\n");
> + goto err_out;
> + }
> +
> + dev_info(dev, "TH2CFG update successful\n");
> +
> + return FW_UPLOAD_ERR_NONE;
> +
> +err_swreset:
> + axiom_u02_swreset(ts);
> +err_unlock:
> + axiom_unlock_input_device(ts);
> +err_out:
> + return ret == -ETIMEDOUT ? FW_UPLOAD_ERR_TIMEOUT : FW_UPLOAD_ERR_HW_ERROR;
> +}
> +
> +static void axiom_cfg_fw_cancel(struct fw_upload *fw_upload)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> + struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
> +
> + mutex_lock(&afw->lock);
> + afw->cancel = true;
> + mutex_unlock(&afw->lock);
> +}
> +
> +static void axiom_cfg_fw_cleanup(struct fw_upload *fw_upload)
> +{
> + struct axiom_data *ts = fw_upload->dd_handle;
> +
> + mutex_unlock(&ts->fwupdate_lock);
> + pm_runtime_mark_last_busy(ts->dev);
> + pm_runtime_put_sync_autosuspend(ts->dev);
> +}
> +
> +static const struct fw_upload_ops axiom_cfg_fw_upload_ops = {
> + .prepare = axiom_cfg_fw_prepare,
> + .write = axiom_cfg_fw_write,
> + .poll_complete = axiom_fw_poll_complete,
> + .cancel = axiom_cfg_fw_cancel,
> + .cleanup = axiom_cfg_fw_cleanup,
> +};
> +
> +static void axiom_remove_axfw_fwl_action(void *data)
> +{
> + struct axiom_data *ts = data;
> +
> + firmware_upload_unregister(ts->fw[AXIOM_FW_AXFW].fwl);
> +}
> +
> +static void axiom_remove_cfg_fwl_action(void *data)
> +{
> + struct axiom_data *ts = data;
> +
> + firmware_upload_unregister(ts->fw[AXIOM_FW_CFG].fwl);
> +}
> +
> +static int axiom_register_fwl(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + struct fw_upload *fwl;
> + char *fw_name;
> + int ret;
> +
> + if (!IS_ENABLED(CONFIG_FW_UPLOAD)) {
> + dev_dbg(dev, "axfw and th2cfgbin update disabled\n");
> + return 0;
> + }
> +
> + mutex_init(&ts->fw[AXIOM_FW_AXFW].lock);
> + fw_name = kasprintf(GFP_KERNEL, "i2c:%s.axfw", dev_name(dev));
> + fwl = firmware_upload_register(THIS_MODULE, ts->dev, fw_name,
> + &axiom_axfw_fw_upload_ops, ts);
> + kfree(fw_name);
> + if (IS_ERR(fwl))
> + return dev_err_probe(dev, PTR_ERR(fwl),
> + "Failed to register firmware upload\n");
> +
> + ret = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
> + if (ret)
> + return ret;
> +
> + ts->fw[AXIOM_FW_AXFW].fwl = fwl;
> +
> + mutex_init(&ts->fw[AXIOM_FW_CFG].lock);
> + fw_name = kasprintf(GFP_KERNEL, "i2c:%s.th2cfgbin", dev_name(dev));
> + fwl = firmware_upload_register(THIS_MODULE, ts->dev, fw_name,
> + &axiom_cfg_fw_upload_ops, ts);
> + kfree(fw_name);
> + if (IS_ERR(fwl))
> + return dev_err_probe(dev, PTR_ERR(fwl),
> + "Failed to register cfg firmware upload\n");
> +
> + ret = devm_add_action_or_reset(dev, axiom_remove_cfg_fwl_action, ts);
> + if (ret)
> + return ret;
> +
> + ts->fw[AXIOM_FW_CFG].fwl = fwl;
> +
> + return 0;
> +}
> +
> +/************************* Device handlig *************************************/
> +
> +#define AXIOM_SIMPLE_FW_DEVICE_ATTR(attr) \
> + static ssize_t \
> + fw_ ## attr ## _show(struct device *dev, \
> + struct device_attribute *_attr, char *buf) \
> + { \
> + struct i2c_client *i2c = to_i2c_client(dev); \
> + struct axiom_data *ts = i2c_get_clientdata(i2c); \
> + \
> + return sysfs_emit(buf, "%u\n", ts->fw_##attr); \
> + } \
> + static DEVICE_ATTR_RO(fw_##attr)
> +
> +AXIOM_SIMPLE_FW_DEVICE_ATTR(major);
> +AXIOM_SIMPLE_FW_DEVICE_ATTR(minor);
> +AXIOM_SIMPLE_FW_DEVICE_ATTR(rc);
> +
> +static ssize_t fw_status_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> + const char *val;
> +
> + if (ts->fw_status)
> + val = "production";
> + else
> + val = "engineering";
> +
> + return sysfs_emit(buf, "%s\n", val);
> +}
> +static DEVICE_ATTR_RO(fw_status);
> +
> +static ssize_t fw_variant_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> + const char *val;
> +
> + switch (ts->fw_variant) {
> + case 0:
> + val = "3d";
> + break;
> + case 1:
> + val = "2d";
> + break;
> + case 3:
> + val = "force";
> + break;
> + default:
> + val = "unknown";
> + break;
> + }
> +
> + return sysfs_emit(buf, "%s\n", val);
> +}
> +static DEVICE_ATTR_RO(fw_variant);
> +
> +static ssize_t device_id_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> +
> + return sysfs_emit(buf, "%u\n", ts->device_id);
> +}
> +static DEVICE_ATTR_RO(device_id);
> +
> +static ssize_t device_state_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct i2c_client *i2c = to_i2c_client(dev);
> + struct axiom_data *ts = i2c_get_clientdata(i2c);
> +
> + return sysfs_emit(buf, "%s\n", axiom_runmode_to_string(ts));
> +}
> +static DEVICE_ATTR_RO(device_state);
> +
> +static struct attribute *axiom_attrs[] = {
> + &dev_attr_fw_major.attr,
> + &dev_attr_fw_minor.attr,
> + &dev_attr_fw_rc.attr,
> + &dev_attr_fw_status.attr,
> + &dev_attr_fw_variant.attr,
> + &dev_attr_device_id.attr,
> + &dev_attr_device_state.attr,
> + NULL
> +};
> +ATTRIBUTE_GROUPS(axiom);
> +
> +static void axiom_poll(struct input_dev *input)
> +{
> + struct axiom_data *ts = input_get_drvdata(input);
> +
> + axiom_process_report(ts, AXIOM_U34, NULL, 0);
> +}
> +
> +static irqreturn_t axiom_irq(int irq, void *dev_id)
> +{
> + struct axiom_data *ts = dev_id;
> +
> + axiom_process_report(ts, AXIOM_U34, NULL, 0);
> +
> + return IRQ_HANDLED;
> +}
> +
> +static int axiom_input_open(struct input_dev *dev)
> +{
> + struct axiom_data *ts = input_get_drvdata(dev);
> +
> + return pm_runtime_resume_and_get(ts->dev);
> +}
> +
> +static void axiom_input_close(struct input_dev *dev)
> +{
> + struct axiom_data *ts = input_get_drvdata(dev);
> +
> + pm_runtime_mark_last_busy(ts->dev);
> + pm_runtime_put_sync_autosuspend(ts->dev);
> +}
> +
> +static int axiom_register_input_dev(struct axiom_data *ts,
> + bool update_in_process)
> +{
> + struct device *dev = ts->dev;
> + struct i2c_client *client = to_i2c_client(dev);
> + struct input_dev *input;
> + int ret;
> +
> + input = input_allocate_device();
> + if (!input) {
> + dev_err(dev, "Failed to allocate input driver data\n");
> + return -ENOMEM;
> + }
> +
> + input->dev.parent = dev;
> + input->name = "TouchNetix aXiom Touchscreen";
> + input->id.bustype = BUS_I2C;
> + input->id.vendor = ts->jedec_id;
> + input->id.product = ts->device_id;
> + input->id.version = ts->silicon_rev;
> +
> + /* Either follow the panel or the open user count, not both */
> + if (!ts->is_panel_follower) {
> + input->open = axiom_input_open;
> + input->close = axiom_input_close;
> + }
> +
> + axiom_u64_cds_enabled(ts);
> + input_set_abs_params(input, ABS_MT_POSITION_X, 0, AXIOM_MAX_XY - 1, 0, 0);
> + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, AXIOM_MAX_XY - 1, 0, 0);
> + input_set_abs_params(input, ABS_MT_DISTANCE, 0, 127, 0, 0);
> + if (ts->cds_enabled)
> + input_set_abs_params(input, ABS_MT_PRESSURE, 0, 127, 0, 0);
> +
> + touchscreen_parse_properties(input, true, &ts->prop);
> +
> + axiom_u42_get_touchslots(ts);
> + if (!ts->num_slots && update_in_process) {
> + input_free_device(input);
> + /*
> + * Skip input device registration but don't throw an error to
> + * not abort the update since some FW updates require a
> + * following CFG update to re-initialize the touchslot handling.
> + */
> + if (update_in_process) {
> + dev_info(dev, "No touchslots found after FW or CFG update, skip registering input device\n");
> + return 0;
> + }
> +
> + dev_err(dev, "Error firmware has no touchslots enabled\n");
> + return -EINVAL;
> + }
> +
> + ret = input_mt_init_slots(input, ts->num_slots, INPUT_MT_DIRECT);
> + if (ret) {
> + input_free_device(input);
> + dev_err(dev, "Failed to init mt slots\n");
> + return ret;
> + }
> +
> + /*
> + * Ensure that the IRQ setup is done only once since the handler belong
> + * to the i2c-dev whereas the input-poller belong to the input-dev. The
> + * input-dev can get unregistered during a firmware update to reflect
> + * the new firmware state. Therefore the input-poller setup must be done
> + * always.
> + */
> + if (!ts->irq_setup_done && client->irq) {
> + ret = devm_request_threaded_irq(dev, client->irq, NULL, axiom_irq,
> + IRQF_ONESHOT, dev_name(dev), ts);
> + if (ret) {
> + dev_err(dev, "Failed to request IRQ\n");
> + return ret;
> + }
> + ts->irq_setup_done = true;
> + } else {
> + ret = input_setup_polling(input, axiom_poll);
> + if (ret) {
> + input_free_device(input);
> + dev_err(dev, "Setup polling mode failed\n");
> + return ret;
> + }
> +
> + input_set_poll_interval(input, ts->poll_interval);
> + }
> +
> + input_set_drvdata(input, ts);
> + ts->input = input;
> +
> + ret = input_register_device(input);
> + if (ret) {
> + input_free_device(input);
> + ts->input = NULL;
> + dev_err(dev, "Failed to register input device\n");
> + };
> +
> + return ret;
> +}
> +
> +static int axiom_update_input_dev(struct axiom_data *ts)
> +{
> + axiom_unregister_input_dev(ts);
> +
> + return axiom_register_input_dev(ts, true);
> +}
> +
> +static int axiom_parse_firmware(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + struct gpio_desc *gpio;
> + int ret;
> +
> + ts->supplies[0].supply = "vddi";
> + ts->supplies[1].supply = "vdda";
> + ts->num_supplies = ARRAY_SIZE(ts->supplies);
> +
> + ret = devm_regulator_bulk_get(dev, ts->num_supplies, ts->supplies);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Failed to get power supplies\n");
> +
> + gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
> + if (IS_ERR(gpio))
> + return dev_err_probe(dev, PTR_ERR(gpio),
> + "Failed to get reset GPIO\n");
> + ts->reset_gpio = gpio;
> +
> + ts->poll_interval = AXIOM_DEFAULT_POLL_INTERVAL_MS;
> + device_property_read_u32(dev, "poll-interval", &ts->poll_interval);
> +
> + return 0;
> +}
> +
> +static int axiom_power_device(struct axiom_data *ts, unsigned int enable)
> +{
> + struct device *dev = ts->dev;
> + int ret;
> +
> + if (!enable) {
> + regulator_bulk_disable(ts->num_supplies, ts->supplies);
> + return 0;
> + }
> +
> + ret = regulator_bulk_enable(ts->num_supplies, ts->supplies);
> + if (ret) {
> + dev_err(dev, "Failed to enable power supplies\n");
> + return ret;
> + }
> +
> + gpiod_set_value_cansleep(ts->reset_gpio, 1);
> + fsleep(2000);
> + gpiod_set_value_cansleep(ts->reset_gpio, 0);
> +
> + fsleep(AXIOM_STARTUP_TIME_MS);
> +
> + return 0;
> +}
> +
> +static int axiom_panel_prepared(struct drm_panel_follower *follower)
> +{
> + struct axiom_data *ts = container_of(follower, struct axiom_data,
> + panel_follower);
> +
> + return pm_runtime_resume_and_get(ts->dev);
> +}
> +
> +static int axiom_panel_unpreparing(struct drm_panel_follower *follower)
> +{
> + struct axiom_data *ts = container_of(follower, struct axiom_data,
> + panel_follower);
> +
> + return pm_runtime_put_sync_suspend(ts->dev);
> +}
> +
> +static const struct drm_panel_follower_funcs axiom_panel_follower_funcs = {
> + .panel_prepared = axiom_panel_prepared,
> + .panel_unpreparing = axiom_panel_unpreparing,
> +};
> +
> +static int axiom_register_panel_follower(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> +
> + if (!drm_is_panel_follower(dev))
> + return 0;
> +
> + if (device_can_wakeup(dev)) {
> + dev_warn(dev, "Can't follow panel if marked as wakup device\n");
> + return 0;
> + }
> +
> + ts->panel_follower.funcs = &axiom_panel_follower_funcs;
> + ts->is_panel_follower = true;
> +
> + return devm_drm_panel_add_follower(dev, &ts->panel_follower);
> +}
> +
> +static int axiom_i2c_probe(struct i2c_client *client)
> +{
> + struct device *dev = &client->dev;
> + struct axiom_data *ts;
> + int ret;
> +
> + ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
> + if (!ts)
> + return dev_err_probe(dev, -ENOMEM,
> + "Failed to allocate driver data\n");
> +
> + ts->regmap = devm_regmap_init_i2c(client, &axiom_i2c_regmap_config);
> + if (IS_ERR(ts->regmap))
> + return dev_err_probe(dev, PTR_ERR(ts->regmap),
> + "Failed to initialize regmap\n");
> +
> + i2c_set_clientdata(client, ts);
> + ts->dev = dev;
> +
> + init_completion(&ts->boot_complete.completion);
> + init_completion(&ts->nvm_write.completion);
> + mutex_init(&ts->fwupdate_lock);
> +
> + ret = axiom_register_fwl(ts);
> + if (ret)
> + return ret;
> +
> + ret = axiom_parse_firmware(ts);
> + if (ret)
> + return ret;
> +
> + ret = axiom_power_device(ts, 1);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to power-on device\n");
> +
> + pm_runtime_set_autosuspend_delay(dev, 10 * MSEC_PER_SEC);
> + pm_runtime_use_autosuspend(dev);
> + pm_runtime_set_active(dev);
> + pm_runtime_get_noresume(dev);
> + ret = devm_pm_runtime_enable(dev);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to enable pm-runtime\n");
> +
> + ret = axiom_register_panel_follower(ts);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to register panel follower\n");
> +
> + ret = axiom_u31_device_discover(ts);
> + /*
> + * Register the device to allow FW updates in case that the current FW
> + * doesn't support the required driver usages or if the device is in
> + * bootloader mode.
> + */
> + if (ret && ret == -EACCES && IS_ENABLED(CONFIG_FW_UPLOAD)) {
> + dev_warn(dev, "Device discovery failed, wait for user fw update\n");
> + pm_runtime_mark_last_busy(dev);
> + pm_runtime_put_sync_autosuspend(dev);
> + return 0;
> + } else if (ret) {
> + pm_runtime_put_sync(dev);
> + return dev_err_probe(dev, ret, "Device discovery failed\n");
> + }
> +
> + ret = axiom_register_input_dev(ts, false);
> + pm_runtime_mark_last_busy(dev);
> + pm_runtime_put_sync_autosuspend(dev);
> + if (ret && IS_ENABLED(CONFIG_FW_UPLOAD))
> + dev_warn(dev, "Failed to register the input device, wait for user fw update\n");
> + else if (ret)
> + return dev_err_probe(dev, ret, "Failed to register input device\n");
> +
> + return 0;
> +}
> +
> +static void axiom_i2c_remove(struct i2c_client *client)
> +{
> + struct axiom_data *ts = i2c_get_clientdata(client);
> +
> + axiom_unregister_input_dev(ts);
> +}
> +
> +static int axiom_runtime_suspend(struct device *dev)
> +{
> + struct axiom_data *ts = dev_get_drvdata(dev);
> + struct i2c_client *client = to_i2c_client(dev);
> +
> + if (client->irq && ts->irq_setup_done)
> + disable_irq(client->irq);
> +
> + return axiom_power_device(ts, 0);
> +}
> +
> +static int axiom_runtime_resume(struct device *dev)
> +{
> + struct axiom_data *ts = dev_get_drvdata(dev);
> + struct i2c_client *client = to_i2c_client(dev);
> + int ret;
> +
> + ret = axiom_power_device(ts, 1);
> + if (ret)
> + return ret;
> +
> + if (client->irq && ts->irq_setup_done)
> + enable_irq(client->irq);
> +
> + return 0;
> +}
> +
> +static DEFINE_RUNTIME_DEV_PM_OPS(axiom_pm_ops, axiom_runtime_suspend,
> + axiom_runtime_resume, NULL);
> +
> +static const struct i2c_device_id axiom_i2c_id_table[] = {
> + { "ax54a" },
> + { },
> +};
> +MODULE_DEVICE_TABLE(i2c, axiom_i2c_id_table);
> +
> +static const struct of_device_id axiom_of_match[] = {
> + { .compatible = "touchnetix,ax54a", },
> + { }
> +};
> +MODULE_DEVICE_TABLE(of, axiom_of_match);
> +
> +static struct i2c_driver axiom_i2c_driver = {
> + .driver = {
> + .name = KBUILD_MODNAME,
> + .dev_groups = axiom_groups,
> + .pm = pm_ptr(&axiom_pm_ops),
> + .of_match_table = axiom_of_match,
> + },
> + .id_table = axiom_i2c_id_table,
> + .probe = axiom_i2c_probe,
> + .remove = axiom_i2c_remove,
> +};
> +module_i2c_driver(axiom_i2c_driver);
> +
> +MODULE_DESCRIPTION("TouchNetix aXiom touchscreen I2C bus driver");
> +MODULE_LICENSE("GPL");
> 
> --
> 2.47.3
> 
> 

-- 
#gernperDu 
#CallMeByMyFirstName

Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply

* [PATCH] HID: Intel-thc-hid: Intel-thc: Fix wrong register fields updating
From: Even Xu @ 2026-02-02  3:01 UTC (permalink / raw)
  To: bentiss, jikos
  Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu,
	Rui Zhang

Clear the target bit fields in register before setting new values. This
ensures proper field updates by removing any existing bits that might
interfere with the new configuration.

Fixes: 22da60f0304b ("HID: Intel-thc-hid: Intel-thc: Introduce interrupt delay control")
Fixes: 45e92a093099 ("HID: Intel-thc-hid: Intel-thc: Introduce max input size control")
Signed-off-by: Even Xu <even.xu@intel.com>
Tested-by: Rui Zhang <rui1.zhang@intel.com>
---
 drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
index 7e220a4c5ded..d8e195189e4b 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
@@ -1597,6 +1597,7 @@ int thc_i2c_set_rx_max_size(struct thc_device *dev, u32 max_rx_size)
 	if (ret)
 		return ret;
 
+	val = val & ~THC_M_PRT_SPI_ICRRD_OPCODE_I2C_MAX_SIZE;
 	val |= FIELD_PREP(THC_M_PRT_SPI_ICRRD_OPCODE_I2C_MAX_SIZE, max_rx_size);
 
 	ret = regmap_write(dev->thc_regmap, THC_M_PRT_SPI_ICRRD_OPCODE_OFFSET, val);
@@ -1667,6 +1668,7 @@ int thc_i2c_set_rx_int_delay(struct thc_device *dev, u32 delay_us)
 		return ret;
 
 	/* THC hardware counts at 10us unit */
+	val = val & ~THC_M_PRT_SPI_ICRRD_OPCODE_I2C_INTERVAL;
 	val |= FIELD_PREP(THC_M_PRT_SPI_ICRRD_OPCODE_I2C_INTERVAL, DIV_ROUND_UP(delay_us, 10));
 
 	ret = regmap_write(dev->thc_regmap, THC_M_PRT_SPI_ICRRD_OPCODE_OFFSET, val);
-- 
2.40.1


^ permalink raw reply related

* Re: [PATCH v6 1/3] input: trackpoint - Enable doubletap by default on capable devices
From: Vishnu Sankar @ 2026-02-02  3:56 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: mpearson-lenovo, hmh, hansg, corbet, derekjohn.clark,
	ilpo.jarvinen, linux-input, linux-kernel, ibm-acpi-devel,
	linux-doc, platform-driver-x86, vsankar
In-Reply-To: <aX8JwB7F8_ypJRAz@google.com>

Hi Dmitry,

Thank you for the comments.

On Sun, Feb 1, 2026 at 5:10 PM Dmitry Torokhov
<dmitry.torokhov@gmail.com> wrote:
>
> Hi Vishnu,
>
> On Tue, Jan 27, 2026 at 07:39:05PM +0900, Vishnu Sankar wrote:
> > @@ -470,6 +509,14 @@ int trackpoint_detect(struct psmouse *psmouse, bool set_properties)
> >                    psmouse->vendor, firmware_id,
> >                    (button_info & 0xf0) >> 4, button_info & 0x0f);
> >
> > +     /* Enable doubletap by default on capable devices */
>
> This is obvious from the code, please drop the comment.
Understood.
Will remove this in v7.
>
>
> > +     if (trackpoint_is_dt_capable(ps2dev->serio->firmware_id)) {
> > +             if (trackpoint_write(ps2dev, TP_DOUBLETAP, TP_DOUBLETAP_ENABLE))
> > +                     psmouse_warn(psmouse, "Failed to enable doubletap: %d\n", error);
> > +             else
> > +                     psmouse_info(psmouse, "Doubletap enabled by default!\n");
>
> Drop psmouse_info(), no need to make the driver too noisy.
Understood.
I will be removing his log as well.
>
> Otherwise:
>
>
> Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Thank you
>
> --
> Dmitry



-- 

Regards,

      Vishnu Sankar
     +817015150407 (Japan)

^ permalink raw reply

* Re: [PATCH v2 2/3] Input: aw86938 - add driver for Awinic AW86938
From: Konrad Dybcio @ 2026-02-02 10:12 UTC (permalink / raw)
  To: Dmitry Torokhov, Griffin Kroah-Hartman
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Luca Weiss, linux-input, devicetree, linux-kernel,
	linux-arm-msm
In-Reply-To: <aX6whqw7XyaKMd9m@google.com>

On 2/1/26 2:49 AM, Dmitry Torokhov wrote:
> Hi Griffin,
> 
> On Wed, Jan 28, 2026 at 04:51:14PM +0100, Griffin Kroah-Hartman wrote:
>> @@ -717,9 +746,19 @@ static int aw86927_detect(struct aw86927_data *haptics)
>>  
>>  	chip_id = be16_to_cpu(read_buf);
>>  
>> -	if (chip_id != AW86927_CHIPID) {
>> -		dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>> -		return -ENODEV;
>> +	switch (haptics->model) {
>> +	case AW86927:
>> +		if (chip_id != AW86927_CHIPID) {
>> +			dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>> +			return -ENODEV;
>> +		}
> 
> If we are able to query chip ID why do we need to have separate
> compatibles? I would define chip data structure with differences between
> variants and assign and use it instead of having separate compatible.

dt-bindings guidelines explicitly call for this, a chipid comparison
then works as a safety net

Konrad

^ permalink raw reply

* Re: [PATCH v2 2/3] Input: aw86938 - add driver for Awinic AW86938
From: Luca Weiss @ 2026-02-02 10:14 UTC (permalink / raw)
  To: Konrad Dybcio, Dmitry Torokhov, Griffin Kroah-Hartman
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Luca Weiss, linux-input, devicetree, linux-kernel,
	linux-arm-msm
In-Reply-To: <472d7db3-db34-4966-aa17-588e1153ba12@oss.qualcomm.com>

Hi Konrad,

On Mon Feb 2, 2026 at 11:12 AM CET, Konrad Dybcio wrote:
> On 2/1/26 2:49 AM, Dmitry Torokhov wrote:
>> Hi Griffin,
>> 
>> On Wed, Jan 28, 2026 at 04:51:14PM +0100, Griffin Kroah-Hartman wrote:
>>> @@ -717,9 +746,19 @@ static int aw86927_detect(struct aw86927_data *haptics)
>>>  
>>>  	chip_id = be16_to_cpu(read_buf);
>>>  
>>> -	if (chip_id != AW86927_CHIPID) {
>>> -		dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>>> -		return -ENODEV;
>>> +	switch (haptics->model) {
>>> +	case AW86927:
>>> +		if (chip_id != AW86927_CHIPID) {
>>> +			dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>>> +			return -ENODEV;
>>> +		}
>> 
>> If we are able to query chip ID why do we need to have separate
>> compatibles? I would define chip data structure with differences between
>> variants and assign and use it instead of having separate compatible.
>
> dt-bindings guidelines explicitly call for this, a chipid comparison
> then works as a safety net

Are you saying, that

1. we should enforce dt-bindings == CHIP_ID (what's currently done)

or

2. we should have both compatibles with no handling based on compatible,
   but only use CHIP_ID at runtime to change behavior

Regards
Luca

^ permalink raw reply

* Re: [PATCH v2 2/3] Input: aw86938 - add driver for Awinic AW86938
From: Konrad Dybcio @ 2026-02-02 10:19 UTC (permalink / raw)
  To: Luca Weiss, Dmitry Torokhov, Griffin Kroah-Hartman
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, linux-input, devicetree, linux-kernel,
	linux-arm-msm
In-Reply-To: <DG4EAYDXMGQS.2MKAJKDFQCFEG@fairphone.com>

On 2/2/26 11:14 AM, Luca Weiss wrote:
> Hi Konrad,
> 
> On Mon Feb 2, 2026 at 11:12 AM CET, Konrad Dybcio wrote:
>> On 2/1/26 2:49 AM, Dmitry Torokhov wrote:
>>> Hi Griffin,
>>>
>>> On Wed, Jan 28, 2026 at 04:51:14PM +0100, Griffin Kroah-Hartman wrote:
>>>> @@ -717,9 +746,19 @@ static int aw86927_detect(struct aw86927_data *haptics)
>>>>  
>>>>  	chip_id = be16_to_cpu(read_buf);
>>>>  
>>>> -	if (chip_id != AW86927_CHIPID) {
>>>> -		dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>>>> -		return -ENODEV;
>>>> +	switch (haptics->model) {
>>>> +	case AW86927:
>>>> +		if (chip_id != AW86927_CHIPID) {
>>>> +			dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
>>>> +			return -ENODEV;
>>>> +		}
>>>
>>> If we are able to query chip ID why do we need to have separate
>>> compatibles? I would define chip data structure with differences between
>>> variants and assign and use it instead of having separate compatible.
>>
>> dt-bindings guidelines explicitly call for this, a chipid comparison
>> then works as a safety net
> 
> Are you saying, that
> 
> 1. we should enforce dt-bindings == CHIP_ID (what's currently done)

This

> 
> or
> 
> 2. we should have both compatibles with no handling based on compatible,
>    but only use CHIP_ID at runtime to change behavior

This is spaghetti

Konrad

^ permalink raw reply

* Re: [PATCH v2 2/3] Input: aw86938 - add driver for Awinic AW86938
From: Dmitry Torokhov @ 2026-02-02 11:04 UTC (permalink / raw)
  To: Konrad Dybcio
  Cc: Luca Weiss, Griffin Kroah-Hartman, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson, Konrad Dybcio,
	linux-input, devicetree, linux-kernel, linux-arm-msm
In-Reply-To: <34fa533c-b9ab-4025-b9ad-4041837e790c@oss.qualcomm.com>

On Mon, Feb 02, 2026 at 11:19:36AM +0100, Konrad Dybcio wrote:
> On 2/2/26 11:14 AM, Luca Weiss wrote:
> > Hi Konrad,
> > 
> > On Mon Feb 2, 2026 at 11:12 AM CET, Konrad Dybcio wrote:
> >> On 2/1/26 2:49 AM, Dmitry Torokhov wrote:
> >>> Hi Griffin,
> >>>
> >>> On Wed, Jan 28, 2026 at 04:51:14PM +0100, Griffin Kroah-Hartman wrote:
> >>>> @@ -717,9 +746,19 @@ static int aw86927_detect(struct aw86927_data *haptics)
> >>>>  
> >>>>  	chip_id = be16_to_cpu(read_buf);
> >>>>  
> >>>> -	if (chip_id != AW86927_CHIPID) {
> >>>> -		dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
> >>>> -		return -ENODEV;
> >>>> +	switch (haptics->model) {
> >>>> +	case AW86927:
> >>>> +		if (chip_id != AW86927_CHIPID) {
> >>>> +			dev_err(haptics->dev, "Unexpected CHIPID value 0x%x\n", chip_id);
> >>>> +			return -ENODEV;
> >>>> +		}
> >>>
> >>> If we are able to query chip ID why do we need to have separate
> >>> compatibles? I would define chip data structure with differences between
> >>> variants and assign and use it instead of having separate compatible.
> >>
> >> dt-bindings guidelines explicitly call for this, a chipid comparison
> >> then works as a safety net
> > 
> > Are you saying, that
> > 
> > 1. we should enforce dt-bindings == CHIP_ID (what's currently done)
> 
> This

No. If there is a compatible chip with different ID (for whatever reason
- maybe there is additional functionality that either board does not
need or the driver does not implement) we absolutely should not refuse
to bind the driver.

Hint: this thing is called _compatible_ for a reason.

> 
> > 
> > or
> > 
> > 2. we should have both compatibles with no handling based on compatible,
> >    but only use CHIP_ID at runtime to change behavior
> 
> This is spaghetti

I really do not understand the aversion of DT maintainers to generic
compatibles. We see this in I2C HID where we keep adding compatibles
for what could be described via device properties.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [Poke?] Re: [PATCH v2 0/1] HID: switch2: Add preliminary Switch 2 controller driver
From: Jiri Kosina @ 2026-02-02 11:41 UTC (permalink / raw)
  To: lyude
  Cc: Vicki Pfau, Dmitry Torokhov, Benjamin Tissoires, linux-input,
	Luiz Augusto von Dentz, Bastien Nocera
In-Reply-To: <721a51815d1cc21aa22e255ce31598035b687592.camel@redhat.com>

On Fri, 30 Jan 2026, lyude@redhat.com wrote:

> Bentiss/Jiri, any chance this patch is on your radar?

I've reviewed the driver already, and I was sure I sent out an e-mail 
asking for a minor nit -- better naming of the driver (we usuall name 
drivers by vendors, not specific devices. But I can't find any traces of 
such e-mail, so I probably hallucinated it, sorry.

Vicki, is there any reason not to follow the standard naming, and either 
fold the whole thing into hid-nintendo, or built it separately under e.g. 
hid-nintendo-switch2.c (which might or not might be then linked into 
hid-nintendo proper)?

Thanks,

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply


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