* [RFC] hid: hid-sjoy: race between init and usage
From: Oliver Neukum @ 2026-03-03 9:48 UTC (permalink / raw)
To: jikos, bentiss, jussi.kivilinna, linux-input; +Cc: Oliver Neukum
The driver uses an initial IO to set the device to a default
state. That initialization is currently being done after the device
node has been created. That means that the single buffer used
for output can be altered while IO is in progress.
Move the intialization before announcement to user space.
Fixes: fac733f029251 ("HID: force feedback support for SmartJoy PLUS PS2/USB adapter")
Signed-off-by: Oliver Neukum <oneukum@suse.com>
---
drivers/hid/hid-sjoy.c | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-sjoy.c b/drivers/hid/hid-sjoy.c
index bab93d71b760..963c45113204 100644
--- a/drivers/hid/hid-sjoy.c
+++ b/drivers/hid/hid-sjoy.c
@@ -91,17 +91,17 @@ static int sjoyff_init(struct hid_device *hid)
set_bit(FF_RUMBLE, dev->ffbit);
- error = input_ff_create_memless(dev, sjoyff, hid_sjoyff_play);
- if (error) {
- kfree(sjoyff);
- return error;
- }
-
sjoyff->report = report;
sjoyff->report->field[0]->value[0] = 0x01;
sjoyff->report->field[0]->value[1] = 0x00;
sjoyff->report->field[0]->value[2] = 0x00;
hid_hw_request(hid, sjoyff->report, HID_REQ_SET_REPORT);
+
+ error = input_ff_create_memless(dev, sjoyff, hid_sjoyff_play);
+ if (error) {
+ kfree(sjoyff);
+ return error;
+ }
}
hid_info(hid, "Force feedback for SmartJoy PLUS PS2/USB adapter\n");
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v3 03/18] HID: quirks: Add additional Arctis headset device IDs
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-4-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add support for additional SteelSeries Arctis headset models to the
> HID
> quirks table. This enables proper device recognition and handling
> for:
> - Arctis 7 series (7, 7P, 7X, 7 Gen2)
> - Arctis 7 Plus series (7 Plus, 7 Plus P, 7 Plus X, 7 Plus Destiny)
> - Arctis Pro
> - Arctis Nova 3 series (3, 3P, 3X)
> - Arctis Nova 5 series (5, 5X)
> - Arctis Nova 7 series (7, 7X, 7P, 7X Rev2, 7 Diablo, 7 WoW, 7 Gen2,
> 7X
> Gen2)
> - Arctis Nova Pro series (Pro, Pro X)
This seems premature, when that code is introduced, the headsets are
not supported yet.
Those definitions should be added as support for the devices is
introduced as one would expect intermediate commits in a patchset to
still be functional.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-quirks.c | 26 ++++++++++++++++++++++++++
> 1 file changed, 26 insertions(+)
>
> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
> index 17349eac5c3e..65a6f6ab30b9 100644
> --- a/drivers/hid/hid-quirks.c
> +++ b/drivers/hid/hid-quirks.c
> @@ -714,7 +714,33 @@ static const struct hid_device_id
> hid_have_special_driver[] = {
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_SRWS1) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
> #endif
> #if IS_ENABLED(CONFIG_HID_SUNPLUS)
> { HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS,
> USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
^ permalink raw reply
* Re: [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-5-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Refactor the SteelSeries driver to improve scalability and support
> the
> modern Arctis Nova headset lineup along with legacy models.
This patch is the biggest in the patchset, and could really do with
being split up.
>
> - Replace the bitmap-based quirk system with `struct
> steelseries_device_info` to encapsulate device-specific traits
> (product ID, name, capabilities, interfaces).
> - Implement asynchronous battery monitoring. Devices that support
> async
> updates (like the Nova series) now rely on interrupt events rather
> than periodic polling, reducing overhead.
> - Add support for complex multi-interface devices (e.g., Nova 7)
> where
> battery events arrive on a separate asynchronous interface.
> - Consolidate battery request and report parsing logic. New helpers
> `steelseries_send_feature_report` and
> `steelseries_send_output_report`
> simplify command dispatch.
> - Add support for over 20 new devices including the entire Arctis
> Nova
> series (3, 5, 7, Pro) and various Arctis 7/9/Pro variants.
You can't mix refactoring and new code to support new devices, it just
makes the code unreviewable.
> - Clean up naming conventions (e.g., removing `_headset_` prefix from
> general functions) and improve locking in the battery timer.
No, there are many other Steelseries devices (keyboards, mice, and the
already support steering wheel!) so removing the headset descriptor
seems like something that could cause problems in the future. It might
be a good idea to split it off into hid-steelseries-headset.c instead,
but that's a lot of work...
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 894 +++++++++++++++++++++++++-------
> --
> 1 file changed, 653 insertions(+), 241 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index d3711022bf86..d8ece8449255 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -4,37 +4,54 @@
> *
> * Copyright (c) 2013 Simon Wood
> * Copyright (c) 2023 Bastien Nocera
> + * Copyright (c) 2025 Sriman Achanta
> */
>
> -/*
> - */
> -
> +#include <linux/delay.h>
> #include <linux/device.h>
> #include <linux/hid.h>
> #include <linux/module.h>
> #include <linux/usb.h>
> #include <linux/leds.h>
> +#include <linux/power_supply.h>
> +#include <linux/workqueue.h>
> +#include <linux/spinlock.h>
>
> #include "hid-ids.h"
>
> -#define STEELSERIES_SRWS1 BIT(0)
> -#define STEELSERIES_ARCTIS_1 BIT(1)
> -#define STEELSERIES_ARCTIS_1_X BIT(2)
> -#define STEELSERIES_ARCTIS_9 BIT(3)
> +#define SS_CAP_BATTERY BIT(0)
> +
> +#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
> +
> +struct steelseries_device;
> +
> +struct steelseries_device_info {
> + unsigned long capabilities;
> + unsigned long quirks;
> +
> + u8 sync_interface;
> + u8 async_interface;
> +
> + int (*request_status)(struct hid_device *hdev);
> + void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> +};
>
> struct steelseries_device {
> struct hid_device *hdev;
> - unsigned long quirks;
> + const struct steelseries_device_info *info;
>
> - struct delayed_work battery_work;
> - spinlock_t lock;
> - bool removed;
> + bool use_async_protocol;
> +
> + struct delayed_work status_work;
>
> struct power_supply_desc battery_desc;
> struct power_supply *battery;
> - uint8_t battery_capacity;
> bool headset_connected;
> + u8 battery_capacity;
> bool battery_charging;
> +
> + spinlock_t lock;
> + bool removed;
> };
>
> #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
> @@ -341,53 +358,118 @@ static int steelseries_srws1_probe(struct
> hid_device *hdev,
> }
> #endif
>
> -#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS 3000
> +static const __u8 *steelseries_srws1_report_fixup(struct hid_device
> *hdev,
> + __u8 *rdesc, unsigned int *rsize)
> +{
> + if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
> + hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
> + return rdesc;
>
> -#define ARCTIS_1_BATTERY_RESPONSE_LEN 8
> -#define ARCTIS_9_BATTERY_RESPONSE_LEN 64
> -static const char arctis_1_battery_request[] = { 0x06, 0x12 };
> -static const char arctis_9_battery_request[] = { 0x00, 0x20 };
> + if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
> + && rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
> + hid_info(hdev, "Fixing up Steelseries SRW-S1 report
> descriptor\n");
> + *rsize = sizeof(steelseries_srws1_rdesc_fixed);
> + return steelseries_srws1_rdesc_fixed;
> + }
> + return rdesc;
> +}
>
> -static int steelseries_headset_request_battery(struct hid_device
> *hdev,
> - const char *request, size_t len)
> +/*
> + * Headset report helpers
> + */
> +
> +static int steelseries_send_report(struct hid_device *hdev, const u8
> *data,
> + int len, enum hid_report_type
> type)
> {
> - u8 *write_buf;
> + u8 *buf;
> int ret;
>
> - /* Request battery information */
> - write_buf = kmemdup(request, len, GFP_KERNEL);
> - if (!write_buf)
> + buf = kmemdup(data, len, GFP_KERNEL);
> + if (!buf)
> return -ENOMEM;
>
> - hid_dbg(hdev, "Sending battery request report");
> - ret = hid_hw_raw_request(hdev, request[0], write_buf, len,
> - HID_OUTPUT_REPORT,
> HID_REQ_SET_REPORT);
> - if (ret < (int)len) {
> - hid_err(hdev, "hid_hw_raw_request() failed with
> %d\n", ret);
> - ret = -ENODATA;
> - }
> + ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
> + HID_REQ_SET_REPORT);
> + kfree(buf);
>
> - kfree(write_buf);
> - return ret;
> + if (ret < 0)
> + return ret;
> + if (ret < len)
> + return -EIO;
> +
> + return 0;
> }
>
> -static void steelseries_headset_fetch_battery(struct hid_device
> *hdev)
> +static inline int steelseries_send_feature_report(struct hid_device
> *hdev,
> + const u8 *data,
> int len)
> {
> - int ret = 0;
> + return steelseries_send_report(hdev, data, len,
> HID_FEATURE_REPORT);
> +}
>
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
> - hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
> - ret = steelseries_headset_request_battery(hdev,
> - arctis_1_battery_request,
> sizeof(arctis_1_battery_request));
> - else if (hdev->product ==
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
> - ret = steelseries_headset_request_battery(hdev,
> - arctis_9_battery_request,
> sizeof(arctis_9_battery_request));
> +static inline int steelseries_send_output_report(struct hid_device
> *hdev,
> + const u8 *data,
> int len)
> +{
> + return steelseries_send_report(hdev, data, len,
> HID_OUTPUT_REPORT);
> +}
>
> - if (ret < 0)
> - hid_dbg(hdev,
> - "Battery query failed (err: %d)\n", ret);
> +/*
> + * Headset status request functions
> + */
> +
> +static int steelseries_arctis_1_request_status(struct hid_device
> *hdev)
> +{
> + const u8 data[] = { 0x06, 0x12 };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> +}
> +
> +static int steelseries_arctis_7_request_status(struct hid_device
> *hdev)
> +{
> + int ret;
> + const u8 connection_data[] = { 0x06, 0x14 };
> + const u8 battery_data[] = { 0x06, 0x18 };
> +
> + ret = steelseries_send_feature_report(hdev, connection_data,
> sizeof(connection_data));
> + if (ret)
> + return ret;
> +
> + msleep(10);
> +
> + return steelseries_send_feature_report(hdev, battery_data,
> sizeof(battery_data));
> +}
> +
> +static int steelseries_arctis_9_request_status(struct hid_device
> *hdev)
> +{
> + const u8 data[] = { 0x00, 0x20 };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> }
>
> +static int steelseries_arctis_nova_request_status(struct hid_device
> *hdev)
> +{
> + const u8 data[] = { 0x00, 0xb0 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +static int steelseries_arctis_nova_3p_request_status(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0xb0 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +static int steelseries_arctis_nova_pro_request_status(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0x06, 0xb0 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +/*
> + * Headset battery helpers
> + */
> +
> static int battery_capacity_to_level(int capacity)
> {
> if (capacity >= 50)
> @@ -397,19 +479,247 @@ static int battery_capacity_to_level(int
> capacity)
> return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
> }
>
> -static void steelseries_headset_battery_timer_tick(struct
> work_struct *work)
> +static u8 steelseries_map_capacity(u8 capacity, u8 min_in, u8
> max_in)
> +{
> + if (capacity >= max_in)
> + return 100;
> + if (capacity <= min_in)
> + return 0;
> + return (capacity - min_in) * 100 / (max_in - min_in);
> +}
> +
> +/*
> + * Headset status parse functions
> + */
> +
> +static void steelseries_arctis_1_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 4)
> + return;
> +
> + sd->headset_connected = (data[2] != 0x01);
> + sd->battery_capacity = data[3];
> +}
> +
> +static void steelseries_arctis_7_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 3)
> + return;
> +
> + if (data[0] == 0x06) {
> + if (data[1] == 0x14)
> + sd->headset_connected = (data[2] == 0x03);
> + else if (data[1] == 0x18)
> + sd->battery_capacity = data[2];
> + }
> +}
> +
> +static void steelseries_arctis_7_plus_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x01);
> + sd->battery_capacity =
> steelseries_map_capacity(data[2], 0x00, 0x04);
> + sd->battery_charging = (data[3] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_9_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 5)
> + return;
> +
> + if (data[0] == 0xaa) {
> + sd->headset_connected = (data[1] == 0x01);
> + sd->battery_charging = (data[4] == 0x01);
> + sd->battery_capacity =
> steelseries_map_capacity(data[3], 0x64, 0x9A);
> + }
> +}
> +
> +static void steelseries_arctis_nova_3p_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x02);
> + sd->battery_capacity =
> steelseries_map_capacity(data[3], 0x00, 0x64);
> + }
> +}
> +
> +static void steelseries_arctis_nova_5_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 5)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x02);
> + sd->battery_capacity = data[3];
> + sd->battery_charging = (data[4] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_nova_7_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = (data[1] == 0x03);
> + sd->battery_capacity =
> steelseries_map_capacity(data[2], 0x00, 0x04);
> + sd->battery_charging = (data[3] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_nova_7_gen2_parse_status(struct
> steelseries_device *sd,
> + u8 *data,
> int size)
> +{
> + if (size < 4)
> + return;
> +
> + switch (data[0]) {
> + case 0xb0:
> + sd->headset_connected = (data[1] == 0x03);
> + sd->battery_capacity = data[2];
> + sd->battery_charging = (data[3] == 0x01);
> + break;
> + case 0xb7:
> + sd->battery_capacity = data[1];
> + break;
> + case 0xb9:
> + sd->headset_connected = (data[1] == 0x03);
> + break;
> + case 0xbb:
> + sd->battery_charging = (data[1] == 0x01);
> + break;
> + }
> +}
> +
> +static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 16)
> + return;
> +
> + if (data[0] == 0x06 && data[1] == 0xb0) {
> + sd->headset_connected = (data[15] == 0x08 ||
> data[15] == 0x02);
> + sd->battery_capacity =
> steelseries_map_capacity(data[6], 0x00, 0x08);
> + sd->battery_charging = (data[15] == 0x02);
> + }
> +}
> +
> +/*
> + * Device info definitions
> + */
> +
> +static const struct steelseries_device_info srws1_info = { };
> +
> +static const struct steelseries_device_info arctis_1_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_1_request_status,
> + .parse_status = steelseries_arctis_1_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_7_info = {
> + .sync_interface = 5,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_7_request_status,
> + .parse_status = steelseries_arctis_7_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_7_plus_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_7_plus_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_9_info = {
> + .sync_interface = 0,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_9_request_status,
> + .parse_status = steelseries_arctis_9_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_3p_info = {
> + .sync_interface = 4,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_3p_request_status,
> + .parse_status = steelseries_arctis_nova_3p_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_5_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_5_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_7_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_7_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_7_gen2_info
> = {
> + .sync_interface = 3,
> + .async_interface = 5,
> + .capabilities = SS_CAP_BATTERY,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_pro_info = {
> + .sync_interface = 4,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status =
> steelseries_arctis_nova_pro_request_status,
> + .parse_status = steelseries_arctis_nova_pro_parse_status,
> +};
> +
> +/*
> + * Headset wireless status and battery infrastructure
> + */
> +
> +#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
> +
> +static void
> +steelseries_headset_set_wireless_status(struct hid_device *hdev,
> + bool connected)
> {
> - struct steelseries_device *sd = container_of(work,
> - struct steelseries_device, battery_work.work);
> - struct hid_device *hdev = sd->hdev;
> + struct usb_interface *intf;
> +
> + if (!hid_is_usb(hdev))
> + return;
>
> - steelseries_headset_fetch_battery(hdev);
> + intf = to_usb_interface(hdev->dev.parent);
> + usb_set_wireless_status(intf, connected ?
> + USB_WIRELESS_STATUS_CONNECTED :
> + USB_WIRELESS_STATUS_DISCONNECTED);
> }
>
> #define STEELSERIES_PREFIX "SteelSeries "
> #define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
>
> -static int steelseries_headset_battery_get_property(struct
> power_supply *psy,
> +static int steelseries_battery_get_property(struct power_supply
> *psy,
> enum power_supply_property psp,
> union power_supply_propval *val)
> {
> @@ -452,22 +762,7 @@ static int
> steelseries_headset_battery_get_property(struct power_supply *psy,
> return ret;
> }
>
> -static void
> -steelseries_headset_set_wireless_status(struct hid_device *hdev,
> - bool connected)
> -{
> - struct usb_interface *intf;
> -
> - if (!hid_is_usb(hdev))
> - return;
> -
> - intf = to_usb_interface(hdev->dev.parent);
> - usb_set_wireless_status(intf, connected ?
> - USB_WIRELESS_STATUS_CONNECTED :
> - USB_WIRELESS_STATUS_DISCONNECTED);
> -}
> -
> -static enum power_supply_property
> steelseries_headset_battery_props[] = {
> +static enum power_supply_property steelseries_battery_props[] = {
> POWER_SUPPLY_PROP_MODEL_NAME,
> POWER_SUPPLY_PROP_MANUFACTURER,
> POWER_SUPPLY_PROP_PRESENT,
> @@ -477,7 +772,26 @@ static enum power_supply_property
> steelseries_headset_battery_props[] = {
> POWER_SUPPLY_PROP_CAPACITY_LEVEL,
> };
>
> -static int steelseries_headset_battery_register(struct
> steelseries_device *sd)
> +/*
> + * Delayed work handlers for status polling and settings requests
> + */
> +
> +static void steelseries_status_timer_work_handler(struct work_struct
> *work)
> +{
> + struct steelseries_device *sd = container_of(
> + work, struct steelseries_device, status_work.work);
> + unsigned long flags;
> +
> + sd->info->request_status(sd->hdev);
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + if (!sd->removed && !sd->use_async_protocol)
> + schedule_delayed_work(&sd->status_work,
> + msecs_to_jiffies(STEELSERIES_HEADSET
> _STATUS_TIMEOUT_MS));
> + spin_unlock_irqrestore(&sd->lock, flags);
> +}
> +
> +static int steelseries_battery_register(struct steelseries_device
> *sd)
> {
> static atomic_t battery_no = ATOMIC_INIT(0);
> struct power_supply_config battery_cfg = { .drv_data = sd,
> };
> @@ -485,9 +799,9 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> int ret;
>
> sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
> - sd->battery_desc.properties =
> steelseries_headset_battery_props;
> - sd->battery_desc.num_properties =
> ARRAY_SIZE(steelseries_headset_battery_props);
> - sd->battery_desc.get_property =
> steelseries_headset_battery_get_property;
> + sd->battery_desc.properties = steelseries_battery_props;
> + sd->battery_desc.num_properties =
> ARRAY_SIZE(steelseries_battery_props);
> + sd->battery_desc.get_property =
> steelseries_battery_get_property;
> sd->battery_desc.use_for_apm = 0;
> n = atomic_inc_return(&battery_no) - 1;
> sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev,
> GFP_KERNEL,
> @@ -496,14 +810,16 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> return -ENOMEM;
>
> /* avoid the warning of 0% battery while waiting for the
> first info */
> - steelseries_headset_set_wireless_status(sd->hdev, false);
> sd->battery_capacity = 100;
> sd->battery_charging = false;
> + sd->headset_connected = false;
> + steelseries_headset_set_wireless_status(sd->hdev, false);
>
> sd->battery = devm_power_supply_register(&sd->hdev->dev,
> &sd->battery_desc, &battery_cfg);
> if (IS_ERR(sd->battery)) {
> ret = PTR_ERR(sd->battery);
> + sd->battery = NULL;
> hid_err(sd->hdev,
> "%s:power_supply_register failed
> with error %d\n",
> __func__, ret);
> @@ -511,68 +827,185 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> }
> power_supply_powers(sd->battery, &sd->hdev->dev);
>
> - INIT_DELAYED_WORK(&sd->battery_work,
> steelseries_headset_battery_timer_tick);
> - steelseries_headset_fetch_battery(sd->hdev);
> + return 0;
> +}
> +
> +static int steelseries_raw_event(struct hid_device *hdev,
> + struct hid_report *report, u8
> *data, int size)
> +{
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> + u8 old_capacity;
> + bool old_connected;
> + bool old_charging;
> + bool is_async_interface = false;
> +
> + if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> + return 0;
> +
> + if (!sd)
> + return 0;
> +
> + old_capacity = sd->battery_capacity;
> + old_connected = sd->headset_connected;
> + old_charging = sd->battery_charging;
> +
> + if (hid_is_usb(hdev)) {
> + struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> +
> + is_async_interface = (intf->cur_altsetting-
> >desc.bInterfaceNumber ==
> + sd->info->async_interface);
> + }
> +
> + sd->info->parse_status(sd, data, size);
> +
> + if (sd->headset_connected != old_connected) {
> + hid_dbg(hdev,
> + "Connected status changed from %sconnected
> to %sconnected\n",
> + old_connected ? "" : "not ",
> + sd->headset_connected ? "" : "not ");
> +
> + if (sd->headset_connected && !old_connected &&
> + sd->use_async_protocol && is_async_interface) {
> + schedule_delayed_work(&sd->status_work, 0);
> + }
>
> - if (sd->quirks & STEELSERIES_ARCTIS_9) {
> - /* The first fetch_battery request can remain
> unanswered in some cases */
> - schedule_delayed_work(&sd->battery_work,
> -
> msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
> + if (sd->battery) {
> + steelseries_headset_set_wireless_status(sd-
> >hdev,
> + sd-
> >headset_connected);
> + power_supply_changed(sd->battery);
> + }
> + }
> +
> + if (sd->battery_capacity != old_capacity) {
> + hid_dbg(hdev, "Battery capacity changed from %d%% to
> %d%%\n",
> + old_capacity, sd->battery_capacity);
> + if (sd->battery)
> + power_supply_changed(sd->battery);
> + }
> +
> + if (sd->battery_charging != old_charging) {
> + hid_dbg(hdev,
> + "Battery charging status changed from
> %scharging to %scharging\n",
> + old_charging ? "" : "not ",
> + sd->battery_charging ? "" : "not ");
> + if (sd->battery)
> + power_supply_changed(sd->battery);
> }
>
> return 0;
> }
>
> -static bool steelseries_is_vendor_usage_page(struct hid_device
> *hdev, uint8_t usage_page)
> +static struct hid_device *steelseries_get_sibling_hdev(struct
> hid_device *hdev,
> + int
> interface_num)
> {
> - return hdev->rdesc[0] == 0x06 &&
> - hdev->rdesc[1] == usage_page &&
> - hdev->rdesc[2] == 0xff;
> + struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> + struct usb_device *usb_dev = interface_to_usbdev(intf);
> + struct usb_interface *sibling_intf;
> + struct hid_device *sibling_hdev;
> +
> + sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
> + if (!sibling_intf)
> + return NULL;
> +
> + sibling_hdev = usb_get_intfdata(sibling_intf);
> +
> + return sibling_hdev;
> }
>
> -static int steelseries_probe(struct hid_device *hdev, const struct
> hid_device_id *id)
> +static int steelseries_probe(struct hid_device *hdev,
> + const struct hid_device_id *id)
> {
> + const struct steelseries_device_info *info =
> + (const struct steelseries_device_info *)id-
> >driver_data;
> struct steelseries_device *sd;
> + struct usb_interface *intf;
> + struct hid_device *master_hdev;
> + u8 interface_num;
> int ret;
>
> if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
> #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
> - (IS_MODULE(CONFIG_LEDS_CLASS) &&
> IS_MODULE(CONFIG_HID_STEELSERIES))
> + (IS_MODULE(CONFIG_LEDS_CLASS) &&
> IS_MODULE(CONFIG_HID_STEELSERIES))
> return steelseries_srws1_probe(hdev, id);
> #else
> return -ENODEV;
> #endif
> }
>
> - sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
> - if (!sd)
> - return -ENOMEM;
> - hid_set_drvdata(hdev, sd);
> - sd->hdev = hdev;
> - sd->quirks = id->driver_data;
> + if (hid_is_usb(hdev)) {
> + intf = to_usb_interface(hdev->dev.parent);
> + interface_num = intf->cur_altsetting-
> >desc.bInterfaceNumber;
> + } else {
> + return -ENODEV;
> + }
>
> ret = hid_parse(hdev);
> if (ret)
> return ret;
>
> - if (sd->quirks & STEELSERIES_ARCTIS_9 &&
> - !steelseries_is_vendor_usage_page(hdev,
> 0xc0))
> - return -ENODEV;
> + /* Let hid-generic handle non-vendor or unknown interfaces
> */
> + if (interface_num != info->sync_interface &&
> + (!info->async_interface || interface_num != info-
> >async_interface))
> + return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
>
> - spin_lock_init(&sd->lock);
> + if (interface_num == info->sync_interface) {
> + sd = devm_kzalloc(&hdev->dev, sizeof(*sd),
> GFP_KERNEL);
> + if (!sd)
> + return -ENOMEM;
>
> - ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> - if (ret)
> - return ret;
> + sd->hdev = hdev;
> + sd->info = info;
> + spin_lock_init(&sd->lock);
>
> - ret = hid_hw_open(hdev);
> - if (ret)
> - return ret;
> + hid_set_drvdata(hdev, sd);
>
> - if (steelseries_headset_battery_register(sd) < 0)
> - hid_err(sd->hdev,
> - "Failed to register battery for headset\n");
> + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> + if (ret)
> + return ret;
> +
> + ret = hid_hw_open(hdev);
> + if (ret)
> + goto err_stop;
> +
> + sd->use_async_protocol = !(info->quirks &
> SS_QUIRK_STATUS_SYNC_POLL);
> +
> + if (info->capabilities & SS_CAP_BATTERY) {
> + ret = steelseries_battery_register(sd);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> + }
> +
> + INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> + schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
> +
> + return 0;
> + }
> +
> + if (info->async_interface && interface_num == info-
> >async_interface) {
> + master_hdev = steelseries_get_sibling_hdev(hdev,
> info->sync_interface);
>
> + if (!master_hdev || !hid_get_drvdata(master_hdev))
> + return -EPROBE_DEFER;
> +
> + sd = hid_get_drvdata(master_hdev);
> + hid_set_drvdata(hdev, sd);
> +
> + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> + if (ret)
> + return ret;
> +
> + ret = hid_hw_open(hdev);
> + if (ret) {
> + hid_hw_stop(hdev);
> + return ret;
> + }
> + return 0;
> + }
> +
> + return -ENODEV;
> +
> +err_stop:
> + hid_hw_stop(hdev);
> return ret;
> }
>
> @@ -580,166 +1013,144 @@ static void steelseries_remove(struct
> hid_device *hdev)
> {
> struct steelseries_device *sd;
> unsigned long flags;
> + struct usb_interface *intf;
> + u8 interface_num;
>
> if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
> #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
> - (IS_MODULE(CONFIG_LEDS_CLASS) &&
> IS_MODULE(CONFIG_HID_STEELSERIES))
> + (IS_MODULE(CONFIG_LEDS_CLASS) &&
> IS_MODULE(CONFIG_HID_STEELSERIES))
> hid_hw_stop(hdev);
> #endif
> return;
> }
>
> - sd = hid_get_drvdata(hdev);
> -
> - spin_lock_irqsave(&sd->lock, flags);
> - sd->removed = true;
> - spin_unlock_irqrestore(&sd->lock, flags);
> -
> - cancel_delayed_work_sync(&sd->battery_work);
> -
> - hid_hw_close(hdev);
> - hid_hw_stop(hdev);
> -}
> -
> -static const __u8 *steelseries_srws1_report_fixup(struct hid_device
> *hdev,
> - __u8 *rdesc, unsigned int *rsize)
> -{
> - if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
> - hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
> - return rdesc;
> -
> - if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
> - && rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
> - hid_info(hdev, "Fixing up Steelseries SRW-S1 report
> descriptor\n");
> - *rsize = sizeof(steelseries_srws1_rdesc_fixed);
> - return steelseries_srws1_rdesc_fixed;
> + if (hid_is_usb(hdev)) {
> + intf = to_usb_interface(hdev->dev.parent);
> + interface_num = intf->cur_altsetting-
> >desc.bInterfaceNumber;
> + } else {
> + return;
> }
> - return rdesc;
> -}
>
> -static uint8_t steelseries_headset_map_capacity(uint8_t capacity,
> uint8_t min_in, uint8_t max_in)
> -{
> - if (capacity >= max_in)
> - return 100;
> - if (capacity <= min_in)
> - return 0;
> - return (capacity - min_in) * 100 / (max_in - min_in);
> -}
> -
> -static int steelseries_headset_raw_event(struct hid_device *hdev,
> - struct hid_report *report,
> u8 *read_buf,
> - int size)
> -{
> - struct steelseries_device *sd = hid_get_drvdata(hdev);
> - int capacity = sd->battery_capacity;
> - bool connected = sd->headset_connected;
> - bool charging = sd->battery_charging;
> - unsigned long flags;
> -
> - /* Not a headset */
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> - return 0;
> + sd = hid_get_drvdata(hdev);
>
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
> - hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
> - hid_dbg(sd->hdev,
> - "Parsing raw event for Arctis 1 headset
> (%*ph)\n", size, read_buf);
> - if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
> - memcmp(read_buf, arctis_1_battery_request,
> sizeof(arctis_1_battery_request))) {
> - if (!delayed_work_pending(&sd-
> >battery_work))
> - goto request_battery;
> - return 0;
> - }
> - if (read_buf[2] == 0x01) {
> - connected = false;
> - capacity = 100;
> - } else {
> - connected = true;
> - capacity = read_buf[3];
> - }
> + if (!sd) {
> + hid_hw_stop(hdev);
> + return;
> }
>
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
> - hid_dbg(sd->hdev,
> - "Parsing raw event for Arctis 9 headset
> (%*ph)\n", size, read_buf);
> - if (size < ARCTIS_9_BATTERY_RESPONSE_LEN) {
> - if (!delayed_work_pending(&sd-
> >battery_work))
> - goto request_battery;
> - return 0;
> - }
> + if (interface_num == sd->info->sync_interface) {
> + if (sd->info->async_interface) {
> + struct hid_device *sibling;
>
> - if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
> - connected = true;
> - charging = read_buf[4] == 0x01;
> -
> - /*
> - * Found no official documentation about min
> and max.
> - * Values defined by testing.
> - */
> - capacity =
> steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
> - } else {
> - /*
> - * Device is off and sends the last known
> status read_buf[1] == 0x03 or
> - * there is no known status of the device
> read_buf[0] == 0x55
> - */
> - connected = false;
> - charging = false;
> + sibling = steelseries_get_sibling_hdev(hdev,
> + sd-
> >info->async_interface);
> + if (sibling)
> + hid_set_drvdata(sibling, NULL);
> }
> - }
>
> - if (connected != sd->headset_connected) {
> - hid_dbg(sd->hdev,
> - "Connected status changed from %sconnected
> to %sconnected\n",
> - sd->headset_connected ? "" : "not ",
> - connected ? "" : "not ");
> - sd->headset_connected = connected;
> - steelseries_headset_set_wireless_status(hdev,
> connected);
> - }
> + spin_lock_irqsave(&sd->lock, flags);
> + sd->removed = true;
> + spin_unlock_irqrestore(&sd->lock, flags);
>
> - if (capacity != sd->battery_capacity) {
> - hid_dbg(sd->hdev,
> - "Battery capacity changed from %d%% to
> %d%%\n",
> - sd->battery_capacity, capacity);
> - sd->battery_capacity = capacity;
> - power_supply_changed(sd->battery);
> - }
> -
> - if (charging != sd->battery_charging) {
> - hid_dbg(sd->hdev,
> - "Battery charging status changed from
> %scharging to %scharging\n",
> - sd->battery_charging ? "" : "not ",
> - charging ? "" : "not ");
> - sd->battery_charging = charging;
> - power_supply_changed(sd->battery);
> + cancel_delayed_work_sync(&sd->status_work);
> }
>
> -request_battery:
> - spin_lock_irqsave(&sd->lock, flags);
> - if (!sd->removed)
> - schedule_delayed_work(&sd->battery_work,
> -
> msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
> - spin_unlock_irqrestore(&sd->lock, flags);
> -
> - return 0;
> + hid_hw_close(hdev);
> + hid_hw_stop(hdev);
> }
>
> static const struct hid_device_id steelseries_devices[] = {
> - { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_SRWS1),
> - .driver_data = STEELSERIES_SRWS1 },
> -
> - { /* SteelSeries Arctis 1 Wireless */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
> - .driver_data = STEELSERIES_ARCTIS_1 },
> -
> - { /* SteelSeries Arctis 1 Wireless for XBox */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
> - .driver_data = STEELSERIES_ARCTIS_1_X },
> -
> - { /* SteelSeries Arctis 9 Wireless for XBox */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
> - .driver_data = STEELSERIES_ARCTIS_9 },
> -
> - { }
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_SRWS1),
> + .driver_data = (unsigned long)&srws1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
> + .driver_data = (unsigned long)&arctis_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
> + .driver_data = (unsigned long)&arctis_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7),
> + .driver_data = (unsigned long)&arctis_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P),
> + .driver_data = (unsigned long)&arctis_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X),
> + .driver_data = (unsigned long)&arctis_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2),
> + .driver_data = (unsigned long)&arctis_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS),
> + .driver_data = (unsigned long)&arctis_7_plus_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P),
> + .driver_data = (unsigned long)&arctis_7_plus_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X),
> + .driver_data = (unsigned long)&arctis_7_plus_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY),
> + .driver_data = (unsigned long)&arctis_7_plus_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
> + .driver_data = (unsigned long)&arctis_9_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P),
> + .driver_data = (unsigned long)&arctis_nova_3p_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X),
> + .driver_data = (unsigned long)&arctis_nova_3p_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5),
> + .driver_data = (unsigned long)&arctis_nova_5_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
> + .driver_data = (unsigned long)&arctis_nova_5_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO),
> + .driver_data = (unsigned long)&arctis_nova_pro_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X),
> + .driver_data = (unsigned long)&arctis_nova_pro_info },
> + {}
> };
> MODULE_DEVICE_TABLE(hid, steelseries_devices);
>
> @@ -749,7 +1160,7 @@ static struct hid_driver steelseries_driver = {
> .probe = steelseries_probe,
> .remove = steelseries_remove,
> .report_fixup = steelseries_srws1_report_fixup,
> - .raw_event = steelseries_headset_raw_event,
> + .raw_event = steelseries_raw_event,
> };
>
> module_hid_driver(steelseries_driver);
> @@ -758,3 +1169,4 @@ MODULE_LICENSE("GPL");
> MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
> MODULE_AUTHOR("Simon Wood <simon@mungewell.org>");
> MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
> +MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
^ permalink raw reply
* Re: [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-2-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> @@ -97,7 +98,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] =
> {
> 0x29, 0x11, /* Usage Maximum (11h), */
> 0x95, 0x11, /* Report Count (17), */
> 0x81, 0x02, /* Input (Variable), */
> - /* ---- Dial patch starts here ---- */
> + /* ---- Dial patch starts here ---- */
> 0x05, 0x01, /* Usage Page (Desktop), */
> 0x09, 0x33, /* Usage (RX), */
> 0x75, 0x04, /* Report Size (4), */
> @@ -110,7 +111,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[]
> = {
> 0x95, 0x01, /* Report Count (1), */
> 0x25, 0x03, /* Logical Maximum (3), */
> 0x81, 0x02, /* Input (Variable), */
> - /* ---- Dial patch ends here ---- */
> + /* ---- Dial patch ends here ---- */
> 0x06, 0x00, 0xFF, /* Usage Page (FF00h), */
> 0x09, 0x01, /* Usage (01h), */
> 0x75, 0x04, /* Changed Report Size (4), */
Unrelated whitespace changes
^ permalink raw reply
* Re: [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-11-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Some headset settings (sidetone level, mic volume, etc.) are not
> reported spontaneously but must be explicitly requested from the
> device.
> Introduce a separate delayed work item (settings_work) for fetching
> these persistent settings, independent of the existing status work.
>
> Settings are requested once at probe time and again whenever the
> headset
> reconnects after being disconnected. Device info structs gain
> request_settings and parse_settings hooks for model-specific
> implementations. The SS_CAP_EXTERNAL_CONFIG capability flag marks
> devices whose writable controls can also be changed from the headset
> hardware directly; writable ALSA controls on such devices will be
> marked
> volatile.
The settings polling work and the addition of SS_CAP_EXTERNAL_CONFIG
for some headsets should probably be made in separate commits.
>
> The initial implementation adds the Arctis Nova 7 Gen2 audio settings
> request (0x00, 0x20).
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 37
> ++++++++++++++++++++++++++++++++++-
> 1 file changed, 36 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 8c6116d02f19..f2423c350154 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -26,6 +26,7 @@
> #define SS_CAP_MIC_MUTE BIT(2)
> #define SS_CAP_BT_ENABLED BIT(3)
> #define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
> +#define SS_CAP_EXTERNAL_CONFIG BIT(5)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -40,6 +41,9 @@ struct steelseries_device_info {
>
> int (*request_status)(struct hid_device *hdev);
> void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> +
> + int (*request_settings)(struct hid_device *hdev);
> + void (*parse_settings)(struct steelseries_device *sd, u8
> *data, int size);
> };
>
> struct steelseries_device {
> @@ -49,6 +53,7 @@ struct steelseries_device {
> bool use_async_protocol;
>
> struct delayed_work status_work;
> + struct delayed_work settings_work;
>
> struct power_supply_desc battery_desc;
> struct power_supply *battery;
> @@ -690,6 +695,14 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> }
> }
>
> +static int steelseries_arctis_nova_7_gen2_request_settings(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0x00, 0x20 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +
> static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> u8 *data, int
> size)
> {
> @@ -791,9 +804,11 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> .sync_interface = 3,
> .async_interface = 5,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> - SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_EXTERNAL_CONFIG,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> + .request_settings =
> steelseries_arctis_nova_7_gen2_request_settings,
> };
>
> static const struct steelseries_device_info arctis_nova_pro_info = {
> @@ -901,6 +916,15 @@ static void
> steelseries_status_timer_work_handler(struct work_struct *work)
> spin_unlock_irqrestore(&sd->lock, flags);
> }
>
> +static void steelseries_settings_work_handler(struct work_struct
> *work)
> +{
> + struct steelseries_device *sd = container_of(
> + work, struct steelseries_device,
> settings_work.work);
> +
> + if (sd->info->request_settings)
> + sd->info->request_settings(sd->hdev);
> +}
> +
> static int steelseries_battery_register(struct steelseries_device
> *sd)
> {
> static atomic_t battery_no = ATOMIC_INIT(0);
> @@ -1185,6 +1209,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
>
> sd->info->parse_status(sd, data, size);
>
> + if (sd->info->parse_settings)
> + sd->info->parse_settings(sd, data, size);
> +
> if (sd->headset_connected != old_connected) {
> hid_dbg(hdev,
> "Connected status changed from %sconnected
> to %sconnected\n",
> @@ -1194,6 +1221,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> if (sd->headset_connected && !old_connected &&
> sd->use_async_protocol && is_async_interface) {
> schedule_delayed_work(&sd->status_work, 0);
> + if (sd->info->request_settings)
> + schedule_delayed_work(&sd-
> >settings_work,
> +
> msecs_to_jiffies(10));
> }
>
> if (sd->battery) {
> @@ -1329,7 +1359,11 @@ static int steelseries_probe(struct hid_device
> *hdev,
> #endif
>
> INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> + INIT_DELAYED_WORK(&sd->settings_work,
> steelseries_settings_work_handler);
> +
> schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
> + if (info->request_settings)
> + schedule_delayed_work(&sd->settings_work,
> msecs_to_jiffies(200));
>
> return 0;
> }
> @@ -1415,6 +1449,7 @@ static void steelseries_remove(struct
> hid_device *hdev)
> spin_unlock_irqrestore(&sd->lock, flags);
>
> cancel_delayed_work_sync(&sd->status_work);
> + cancel_delayed_work_sync(&sd->settings_work);
> }
>
> hid_hw_close(hdev);
^ permalink raw reply
* Re: [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-7-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Register an ALSA sound card for each supported Arctis headset to
> expose
> headset-specific audio controls to userspace. The card is created in
> steelseries_snd_register() and freed in steelseries_snd_unregister(),
> both guarded by a module compatibility check so that registration
> only
> occurs when both SND and HID_STEELSERIES are built-in or are both
> modules, avoiding a dependency mismatch.
This seems pretty weird, are there examples of that pattern somewhere
else in the kernel source tree?
As we're adding new sound devices, it would probably be nice to CC: the
linux-sound@ mailing-list for their opinion.
>
> The Kconfig entry is updated to add SND as a dependency. Subsequent
> commits build on this infrastructure to register mixer controls.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/Kconfig | 2 +-
> drivers/hid/hid-steelseries.c | 52
> +++++++++++++++++++++++++++++++++++
> 2 files changed, 53 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index 29f05d4b7e30..fcdb5406159a 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -1139,7 +1139,7 @@ config STEAM_FF
>
> config HID_STEELSERIES
> tristate "Steelseries devices support"
> - depends on USB_HID
> + depends on USB_HID && SND
> help
> Support for Steelseries SRW-S1 steering wheel, and the
> Steelseries
> Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis
> 9,
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index d8ece8449255..b7f932cde98d 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -16,6 +16,7 @@
> #include <linux/power_supply.h>
> #include <linux/workqueue.h>
> #include <linux/spinlock.h>
> +#include <sound/core.h>
>
> #include "hid-ids.h"
>
> @@ -50,6 +51,8 @@ struct steelseries_device {
> u8 battery_capacity;
> bool battery_charging;
>
> + struct snd_card *card;
> +
> spinlock_t lock;
> bool removed;
> };
> @@ -830,6 +833,43 @@ static int steelseries_battery_register(struct
> steelseries_device *sd)
> return 0;
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> +
> +static int steelseries_snd_register(struct steelseries_device *sd)
> +{
> + struct hid_device *hdev = sd->hdev;
> + int ret;
> +
> + ret = snd_card_new(&hdev->dev, -1, "SteelSeries",
> THIS_MODULE,
> + 0, &sd->card);
> + if (ret < 0)
> + return ret;
> +
> + sd->card->private_data = sd;
> + strscpy(sd->card->driver, "SteelSeries");
> + strscpy(sd->card->shortname, hdev->name);
> + snprintf(sd->card->longname, sizeof(sd->card->longname),
> + "%s at USB %s", hdev->name, dev_name(&hdev->dev));
> +
> + ret = snd_card_register(sd->card);
> + if (ret < 0) {
> + snd_card_free(sd->card);
> + sd->card = NULL;
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void steelseries_snd_unregister(struct steelseries_device
> *sd)
> +{
> + if (sd->card)
> + snd_card_free(sd->card);
> +}
> +
> +#endif
> +
> static int steelseries_raw_event(struct hid_device *hdev,
> struct hid_report *report, u8
> *data, int size)
> {
> @@ -975,6 +1015,13 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> + ret = steelseries_snd_register(sd);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to register sound
> card: %d\n", ret);
> +#endif
> +
> INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
>
> @@ -1048,6 +1095,11 @@ static void steelseries_remove(struct
> hid_device *hdev)
> hid_set_drvdata(sibling, NULL);
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> + steelseries_snd_unregister(sd);
> +#endif
> +
> spin_lock_irqsave(&sd->lock, flags);
> sd->removed = true;
> spin_unlock_irqrestore(&sd->lock, flags);
^ permalink raw reply
* Re: [PATCH v3 18/18] HID: steelseries: Document sysfs ABI
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-19-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add Documentation/ABI/testing/sysfs-driver-hid-steelseries
> documenting
> the sysfs attributes and LED class device exposed by the driver:
>
> - bt_enabled, bt_device_connected: read-only Bluetooth radio state
> - inactive_time: read/write auto-shutoff timer in minutes
> - bt_auto_enable: read/write Bluetooth radio power-on behavior
> - <dev>::micmute/brightness: mic mute LED brightness via LED class
This should probably be documented in the commit that introduces the
new attribute, rather than all of them being updated at once.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> .../ABI/testing/sysfs-driver-hid-steelseries | 87
> +++++++++++++++++++
> 1 file changed, 87 insertions(+)
> create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-
> steelseries
>
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> new file mode 100644
> index 000000000000..7b8d29282ed6
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> @@ -0,0 +1,87 @@
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_enabled
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RO) Whether the Bluetooth radio on the headset is
> currently
> + enabled.
> +
> + * 0 = Bluetooth radio off
> + * 1 = Bluetooth radio on
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro
> Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_device_con
> nected
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RO) Whether a Bluetooth device is currently
> connected to
> + the headset.
> +
> + * 0 = no Bluetooth device connected
> + * 1 = Bluetooth device connected
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro
> Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/inactive_time
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Auto-shutoff timer for the headset, in minutes.
> A
> + value of 0 disables the timer. The maximum accepted
> value
> + is device-specific.
> +
> + The encoding sent to the firmware varies by device
> family:
> + the Arctis 9 converts the value to seconds, the Nova
> 3P
> + rounds down to its nearest supported discrete step,
> and the
> + Nova Pro maps to six firmware-defined level indices.
> For all
> + other devices the value is sent in minutes directly.
> +
> + Reading the attribute returns the last value
> reported by the
> + firmware. Writing immediately sends the new timeout
> to the
> + device.
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis 1 Wireless, Arctis 7, Arctis
> 7+,
> + Arctis 9, Arctis Nova 3P, Arctis Nova 5, Arctis Nova
> 5X,
> + Arctis Nova 7, Arctis Nova 7P, Arctis Nova 7 Gen2,
> + Arctis Nova Pro Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_auto_enabl
> e
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Whether the headset automatically enables its
> + Bluetooth radio on power-on.
> +
> + * 0 = Bluetooth radio stays off at power-on
> + * 1 = Bluetooth radio activates automatically at
> power-on
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7, Arctis Nova 7P,
> + Arctis Nova 7 Gen2
> +
> +What: /sys/class/leds/<dev>::micmute/brightness
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Brightness of the microphone mute status LED.
> + <dev> is the HID device node name (e.g.
> + 0003:1038:12AE.0001).
> +
> + * 0 = off
> + * 1 = low
> + * 2 = medium
> + * 3 = high
> +
> + Supported on: Arctis Nova 5, Arctis Nova 5X, Arctis
> Nova 7,
> + Arctis Nova 7P, Arctis Nova 7 Gen2
^ permalink raw reply
* Re: [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-16-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Expose the headset auto-shutoff timer as a read/write sysfs attribute
> (inactive_time), in minutes. Writing the attribute immediately sends
> the
> new value to the device; reading it returns the last value reported
> by
> the firmware.
Question to you and the HID maintainers:
Is there no existing sysfs definition that could match this description
and could be reused instead of needing a new one?
>
> The wire encoding differs per family:
> - Arctis 1: HID feature report 0x06/0x53 with the value in minutes
> - Arctis 7: HID feature report 0x06/0x51; split into its own write
> function as the command byte differs from the Arctis 1
> - Arctis 9: converts minutes to seconds in a big-endian u16
> - Nova 3P: rounds down to the nearest value in a discrete set
> {0,1,5,10,15,30,45,60,75,90} before sending command 0xa3
> - Nova 5/7: output report with command 0xa3, no rounding required
> - Nova Pro: maps minutes to six firmware-defined level indices via
> command 0xc1
>
> The inactive_time_max field is added to the device info struct to
> enforce the per-device maximum at write time.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 183 +++++++++++++++++++++++++++++++-
> --
> 1 file changed, 167 insertions(+), 16 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index bb9abbb0b6f8..f076a0ef8af1 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -31,6 +31,7 @@
> #define SS_CAP_MIC_VOLUME BIT(7)
> #define SS_CAP_VOLUME_LIMITER BIT(8)
> #define SS_CAP_BT_CALL_DUCKING BIT(9)
> +#define SS_CAP_INACTIVE_TIME BIT(10)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -38,6 +39,7 @@
> #define SS_SETTING_MIC_VOLUME 1
> #define SS_SETTING_VOLUME_LIMITER 2
> #define SS_SETTING_BT_CALL_DUCKING 3
> +#define SS_SETTING_INACTIVE_TIME 4
>
> struct steelseries_device;
>
> @@ -51,6 +53,7 @@ struct steelseries_device_info {
> u8 sidetone_max;
> u8 mic_volume_min;
> u8 mic_volume_max;
> + u8 inactive_time_max;
>
> int (*request_status)(struct hid_device *hdev);
> void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> @@ -93,6 +96,7 @@ struct steelseries_device {
>
> bool bt_enabled;
> bool bt_device_connected;
> + u8 inactive_timeout;
>
> spinlock_t lock;
> bool removed;
> @@ -476,6 +480,37 @@ static int
> steelseries_arctis_1_write_setting(struct hid_device *hdev,
> return steelseries_send_feature_report(hdev,
> data,
>
> sizeof(data));
> }
> + case SS_SETTING_INACTIVE_TIME: {
> + const u8 data[] = { 0x06, 0x53, value };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> + default:
> + return -EINVAL;
> + }
> +}
> +
> +static int steelseries_arctis_7_write_setting(struct hid_device
> *hdev,
> + u8 setting, u8 value)
> +{
> + switch (setting) {
> + case SS_SETTING_SIDETONE:
> + if (value == 0) {
> + const u8 data[] = { 0x06, 0x35 };
> +
> + return steelseries_send_feature_report(hdev,
> data,
> +
> sizeof(data));
> + } else {
> + const u8 data[] = { 0x06, 0x35, 0x01, 0x00,
> value };
> +
> + return steelseries_send_feature_report(hdev,
> data,
> +
> sizeof(data));
> + }
> + case SS_SETTING_INACTIVE_TIME: {
> + const u8 data[] = { 0x06, 0x51, value };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> default:
> return -EINVAL;
> }
> @@ -490,11 +525,30 @@ static int
> steelseries_arctis_9_write_setting(struct hid_device *hdev,
>
> return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> }
> + case SS_SETTING_INACTIVE_TIME: {
> + u16 seconds = (u16)value * 60;
> + const u8 data[] = { 0x04, 0x00, seconds >> 8,
> seconds & 0xff };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> default:
> return -EINVAL;
> }
> }
>
> +static u8 steelseries_arctis_nova_3p_round_inactive_time(u8 minutes)
> +{
> + static const u8 supported[] = { 0, 1, 5, 10, 15, 30, 45, 60,
> 75, 90 };
> + int i;
> +
> + for (i = ARRAY_SIZE(supported) - 1; i > 0; i--) {
> + if (minutes >= supported[i])
> + return supported[i];
> + }
> +
> + return 0;
> +}
> +
> static int steelseries_arctis_nova_3p_write_setting(struct
> hid_device *hdev,
> u8 setting, u8
> value)
> {
> @@ -510,6 +564,10 @@ static int
> steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
> case SS_SETTING_MIC_VOLUME:
> cmd = 0x37;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + value =
> steelseries_arctis_nova_3p_round_inactive_time(value);
> + break;
> default:
> return -EINVAL;
> }
> @@ -542,6 +600,9 @@ static int
> steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
> case SS_SETTING_VOLUME_LIMITER:
> cmd = 0x27;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -580,6 +641,9 @@ static int
> steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
> case SS_SETTING_BT_CALL_DUCKING:
> cmd = 0xb3;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -612,6 +676,24 @@ static int
> steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
> case SS_SETTING_MIC_VOLUME:
> cmd = 0x37;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xc1;
> + /* Map minutes to firmware level */
> + if (value >= 45)
> + value = 6; /* 60 min */
> + else if (value >= 23)
> + value = 5; /* 30 min */
> + else if (value >= 13)
> + value = 4; /* 15 min */
> + else if (value >= 8)
> + value = 3; /* 10 min */
> + else if (value >= 3)
> + value = 2; /* 5 min */
> + else if (value > 0)
> + value = 1; /* 1 min */
> + else
> + value = 0; /* disabled */
> + break;
> default:
> return -EINVAL;
> }
> @@ -916,6 +998,7 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> sd->volume_limiter = data[3];
> break;
> case 0xa0:
> + sd->inactive_timeout = data[1];
> sd->bt_call_ducking = data[4];
> break;
> case 0x37:
> @@ -927,6 +1010,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> case 0x3a:
> sd->volume_limiter = data[1];
> break;
> + case 0xa3:
> + sd->inactive_timeout = data[1];
> + break;
> case 0xb3:
> sd->bt_call_ducking = data[1];
> break;
> @@ -936,11 +1022,13 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> static void steelseries_arctis_nova_pro_parse_settings(
> struct steelseries_device *sd, u8 *data, int size)
> {
> - if (size < 10)
> + if (size < 13)
> return;
>
> - if (data[0] == 0x06 && data[1] == 0xb0)
> + if (data[0] == 0x06 && data[1] == 0xb0) {
> sd->mic_volume = data[9];
> + sd->inactive_timeout = data[12];
> + }
> }
>
> static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> @@ -970,9 +1058,10 @@ static const struct steelseries_device_info
> srws1_info = { };
>
> static const struct steelseries_device_info arctis_1_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 18,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_1_request_status,
> .parse_status = steelseries_arctis_1_parse_status,
> .write_setting = steelseries_arctis_1_write_setting,
> @@ -980,19 +1069,23 @@ static const struct steelseries_device_info
> arctis_1_info = {
>
> static const struct steelseries_device_info arctis_7_info = {
> .sync_interface = 5,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 18,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_7_request_status,
> .parse_status = steelseries_arctis_7_parse_status,
> - .write_setting = steelseries_arctis_1_write_setting,
> + .write_setting = steelseries_arctis_7_write_setting,
> };
>
> static const struct steelseries_device_info arctis_7_plus_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_7_plus_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1000,9 +1093,11 @@ static const struct steelseries_device_info
> arctis_7_plus_info = {
>
> static const struct steelseries_device_info arctis_9_info = {
> .sync_interface = 0,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 61,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_9_request_status,
> .parse_status = steelseries_arctis_9_parse_status,
> .write_setting = steelseries_arctis_9_write_setting,
> @@ -1010,10 +1105,12 @@ static const struct steelseries_device_info
> arctis_9_info = {
>
> static const struct steelseries_device_info arctis_nova_3p_info = {
> .sync_interface = 4,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 14,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_nova_3p_request_status,
> .parse_status = steelseries_arctis_nova_3p_parse_status,
> .write_setting = steelseries_arctis_nova_3p_write_setting,
> @@ -1022,10 +1119,11 @@ static const struct steelseries_device_info
> arctis_nova_3p_info = {
> static const struct steelseries_device_info arctis_nova_5_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME |
> - SS_CAP_VOLUME_LIMITER,
> + SS_CAP_VOLUME_LIMITER |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 15,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_5_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1034,10 +1132,12 @@ static const struct steelseries_device_info
> arctis_nova_5_info = {
> static const struct steelseries_device_info arctis_nova_5x_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 15,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_5x_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1047,10 +1147,11 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_parse_status,
> .write_setting = steelseries_arctis_nova_7_write_setting,
> @@ -1059,9 +1160,10 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
> static const struct steelseries_device_info arctis_nova_7p_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_parse_status,
> .write_setting = steelseries_arctis_nova_7_write_setting,
> @@ -1074,9 +1176,10 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> .request_settings =
> steelseries_arctis_nova_7_gen2_request_settings,
> @@ -1088,11 +1191,12 @@ static const struct steelseries_device_info
> arctis_nova_pro_info = {
> .sync_interface = 4,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> - SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
> + SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_min = 1,
> .mic_volume_max = 10,
> + .inactive_time_max = 60,
> .request_status =
> steelseries_arctis_nova_pro_request_status,
> .parse_status = steelseries_arctis_nova_pro_parse_status,
> .parse_settings =
> steelseries_arctis_nova_pro_parse_settings,
> @@ -1271,12 +1375,55 @@ static ssize_t
> bt_device_connected_show(struct device *dev,
> return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
> }
>
> +static ssize_t inactive_time_show(struct device *dev,
> + struct device_attribute *attr,
> char *buf)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->inactive_timeout);
> +}
> +
> +static ssize_t inactive_time_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> + unsigned int value;
> + int ret;
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + ret = kstrtouint(buf, 10, &value);
> + if (ret)
> + return ret;
> +
> + if (value > sd->info->inactive_time_max)
> + return -EINVAL;
> +
> + ret = sd->info->write_setting(sd->hdev,
> SS_SETTING_INACTIVE_TIME,
> + value);
> + if (ret)
> + return ret;
> +
> + sd->inactive_timeout = value;
> +
> + return count;
> +}
> +
> static DEVICE_ATTR_RO(bt_enabled);
> static DEVICE_ATTR_RO(bt_device_connected);
> +static DEVICE_ATTR_RW(inactive_time);
>
> static struct attribute *steelseries_headset_attrs[] = {
> &dev_attr_bt_enabled.attr,
> &dev_attr_bt_device_connected.attr,
> + &dev_attr_inactive_time.attr,
> NULL,
> };
>
> @@ -1298,6 +1445,8 @@ static umode_t
> steelseries_headset_attr_is_visible(struct kobject *kobj,
> return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
> if (attr == &dev_attr_bt_device_connected.attr)
> return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr-
> >mode : 0;
> + if (attr == &dev_attr_inactive_time.attr)
> + return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode :
> 0;
>
> return 0;
> }
> @@ -1956,7 +2105,8 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> - if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED)) {
> + if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_INACTIVE_TIME)) {
> ret = sysfs_create_group(&hdev->dev.kobj,
>
> &steelseries_headset_attr_group);
> if (ret)
> @@ -2038,7 +2188,8 @@ static void steelseries_remove(struct
> hid_device *hdev)
> }
>
> if (interface_num == sd->info->sync_interface) {
> - if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED))
> + if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_INACTIVE_TIME))
> sysfs_remove_group(&hdev->dev.kobj,
>
> &steelseries_headset_attr_group);
>
^ permalink raw reply
* Re: [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Hey Sriman,
Great work splitting up your original code. I'm afraid that I see some
of the patches as still needing more splitting up to be easily
reviewable, especially "HID: steelseries: Add async support and unify
device definitions".
The code looks good to me from a cursory glance, though we probably
want to get more eyeballs on the sound code.
I won't have time to test this patchset on real hardware for a little
while, but I'll test this version or any updates if there are any when
I have time.
I also don't know what out subsystem maintainers think, but, if you
have the patience, this might be the opportunity to split off the
headset support from the completely unrelated force feedback steering
wheel driver. I'm not going to block your patchset on this but
something to consider for the future.
Cheers
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> This patch series adds comprehensive support for the SteelSeries
> Arctis
> wireless gaming headset lineup to the hid-steelseries driver.
>
> The current driver provides only basic battery monitoring for Arctis
> 1
> and Arctis 9. This series extends support to 25+ Arctis models with
> full feature control including sidetone, auto-sleep, microphone
> controls, volume limiting, and Bluetooth settings.
>
> The driver restructure uses a capability-based device info system to
> cleanly handle the varying feature sets across different Arctis
> generations while maintaining support for the legacy SRW-S1 racing
> wheel.
>
> The driver also sets up future support for async device control which
> is currently implemented for the Arctis Nova 7 Gen 2 and Post-January
> update
> Gen 1 devices as implemented.
>
> Tested on Arctis Nova 7 (0x2202) and Arctis Nova 7 (0x22a1). All
> other
> implementation details are based on the reverse engineering done in
> the
> HeadsetControl library (902e9bc).
>
> Changes since v2:
> * Expose audio related controls via ALSA mixers
> * Implement async inputs from supported devices with known protocols
> * Overall code cleanup and improvements to initalization logic
> * Fixed several logical and protocol issues for Arctis 7 and 9
>
> Sriman Achanta (18):
> HID: steelseries: Fix ARCTIS_1_X device mislabeling
> HID: hid-ids: Add SteelSeries Arctis headset device IDs
> HID: quirks: Add additional Arctis headset device IDs
> HID: steelseries: Add async support and unify device definitions
> HID: steelseries: Update Kconfig help text for expanded headset
> support
> HID: steelseries: Add ALSA sound card infrastructure
> HID: steelseries: Add ChatMix ALSA mixer controls
> HID: steelseries: Add mic mute ALSA mixer control
> HID: steelseries: Add Bluetooth state sysfs attributes
> HID: steelseries: Add settings poll infrastructure
> HID: steelseries: Add sidetone ALSA mixer control
> HID: steelseries: Add mic volume ALSA mixer control
> HID: steelseries: Add volume limiter ALSA mixer control
> HID: steelseries: Add Bluetooth call audio ducking control
> HID: steelseries: Add inactive time sysfs attribute
> HID: steelseries: Add Bluetooth auto-enable sysfs attribute
> HID: steelseries: Add mic mute LED brightness control
> HID: steelseries: Document sysfs ABI
>
> .../ABI/testing/sysfs-driver-hid-steelseries | 87 +
> drivers/hid/Kconfig | 5 +-
> drivers/hid/hid-ids.h | 35 +-
> drivers/hid/hid-quirks.c | 27 +
> drivers/hid/hid-steelseries.c | 2329 ++++++++++++++-
> --
> 5 files changed, 2184 insertions(+), 299 deletions(-)
> create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-
> steelseries
^ permalink raw reply
* Re: [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-15-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Expose Bluetooth call audio ducking behavior as a writable ALSA
> enumerated mixer control ("Bluetooth Call Audio Ducking"), with three
> options: off, lower game audio by 12 dB, or mute game audio entirely.
You probably want to explain what "call ducking" or even "Bluetooth
call ducking" is.
>
> On the Arctis Nova 7 Gen2, this setting is stored alongside inactive
> timeout and Bluetooth auto-enable in a dedicated device configuration
> block. The settings request is expanded to also send a 0x00/0xa0
> device
> query in addition to the existing 0x00/0x20 audio settings query.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 120
> ++++++++++++++++++++++++++++++++--
> 1 file changed, 114 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 47ffec481571..bb9abbb0b6f8 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -30,12 +30,14 @@
> #define SS_CAP_SIDETONE BIT(6)
> #define SS_CAP_MIC_VOLUME BIT(7)
> #define SS_CAP_VOLUME_LIMITER BIT(8)
> +#define SS_CAP_BT_CALL_DUCKING BIT(9)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> #define SS_SETTING_SIDETONE 0
> #define SS_SETTING_MIC_VOLUME 1
> #define SS_SETTING_VOLUME_LIMITER 2
> +#define SS_SETTING_BT_CALL_DUCKING 3
>
> struct steelseries_device;
>
> @@ -80,12 +82,14 @@ struct steelseries_device {
> struct snd_ctl_elem_id sidetone_id;
> struct snd_ctl_elem_id mic_volume_id;
> struct snd_ctl_elem_id volume_limiter_id;
> + struct snd_ctl_elem_id bt_call_ducking_id;
> u8 chatmix_chat;
> u8 chatmix_game;
> bool mic_muted;
> u8 sidetone;
> u8 mic_volume;
> bool volume_limiter;
> + u8 bt_call_ducking;
>
> bool bt_enabled;
> bool bt_device_connected;
> @@ -573,6 +577,9 @@ static int
> steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
> case SS_SETTING_VOLUME_LIMITER:
> cmd = 0x3a;
> break;
> + case SS_SETTING_BT_CALL_DUCKING:
> + cmd = 0xb3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -883,15 +890,23 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
>
> static int steelseries_arctis_nova_7_gen2_request_settings(struct
> hid_device *hdev)
> {
> - const u8 data[] = { 0x00, 0x20 };
> + const u8 audio_data[] = { 0x00, 0x20 };
> + const u8 device_data[] = { 0x00, 0xa0 };
> + int ret;
>
> - return steelseries_send_output_report(hdev, data,
> sizeof(data));
> + ret = steelseries_send_output_report(hdev, audio_data,
> sizeof(audio_data));
> + if (ret)
> + return ret;
> +
> + msleep(10);
> +
> + return steelseries_send_output_report(hdev, device_data,
> sizeof(device_data));
> }
>
> static void steelseries_arctis_nova_7_gen2_parse_settings(
> struct steelseries_device *sd, u8 *data, int size)
> {
> - if (size < 4)
> + if (size < 5)
> return;
>
> switch (data[0]) {
> @@ -900,6 +915,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> sd->sidetone = data[2];
> sd->volume_limiter = data[3];
> break;
> + case 0xa0:
> + sd->bt_call_ducking = data[4];
> + break;
> case 0x37:
> sd->mic_volume = data[1];
> break;
> @@ -909,6 +927,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> case 0x3a:
> sd->volume_limiter = data[1];
> break;
> + case 0xb3:
> + sd->bt_call_ducking = data[1];
> + break;
> }
> }
>
> @@ -1025,7 +1046,8 @@ static const struct steelseries_device_info
> arctis_nova_5x_info = {
> static const struct steelseries_device_info arctis_nova_7_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> @@ -1036,7 +1058,8 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
>
> static const struct steelseries_device_info arctis_nova_7p_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .mic_volume_max = 7,
> .request_status = steelseries_arctis_nova_request_status,
> @@ -1050,7 +1073,8 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> .request_status = steelseries_arctis_nova_request_status,
> @@ -1557,6 +1581,70 @@ static const struct snd_kcontrol_new
> steelseries_volume_limiter_control = {
> .put = steelseries_volume_limiter_put,
> };
>
> +static const char *const bt_call_ducking_texts[] = {
> + "Off",
> + "Lower Volume (-12dB)",
> + "Mute Game",
> +};
> +
> +static int steelseries_bt_call_ducking_info(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_info
> *uinfo)
> +{
> + return snd_ctl_enum_info(uinfo, 1,
> ARRAY_SIZE(bt_call_ducking_texts),
> + bt_call_ducking_texts);
> +}
> +
> +static int steelseries_bt_call_ducking_get(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_value
> *ucontrol)
> +{
> + struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
> + unsigned long flags;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + ucontrol->value.enumerated.item[0] = sd->bt_call_ducking;
> + spin_unlock_irqrestore(&sd->lock, flags);
> + return 0;
> +}
> +
> +static int steelseries_bt_call_ducking_put(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_value
> *ucontrol)
> +{
> + struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
> + unsigned long flags;
> + u8 new_value;
> + int ret;
> +
> + new_value = ucontrol->value.enumerated.item[0];
> + if (new_value >= ARRAY_SIZE(bt_call_ducking_texts))
> + return -EINVAL;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + if (sd->bt_call_ducking == new_value) {
> + spin_unlock_irqrestore(&sd->lock, flags);
> + return 0;
> + }
> + spin_unlock_irqrestore(&sd->lock, flags);
> +
> + ret = sd->info->write_setting(sd->hdev,
> SS_SETTING_BT_CALL_DUCKING,
> + new_value);
> + if (ret)
> + return ret;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + sd->bt_call_ducking = new_value;
> + spin_unlock_irqrestore(&sd->lock, flags);
> +
> + return 1;
> +}
> +
> +static const struct snd_kcontrol_new
> steelseries_bt_call_ducking_control = {
> + .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
> + .name = "Bluetooth Call Audio Ducking",
> + .info = steelseries_bt_call_ducking_info,
> + .get = steelseries_bt_call_ducking_get,
> + .put = steelseries_bt_call_ducking_put,
> +};
> +
> static int steelseries_snd_register(struct steelseries_device *sd)
> {
> struct hid_device *hdev = sd->hdev;
> @@ -1644,6 +1732,21 @@ static int steelseries_snd_register(struct
> steelseries_device *sd)
> sd->volume_limiter_id = kctl->id;
> }
>
> + if (sd->info->capabilities & SS_CAP_BT_CALL_DUCKING) {
> + struct snd_kcontrol *kctl;
> + struct snd_kcontrol_new ducking_ctl =
> steelseries_bt_call_ducking_control;
> +
> + ducking_ctl.access =
> SNDRV_CTL_ELEM_ACCESS_READWRITE;
> + if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
> + ducking_ctl.access |=
> SNDRV_CTL_ELEM_ACCESS_VOLATILE;
> +
> + kctl = snd_ctl_new1(&ducking_ctl, sd);
> + ret = snd_ctl_add(sd->card, kctl);
> + if (ret < 0)
> + goto err_free_card;
> + sd->bt_call_ducking_id = kctl->id;
> + }
> +
> ret = snd_card_register(sd->card);
> if (ret < 0)
> goto err_free_card;
> @@ -1677,6 +1780,7 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> u8 old_sidetone;
> u8 old_mic_volume;
> bool old_volume_limiter;
> + u8 old_bt_call_ducking;
> bool is_async_interface = false;
>
> if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> @@ -1694,6 +1798,7 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> old_sidetone = sd->sidetone;
> old_mic_volume = sd->mic_volume;
> old_volume_limiter = sd->volume_limiter;
> + old_bt_call_ducking = sd->bt_call_ducking;
>
> if (hid_is_usb(hdev)) {
> struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> @@ -1763,6 +1868,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> if (sd->volume_limiter != old_volume_limiter)
> snd_ctl_notify(sd->card,
> SNDRV_CTL_EVENT_MASK_VALUE,
> &sd->volume_limiter_id);
> + if (sd->bt_call_ducking != old_bt_call_ducking)
> + snd_ctl_notify(sd->card,
> SNDRV_CTL_EVENT_MASK_VALUE,
> + &sd->bt_call_ducking_id);
> }
>
> return 0;
^ permalink raw reply
* Re: [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
In-Reply-To: <20260227235042.410062-10-srimanachanta@gmail.com>
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add read-only sysfs attributes bt_enabled and bt_device_connected
> that
> reflect the current Bluetooth radio state for headsets that support
> it.
> Attributes are registered via an attribute group with an is_visible
> callback so they only appear on capable devices.
This needs a longer explanation as to what that "Bluetooth state" is,
so folks that aren't familiar with the Steelseries products'
capabilities.
> Bluetooth state is decoded from the following HID reports:
> - Arctis Nova 7 Gen2: 0xb0 initial status packet and 0xb5 async
> events
> - Arctis Nova Pro: initial 0x06/0x14 status packet
>
> Returns -ENODEV if the headset is not currently connected.
Do you have any ideas on how it might be used? Maybe a link to a
product page that shows the Windows or macOS software using the
feature, or another manufacturer using something similar?
Adding sysfs files like this is pretty frowned upon, so you probably
want to make sure that this is the last or close to last patch in your
patchset so maintainers can easily defer it without blocking the rest.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 111
> +++++++++++++++++++++++++++++++++-
> 1 file changed, 109 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 3de8e1555263..8c6116d02f19 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -24,6 +24,8 @@
> #define SS_CAP_BATTERY BIT(0)
> #define SS_CAP_CHATMIX BIT(1)
> #define SS_CAP_MIC_MUTE BIT(2)
> +#define SS_CAP_BT_ENABLED BIT(3)
> +#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -62,6 +64,9 @@ struct steelseries_device {
> u8 chatmix_game;
> bool mic_muted;
>
> + bool bt_enabled;
> + bool bt_device_connected;
> +
> spinlock_t lock;
> bool removed;
> };
> @@ -641,6 +646,20 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> sd->battery_charging = (data[3] == 0x01);
> sd->chatmix_game = data[4];
> sd->chatmix_chat = data[5];
> + switch (data[6]) {
> + case 0x00:
> + sd->bt_enabled = false;
> + sd->bt_device_connected = false;
> + break;
> + case 0x03:
> + sd->bt_enabled = true;
> + sd->bt_device_connected = false;
> + break;
> + case 0x02:
> + sd->bt_enabled = true;
> + sd->bt_device_connected = true;
> + break;
> + }
> sd->mic_muted = (data[9] == 0x01);
> break;
> case 0xb7:
> @@ -659,6 +678,15 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> case 0x52:
> sd->mic_muted = (data[2] == 0x01);
> break;
> + case 0xb5:
> + if (data[1] == 0x01) {
> + sd->bt_enabled = false;
> + sd->bt_device_connected = false;
> + } else if (data[1] == 0x04) {
> + sd->bt_enabled = true;
> + sd->bt_device_connected = (data[2] == 0x01);
> + }
> + break;
> }
> }
>
> @@ -673,6 +701,8 @@ static void
> steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
> sd->battery_capacity =
> steelseries_map_capacity(data[6], 0x00, 0x08);
> sd->battery_charging = (data[15] == 0x02);
> sd->mic_muted = (data[9] == 0x01);
> + sd->bt_enabled = (data[4] == 0x00);
> + sd->bt_device_connected = (data[5] == 0x01);
> } else if (data[0] == 0x07 && data[1] == 0x45) {
> sd->chatmix_game = data[2];
> sd->chatmix_chat = data[3];
> @@ -760,14 +790,16 @@ static const struct steelseries_device_info
> arctis_nova_7p_info = {
> static const struct steelseries_device_info arctis_nova_7_gen2_info
> = {
> .sync_interface = 3,
> .async_interface = 5,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> };
>
> static const struct steelseries_device_info arctis_nova_pro_info = {
> .sync_interface = 4,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .request_status =
> steelseries_arctis_nova_pro_request_status,
> .parse_status = steelseries_arctis_nova_pro_parse_status,
> @@ -908,6 +940,70 @@ static int steelseries_battery_register(struct
> steelseries_device *sd)
> return 0;
> }
>
> +/*
> + * Sysfs attributes for device state
> + */
> +
> +static ssize_t bt_enabled_show(struct device *dev,
> + struct device_attribute *attr, char
> *buf)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->bt_enabled);
> +}
> +
> +static ssize_t bt_device_connected_show(struct device *dev,
> + struct device_attribute
> *attr, char *buf)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
> +}
> +
> +static DEVICE_ATTR_RO(bt_enabled);
> +static DEVICE_ATTR_RO(bt_device_connected);
> +
> +static struct attribute *steelseries_headset_attrs[] = {
> + &dev_attr_bt_enabled.attr,
> + &dev_attr_bt_device_connected.attr,
> + NULL,
> +};
> +
> +static umode_t steelseries_headset_attr_is_visible(struct kobject
> *kobj,
> + struct attribute
> *attr,
> + int index)
> +{
> + struct device *dev = kobj_to_dev(kobj);
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> + unsigned long caps;
> +
> + if (!sd)
> + return 0;
> +
> + caps = sd->info->capabilities;
> +
> + if (attr == &dev_attr_bt_enabled.attr)
> + return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
> + if (attr == &dev_attr_bt_device_connected.attr)
> + return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr-
> >mode : 0;
> +
> + return 0;
> +}
> +
> +static const struct attribute_group steelseries_headset_attr_group =
> {
> + .attrs = steelseries_headset_attrs,
> + .is_visible = steelseries_headset_attr_is_visible,
> +};
> +
> #if IS_BUILTIN(CONFIG_SND) || \
> (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
>
> @@ -1218,6 +1314,13 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> + if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED)) {
> + ret = sysfs_create_group(&hdev->dev.kobj,
> +
> &steelseries_headset_attr_group);
> + if (ret)
> + hid_warn(hdev, "Failed to create
> sysfs group: %d\n", ret);
> + }
> +
> #if IS_BUILTIN(CONFIG_SND) || \
> (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> ret = steelseries_snd_register(sd);
> @@ -1289,6 +1392,10 @@ static void steelseries_remove(struct
> hid_device *hdev)
> }
>
> if (interface_num == sd->info->sync_interface) {
> + if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED))
> + sysfs_remove_group(&hdev->dev.kobj,
> +
> &steelseries_headset_attr_group);
> +
> if (sd->info->async_interface) {
> struct hid_device *sibling;
>
^ permalink raw reply
* Re: [PATCH] Input: atkbd: add keymap fixup for notebooks using 0x6e as Fn modifier
From: Mikhail Novosyolov @ 2026-03-03 12:42 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, mpearson-lenovo
In-Reply-To: <aZVZokRqcqWMfLbM@google.com>
Hello,
18.02.2026 09:26, Dmitry Torokhov пишет:
> On Wed, Feb 18, 2026 at 07:13:52AM +0300, Mikhail Novosyolov wrote:
>> Commit dc8c9c171ef3 ("Input: atkbd - map F23 key to support default
>> copilot shortcut") mapped scancode 0x6e to KEY_F23 to support the
>> Microsoft Copilot key on Lenovo, HP, and Dell notebooks.
>>
>> However, some notebook platforms (including Positron Proxima 15 and
>> possibly others based on the same OEM design) use scancode 0x6e for the
>> Fn modifier key instead of a dedicated Copilot key. When 0x6e generates
>> KEY_F23 events, the Fn key breaks Fn combinations such as Fn+F5
>> (touchpad toggle).
>>
>> On these platforms, the hardware relies on 0x6e being unmapped to
>> properly handle Fn combinations at the firmware level. When the kernel
>> maps it to KEY_F23, desktop environments intercept this as a global
>> hotkey and toggle the touchpad, but cannot re-enable it because the
>> firmware no longer recognizes Fn as a valid modifier.
>>
>> Userspace solutions (systemd hwdb) cannot fix this because the keycode
>> mapping happens in the atkbd driver before events reach userspace.
>> A kernel-level quirk is required.
> ? That is exactly what udev hwdb is for. Use it. Check 60-keyboard.hwdb
> for examples.
>
> Thanks.
Thanks. Probably I tried hwdb incorrectly.
Now I've tried like
https://github.com/systemd/systemd/commit/d2502f55a2d9dc13f82cc1551a0639843fefb69e
user@rosa-jr4mnj ~ $ cat /etc/udev/hwdb.d/61-positron-fn-fix.hwdb
evdev:atkbd:dmi:bvn*:bvr*:bd*:svn*Positron*:pnG1569*:*
KEYBOARD_KEY_6e=fn
user@rosa-jr4mnj ~ $ sudo systemd-hwdb update
and reboot.
It seems to work correctly.
>
^ permalink raw reply
* Re: [PATCH 09/12] dt-bindings: input: Document hid-over-spi DT schema
From: Rob Herring @ 2026-03-03 13:53 UTC (permalink / raw)
To: Jingyuan Liang
Cc: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Krzysztof Kozlowski, Conor Dooley, linux-input,
linux-doc, linux-kernel, linux-spi, linux-trace-kernel,
devicetree, hbarnor, Dmitry Antipov, Jarrett Schultz
In-Reply-To: <20260303-send-upstream-v1-9-1515ba218f3d@chromium.org>
On Tue, Mar 3, 2026 at 12:14 AM Jingyuan Liang <jingyliang@chromium.org> wrote:
>
> Documentation describes the required and optional properties for
> implementing Device Tree for a Microsoft G6 Touch Digitizer that
> supports HID over SPI Protocol 1.0 specification.
>
> The properties are common to HID over SPI.
>
> Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
> Signed-off-by: Jarrett Schultz <jaschultz@microsoft.com>
> Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
> ---
> .../devicetree/bindings/input/hid-over-spi.yaml | 153 +++++++++++++++++++++
> 1 file changed, 153 insertions(+)
>
> diff --git a/Documentation/devicetree/bindings/input/hid-over-spi.yaml b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> new file mode 100644
> index 000000000000..b623629ed9d3
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> @@ -0,0 +1,153 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/input/hid-over-spi.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: HID over SPI Devices
> +
> +maintainers:
> + - Benjamin Tissoires <benjamin.tissoires@redhat.com>
> + - Jiri Kosina <jkosina@suse.cz>
> +
> +description: |+
> + HID over SPI provides support for various Human Interface Devices over the
> + SPI bus. These devices can be for example touchpads, keyboards, touch screens
> + or sensors.
> +
> + The specification has been written by Microsoft and is currently available here:
> + https://www.microsoft.com/en-us/download/details.aspx?id=103325
> +
> + If this binding is used, the kernel module spi-hid will handle the communication
> + with the device and the generic hid core layer will handle the protocol.
> +
> +allOf:
> + - $ref: /schemas/input/touchscreen/touchscreen.yaml#
> +
> +properties:
> + compatible:
> + oneOf:
> + - items:
> + - enum:
> + - microsoft,g6-touch-digitizer
> + - const: hid-over-spi
> + - description: Just "hid-over-spi" alone is allowed, but not recommended.
> + const: hid-over-spi
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + reset-gpios:
> + maxItems: 1
> + description:
> + GPIO specifier for the digitizer's reset pin (active low). The line must
> + be flagged with GPIO_ACTIVE_LOW.
> +
> + vdd-supply:
> + description:
> + Regulator for the VDD supply voltage.
Is this part of the spec? This won't scale for multiple devices with
different power rails.
> +
> + input-report-header-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Read Approval packet, listing an address of
> + the input report header to be put on the SPI bus. This address has 24
> + bits.
> +
> + input-report-body-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Read Approval packet, listing an address of
> + the input report body to be put on the SPI bus. This address has 24 bits.
> +
> + output-report-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Output Report sent by the host, listing an
> + address where the output report on the SPI bus is to be written to. This
> + address has 24 bits.
> +
> + post-power-on-delay-ms:
> + description:
> + Optional time in ms required by the device after enabling its regulators
> + or powering it on, before it is ready for communication.
Drop. This should be implied by the compatible.
> +
> + minimal-reset-delay-ms:
> + description:
> + Optional minimum amount of time in ms that device needs to be in reset
> + state for the reset to take effect.
Drop. This should be implied by the compatible.
> +
> + read-opcode:
> + $ref: /schemas/types.yaml#/definitions/uint8
> + description:
> + Value to be used in Read Approval packets. 1 byte.
> +
> + write-opcode:
> + $ref: /schemas/types.yaml#/definitions/uint8
> + description:
> + Value to be used in Write Approval packets. 1 byte.
Why are these and the address properties above not defined by the
spec? Do they vary for a specific device? If not, then they should be
implied by the compatible.
> +
> + hid-over-spi-flags:
> + $ref: /schemas/types.yaml#/definitions/uint16
> + description:
> + 16 bits.
> + Bits 0-12 - Reserved (must be 0)
> + Bit 13 - SPI Write Mode. Possible values -
> + * 0b0- Writes are carried out in Single-SPI mode
> + * 0b1- Writes are carried out in the Multi-SPI mode specified by bits
> + 14-15
> + Bits 14-15 - Multi-SPI Mode. Possible values -
> + * 0b00- Single SPI
> + * 0b01- Dual SPI
> + * 0b10- Quad SPI
We already have SPI properties to define the bus width for read and write.
> +
> +required:
> + - compatible
> + - interrupts
> + - reset-gpios
> + - vdd-supply
> + - input-report-header-address
> + - input-report-body-address
> + - output-report-address
> + - read-opcode
> + - write-opcode
> + - hid-over-spi-flags
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/irq.h>
> + #include <dt-bindings/gpio/gpio.h>
> +
> + spi {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + hid@0 {
> + compatible = "hid-over-spi";
> + reg = <0x0>;
> + interrupts-extended = <&gpio 42 IRQ_TYPE_EDGE_FALLING>;
> + reset-gpios = <&gpio 27 GPIO_ACTIVE_LOW>;
> + vdd-supply = <&pm8350c_l3>;
> + pinctrl-names = "default";
> + pinctrl-0 = <&ts_d6_reset_assert &ts_d6_int_bias>;
> + input-report-header-address = <0x1000>;
> + input-report-body-address = <0x1004>;
> + output-report-address = <0x2000>;
> + read-opcode = <0x0b>;
> + write-opcode = <0x02>;
> + hid-over-spi-flags = <0x0000>;
> + post-power-on-delay-ms = <5>;
> + minimal-reset-delay-ms = <5>;
> + };
> + };
> \ No newline at end of file
Fix this.
Rob
^ permalink raw reply
* [PATCH] HID: wacom: fix out-of-bounds read in wacom_intuos_bt_irq
From: Benoît Sevens @ 2026-03-03 13:58 UTC (permalink / raw)
To: Ping Cheng, Jason Gerecke, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Benoît Sevens
The wacom_intuos_bt_irq() function processes Bluetooth HID reports
without sufficient bounds checking. A maliciously crafted short report
can trigger an out-of-bounds read when copying data into the wacom
structure.
Specifically, report 0x03 requires at least 22 bytes to safely read
the processed data and battery status, while report 0x04 (which
falls through to 0x03) requires 32 bytes.
Add explicit length checks for these report IDs and log a warning if
a short report is received.
Signed-off-by: Benoît Sevens <bsevens@google.com>
---
drivers/hid/wacom_wac.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/drivers/hid/wacom_wac.c b/drivers/hid/wacom_wac.c
index 9b2c710f8da1..da1f0ea85625 100644
--- a/drivers/hid/wacom_wac.c
+++ b/drivers/hid/wacom_wac.c
@@ -1208,10 +1208,20 @@ static int wacom_intuos_bt_irq(struct wacom_wac *wacom, size_t len)
switch (data[0]) {
case 0x04:
+ if (len < 32) {
+ dev_warn(wacom->pen_input->dev.parent,
+ "Report 0x04 too short: %zu bytes\n", len);
+ break;
+ }
wacom_intuos_bt_process_data(wacom, data + i);
i += 10;
fallthrough;
case 0x03:
+ if (i == 1 && len < 22) {
+ dev_warn(wacom->pen_input->dev.parent,
+ "Report 0x03 too short: %zu bytes\n", len);
+ break;
+ }
wacom_intuos_bt_process_data(wacom, data + i);
i += 10;
wacom_intuos_bt_process_data(wacom, data + i);
--
2.53.0.473.g4a7958ca14-goog
^ permalink raw reply related
* Re: [PATCH v5 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-03-03 16:14 UTC (permalink / raw)
To: Andrew Thomas
Cc: 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, linux-kernel,
devicetree, linux-input
In-Reply-To: <lgpdkwl7hxz7ok7qtujzdhf3c3iehwvm5d7myxfewr4kgrcq5k@3s4v4sjus7go>
Hi Andrew,
On 26-02-26, Andrew Thomas wrote:
> On Wed, Feb 25, 2026 at 09:50:11PM +0100, Marco Felsch wrote:
...
> > > > +static int axiom_u02_enter_bootloader(struct axiom_data *ts)
> > > > +{
> > > > + struct axiom_u02_rev1_system_manager_msg msg = { };
> > > > + struct device *dev = ts->dev;
> > > > + unsigned int val;
> > > > + int ret;
> > > > +
> > > > + if (!axiom_driver_supports_usage(ts, AXIOM_U02))
> > > > + return -EINVAL;
> > > > +
> > > > + /*
> > > > + * Enter the bootloader mode requires 3 consecutive messages so we can't
> > > > + * check for the response.
> > > > + */
> > > > + msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_ENTERBOOTLOADER);
> > > > + msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1);
> > > > + ret = axiom_u02_send_msg(ts, &msg, false);
> > > > + if (ret) {
> > > > + dev_err(dev, "Failed to send bootloader-key1: %d\n", ret);
> > > > + return ret;
> > > > + }
> > >
> > > A delay is required between commands. 10ms is fine.
> >
> > Can I make use of the axiom_u02_wait_idle() logic which checks the
> > AXIOM_U02_REV1_RESP_SUCCESS? Arbitrary delays are always a source of
> > trouble.
>
> Yes, I tested with axiom_u02_wait_idle() which is OK.
I tested it too, unfortunately I wasn't able to enter the bootlaoder
mode with:
- axiom_u02_send_msg(.., .., true);
This aligns with the "Programmer's Guide":
|
| 0x000B: ENTERBOOTLOADER
|
| This command must be sent sequentially, in 3 phases, with no other usage
| accesses in between each phase...
|
> I am slightly worried about too short a delay to axiom causing instability,
> however this works fine.
> It can unfortunately be an unstable device..
That could be one issue. I've added a delay of 10ms between each
enter-bootloader-cmd and tested the update. It turns out that the 10ms
delay makes the "enter bootloader" process more unstable. Without the
delay, I mostly enter the bootloader on the first attempt or at least
after three attempts. With the delay I needed 5+ attempts sometimes.
Therefore I would like to keep it as it is right now (no delay). Note:
The "Programmer's Guide" mentions no timing contraints as well. I added
a TODO comment to the driver.
...
> > > > +/* Custom regmap read/write handling is required due to the aXiom protocol */
> > > > +static int axiom_regmap_read(void *context, const void *reg_buf, size_t reg_size,
> > > > + void *val_buf, size_t val_size)
> > > > +{
> > > > + struct device *dev = context;
> > > > + struct i2c_client *i2c = to_i2c_client(dev);
> > > > + struct axiom_data *ts = i2c_get_clientdata(i2c);
> > > > + struct axiom_cmd_header hdr;
> > > > + u16 xferlen, addr, baseaddr;
> > > > + struct i2c_msg xfer[2];
> > > > + int ret;
> > > > +
> > > > + if (val_size > AXIOM_MAX_XFERLEN) {
> > > > + dev_err(ts->dev, "Exceed max xferlen: %zu > %u\n",
> > > > + val_size, AXIOM_MAX_XFERLEN);
> > > > + return -EINVAL;
> > > > + }
> > > > +
> > > > + addr = *((u16 *)reg_buf);
> > > > + hdr.target_address = cpu_to_le16(addr);
> > > > + xferlen = FIELD_PREP(AXIOM_CMD_HDR_DIR_MASK, AXIOM_CMD_HDR_READ) |
> > > > + FIELD_PREP(AXIOM_CMD_HDR_LEN_MASK, val_size);
> > > > + hdr.xferlen = cpu_to_le16(xferlen);
> > > > +
> > > > + /* Verify that usage including the usage rev is supported */
> > > > + baseaddr = addr & AXIOM_USAGE_BASEADDR_MASK;
> > > > + if (!axiom_usage_supported(ts, baseaddr))
> > > > + return -EINVAL;
> > > > +
> > > > + xfer[0].addr = i2c->addr;
> > > > + xfer[0].flags = 0;
> > > > + xfer[0].len = sizeof(hdr);
> > > > + xfer[0].buf = (u8 *)&hdr;
> > > > +
> > > > + xfer[1].addr = i2c->addr;
> > > > + xfer[1].flags = I2C_M_RD;
> > > > + xfer[1].len = val_size;
> > > > + xfer[1].buf = val_buf;
> > > > +
> > > > + ret = i2c_transfer(i2c->adapter, xfer, 2);
> > > > + if (ret == 2)
> > > > + return 0;
> > > > + else if (ret < 0)
> > > > + return ret;
> > > > + else
> > > > + return -EIO;
> > > > +}
> > >
> > > There needs to be atleast 40us holdoff between axiom bus transfers.
> > > I am not sure that has been considered here.
> >
> > Is this written somewhere within the datasheet/programming-guide?
>
> In aXiom Comms Protocol in v4.8.9 if you have access to the webportal
> it says to use 40us holdoff for report reading. Although this may apply
> to all transactions.
Okay this is new information :) TBH the paragraph is bit hard to read.
Furthermore in POLL-mode the IRQ-pin can't be sampled :/ As already
pointed out, I'm also not a fan of adding arbitrary delays. The driver
has already enough delays.
It's also not quite clear to me why the device keeps the IRQ line
asserted till the I2C-STOP was received? It's like your postman is
holding your door bell till you go to the door, open it, provide your
signature and close the door. In other words, this sounds like a
firmware bug.
For polling there should be a simple 'is data-avaiable?' -> yes: do
further processing; -> no: try again in N-poll-ms.
> Doing comms while axiom is changing the DMA causes issues (NAKs), for the
> driver I posted atleast, the holdoff was required otherwise I would receive
> 0-length reports frequently.
Yep in this is actually the case but the driver can handle 0-length
reports just fine. I will add this new information as code comment.
Also according "aXiom Comms Protocol v4.8.9":
|
| This will not result in a communications failure but the host will
| read an empty report, as the device will not have had time to prepare
| the next report.
|
it shouldn't be a problem.
> It looks to be fine currently, however if there is unstability for users
> we should consider adding this or something similar.
I would like to use the same mechanism for polling and IRQ mode to check
if the device POLL/IRQ is done or not. Since the driver can handle
0-length reports just fine, there should be no issue.
> > ...
> >
> > > > +static enum fw_upload_err
> > > > +axiom_cfg_fw_prepare(struct fw_upload *fw_upload, const u8 *data, u32 size)
> > > > +{
> >
> > ...
> >
> > > > + cur_runtime_crc = ts->crc[AXIOM_CRC_CUR].runtime;
> > > > + fw_runtime_crc = ts->crc[AXIOM_CRC_NEW].runtime;
> > > > + if (cur_runtime_crc != fw_runtime_crc) {
> > > > + dev_err(dev, "TH2CFG and device runtime CRC doesn't match: %#x != %#x\n",
> > > > + fw_runtime_crc, cur_runtime_crc);
> > > > + ret = FW_UPLOAD_ERR_FW_INVALID;
> > > > + goto out;
> > > > + }
> > >
> > > The firmware CRCs dont need to match for a config load, only the usage revision/length.
> >
> > What difference does it make? The firmware CRC implicit includes the
> > usage revision and the length (register layout). So we can ensure that
> > the configuration was made for the correct register layout without
> > checking each register and revision.
>
> Different firmware revisions/CRCs can have compatible usages.
Yes this could be possible, but...
> Aslong as the usage revisions match to u31 the usages will be compatible.
isn't a th2cfg binary a complete register value update or can a th2cfg
binary update only register parts?
> For us atleast we have different CRCs for small firmware changes, therefore
> if testing such firmware here we would always have to uncomment this section.
So it's rather a feature used by you guys during development?
> In the updated python I changed the check to the following:
>
> # Compare the firmware runtime CRC from the file with the CRC from the device.
> # Only proceed if the CRCs match.
> if not force:
> if u33_from_file.fld_runtime_crc != ax.u33.fld_runtime_crc:
> logging.error("Cannot load config file as it was saved from a different revision of firmware:")
> logging.error("Firmware info from device : 0x{0:08X}, {1}".format(ax.u33.fld_runtime_crc, ax.u31.get_device_info_short()))
> logging.error("Firmware info from config file : 0x{0:08X}, {1}".format(u33_from_file.fld_runtime_crc, u31_from_file.get_device_info_short()))
> return ERROR_CFG_FILE_NOT_COMPATIBLE
> else:
> if u33_from_file.fld_runtime_crc != ax.u33.fld_runtime_crc:
> logging.warning("The config file was saved from a different revision of firmware therefore it may not be compatible:")
> logging.warning("Firmware info from device : 0x{0:08X}, {1}".format(ax.u33.fld_runtime_crc, ax.u31.get_device_info_short()))
> logging.warning("Firmware info from config file : 0x{0:08X}, {1}".format(u33_from_file.fld_runtime_crc, u31_from_file.get_device_info_short()))
>
> # Now ensure the config is compatible with the device
> valid_cfg = False
> file_usage_table = u31_from_file.get_usage_table()
>
> for usage in usages.keys():
> if usage not in ax.u31.get_usages():
> logging.error(f"Usage u{usage:02x} unsupported on this device.")
> break
I don't understand the logic here. Above you have a 'force' switch but
here you perform the checks anyway. So it's more like a 'light-force'?
Don't get me wrong I get your point but since this the mainline kernel
and the kernel needs to ensure that everything is correct I would like
to keep the simple firmware-crc check.
> usage_entry = ax.u31.get_usage_entry(usage)
> if usage_entry.start_page != file_usage_table[usage].start_page:
> logging.error(f"Incompatible config address for u{usage:02x}:")
> logging.error(f" Device: 0x{usage_entry.start_page:02x}00 File: 0x{file_usage_table[usage].start_page:02x}00")
> break
>
> if ax.get_usage_length(usage) != len(usages[usage][2]):
> logging.error(f"Incompatible config length for u{usage:02x}:")
> logging.error(f" Device: {ax.get_usage_length(usage)} File: {len(usages[usage][2])}")
> break
> else:
> valid_cfg = True
>
> if not valid_cfg:
> logging.error("Cannot load config as the usages are incompatible with the device.")
> return ERROR_CFG_FILE_NOT_COMPATIBLE
>
> Possibly we could add a force parameter to the sysfs like above?
Yes :) But this would be a 'force' aka no CRC check at all. I would see
this as addition which could be provided by you guys :)
> > ...
> >
> > > > +static enum fw_upload_err
> > > > +axiom_cfg_fw_write(struct fw_upload *fw_upload, const u8 *data, u32 offset,
> > > > + u32 size, u32 *written)
> > > > +{
> >
> > ....
> >
> > > > + /* Ensure that the chunks are written correctly */
> > > > + ret = axiom_verify_volatile_mem(ts);
> > > > + if (ret) {
> > > > + dev_err(dev, "Failed to verify written config, abort\n");
> > > > + goto err_swreset;
> > > > + }
> > > > +
> > > > + ret = axiom_u02_save_config(ts);
> > > > + if (ret)
> > > > + goto err_swreset;
> > > > +
> > > > + /*
> > > > + * TODO: Check if u02 start would be sufficient to load the new config
> > > > + * values
> > > > + */
> > >
> > > It is not necessarily needed.
> >
> > What do you mean by this? Do we need the axiom_u02_swreset() or can we
> > just start the system via u02 (without swreset)?
>
> There is no need to do a reset after a config load you can just start the AE
> with CMD_START, but we can keep it as is since it does the same thing.
Okay, I will update the comment then.
> > > > + ret = axiom_u02_swreset(ts);
> > > > + if (ret) {
> > > > + dev_err(dev, "Soft reset failed\n");
> > > > + goto err_unlock;
> > > > + }
> >
> > ....
> >
> > > > +static ssize_t fw_variant_show(struct device *dev,
> > > > + struct device_attribute *attr, char *buf)
> > > > +{
> > > > + struct i2c_client *i2c = to_i2c_client(dev);
> > > > + struct axiom_data *ts = i2c_get_clientdata(i2c);
> > > > + const char *val;
> > > > +
> > > > + switch (ts->fw_variant) {
> > > > + case 0:
> > > > + val = "3d";
> > > > + break;
> > > > + case 1:
> > > > + val = "2d";
> > > > + break;
> > > > + case 3:
> > > > + val = "force";
> > > > + break;
> > > > + default:
> > > > + val = "unknown";
> > > > + break;
> > > > + }
> > >
> > > The following are all the variants we currently support in order:
> > > FW_VARIANTS = ["3D", "2D", "FORCE", "0D", "XL"]
> >
> > Means:
> >
> > 0 == 3d
> > 1 == 2d
> > 3 == force
> > 4 == 0d
> > 5 == xl
> >
> > ?
> >
> > This is also something I can test on my site. Patches are welcome once
> > this is mainline of course :)
>
> It is like so:
> #define DEVICE_BUILD_VARIANT_3D (0U)
> #define DEVICE_BUILD_VARIANT_2D (1U)
> #define DEVICE_BUILD_VARIANT_FORCE (2U)
> #define DEVICE_BUILD_VARIANT_0D (3U)
> #define DEVICE_BUILD_VARIANT_XL (4U)
...
> I shall try to give a more prompt review once you have the new version up.
Thanks for your input, but keep in mind, that you guys could add your
additions ontop of our changes since our use-cases are fulfilled with
the current driver. However I've added the 0d, xl fw_variants and
updated the code comments.
Regards,
Marco
>
> Many Thanks,
> Andrew
>
>
--
#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 05/11] iio: adc: cpcap-adc: add support for Mot ADC
From: Jonathan Cameron @ 2026-03-03 21:27 UTC (permalink / raw)
To: Svyatoslav Ryhel
Cc: David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Lee Jones,
Pavel Machek, Liam Girdwood, Mark Brown, Dixit Parmar,
Tony Lindgren, linux-iio, devicetree, linux-kernel, linux-input,
linux-leds
In-Reply-To: <20260207135555.6e82e6d3@jic23-huawei>
On Sat, 7 Feb 2026 13:55:55 +0000
Jonathan Cameron <jic23@kernel.org> wrote:
> On Fri, 6 Feb 2026 19:28:39 +0200
> Svyatoslav Ryhel <clamor95@gmail.com> wrote:
>
> > Add support for ADC found in Motorola Mot board, used as a base for
> > Atrix 4G and Droid X2 smartphones.
> >
> > Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
> Applied to the testing branch of iio.git.
FWIW I messed up my trees, and lost this for a bit, it's there now though!
^ permalink raw reply
* [PATCH v6 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Marco Felsch @ 2026-03-03 22:41 UTC (permalink / raw)
To: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, andrew.thomas,
Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch
In-Reply-To: <20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@pengutronix.de>
Add FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the firmware_loader
framework that the update is not required. This can be the case if the
user provided firmware matches the current running firmware.
Sync lib/test_firmware.c accordingly.
Reviewed-by: Russ Weight <russ.weight@linux.dev>
Reviewed-by: Luis Chamberlain <mcgrof@kernel.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
drivers/base/firmware_loader/sysfs_upload.c | 1 +
include/linux/firmware.h | 2 ++
lib/test_firmware.c | 1 +
3 files changed, 4 insertions(+)
diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
index 829270067d1632f92656859fb9143e3fa9635670..0a583a1b3f4fde563257566426d523fbf839b13f 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",
};
static const char *fw_upload_progress(struct device *dev,
diff --git a/include/linux/firmware.h b/include/linux/firmware.h
index aae1b85ffc10e20e9c3c9b6009d26b83efd8cb24..fe7797be4c08cd62cdad9617b8f70095d5e0af2f 100644
--- a/include/linux/firmware.h
+++ b/include/linux/firmware.h
@@ -29,6 +29,7 @@ struct firmware {
* @FW_UPLOAD_ERR_RW_ERROR: read or write to HW failed, see kernel log
* @FW_UPLOAD_ERR_WEAROUT: FLASH device is approaching wear-out, wait & retry
* @FW_UPLOAD_ERR_FW_INVALID: invalid firmware file
+ * @FW_UPLOAD_ERR_DUPLICATE: firmware is already up to date (duplicate)
* @FW_UPLOAD_ERR_MAX: Maximum error code marker
*/
enum fw_upload_err {
@@ -41,6 +42,7 @@ enum fw_upload_err {
FW_UPLOAD_ERR_RW_ERROR,
FW_UPLOAD_ERR_WEAROUT,
FW_UPLOAD_ERR_FW_INVALID,
+ FW_UPLOAD_ERR_DUPLICATE,
FW_UPLOAD_ERR_MAX
};
diff --git a/lib/test_firmware.c b/lib/test_firmware.c
index 211222e63328f970228920f5662ee80cc7f51215..603c3a4b385c849944a695849a1894693234b5eb 100644
--- a/lib/test_firmware.c
+++ b/lib/test_firmware.c
@@ -1133,6 +1133,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",
};
static void upload_err_inject_error(struct test_firmware_upload *tst,
--
2.47.3
^ permalink raw reply related
* [PATCH v6 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-03-03 22:41 UTC (permalink / raw)
To: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, andrew.thomas,
Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch
In-Reply-To: <20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@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.
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 | 3084 ++++++++++++++++++++
4 files changed, 3182 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 196905162945d59e775c3e0bff6540a82842229a..9263dd79dab7e518e27af35364fcebbff0ba706e 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -806,6 +806,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 97a025c6a3770fb80255246eb63c11688ebd79eb..0591cb304784699bf2a8bda204461ac5f4532bb1 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..d909b108ee24dfc5a56b0ba735cb8a7882612d34
--- /dev/null
+++ b/drivers/input/touchscreen/touchnetix_axiom.c
@@ -0,0 +1,3084 @@
+// 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 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_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;
+
+ /*
+ * Enter the bootloader mode requires 3 consecutive messages so we can't
+ * check for the response.
+ * TODO: Check if it's required to add a delay between the consecutive
+ * CMD_ENTERBOOTLOADER cmds.
+ */
+ 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, false);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key1: %d\n", error);
+ return error;
+ }
+
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2);
+ error = axiom_u02_send_msg(ts, &msg, false);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key2: %d\n", error);
+ return error;
+ }
+
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3);
+ 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;
+ unsigned int size_bytes;
+
+ axiom_dump_usage_entry(dev, entry);
+
+ /*
+ * Verify that the driver used usages are supported. Don't abort
+ * yet if a usage isn't supported to allow the user to dump the
+ * actual usage table.
+ */
+ info = axiom_get_usage_info(entry);
+ if (IS_ERR(info)) {
+ dev_info(dev, "Required usage u%02X isn't supported for rev.%u\n",
+ entry->usage_num, entry->uifrevision);
+ error = -EACCES;
+ }
+
+ 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;
+
+ /* 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_cleanup(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+
+ mutex_unlock(&ts->fwupdate_lock);
+ pm_runtime_mark_last_busy(ts->dev);
+ pm_runtime_put_sync_autosuspend(ts->dev);
+}
+
+static const struct fw_upload_ops axiom_axfw_fw_upload_ops = {
+ .prepare = axiom_axfw_fw_prepare,
+ .write = axiom_axfw_fw_write,
+ .poll_complete = axiom_fw_poll_complete,
+ .cancel = axiom_axfw_fw_cancel,
+ .cleanup = axiom_axfw_fw_cleanup,
+};
+
+static int axiom_set_new_crcs(struct axiom_data *ts,
+ const struct axiom_fw_cfg_chunk *cfg)
+{
+ struct axiom_crc *crc = &ts->crc[AXIOM_CRC_NEW];
+ const u32 *u33_data = (const u32 *)cfg->usage_content;
+ 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[5]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[6]);
+ 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.
+ */
+ if (error) {
+ if (IS_ENABLED(CONFIG_FW_UPLOAD) &&
+ (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);
+ struct i2c_client *client = to_i2c_client(dev);
+
+ if (client->irq && ts->irq_setup_done)
+ disable_irq(client->irq);
+
+ axiom_power_down_device(ts);
+
+ return 0;
+}
+
+static int axiom_runtime_resume(struct device *dev)
+{
+ struct axiom_data *ts = dev_get_drvdata(dev);
+ struct i2c_client *client = to_i2c_client(dev);
+ int error;
+
+ error = axiom_power_up_device(ts);
+ if (error)
+ return error;
+
+ if (client->irq && ts->irq_setup_done)
+ enable_irq(client->irq);
+
+ return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(axiom_pm_ops, axiom_runtime_suspend,
+ axiom_runtime_resume, NULL);
+
+static const struct i2c_device_id axiom_i2c_id_table[] = {
+ { "ax54a" },
+ { },
+};
+MODULE_DEVICE_TABLE(i2c, axiom_i2c_id_table);
+
+static const struct of_device_id axiom_of_match[] = {
+ { .compatible = "touchnetix,ax54a", },
+ { }
+};
+MODULE_DEVICE_TABLE(of, axiom_of_match);
+
+static struct i2c_driver axiom_i2c_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .dev_groups = axiom_groups,
+ .pm = pm_ptr(&axiom_pm_ops),
+ .of_match_table = axiom_of_match,
+ },
+ .id_table = axiom_i2c_id_table,
+ .probe = axiom_i2c_probe,
+ .remove = axiom_i2c_remove,
+};
+module_i2c_driver(axiom_i2c_driver);
+
+MODULE_DESCRIPTION("TouchNetix aXiom touchscreen I2C bus driver");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related
* [PATCH v6 0/4] Input: Add support for TouchNetix aXiom touchscreen
From: Marco Felsch @ 2026-03-03 22:41 UTC (permalink / raw)
To: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, andrew.thomas,
Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
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 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 | 3084 ++++++++++++++++++++
include/linux/firmware.h | 2 +
lib/test_firmware.c | 1 +
9 files changed, 3250 insertions(+)
---
base-commit: 038d61fd642278bab63ee8ef722c50d10ab01e8f
change-id: 20240704-v6-10-topic-touchscreen-axiom-105761e81011
Best regards,
--
Marco Felsch <m.felsch@pengutronix.de>
^ permalink raw reply
* [PATCH v6 2/4] dt-bindings: vendor-prefixes: Add TouchNetix AS
From: Marco Felsch @ 2026-03-03 22:41 UTC (permalink / raw)
To: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, andrew.thomas,
Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel,
Krzysztof Kozlowski, Marco Felsch
In-Reply-To: <20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@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 5d2a7a8d3ac6c666c8b557c2ef385918e5e97bf9..a45b3091f50041cb79a99631c52a1b0c5d6b5f2b 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1567,6 +1567,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 v6 3/4] dt-bindings: input: Add TouchNetix axiom touchscreen
From: Marco Felsch @ 2026-03-03 22:41 UTC (permalink / raw)
To: Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, andrew.thomas,
Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel,
Krzysztof Kozlowski, Marco Felsch
In-Reply-To: <20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@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] xpad: expose xinput capabilities via sysattr
From: Sanjay Govind @ 2026-03-04 1:03 UTC (permalink / raw)
To: Dmitry Torokhov, Vicki Pfau, Mario Limonciello, Sanjay Govind,
Nilton Perim Neto
Cc: Pierre-Loup A. Griffais, linux-input, linux-kernel
Fetch xinput capabilities for x360 wired and wireless
and then expose them via the following attributes:
ATTRS{xpad/flags}=="3"
ATTRS{xpad/gamepad_buttons}=="ffff"
ATTRS{xpad/gamepad_lsx}=="0"
ATTRS{xpad/gamepad_lsy}=="0"
ATTRS{xpad/gamepad_lt}=="3f"
ATTRS{xpad/gamepad_rsx}=="ffc0"
ATTRS{xpad/gamepad_rsy}=="ffc0"
ATTRS{xpad/gamepad_rt}=="ff"
ATTRS{xpad/rumble_l}=="0"
ATTRS{xpad/rumble_r}=="0"
ATTRS{xpad/subtype}=="7"
ATTRS{xpad/type}=="1"
Signed-off-by: Sanjay Govind <sanjay.govind9@gmail.com>
---
drivers/input/joystick/xpad.c | 197 ++++++++++++++++++++++++++++++++--
1 file changed, 188 insertions(+), 9 deletions(-)
diff --git a/drivers/input/joystick/xpad.c b/drivers/input/joystick/xpad.c
index bf4accf3f581..70e4a7c85ab5 100644
--- a/drivers/input/joystick/xpad.c
+++ b/drivers/input/joystick/xpad.c
@@ -94,6 +94,12 @@
#define XTYPE_XBOXONE 3
#define XTYPE_UNKNOWN 4
+#define FLAG_FORCE_FEEDBACK 0x01
+#define FLAG_WIRELESS 0x02
+#define FLAG_VOICE 0x04
+#define FLAG_PLUGIN_MODULES 0x08
+#define FLAG_NO_NAVIGATION 0x10
+
/* Send power-off packet to xpad360w after holding the mode button for this many
* seconds
*/
@@ -747,6 +753,47 @@ static const struct xboxone_init_packet xboxone_init_packets[] = {
XBOXONE_INIT_PKT(0x24c6, 0x543a, xboxone_rumbleend_init),
};
+struct xpad_x360_gamepad_descriptor {
+ u8 bLength;
+ u8 bDescriptorType;
+ u8 reserved;
+ u8 type;
+ u8 subType;
+ u8 reserved2;
+ u8 bEndpointAddressIn;
+ u8 bMaxDataSizeIn;
+ u8 reserved3[5];
+ u8 bEndpointAddressOut;
+ u8 bMaxDataSizeOut;
+ u8 reserved4[2];
+} __packed;
+
+struct x360_capabilities {
+ u8 type;
+ u8 subType;
+ struct {
+ u8 id;
+ u8 rsize;
+ u16 buttons;
+ u8 leftTrigger;
+ u8 rightTrigger;
+ u16 leftThumbX;
+ u16 leftThumbY;
+ u16 rightThumbX;
+ u16 rightThumbY;
+ u8 reserved[4];
+ u16 flags;
+ } gamepad;
+ struct {
+ u8 id;
+ u8 rsize;
+ u8 padding;
+ u16 leftMotorSpeed;
+ u16 rightMotorSpeed;
+ u8 padding2[3];
+ } vibration;
+} __packed;
+
struct xpad_output_packet {
u8 data[XPAD_PKT_LEN];
u8 len;
@@ -795,6 +842,7 @@ struct usb_xpad {
int xtype; /* type of xbox device */
int packet_type; /* type of the extended packet */
int pad_nr; /* the order x360 pads were attached */
+ struct x360_capabilities capabilities; /* capabilities of the device */
const char *name; /* name of the device */
struct work_struct work; /* init/remove device from callback */
time64_t mode_btn_down_ts;
@@ -802,11 +850,63 @@ struct usb_xpad {
bool delayed_init_done;
};
+#define XPAD_SHOW(name, object) \
+static ssize_t name##_show(struct device *dev,\
+ struct device_attribute *attr,\
+ char *buf)\
+{\
+ struct usb_xpad *xpad = input_get_drvdata(to_input_dev(dev));\
+\
+ return sysfs_emit(buf, "%x\n", xpad->capabilities.object);\
+} \
+\
+static DEVICE_ATTR_RO(name)
+
+XPAD_SHOW(subtype, subType);
+XPAD_SHOW(type, type);
+XPAD_SHOW(flags, gamepad.flags);
+XPAD_SHOW(gamepad_buttons, gamepad.buttons);
+XPAD_SHOW(gamepad_lt, gamepad.leftTrigger);
+XPAD_SHOW(gamepad_rt, gamepad.rightTrigger);
+XPAD_SHOW(gamepad_lsx, gamepad.leftThumbX);
+XPAD_SHOW(gamepad_lsy, gamepad.leftThumbY);
+XPAD_SHOW(gamepad_rsx, gamepad.rightThumbX);
+XPAD_SHOW(gamepad_rsy, gamepad.rightThumbY);
+XPAD_SHOW(rumble_l, vibration.leftMotorSpeed);
+XPAD_SHOW(rumble_r, vibration.rightMotorSpeed);
+
+static struct attribute *xpad_attrs[] = {
+ &dev_attr_type.attr,
+ &dev_attr_subtype.attr,
+ &dev_attr_flags.attr,
+ &dev_attr_gamepad_buttons.attr,
+ &dev_attr_gamepad_lt.attr,
+ &dev_attr_gamepad_rt.attr,
+ &dev_attr_gamepad_lsx.attr,
+ &dev_attr_gamepad_lsy.attr,
+ &dev_attr_gamepad_rsx.attr,
+ &dev_attr_gamepad_rsy.attr,
+ &dev_attr_rumble_l.attr,
+ &dev_attr_rumble_r.attr,
+ NULL
+};
+
+static struct attribute_group xpad_group = {
+ .attrs = xpad_attrs,
+ .name = "xpad"
+};
+
+static const struct attribute_group *xpad_groups[] = {
+ &xpad_group,
+ NULL,
+};
+
static int xpad_init_input(struct usb_xpad *xpad);
static void xpad_deinit_input(struct usb_xpad *xpad);
static int xpad_start_input(struct usb_xpad *xpad);
static void xpadone_ack_mode_report(struct usb_xpad *xpad, u8 seq_num);
static void xpad360w_poweroff_controller(struct usb_xpad *xpad);
+static int xpad_inquiry_pad_capabilities(struct usb_xpad *xpad);
/*
* xpad_process_packet
@@ -1032,6 +1132,29 @@ static void xpad360w_process_packet(struct usb_xpad *xpad, u16 cmd, unsigned cha
}
}
+ /* Link report */
+ if (data[0] == 0x00 && data[1] == 0x0F) {
+ xpad->capabilities.subType = data[25] & 0x7f;
+ xpad->capabilities.gamepad.flags = FLAG_WIRELESS;
+ if ((data[25] & 0x80) != 0)
+ xpad->capabilities.gamepad.flags |= FLAG_FORCE_FEEDBACK;
+ xpad_inquiry_pad_capabilities(xpad);
+ }
+
+ /* Capabilities report */
+ if (data[0] == 0x00 && data[1] == 0x05 && data[5] == 0x12) {
+ xpad->capabilities.gamepad.buttons = (data[7] << 8) | data[6];
+ xpad->capabilities.gamepad.leftTrigger = data[8];
+ xpad->capabilities.gamepad.rightTrigger = data[9];
+ xpad->capabilities.gamepad.leftThumbX = (data[11] << 8) | data[10];
+ xpad->capabilities.gamepad.leftThumbY = (data[13] << 8) | data[12];
+ xpad->capabilities.gamepad.rightThumbX = (data[15] << 8) | data[14];
+ xpad->capabilities.gamepad.rightThumbY = (data[17] << 8) | data[16];
+ xpad->capabilities.gamepad.flags |= data[20];
+ xpad->capabilities.vibration.leftMotorSpeed = data[18];
+ xpad->capabilities.vibration.rightMotorSpeed = data[19];
+ }
+
/* Valid pad data */
if (data[1] != 0x1)
return;
@@ -1495,6 +1618,31 @@ static int xpad_inquiry_pad_presence(struct usb_xpad *xpad)
return xpad_try_sending_next_out_packet(xpad);
}
+static int xpad_inquiry_pad_capabilities(struct usb_xpad *xpad)
+{
+ struct xpad_output_packet *packet =
+ &xpad->out_packets[XPAD_OUT_CMD_IDX];
+
+ guard(spinlock_irqsave)(&xpad->odata_lock);
+
+ packet->data[0] = 0x00;
+ packet->data[1] = 0x00;
+ packet->data[2] = 0x02;
+ packet->data[3] = 0x80;
+ packet->data[4] = 0x00;
+ packet->data[5] = 0x00;
+ packet->data[6] = 0x00;
+ packet->data[7] = 0x00;
+ packet->data[8] = 0x00;
+ packet->data[9] = 0x00;
+ packet->data[10] = 0x00;
+ packet->data[11] = 0x00;
+ packet->len = 12;
+ packet->pending = true;
+
+ return xpad_try_sending_next_out_packet(xpad);
+}
+
static int xpad_start_xbox_one(struct usb_xpad *xpad)
{
int error;
@@ -1808,25 +1956,37 @@ static int xpad_start_input(struct usb_xpad *xpad)
}
}
if (xpad->xtype == XTYPE_XBOX360) {
- /*
- * Some third-party controllers Xbox 360-style controllers
- * require this message to finish initialization.
- */
- u8 dummy[20];
-
error = usb_control_msg_recv(xpad->udev, 0,
/* bRequest */ 0x01,
/* bmRequestType */
USB_TYPE_VENDOR | USB_DIR_IN |
- USB_RECIP_INTERFACE,
+ USB_RECIP_INTERFACE,
/* wValue */ 0x100,
/* wIndex */ 0x00,
- dummy, sizeof(dummy),
+ &xpad->capabilities.gamepad,
+ sizeof(xpad->capabilities.gamepad),
25, GFP_KERNEL);
if (error)
dev_warn(&xpad->dev->dev,
- "unable to receive magic message: %d\n",
+ "unable to receive input capabilities: %d\n",
error);
+
+ if (xpad->capabilities.gamepad.flags & FLAG_FORCE_FEEDBACK) {
+ error = usb_control_msg_recv(xpad->udev, 0,
+ /* bRequest */ 0x01,
+ /* bmRequestType */
+ USB_TYPE_VENDOR | USB_DIR_IN |
+ USB_RECIP_INTERFACE,
+ /* wValue */ 0x00,
+ /* wIndex */ 0x00,
+ &xpad->capabilities.vibration,
+ sizeof(xpad->capabilities.vibration),
+ 25, GFP_KERNEL);
+ if (error)
+ dev_warn(&xpad->dev->dev,
+ "unable to receive vibration capabilities: %d\n",
+ error);
+ }
}
return 0;
@@ -1953,6 +2113,7 @@ static void xpad_deinit_input(struct usb_xpad *xpad)
static int xpad_init_input(struct usb_xpad *xpad)
{
struct input_dev *input_dev;
+ struct xpad_x360_gamepad_descriptor *input_desc;
int i, error;
input_dev = input_allocate_device();
@@ -1962,11 +2123,29 @@ static int xpad_init_input(struct usb_xpad *xpad)
xpad->dev = input_dev;
input_dev->name = xpad->name;
input_dev->phys = xpad->phys;
+ xpad->capabilities.subType = 1;
+ xpad->capabilities.type = 1;
+ xpad->capabilities.gamepad.flags = 0;
+ xpad->capabilities.gamepad.buttons = 0xFFFF;
+ xpad->capabilities.gamepad.leftTrigger = 0xFF;
+ xpad->capabilities.gamepad.rightTrigger = 0xFF;
+ xpad->capabilities.gamepad.leftThumbX = 0xFFC0;
+ xpad->capabilities.gamepad.leftThumbY = 0xFFC0;
+ xpad->capabilities.gamepad.rightThumbX = 0xFFC0;
+ xpad->capabilities.gamepad.rightThumbY = 0xFFC0;
+ xpad->dev->dev.groups = xpad_groups;
usb_to_input_id(xpad->udev, &input_dev->id);
if (xpad->xtype == XTYPE_XBOX360W) {
/* x360w controllers and the receiver have different ids */
input_dev->id.product = 0x02a1;
+ xpad->capabilities.gamepad.flags = FLAG_WIRELESS;
+ }
+
+ if (xpad->xtype == XTYPE_XBOX360 &&
+ usb_get_extra_descriptor(xpad->intf->cur_altsetting, 0x21, &input_desc) == 0) {
+ xpad->capabilities.subType = input_desc->subType;
+ xpad->capabilities.type = input_desc->type;
}
input_dev->dev.parent = &xpad->intf->dev;
--
2.53.0
^ permalink raw reply related
* Re: [PATCH] xpad: expose xinput capabilities via sysattr
From: Dmitry Torokhov @ 2026-03-04 1:22 UTC (permalink / raw)
To: Sanjay Govind
Cc: Vicki Pfau, Mario Limonciello, Nilton Perim Neto,
Pierre-Loup A. Griffais, linux-input, linux-kernel
In-Reply-To: <20260304010345.1355896-2-sanjay.govind9@gmail.com>
Hi Sanjay,
On Wed, Mar 04, 2026 at 02:03:43PM +1300, Sanjay Govind wrote:
> Fetch xinput capabilities for x360 wired and wireless
> and then expose them via the following attributes:
>
> ATTRS{xpad/flags}=="3"
> ATTRS{xpad/gamepad_buttons}=="ffff"
> ATTRS{xpad/gamepad_lsx}=="0"
> ATTRS{xpad/gamepad_lsy}=="0"
> ATTRS{xpad/gamepad_lt}=="3f"
> ATTRS{xpad/gamepad_rsx}=="ffc0"
> ATTRS{xpad/gamepad_rsy}=="ffc0"
> ATTRS{xpad/gamepad_rt}=="ff"
> ATTRS{xpad/rumble_l}=="0"
> ATTRS{xpad/rumble_r}=="0"
> ATTRS{xpad/subtype}=="7"
> ATTRS{xpad/type}=="1"
My inclination is "No" as we have other ways to communicate this
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH] xpad: expose xinput capabilities via sysattr
From: Dmitry Torokhov @ 2026-03-04 1:34 UTC (permalink / raw)
To: Sanjay Govind
Cc: Vicki Pfau, Mario Limonciello, Nilton Perim Neto,
Pierre-Loup A. Griffais, linux-input, linux-kernel
In-Reply-To: <CALQgdA0PL82yLxWbjcqchWzsf+bA7_Egq9hZFzz7toL4kByAvw@mail.gmail.com>
On Wed, Mar 04, 2026 at 02:27:42PM +1300, Sanjay Govind wrote:
> I don't really mind how we communicate this data, if there's a better route
> for exposing these capabilities I'd be happy to use that.
Please do not top post.
You need to start with enumerating what data is currently not available
through other means and why it is needed. Is this something that other
gamepad-like devices also lack?
Thanks.
--
Dmitry
^ permalink raw reply
* [PATCH 1/2] HID: quirks: add quirk to always keep device open
From: leo vriska @ 2026-03-04 3:32 UTC (permalink / raw)
To: linux-input; +Cc: leo vriska, Jiri Kosina, Benjamin Tissoires, linux-kernel
Some devices expect the host to open the device shortly after it is
connected. If this does not occur, they may freeze or disconnect. A
quirk allows these devices to function properly without userspace hacks.
The existing hid-axff driver solves this problem for some generic
controllers. This implementation is modelled after that driver, which
still needs to exist for force feedback on the controllers that use it.
Signed-off-by: leo vriska <leo@60228.dev>
---
drivers/hid/hid-generic.c | 23 ++++++++++++++++++++++-
include/linux/hid.h | 2 ++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-generic.c b/drivers/hid/hid-generic.c
index c2de916747de..0595e653b7e7 100644
--- a/drivers/hid/hid-generic.c
+++ b/drivers/hid/hid-generic.c
@@ -67,7 +67,27 @@ static int hid_generic_probe(struct hid_device *hdev,
if (ret)
return ret;
- return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ return ret;
+
+ if (hdev->quirks & HID_QUIRK_KEEP_OPEN) {
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_hw_stop(hdev);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static void hid_generic_remove(struct hid_device *hdev)
+{
+ if (hdev->quirks & HID_QUIRK_KEEP_OPEN)
+ hid_hw_close(hdev);
+
+ hid_hw_stop(hdev);
}
static int hid_generic_reset_resume(struct hid_device *hdev)
@@ -89,6 +109,7 @@ static struct hid_driver hid_generic = {
.id_table = hid_table,
.match = hid_generic_match,
.probe = hid_generic_probe,
+ .remove = hid_generic_remove,
.reset_resume = hid_generic_reset_resume,
};
module_hid_driver(hid_generic);
diff --git a/include/linux/hid.h b/include/linux/hid.h
index 2990b9f94cb5..9d0ab7c217f3 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -388,6 +388,7 @@ struct hid_item {
* | @HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE:
* | @HID_QUIRK_IGNORE_SPECIAL_DRIVER
* | @HID_QUIRK_POWER_ON_AFTER_BACKLIGHT
+ * | @HID_QUIRK_KEEP_OPEN:
* | @HID_QUIRK_FULLSPEED_INTERVAL:
* | @HID_QUIRK_NO_INIT_REPORTS:
* | @HID_QUIRK_NO_IGNORE:
@@ -416,6 +417,7 @@ struct hid_item {
#define HID_QUIRK_NOINVERT BIT(21)
#define HID_QUIRK_IGNORE_SPECIAL_DRIVER BIT(22)
#define HID_QUIRK_POWER_ON_AFTER_BACKLIGHT BIT(23)
+#define HID_QUIRK_KEEP_OPEN BIT(24)
#define HID_QUIRK_FULLSPEED_INTERVAL BIT(28)
#define HID_QUIRK_NO_INIT_REPORTS BIT(29)
#define HID_QUIRK_NO_IGNORE BIT(30)
--
2.53.0
^ permalink raw reply related
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