* [PATCH v7 2/4] dt-bindings: vendor-prefixes: Add TouchNetix AS
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, 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, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add vendor prefix for TouchNetix AS (https://www.touchnetix.com/products/).
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Acked-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e7cf0ca1436713ae44f1950259f006..b8450050d7299cea2b5cf6f57fe71f5ead1140c8 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1715,6 +1715,8 @@ patternProperties:
description: Toradex AG
"^toshiba,.*":
description: Toshiba Corporation
+ "^touchnetix,.*":
+ description: TouchNetix AS
"^toumaz,.*":
description: Toumaz
"^tpk,.*":
--
2.47.3
^ permalink raw reply related
* [PATCH v7 3/4] dt-bindings: input: Add TouchNetix axiom touchscreen
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, 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, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add the TouchNetix axiom I2C touchscreen device tree bindings
documentation.
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
.../input/touchscreen/touchnetix,ax54a.yaml | 62 ++++++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d388c41a75dd4d6d6d0e6de0eaef4d493d439a90
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/touchnetix,ax54a.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: TouchNetix Axiom series touchscreen controller
+
+maintainers:
+ - Marco Felsch <kernel@pengutronix.de>
+
+allOf:
+ - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+ - $ref: /schemas/input/input.yaml#
+
+properties:
+ compatible:
+ const: touchnetix,ax54a
+
+ reg:
+ enum: [ 0x66, 0x67 ]
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ vdda-supply:
+ description: Analog power supply regulator on VDDA pin
+
+ vddi-supply:
+ description: I/O power supply regulator on VDDI pin
+
+required:
+ - compatible
+ - reg
+ - vdda-supply
+ - vddi-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ touchscreen@66 {
+ compatible = "touchnetix,ax54a";
+ reg = <0x66>;
+ interrupt-parent = <&gpio2>;
+ interrupts = <2 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>;
+ vdda-supply = <&vdda_reg>;
+ vddi-supply = <&vddi_reg>;
+ poll-interval = <20>;
+ };
+ };
+...
--
2.47.3
^ permalink raw reply related
* [PATCH v7 0/4] Input: Add support for TouchNetix aXiom touchscreen
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, 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, devicetree, linux-input, kernel, Marco Felsch,
Mamta Shukla, Krzysztof Kozlowski
Hi,
this adds the support for the TouchNetix aXiom touchcontroller family.
The following features are added:
- I2C communication
- Input event handling
- Touchcontroller firmware (AXFW) updates
- Touchcontroller config (TH2CFGBIN) updates
- Poll or IRQ support
Many thanks for Dmitry's input on my v5. I included all changes
requested from him.
Regards,
Marco
Changes in v7:
- Link to v6: https://lore.kernel.org/all/20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@pengutronix.de/
- Rebased on top of 7.2-rc1
- Fix U33 rev.6 usage (Mamta)
- Fix bootloader mode handling, required to disable the IRQ (Andrew)
- Fix bug in usage table population which led to a nullptr bug (Andrew)
Changes in v6:
- Link to v5: https://lore.kernel.org/r/20260111-v6-10-topic-touchscreen-axiom-v5-0-f94e0ae266cb@pengutronix.de
- Fix update POLL mode
- Fix max. input values
- Resolve some "TODO" and "Downstream" comments (Andrew)
- Add 0d, xl fw_variant (Andrew)
- Use 2sec timeout for u02 swreset (Andrew)
- Fix IRQ-runmode input-device registration after FW update
- Drop legacy ALC firmware support, since no fw sanity check could be performed
which is bad for user experience if they provided the wrong file accidentally.
- drop BLP_PRE_MODE
- make AXIOM_U31 rev.1 required
- axiom_usage_supported: drop iter and instead use new usage_table_by_baseaddr
- axiom_axfw_fw_write: drop goto error handling
- adapt comments
- rework axiom_i2c_probe (Dmitry)
- rework axiom_power_*_device and axiom_parse_firmware (Dmitry)
- rework axiom_register_input_dev (Dmitry)
- rework axiom_register_fwl (Dmitry)
- rework axiom_cfg_fw_cancel (Dmitry)
- rework axiom_cfg_fw_write (Dmitry)
- rework axiom_verify_volatile_mem (Dmitry)
- rework axiom_write_cfg_chunk (Dmitry)
- rework axiom_cfg_fw_prepare (Dmitry)
- rework axiom_axfw_fw_* functions (Dmitry)
- rework axiom_blp_reset (Dmitry)
- rework axiom_blp_write_chunk (Dmitry)
- rework axiom_enter_bootloader_mode (Dmitry)
- rework axiom_axfw_fw_prepare (Dmitry)
- rework regmap_read/write (Dmitry)
- rework axiom_u34_rev1_process_report (Dmitry)
- rework axiom_cdu_wait_idle (Dmitry)
- rework axiom_u64_cds_enabled (Dmitry)
- rework axiom_u42_get_touchslots (Dmitry)
- rework axiom_u33_read (s/ret/error/) (Dmitry)
- rework axiom_u31_device_discover (Dmitry)
- simple s/ret/error/ (Dmitry)
- rework u04 handling (Dmitry)
- rework u02 handling (Dmitry)
- align function name accordingly (Dmitry)
- fix indentation (Dmitry)
Changes in v5:
- Link to v4: https://lore.kernel.org/r/20260106-v6-10-topic-touchscreen-axiom-v4-0-9e9b69c84926@pengutronix.de
- fix sysfs documentation description indentation and date
Changes in v4:
- Link to v3: https://lore.kernel.org/r/20250821-v6-10-topic-touchscreen-axiom-v3-0-940ccee6dba3@pengutronix.de
- rebased on top of v6.19-rc1
- collect r-b tags
Changes in v3:
- Link to v2: https://lore.kernel.org/r/20250529-v6-10-topic-touchscreen-axiom-v2-0-a5edb105a600@pengutronix.de
- firmware: fix commit message (Russ)
- dt-bindings: Add ack from Krzysztof
- dt-bindings: make use of GPIO_ACTIVE_LOW (Krzysztof)
- dt-bindings: drop 'panel: true' property (Krzysztof)
- driver: make use of sysfs_emit (Greg)
- driver: s/WARN()/dev_warn()/ to not take down the system (Greg)
- driver: fix build dependency error by adding "depends on DRM || !DRM"
- driver: harmonize usage printing to u%02X
Changes in v2:
- Link to v1: https://lore.kernel.org/r/20241119-v6-10-topic-touchscreen-axiom-v1-0-6124925b9718@pengutronix.de
- Rework the firmware-duplicate handling -> expose the error to the
userspace
- Drop Krzysztof Kozlowski ACK and RB
- Add panel-follower support
- Add sysfs-driver-input-touchnetix-axiom documentation
- Add support for new firmware 4.8.9
- Add support to handle 2D and 3D firmware
---
Kamel Bouhara (2):
dt-bindings: vendor-prefixes: Add TouchNetix AS
dt-bindings: input: Add TouchNetix axiom touchscreen
Marco Felsch (2):
firmware_loader: expand firmware error codes with up-to-date error
Input: Add TouchNetix aXiom I2C Touchscreen support
.../testing/sysfs-driver-input-touchnetix-axiom | 80 +
.../input/touchscreen/touchnetix,ax54a.yaml | 62 +
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
drivers/base/firmware_loader/sysfs_upload.c | 1 +
drivers/input/touchscreen/Kconfig | 17 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/touchnetix_axiom.c | 3141 ++++++++++++++++++++
include/linux/firmware.h | 2 +
lib/test_firmware.c | 1 +
9 files changed, 3307 insertions(+)
---
base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
change-id: 20240704-v6-10-topic-touchscreen-axiom-105761e81011
Best regards,
--
Marco Felsch <m.felsch@pengutronix.de>
^ permalink raw reply
* [PATCH v7 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, 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, devicetree, linux-input, kernel, Mamta Shukla,
Marco Felsch
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
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)
update the other handles the touchcontroller configuration (TH2CFGBIN)
update.
Co-developed-by: Mamta Shukla <mamta.shukla@leica-geosystems.com>
Signed-off-by: Mamta Shukla <mamta.shukla@leica-geosystems.com>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
.../testing/sysfs-driver-input-touchnetix-axiom | 80 +
drivers/input/touchscreen/Kconfig | 17 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/touchnetix_axiom.c | 3141 ++++++++++++++++++++
4 files changed, 3239 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..8262673630557bf1e595a97ec23e66c1c5370f71
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom
@@ -0,0 +1,80 @@
+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: "0d", "2d", "3d", "force", "xl" 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: 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 9b9ae8ac3f7fd3b9e948083fcd05afabdcd69bca..399db93c337de61a0d759fd667de1fffa10aacb4 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 bfd9de83389d8833dbe7cc2641cb24493cdf3807..0d139fff701aa6466d2ecd6f6b27fcc8006a0575 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -88,6 +88,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..b945ccda326bdd726420e2a26021ba8529d6e8b8
--- /dev/null
+++ b/drivers/input/touchscreen/touchnetix_axiom.c
@@ -0,0 +1,3141 @@
+// 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_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;
+};
+
+struct axiom_u33_rev6 {
+ __le32 runtime_crc;
+ __le32 runtime_nvm_crc;
+ __le32 bootloader_crc;
+ __le32 nvltlusageconfig_crc;
+ __le32 vltusageconfig_crc;
+ __le32 reserved[3];
+ __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 [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
+ */
+#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 */
+#define AXIOM_MAX_BASEADDR 256
+/*
+ * 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_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;
+ /* Used for access by usage number */
+ 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];
+ /* Used for access by usage base address */
+ struct axiom_usage_table_entry *usage_table_by_baseaddr[AXIOM_MAX_BASEADDR];
+};
+
+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_U31, 1),
+ AXIOM_RO_USAGE(AXIOM_U33, 2),
+ AXIOM_RO_USAGE(AXIOM_U33, 3),
+ AXIOM_RO_USAGE(AXIOM_U33, 6),
+ 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_REPORT_USAGE(AXIOM_U41, 5, axiom_u41_rev2_process_report),
+ AXIOM_USAGE(AXIOM_U42, 1),
+ AXIOM_USAGE(AXIOM_U42, 4),
+ AXIOM_USAGE(AXIOM_U42, 7),
+ 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_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_enable_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+
+ if (client->irq && ts->irq_setup_done)
+ enable_irq(client->irq);
+}
+
+static void axiom_disable_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+
+ if (client->irq && ts->irq_setup_done)
+ disable_irq(client->irq);
+}
+
+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_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_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 regaddr)
+{
+ struct axiom_usage_table_entry *entry;
+ struct device *dev = ts->dev;
+ unsigned char baseaddr;
+
+ if (axiom_skip_usage_check(ts))
+ return true;
+
+ baseaddr = FIELD_GET(AXIOM_USAGE_BASEADDR_MASK, regaddr);
+ entry = ts->usage_table_by_baseaddr[baseaddr];
+
+ dev_dbg(dev, "Checking support for i2c-reg:%#04x (reg-baseaddr:%#02x)\n",
+ regaddr, baseaddr);
+
+ /*
+ * Ensure that no one messed with the driver e.g.:
+ * - by not retrieving the baseaddr properly, or
+ * - by making use of an usage without adding it to
+ * driver_required_usages[].
+ */
+ if (!entry) {
+ dev_warn(dev, "Device doesn't support this baseaddr, driver bug!\n");
+ return false;
+ }
+
+ if (!entry->info) {
+ dev_warn(dev, "Driver doesn't support this usage, driver bug!\n");
+ return false;
+ }
+
+ return true;
+}
+
+static int axiom_process_report(struct axiom_data *ts, unsigned char usage_num,
+ const u8 *buf, size_t buflen);
+
+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. Poll via axiom_process_report()
+ * and without the help of axiom_poll() since the input device may not
+ * be available yet.
+ */
+ do {
+ axiom_process_report(ts, AXIOM_U34, NULL, 0);
+ 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 error, 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.
+ */
+ 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 0;
+}
+
+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 error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U02);
+ reg += AXIOM_U02_REV1_COMMAND_REG;
+
+ error = regmap_raw_write(ts->regmap, reg, msg, sizeof(*msg));
+ if (error)
+ return error;
+
+ 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 error;
+
+ 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);
+
+ error = axiom_u02_send_msg(ts, &msg, false);
+ if (error)
+ return error;
+
+ /* Downstream axcfg.py waits for 2sec without checking U01 response */
+ 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 0;
+}
+
+static int axiom_u02_swreset(struct axiom_data *ts)
+{
+ 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;
+
+ 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 to add the additional fsleep(1sec).
+ * Touchnetix said that the boot can take up to 2sec if all self tests
+ * are enabled, so wait 2sec for the HELLO message.
+ */
+ if (!axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
+ msecs_to_jiffies(2 * MSEC_PER_SEC))) {
+ dev_err(ts->dev, "Error swreset timedout\n");
+ error = -ETIMEDOUT;
+ }
+
+ fsleep(USEC_PER_SEC);
+
+ return error;
+}
+
+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 error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_ENTERBOOTLOADER);
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1);
+ error = axiom_u02_send_msg(ts, &msg, true);
+ 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);
+ error = axiom_u02_send_msg(ts, &msg, true);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key2: %d\n", error);
+ return error;
+ }
+
+ /* No need to wait since we wait afterwards anyway */
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3);
+ 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 */
+ 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 u8 *axiom_u04_get(struct axiom_data *ts)
+{
+ unsigned int bufsize;
+ unsigned int reg;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U04))
+ return ERR_PTR(-EINVAL);
+
+ bufsize = axiom_usage_size(ts, AXIOM_U04);
+ u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
+ if (!buf)
+ return ERR_PTR(-ENOMEM);
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U04);
+ error = regmap_raw_read(ts->regmap, reg, buf, bufsize);
+ if (error)
+ return ERR_PTR(error);
+
+ return_ptr(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;
+
+ bufsize = axiom_usage_size(ts, AXIOM_U04);
+ 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 error;
+
+ 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);
+
+ 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;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG, &val);
+ if (error)
+ return error;
+ ts->fw_major = val;
+
+ 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;
+
+ 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);
+
+ 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);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_HIGH_REG, &val);
+ if (error)
+ return error;
+ ts->jedec_id = val << 8;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_LOW_REG, &val);
+ if (error)
+ return error;
+ 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 *entry;
+ struct regmap *regmap = ts->regmap;
+ unsigned int mode, num_usages;
+ struct device *dev = ts->dev;
+ unsigned int i;
+ int error;
+
+ axiom_set_runmode(ts, AXIOM_DISCOVERY_MODE);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &mode);
+ if (error) {
+ dev_err(dev, "Failed to read MODE\n");
+ return error;
+ }
+
+ mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
+ if (mode == AXIOM_U31_REV1_MODE_BLP)
+ axiom_set_runmode(ts, AXIOM_BLP_MODE);
+
+ error = axiom_u31_parse_device_info(ts);
+ if (error) {
+ dev_err(dev, "Failed to parse device info\n");
+ return error;
+ }
+
+ /* 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;
+ }
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_NUM_USAGES_REG, &num_usages);
+ if (error) {
+ dev_err(dev, "Failed to read NUM_USAGES\n");
+ return error;
+ }
+
+ 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;
+
+ 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 error;
+ }
+
+ /*
+ * 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));
+ memset(ts->usage_table_by_baseaddr, 0, sizeof(ts->usage_table_by_baseaddr));
+
+ for (i = 0, entry = u31_usage_table; i < num_usages; i++, entry++) {
+ unsigned char idx = entry->usage_num;
+ const struct axiom_usage_info *info;
+ bool skip_usage_add = false;
+ 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 but skip adding it to the 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);
+ error = -EACCES;
+ skip_usage_add = true;
+ }
+
+ if (skip_usage_add)
+ continue;
+
+ size_bytes = axiom_get_usage_size_bytes(entry);
+
+ ts->usage_table[idx].baseaddr =
+ FIELD_PREP(AXIOM_USAGE_BASEADDR_MASK, entry->start_page);
+ ts->usage_table[idx].size_bytes = size_bytes;
+ ts->usage_table[idx].populated = true;
+ ts->usage_table[idx].info = info;
+ /* Reports are indirectly accessed, no baseaddr */
+ if (!axiom_usage_entry_is_report(entry))
+ ts->usage_table_by_baseaddr[entry->start_page] =
+ &ts->usage_table[idx];
+
+ if (axiom_usage_entry_is_report(entry) &&
+ ts->max_report_byte_len < size_bytes)
+ ts->max_report_byte_len = size_bytes;
+ }
+
+ /*
+ * Each usage support up to 256-byte. The 257-byte would point to the
+ * next usage.
+ */
+ if (ts->max_report_byte_len > AXIOM_PAGE_BYTE_LEN) {
+ dev_err(dev, "Invalid report length (%u-byte) detected\n",
+ ts->max_report_byte_len);
+ error = -EACCES;
+ }
+
+ if (error)
+ return error;
+
+ /* From now on we are in TCP mode to include usage revision checks */
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ 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 error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U33))
+ return -EINVAL;
+
+ /* Reset previous stored values */
+ memset(crc, 0, sizeof(*crc));
+
+ if (axiom_usage_rev(ts, AXIOM_U33) == 2) {
+ struct axiom_u33_rev2 val;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U33);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ 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);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ 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);
+ } else if (axiom_usage_rev(ts, AXIOM_U33) == 6) {
+ struct axiom_u33_rev6 val;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U33);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ 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->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;
+ case 7:
+ return buf[AXIOM_U42_REV4_REPORT_ID_CONTAINS(touch_num)] ==
+ AXIOM_U42_REV4_REPORT_ID_TOUCH;
+ default:
+ /* Should never happen */
+ return false;
+ }
+}
+
+static bool axiom_u42_get_touchslots(struct axiom_data *ts)
+{
+ unsigned int bufsize;
+ unsigned int reg;
+ int error, i;
+
+ bufsize = axiom_usage_size(ts, AXIOM_U42);
+ u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
+ if (!buf)
+ return false;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U42);
+ 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;
+ 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 true;
+}
+
+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 error;
+
+ ts->cds_enabled = false;
+
+ if (axiom_driver_supports_usage(ts, AXIOM_U64)) {
+ reg = axiom_usage_baseaddr(ts, AXIOM_U64);
+ reg += AXIOM_U64_REV2_ENABLECDSPROCESSING_REG;
+
+ 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 error, 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.
+ */
+ 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 0;
+}
+
+/*********************** 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 error;
+
+ error = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
+ if (error)
+ return error;
+
+ /* TODO: Add overflow statistics */
+
+ /* REPORTLENGTH is in uint16 */
+ len = FIELD_GET(AXIOM_U34_REV1_REPORTLENGTH_MASK, buf[0]);
+ len *= 2;
+
+ /*
+ * The device keeps the IRQ asserted till the I2C-STOP signal was
+ * received and optional longer (up to 40us). This can trigger the ISR
+ * a 2nd time albeit the device data is not ready yet. In such case the
+ * device sends 0-length reports. Don't treat this as error.
+ */
+ if (len == 0) {
+ dev_dbg_ratelimited(dev, "0-length report received\n");
+ return 0;
+ }
+
+ /*
+ * 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:
+ /*
+ * axiom_driver_supports_usage() is not required since the
+ * correct .process_report() hooks are assigned during the
+ * discovery.
+ */
+ 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;
+ struct i2c_msg xfer[2];
+ u16 xferlen, addr;
+ 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);
+
+ if (!axiom_usage_supported(ts, addr))
+ 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, ARRAY_SIZE(xfer));
+ if (likely(ret == ARRAY_SIZE(xfer)))
+ return 0;
+
+ return ret < 0 ? ret : -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);
+ struct axiom_cmd_header hdr;
+ size_t val_size, msg_size;
+ u16 xferlen, addr;
+ 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);
+
+ if (!axiom_usage_supported(ts, addr))
+ return -EINVAL;
+
+ msg_size = sizeof(hdr) + val_size;
+ u8 *buf __free(kfree) = 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);
+ if (likely(ret == msg_size))
+ return 0;
+
+ return ret < 0 ? ret : -EIO;
+}
+
+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 axiom_data *ts,
+ struct axiom_firmware *afw,
+ const u8 *data, u32 size)
+{
+ u8 major_ver, minor_ver, rc_ver, status, variant;
+ u32 fw_file_crc32, crc32_calc;
+ struct device *dev = ts->dev;
+ unsigned int signature_len;
+ u16 fw_file_format_ver;
+ u16 fw_file_device_id;
+
+ if (size < sizeof(struct axiom_fw_axfw_hdr)) {
+ dev_err(dev, "Invalid AXFW file size\n");
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+ }
+
+ signature_len = strlen(AXIOM_FW_AXFW_SIGNATURE);
+ if (strncmp(data, AXIOM_FW_AXFW_SIGNATURE, signature_len)) {
+ dev_err(dev, "No AXFW signature, abort firmware update\n");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ 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);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ 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);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ 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);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ /*
+ * The afterward fw duplication check requires that the devices
+ * discovery ran successfully at least once (-> device is in TCP mode).
+ * This is not always ensured because:
+ * * The usage discovery failed either during the probe or after the
+ * firmware update.
+ * * The device came up in bootloader mode because no valid firmware
+ * was found by the bootloader.
+ * * Downloading the firmware failed in between and the device is still
+ * in bootloader mode, or
+ */
+ 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) {
+ return FW_UPLOAD_ERR_DUPLICATE;
+ }
+
+ dev_info(dev, "Detected AXFW %02u.%02u.%02u (%s)\n",
+ major_ver, minor_ver, rc_ver,
+ status ? "production" : "engineering");
+
+ 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);
+
+ /*
+ * 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 error;
+
+ error = axiom_u02_wait_idle(ts);
+ if (error)
+ goto err_out;
+
+ 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:
+ return error;
+}
+
+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 error;
+
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+
+ /*
+ * 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;
+
+ 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;
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+ }
+
+ 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 error;
+
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+
+ /*
+ * For some reason this write fail with -ENXIO. Skip checking the return
+ * code (which is also done by the downstream axfw.py tool and poll u31
+ * instead.
+ */
+ regmap_raw_write(ts->regmap, reg, &reset_cmd, sizeof(reset_cmd));
+
+ do {
+ error = regmap_read(ts->regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG,
+ &mode);
+ if (!error)
+ break;
+
+ fsleep(250 * USEC_PER_MSEC);
+ } while (attempts--);
+
+ 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);
+ if (mode == AXIOM_U31_REV1_MODE_BLP) {
+ dev_err(dev, "Device still in BLP mode, abort\n");
+ return -EINVAL;
+ }
+
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ 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_unlock_input_return_hw_error(struct axiom_data *ts)
+{
+ axiom_unlock_input_device(ts);
+ return FW_UPLOAD_ERR_HW_ERROR;
+}
+
+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;
+ int error;
+
+ /*
+ * According Touchnetix the IRQ pin gets reconfigured in bootloader
+ * mode. Which can cause spurious IRQs. Therefore the IRQ should be
+ * disabled on the host.
+ *
+ * See the cleanup routine for the balanced enable.
+ */
+ if (axiom_get_runmode(ts) != AXIOM_BLP_MODE)
+ axiom_disable_irq(ts);
+
+ /* Done before cancel check due to cleanup based put */
+ error = pm_runtime_resume_and_get(ts->dev);
+ if (error)
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ scoped_guard(mutex, &afw->lock) {
+ if (afw->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 update\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ /* 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_get_runmode(ts) != AXIOM_BLP_MODE) {
+ error = axiom_enter_bootloader_mode(ts);
+ if (error)
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ 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.
+ */
+ error = axiom_blp_write_chunk(ts, data, len);
+ if (error) {
+ /*
+ * Tests showed that the bootloader mode must be exited
+ * if an invalid chunk was received by the bootloader fw
+ * since all following attempts to download valid chunks
+ * will fail. Try do so via axiom_blp_reset() but tests
+ * also showed that this may fail too. So inform the
+ * user and hope that the full power-cycle helps. To get
+ * the device back into a working mode where the device
+ * accepts data again.
+ */
+ if (axiom_blp_reset(ts))
+ dev_warn(dev, "Couldn't recover device, device requires power-cycle\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ size -= len;
+ *written += len;
+ data += len;
+ }
+
+ error = axiom_blp_reset(ts);
+ if (error) {
+ dev_err(dev, "BLP reset failed\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ error = axiom_u31_device_discover(ts);
+ /* Unlock before the input device gets unregistered/updated */
+ axiom_unlock_input_device(ts);
+ if (error) {
+ /*
+ * This is critical and we need to avoid that the user-space can
+ * still use the input-dev.
+ */
+ axiom_unregister_input_dev(ts);
+ dev_err(dev, "Device discovery failed after AXFW firmware update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ error = axiom_update_input_dev(ts);
+ if (error) {
+ dev_err(dev, "Input device update failed after AXFW firmware update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ dev_info(dev, "AXFW update successful\n");
+
+ return FW_UPLOAD_ERR_NONE;
+}
+
+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];
+
+ guard(mutex)(&afw->lock);
+ afw->cancel = true;
+}
+
+static void axiom_axfw_fw_handle_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+ struct irq_data *data;
+
+ if (!ts->irq_setup_done)
+ return;
+
+ data = irq_get_irq_data(client->irq);
+ /*
+ * The cleanup is called in both cases: success and failure. In case
+ * of failure the device may still be in bootloader mode. Don't enable
+ * the IRQ in such case to avoid a possible IRQ storm because of the
+ * unknown IRQ pin behavior in bootloader mode.
+ *
+ * Furthermore if the IRQ is enabled at this point this means that the
+ * firmware update was necessary to get the device in a working state
+ * because the probe() aborted due to firmware incompatibilities. Skip
+ * the enable step if the IRQ is not disabled to not mess with the
+ * refcount.
+ */
+ if (irqd_irq_disabled(data) &&
+ axiom_get_runmode(ts) != AXIOM_BLP_MODE)
+ axiom_enable_irq(ts);
+}
+
+static void axiom_axfw_fw_cleanup(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+
+ mutex_unlock(&ts->fwupdate_lock);
+ axiom_axfw_fw_handle_irq(ts);
+ 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;
+ unsigned int i;
+
+ /*
+ * Provide some debug output of u33, because the th2cfg chunks caused
+ * a few headaches already.
+ */
+ for (i = 0; i < cfg->usage_length / sizeof(u32); i++)
+ dev_dbg(ts->dev, "u33 th2cfgbin chunk[%u]:%#x\n", i,
+ get_unaligned_le32(&u33_data[i]));
+
+ switch (cfg->usage_rev) {
+ case 2:
+ 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]);
+ crc->u93_profiles = get_unaligned_le32(&u33_data[7]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[8]);
+ return 0;
+ case 3:
+ 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]);
+ 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;
+ case 6:
+ crc->runtime = get_unaligned_le32(u33_data);
+ crc->nvltlusageconfig = get_unaligned_le32(&u33_data[3]);
+ crc->vltusageconfig = get_unaligned_le32(&u33_data[4]);
+ crc->u93_profiles = get_unaligned_le32(&u33_data[8]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[9]);
+ return 0;
+ default:
+ dev_err(ts->dev, "The driver doesn't support u33 revision %u\n",
+ cfg->usage_rev);
+ return -EINVAL;
+ }
+}
+
+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 axiom_data *ts,
+ struct axiom_firmware *afw,
+ const u8 *data, u32 size)
+{
+ u32 cur_runtime_crc, fw_runtime_crc;
+ struct axiom_fw_cfg_chunk chunk;
+ struct device *dev = ts->dev;
+ u32 signature;
+ int error;
+
+ if (axiom_get_runmode(ts) != AXIOM_TCP_MODE) {
+ dev_err(dev, "Device not in TCP mode, abort TH2CFG update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ if (size < sizeof(struct axiom_fw_cfg_hdr)) {
+ dev_err(dev, "Invalid TH2CFG file size\n");
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+ }
+
+ signature = get_unaligned_be32(data);
+ if (signature != AXIOM_FW_CFG_SIGNATURE) {
+ dev_err(dev, "Invalid TH2CFG signature\n");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ /* 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");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ 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))
+ 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);
+ 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(&ts->fwupdate_lock);
+
+ ret = __axiom_cfg_fw_prepare(ts, afw, data, size);
+
+ /*
+ * 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 error;
+
+ /* Zero out the volatile memory except for the user content in u04 */
+ u8 *buf __free(kfree) = axiom_u04_get(ts);
+ if (IS_ERR(buf))
+ return PTR_ERR(buf);
+
+ error = axiom_u02_fillconfig(ts);
+ if (error)
+ return error;
+
+ 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)
+{
+ 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 error;
+
+ 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, 0, sizeof(cdu.data));
+ memcpy(cdu.data, &chunk->usage_content[pos], size);
+
+ 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 error;
+ }
+
+ error = axiom_cdu_wait_idle(ts, chunk->usage_num);
+ if (error) {
+ dev_err(dev, "CDU write wait-idle failed\n");
+ return error;
+ }
+
+ 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);
+
+ 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 error;
+ }
+
+ 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;
+ }
+
+ 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)
+{
+ unsigned int reg;
+ int error;
+
+ dev_dbg(ts->dev, "Write th2cfg-chunk - u%02X.rev%u length:%u\n",
+ chunk->usage_num, chunk->usage_rev, chunk->usage_length);
+
+ if (ts->usage_table[chunk->usage_num].info &&
+ ts->usage_table[chunk->usage_num].info->is_cdu) {
+ 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;
+ }
+
+ error = axiom_u02_wait_idle(ts);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static int axiom_verify_volatile_mem(struct axiom_data *ts)
+{
+ int error;
+
+ error = axiom_u02_computecrc(ts);
+ if (error)
+ return error;
+
+ /* Query the new CRCs after they are re-computed */
+ 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;
+}
+
+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;
+ int error;
+
+ /* Done before cancel check due to cleanup based put */
+ error = pm_runtime_resume_and_get(ts->dev);
+ if (error)
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ scoped_guard(mutex, &afw->lock) {
+ if (afw->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;
+ }
+
+ error = axiom_u02_stop(ts);
+ if (error)
+ goto err_swreset;
+
+ error = axiom_zero_volatile_mem(ts);
+ if (error)
+ 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);
+ } else {
+ error = axiom_write_cfg_chunk(ts, &chunk);
+ if (error) {
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+ goto err_swreset;
+ }
+ }
+
+ data += chunk_len;
+ size -= chunk_len;
+ *written += chunk_len;
+ }
+
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ /* Ensure that the chunks are written correctly */
+ error = axiom_verify_volatile_mem(ts);
+ if (error) {
+ dev_err(dev, "Failed to verify written config, abort\n");
+ goto err_swreset;
+ }
+
+ error = axiom_u02_save_config(ts);
+ if (error)
+ goto err_swreset;
+
+ /*
+ * (Re)start the device with the new config. Start the device AE
+ * either via u02 SW_RESET or u02 CMD_START, the behavior is the same
+ * according Touchnetix.
+ */
+ error = axiom_u02_swreset(ts);
+ if (error) {
+ dev_err(dev, "Soft reset failed\n");
+ goto err_unlock;
+ }
+
+ error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
+ if (error)
+ goto err_unlock;
+
+ if (axiom_verify_crcs(ts))
+ goto err_unlock;
+
+ /* Unlock before the input device gets unregistered */
+ axiom_unlock_input_device(ts);
+
+ error = axiom_update_input_dev(ts);
+ if (error) {
+ 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 error == -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];
+
+ guard(mutex)(&afw->lock);
+ afw->cancel = true;
+}
+
+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 error;
+
+ 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");
+
+ error = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
+ if (error)
+ return error;
+
+ 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");
+
+ error = devm_add_action_or_reset(dev, axiom_remove_cfg_fwl_action, ts);
+ if (error)
+ return error;
+
+ 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 2:
+ val = "force";
+ break;
+ case 3:
+ val = "0d";
+ break;
+ case 4:
+ val = "xl";
+ 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 error;
+
+ 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, 0, 0);
+ input_set_abs_params(input, ABS_MT_POSITION_Y, 0, AXIOM_MAX_XY, 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_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;
+ }
+
+ 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 error;
+ }
+
+ /*
+ * 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 (client->irq) {
+ if (!ts->irq_setup_done) {
+ 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 error;
+ }
+ ts->irq_setup_done = true;
+ }
+ } else {
+ error = input_setup_polling(input, axiom_poll);
+ if (error) {
+ input_free_device(input);
+ dev_err(dev, "Setup polling mode failed\n");
+ return error;
+ }
+
+ input_set_poll_interval(input, ts->poll_interval);
+ }
+
+ input_set_drvdata(input, ts);
+ ts->input = input;
+
+ error = input_register_device(input);
+ if (error) {
+ input_free_device(input);
+ ts->input = NULL;
+ dev_err(dev, "Failed to register input device\n");
+ };
+
+ return error;
+}
+
+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 error;
+
+ ts->supplies[0].supply = "vddi";
+ ts->supplies[1].supply = "vdda";
+ ts->num_supplies = ARRAY_SIZE(ts->supplies);
+
+ 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);
+ 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_up_device(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ int error;
+
+ error = regulator_bulk_enable(ts->num_supplies, ts->supplies);
+ if (error) {
+ dev_err(dev, "Failed to enable power supplies\n");
+ return error;
+ }
+
+ 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 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,
+ 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 error;
+
+ 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);
+
+ error = axiom_register_fwl(ts);
+ if (error)
+ return error;
+
+ error = axiom_parse_firmware(ts);
+ if (error)
+ return error;
+
+ 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);
+ error = devm_pm_runtime_enable(dev);
+ if (error)
+ return dev_err_probe(dev, error, "Failed to enable pm-runtime\n");
+
+ error = axiom_register_panel_follower(ts);
+ if (error)
+ return dev_err_probe(dev, error, "Failed to register panel follower\n");
+
+ 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.
+ *
+ * The U02 usage has to be supported, since this is the usage which is
+ * used to put the device into bootloader mode!
+ *
+ * TODO: Add support for the emergency bootloader mode enter sequence.
+ * The emergency enter is done by toggling the nRESET pin 5-times in a
+ * row.
+ */
+ if (error) {
+ if (IS_ENABLED(CONFIG_FW_UPLOAD) &&
+ axiom_driver_supports_usage(ts, AXIOM_U02) &&
+ (axiom_get_runmode(ts) == AXIOM_DISCOVERY_MODE ||
+ axiom_get_runmode(ts) == AXIOM_BLP_MODE)) {
+ 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, error, "Device discovery failed\n");
+ }
+
+ error = axiom_register_input_dev(ts, false);
+ pm_runtime_mark_last_busy(dev);
+ pm_runtime_put_sync_autosuspend(dev);
+ 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");
+ }
+
+ 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);
+
+ axiom_disable_irq(ts);
+ axiom_power_down_device(ts);
+
+ return 0;
+}
+
+static int axiom_runtime_resume(struct device *dev)
+{
+ struct axiom_data *ts = dev_get_drvdata(dev);
+ int error;
+
+ error = axiom_power_up_device(ts);
+ if (error)
+ return error;
+
+ axiom_enable_irq(ts);
+
+ 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
^ permalink raw reply related
* Re: [PATCH 05/10] HID: steam: Coalesce rumble packets
From: sashiko-bot @ 2026-07-02 22:34 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702222145.1863104-5-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Self-deadlock in workqueue callbacks due to double locking of `steam->report_mutex`.
--
commit d77d97e537a9b42840329852b61faf365ef03366
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Coalesce rumble packets
This commit introduces a delayed workqueue to rate-limit and coalesce rumble
packets to 20Hz on Steam Deck devices. It also modifies the existing rumble
work callback to add a mutex guard.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 34653ad383ac0..01b64194ec97b 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -603,10 +604,26 @@ static void steam_haptic_rumble_cb(struct work_struct *work)
> {
> struct steam_device *steam = container_of(work, struct steam_device,
> rumble_work);
> +
> + guard(mutex)(&steam->report_mutex);
> steam_haptic_rumble(steam, 0, steam->rumble_left,
> steam->rumble_right, 2, 0);
[Severity: High]
Will this cause a self-deadlock when playing force-feedback rumble effects?
This callback acquires report_mutex using guard(), but then immediately calls
steam_haptic_rumble(), which acquires the same mutex again:
steam_haptic_rumble() {
...
mutex_lock(&steam->report_mutex);
ret = steam_send_report(steam, report, sizeof(report));
mutex_unlock(&steam->report_mutex);
return ret;
}
Since Linux mutexes are not recursive, won't this hang the worker thread
and block any subsequent operations needing the report_mutex?
> }
>
> +static void steam_coalesce_rumble_cb(struct work_struct *work)
> +{
> + struct steam_device *steam = container_of(to_delayed_work(work),
> + struct steam_device,
> + coalesce_rumble_work);
> +
> + guard(mutex)(&steam->report_mutex);
> + steam_haptic_rumble(steam, 0, steam->rumble_left,
> + steam->rumble_right, 2, 0);
[Severity: High]
Does this new callback have the same self-deadlock issue as described above,
since it also calls steam_haptic_rumble() while holding report_mutex?
> +
> + if (steam->rumble_left || steam->rumble_right)
> + schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=5
^ permalink raw reply
* Re: [PATCH 06/10] HID: steam: Fully unregister controller when hidraw is opened
From: sashiko-bot @ 2026-07-02 22:34 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702222145.1863104-6-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] Repeatedly opening and closing the hidraw node causes an unbounded kernel memory leak due to devm_kasprintf() allocations.
- [Medium] Unregistering and reregistering the controller on hidraw open/close causes connection state messages to spam the kernel log.
Pre-existing issues:
- [High] Error paths in steam_probe fail to clean up the global steam_devices list, leading to a use-after-free.
--
commit e81d4b5bb1894be8818d15e10c2e8f4c979081fe
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Fully unregister controller when hidraw is opened
To avoid conflicts between anything touching the hidraw and the driver we
had previously detached the evdev nodes when the hidraw is opened. However,
this isn't sufficient to avoid FEATURE reports from conflicting, so we
change to fully unregistering the controller internally, leaving only the
hidraw active until it's closed.
This also unifies the unregister and connect callbacks, as now the logic
between these two callbacks is identical.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 01b64194ec97b..546916da31f92 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -1144,38 +1143,41 @@ static int steam_register(struct steam_device *steam)
>
> static void steam_unregister(struct steam_device *steam)
> {
> + if (!steam->serial_no[0])
> + return;
> +
> + hid_info(steam->hdev, "Steam Controller '%s' disconnected",
> + steam->serial_no);
[Severity: Medium]
Will repeatedly opening the hidraw node spam the kernel log?
Opening the hidraw node triggers this unregister path, printing a
"disconnected" message. When hidraw is closed, steam_register() is called and
logs a "connected" message.
Can a user-space daemon or application opening and closing the hidraw node
overwrite important system logs?
> steam_battery_unregister(steam);
> steam_sensors_unregister(steam);
> steam_input_unregister(steam);
> - if (steam->serial_no[0]) {
> - hid_info(steam->hdev, "Steam Controller '%s' disconnected",
> - steam->serial_no);
> - mutex_lock(&steam_devices_lock);
> - list_del_init(&steam->list);
> - mutex_unlock(&steam_devices_lock);
> - steam->serial_no[0] = 0;
> - }
> + mutex_lock(&steam_devices_lock);
> + list_del_init(&steam->list);
> + mutex_unlock(&steam_devices_lock);
> + steam->serial_no[0] = 0;
> }
>
> static void steam_work_connect_cb(struct work_struct *work)
> {
> struct steam_device *steam = container_of(work, struct steam_device,
> work_connect);
> +
> unsigned long flags;
> bool connected;
> + bool opened;
> int ret;
>
> spin_lock_irqsave(&steam->lock, flags);
> + opened = steam->client_opened;
> connected = steam->connected;
> spin_unlock_irqrestore(&steam->lock, flags);
>
> - if (connected) {
> + if (connected && !opened) {
> ret = steam_register(steam);
[Severity: High]
Does this cause a memory leak by repeatedly registering the battery?
When hidraw is opened, steam_unregister() is called, which sets
steam->serial_no[0] = 0.
When hidraw is closed, steam_register() sees the cleared serial number and
calls steam_battery_register():
drivers/hid/hid-steam.c:steam_register() {
...
if (!steam->serial_no[0]) {
...
if (steam->quirks & STEAM_QUIRK_WIRELESS)
steam_battery_register(steam);
...
}
drivers/hid/hid-steam.c:steam_battery_register() {
...
steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
GFP_KERNEL, "steam-controller-%s-battery",
steam->serial_no);
...
}
Because devm_ allocations are bound to the physical device's lifetime, and the
device is never unbound during this process, will repeatedly opening and
closing hidraw cause an unbounded memory leak?
> - if (ret) {
> + if (ret)
> hid_err(steam->hdev,
> "%s:steam_register failed with error %d\n",
> __func__, ret);
> - }
> } else {
> steam_unregister(steam);
> }
[ ... ]
> @@ -1453,7 +1429,6 @@ static int steam_probe(struct hid_device *hdev,
> cancel_delayed_work_sync(&steam->mode_switch);
> cancel_work_sync(&steam->rumble_work);
> cancel_delayed_work_sync(&steam->coalesce_rumble_work);
> - cancel_work_sync(&steam->unregister_work);
>
> return ret;
> }
[Severity: High]
This is a pre-existing issue, but do the error paths in steam_probe() fail
to clean up the global steam_devices list?
If steam_register() fails halfway (e.g., steam_input_register() fails), it
returns an error but does not remove the device from the steam_devices list
which it added it to earlier:
drivers/hid/hid-steam.c:steam_register() {
...
if (!steam->serial_no[0]) {
...
mutex_lock(&steam_devices_lock);
if (list_empty(&steam->list))
list_add(&steam->list, &steam_devices);
mutex_unlock(&steam_devices_lock);
}
...
}
When steam_probe() receives the error, it jumps to err_hw_close, bypassing
err_steam_unregister:
drivers/hid/hid-steam.c:steam_probe() {
...
} else {
/* A wired connection is always present */
steam->connected = true;
ret = steam_register(steam);
if (ret) {
hid_err(hdev,
"%s:steam_register failed with error %d\n",
__func__, ret);
goto err_hw_close;
}
}
...
}
The steam object is subsequently freed by devres, leaving a dangling pointer
in the steam_devices list.
Can a local user trigger a use-after-free by writing to the lizard_mode
module parameter (which iterates over steam_devices)?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=6
^ permalink raw reply
* Re: [PATCH 07/10] HID: steam: Rearrange deinitialization sequence
From: sashiko-bot @ 2026-07-02 22:35 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702222145.1863104-7-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Use-After-Free due to work item rescheduling during device removal
- [High] Double-free and Use-After-Free due to lockless concurrent execution of steam_unregister()
--
commit 3f510b9fc8a4fe35042e364f53f44a52a7f1b526
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Rearrange deinitialization sequence
This fixes a narrow window during the deinitialization where callbacks
could still be scheduled during cleanup that would then have a dangling
pointer to the now-freed steam struct.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
> @@ -1443,18 +1443,18 @@ static void steam_remove(struct hid_device *hdev)
> }
>
> hid_destroy_device(steam->client_hdev);
> - cancel_delayed_work_sync(&steam->mode_switch);
> - cancel_work_sync(&steam->work_connect);
> - cancel_work_sync(&steam->rumble_work);
> - cancel_delayed_work_sync(&steam->coalesce_rumble_work);
> steam->client_hdev = NULL;
> + hid_hw_close(hdev);
> + cancel_work_sync(&steam->work_connect);
[Severity: High]
This is a pre-existing issue, but does this still leave a window where the
work item can be rescheduled?
Because hid_hw_stop() has not been called yet, the hardware can still deliver
a raw event like ID_CONTROLLER_WIRELESS. This calls steam_raw_event() which
calls steam_do_connect_event() and unconditionally queues the work item:
steam_do_connect_event()
if (changed && schedule_work(&steam->work_connect) == 0)
If this happens after cancel_work_sync() but before hid_hw_stop(), will the
delayed work item execute on a freed steam struct?
> steam->client_opened = 0;
> if (steam->quirks & STEAM_QUIRK_WIRELESS) {
> hid_info(hdev, "Steam wireless receiver disconnected");
> }
> - hid_hw_close(hdev);
> - hid_hw_stop(hdev);
> steam_unregister(steam);
[Severity: High]
This is a pre-existing issue, but could concurrent calls to steam_unregister()
result in a double-free?
If a wireless disconnect event arrives concurrently with a device unplug,
the worker thread executing steam_work_connect_cb() and the unplug thread
executing steam_remove() might both call steam_unregister().
Since steam_unregister() relies on a lockless check of steam->serial_no[0],
could both threads pass the check simultaneously and call
steam_input_unregister()? Both threads would then read steam->input via RCU
and call input_unregister_device() on the same pointer.
Also, does this lack of serialization allow the probe error path to race
with steam_register(), potentially leaving active input devices pointing
to a freed steam struct?
> + cancel_work_sync(&steam->rumble_work);
> + cancel_delayed_work_sync(&steam->mode_switch);
> + cancel_delayed_work_sync(&steam->coalesce_rumble_work);
> + hid_hw_stop(hdev);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=7
^ permalink raw reply
* Re: [PATCH 10/10] HID: steam: Retry send/recv reports if stale
From: sashiko-bot @ 2026-07-02 22:36 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260702222145.1863104-10-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Insufficient bounds checking allows uninitialized kernel stack memory to be parsed, exposed to userspace, and logged to dmesg.
--
commit 74668d65404cd2e20c8afd5929ec0da105fd2ec0
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Retry send/recv reports if stale
This commit introduces a retry mechanism for sending and receiving reports to
handle stale replies. It adds steam_exchange_report() to wrap the send and
receive logic with retries, avoiding failures from firmware bugs.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index e97431bc2828c..8d036b3412539 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> +static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
> + u8 *reply, int rsize)
> +{
> + unsigned int retries = 5;
> + int ret;
> +
> + guard(mutex)(&steam->report_mutex);
> + do {
> + ret = steam_send_report(steam, cmd, csize);
> + if (ret < 0)
> + return ret;
> + ret = steam_recv_report(steam, reply, rsize);
[ ... ]
> + if (ret < 0) {
> + hid_err(steam->hdev, "%s: error reading reply (%*ph)\n",
> + __func__, csize, cmd);
> + return ret;
> + }
> + if (reply[0] == cmd[0] && reply[1] >= 1)
[Severity: High]
This is a pre-existing issue, but does this code evaluate uninitialized
stack memory if steam_recv_report() returns a short read?
steam_recv_report() has a mathematically flawed bounds check that can fail
to reject truncated packets or bypass the check entirely for 0-1 byte replies:
if (ret >= 2 && data[1] > ret + 2) {
...
return -EPROTO;
}
Because reply is uninitialized on the stack in callers like steam_get_serial(),
can a short read from a malfunctioning or malicious USB device cause us to
read uninitialized memory here?
Could it also potentially return success if the stack happens to match the
expected values?
> + break;
> + if (retries > 0)
> + continue;
> + hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
> + rsize, reply);
[Severity: High]
This is a pre-existing issue, but if steam_recv_report() returned a short read,
does this log uninitialized stack memory to dmesg since it dumps the full
rsize bytes of the reply buffer?
> + return -EPROTO;
> + } while (retries--);
> +
> + return ret;
> +}
> +
> static int steam_get_serial(struct steam_device *steam)
> {
[ ... ]
> u8 cmd[] = {ID_GET_STRING_ATTRIBUTE, sizeof(steam->serial_no), ATTRIB_STR_UNIT_SERIAL};
> u8 reply[3 + STEAM_SERIAL_LEN + 1];
>
> - guard(mutex)(&steam->report_mutex);
> - ret = steam_send_report(steam, cmd, sizeof(cmd));
> + ret = steam_exchange_report(steam, cmd, sizeof(cmd), reply, sizeof(reply));
> if (ret < 0)
> return ret;
> - ret = steam_recv_report(steam, reply, sizeof(reply));
> - if (ret < 0)
> - return ret;
> - if (reply[0] != ID_GET_STRING_ATTRIBUTE || reply[1] < 1 ||
> - reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
> + if (reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
[Severity: High]
This is a pre-existing issue, but since the length from reply[1] is not
validated against the actual bytes received, does the later call to strscpy()
leak up to 31 bytes of uninitialized kernel stack memory to userspace?
drivers/hid/hid-steam.c:steam_get_serial() {
...
reply[3 + STEAM_SERIAL_LEN] = 0;
strscpy(steam->serial_no, reply + 3, reply[1]);
return ret;
}
Can a remote or malfunctioning USB device returning short or empty HID payloads
trigger this information leak?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=10
^ permalink raw reply
* Re: [PATCH 08/10] HID: steam: Improve logging and other cleanup
From: sashiko-bot @ 2026-07-02 22:36 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260702222145.1863104-8-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] Self-deadlock in rumble callbacks due to nested acquisition of `steam->report_mutex`.
- [Medium] The newly added error path in `steam_get_serial()` unconditionally prints the uninitialized stack buffer `reply`, leading to a kernel stack information leak.
Pre-existing issues:
- [High] Race condition in `steam_probe` error path with `work_connect` leads to UAF and dangling input devices.
--
commit 8cdb505892ea25a09ef1370038b435985daea2fb
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Improve logging and other cleanup
This commit adds debug logging to report sending and receiving, re-indents
input report identifiers for consistent styling, and converts several mutex
lock/unlock sequences to use the guard(mutex) scoped cleanup pattern.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 9d1fe97921011..593151709cf1d 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -481,22 +490,21 @@ static int steam_get_serial(struct steam_device *steam)
> u8 cmd[] = {ID_GET_STRING_ATTRIBUTE, sizeof(steam->serial_no), ATTRIB_STR_UNIT_SERIAL};
> u8 reply[3 + STEAM_SERIAL_LEN + 1];
>
> - mutex_lock(&steam->report_mutex);
> + guard(mutex)(&steam->report_mutex);
> ret = steam_send_report(steam, cmd, sizeof(cmd));
> if (ret < 0)
> - goto out;
> + return ret;
> ret = steam_recv_report(steam, reply, sizeof(reply));
> if (ret < 0)
> - goto out;
> + return ret;
> if (reply[0] != ID_GET_STRING_ATTRIBUTE || reply[1] < 1 ||
> reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
> - ret = -EIO;
> - goto out;
> + hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
> + (int)sizeof(reply), reply);
[Severity: Medium]
Could this unconditionally print the uninitialized reply stack buffer?
steam_get_serial() allocates reply on the stack without initializing it.
steam_recv_report() reads data into reply, but if the device returns a
short response, only part of reply is written.
In this error path, hid_err() uses %*ph to print the entire sizeof(reply)
bytes. Could this expose uninitialized kernel stack memory to dmesg if the
device malfunctions or returns a short response?
> + return -EIO;
> }
> reply[3 + STEAM_SERIAL_LEN] = 0;
> strscpy(steam->serial_no, reply + 3, reply[1]);
> -out:
> - mutex_unlock(&steam->report_mutex);
> return ret;
> }
I also noticed a couple of other issues in this file while reviewing this
patch.
drivers/hid/hid-steam.c:
static void steam_haptic_rumble_cb(struct work_struct *work)
{
struct steam_device *steam = container_of(work, struct steam_device,
rumble_work);
guard(mutex)(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
}
[Severity: High]
I noticed an issue introduced by the "Coalesce rumble packets" patch earlier in
this same series. Does this callback cause a self-deadlock?
Both steam_haptic_rumble_cb() and steam_coalesce_rumble_cb() acquire
steam->report_mutex via guard(mutex)(&steam->report_mutex) and then immediately
invoke steam_haptic_rumble().
Since steam_haptic_rumble() unconditionally calls
mutex_lock(&steam->report_mutex) and Linux mutexes are not recursive, won't
this result in a guaranteed self-deadlock of the workqueue thread the first
time a rumble effect is played?
drivers/hid/hid-steam.c:steam_probe() {
...
err_steam_unregister:
if (steam->connected)
steam_unregister(steam);
err_hw_close:
hid_hw_close(hdev);
err_hw_stop:
hid_hw_stop(hdev);
err_cancel_work:
cancel_work_sync(&steam->work_connect);
...
}
[Severity: High]
This is a pre-existing issue, but is there a race condition in this teardown
order?
If steam_probe() fails after requesting wireless connection status, the device
may have already triggered steam_do_connect_event(), which schedules
steam->work_connect.
In the error path above, steam_unregister() is called before the worker is
cancelled with cancel_work_sync(&steam->work_connect).
If the worker is executing concurrently, steam_unregister() may do nothing
because serial_no isn't set yet. The worker would then complete
steam_register() and register the input devices.
After cancel_work_sync() waits for the worker to finish, steam_probe()
returns, freeing the steam struct. Could this leave dangling input devices
that will trigger a use-after-free when accessed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=8
^ permalink raw reply
* Re: [PATCH 04/10] HID: steam: Add support for sensor events on the Steam Controller (2015)
From: sashiko-bot @ 2026-07-02 22:36 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702222145.1863104-4-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [Medium] `steam_get_attributes()` incorrectly parses the device attributes payload due to a wrong buffer offset and incorrect loop bounds, misinterpreting the report header as payload data.
Pre-existing issues:
- [High] Concurrent execution of device registration due to an unprotected race between `steam_probe` and `steam_work_connect_cb` leads to double allocation and a memory leak of input devices.
- [High] If `steam_register` fails, the device remains in the global `steam_devices` list, leading to a Use-After-Free when the memory is freed by devres.
--
commit 73cdd9e51b7293cba488d76f23eb3d6d1d4ca204
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Add support for sensor events on the Steam Controller (2015)
Sensor support was added for the Steam Deck previously, but Steam
Controller sensor events were never added. This adds that missing support,
bringing Steam Controller support much closer to feature parity with things
like SDL and Steam itself.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index add9f4f27fff9..34653ad383ac0 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -468,6 +500,38 @@ static int steam_get_serial(struct steam_device *steam)
> return ret;
> }
>
> +static int steam_get_attributes(struct steam_device *steam)
> +{
> + int ret = 0;
> + u8 cmd[] = {ID_GET_ATTRIBUTES_VALUES, 0};
> + u8 reply[64] = {};
> + u8 size;
> + int i;
> + struct steam_controller_attribute *attr;
> +
> + guard(mutex)(&steam->report_mutex);
> + ret = steam_send_report(steam, cmd, sizeof(cmd));
> + if (ret < 0)
> + return ret;
> + ret = steam_recv_report(steam, reply, sizeof(reply));
> + if (ret < 0)
> + return ret;
> + if (reply[0] != ID_GET_ATTRIBUTES_VALUES || reply[1] < 2)
> + return -EIO;
> +
> + size = min(reply[1], sizeof(reply) - 2);
> + for (i = 0; i + sizeof(*attr) <= size; i += sizeof(*attr)) {
> + attr = (struct steam_controller_attribute *)&reply[i];
[Severity: Medium]
Does this code incorrectly parse the device attributes payload?
The iteration starts at index 0, which is the message ID reply[0], instead
of index 2 where the payload actually begins.
Also, the size check relies on reply[1] but misses the +2 offset for the
header.
Will this misread the header as payload data and fail to extract attributes
like ATTRIB_CONNECTION_INTERVAL_IN_US?
> + if (attr->tag == ATTRIB_CONNECTION_INTERVAL_IN_US) {
> + steam->sensor_update_rate_us = get_unaligned_le32(&attr->value);
> + hid_dbg(steam->hdev, "Sensor update rate: %uus\n",
> + steam->sensor_update_rate_us);
> + }
> + }
> +
> + return 0;
> +}
> +
[ ... ]
> @@ -839,9 +939,6 @@ static int steam_sensors_register(struct steam_device *steam)
> struct input_dev *sensors;
> int ret;
>
> - if (!(steam->quirks & STEAM_QUIRK_DECK))
> - return 0;
> -
> rcu_read_lock();
> sensors = rcu_dereference(steam->sensors);
> rcu_read_unlock();
[Severity: High]
This is a pre-existing issue, but does steam_input_register() have a race
condition that leaks memory?
Looking at steam_input_register() (and steam_sensors_register() as well), the
check for an existing input device is not protected by a lock:
rcu_read_lock();
input = rcu_dereference(steam->input);
rcu_read_unlock();
if (input) {
dbg_hid("%s: already connected\n", __func__);
return 0;
}
input = input_allocate_device();
If a wired Steam controller sends a wireless connection event
(ID_CONTROLLER_WIRELESS) immediately after its hardware is started in
steam_probe(), steam_work_connect_cb() is queued and runs concurrently with
the rest of steam_probe().
Can both threads enter steam_register() and steam_input_register(), see a
NULL pointer, and allocate/register the input device twice?
[ ... ]
> @@ -968,6 +1077,12 @@ static int steam_register(struct steam_device *steam)
> strscpy(steam->serial_no, "XXXXXXXXXX",
> sizeof(steam->serial_no));
>
> + ret = steam_get_attributes(steam);
> + if (ret < 0)
> + hid_err(steam->hdev,
> + "%s:steam_get_attributes failed with error %d\n",
> + __func__, ret);
> +
[Severity: High]
This is a pre-existing issue, but does the error handling in steam_probe()
leak the device in the global steam_devices list?
When steam_register() is called, it adds the device to the global list:
mutex_lock(&steam_devices_lock);
if (list_empty(&steam->list))
list_add(&steam->list, &steam_devices);
mutex_unlock(&steam_devices_lock);
If a subsequent step in steam_register() fails (like input_allocate_device()
failing due to memory pressure), it returns an error but leaves the device in
the list.
Then in steam_probe():
ret = steam_register(steam);
if (ret) {
hid_err(hdev,
"%s:steam_register failed with error %d\n",
__func__, ret);
goto err_hw_close;
}
The error handling path skips the err_steam_unregister label. The driver core
frees the devres-managed steam_device, leaving a dangling pointer in the
steam_devices list.
Could subsequent iterations of this list (like when writing to the lizard_mode
module parameter) dereference the freed pointer and cause a Use-After-Free?
> hid_info(steam->hdev, "Steam Controller '%s' connected",
> steam->serial_no);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=4
^ permalink raw reply
* Re: [PATCH 09/10] HID: steam: Reject short reads
From: sashiko-bot @ 2026-07-02 22:36 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260702222145.1863104-9-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] The newly added short-read check in `steam_recv_report` contains multiple logic errors that render it ineffective, allowing short reads to bypass validation and leak uninitialized kernel stack memory, while also erroneously rejecting valid reports.
Pre-existing issues:
- [High] Self-deadlock in haptic rumble callbacks due to recursive acquisition of `steam->report_mutex`.
--
commit 27b5eb27907342525c15b39fe076131c6198f9b3
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Reject short reads
Steam Controller FEATURE reports encode the size of the message in the
message itself. Previously we were trusting that the size reported matched
the size we actually read, leading to a potential issue with short reads.
Instead, we should actually verify the length of the read.
Link: https://syzkaller.appspot.com/bug?extid=75f3f9bff8c510602d36
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 593151709cf1d..e97431bc2828c 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
> @@ -389,6 +389,12 @@ static int steam_recv_report(struct steam_device *steam,
> hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
> else
> hid_dbg(steam->hdev, "Received report %*ph\n", ret, data);
> +
> + if (ret >= 2 && data[1] > ret + 2) {
[Severity: High]
Does this logic effectively catch short reads, or does it contain an algebraic
error?
It appears data[1] > ret + 2 should be data[1] + 2 > ret. With the current
check, a device returning a short payload would bypass validation.
Additionally, since this check happens after ret is clamped to size earlier
in the function (ret = min(size, ret - 1);), won't this reject perfectly valid
large reports if the caller only requested a smaller buffer?
Also, the ret >= 2 gate allows 0 or 1 byte reads to return successfully. If
this occurs, callers like steam_get_serial() will read from an uninitialized
stack buffer.
For instance, in steam_get_serial():
drivers/hid/hid-steam.c:steam_get_serial() {
u8 reply[3 + STEAM_SERIAL_LEN + 1];
...
res = steam_recv_report(steam, reply, sizeof(reply));
if (res < 0)
return res;
reply[3 + reply[1]] = 0;
strscpy(steam->serial_no, reply + 3, sizeof(steam->serial_no));
}
If steam_recv_report() returns 0 or 1, reply[1] is uninitialized,
potentially causing out-of-bounds reads or leaking kernel stack memory into
steam->serial_no.
> + hid_err(steam->hdev, "%s: expected %u bytes, read %i\n",
> + __func__, data[1] + 2, ret);
> + return -EPROTO;
> + }
> return ret;
> }
[Severity: High]
This is a pre-existing issue, but does steam_haptic_rumble_cb() cause a
self-deadlock when triggering rumble?
This was introduced by the commit "HID: steam: Coalesce rumble packets"
earlier in the same series and persists through the end of the series.
In steam_haptic_rumble_cb():
drivers/hid/hid-steam.c:steam_haptic_rumble_cb() {
guard(mutex)(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
}
And steam_haptic_rumble() unconditionally locks the same mutex:
drivers/hid/hid-steam.c:steam_haptic_rumble() {
mutex_lock(&steam->report_mutex);
ret = steam_send_report(steam, report, sizeof(report));
mutex_unlock(&steam->report_mutex);
}
Since Linux mutexes are not recursive, won't this permanently hang the system
workqueue worker executing the rumble callbacks?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702222145.1863104-1-vi@endrift.com?part=9
^ permalink raw reply
* Re: [PATCH v6 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: sashiko-bot @ 2026-07-02 22:39 UTC (permalink / raw)
To: Marco Felsch; +Cc: devicetree, robh, linux-input, conor+dt, dmitry.torokhov
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-1-1aa50ba3bc5a@pengutronix.de>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The newly added "firmware-duplicate" sysfs error string is omitted from the sysfs ABI documentation and the corresponding selftest script.
--
commit 59f39aaf73939b5dbe553a0c6fe3c5d3c9ce3d78
Author: Marco Felsch <m.felsch@pengutronix.de>
firmware_loader: expand firmware error codes with up-to-date error
This commit adds FW_UPLOAD_ERR_DUPLICATE to allow drivers to signal
that a firmware update is not required. It also updates the error
strings in the core sysfs interface and the test firmware module.
> diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
> --- a/drivers/base/firmware_loader/sysfs_upload.c
> +++ b/drivers/base/firmware_loader/sysfs_upload.c
> @@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
> [FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
> [FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
> [FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
> + [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
[Severity: Low]
Since firmware-duplicate is now exposed to userspace, should it also
be documented in Documentation/ABI/testing/sysfs-class-firmware? The
existing documentation lists the other expected error strings but is
missing this one.
Additionally, does this require an update to the selftests? Looking at
tools/testing/selftests/firmware/fw_upload.sh, the errors list is
hardcoded:
errors="hw-error
timeout
device-busy
invalid-file-size
read-write-error
flash-wearout"
Without adding firmware-duplicate to this list, the new error injection
capability added to lib/test_firmware.c might not be exercised by the
test suite.
> };
>
> static const char *fw_upload_progress(struct device *dev,
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de?part=1
^ permalink raw reply
* Re: [PATCH v6 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: sashiko-bot @ 2026-07-02 22:43 UTC (permalink / raw)
To: Marco Felsch; +Cc: linux-input, conor+dt, dmitry.torokhov, robh, devicetree
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-4-1aa50ba3bc5a@pengutronix.de>
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [High] Custom regmap callbacks interpret 16-bit register addresses with incorrect endianness, leading to I2C transfer failures on little-endian hosts and corrupted addresses on big-endian hosts.
- [High] The firmware upload cleanup handler double-unlocks `fwupdate_lock` and permanently underflows the PM runtime usage counter.
- [High] Missing bounds checks when parsing firmware chunks lead to out-of-bounds memory reads and integer underflows.
- [High] Insufficient startup delay due to using a millisecond macro with `fsleep()`, causing a 1000x shorter sleep than intended.
- [High] Manual unregistration of the input device in `remove()` races with devres-managed IRQs and sysfs interfaces, leading to Use-After-Free.
- [High] The TH2CFG firmware update path replaces the input device dynamically without disabling the IRQ, causing a Use-After-Free race with active interrupt handlers.
- [High] Missing `reinit_completion()` allows late interrupts to cause spurious wakeups in subsequent operations.
- [Medium] Probe error paths leak the PM runtime usage counter and leave hardware regulators permanently enabled.
- [Medium] The allocated input device is leaked if requesting the threaded IRQ fails.
- [Medium] Arithmetic error calculating the payload length due to subtraction of preamble and postamble bytes without parentheses.
- [Medium] Unreachable error handling when the device reports 0 touch slots during initial probe leads to a logic flaw and dead code.
- [Medium] Dead code pointer check `if (!entry)` is used instead of checking if the usage entry is populated.
- [Medium] Insufficient bounds checking on device-supplied usage size leads to a heap out-of-bounds read in `axiom_u42_get_touchslots`.
--
A review of this patch has revealed the following issues:
commit ee908a9756d78a49fffc38a01178c9ab48ed3f33
Author: Marco Felsch <m.felsch@pengutronix.de>
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.
> diff --git a/drivers/input/touchscreen/touchnetix_axiom.c b/drivers/input/touchscreen/touchnetix_axiom.c
> --- a/drivers/input/touchscreen/touchnetix_axiom.c
> +++ b/drivers/input/touchscreen/touchnetix_axiom.c
[ ... ]
> @@ -854,16 +854,16 @@ static int axiom_u02_save_config(struct axiom_data *ts)
> msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM);
>
> error = axiom_u02_send_msg(ts, &msg, false);
> if (error)
> return error;
>
> /* Downstream axcfg.py waits for 2sec without checking U01 response */
> if (!axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
> msecs_to_jiffies(2 * MSEC_PER_SEC))) {
[Severity: High]
Does this code risk consuming a stale completion signal? The completion
variable ts->nvm_write doesn't appear to be reinitialized. If a previous
operation timed out but completed late, could wait_for_completion_timeout()
return immediately here without actually waiting? We might need a call to
reinit_completion() before starting the wait.
> @@ -876,16 +876,16 @@ static int axiom_u02_swreset(struct axiom_data *ts)
> 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 to add the additional fsleep(1sec).
> * Touchnetix said that the boot can take up to 2sec if all self tests
> * are enabled, so wait 2sec for the HELLO message.
> */
> if (!axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
> msecs_to_jiffies(2 * MSEC_PER_SEC))) {
[Severity: High]
Could we hit the same stale completion issue here for ts->boot_complete? A
late interrupt from a previous operation might cause this to return early
if reinit_completion() is missing.
[ ... ]
> @@ -998,16 +998,16 @@ static int axiom_regmap_read(void *context, const void *reg_buf, size_t reg_size
>
> 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);
[Severity: High]
Will this address casting work correctly on big-endian hosts? Since I2C
regmap sends the buffer in big-endian format by default, casting the byte
buffer directly to u16 will swap the bytes on little-endian systems. Then,
when calling cpu_to_le16(addr), a big-endian system will swap the big-endian
bytes to little-endian, completely breaking the hardware communication.
Does this code need to read the address using get_unaligned_be16() instead?
> @@ -1035,16 +1035,16 @@ static int axiom_regmap_write(void *context, const void *data, size_t count)
> 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);
[Severity: High]
Does this have the same endianness issue as axiom_regmap_read()?
[ ... ]
> @@ -1078,16 +1078,16 @@ static bool axiom_u42_get_touchslots(struct axiom_data *ts)
>
> reg = axiom_usage_baseaddr(ts, AXIOM_U42);
> 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;
> ts->num_slots = 0;
>
> for (i = 0; i < AXIOM_MAX_TOUCHSLOTS; i++) {
> if (axiom_u42_touch_enabled(ts, buf, i)) {
[Severity: Medium]
Could this loop read out of bounds? The buf buffer is sized based on bufsize
which is supplied by the device. If the device advertises a U42 usage size
smaller than the maximum required index (e.g., 4 bytes), iterating 10 times
and indexing into buf might result in a heap out-of-bounds read. Do we need
to verify that bufsize is large enough for all max touch slots?
[ ... ]
> @@ -1117,16 +1117,16 @@ 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;
> }
[Severity: Medium]
Is this pointer check effective? Since entry is initialized using the address
of an element in the array (&ts->usage_table[usage_num]), it will never
be NULL. Should this check !entry->populated instead?
[ ... ]
> @@ -1158,16 +1158,16 @@ static int axiom_u34_rev1_process_report(struct axiom_data *ts,
> * 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;
[Severity: Medium]
Does this calculation result in an incorrect length? Due to C operator
precedence, len -= A - B calculates len -= (A - B). This evaluates to
len -= (2 - 4), effectively adding 2 to the length instead of subtracting 6.
Would len -= (AXIOM_U34_REV1_PREAMBLE_BYTES + AXIOM_U34_REV1_POSTAMBLE_BYTES)
be the intended math here?
[ ... ]
> @@ -1838,16 +1838,16 @@ 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);
>
> /*
> * In FW_UPLOAD_ERR_NONE case the complete handler will release the
> * lock.
> */
> if (ret != FW_UPLOAD_ERR_NONE)
> mutex_unlock(&ts->fwupdate_lock);
[Severity: High]
Does this code risk a double-unlock of ts->fwupdate_lock? The firmware
upload core unconditionally calls the cleanup callback when prepare fails.
Since axiom_axfw_fw_cleanup() also unconditionally unlocks the mutex,
unlocking it here on the error path will cause a double-unlock panic.
[ ... ]
> @@ -1934,16 +1934,16 @@ static enum fw_upload_err axiom_axfw_fw_write(struct fw_upload *fw_upload,
> error = axiom_enter_bootloader_mode(ts);
> if (error)
> return axiom_unlock_input_return_hw_error(ts);
> }
>
> 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.
> */
> error = axiom_blp_write_chunk(ts, data, len);
> if (error) {
> /*
> * Tests showed that the bootloader mode must be exited
> * if an invalid chunk was received by the bootloader fw
> * since all following attempts to download valid chunks
> * will fail. Try do so via axiom_blp_reset() but tests
> * also showed that this may fail too. So inform the
> * user and hope that the full power-cycle helps. To get
> * the device back into a working mode where the device
> * accepts data again.
> */
> if (axiom_blp_reset(ts))
> dev_warn(dev, "Couldn't recover device, device requires power-cycle\n");
> return axiom_unlock_input_return_hw_error(ts);
> }
>
> size -= len;
[Severity: High]
Could this loop read out of bounds on the kernel heap? The chunk_len is
extracted directly from the untrusted firmware data, but there is no check
to ensure that size is large enough to contain the full chunk. If the
firmware is malformed such that len > size, size will underflow to a huge
value causing an infinite out of bounds read.
[ ... ]
> @@ -2022,16 +2022,16 @@ static void axiom_axfw_fw_handle_irq(struct axiom_data *ts)
> * because the probe() aborted due to firmware incompatibilities. Skip
> * the enable step if the IRQ is not disabled to not mess with the
> * refcount.
> */
> if (irqd_irq_disabled(data) &&
> axiom_get_runmode(ts) != AXIOM_BLP_MODE)
> axiom_enable_irq(ts);
> }
>
> static void axiom_axfw_fw_cleanup(struct fw_upload *fw_upload)
> {
> struct axiom_data *ts = fw_upload->dd_handle;
>
> mutex_unlock(&ts->fwupdate_lock);
> axiom_axfw_fw_handle_irq(ts);
> pm_runtime_mark_last_busy(ts->dev);
> pm_runtime_put_sync_autosuspend(ts->dev);
> }
[Severity: High]
Does this pm_runtime_put_sync_autosuspend() call risk an underflow? If the
firmware upload fails during the prepare callback, the core will call this
cleanup function. Since the pm_runtime_resume_and_get() is only called later
inside the write callback, dropping the reference unconditionally here will
permanently underflow the PM runtime usage counter.
[ ... ]
> @@ -2171,16 +2171,16 @@ static enum fw_upload_err __axiom_cfg_fw_prepare(struct axiom_data *ts,
> /* 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;
[Severity: High]
Is it possible for this loop to underflow the size variable if the chunk
is malformed? There doesn't appear to be a check verifying that size is
greater than or equal to chunk_len before subtracting it.
[ ... ]
> @@ -2435,16 +2435,16 @@ static enum fw_upload_err axiom_cfg_fw_write(struct fw_upload *fw_upload,
>
> /*
> * (Re)start the device with the new config. Start the device AE
> * either via u02 SW_RESET or u02 CMD_START, the behavior is the same
> * according Touchnetix.
> */
> error = axiom_u02_swreset(ts);
> if (error) {
> dev_err(dev, "Soft reset failed\n");
> goto err_unlock;
> }
>
> error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
> if (error)
> goto err_unlock;
>
> if (axiom_verify_crcs(ts))
> goto err_unlock;
>
> /* Unlock before the input device gets unregistered */
> axiom_unlock_input_device(ts);
>
> error = axiom_update_input_dev(ts);
[Severity: High]
Could this dynamic input device replacement lead to a Use-After-Free? The
driver softly resets the device and calls axiom_update_input_dev(ts) which
frees the old ts->input and allocates a new one. Unlike the AXFW update,
axiom_disable_irq(ts) is never called here. A soft-reset triggers an immediate
interrupt, which would cause axiom_u41_rev2_process_report() to execute and
access ts->input without locks while it is being freed.
[ ... ]
> @@ -2776,16 +2776,16 @@ static int axiom_register_input_dev(struct axiom_data *ts,
> input_set_abs_params(input, ABS_MT_POSITION_X, 0, AXIOM_MAX_XY, 0, 0);
> input_set_abs_params(input, ABS_MT_POSITION_Y, 0, AXIOM_MAX_XY, 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_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) {
[Severity: Medium]
Will this error path ever execute during the initial probe? When called from
probe, update_in_process is false, making the condition evaluate to false
even if ts->num_slots is 0. This skips the error block, calls
input_mt_init_slots (which returns 0 without initializing dev->mt), and
registers a potentially useless input device instead of returning an error.
[ ... ]
> @@ -2831,16 +2831,16 @@ static int axiom_register_input_dev(struct axiom_data *ts,
> * 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 (client->irq) {
> if (!ts->irq_setup_done) {
> 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 error;
> }
[Severity: Medium]
Does this error path leak the input device? It returns immediately when
devm_request_threaded_irq() fails, but it appears to miss calling
input_free_device(input) which is done in the other error paths in this
function.
[ ... ]
> @@ -2866,16 +2866,16 @@ static int axiom_power_up_device(struct axiom_data *ts)
> int error;
>
> error = regulator_bulk_enable(ts->num_supplies, ts->supplies);
> if (error) {
> dev_err(dev, "Failed to enable power supplies\n");
> return error;
> }
>
> 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;
> }
[Severity: High]
Does this use the intended sleep duration? fsleep() takes its argument in
microseconds, but it's passed AXIOM_STARTUP_TIME_MS (which is 110). This will
cause the driver to wait for 110 microseconds instead of 110 milliseconds,
which could prevent the touch controller from finishing its boot sequence
before communication is attempted.
[ ... ]
> @@ -2982,16 +2982,16 @@ static int axiom_i2c_probe(struct i2c_client *client)
> if (error)
> return error;
>
> 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);
> error = devm_pm_runtime_enable(dev);
> if (error)
> return dev_err_probe(dev, error, "Failed to enable pm-runtime\n");
>
> error = axiom_register_panel_follower(ts);
> if (error)
> return dev_err_probe(dev, error, "Failed to register panel follower\n");
[Severity: Medium]
Do these probe error paths leak the PM runtime usage counter? Returning
directly here skips calling pm_runtime_put_sync() and leaves the hardware
regulators permanently enabled since axiom_power_down_device(ts) isn't called.
[ ... ]
> @@ -3099,14 +3099,14 @@ static int axiom_i2c_probe(struct i2c_client *client)
> 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");
> }
>
> return 0;
> }
>
> static void axiom_i2c_remove(struct i2c_client *client)
> {
> struct axiom_data *ts = i2c_get_clientdata(client);
>
> axiom_unregister_input_dev(ts);
> }
[Severity: High]
Could manually unregistering the input device here race with devres-managed
resources? The threaded IRQ handler and sysfs firmware upload callbacks are
managed by devm, so they won't be disabled until after remove() finishes. If
an interrupt fires or a sysfs upload starts during unbind, it might access the
already-freed input device.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de?part=4
^ permalink raw reply
* Re: [PATCH v7 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: sashiko-bot @ 2026-07-02 22:45 UTC (permalink / raw)
To: Marco Felsch; +Cc: dmitry.torokhov, devicetree, linux-input, conor+dt, robh
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-1-0d2a550a7ee8@pengutronix.de>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The selftest script for firmware uploads does not inject and test the new 'firmware-duplicate' error.
- [Low] Missing ABI documentation for the newly added sysfs error string 'firmware-duplicate'.
--
commit b451d666b20500e69051bac63ead44572b79d708
Author: Marco Felsch <m.felsch@pengutronix.de>
firmware_loader: expand firmware error codes with up-to-date error
This commit adds FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the
firmware_loader that an update is not required. It also updates the
test_firmware module to support injecting this new error state.
> diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
> index efc33294212fd..82c4b1fedf4cd 100644
> --- a/drivers/base/firmware_loader/sysfs_upload.c
> +++ b/drivers/base/firmware_loader/sysfs_upload.c
> @@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
> [FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
> [FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
> [FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
> + [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
[Severity: Low]
Since a new error string "firmware-duplicate" is exposed to userspace,
should this also be documented in
Documentation/ABI/testing/sysfs-class-firmware?
It looks like the new string is missing from the allowed error values
documented for the sysfs interface.
> };
>
> static const char *fw_upload_progress(struct device *dev,
[ ... ]
> diff --git a/lib/test_firmware.c b/lib/test_firmware.c
> index 7459bba65444d..a822d9641eb4c 100644
> --- a/lib/test_firmware.c
> +++ b/lib/test_firmware.c
> @@ -1134,6 +1134,7 @@ static const char * const fw_upload_err_str[] = {
> [FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
> [FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
> [FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
> + [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
[Severity: Medium]
Does the test script also need to be updated to exercise this new code path?
While this properly adds injection support to the test module, the hardcoded
errors list in tools/testing/selftests/firmware/fw_upload.sh does not appear
to include "firmware-duplicate", meaning the kselftest won't actually
inject or test this new error type.
> };
>
> static void upload_err_inject_error(struct test_firmware_upload *tst,
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de?part=1
^ permalink raw reply
* Re: [PATCH v7 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: sashiko-bot @ 2026-07-02 22:48 UTC (permalink / raw)
To: Marco Felsch; +Cc: robh, linux-input, conor+dt, dmitry.torokhov, devicetree
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-4-0d2a550a7ee8@pengutronix.de>
Thank you for your contribution! Sashiko AI review found 18 potential issue(s) to consider:
- [High] Missing bounds checking during firmware chunk parsing allows malformed firmware to trigger integer underflow and out-of-bounds memory accesses.
- [High] Invalid cast of regmap buffer to u16* causes endian-dependent behavior and corrupts register addresses.
- [High] Unbalanced pm_runtime_put in firmware upload cleanup leads to negative PM usage counts.
- [High] Firmware-controlled allocation size allows out-of-bounds heap read when checking touch slots.
- [High] The driver prevents firmware recovery when the device boots in bootloader mode by improperly failing the probe.
- [High] fwupdate_lock is held across system calls violating kernel mutex semantics, and is double-unlocked on error paths.
- [High] Improper devm teardown order causes Use-After-Free of the input device by background firmware upload threads.
- [High] Use-after-free of ts->input in the threaded IRQ handler during config firmware updates and device removal.
- [High] The driver waits on completion primitives for hardware resets and saves without reinitializing them, causing future waits to falsely succeed immediately.
- [High] The driver passes stack-allocated buffers to I2C transfers, which violates DMA requirements and can cause memory corruption.
- [High] NULL pointer dereference in `axiom_remove_axfw_fwl_action` if `devm_add_action_or_reset` fails.
- [Medium] An operator precedence error in subtracting the preamble and postamble causes the calculated report payload length to be artificially inflated.
- [Medium] Probe error paths leak PM runtime references and fail to disable regulators.
- [Medium] I2C read errors are swallowed and falsely reported as success in polling functions.
- [Medium] Missing NULL check on `kasprintf` results in a crash or broken sysfs state when registering the firmware upload interfaces.
- [Medium] The driver lacks system sleep power management callbacks, causing the device to remain powered on during system suspend if not acting as a panel follower.
- [Medium] Dead code in `axiom_register_input_dev` prevents failing initialization when no touchslots are enabled.
- [Low] Typo in the sysfs `device_state` output violates the documented ABI.
--
commit 7ff2ad22a9b84958a9fb53d181dbd9af7a0e7b00
Author: Marco Felsch <m.felsch@pengutronix.de>
Input: Add TouchNetix aXiom I2C Touchscreen support
This patch adds initial support for the TouchNetix AX54A touchscreen
controller. It handles I2C input events, touch coordinate reporting, and
supports firmware and configuration updates via the fw_upload subsystem.
> diff --git a/drivers/input/touchscreen/touchnetix_axiom.c b/drivers/input/touchscreen/touchnetix_axiom.c
> --- /dev/null
> +++ b/drivers/input/touchscreen/touchnetix_axiom.c
[ ... ]
> +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_MODE: return "bootlaoder";
[Severity: Low]
Does this output "bootlaoder" instead of the documented "bootloader" state?
This typo might break userspace tools reading the device_state sysfs file.
> + default: return "unknown";
> + }
> +}
[ ... ]
> +static int axiom_u02_wait_idle(struct axiom_data *ts)
> +{
> + unsigned int reg;
> + int error, 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.
> + */
> + 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);
[Severity: Medium]
Can this swallow I2C read errors?
If regmap_raw_read fails, ret will be non-zero (an error code). The
condition evaluates to true, ending the poll successfully. The function
then incorrectly assumes commands completed successfully and returns 0.
> + if (error) {
> + dev_err(ts->dev, "Poll u02 timedout with: %#x\n", cmd);
> + return error;
> + }
> +
> + return 0;
> +}
[ ... ]
> +static int axiom_u02_swreset(struct axiom_data *ts)
> +{
> + 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;
> +
> + 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 to add the additional fsleep(1sec).
> + * Touchnetix said that the boot can take up to 2sec if all self tests
> + * are enabled, so wait 2sec for the HELLO message.
> + */
> + if (!axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
> + msecs_to_jiffies(2 * MSEC_PER_SEC))) {
[Severity: High]
Is reinit_completion needed here before waiting?
Since boot_complete and nvm_write are signaled via complete, future calls
to axiom_wait_for_completion_timeout will return immediately because
reinit_completion is never called to reset their internal state. This might
cause premature hardware communication.
> + dev_err(ts->dev, "Error swreset timedout\n");
> + error = -ETIMEDOUT;
> + }
> +
> + fsleep(USEC_PER_SEC);
> +
> + return error;
> +}
[ ... ]
> +static bool axiom_u42_get_touchslots(struct axiom_data *ts)
> +{
> + unsigned int bufsize;
> + unsigned int reg;
> + int error, i;
> +
> + bufsize = axiom_usage_size(ts, AXIOM_U42);
> + u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
> + if (!buf)
> + return false;
> +
> + reg = axiom_usage_baseaddr(ts, AXIOM_U42);
> + 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;
> + ts->num_slots = 0;
> +
> + for (i = 0; i < AXIOM_MAX_TOUCHSLOTS; i++) {
> + if (axiom_u42_touch_enabled(ts, buf, i)) {
[Severity: High]
Could this lead to an out-of-bounds heap read?
The bufsize is determined by the device via axiom_usage_size. If a
compromised or malfunctioning device reports a size smaller than 18 bytes,
the loop still iterates up to 10 slots checking fixed offsets up to index
17 in axiom_u42_touch_enabled, reading past the allocated buffer.
> + ts->enabled_slots |= BIT(i);
> + ts->num_slots++;
> + }
> + }
> +
> + return true;
> +}
[ ... ]
> +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] = { };
[Severity: High]
Can this cause memory corruption on architectures lacking hardware cache
coherence?
The 256-byte buf array is allocated on the stack and passed to
regmap_raw_read, which eventually reaches the I2C core. Stack memory is
generally not DMA-safe, violating I2C transfer requirements.
> + struct device *dev = ts->dev;
> + unsigned char report_usage;
> + u16 crc_report, crc_calc;
> + unsigned int len;
> + u8 *payload;
> + int error;
> +
> + error = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
> + if (error)
> + return error;
[ ... ]
> + report_usage = buf[1];
> + payload = &buf[AXIOM_U34_REV1_PREAMBLE_BYTES];
> + len -= AXIOM_U34_REV1_PREAMBLE_BYTES - AXIOM_U34_REV1_POSTAMBLE_BYTES;
[Severity: Medium]
Could this calculation artificially inflate the payload length?
Mathematically, subtracting without parentheses evaluates like this:
len -= (2 - 4), which results in len += 2.
Should this have parentheses around the values being subtracted?
> +
> + switch (report_usage) {
> + case AXIOM_U01:
> + case AXIOM_U41:
[ ... ]
> +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;
[Severity: High]
Could this lead to a use-after-free if the input device is unregistered?
During config firmware updates (axiom_cfg_fw_write) and module removal,
the input device is freed. If the threaded IRQ handler runs concurrently
and reads ts->input before it is nulled out, it might access a freed pointer
when calling input_mt_slot and touchscreen_report_pos.
> +
> + targets = get_unaligned_le16(&buf[AXIOM_U41_REV2_TARGETSTATUS_REG]);
[ ... ]
> +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;
> + struct i2c_msg xfer[2];
> + u16 xferlen, addr;
> + 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);
[Severity: High]
Does this cast cause endianness issues on big-endian architectures?
Casting a byte buffer directly to a u16 pointer evaluates differently
depending on the host byte order. When cpu_to_le16 is applied afterwards,
the register address sent to the device will be corrupted on big-endian
systems. The same pattern is present in axiom_regmap_write.
> + 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);
> +
> + if (!axiom_usage_supported(ts, addr))
> + return -EINVAL;
> +
> + xfer[0].addr = i2c->addr;
> + xfer[0].flags = 0;
> + xfer[0].len = sizeof(hdr);
> + xfer[0].buf = (u8 *)&hdr;
[Severity: High]
Could passing the stack-allocated hdr struct to i2c_transfer cause issues
with DMA mapping?
The I2C core expects buffers that are DMA-safe, but stack variables might
violate this on non-coherent architectures. A similar stack-buffer is passed
in axiom_regmap_write.
> +
> + xfer[1].addr = i2c->addr;
> + xfer[1].flags = I2C_M_RD;
> + xfer[1].len = val_size;
> + xfer[1].buf = val_buf;
[ ... ]
> +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);
> +
> + /*
> + * In FW_UPLOAD_ERR_NONE case the complete handler will release the
> + * lock.
> + */
> + if (ret != FW_UPLOAD_ERR_NONE)
> + mutex_unlock(&ts->fwupdate_lock);
[Severity: High]
Can this lead to a double-unlock of fwupdate_lock?
If __axiom_axfw_fw_prepare returns an error, this code unlocks the mutex.
However, the firmware upload core will subsequently call the cleanup
callback axiom_axfw_fw_cleanup, which unconditionally unlocks the mutex
again. The same issue exists in axiom_cfg_fw_prepare.
> +
> + return ret;
> +}
[ ... ]
> +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;
> + int error;
> +
> + /*
> + * According Touchnetix the IRQ pin gets reconfigured in bootloader
> + * mode. Which can cause spurious IRQs. Therefore the IRQ should be
> + * disabled on the host.
> + *
> + * See the cleanup routine for the balanced enable.
> + */
> + if (axiom_get_runmode(ts) != AXIOM_BLP_MODE)
> + axiom_disable_irq(ts);
> +
> + /* Done before cancel check due to cleanup based put */
> + error = pm_runtime_resume_and_get(ts->dev);
> + if (error)
> + return FW_UPLOAD_ERR_HW_ERROR;
[Severity: High]
Does this error path lead to an unbalanced PM runtime usage count?
If pm_runtime_resume_and_get fails, it returns an error and no reference
is acquired. The firmware upload core will then call axiom_axfw_fw_cleanup,
which unconditionally calls pm_runtime_put_sync_autosuspend, potentially
causing a refcount underflow.
> +
> + scoped_guard(mutex, &afw->lock) {
> + if (afw->cancel)
> + return FW_UPLOAD_ERR_CANCELED;
> + }
[ ... ]
> + 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.
> + */
> + error = axiom_blp_write_chunk(ts, data, len);
> + if (error) {
> + /*
> + * Tests showed that the bootloader mode must be exited
> + * if an invalid chunk was received by the bootloader fw
> + * since all following attempts to download valid chunks
> + * will fail. Try do so via axiom_blp_reset() but tests
> + * also showed that this may fail too. So inform the
> + * user and hope that the full power-cycle helps. To get
> + * the device back into a working mode where the device
> + * accepts data again.
> + */
> + if (axiom_blp_reset(ts))
> + dev_warn(dev, "Couldn't recover device, device requires power-cycle\n");
> + return axiom_unlock_input_return_hw_error(ts);
> + }
> +
> + size -= len;
> + *written += len;
> + data += len;
[Severity: High]
Is there a bounds checking issue here that could lead to integer underflow?
The length is calculated and subtracted from size without checking if len
is actually less than or equal to size. If a malformed firmware chunk
reports a large chunk_len, size will wrap around, causing out-of-bounds
accesses on the next iteration. This also occurs in axiom_cfg_fw_write
and __axiom_cfg_fw_prepare.
> + }
[ ... ]
> +static int axiom_register_fwl(struct axiom_data *ts)
> +{
> + struct device *dev = ts->dev;
> + struct fw_upload *fwl;
> + char *fw_name;
> + int error;
> +
> + 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);
[Severity: Medium]
Should there be a NULL pointer check for fw_name?
If kasprintf fails under memory pressure, it returns NULL, which is then
passed directly to firmware_upload_register.
> + kfree(fw_name);
> + if (IS_ERR(fwl))
> + return dev_err_probe(dev, PTR_ERR(fwl),
> + "Failed to register firmware upload\n");
> +
> + error = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
> + if (error)
> + return error;
> +
> + ts->fw[AXIOM_FW_AXFW].fwl = fwl;
[Severity: High]
Could this cause a NULL pointer dereference if devm_add_action_or_reset fails?
If it fails, devm_add_action_or_reset executes axiom_remove_axfw_fwl_action
immediately. However, ts->fw[AXIOM_FW_AXFW].fwl is currently NULL because
it hasn't been assigned yet, causing firmware_upload_unregister to crash.
> +
> + mutex_init(&ts->fw[AXIOM_FW_CFG].lock);
[ ... ]
> +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 error;
[ ... ]
> + axiom_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;
> + }
[Severity: Medium]
Is the -EINVAL error path dead code?
The outer if-statement requires update_in_process to be true. The inner
if-statement also checks if update_in_process is true and returns 0.
The code will never reach the dev_err and -EINVAL return.
> +
> + error = input_mt_init_slots(input, ts->num_slots, INPUT_MT_DIRECT);
[ ... ]
> +static int axiom_i2c_probe(struct i2c_client *client)
> +{
> + struct device *dev = &client->dev;
> + struct axiom_data *ts;
> + int error;
[ ... ]
> + 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);
> + error = devm_pm_runtime_enable(dev);
> + if (error)
> + return dev_err_probe(dev, error, "Failed to enable pm-runtime\n");
> +
> + error = axiom_register_panel_follower(ts);
> + if (error)
> + return dev_err_probe(dev, error, "Failed to register panel follower\n");
[Severity: Medium]
Do these error returns leak the PM runtime reference and leave the power
supplies enabled?
If devm_pm_runtime_enable or axiom_register_panel_follower fail, the
function returns directly without dropping the reference acquired by
pm_runtime_get_noresume or disabling the regulators.
> +
> + 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.
> + *
> + * The U02 usage has to be supported, since this is the usage which is
> + * used to put the device into bootloader mode!
> + *
> + * TODO: Add support for the emergency bootloader mode enter sequence.
> + * The emergency enter is done by toggling the nRESET pin 5-times in a
> + * row.
> + */
> + if (error) {
> + if (IS_ENABLED(CONFIG_FW_UPLOAD) &&
> + axiom_driver_supports_usage(ts, AXIOM_U02) &&
> + (axiom_get_runmode(ts) == AXIOM_DISCOVERY_MODE ||
> + axiom_get_runmode(ts) == AXIOM_BLP_MODE)) {
> + 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, error, "Device discovery failed\n");
> + }
[Severity: High]
Does this logic prevent firmware recovery when the device boots in bootloader
mode?
When starting in BLP mode, axiom_u31_device_discover returns -EACCES
without populating the usage table. The recovery check here evaluates
axiom_driver_supports_usage(ts, AXIOM_U02), which returns false because the
usage table is empty. This aborts the probe instead of enabling the upload
interface.
> +
> + error = axiom_register_input_dev(ts, false);
[ ... ]
> +static void axiom_i2c_remove(struct i2c_client *client)
> +{
> + struct axiom_data *ts = i2c_get_clientdata(client);
> +
> + axiom_unregister_input_dev(ts);
> +}
[Severity: High]
Could this lead to a use-after-free during unbind?
The firmware upload cleanup actions are registered via devres, meaning they
execute after axiom_i2c_remove. However, axiom_i2c_remove explicitly frees
the input device. If a background firmware upload thread is active, it will
access the freed input device.
> +
> +static int axiom_runtime_suspend(struct device *dev)
> +{
> + struct axiom_data *ts = dev_get_drvdata(dev);
> +
> + axiom_disable_irq(ts);
> + axiom_power_down_device(ts);
> +
> + return 0;
> +}
> +
> +static int axiom_runtime_resume(struct device *dev)
> +{
> + struct axiom_data *ts = dev_get_drvdata(dev);
> + int error;
> +
> + error = axiom_power_up_device(ts);
> + if (error)
> + return error;
> +
> + axiom_enable_irq(ts);
> +
> + return 0;
> +}
> +
> +static DEFINE_RUNTIME_DEV_PM_OPS(axiom_pm_ops, axiom_runtime_suspend,
> + axiom_runtime_resume, NULL);
[Severity: Medium]
Should this include system sleep power management callbacks?
DEFINE_RUNTIME_DEV_PM_OPS is used, but without SYSTEM_SLEEP_PM_OPS. If the
touchscreen is held open by userspace during a system suspend (S3), the
device will remain in RPM_ACTIVE and stay powered on.
> +
> +static const struct i2c_device_id axiom_i2c_id_table[] = {
> + { "ax54a" },
> + { },
> +};
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de?part=4
^ permalink raw reply
* Re: [PATCH v7 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Marco Felsch @ 2026-07-02 23:11 UTC (permalink / raw)
To: sashiko-reviews; +Cc: dmitry.torokhov, devicetree, linux-input, conor+dt, robh
In-Reply-To: <20260702224537.0823C1F000E9@smtp.kernel.org>
On 26-07-02, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] The selftest script for firmware uploads does not inject and test the new 'firmware-duplicate' error.
> - [Low] Missing ABI documentation for the newly added sysfs error string 'firmware-duplicate'.
> --
>
> commit b451d666b20500e69051bac63ead44572b79d708
> Author: Marco Felsch <m.felsch@pengutronix.de>
>
> firmware_loader: expand firmware error codes with up-to-date error
>
> This commit adds FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the
> firmware_loader that an update is not required. It also updates the
> test_firmware module to support injecting this new error state.
>
> > diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
> > index efc33294212fd..82c4b1fedf4cd 100644
> > --- a/drivers/base/firmware_loader/sysfs_upload.c
> > +++ b/drivers/base/firmware_loader/sysfs_upload.c
> > @@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
> > [FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
> > [FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
> > [FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
> > + [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
>
> [Severity: Low]
> Since a new error string "firmware-duplicate" is exposed to userspace,
> should this also be documented in
> Documentation/ABI/testing/sysfs-class-firmware?
I checked the firmware-invalid which is also not documented. I could add
2nd patch to fix this if this is urgent.
>
> It looks like the new string is missing from the allowed error values
> documented for the sysfs interface.
>
> > };
> >
> > static const char *fw_upload_progress(struct device *dev,
>
> [ ... ]
>
> > diff --git a/lib/test_firmware.c b/lib/test_firmware.c
> > index 7459bba65444d..a822d9641eb4c 100644
> > --- a/lib/test_firmware.c
> > +++ b/lib/test_firmware.c
> > @@ -1134,6 +1134,7 @@ static const char * const fw_upload_err_str[] = {
> > [FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
> > [FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
> > [FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
> > + [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
>
> [Severity: Medium]
> Does the test script also need to be updated to exercise this new code path?
>
> While this properly adds injection support to the test module, the hardcoded
> errors list in tools/testing/selftests/firmware/fw_upload.sh does not appear
> to include "firmware-duplicate", meaning the kselftest won't actually
> inject or test this new error type.
I'm not sure how much sense it is to have a test reporting this error.
Regards,
Marco
>
> > };
> >
> > static void upload_err_inject_error(struct test_firmware_upload *tst,
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de?part=1
>
--
#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 v2 2/4] dt-bindings: media: remove obsolete rc.txt
From: Akash Sukhavasi @ 2026-07-03 5:21 UTC (permalink / raw)
To: Rob Herring (Arm)
Cc: Jakub Kicinski, Jonathan Hunter, Paolo Abeni, linux-kernel,
linux-input, Heiner Kallweit, Eric Dumazet, devicetree,
Simon Horman, Mauro Carvalho Chehab, linux-media, linux-tegra,
David S. Miller, Jonathan Corbet, Shuah Khan, Conor Dooley,
Lee Jones, Andrew Lunn, Krzysztof Kozlowski, Russell King,
linux-doc, netdev, Vladimir Oltean, Dmitry Torokhov,
Thierry Reding
In-Reply-To: <178052520503.2279647.12293343206224505400.robh@kernel.org>
On Wed, Jun 03, 2026 at 05:20:05PM -0500, Rob Herring (Arm) wrote:
>
> On Wed, 03 Jun 2026 15:42:19 -0500, Akash Sukhavasi wrote:
> > rc.txt has been a single-line redirect to rc.yaml since
> > commit 7c31b9d67342 ("media: dt-bindings: media: Add YAML schemas for
> > the generic RC bindings"), which introduced the .yaml schema and
> > reduced the .txt to a stub in the same change. The .yaml has the same
> > filename in the same directory, making this redirect unnecessary
> > for discoverability.
> >
> > One file still references rc.txt, forcing readers through an extra
> > hop to reach the .yaml. The stub has not been touched since August
> > 2019. Update the reference in hix5hd2-ir.txt to point directly to
> > rc.yaml and remove the stub.
> >
> > Signed-off-by: Akash Sukhavasi <akash.sukhavasi@gmail.com>
> > ---
> > Documentation/devicetree/bindings/media/hix5hd2-ir.txt | 2 +-
> > Documentation/devicetree/bindings/media/rc.txt | 1 -
> > 2 files changed, 1 insertion(+), 2 deletions(-)
> >
>
> Acked-by: Rob Herring (Arm) <robh@kernel.org>
>
Friendly ping on this one. Rob's Acked-by has been on v2 since June 3
and the patch still applies cleanly.
--
Thanks,
Akash
^ permalink raw reply
* Re: [PATCH v2 4/4] dt-bindings: input: remove obsolete matrix-keymap.txt
From: Akash Sukhavasi @ 2026-07-03 5:24 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: Rob Herring (Arm), Lee Jones, Conor Dooley, David S. Miller,
Andrew Lunn, linux-tegra, linux-input, Mauro Carvalho Chehab,
Heiner Kallweit, Thierry Reding, linux-media, Krzysztof Kozlowski,
Jakub Kicinski, Vladimir Oltean, linux-doc, linux-kernel,
Eric Dumazet, Jonathan Hunter, Simon Horman, devicetree,
Paolo Abeni, netdev, Shuah Khan, Russell King, Jonathan Corbet
In-Reply-To: <aiSK6_n4ZnB_KRd8@google.com>
On Sat, Jun 06, 2026 at 02:03:43PM -0700, Dmitry Torokhov wrote:
> On Wed, Jun 03, 2026 at 05:26:38PM -0500, Rob Herring (Arm) wrote:
> >
> > On Wed, 03 Jun 2026 15:42:21 -0500, Akash Sukhavasi wrote:
> > > matrix-keymap.txt has been a single-line redirect to
> > > matrix-keymap.yaml since commit 639d6eda3b80 ("dt-bindings: input:
> > > Convert matrix-keymap to json-schema"), which introduced the .yaml
> > > schema and reduced the .txt to a stub in the same change. The .yaml
> > > has the same filename in the same directory, making this redirect
> > > unnecessary for discoverability.
> > >
> > > Eight instances across six files still reference matrix-keymap.txt,
> > > forcing readers through an extra hop to reach the .yaml. The stub has
> > > not been touched since June 2020. Update all references across input
> > > and mfd binding documentation to point directly to matrix-keymap.yaml
> > > and remove the stub.
> > >
> > > Signed-off-by: Akash Sukhavasi <akash.sukhavasi@gmail.com>
> > > ---
> > > v2:
> > > - Patch 4/4: corrected commit message (eight references in six files,
> > > not eight files), Sashiko review.
> > > https://sashiko.dev/#/patchset/20260529052246.4934-1-akash.sukhavasi@gmail.com?part=4
> > >
> > > v1: https://lore.kernel.org/all/20260529052246.4934-5-akash.sukhavasi@gmail.com/
> > > ---
> > > Documentation/devicetree/bindings/input/brcm,bcm-keypad.txt | 2 +-
> > > Documentation/devicetree/bindings/input/clps711x-keypad.txt | 2 +-
> > > Documentation/devicetree/bindings/input/matrix-keymap.txt | 1 -
> > > Documentation/devicetree/bindings/input/nvidia,tegra20-kbc.txt | 2 +-
> > > Documentation/devicetree/bindings/input/pxa27x-keypad.txt | 2 +-
> > > Documentation/devicetree/bindings/input/st-keyscan.txt | 2 +-
> > > Documentation/devicetree/bindings/mfd/tc3589x.txt | 6 +++---
> > > 7 files changed, 8 insertions(+), 9 deletions(-)
> > >
> >
> > Acked-by: Rob Herring (Arm) <robh@kernel.org>
> >
>
> Lee, could you please ack for MFD piece and I can take it through input?
>
> Thanks.
>
> --
> Dmitry
Friendly ping on this as well. Rob's Acked-by has been on v2 since
June 3, patch still applies cleanly.
--
Thanks,
Akash
^ permalink raw reply
* Re: [PATCH] Input: maplemouse - fix NULL pointer dereference in open()
From: Dmitry Torokhov @ 2026-07-03 5:46 UTC (permalink / raw)
To: Florian Fuchs; +Cc: linux-input, linux-sh, Guenter Roeck, linux-kernel
In-Reply-To: <20260628230715.2982552-1-fuchsfl@gmail.com>
Hi Florian,
On Mon, Jun 29, 2026 at 01:07:15AM +0200, Florian Fuchs wrote:
> Commit 555c765b0cc2 ("Input: mouse - drop unnecessary calls to
> input_set_drvdata") dropped the input_set_drvdata() call in probe
> because the data appeared to be unused. However, dc_mouse_open() and
> dc_mouse_close() were using maple_get_drvdata(to_maple_dev(&dev->dev)).
> This actually retrieves driver data from the input device's embedded
> struct device. After input_set_drvdata() was removed, that lookup started
> returning NULL and opening the input device dereferences mse->mdev.
>
> Restore input_set_drvdata() and convert open() and close() to use
> input_get_drvdata() so the dependency is no longer hidden.
Thank you for the patch. While the patch itself is correct I believe the
main culprit is actually 6b3480855aad ("maple: input: fix up maple mouse driver")
that introduced incorrect use of
maple_get_drvdata() and to_maple_dev() on instance of input_dev instead
of maple_device.
I added another Fixes: tag and applied.
Thanks.
--
Dmitry
^ permalink raw reply
* [PATCH v2 0/3] HID: Intel-thc-hid: Refine error recovery flow
From: Even Xu @ 2026-07-03 7:58 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
This series refines the fatal error recovery flow for the Intel THC
(Touch Host Controller) subsystem, covering both the QuickI2C and
QuickSPI drivers.
Currently, when a fatal DMA error is detected in the IRQ thread handler,
the recovery is performed inline: the interrupt handler calls
try_recover() directly, which unconfigures and reconfigures the DMA
engine.
This approach has several problems:
1. Recovery runs in the IRQ thread context, which is not ideal for
potentially slow reset operations.
2. The interrupt is re-enabled before recovery completes, risking an
interrupt storm if DMA errors persist.
3. The DMA reset logic is open-coded in each protocol driver, leading
to duplication and divergence over time.
This patch series addresses all of the above:
By adding a new thc_rxdma_reset() API to the THC core layer, QuickI2C
and QuickSPI drivers can call it respectively to refine the recovery
callback.
The synchronous try_recover() call in the IRQ thread is replaced with
schedule_work(), deferring recovery to a workqueue. Within the work
function:
- The interrupt line is disabled before any DMA manipulation.
- thc_rxdma_reset() is used instead of the open-coded sequence.
- On failure the device is marked DISABLED and the interrupt remains
off, preventing an interrupt storm.
Change log:
v2:
- Use dev_err() instead of dev_err_once() so repeated failures during
recurring recovery are not silently suppressed.
- Pause both RxDMA channels via thc_wait_for_dma_pause() before calling
thc_dma_unconfigure() to ensure the DMA engines are inactive before
clearing PRD base addresses, preventing potential IOMMU faults or
memory corruption.
- Hold a runtime PM reference inside try_recover() to prevent the
device from suspending while the work accesses hardware registers.
- Add cancel_work_sync() in quicki2c_remove() and quicki2c_shutdown()
to prevent use-after-free if recovery work is still queued at teardown.
- Only re-enable the interrupt in the IRQ thread handler when no recovery
is needed; the work function handles re-enabling after successful reset,
avoiding an interrupt storm from the uncleared hardware error state.
Even Xu (3):
HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA
HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
.../intel-quicki2c/pci-quicki2c.c | 42 ++++++++-------
.../intel-quicki2c/quicki2c-dev.h | 2 +
.../intel-quickspi/pci-quickspi.c | 46 ++++++++---------
.../intel-quickspi/quickspi-dev.h | 3 ++
.../intel-thc-hid/intel-thc/intel-thc-dma.c | 51 +++++++++++++++++++
.../intel-thc-hid/intel-thc/intel-thc-dma.h | 1 +
6 files changed, 102 insertions(+), 43 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH v2 1/3] HID: Intel-thc-hid: Intel-thc: Add API to reset read DMA
From: Even Xu @ 2026-07-03 7:58 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
In-Reply-To: <20260703075858.2780398-1-even.xu@intel.com>
Add a helper function thc_rxdma_reset() to do read DMA reset, it can be
called when fatal DMA error happens.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-thc-hid/intel-thc/intel-thc-dma.c | 51 +++++++++++++++++++
.../intel-thc-hid/intel-thc/intel-thc-dma.h | 1 +
2 files changed, 52 insertions(+)
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
index 6ee675e0a738..7ceb8aeeccd3 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
@@ -561,6 +561,57 @@ static int thc_wait_for_dma_pause(struct thc_device *dev, enum thc_dma_channel c
return 0;
}
+/**
+ * thc_rxdma_reset - Reset all read DMA engines
+ *
+ * @dev: The pointer of THC private device context
+ *
+ * This is a helper function to reset RxDMA configure. It's typically used
+ * for RxDMA recovery when fatal error happens.
+ *
+ * Return: 0 if successful or error code on failure.
+ */
+int thc_rxdma_reset(struct thc_device *dev)
+{
+ int ret;
+
+ if (mutex_lock_interruptible(&dev->thc_bus_lock))
+ return -EINTR;
+
+ ret = thc_interrupt_quiesce(dev, true);
+ if (ret) {
+ dev_err(dev->dev, "Quiesce interrupt failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA1);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA1 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA2);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA2 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_dma_unconfigure(dev);
+
+ ret = thc_dma_configure(dev);
+ if (ret) {
+ dev_err(dev->dev, "Re-config DMA failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_interrupt_quiesce(dev, false);
+
+end:
+ mutex_unlock(&dev->thc_bus_lock);
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(thc_rxdma_reset, "INTEL_THC");
+
static int read_dma_buffer(struct thc_device *dev,
struct thc_dma_configuration *read_config,
u8 prd_table_index, void *read_buff)
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
index 541d33995baf..715423453a9d 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
@@ -145,6 +145,7 @@ int thc_dma_allocate(struct thc_device *dev);
int thc_dma_configure(struct thc_device *dev);
void thc_dma_unconfigure(struct thc_device *dev);
void thc_dma_release(struct thc_device *dev);
+int thc_rxdma_reset(struct thc_device *dev);
int thc_rxdma_read(struct thc_device *dev, enum thc_dma_channel dma_channel,
void *read_buff, size_t *read_len, int *read_finished);
int thc_swdma_read(struct thc_device *dev, void *write_buff, size_t write_len,
--
2.43.0
^ permalink raw reply related
* [PATCH v2 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
From: Even Xu @ 2026-07-03 7:58 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
In-Reply-To: <20260703075858.2780398-1-even.xu@intel.com>
Refine recover flow:
1. Use workqueue to handle recover flow instead of processing in irq
handler.
2. Call thc_rxdma_reset() API to simplify the recover operation.
3. Disable interrupt during whole recover flow.
4. If recover fails, disable interrupt to avoid interrupt storm.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-quicki2c/pci-quicki2c.c | 42 ++++++++++---------
.../intel-quicki2c/quicki2c-dev.h | 2 +
2 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
index 46d3e9a01999..11e0e129b44c 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
@@ -245,28 +245,28 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qcdev: Pointer to quicki2c_device structure
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
* This function is an error handler, called when fatal error happens.
- * It try to reset touch device and re-configure THC to recovery
- * communication between touch device and THC.
- *
- * Return: 0 if successful or error code on failure
+ * It try to reset Touch Device and re-configure THC to recover
+ * transferring between Device and THC.
*/
-static int try_recover(struct quicki2c_device *qcdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quicki2c_device *qcdev = container_of(work, struct quicki2c_device, recover_work);
- thc_dma_unconfigure(qcdev->thc_hw);
+ if (pm_runtime_resume_and_get(qcdev->dev))
+ return;
- ret = thc_dma_configure(qcdev->thc_hw);
- if (ret) {
- dev_err(qcdev->dev, "Reconfig DMA failed\n");
- return ret;
- }
+ thc_interrupt_enable(qcdev->thc_hw, false);
- return 0;
+ if (thc_rxdma_reset(qcdev->thc_hw))
+ qcdev->state = QUICKI2C_DISABLED;
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
+
+ pm_runtime_put_autosuspend(qcdev->dev);
}
static int handle_input_report(struct quicki2c_device *qcdev)
@@ -343,11 +343,10 @@ static irqreturn_t quicki2c_irq_thread_handler(int irq, void *dev_id)
}
exit:
- thc_interrupt_enable(qcdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qcdev))
- qcdev->state = QUICKI2C_DISABLED;
+ schedule_work(&qcdev->recover_work);
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
pm_runtime_put_autosuspend(qcdev->dev);
@@ -386,6 +385,7 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
qcdev->ddata = ddata;
init_waitqueue_head(&qcdev->reset_ack_wq);
+ INIT_WORK(&qcdev->recover_work, try_recover);
/* THC hardware init */
qcdev->thc_hw = thc_dev_init(qcdev->dev, qcdev->mem_addr);
@@ -771,6 +771,8 @@ static void quicki2c_remove(struct pci_dev *pdev)
if (!qcdev)
return;
+ cancel_work_sync(&qcdev->recover_work);
+
quicki2c_hid_remove(qcdev);
quicki2c_dma_deinit(qcdev);
@@ -796,6 +798,8 @@ static void quicki2c_shutdown(struct pci_dev *pdev)
if (!qcdev)
return;
+ cancel_work_sync(&qcdev->recover_work);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quicki2c_dma_deinit(qcdev);
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
index 61dbdece59a1..aedf85291e60 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
@@ -222,6 +222,8 @@ struct quicki2c_device {
wait_queue_head_t reset_ack_wq;
bool reset_ack;
+ struct work_struct recover_work;
+
u32 i2c_max_frame_size_enable;
u32 i2c_max_frame_size;
u32 i2c_int_delay_enable;
--
2.43.0
^ permalink raw reply related
* [PATCH v2 3/3] HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
From: Even Xu @ 2026-07-03 7:58 UTC (permalink / raw)
To: bentiss, jikos; +Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu
In-Reply-To: <20260703075858.2780398-1-even.xu@intel.com>
Refine recover flow:
1. Use workqueue to handle recover flow instead of processing in irq
handler.
2. Call thc_rxdma_reset() API to simplify the recover operation.
3. Disable interrupt during whole recover flow.
4. If recover fails, disable interrupt to avoid interrupt storm.
Signed-off-by: Even Xu <even.xu@intel.com>
---
.../intel-quickspi/pci-quickspi.c | 46 +++++++++----------
.../intel-quickspi/quickspi-dev.h | 3 ++
2 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
index f669235f1883..bc9eead77562 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
@@ -252,34 +252,28 @@ static irqreturn_t quickspi_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qsdev: pointer to quickspi device
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
- * This function is a error handler, called when fatal error happens.
- * It try to reset Touch Device and re-configure THC to recovery
+ * This function is an error handler, called when fatal error happens.
+ * It try to reset Touch Device and re-configure THC to recover
* transferring between Device and THC.
- *
- * Return: 0 if successful or error code on failed.
*/
-static int try_recover(struct quickspi_device *qsdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quickspi_device *qsdev = container_of(work, struct quickspi_device, recover_work);
- ret = reset_tic(qsdev);
- if (ret) {
- dev_err(qsdev->dev, "Reset touch device failed, ret = %d\n", ret);
- return ret;
- }
+ if (pm_runtime_resume_and_get(qsdev->dev))
+ return;
- thc_dma_unconfigure(qsdev->thc_hw);
+ thc_interrupt_enable(qsdev->thc_hw, false);
- ret = thc_dma_configure(qsdev->thc_hw);
- if (ret) {
- dev_err(qsdev->dev, "Re-configure THC DMA failed, ret = %d\n", ret);
- return ret;
- }
+ if (thc_rxdma_reset(qsdev->thc_hw))
+ qsdev->state = QUICKSPI_DISABLED;
+ else
+ thc_interrupt_enable(qsdev->thc_hw, true);
- return 0;
+ pm_runtime_put_autosuspend(qsdev->dev);
}
/**
@@ -337,11 +331,10 @@ static irqreturn_t quickspi_irq_thread_handler(int irq, void *dev_id)
}
end:
- thc_interrupt_enable(qsdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qsdev))
- qsdev->state = QUICKSPI_DISABLED;
+ schedule_work(&qsdev->recover_work);
+ else
+ thc_interrupt_enable(qsdev->thc_hw, true);
pm_runtime_put_autosuspend(qsdev->dev);
@@ -385,6 +378,7 @@ static struct quickspi_device *quickspi_dev_init(struct pci_dev *pdev, void __io
init_waitqueue_head(&qsdev->report_desc_got_wq);
init_waitqueue_head(&qsdev->get_report_cmpl_wq);
init_waitqueue_head(&qsdev->set_report_cmpl_wq);
+ INIT_WORK(&qsdev->recover_work, try_recover);
/* thc hw init */
qsdev->thc_hw = thc_dev_init(qsdev->dev, qsdev->mem_addr);
@@ -710,6 +704,8 @@ static void quickspi_remove(struct pci_dev *pdev)
if (!qsdev)
return;
+ cancel_work_sync(&qsdev->recover_work);
+
quickspi_hid_remove(qsdev);
quickspi_dma_deinit(qsdev);
@@ -737,6 +733,8 @@ static void quickspi_shutdown(struct pci_dev *pdev)
if (!qsdev)
return;
+ cancel_work_sync(&qsdev->recover_work);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quickspi_dma_deinit(qsdev);
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
index bf5e18f5a5f4..363e589b0bde 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
@@ -8,6 +8,7 @@
#include <linux/hid-over-spi.h>
#include <linux/sizes.h>
#include <linux/wait.h>
+#include <linux/workqueue.h>
#include "quickspi-protocol.h"
@@ -173,6 +174,8 @@ struct quickspi_device {
wait_queue_head_t set_report_cmpl_wq;
bool set_report_cmpl;
+
+ struct work_struct recover_work;
};
#endif /* _QUICKSPI_DEV_H_ */
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v2 3/3] HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
From: sashiko-bot @ 2026-07-03 8:08 UTC (permalink / raw)
To: Even Xu; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260703075858.2780398-4-even.xu@intel.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] `cancel_work_sync()` is called before interrupts are safely disabled during device teardown, and is missing entirely from the probe error path, allowing the work item to execute after the device structure is freed.
- [High] Non-fatal or spurious interrupts occurring during async recovery will prematurely re-enable hardware interrupts, racing with the DMA reset and potentially causing DMA corruption.
--
commit f745eb96a11ec6734e09736d8674beb349a74ab9
Author: Even Xu <even.xu@intel.com>
HID: Intel-thc-hid: Intel-quickspi: Refine recover callback
This commit shifts the QuickSPI recover flow from the IRQ handler to a
workqueue. It relies on thc_rxdma_reset() for recovery and keeps interrupts
disabled during the process.
> diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> --- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> +++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
[ ... ]
> @@ -337,11 +331,10 @@ static irqreturn_t quickspi_irq_thread_handler(int irq, void *dev_id)
> }
>
> end:
> - thc_interrupt_enable(qsdev->thc_hw, true);
> -
> if (err_recover)
> - if (try_recover(qsdev))
> - qsdev->state = QUICKSPI_DISABLED;
> + schedule_work(&qsdev->recover_work);
> + else
> + thc_interrupt_enable(qsdev->thc_hw, true);
[Severity: High]
If try_recover() disables device interrupts and is actively resetting DMA
engines, could a spurious or non-fatal interrupt cause
quickspi_irq_thread_handler() to evaluate err_recover == 0 and unconditionally
re-enable hardware interrupts here?
If so, a subsequent real interrupt could race with the ongoing DMA reset
since thc_rxdma_read() does not appear to hold thc_bus_lock.
>
> pm_runtime_put_autosuspend(qsdev->dev);
>
[ ... ]
> @@ -710,6 +704,8 @@ static void quickspi_remove(struct pci_dev *pdev)
> if (!qsdev)
> return;
>
> + cancel_work_sync(&qsdev->recover_work);
> +
[Severity: High]
Since quickspi_dev_deinit() disables hardware interrupts later in this function,
is there a race window here?
If a fatal error interrupt occurs after cancel_work_sync() but before
hardware interrupts are disabled, the IRQ thread handler could schedule
recover_work again. The qsdev structure would then be freed, leaving a
pending work item that dereferences freed memory.
Also, should cancel_work_sync() be called in the error paths of
quickspi_probe() after the work item is initialized and interrupts are
enabled?
> quickspi_hid_remove(qsdev);
> quickspi_dma_deinit(qsdev);
>
[ ... ]
> @@ -737,6 +733,8 @@ static void quickspi_shutdown(struct pci_dev *pdev)
> if (!qsdev)
> return;
>
> + cancel_work_sync(&qsdev->recover_work);
> +
[Severity: High]
Could this have the same race window as quickspi_remove(), where a fatal
error interrupt might reschedule recover_work before quickspi_dev_deinit()
disables interrupts?
> /* Must stop DMA before reboot to avoid DMA entering into unknown state */
> quickspi_dma_deinit(qsdev);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703075858.2780398-1-even.xu@intel.com?part=3
^ permalink raw reply
* Re: [PATCH v2 2/3] HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
From: sashiko-bot @ 2026-07-03 8:10 UTC (permalink / raw)
To: Even Xu; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260703075858.2780398-3-even.xu@intel.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] Use-After-Free due to premature `cancel_work_sync` during device removal and shutdown.
Pre-existing issues:
- [High] Missing `pci_free_irq_vectors()` in probe error paths and device removal leaks IRQ resources.
- [High] The hard IRQ handler accesses hardware registers unconditionally, violating PM subsystem guidelines.
--
commit e88da9d40562f5d0be755568ae60010a7ba925df
Author: Even Xu <even.xu@intel.com>
HID: Intel-thc-hid: Intel-quicki2c: Refine recover callback
This commit refines the recover flow by offloading it to a workqueue instead
of processing it within the IRQ handler. It simplifies the operation with a
reset API and ensures interrupts are disabled during the recovery process.
> diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> index 46d3e9a019995..11e0e129b44c3 100644
> --- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> +++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
> @@ -245,28 +245,28 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
> }
[Severity: High]
This is a pre-existing issue, but does quicki2c_irq_quick_handler() access
hardware registers unconditionally?
If this device shares an INTx interrupt line, a spurious interrupt from
another device could cause quicki2c_irq_quick_handler() to execute. It then
calls thc_interrupt_enable() which writes to memory-mapped registers. If the
device is in a low-power state, this hardware access can trigger a bus fault.
Should pm_runtime_get_if_active() be used to check if the device is active
before touching the hardware registers?
[ ... ]
> @@ -771,6 +771,8 @@ static void quicki2c_remove(struct pci_dev *pdev)
> if (!qcdev)
> return;
>
> + cancel_work_sync(&qcdev->recover_work);
[Severity: High]
Can this sequence lead to a use-after-free?
cancel_work_sync() is called here, but hardware interrupts are not yet
quiesced, and the state is still active. If an interrupt fires immediately
after cancel_work_sync(), quicki2c_irq_thread_handler() can execute and call:
schedule_work(&qcdev->recover_work);
This re-enqueues the work item. After quicki2c_remove() returns, devres
frees the qcdev structure. When the scheduled work later executes, it will
dereference the freed qcdev pointer.
Should the work be cancelled after interrupts are disabled and the device
state is set to QUICKI2C_DISABLED?
> quicki2c_hid_remove(qcdev);
> quicki2c_dma_deinit(qcdev);
>
> pm_runtime_get_noresume(qcdev->dev);
>
> quicki2c_dev_deinit(qcdev);
>
> pci_clear_master(pdev);
> }
[Severity: High]
This is a pre-existing issue, but does quicki2c_remove() leak IRQ resources?
The remove path unbinds the device but doesn't appear to call
pci_free_irq_vectors(). Similarly, if quicki2c_probe() fails after
pci_alloc_irq_vectors(), the error path (disable_pci_device) does not
free the vectors.
Repeated unbind/bind operations or probe deferrals will continually leak
these vectors, which can eventually exhaust system IRQ capacity.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260703075858.2780398-1-even.xu@intel.com?part=2
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox