Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

The SteelSeries Arctis 1 Wireless for Xbox (0x12b6) was labelled as the
plain Arctis 1 Wireless. Rename USB_DEVICE_ID_STEELSERIES_ARCTIS_1 to
USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X, along with the matching quirk flag
and device table entry. The device ID value is unchanged.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-ids.h         |  4 ++--
 drivers/hid/hid-quirks.c      |  2 +-
 drivers/hid/hid-steelseries.c | 10 +++++-----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..915e936cbf8b 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1377,8 +1377,8 @@
 
 #define USB_VENDOR_ID_STEELSERIES	0x1038
 #define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1  0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9  0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X	0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9	0x12c2
 
 #define USB_VENDOR_ID_SUN		0x0430
 #define USB_DEVICE_ID_RARITAN_KVM_DONGLE	0xcdab
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 57d8efdd9b89..f546179858c2 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -747,7 +747,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #endif
 #if IS_ENABLED(CONFIG_HID_STEELSERIES)
 	{ 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_9) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..fd38ee3ea6fc 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -18,7 +18,7 @@
 #include "hid-ids.h"
 
 #define STEELSERIES_SRWS1		BIT(0)
-#define STEELSERIES_ARCTIS_1		BIT(1)
+#define STEELSERIES_ARCTIS_1_X		BIT(1)
 #define STEELSERIES_ARCTIS_9		BIT(2)
 
 struct steelseries_device {
@@ -374,7 +374,7 @@ static void steelseries_headset_fetch_battery(struct hid_device *hdev)
 {
 	int ret = 0;
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1)
+	if (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)
@@ -638,7 +638,7 @@ static int steelseries_headset_raw_event(struct hid_device *hdev,
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
 		return 0;
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
+	if (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 ||
@@ -725,8 +725,8 @@ static const struct hid_device_id steelseries_devices[] = {
 	  .driver_data = STEELSERIES_SRWS1 },
 
 	{ /* SteelSeries Arctis 1 Wireless for XBox */
-	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
-	  .driver_data = STEELSERIES_ARCTIS_1 },
+	  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),
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 02/10] HID: steelseries: Fix whitespace in srws1 report descriptor
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Align the SRW-S1 report descriptor so the comment on each line sits in a
single column. No functional change.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries.c | 164 +++++++++++++++++-----------------
 1 file changed, 82 insertions(+), 82 deletions(-)

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index fd38ee3ea6fc..4c97126a4342 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -37,7 +37,7 @@ struct steelseries_device {
 };
 
 #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))
 #define SRWS1_NUMBER_LEDS 15
 struct steelseries_srws1_data {
 	__u16 led_state;
@@ -54,80 +54,80 @@ struct steelseries_srws1_data {
  */
 
 static const __u8 steelseries_srws1_rdesc_fixed[] = {
-0x05, 0x01,         /*  Usage Page (Desktop)                */
-0x09, 0x08,         /*  Usage (MultiAxis), Changed          */
-0xA1, 0x01,         /*  Collection (Application),           */
-0xA1, 0x02,         /*      Collection (Logical),           */
-0x95, 0x01,         /*          Report Count (1),           */
-0x05, 0x01,         /* Changed  Usage Page (Desktop),       */
-0x09, 0x30,         /* Changed  Usage (X),                  */
-0x16, 0xF8, 0xF8,   /*          Logical Minimum (-1800),    */
-0x26, 0x08, 0x07,   /*          Logical Maximum (1800),     */
-0x65, 0x14,         /*          Unit (Degrees),             */
-0x55, 0x0F,         /*          Unit Exponent (15),         */
-0x75, 0x10,         /*          Report Size (16),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x31,         /* Changed  Usage (Y),                  */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x26, 0xFF, 0x03,   /*          Logical Maximum (1023),     */
-0x75, 0x0C,         /*          Report Size (12),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x32,         /* Changed  Usage (Z),                  */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x26, 0xFF, 0x03,   /*          Logical Maximum (1023),     */
-0x75, 0x0C,         /*          Report Size (12),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x05, 0x01,         /*          Usage Page (Desktop),       */
-0x09, 0x39,         /*          Usage (Hat Switch),         */
-0x25, 0x07,         /*          Logical Maximum (7),        */
-0x35, 0x00,         /*          Physical Minimum (0),       */
-0x46, 0x3B, 0x01,   /*          Physical Maximum (315),     */
-0x65, 0x14,         /*          Unit (Degrees),             */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x01,         /*          Report Count (1),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x25, 0x01,         /*          Logical Maximum (1),        */
-0x45, 0x01,         /*          Physical Maximum (1),       */
-0x65, 0x00,         /*          Unit,                       */
-0x75, 0x01,         /*          Report Size (1),            */
-0x95, 0x03,         /*          Report Count (3),           */
-0x81, 0x01,         /*          Input (Constant),           */
-0x05, 0x09,         /*          Usage Page (Button),        */
-0x19, 0x01,         /*          Usage Minimum (01h),        */
-0x29, 0x11,         /*          Usage Maximum (11h),        */
-0x95, 0x11,         /*          Report Count (17),          */
-0x81, 0x02,         /*          Input (Variable),           */
-                    /*   ---- Dial patch starts here ----   */
-0x05, 0x01,         /*          Usage Page (Desktop),       */
-0x09, 0x33,         /*          Usage (RX),                 */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x02,         /*          Report Count (2),           */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x25, 0x0b,         /*          Logical Maximum (b),        */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x35,         /*          Usage (RZ),                 */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x01,         /*          Report Count (1),           */
-0x25, 0x03,         /*          Logical Maximum (3),        */
-0x81, 0x02,         /*          Input (Variable),           */
-                    /*    ---- Dial patch ends here ----    */
-0x06, 0x00, 0xFF,   /*          Usage Page (FF00h),         */
-0x09, 0x01,         /*          Usage (01h),                */
-0x75, 0x04,         /* Changed  Report Size (4),            */
-0x95, 0x0D,         /* Changed  Report Count (13),          */
-0x81, 0x02,         /*          Input (Variable),           */
-0xC0,               /*      End Collection,                 */
-0xA1, 0x02,         /*      Collection (Logical),           */
-0x09, 0x02,         /*          Usage (02h),                */
-0x75, 0x08,         /*          Report Size (8),            */
-0x95, 0x10,         /*          Report Count (16),          */
-0x91, 0x02,         /*          Output (Variable),          */
-0xC0,               /*      End Collection,                 */
-0xC0                /*  End Collection                      */
+0x05, 0x01,					/*  Usage Page (Desktop)                */
+0x09, 0x08,					/*  Usage (MultiAxis), Changed          */
+0xA1, 0x01,					/*  Collection (Application),           */
+0xA1, 0x02,					/*      Collection (Logical),           */
+0x95, 0x01,					/*          Report Count (1),           */
+0x05, 0x01,				/* Changed  Usage Page (Desktop),       */
+0x09, 0x30,				/* Changed  Usage (X),                  */
+0x16, 0xF8, 0xF8,	/*          Logical Minimum (-1800),    */
+0x26, 0x08, 0x07,	/*          Logical Maximum (1800),     */
+0x65, 0x14,				/*          Unit (Degrees),             */
+0x55, 0x0F,				/*          Unit Exponent (15),         */
+0x75, 0x10,				/*          Report Size (16),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x31,				/* Changed  Usage (Y),                  */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x26, 0xFF, 0x03,	/*          Logical Maximum (1023),     */
+0x75, 0x0C,				/*          Report Size (12),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x32,				/* Changed  Usage (Z),                  */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x26, 0xFF, 0x03,	/*          Logical Maximum (1023),     */
+0x75, 0x0C,				/*          Report Size (12),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x05, 0x01,				/*          Usage Page (Desktop),       */
+0x09, 0x39,				/*          Usage (Hat Switch),         */
+0x25, 0x07,				/*          Logical Maximum (7),        */
+0x35, 0x00,				/*          Physical Minimum (0),       */
+0x46, 0x3B, 0x01,	/*          Physical Maximum (315),     */
+0x65, 0x14,				/*          Unit (Degrees),             */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x01,				/*          Report Count (1),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x25, 0x01,				/*          Logical Maximum (1),        */
+0x45, 0x01,				/*          Physical Maximum (1),       */
+0x65, 0x00,				/*          Unit,                       */
+0x75, 0x01,				/*          Report Size (1),            */
+0x95, 0x03,				/*          Report Count (3),           */
+0x81, 0x01,				/*          Input (Constant),           */
+0x05, 0x09,				/*          Usage Page (Button),        */
+0x19, 0x01,				/*          Usage Minimum (01h),        */
+0x29, 0x11,				/*          Usage Maximum (11h),        */
+0x95, 0x11,				/*          Report Count (17),          */
+0x81, 0x02,				/*          Input (Variable),           */
+									/*   ---- Dial patch starts here ----   */
+0x05, 0x01,				/*          Usage Page (Desktop),       */
+0x09, 0x33,				/*          Usage (RX),                 */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x02,				/*          Report Count (2),           */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x25, 0x0b,				/*          Logical Maximum (b),        */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x35,				/*          Usage (RZ),                 */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x01,				/*          Report Count (1),           */
+0x25, 0x03,				/*          Logical Maximum (3),        */
+0x81, 0x02,				/*          Input (Variable),           */
+									/*    ---- Dial patch ends here ----    */
+0x06, 0x00, 0xFF,	/*          Usage Page (FF00h),         */
+0x09, 0x01,				/*          Usage (01h),                */
+0x75, 0x04,				/* Changed  Report Size (4),            */
+0x95, 0x0D,				/* Changed  Report Count (13),          */
+0x81, 0x02,				/*          Input (Variable),           */
+0xC0,							/*      End Collection,                 */
+0xA1, 0x02,				/*      Collection (Logical),           */
+0x09, 0x02,				/*          Usage (02h),                */
+0x75, 0x08,				/*          Report Size (8),            */
+0x95, 0x10,				/*          Report Count (16),          */
+0x91, 0x02,				/*          Output (Variable),          */
+0xC0,							/*      End Collection,                 */
+0xC0							/*  End Collection                      */
 };
 
 #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))
 static void steelseries_srws1_set_leds(struct hid_device *hdev, __u16 leds)
 {
 	struct list_head *report_list = &hdev->report_enum[HID_OUTPUT_REPORT].report_list;
@@ -489,7 +489,7 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
 	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,
-						    "steelseries_headset_battery_%ld", n);
+							"steelseries_headset_battery_%ld", n);
 	if (!sd->battery_desc.name)
 		return -ENOMEM;
 
@@ -535,7 +535,7 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
 
 	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;
@@ -581,7 +581,7 @@ static void steelseries_remove(struct hid_device *hdev)
 
 	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;
@@ -603,7 +603,7 @@ 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)
+		hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
 		return rdesc;
 
 	if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
@@ -642,7 +642,7 @@ static int steelseries_headset_raw_event(struct hid_device *hdev,
 		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))) {
+			memcmp(read_buf, arctis_1_battery_request, sizeof(arctis_1_battery_request))) {
 			if (!delayed_work_pending(&sd->battery_work))
 				goto request_battery;
 			return 0;
@@ -722,15 +722,15 @@ static int steelseries_headset_raw_event(struct hid_device *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 },
+		.driver_data = STEELSERIES_SRWS1 },
 
 	{ /* 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 },
+		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_ARCTIS_9),
+		.driver_data = STEELSERIES_ARCTIS_9 },
 
 	{ }
 };
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 04/10] HID: steelseries: Inline and simplify SRWS1 wheel driver
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Arctis headsets have their own driver now, so hid-steelseries.c only
handles the SRWS1 racing wheel. Remove the dispatch layer:

- Inline steelseries_srws1_probe() as steelseries_probe()
- Add a steelseries_remove() that calls hid_hw_stop()
- Drop the STEELSERIES_SRWS1 quirk bit, no longer needed
- Remove the vendor/product check in steelseries_srws1_report_fixup(),
  since this driver only binds the SRWS1

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries.c | 30 +++++++++---------------------
 1 file changed, 9 insertions(+), 21 deletions(-)

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 984b13999d28..9a7047fbd6dd 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -15,8 +15,6 @@
 
 #include "hid-ids.h"
 
-#define STEELSERIES_SRWS1		BIT(0)
-
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 #define SRWS1_NUMBER_LEDS 15
@@ -225,7 +223,7 @@ static enum led_brightness steelseries_srws1_led_get_brightness(struct led_class
 	return value ? LED_FULL : LED_OFF;
 }
 
-static int steelseries_srws1_probe(struct hid_device *hdev,
+static int steelseries_probe(struct hid_device *hdev,
 		const struct hid_device_id *id)
 {
 	int ret, i;
@@ -319,33 +317,24 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
 err:
 	return ret;
 }
-#endif
 
-static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static void steelseries_remove(struct hid_device *hdev)
 {
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-	return steelseries_srws1_probe(hdev, id);
+	hid_hw_stop(hdev);
+}
 #else
+static int steelseries_probe(struct hid_device *hdev,
+		const struct hid_device_id *id)
+{
 	return -ENODEV;
-#endif
 }
 
-static void steelseries_remove(struct hid_device *hdev)
-{
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-	hid_hw_stop(hdev);
+static void steelseries_remove(struct hid_device *hdev) {}
 #endif
-}
 
 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");
@@ -356,8 +345,7 @@ static const __u8 *steelseries_srws1_report_fixup(struct hid_device *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 },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
 
 	{ }
 };
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 03/10] HID: steelseries: Split Arctis headset driver into separate module
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Move all Arctis headset code (battery monitoring, wireless status,
power supply registration, raw event handling) from hid-steelseries.c
into the new hid-steelseries-arctis.c driver module.

hid-steelseries.c now handles only the SRWS1 racing wheel, while
hid-steelseries-arctis.c handles the Arctis 1 (Xbox) and Arctis 9
wireless headsets with their own device table, probe, remove, and
raw_event implementations.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/Makefile                 |   2 +-
 drivers/hid/hid-steelseries-arctis.c | 404 +++++++++++++++++++++++++++
 drivers/hid/hid-steelseries.c        | 386 +------------------------
 3 files changed, 410 insertions(+), 382 deletions(-)
 create mode 100644 drivers/hid/hid-steelseries-arctis.c

diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 23e6e3dd0c56..4a172bd27b11 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -134,7 +134,7 @@ obj-$(CONFIG_HID_SMARTJOYPLUS)	+= hid-sjoy.o
 obj-$(CONFIG_HID_SONY)		+= hid-sony.o
 obj-$(CONFIG_HID_SPEEDLINK)	+= hid-speedlink.o
 obj-$(CONFIG_HID_STEAM)		+= hid-steam.o
-obj-$(CONFIG_HID_STEELSERIES)	+= hid-steelseries.o
+obj-$(CONFIG_HID_STEELSERIES)	+= hid-steelseries.o hid-steelseries-arctis.o
 obj-$(CONFIG_HID_SUNPLUS)	+= hid-sunplus.o
 obj-$(CONFIG_HID_GREENASIA)	+= hid-gaff.o
 obj-$(CONFIG_HID_THRUSTMASTER)	+= hid-tmff.o hid-thrustmaster.o
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
new file mode 100644
index 000000000000..079504e6932a
--- /dev/null
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Steelseries arctis headsets
+ *
+ *  Copyright (c) 2023 Bastien Nocera
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define STEELSERIES_ARCTIS_1_X		BIT(0)
+#define STEELSERIES_ARCTIS_9		BIT(1)
+
+struct steelseries_device {
+	struct hid_device *hdev;
+	unsigned long quirks;
+
+	struct delayed_work battery_work;
+	spinlock_t lock;
+	bool removed;
+
+	struct power_supply_desc battery_desc;
+	struct power_supply *battery;
+	uint8_t battery_capacity;
+	bool headset_connected;
+	bool battery_charging;
+};
+
+#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
+
+#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 };
+
+static int steelseries_headset_request_battery(struct hid_device *hdev,
+	const char *request, size_t len)
+{
+	u8 *write_buf;
+	int ret;
+
+	/* Request battery information */
+	write_buf = kmemdup(request, len, GFP_KERNEL);
+	if (!write_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;
+	}
+
+	kfree(write_buf);
+	return ret;
+}
+
+static void steelseries_headset_fetch_battery(struct hid_device *hdev)
+{
+	int ret = 0;
+
+	if (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));
+
+	if (ret < 0)
+		hid_dbg(hdev,
+			"Battery query failed (err: %d)\n", ret);
+}
+
+static int battery_capacity_to_level(int capacity)
+{
+	if (capacity >= 50)
+		return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
+	if (capacity >= 20)
+		return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+	return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+}
+
+static void steelseries_headset_battery_timer_tick(struct work_struct *work)
+{
+	struct steelseries_device *sd = container_of(work,
+		struct steelseries_device, battery_work.work);
+	struct hid_device *hdev = sd->hdev;
+
+	steelseries_headset_fetch_battery(hdev);
+}
+
+#define STEELSERIES_PREFIX "SteelSeries "
+#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
+
+static int steelseries_headset_battery_get_property(struct power_supply *psy,
+				enum power_supply_property psp,
+				union power_supply_propval *val)
+{
+	struct steelseries_device *sd = power_supply_get_drvdata(psy);
+	int ret = 0;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_MODEL_NAME:
+		val->strval = sd->hdev->name;
+		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
+			val->strval += STEELSERIES_PREFIX_LEN;
+		break;
+	case POWER_SUPPLY_PROP_MANUFACTURER:
+		val->strval = "SteelSeries";
+		break;
+	case POWER_SUPPLY_PROP_PRESENT:
+		val->intval = 1;
+		break;
+	case POWER_SUPPLY_PROP_STATUS:
+		if (sd->headset_connected) {
+			val->intval = sd->battery_charging ?
+				POWER_SUPPLY_STATUS_CHARGING :
+				POWER_SUPPLY_STATUS_DISCHARGING;
+		} else
+			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		break;
+	case POWER_SUPPLY_PROP_SCOPE:
+		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY:
+		val->intval = sd->battery_capacity;
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+		val->intval = battery_capacity_to_level(sd->battery_capacity);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+	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[] = {
+	POWER_SUPPLY_PROP_MODEL_NAME,
+	POWER_SUPPLY_PROP_MANUFACTURER,
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_SCOPE,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+};
+
+static int steelseries_headset_battery_register(struct steelseries_device *sd)
+{
+	static atomic_t battery_no = ATOMIC_INIT(0);
+	struct power_supply_config battery_cfg = { .drv_data = sd, };
+	unsigned long n;
+	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.use_for_apm = 0;
+	n = atomic_inc_return(&battery_no) - 1;
+	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
+							"steelseries_headset_battery_%ld", n);
+	if (!sd->battery_desc.name)
+		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->battery = devm_power_supply_register(&sd->hdev->dev,
+			&sd->battery_desc, &battery_cfg);
+	if (IS_ERR(sd->battery)) {
+		ret = PTR_ERR(sd->battery);
+		hid_err(sd->hdev,
+				"%s:power_supply_register failed with error %d\n",
+				__func__, ret);
+		return ret;
+	}
+	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);
+
+	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));
+	}
+
+	return 0;
+}
+
+static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+{
+	return hdev->rdesc[0] == 0x06 &&
+		hdev->rdesc[1] == usage_page &&
+		hdev->rdesc[2] == 0xff;
+}
+
+static int steelseries_arctis_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct steelseries_device *sd;
+	int ret;
+
+	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;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return ret;
+
+	if (sd->quirks & STEELSERIES_ARCTIS_9 &&
+			!steelseries_is_vendor_usage_page(hdev, 0xc0))
+		return -ENODEV;
+
+	spin_lock_init(&sd->lock);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret)
+		return ret;
+
+	ret = hid_hw_open(hdev);
+	if (ret)
+		return ret;
+
+	if (steelseries_headset_battery_register(sd) < 0)
+		hid_err(sd->hdev,
+			"Failed to register battery for headset\n");
+
+	return ret;
+}
+
+static void steelseries_arctis_remove(struct hid_device *hdev)
+{
+	struct steelseries_device *sd;
+	unsigned long flags;
+
+	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 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_arctis_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;
+
+	if (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 (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 (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;
+		}
+	}
+
+	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);
+	}
+
+	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);
+	}
+
+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;
+}
+
+static const struct hid_device_id steelseries_arctis_devices[] = {
+	{ /* 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 },
+
+	{ }
+};
+MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
+
+static struct hid_driver steelseries_arctis_driver = {
+	.name = "hid-steelseries-arctis",
+	.id_table = steelseries_arctis_devices,
+	.probe = steelseries_arctis_probe,
+	.remove = steelseries_arctis_remove,
+	.raw_event = steelseries_arctis_raw_event,
+};
+
+module_hid_driver(steelseries_arctis_driver);
+MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 4c97126a4342..984b13999d28 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -3,7 +3,6 @@
  *  HID driver for Steelseries devices
  *
  *  Copyright (c) 2013 Simon Wood
- *  Copyright (c) 2023 Bastien Nocera
  */
 
 /*
@@ -12,29 +11,11 @@
 #include <linux/device.h>
 #include <linux/hid.h>
 #include <linux/module.h>
-#include <linux/usb.h>
 #include <linux/leds.h>
 
 #include "hid-ids.h"
 
 #define STEELSERIES_SRWS1		BIT(0)
-#define STEELSERIES_ARCTIS_1_X		BIT(1)
-#define STEELSERIES_ARCTIS_9		BIT(2)
-
-struct steelseries_device {
-	struct hid_device *hdev;
-	unsigned long quirks;
-
-	struct delayed_work battery_work;
-	spinlock_t lock;
-	bool removed;
-
-	struct power_supply_desc battery_desc;
-	struct power_supply *battery;
-	uint8_t battery_capacity;
-	bool headset_connected;
-	bool battery_charging;
-};
 
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
@@ -97,7 +78,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 +91,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),            */
@@ -340,263 +321,22 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
 }
 #endif
 
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
-
-#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 };
-
-static int steelseries_headset_request_battery(struct hid_device *hdev,
-	const char *request, size_t len)
-{
-	u8 *write_buf;
-	int ret;
-
-	/* Request battery information */
-	write_buf = kmemdup(request, len, GFP_KERNEL);
-	if (!write_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;
-	}
-
-	kfree(write_buf);
-	return ret;
-}
-
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
-{
-	int ret = 0;
-
-	if (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));
-
-	if (ret < 0)
-		hid_dbg(hdev,
-			"Battery query failed (err: %d)\n", ret);
-}
-
-static int battery_capacity_to_level(int capacity)
-{
-	if (capacity >= 50)
-		return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
-	if (capacity >= 20)
-		return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
-	return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
-}
-
-static void steelseries_headset_battery_timer_tick(struct work_struct *work)
-{
-	struct steelseries_device *sd = container_of(work,
-		struct steelseries_device, battery_work.work);
-	struct hid_device *hdev = sd->hdev;
-
-	steelseries_headset_fetch_battery(hdev);
-}
-
-#define STEELSERIES_PREFIX "SteelSeries "
-#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
-
-static int steelseries_headset_battery_get_property(struct power_supply *psy,
-				enum power_supply_property psp,
-				union power_supply_propval *val)
-{
-	struct steelseries_device *sd = power_supply_get_drvdata(psy);
-	int ret = 0;
-
-	switch (psp) {
-	case POWER_SUPPLY_PROP_MODEL_NAME:
-		val->strval = sd->hdev->name;
-		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
-			val->strval += STEELSERIES_PREFIX_LEN;
-		break;
-	case POWER_SUPPLY_PROP_MANUFACTURER:
-		val->strval = "SteelSeries";
-		break;
-	case POWER_SUPPLY_PROP_PRESENT:
-		val->intval = 1;
-		break;
-	case POWER_SUPPLY_PROP_STATUS:
-		if (sd->headset_connected) {
-			val->intval = sd->battery_charging ?
-				POWER_SUPPLY_STATUS_CHARGING :
-				POWER_SUPPLY_STATUS_DISCHARGING;
-		} else
-			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
-		break;
-	case POWER_SUPPLY_PROP_SCOPE:
-		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
-		break;
-	case POWER_SUPPLY_PROP_CAPACITY:
-		val->intval = sd->battery_capacity;
-		break;
-	case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
-		val->intval = battery_capacity_to_level(sd->battery_capacity);
-		break;
-	default:
-		ret = -EINVAL;
-		break;
-	}
-	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[] = {
-	POWER_SUPPLY_PROP_MODEL_NAME,
-	POWER_SUPPLY_PROP_MANUFACTURER,
-	POWER_SUPPLY_PROP_PRESENT,
-	POWER_SUPPLY_PROP_STATUS,
-	POWER_SUPPLY_PROP_SCOPE,
-	POWER_SUPPLY_PROP_CAPACITY,
-	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
-};
-
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
-{
-	static atomic_t battery_no = ATOMIC_INIT(0);
-	struct power_supply_config battery_cfg = { .drv_data = sd, };
-	unsigned long n;
-	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.use_for_apm = 0;
-	n = atomic_inc_return(&battery_no) - 1;
-	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
-							"steelseries_headset_battery_%ld", n);
-	if (!sd->battery_desc.name)
-		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->battery = devm_power_supply_register(&sd->hdev->dev,
-			&sd->battery_desc, &battery_cfg);
-	if (IS_ERR(sd->battery)) {
-		ret = PTR_ERR(sd->battery);
-		hid_err(sd->hdev,
-				"%s:power_supply_register failed with error %d\n",
-				__func__, ret);
-		return ret;
-	}
-	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);
-
-	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));
-	}
-
-	return 0;
-}
-
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
-{
-	return hdev->rdesc[0] == 0x06 &&
-		hdev->rdesc[1] == usage_page &&
-		hdev->rdesc[2] == 0xff;
-}
-
 static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
 {
-	struct steelseries_device *sd;
-	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))
-		return steelseries_srws1_probe(hdev, id);
+	return steelseries_srws1_probe(hdev, id);
 #else
-		return -ENODEV;
+	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;
-
-	ret = hid_parse(hdev);
-	if (ret)
-		return ret;
-
-	if (sd->quirks & STEELSERIES_ARCTIS_9 &&
-			!steelseries_is_vendor_usage_page(hdev, 0xc0))
-		return -ENODEV;
-
-	spin_lock_init(&sd->lock);
-
-	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
-	if (ret)
-		return ret;
-
-	ret = hid_hw_open(hdev);
-	if (ret)
-		return ret;
-
-	if (steelseries_headset_battery_register(sd) < 0)
-		hid_err(sd->hdev,
-			"Failed to register battery for headset\n");
-
-	return ret;
 }
 
 static void steelseries_remove(struct hid_device *hdev)
 {
-	struct steelseries_device *sd;
-	unsigned long flags;
-
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(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);
+#endif
 }
 
 static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
@@ -615,123 +355,10 @@ static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
 	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;
-
-	if (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 (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 (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;
-		}
-	}
-
-	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);
-	}
-
-	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);
-	}
-
-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;
-}
-
 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 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 },
-
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, steelseries_devices);
@@ -742,12 +369,9 @@ static struct hid_driver steelseries_driver = {
 	.probe = steelseries_probe,
 	.remove = steelseries_remove,
 	.report_fixup = steelseries_srws1_report_fixup,
-	.raw_event = steelseries_headset_raw_event,
 };
 
 module_hid_driver(steelseries_driver);
 MODULE_DESCRIPTION("HID driver for Steelseries devices");
 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>");
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Replace the per-product if/else quirk bitmap with a
steelseries_device_info struct. Each model provides its capabilities,
sync_interface, and request_status/parse_status callbacks. Report
sending is folded into steelseries_send_report() and the feature and
output wrappers, and the battery identifiers lose their per-model names.

This is mostly a refactor, but it changes two things:

  - Battery status is polled from a periodic delayed work (status_work)
    instead of being requested from raw_event(). The interval stays at
    3s.
  - Arctis 1 no longer clamps the reported capacity to 100% while
    disconnected. The connection state already controls how this is
    shown to userspace.

ARCTIS_1_X and ARCTIS_9 keep working. No new devices are added.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 469 +++++++++++++++------------
 1 file changed, 268 insertions(+), 201 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 079504e6932a..f00f4c5e6d9e 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -3,6 +3,7 @@
  *  HID driver for Steelseries arctis headsets
  *
  *  Copyright (c) 2023 Bastien Nocera
+ *  Copyright (c) 2026 Sriman Achanta
  */
 
 #include <linux/device.h>
@@ -15,70 +16,95 @@
 
 #include "hid-ids.h"
 
-#define STEELSERIES_ARCTIS_1_X		BIT(0)
-#define STEELSERIES_ARCTIS_9		BIT(1)
+#define SS_CAP_BATTERY			BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+	unsigned long capabilities;
+
+	u8 sync_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;
+	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;
-};
 
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
+	spinlock_t lock;
+	bool removed;
+};
 
-#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 };
+/*
+ * Headset report helpers
+ */
 
-static int steelseries_headset_request_battery(struct hid_device *hdev,
-	const char *request, size_t len)
+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);
+}
+
+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 (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));
+/*
+ * Headset status request functions
+ */
 
-	if (ret < 0)
-		hid_dbg(hdev,
-			"Battery query failed (err: %d)\n", ret);
+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_9_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x00, 0x20 };
+
+	return steelseries_send_feature_report(hdev, data, sizeof(data));
 }
 
+/*
+ * Headset battery helpers
+ */
+
 static int battery_capacity_to_level(int capacity)
 {
 	if (capacity >= 50)
@@ -88,30 +114,96 @@ 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_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], 0x68, 0x9d);
+	}
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info arctis_1_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.request_status = steelseries_arctis_1_request_status,
+	.parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+	.sync_interface = 0,
+	.capabilities = SS_CAP_BATTERY,
+	.request_status = steelseries_arctis_9_request_status,
+	.parse_status = steelseries_arctis_9_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)
 {
 	struct steelseries_device *sd = power_supply_get_drvdata(psy);
+	size_t prefix_len;
 	int ret = 0;
 
 	switch (psp) {
 	case POWER_SUPPLY_PROP_MODEL_NAME:
 		val->strval = sd->hdev->name;
-		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
-			val->strval += STEELSERIES_PREFIX_LEN;
+		while ((prefix_len = str_has_prefix(val->strval, STEELSERIES_PREFIX)))
+			val->strval += prefix_len;
 		break;
 	case POWER_SUPPLY_PROP_MANUFACTURER:
 		val->strval = "SteelSeries";
@@ -120,12 +212,12 @@ static int steelseries_headset_battery_get_property(struct power_supply *psy,
 		val->intval = 1;
 		break;
 	case POWER_SUPPLY_PROP_STATUS:
-		if (sd->headset_connected) {
-			val->intval = sd->battery_charging ?
-				POWER_SUPPLY_STATUS_CHARGING :
-				POWER_SUPPLY_STATUS_DISCHARGING;
-		} else
+		if (!sd->headset_connected)
 			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		else if (sd->battery_charging)
+			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else
+			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
 		break;
 	case POWER_SUPPLY_PROP_SCOPE:
 		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
@@ -143,22 +235,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,
@@ -168,7 +245,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
+ */
+
+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)
+		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, };
@@ -176,25 +272,27 @@ 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,
-							"steelseries_headset_battery_%ld", n);
+						"steelseries_headset_battery_%ld", n);
 	if (!sd->battery_desc.name)
 		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);
@@ -202,59 +300,65 @@ 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);
-
-	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));
-	}
-
 	return 0;
 }
 
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
-{
-	return hdev->rdesc[0] == 0x06 &&
-		hdev->rdesc[1] == usage_page &&
-		hdev->rdesc[2] == 0xff;
-}
-
-static int steelseries_arctis_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static int steelseries_arctis_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;
+	u8 interface_num;
 	int ret;
 
-	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-sync interfaces */
+	if (interface_num != info->sync_interface)
+		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
+	sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+
+	sd->hdev = hdev;
+	sd->info = info;
 	spin_lock_init(&sd->lock);
 
+	hid_set_drvdata(hdev, sd);
+
 	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 	if (ret)
 		return ret;
 
 	ret = hid_hw_open(hdev);
 	if (ret)
-		return ret;
+		goto err_stop;
 
-	if (steelseries_headset_battery_register(sd) < 0)
-		hid_err(sd->hdev,
-			"Failed to register battery for headset\n");
+	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;
 
+err_stop:
+	hid_hw_stop(hdev);
 	return ret;
 }
 
@@ -262,130 +366,92 @@ static void steelseries_arctis_remove(struct hid_device *hdev)
 {
 	struct steelseries_device *sd;
 	unsigned long flags;
+	struct usb_interface *intf;
+	u8 interface_num;
+
+	if (hid_is_usb(hdev)) {
+		intf = to_usb_interface(hdev->dev.parent);
+		interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+	} else {
+		return;
+	}
 
 	sd = hid_get_drvdata(hdev);
 
-	spin_lock_irqsave(&sd->lock, flags);
-	sd->removed = true;
-	spin_unlock_irqrestore(&sd->lock, flags);
+	if (!sd) {
+		hid_hw_stop(hdev);
+		return;
+	}
+
+	if (interface_num == sd->info->sync_interface) {
+		spin_lock_irqsave(&sd->lock, flags);
+		sd->removed = true;
+		spin_unlock_irqrestore(&sd->lock, flags);
 
-	cancel_delayed_work_sync(&sd->battery_work);
+		cancel_delayed_work_sync(&sd->status_work);
+	}
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
 }
 
-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_arctis_raw_event(struct hid_device *hdev,
-					struct hid_report *report, u8 *read_buf,
-					int size)
+				 struct hid_report *report, u8 *data, 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;
+	u8 old_capacity;
+	bool old_connected;
+	bool old_charging;
 
-	if (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)
+		return 0;
 
-	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;
-		}
+	old_capacity = sd->battery_capacity;
+	old_connected = sd->headset_connected;
+	old_charging = sd->battery_charging;
 
-		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;
-		}
-	}
+	sd->info->parse_status(sd, data, size);
 
-	if (connected != sd->headset_connected) {
-		hid_dbg(sd->hdev,
+	if (sd->headset_connected != old_connected) {
+		hid_dbg(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);
+			old_connected ? "" : "not ",
+			sd->headset_connected ? "" : "not ");
+
+		if (sd->battery) {
+			steelseries_headset_set_wireless_status(sd->hdev,
+							       sd->headset_connected);
+			power_supply_changed(sd->battery);
+		}
 	}
 
-	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 (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 (charging != sd->battery_charging) {
-		hid_dbg(sd->hdev,
+	if (sd->battery_charging != old_charging) {
+		hid_dbg(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);
+			old_charging ? "" : "not ",
+			sd->battery_charging ? "" : "not ");
+		if (sd->battery)
+			power_supply_changed(sd->battery);
 	}
 
-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;
 }
 
 static const struct hid_device_id steelseries_arctis_devices[] = {
-	{ /* 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_ARCTIS_1_X),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+	  .driver_data = (unsigned long)&arctis_9_info },
+	{}
 };
 MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
 
@@ -402,3 +468,4 @@ MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
 MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 06/10] HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Report POWER_SUPPLY_STATUS_FULL when the headset is connected, not
charging, and at 100% capacity. It reported DISCHARGING in that case
before.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index f00f4c5e6d9e..4be586db0004 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -216,6 +216,8 @@ static int steelseries_battery_get_property(struct power_supply *psy,
 			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
 		else if (sd->battery_charging)
 			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else if (sd->battery_capacity >= 100)
+			val->intval = POWER_SUPPLY_STATUS_FULL;
 		else
 			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
 		break;
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 07/10] HID: steelseries: Correct Arctis 9 battery calibration range
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Map the Arctis 9 raw battery value over 0x64 (empty) to 0x9a (full)
instead of 0x68 to 0x9d. These values match the HeadsetControl project
[1] and fit the calibration points from an independent reverse
engineering of the battery tray (about 25% at raw 112, 50% at raw 125)
[2].

I do not have this headset. The values come from those references and
were not measured directly.

[1] https://github.com/Sapd/HeadsetControl/blob/master/lib/devices/steelseries_arctis_9.hpp
[2] https://magnier.io/reverse-engineering-arctis-9-battery-tray/

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 4be586db0004..1f0e9cb5138f 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -146,7 +146,7 @@ static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
 	if (data[0] == 0xaa) {
 		sd->headset_connected = (data[1] == 0x01);
 		sd->battery_charging = (data[4] == 0x01);
-		sd->battery_capacity = steelseries_map_capacity(data[3], 0x68, 0x9d);
+		sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9a);
 	}
 }
 
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

The next change shares one steelseries_device between two HID
interfaces, so the state can outlive either interface. Stop using devm
for it. Reference count the struct with a kref and free it from
steelseries_device_release(). Register and unregister the power supply
explicitly, and clear sd->battery under sd->lock in remove() so it is
not touched after it is unregistered.

Drop the global atomic battery counter and name the power supply after
the device (hdev->uniq, or dev_name() when empty), as hid-input and the
other HID battery drivers do.

No functional change for the current single-interface devices.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 36 ++++++++++++++++++++++------
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 1f0e9cb5138f..734cf1eb8789 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -8,6 +8,8 @@
 
 #include <linux/device.h>
 #include <linux/hid.h>
+#include <linux/kref.h>
+#include <linux/slab.h>
 #include <linux/module.h>
 #include <linux/power_supply.h>
 #include <linux/spinlock.h>
@@ -30,6 +32,8 @@ struct steelseries_device_info {
 };
 
 struct steelseries_device {
+	struct kref refcnt;
+
 	struct hid_device *hdev;
 	const struct steelseries_device_info *info;
 
@@ -45,6 +49,14 @@ struct steelseries_device {
 	bool removed;
 };
 
+static void steelseries_device_release(struct kref *ref)
+{
+	struct steelseries_device *sd =
+		container_of(ref, struct steelseries_device, refcnt);
+
+	kfree(sd);
+}
+
 /*
  * Headset report helpers
  */
@@ -268,9 +280,7 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
 
 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, };
-	unsigned long n;
 	int ret;
 
 	sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
@@ -278,9 +288,10 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	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,
-						"steelseries_headset_battery_%ld", n);
+					       "steelseries_headset_battery_%s",
+					       sd->hdev->uniq[0] ? sd->hdev->uniq :
+					       dev_name(&sd->hdev->dev));
 	if (!sd->battery_desc.name)
 		return -ENOMEM;
 
@@ -290,7 +301,7 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	sd->headset_connected = false;
 	steelseries_headset_set_wireless_status(sd->hdev, false);
 
-	sd->battery = devm_power_supply_register(&sd->hdev->dev,
+	sd->battery = power_supply_register(&sd->hdev->dev,
 			&sd->battery_desc, &battery_cfg);
 	if (IS_ERR(sd->battery)) {
 		ret = PTR_ERR(sd->battery);
@@ -330,10 +341,11 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 	if (interface_num != info->sync_interface)
 		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
-	sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+	sd = kzalloc_obj(*sd, GFP_KERNEL);
 	if (!sd)
 		return -ENOMEM;
 
+	kref_init(&sd->refcnt);
 	sd->hdev = hdev;
 	sd->info = info;
 	spin_lock_init(&sd->lock);
@@ -342,7 +354,7 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 
 	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 	if (ret)
-		return ret;
+		goto err_put;
 
 	ret = hid_hw_open(hdev);
 	if (ret)
@@ -361,12 +373,15 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 
 err_stop:
 	hid_hw_stop(hdev);
+err_put:
+	kref_put(&sd->refcnt, steelseries_device_release);
 	return ret;
 }
 
 static void steelseries_arctis_remove(struct hid_device *hdev)
 {
 	struct steelseries_device *sd;
+	struct power_supply *battery;
 	unsigned long flags;
 	struct usb_interface *intf;
 	u8 interface_num;
@@ -388,13 +403,20 @@ static void steelseries_arctis_remove(struct hid_device *hdev)
 	if (interface_num == sd->info->sync_interface) {
 		spin_lock_irqsave(&sd->lock, flags);
 		sd->removed = true;
+		battery = sd->battery;
+		sd->battery = NULL;
 		spin_unlock_irqrestore(&sd->lock, flags);
 
 		cancel_delayed_work_sync(&sd->status_work);
+
+		if (battery)
+			power_supply_unregister(battery);
 	}
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
+
+	kref_put(&sd->refcnt, steelseries_device_release);
 }
 
 static int steelseries_arctis_raw_event(struct hid_device *hdev,
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 09/10] HID: steelseries: Add async status interface support
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

Some headsets expose a second HID interface that sends battery and
connection updates on its own. Watching that interface lets the driver
stop polling the sync interface. Add a
steelseries_device_info::async_interface field and the code to handle
it:

  - The driver binds both the sync and async interfaces. The async
    interface shares the steelseries_device created by the sync
    interface. It finds the sibling with usb_ifnum_to_if(), takes a
    reference, and returns -EPROBE_DEFER until the sync interface has
    probed. If the sync interface never binds, the async interface
    defers forever, which is fine here.
  - raw_event() now holds sd->lock and re-checks sd->removed so events
    on either interface are serialised against removal.
  - status_work runs once for async devices instead of rearming. A
    single status request is sent when the headset connects to get the
    initial battery level.

No device sets async_interface yet. This is the infrastructure for the
next commit.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 121 +++++++++++++++++++++------
 1 file changed, 97 insertions(+), 24 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 734cf1eb8789..2208d0e4cd2a 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -26,6 +26,7 @@ struct steelseries_device_info {
 	unsigned long capabilities;
 
 	u8 sync_interface;
+	u8 async_interface;
 
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -272,7 +273,8 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
 	sd->info->request_status(sd->hdev);
 
 	spin_lock_irqsave(&sd->lock, flags);
-	if (!sd->removed)
+	/* Async devices push status events themselves; only poll once. */
+	if (!sd->removed && !sd->info->async_interface)
 		schedule_delayed_work(&sd->status_work,
 				msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
 	spin_unlock_irqrestore(&sd->lock, flags);
@@ -316,6 +318,23 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	return 0;
 }
 
+static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
+						       int interface_num)
+{
+	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_arctis_probe(struct hid_device *hdev,
 				    const struct hid_device_id *id)
 {
@@ -337,39 +356,76 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 	if (ret)
 		return ret;
 
-	/* Let hid-generic handle non-sync interfaces */
-	if (interface_num != info->sync_interface)
+	/* 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);
 
-	sd = kzalloc_obj(*sd, GFP_KERNEL);
-	if (!sd)
-		return -ENOMEM;
+	if (interface_num == info->sync_interface) {
+		sd = kzalloc_obj(*sd, GFP_KERNEL);
+		if (!sd)
+			return -ENOMEM;
 
-	kref_init(&sd->refcnt);
-	sd->hdev = hdev;
-	sd->info = info;
-	spin_lock_init(&sd->lock);
+		kref_init(&sd->refcnt);
+		sd->hdev = hdev;
+		sd->info = info;
+		spin_lock_init(&sd->lock);
 
-	hid_set_drvdata(hdev, sd);
+		hid_set_drvdata(hdev, sd);
 
-	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
-	if (ret)
-		goto err_put;
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret)
+			goto err_put;
 
-	ret = hid_hw_open(hdev);
-	if (ret)
-		goto err_stop;
+		ret = hid_hw_open(hdev);
+		if (ret)
+			goto err_stop;
+
+		if (info->capabilities & SS_CAP_BATTERY) {
+			ret = steelseries_battery_register(sd);
+			if (ret < 0)
+				hid_warn(hdev, "Failed to register battery: %d\n", ret);
+		}
 
-	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;
 	}
 
-	INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
-	schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+	/*
+	 * The async interface shares the steelseries_device created by the
+	 * sync interface. Defer until the sync interface has probed and
+	 * published its drvdata.
+	 */
+	if (info->async_interface && interface_num == info->async_interface) {
+		struct hid_device *master_hdev;
 
-	return 0;
+		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);
+		kref_get(&sd->refcnt);
+		hid_set_drvdata(hdev, sd);
+
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret) {
+			kref_put(&sd->refcnt, steelseries_device_release);
+			return ret;
+		}
+
+		ret = hid_hw_open(hdev);
+		if (ret) {
+			hid_hw_stop(hdev);
+			kref_put(&sd->refcnt, steelseries_device_release);
+			return ret;
+		}
+		return 0;
+	}
+
+	return -ENODEV;
 
 err_stop:
 	hid_hw_stop(hdev);
@@ -426,10 +482,21 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 	u8 old_capacity;
 	bool old_connected;
 	bool old_charging;
+	bool is_async_interface;
+	unsigned long flags;
 
 	if (!sd)
 		return 0;
 
+	is_async_interface = (hdev != sd->hdev);
+
+	spin_lock_irqsave(&sd->lock, flags);
+
+	if (sd->removed) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+
 	old_capacity = sd->battery_capacity;
 	old_connected = sd->headset_connected;
 	old_charging = sd->battery_charging;
@@ -442,6 +509,10 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 			old_connected ? "" : "not ",
 			sd->headset_connected ? "" : "not ");
 
+		if (sd->headset_connected && !old_connected &&
+		    sd->info->async_interface && is_async_interface)
+			schedule_delayed_work(&sd->status_work, 0);
+
 		if (sd->battery) {
 			steelseries_headset_set_wireless_status(sd->hdev,
 							       sd->headset_connected);
@@ -465,6 +536,8 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 			power_supply_changed(sd->battery);
 	}
 
+	spin_unlock_irqrestore(&sd->lock, flags);
+
 	return 0;
 }
 
-- 
2.54.0


^ permalink raw reply related

* [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta
In-Reply-To: <20260623172310.272708-1-srimanachanta@gmail.com>

The Arctis Nova 7 Gen2 headsets answer status polls, but they also send
unsolicited battery and connection updates on a second HID interface
(interface 5). Use that interface through async_interface so the driver
does not have to poll. Add request and parse helpers for the Gen2 status
format (0xb0/0xb7/0xb9/0xbb opcodes).

Add the eight USB product IDs that share this protocol:

  0x22a1  Arctis Nova 7 2026
  0x22a7  Arctis Nova 7P 2026
  0x22a5  Arctis Nova 7X 2026
  0x22a9  Arctis Nova 7 Diablo 2026
  0x227e  Arctis Nova 7 Gen 2
  0x2258  Arctis Nova 7X Gen 2
  0x229e  Arctis Nova 7X Gen 2 (alternate PID)
  0x22ad  Arctis Nova 7X Gen 2 (alternate PID)

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-ids.h                |  8 ++++
 drivers/hid/hid-quirks.c             |  8 ++++
 drivers/hid/hid-steelseries-arctis.c | 63 ++++++++++++++++++++++++++++
 3 files changed, 79 insertions(+)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 915e936cbf8b..6b5be86b1bdf 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1379,6 +1379,14 @@
 #define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
 #define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X	0x12b6
 #define USB_DEVICE_ID_STEELSERIES_ARCTIS_9	0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026	0x22a1
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026	0x22a7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026	0x22a5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026	0x22a9
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2	0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2	0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2	0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3	0x22ad
 
 #define USB_VENDOR_ID_SUN		0x0430
 #define USB_DEVICE_ID_RARITAN_KVM_DONGLE	0xcdab
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index f546179858c2..40a761b5df91 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -749,6 +749,14 @@ 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_X) },
 	{ 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_7_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026) },
+	{ 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_7_X_GEN2_3) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 2208d0e4cd2a..eecd384b1a33 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -114,6 +114,13 @@ static int steelseries_arctis_9_request_status(struct hid_device *hdev)
 	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));
+}
+
 /*
  * Headset battery helpers
  */
@@ -163,6 +170,30 @@ static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
 	}
 }
 
+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;
+	}
+}
+
 /*
  * Device info definitions
  */
@@ -181,6 +212,14 @@ static const struct steelseries_device_info arctis_9_info = {
 	.parse_status = steelseries_arctis_9_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,
+};
+
 /*
  * Headset wireless status and battery infrastructure
  */
@@ -548,6 +587,30 @@ static const struct hid_device_id steelseries_arctis_devices[] = {
 	{ 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_7_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_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_7_X_GEN2_3),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
 	{}
 };
 MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
-- 
2.54.0


^ permalink raw reply related

* Re: [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework
From: sashiko-bot @ 2026-06-23 17:33 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260623172310.272708-6-srimanachanta@gmail.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

New issues:
- [High] Missing report ID signature validation for Arctis 1 causes arbitrary raw events to corrupt headset state.
- [High] Battery status requests are incorrectly sent as Feature reports instead of Output reports, breaking battery polling.
- [Medium] Dropped logic for marking Arctis 9 headset as disconnected when it reports an unknown status.

Pre-existing issues:
- [High] The raw_event interrupt handler can dereference an ERR_PTR due to a TOCTOU race window when battery registration fails.
--

commit 0576cf59ec529dc31ed68f59a8d598bdb4ca4a82
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Refactor Arctis driver to use device_info framework

Replace the per-product if/else quirk bitmap with a steelseries_device_info
struct. Each model provides its capabilities, sync_interface, and
request_status/parse_status callbacks.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 079504e6932a1..f00f4c5e6d9e4 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -15,70 +16,95 @@
[ ... ]
> +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_9_request_status(struct hid_device *hdev)
> +{
> +	const u8 data[] = { 0x00, 0x20 };
> +
> +	return steelseries_send_feature_report(hdev, data, sizeof(data));
> +}

[Severity: High]
Do these devices expect feature reports instead of output reports for status
requests?

The original code sent the periodic battery status request to the device
using an output report via hid_hw_raw_request(..., HID_OUTPUT_REPORT, ...).
This refactoring switches it to use steelseries_send_feature_report()
instead.

Sending a feature report to a device endpoint expecting an output report
might result in a USB stall, which would break battery polling.

[ ... ]
> @@ -88,30 +114,96 @@ static int battery_capacity_to_level(int capacity)
[ ... ]
> +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];
> +}

[Severity: High]
Will this corrupt the battery state if other reports are received?

The original raw event handler verified that the incoming Arctis 1 report
matched the expected battery status signature using memcmp().

Since steelseries_arctis_raw_event() now invokes parse_status()
unconditionally for all incoming raw events, could a different report
(such as volume control) with a payload of at least 4 bytes be erroneously
parsed as battery data?

> +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], 0x68, 0x9d);
> +	}
> +}

[Severity: Medium]
What happens if the device is turned off and sends the "no known status"
packet?

The original code explicitly handled the 0x55 response and used it to mark
the headset as disconnected.

By only processing 0xaa and dropping other responses without taking any
action, will we fail to update the connection state and leave the device
incorrectly reported as connected?

[ ... ]
> @@ -168,7 +245,26 @@ static enum power_supply_property steelseries_headset_battery_props[] = {
[ ... ]
> +static int steelseries_battery_register(struct steelseries_device *sd)
>  {
[ ... ]
>  	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;

[Severity: High]
This isn't a bug introduced by this patch, but does assigning the return
value directly to sd->battery before checking for an error create a race
condition?

Since hid_hw_start() and hid_hw_open() are already called in the probe
function, hardware interrupts are active. If power_supply_register() fails,
sd->battery temporarily holds an error pointer.

If steelseries_arctis_raw_event() triggers during this small window, the
following check would evaluate to true because an error pointer is not NULL:

steelseries_arctis_raw_event() {
    ...
    if (sd->battery)
        power_supply_changed(sd->battery);
}

Could this lead to an invalid pointer dereference and a kernel panic?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=5

^ permalink raw reply

* Re: [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family
From: sashiko-bot @ 2026-06-23 17:35 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260623172310.272708-11-srimanachanta@gmail.com>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

New issues:
- [Medium] The unconditional minimum size check in the Gen2 parsing function may silently drop valid unsolicited status updates.

Pre-existing issues:
- [High] Unsafe fetching of the sibling interface's private data lacks driver validation and synchronization, leading to Type Confusion and Use-After-Free.
--

commit ca415cb624472143e783db3389a867c4e203e6c8
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Add support for Arctis Nova 7 Gen2 family

This commit adds support for the Arctis Nova 7 Gen2 headsets.
It introduces a new parsing helper for the Gen2 status format and uses an
async interface to handle unsolicited battery and connection updates without
polling.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -163,6 +170,30 @@ static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
>  	}
>  }
>  
> +static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
> +							u8 *data, int size)
> +{
> +	if (size < 4)
> +		return;

[Severity: Medium]
Will this strict minimum size check silently drop valid short packets?

Since opcodes like 0xb7, 0xb9, and 0xbb only appear to require 2 bytes of
payload (e.g., data[0] and data[1]), if the hardware sends an unsolicited
report as a 2 or 3 byte packet, it seems it would be dropped before
processing.

> +
> +	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;
> +	}
> +}
> +
>  /*
>   * Device info definitions
>   */
> @@ -181,6 +212,14 @@ static const struct steelseries_device_info arctis_9_info = {
>  	.parse_status = steelseries_arctis_9_parse_status,
>  };
>  
> +static const struct steelseries_device_info arctis_nova_7_gen2_info = {
> +	.sync_interface = 3,
> +	.async_interface = 5,

[Severity: High]
This isn't a bug introduced by this patch, but since these devices use an
async interface, does the sibling interface lookup in 
steelseries_get_sibling_hdev() have adequate locking and validation?

When a device probes the async interface, it appears
steelseries_get_sibling_hdev() fetches intfdata from the sibling interface
via usb_get_intfdata() locklessly.

If the sibling interface (interface 3) is unbound via sysfs and bound to
another driver (like usbfs), or physically unplugged concurrently, could
this return a foreign private context or a soon-to-be-freed pointer?

Then in steelseries_arctis_probe(), this unvalidated pointer is cast and
dereferenced to get the driver data:

    sd = hid_get_drvdata(master_hdev);
    kref_get(&sd->refcnt);

Could this lead to type confusion or a use-after-free if the sibling driver
is no longer the expected usbhid driver?

> +	.capabilities = SS_CAP_BATTERY,
> +	.request_status = steelseries_arctis_nova_request_status,
> +	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> +};
> +

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=10

^ permalink raw reply

* Re: [PATCH v4 09/10] HID: steelseries: Add async status interface support
From: sashiko-bot @ 2026-06-23 17:36 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260623172310.272708-10-srimanachanta@gmail.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

New issues:
- [Critical] The driver blindly casts the sibling interface's driver data to a `struct hid_device` without verifying the bound driver, leading to type confusion and memory corruption.
- [Critical] The sync interface publishes `sd` to `drvdata` before initialization is complete, allowing the async interface to crash the kernel by scheduling an uninitialized work struct.
- [High] Lockless retrieval and access of the sibling interface's driver data leads to a Use-After-Free if the sync interface is unbound concurrently.
- [High] If the sync interface probe fails after the async interface binds to it, the async interface will access a freed `sd->hdev` pointer.

Pre-existing issues:
- [High] An ERR_PTR temporarily assigned to `sd->battery` during initialization failure can be concurrently dereferenced by the raw event handler.
--

commit 002dd61306f7255151ab901886b17a9e612226ad
Author: Sriman Achanta <srimanachanta@gmail.com>
Subject: HID: steelseries: Add async status interface support

This commit adds support for a second HID interface that sends battery
and connection updates asynchronously. This allows the driver to stop
polling the sync interface by watching the async interface instead.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 734cf1eb87894..2208d0e4cd2ad 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -316,6 +318,23 @@ static int steelseries_battery_register(struct steelseries_device *sd)
>  	return 0;
>  }
>  
> +static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
> +						       int interface_num)
> +{
> +	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);

[Severity: High]
Does this code safely retrieve the sibling interface's driver data? Because
no reference is taken on the USB interface and usb_driver_claim_interface()
is not used, could a concurrent sysfs unbind destroy the interface and the
retrieved data while we are accessing it?

[Severity: Critical]
Is it safe to assume usb_get_intfdata() returns a struct hid_device pointer
here? If the sync interface is bound to a different driver (such as
snd-usb-audio), its driver data would be a completely different structure.
Could this lead to type confusion and memory corruption when the pointer is
dereferenced later in the probe function?

> +
> +	return sibling_hdev;
> +}
> +
>  static int steelseries_arctis_probe(struct hid_device *hdev,
>  				    const struct hid_device_id *id)
>  {
> @@ -337,39 +356,76 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
>  	if (ret)
>  		return ret;
>  
> -	/* Let hid-generic handle non-sync interfaces */
> -	if (interface_num != info->sync_interface)
> +	/* 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);
>  
> -	sd = kzalloc_obj(*sd, GFP_KERNEL);
> -	if (!sd)
> -		return -ENOMEM;
> +	if (interface_num == info->sync_interface) {
> +		sd = kzalloc_obj(*sd, GFP_KERNEL);
> +		if (!sd)
> +			return -ENOMEM;
>  
> -	kref_init(&sd->refcnt);
> -	sd->hdev = hdev;
> -	sd->info = info;
> -	spin_lock_init(&sd->lock);
> +		kref_init(&sd->refcnt);
> +		sd->hdev = hdev;
> +		sd->info = info;
> +		spin_lock_init(&sd->lock);
>  
> -	hid_set_drvdata(hdev, sd);
> +		hid_set_drvdata(hdev, sd);

[Severity: Critical]
By calling hid_set_drvdata() here, sd is globally published before
INIT_DELAYED_WORK() is called below. If the async interface probes concurrently
and immediately receives an event, could it call schedule_delayed_work() on
an uninitialized work struct?

>  
> -	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> -	if (ret)
> -		goto err_put;
> +		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> +		if (ret)
> +			goto err_put;
>  
> -	ret = hid_hw_open(hdev);
> -	if (ret)
> -		goto err_stop;
> +		ret = hid_hw_open(hdev);
> +		if (ret)
> +			goto err_stop;

[Severity: High]
If the sync interface probe fails after this point (for example, if hid_hw_open()
or steelseries_battery_register() fails) and returns an error, but the async
interface has already bound and incremented sd->refcnt:

Since steelseries_arctis_remove() is never called for the failed sync probe,
sd->removed remains false. If the driver core clears the sync interface's driver
data and the sibling interface is unbound, sd->hdev is freed.

Could the still-active async interface then process raw events, bypass the
sd->removed check, and access the freed sd->hdev pointer via
steelseries_headset_set_wireless_status()?

> +
> +		if (info->capabilities & SS_CAP_BATTERY) {
> +			ret = steelseries_battery_register(sd);
> +			if (ret < 0)
> +				hid_warn(hdev, "Failed to register battery: %d\n", ret);
> +		}
>  
> -	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;
>  	}
[ ... ]
> @@ -442,6 +509,10 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
>  			old_connected ? "" : "not ",
>  			sd->headset_connected ? "" : "not ");
>  
> +		if (sd->headset_connected && !old_connected &&
> +		    sd->info->async_interface && is_async_interface)
> +			schedule_delayed_work(&sd->status_work, 0);
> +
>  		if (sd->battery) {
>  			steelseries_headset_set_wireless_status(sd->hdev,
>  							       sd->headset_connected);
>  			power_supply_changed(sd->battery);

[Severity: High]
This is a pre-existing issue, but is there a data race with sd->battery here?

During initialization in steelseries_battery_register():

	sd->battery = power_supply_register(&sd->hdev->dev,
			&sd->battery_desc, &battery_cfg);
	if (IS_ERR(sd->battery)) {
		ret = PTR_ERR(sd->battery);
		sd->battery = NULL;

Since hardware has already been enabled via hid_hw_start(), interrupts can
arrive concurrently. If an event arrives during the window where sd->battery
holds an ERR_PTR, if (sd->battery) will evaluate to true.

Could this cause power_supply_changed(sd->battery) to dereference the error
pointer?

>  		}
>  
> @@ -465,6 +536,8 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
>  			power_supply_changed(sd->battery);
>  	}
>  
> +	spin_unlock_irqrestore(&sd->lock, flags);
> +
>  	return 0;
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=9

^ permalink raw reply

* Re: [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting
From: sashiko-bot @ 2026-06-23 17:36 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260623172310.272708-9-srimanachanta@gmail.com>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Dangling pointer left in device driver data on probe error path leads to Use-After-Free.
- [High] Race condition in sibling interface lookup during probe leading to Use-After-Free of `hid_device`.
--

commit 5a206f2162dc642386285174c7af698f767dad15
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Manage battery lifetime with refcounting

The next change shares one steelseries_device between two HID interfaces,
so the state can outlive either interface. Stop using devm for it. Reference
count the struct with a kref and free it from steelseries_device_release().

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 1f0e9cb5138f6..734cf1eb87894 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -361,12 +373,15 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
>  
>  err_stop:
>  	hid_hw_stop(hdev);
> +err_put:
> +	kref_put(&sd->refcnt, steelseries_device_release);

[Severity: High]
Does this error path leave a dangling pointer in the device's driver data?

If hid_hw_start() or hid_hw_open() fails during the sync interface probe,
the driver explicitly frees the sd struct via kref_put(). However, it leaves
a dangling pointer because hid_set_drvdata(hdev, sd) was already called
earlier and is not reverted here.

If the async interface is probed concurrently (e.g., via sysfs bind),
it calls steelseries_get_sibling_hdev() to find the sync interface's HID
device and reads its driver data. It might receive this dangling pointer
and call kref_get() on freed memory, resulting in a use-after-free.

>  	return ret;
>  }

[Severity: High]
Is there a race condition when looking up the sibling interface during probe
that could lead to a use-after-free of the hid_device?

When probing the async interface, steelseries_get_sibling_hdev() retrieves
the sibling interface's hid_device pointer locklessly:

drivers/hid/hid-steelseries-arctis.c:steelseries_get_sibling_hdev() {
    ...
    sibling_hdev = usb_get_intfdata(sibling_intf);
    return sibling_hdev;
}

Then in steelseries_arctis_probe():

    master_hdev = steelseries_get_sibling_hdev(hdev, info->sync_interface);
    if (!master_hdev || !hid_get_drvdata(master_hdev))
        return -EPROBE_DEFER;

If the sync interface is unbound concurrently (e.g., via sysfs unbind),
hid_destroy_device() can free the hid_device. If the async probe reads
the intfdata pointer just before it is cleared and dereferences it via
hid_get_drvdata(master_hdev), it accesses freed memory. The HID device
probe path does not seem to hold the parent USB device lock to prevent
this concurrent execution.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=8

^ permalink raw reply

* Re: [PATCH 0/3] HID: iio: callback API signature match for usage_id
From: Jonathan Cameron @ 2026-06-23 19:02 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: Sanjay Chitroda, Jiri Kosina, Srinivas Pandruvada, David Lechner,
	Nuno Sá, Andy Shevchenko, linux-input, linux-iio,
	linux-kernel
In-Reply-To: <ajLC09uEIOClVzEq@ashevche-desk.local>

On Wed, 17 Jun 2026 18:52:51 +0300
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:

> On Tue, Jun 16, 2026 at 06:55:19PM +0530, Sanjay Chitroda wrote:
> > 
> > Most of HID IIO driver has correct 'u32' type of usage_id with
> > https://lore.kernel.org/all/20260610-6-june-hid-iio-correct-usage-id-v2-0-c3c5f0720493@gmail.com/
> > series which is applied on iio/testing branch.
> > 
> > On top of the same, this series updates remaining HID IIO drivers
> > to use 'u32' for the usage_id parameter.
> > 
> > Pending list of HID IIO drivers are extracted with command line:
> > find drivers/iio/ -type f -name "*hid*" | xargs grep -A 5 static | \
> >     grep -E -A 5 "_proc_event\(|_capture_sample\(|_parse_report\(" --color | \
> >     grep usage_id  
> 
> I recommend to get used with `git grep ...` which is more powerful and much
> faster (on a Git index).
> 
> 	git grep -lw 'u[^3].* usage_id' -- drivers/iio/
> 
> for the list of files, and
> 
> 	git grep -np -w 'u[^3].* usage_id' -- drivers/iio/
> 
> for a better view.
> 
> (It gives one false positive, though :-)
> 
> > This matches expected callback API type as HID usage IDs are
> > defined as 32-bit values.
> > 
> > No functional changes are introduced.
> > 
> > Testing:
> >   - Compiled with W=1 for each patch in the series  
> 
> Reviewed-by: Andy Shevchenko <andriy.shevchenko@intel.com>
> 
> Jonathan, it seems the missing part of the initial unification. Please, apply
> to your testing branch.
Applied to the testing branch of iio.git.

Thanks,

Jonathan

> 


^ permalink raw reply

* Re: [PATCH v1] iio: temperature: hid-sensor-temperature: switch to non-devm iio_device_register()
From: Jonathan Cameron @ 2026-06-23 19:06 UTC (permalink / raw)
  To: srinivas pandruvada
  Cc: Maxwell Doose, Sanjay Chitroda, jikos, dlechner, nuno.sa, andy,
	hongyan.song, linux-input, linux-iio, linux-kernel
In-Reply-To: <0e198f80c6f28e448611e02e1fe20af632931dd3.camel@linux.intel.com>

On Mon, 22 Jun 2026 13:50:22 -0700
srinivas pandruvada <srinivas.pandruvada@linux.intel.com> wrote:

> On Mon, 2026-06-22 at 10:27 -0500, Maxwell Doose wrote:
> > On Mon, Jun 22, 2026 at 10:26 AM srinivas pandruvada
> > <srinivas.pandruvada@linux.intel.com> wrote:  
> > > 
> > > On Mon, 2026-06-22 at 10:51 +0530, Sanjay Chitroda wrote:  
> > > > From: Sanjay Chitroda <sanjayembeddedse@gmail.com>
> > > > 
> > > > Avoid using devm_iio_device_register(), as this driver requires
> > > > explicit
> > > > error handling and teardown ordering.
> > > > 
> > > > Mixing devm_* APIs with goto-based error unwinding breaks the
> > > > expected
> > > > LIFO resource release model and can introduce race windows during
> > > > device
> > > > removal. In particular, the IIO device may remain visible to
> > > > userspace
> > > > while dependent resources are already being freed, potentially
> > > > leading
> > > > to use-after-free issues.  
> > > 
> > > Please explain this use after free case here.
> > > 
> > > Thanks,
> > > Srinivas  
> > 
> > My guess is that because the device would still be registered but
> > would actually be removed, sysfs still has "wild" pointers to
> > read_raw() and write_raw() (which don't exist anymore), causing the
> > UAF. If I'm wrong feel free to correct me though.  
> 
> iio_device_unregister() will be last one to be called after device
> removal from devm action handler. This will cleanup attributes. So,
> read_raw() or write_raw() can be called. The problem can be handlers
> for read_raw() and write_raw() if anything there which are dependent on
> clean done by hid_temperature_remove(). Here callbacks are cleaned up,
> so nothing to respond to read  sensor_hub_input_attr_get_raw_value(),
> so it has to wait for 5 seconds to timeout, which is not great. So
> nothing against change done here.
> 
> But still not sure any use after free case, unless I am missing
> something.
> 
Agreed that to call UAF you need an explained path (and preferably
testing that it happens).  The timeout issue Srinivas calls out is
sufficient for us to merge this as a fix, but the patch description
should then talk about that.

Thanks,

Jonathan
> Thanks,
> Srinivas
> 
> 


^ permalink raw reply

* [PATCH v5 0/6] Add support for Baijie Helper A133 board
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input

Baijie Helper A133 board is a development board around Baijie A133 Core
SBC. Features:

- 1/2/4GiB LPDDR4 DRAM
- 8/16/32GiB eMMC
- AXP707 PMIC
- USB-C OTG port in peripheral mode (via onboard hub)
- 2 USB 2.0 ports
- MicroSD slot and on-board eMMC module
- Gigabit Ethernet
- Bluetooth
- WiFi

Add initial support for both the Helper and Core boards, including UART,
PMU, eMMC, USB, Ethernet, LRADC-connected buttons.

UART1 can only be used for Bluetooth module, but BT-WiFi combo Allwinner
AW869A chip has not mainline driver currently.

Link: https://szbaijie.com/index/product/product_detail.html?product_id=23&language=en

Changelog:
v5:
- sun50i-a100.dtsi: reflowed "compatible" property of lradc node
- dropped "reserve RAM for ATF" patch [v4 6/7]
v4:
- reserve RAM for ATF
- sun4i-lradc-keys: Add A100/A133 compatible
- dt-bindings: renamed "Baijie Helper A133" -> "Baijie A133 HelperBoard"
- dt-bindings: renamed "baijie,helper-a133" -> "baijie,helperboard-a133"
- dt-bindings: introduced allwinner,sun50i-a100-lradc
- reserve RAM for ATF
- renamed "sun50i-a133-baijie-helper.dtb" -> "sun50i-a133-helperboard.dtb"
- added "model" property into root of sun50i-a133-helperboard-core.dtsi
- added "cap-mmc-highspeed" and "max-frequency" into &mmc2
- added "x-powers,drive-vbus-en" and "*-supply" into &axp803
- dropped all "regulator-enable-ramp-delay" properties
- replaced &reg_dcdc3 with a "polyphased" comment
- exact DRAM voltage in &reg_dcdc5
- disabled &reg_dcdc6 to avoid "[   31.710641] dcdc6: disabling"
- added &reg_vdd5v "root" regulator
- added "disable-wp" into &mmc0
- commented &usb_otg
- assigned usb1_vbus-supply in &usbphy
- https://lore.kernel.org/all/20260605070923.3045073-1-alexander.sverdlin@gmail.com/
v3:
- added lradc node to sun50i-a100.dtsi
- enabled LRADC driver in arm64 defconfig
- added my copyrights into the newly introduced DTs
- all DT nodes sorted alphabetically
- all always-on regulators commented/propetly named
- all regulators got proper voltages (not default ranges)
- ADC-sensed buttons K1..K5 added
- re-labelled "eth_phy" -> "rgmii_phy"
- usbphy 0 switched from host into peripheral mode (downstream from an
  onboard hub)
- typo sun50i-a133-baije-core.dtsi -> sun50i-a133-baijie-core.dtsi
- https://lore.kernel.org/all/20260517234134.2737320-1-alexander.sverdlin@gmail.com/
v2:
- introduced baijie,helper-a133-core compatible for the Core (SoM) board
- https://lore.kernel.org/all/20260510201644.4143710-1-alexander.sverdlin@gmail.com/
v1:
- https://lore.kernel.org/all/20260503191842.2736130-1-alexander.sverdlin@gmail.com/

Alexander Sverdlin (6):
  arm64: defconfig: Enable Allwinner LRADC input driver
  dt-bindings: vendor-prefixes: Add Shenzhen Baijie Technology Co., Ltd.
  dt-bindings: arm: sunxi: Add Baijie HelperBoard A133 compatible
  dt-bindings: input: sun4i-lradc-keys: Add A100/A133 compatible
  arm64: dts: allwinner: a100: Add LRADC node
  arm64: dts: allwinner: A133: add support for Baijie Helper A133 board

 .../devicetree/bindings/arm/sunxi.yaml        |   6 +
 .../input/allwinner,sun4i-a10-lradc-keys.yaml |   1 +
 .../devicetree/bindings/vendor-prefixes.yaml  |   2 +
 arch/arm64/boot/dts/allwinner/Makefile        |   1 +
 .../arm64/boot/dts/allwinner/sun50i-a100.dtsi |  10 +
 .../sun50i-a133-helperboard-core.dtsi         | 197 ++++++++++++++++++
 .../dts/allwinner/sun50i-a133-helperboard.dts | 148 +++++++++++++
 arch/arm64/configs/defconfig                  |   1 +
 8 files changed, 366 insertions(+)
 create mode 100644 arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard-core.dtsi
 create mode 100644 arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard.dts

-- 
2.54.0


^ permalink raw reply

* [PATCH v5 1/6] arm64: defconfig: Enable Allwinner LRADC input driver
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

Enable Allwinner LRADC input driver as module to support buttons on Baijie
HelperBoard A133.

Acked-by: Jernej Skrabec <jernej.skrabec@gmail.com>
Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v4-v5:
- no changes
v3:
- new patch

 arch/arm64/configs/defconfig | 1 +
 1 file changed, 1 insertion(+)

diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig
index 654a102cb5bc..c267f0906460 100644
--- a/arch/arm64/configs/defconfig
+++ b/arch/arm64/configs/defconfig
@@ -527,6 +527,7 @@ CONFIG_KEYBOARD_GPIO=y
 CONFIG_KEYBOARD_GPIO_POLLED=m
 CONFIG_KEYBOARD_SNVS_PWRKEY=m
 CONFIG_KEYBOARD_IMX_SC_KEY=m
+CONFIG_KEYBOARD_SUN4I_LRADC=m
 CONFIG_KEYBOARD_CROS_EC=y
 CONFIG_KEYBOARD_MTK_PMIC=m
 CONFIG_MOUSE_ELAN_I2C=m
-- 
2.54.0


^ permalink raw reply related

* [PATCH v5 2/6] dt-bindings: vendor-prefixes: Add Shenzhen Baijie Technology Co., Ltd.
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input, Conor Dooley, Paul Kocialkowski
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

Shenzhen Baijie Technology Co., Ltd. focuses on R&D and production of
embedded products as well as customization of embedded solutions.

Link: https://szbaijie.com/
Acked-by: Conor Dooley <conor.dooley@microchip.com>
Reviewed-by: Paul Kocialkowski <paulk@sys-base.io>
Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v2-v5:
- no changes

 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 6b9fb6a6bf0b..88225786e216 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -229,6 +229,8 @@ patternProperties:
     description: Azoteq (Pty) Ltd
   "^azw,.*":
     description: Shenzhen AZW Technology Co., Ltd.
+  "^baijie,.*":
+    description: Shenzhen Baijie Technology Co., Ltd.
   "^baikal,.*":
     description: BAIKAL ELECTRONICS, JSC
   "^bananapi,.*":
-- 
2.54.0


^ permalink raw reply related

* [PATCH v5 3/6] dt-bindings: arm: sunxi: Add Baijie HelperBoard A133 compatible
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input, Conor Dooley
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

Baijie HelperBoard A133 is a development board around their A133 Core
board. Introduce a compatible for both the Core and the development
boards.

Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v5:
- no changes
v4:
- renamed "Baijie Helper A133" -> "Baijie A133 HelperBoard"
- renamed "baijie,helper-a133" -> "baijie,helperboard-a133"
v3:
- no separate section for "core" .dtsi
v2:
- introduced baijie,helper-a133-core compatible for the Core (SoM) board

 Documentation/devicetree/bindings/arm/sunxi.yaml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Documentation/devicetree/bindings/arm/sunxi.yaml b/Documentation/devicetree/bindings/arm/sunxi.yaml
index e6443c266fa1..82dd58b95f8a 100644
--- a/Documentation/devicetree/bindings/arm/sunxi.yaml
+++ b/Documentation/devicetree/bindings/arm/sunxi.yaml
@@ -96,6 +96,12 @@ properties:
           - const: allwinner,ba10-tvbox
           - const: allwinner,sun4i-a10
 
+      - description: Baijie A133 HelperBoard
+        items:
+          - const: baijie,helperboard-a133
+          - const: baijie,helperboard-a133-core
+          - const: allwinner,sun50i-a100
+
       - description: BananaPi
         items:
           - const: lemaker,bananapi
-- 
2.54.0


^ permalink raw reply related

* [PATCH v5 4/6] dt-bindings: input: sun4i-lradc-keys: Add A100/A133 compatible
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

The Allwinner A100/A133 SoCs have an LRADC which is compatible with the
versions in existing SoCs. Add a compatible string for A100, with the R329
fallback.

Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v5:
- no changes
v4:
- new patch

 .../bindings/input/allwinner,sun4i-a10-lradc-keys.yaml           | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Documentation/devicetree/bindings/input/allwinner,sun4i-a10-lradc-keys.yaml b/Documentation/devicetree/bindings/input/allwinner,sun4i-a10-lradc-keys.yaml
index 6bdb8040be65..524c8b51f53f 100644
--- a/Documentation/devicetree/bindings/input/allwinner,sun4i-a10-lradc-keys.yaml
+++ b/Documentation/devicetree/bindings/input/allwinner,sun4i-a10-lradc-keys.yaml
@@ -23,6 +23,7 @@ properties:
       - const: allwinner,sun50i-r329-lradc
       - items:
           - enum:
+              - allwinner,sun50i-a100-lradc
               - allwinner,sun50i-h616-lradc
               - allwinner,sun20i-d1-lradc
           - const: allwinner,sun50i-r329-lradc
-- 
2.54.0


^ permalink raw reply related

* [PATCH v5 6/6] arm64: dts: allwinner: A133: add support for Baijie Helper A133 board
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

Baijie Helper A133 board is a development board around Baijie A133 Core
SBC. Features:

- 1/2/4GiB LPDDR4 DRAM
- 8/16/32GiB eMMC
- AXP707 PMIC
- USB-C OTG port in peripheral mode (via onboard hub)
- 2 USB 2.0 ports
- MicroSD slot and on-board eMMC module
- Gigabit Ethernet
- Bluetooth
- WiFi

Add initial support for both the Helper and Core boards, including UART,
PMU, eMMC, USB, Ethernet, LRADC-connected buttons.

UART1 can only be used for Bluetooth module, but BT-WiFi combo Allwinner
AW869A chip has no mainline driver currently.

Acked-by: Jernej Skrabec <jernej.skrabec@gmail.com>
Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v5:
- no changes
v4:
- renamed "sun50i-a133-baijie-helper.dtb" -> "sun50i-a133-helperboard.dtb"
- added "model" property into root of sun50i-a133-helperboard-core.dtsi
- added "cap-mmc-highspeed" and "max-frequency" into &mmc2
- added "x-powers,drive-vbus-en" and "*-supply" into &axp803
- dropped all "regulator-enable-ramp-delay" properties
- replaced &reg_dcdc3 with a "polyphased" comment
- exact DRAM voltage in &reg_dcdc5
- disabled &reg_dcdc6 to avoid "[   31.710641] dcdc6: disabling"
- added &reg_vdd5v "root" regulator
- added "disable-wp" into &mmc0
- commented &usb_otg
- assigned usb1_vbus-supply in &usbphy
v3:
- added my copyrights into the newly introduced DTs
- all DT nodes sorted alphabetically
- all always-on regulators commented/propetly named
- all regulators got proper voltages (not default ranges)
- ADC-sensed buttons K1..K5 added
- re-labelled "eth_phy" -> "rgmii_phy"
- usbphy 0 switched from host into peripheral mode (downstream from an
  onboard hub)
- typo sun50i-a133-baije-core.dtsi -> sun50i-a133-baijie-core.dtsi
v2:
- introduced baijie,helper-a133-core compatible for the Core (SoM) board

 arch/arm64/boot/dts/allwinner/Makefile        |   1 +
 .../sun50i-a133-helperboard-core.dtsi         | 197 ++++++++++++++++++
 .../dts/allwinner/sun50i-a133-helperboard.dts | 148 +++++++++++++
 3 files changed, 346 insertions(+)
 create mode 100644 arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard-core.dtsi
 create mode 100644 arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard.dts

diff --git a/arch/arm64/boot/dts/allwinner/Makefile b/arch/arm64/boot/dts/allwinner/Makefile
index 53e6b701e7d3..aa21f58a4be1 100644
--- a/arch/arm64/boot/dts/allwinner/Makefile
+++ b/arch/arm64/boot/dts/allwinner/Makefile
@@ -24,6 +24,7 @@ dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a64-sopine-baseboard.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a64-teres-i.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h64-remix-mini-pc.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a100-allwinner-perf1.dtb
+dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a133-helperboard.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a133-liontron-h-a133l.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h5-bananapi-m2-plus.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h5-bananapi-m2-plus-v1.2.dtb
diff --git a/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard-core.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard-core.dtsi
new file mode 100644
index 000000000000..545972d2324a
--- /dev/null
+++ b/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard-core.dtsi
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+/*
+ * Copyright (c) 2025 Arm Ltd.
+ * Copyright (c) 2026 Alexander Sverdlin <alexander.sverdlin@gmail.com>
+ */
+
+/dts-v1/;
+
+#include "sun50i-a100.dtsi"
+#include "sun50i-a100-cpu-opp.dtsi"
+
+/{
+	model = "Baijie A133 HelperBoard Core";
+	compatible = "baijie,helperboard-a133-core",
+		     "allwinner,sun50i-a100";
+
+	aliases {
+		serial1 = &uart1;	/* BT module */
+	};
+};
+
+&cpu0 {
+	cpu-supply = <&reg_dcdc2>;
+};
+
+&lradc {
+	vref-supply = <&reg_aldo1>;
+};
+
+&mmc2 {
+	vmmc-supply = <&reg_dcdc1>;
+	vqmmc-supply = <&reg_eldo1>;
+	cap-mmc-highspeed;
+	cap-mmc-hw-reset;
+	max-frequency = <100000000>;
+	non-removable;
+	bus-width = <8>;
+	mmc-ddr-1_8v;
+	mmc-hs200-1_8v;
+	status = "okay";
+};
+
+&pio {
+	vcc-pb-supply = <&reg_dcdc1>;
+	vcc-pc-supply = <&reg_eldo1>;
+	vcc-pd-supply = <&reg_dcdc1>;
+	vcc-pe-supply = <&reg_dldo2>;
+	vcc-pf-supply = <&reg_dcdc1>;
+	vcc-pg-supply = <&reg_dldo1>;
+	vcc-ph-supply = <&reg_dcdc1>;
+	/*
+	 * PL0/PL1 are the I2C connection to PMIC, but it would create a
+	 * circular dependency:
+	 * vcc-pl-supply = <&reg_aldo3>;
+	 */
+};
+
+&r_i2c0 {
+	status = "okay";
+
+	axp803: pmic@34 {
+		compatible = "x-powers,axp803";
+		reg = <0x34>;
+		interrupt-parent = <&r_intc>;
+		interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
+		x-powers,drive-vbus-en;		/* set N_VBUSEN as output pin */
+		aldoin-supply = <&reg_vdd5v>;
+		dldoin-supply = <&reg_vdd5v>;
+		eldoin-supply = <&reg_vdd5v>;
+		fldoin-supply = <&reg_dcdc5>;
+		vin1-supply = <&reg_vdd5v>;
+		vin2-supply = <&reg_vdd5v>;
+		vin3-supply = <&reg_vdd5v>;
+		vin4-supply = <&reg_vdd5v>;
+		vin5-supply = <&reg_vdd5v>;
+		vin6-supply = <&reg_vdd5v>;
+		drivevbus-supply = <&reg_vdd5v>;
+	};
+};
+
+#include "axp803.dtsi"
+
+&ac_power_supply {
+	status = "okay";
+};
+
+&reg_aldo1 {
+	/* PLL + LRADC analog reference */
+	regulator-always-on;
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vcc-pll";
+};
+
+&reg_aldo2 {
+	/* LPDDR */
+	regulator-always-on;
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vdd18-lpddr";
+};
+
+&reg_aldo3 {
+	/*
+	 * Port L, but linking it to &pio node would create a circular
+	 * dependency because of PL0/PL1 I2C connection to PMIC
+	 */
+	regulator-always-on;
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vcc-pl";
+};
+
+&reg_dcdc1 {
+	/* Besides Port D it also powers analog part of USB IP and SoC I/O */
+	regulator-always-on;
+	regulator-min-microvolt = <3300000>;
+	regulator-max-microvolt = <3300000>;
+	regulator-name = "vcc-3v3";
+};
+
+&reg_dcdc2 {
+	regulator-always-on;
+	regulator-min-microvolt = <810000>;
+	regulator-max-microvolt = <1200000>;
+	regulator-name = "vdd-cpu";
+};
+
+/* DCDC3 is polyphased with DCDC2 */
+
+&reg_dcdc4 {
+	/* Digital part of USB IP, "System" SoC power rail */
+	regulator-always-on;
+	regulator-min-microvolt = <950000>;
+	regulator-max-microvolt = <950000>;
+	regulator-name = "vdd-sys";
+};
+
+&reg_dcdc5 {
+	regulator-always-on;
+	regulator-min-microvolt = <1100000>;
+	regulator-max-microvolt = <1100000>;
+	regulator-name = "vcc-dram";
+};
+
+/* DCDC6 unused */
+&reg_dcdc6 {
+	status = "disabled";
+};
+
+&reg_dldo1 {
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vcc-pg";
+};
+
+&reg_dldo2 {
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vcc-pe";
+};
+
+&reg_dldo3 {
+	regulator-min-microvolt = <2800000>;
+	regulator-max-microvolt = <2800000>;
+	regulator-name = "avdd-csi";
+};
+
+&reg_dldo4 {
+	regulator-min-microvolt = <2800000>;
+	regulator-max-microvolt = <2800000>;
+	regulator-name = "afvcc-csi";
+};
+
+&reg_eldo1 {
+	regulator-min-microvolt = <1800000>;
+	regulator-max-microvolt = <1800000>;
+	regulator-name = "vcc-pc";
+};
+
+&reg_eldo2 {
+	regulator-min-microvolt = <1200000>;
+	regulator-max-microvolt = <1200000>;
+	regulator-name = "dvdd-csi";
+};
+
+/* ELDO3 unused */
+
+&reg_fldo1 {
+	/* CPUS power rail */
+	regulator-always-on;
+	regulator-min-microvolt = <900000>;
+	regulator-max-microvolt = <900000>;
+	regulator-name = "vdd-cpus";
+};
+
+/* reg_drivevbus unused */
diff --git a/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard.dts b/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard.dts
new file mode 100644
index 000000000000..694c0cacf906
--- /dev/null
+++ b/arch/arm64/boot/dts/allwinner/sun50i-a133-helperboard.dts
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+/*
+ * Copyright (c) 2025 Arm Ltd.
+ * Copyright (c) 2026 Alexander Sverdlin <alexander.sverdlin@gmail.com>
+ */
+
+/dts-v1/;
+
+#include "sun50i-a133-helperboard-core.dtsi"
+
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/linux-event-codes.h>
+#include <dt-bindings/leds/common.h>
+
+/{
+	model = "Baijie HelperBoard A133";
+	compatible = "baijie,helperboard-a133",
+		     "baijie,helperboard-a133-core",
+		     "allwinner,sun50i-a100";
+
+	aliases {
+		serial0 = &uart0;
+	};
+
+	chosen {
+		stdout-path = "serial0:115200n8";
+	};
+
+	leds {
+		compatible = "gpio-leds";
+
+		led {
+			function = LED_FUNCTION_INDICATOR;
+			color = <LED_COLOR_ID_GREEN>;
+			gpios = <&pio 7 13 GPIO_ACTIVE_LOW>;	/* PH13 */
+		};
+	};
+
+	reg_vdd5v: vdd5v {
+		/* board wide 5V supply from a 12V->5V regulator */
+		compatible = "regulator-fixed";
+		regulator-name = "vdd-5v";
+		regulator-min-microvolt = <5000000>;
+		regulator-max-microvolt = <5000000>;
+		regulator-always-on;
+	};
+};
+
+&ehci1 {
+	status = "okay";
+};
+
+&emac0 {
+	pinctrl-names = "default";
+	pinctrl-0 = <&rgmii0_pins>;
+	phy-handle = <&rgmii_phy>;
+	phy-mode = "rgmii-id";
+	allwinner,rx-delay-ps = <200>;
+	allwinner,tx-delay-ps = <200>;
+	status = "okay";
+};
+
+&lradc {
+	wakeup-source;
+	status = "okay";
+
+	button-115 {
+		label = "K1";
+		linux,code = <KEY_1>;
+		channel = <0>;
+		voltage = <114607>;
+	};
+
+	button-235 {
+		label = "K2";
+		linux,code = <KEY_2>;
+		channel = <0>;
+		voltage = <234783>;
+	};
+
+	button-360 {
+		label = "K3";
+		linux,code = <KEY_3>;
+		channel = <0>;
+		voltage = <360000>;
+	};
+
+	button-476 {
+		label = "K4";
+		linux,code = <KEY_4>;
+		channel = <0>;
+		voltage = <476471>;
+	};
+
+	button-592 {
+		label = "K5";
+		linux,code = <KEY_5>;
+		channel = <0>;
+		voltage = <591946>;
+	};
+};
+
+&mdio0 {
+	reset-gpios = <&pio 7 11 GPIO_ACTIVE_LOW>;	/* PH11 */
+	reset-delay-us = <10000>;
+	reset-post-delay-us = <150000>;
+
+	rgmii_phy: ethernet-phy@1 {
+		compatible = "ethernet-phy-ieee802.3-c22";
+		reg = <1>;
+	};
+};
+
+&mmc0 {
+	vmmc-supply = <&reg_dcdc1>;
+	cd-gpios = <&pio 5 6 GPIO_ACTIVE_LOW>;	/* PF6 */
+	bus-width = <4>;
+	disable-wp;
+	status = "okay";
+};
+
+&ohci1 {
+	status = "okay";
+};
+
+&rgmii0_pins {
+	drive-strength = <30>;
+};
+
+&uart0 {
+	pinctrl-names = "default";
+	pinctrl-0 = <&uart0_pb_pins>;
+	status = "okay";
+};
+
+&usb_otg {
+	/*
+	 * Connected to a downstream port of an onboard hub, therefore only
+	 * "peripheral" mode will work here.
+	 */
+	dr_mode = "peripheral";
+	status = "okay";
+};
+
+&usbphy {
+	usb1_vbus-supply = <&reg_vdd5v>;
+	status = "okay";
+};
-- 
2.54.0


^ permalink raw reply related

* [PATCH v5 5/6] arm64: dts: allwinner: a100: Add LRADC node
From: Alexander Sverdlin @ 2026-06-23 20:48 UTC (permalink / raw)
  To: linux-arm-kernel, linux-sunxi
  Cc: Alexander Sverdlin, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Hans de Goede, Dmitry Torokhov, Andre Przywara, Jun Yan,
	Lukas Schmid, J. Neuschäfer, Eric Biggers, Michal Simek,
	Luca Weiss, Sven Peter, Maxime Ripard, devicetree, linux-kernel,
	linux-input
In-Reply-To: <20260623204824.691832-1-alexander.sverdlin@gmail.com>

A100/A133 SoCs feature a Low Rate ADC (LRADC) for Key application.

Specs:
- Power supply voltage: 1.8 V
- Reference voltage: 1.35 V
- Interrupt support
- Support Hold Key and General Key
- Support normal, continue and single work mode
- 6-bits resolution, sample rate up to 2 kHz
- Voltage input range between 0 and 1.35 V

Reviewed-by: Jernej Skrabec <jernej.skrabec@gmail.com>
Signed-off-by: Alexander Sverdlin <alexander.sverdlin@gmail.com>
---
Changelog:
v5:
- reflowed "compatible" property of lradc node
v4:
- added allwinner,sun50i-a100-lradc compatible
v3:
- new patch

 arch/arm64/boot/dts/allwinner/sun50i-a100.dtsi | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/arch/arm64/boot/dts/allwinner/sun50i-a100.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-a100.dtsi
index b3fb1e0ee796..ba6020989ce9 100644
--- a/arch/arm64/boot/dts/allwinner/sun50i-a100.dtsi
+++ b/arch/arm64/boot/dts/allwinner/sun50i-a100.dtsi
@@ -466,6 +466,16 @@ ths: thermal-sensor@5070400 {
 			#thermal-sensor-cells = <1>;
 		};
 
+		lradc: lradc@5070800 {
+			compatible = "allwinner,sun50i-a100-lradc",
+				     "allwinner,sun50i-r329-lradc";
+			reg = <0x05070800 0x400>;
+			interrupts = <GIC_SPI 22 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&ccu CLK_BUS_LRADC>;
+			resets = <&ccu RST_BUS_LRADC>;
+			status = "disabled";
+		};
+
 		usb_otg: usb@5100000 {
 			compatible = "allwinner,sun50i-a100-musb",
 				     "allwinner,sun8i-a33-musb";
-- 
2.54.0


^ permalink raw reply related

* Re: [PATCH v2 0/8] HID: iio: Avoid race between callback setup and device exposure
From: Sanjay Chitroda @ 2026-06-24  1:43 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: Jiri Kosina, Jonathan Cameron, Srinivas Pandruvada, David Lechner,
	Nuno Sá, Andy Shevchenko, Archana Patni, Song Hongyan,
	linux-input, linux-iio, linux-kernel, srinivas pandruvada
In-Reply-To: <ajpgQ8vCHacv_klG@ashevche-desk.local>



On 23 June 2026 4:00:27 pm IST, Andy Shevchenko <andriy.shevchenko@intel.com> wrote:
>On Mon, Jun 22, 2026 at 10:59:56AM +0530, Sanjay Chitroda wrote:
>> 
>> This series avoid a race condition in HID IIO drivers related to the
>> ordering between callback registration and device exposure.
>> 
>> Currently, several HID IIO drivers register the IIO device (making it
>> visible to userspace and other kernel consumers) before all required
>> callbacks and resources are fully initialized, or rely on devm-based
>> cleanup in a way that does not guarantee correct teardown ordering.
>> This creates a window where the device can be accessed while it is
>
>There is a difference between "this creates" and "this might create".
>I believe Srinivas and others were asking for the proof. So, what path
>in the code makes this happen or possible to happen?
>
iio_device_register() exposes the IIO device to user space, while sensor_hub_register_callback() registers callbacks for buffered IIO(streaming mode).

This might create window where from userspace buffer mode is enabled and callbacks are not registered which would result into loss of samples until callback registration completes, although no explicit failure. In teardown path which can resulting in stale/no data.

This was discussed in the v1 thread and v2 was posted based on discussion and agreement:
 https://lore.kernel.org/all/3FED088A-651B-4E8B-840B-1B92CB4DF6F4@gmail.com/


>> not fully initialized or is being torn down, potentially leading to
>> sample drop or stale/no data.
>> 
>> To handle this, the series ensures that:
>>   - All required callbacks and resources are set up before the device
>>     is registered with the IIO core
>>   - Resource cleanup is performed explicitly where ordering matters
>> 
>> PS: This is prepratory series to convert all HID IIO driver to devm.
>> 
>> Testing:
>>   - Compiled with W=1 for each patch in series
>> 
>> ---
>> Changes in v2:
>> - Drop fixes tag and rectify commit message with reference to that
>
>You also dropped my tag. Why?
>
Thank you for the review and tag on v1.

While code changes are intact in v2, the rational and commit message were updated substantially. Since commit message is as important as change which will be permanent in history for future reference, I chose to drop the tag to request a fresh review.

I shall highlight the same in change log. I'll make sure to note in future revision.

Thanks, Sanjay

>> - Link to v1: https://patch.msgid.link/20260606-5-june-hid-iio-race-fixes-v1-0-27a848c5758f@gmail.com
>

^ permalink raw reply

* [git pull] Input updates for v7.2-rc0
From: Dmitry Torokhov @ 2026-06-24  2:21 UTC (permalink / raw)
  To: Linus Torvalds; +Cc: linux-kernel, linux-input

Hi Linus,

Please pull from:

	git://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git tags/input-for-v7.2-rc0

to receive updates for the input subsystem. You will get:

- A new driver for Wacom W9000-series penabled touchscreens

- Updates to STM FTS driver adding support for reset line and preparing
  the driver for STMFTS5 support
  
- Updates to RMI4 and IMS PCU drivers hardening the code

- Support for half-duplex mode restored in ADS7846 driver

- Updates to driver's device_id tables to use named initializers

- Removal of no longer used PCAP keys and touchscreen drivers (support
  for the ezx series of phones was removed in 2022)

- Removal of xilinx_ps2 driver which is no longer used either

- Updates to userio to allow setting up additional serio port
  characteristics (such as id, extra and proto)

- Assorted hardening and cleanup fixes for other drivers.


Changelog:
---------

Aaro Koskinen (1):
      Input: ads7846 - restore half-duplex support

Arnd Bergmann (2):
      Input: pcap_keys - remove unused driver
      Input: pcap_ts - remove unused driver

Bryam Vargas (4):
      Input: touchwin - reset the packet index on every complete packet
      Input: mms114 - reject an oversized device packet size
      Input: goodix - clamp the device-reported contact count
      Input: iforce - bound the device-reported force-feedback effect index

Colin Ian King (1):
      Input: lm8323 - remove space before newline

David Heidelberg (6):
      Input: stmfts - fix the MODULE_LICENSE() string
      Input: stmfts - use dev struct directly
      Input: stmfts - switch to devm_regulator_bulk_get_const
      Input: stmfts - abstract reading information from the firmware
      Input: stmfts - disable regulators and disable irq when power on fails
      dt-bindings: input: touchscreen: st,stmfts: Introduce reset GPIO

Dmitry Torokhov (40):
      Input: stmfts - fix formatting issues
      Input: atmel_mxt_ts - use __free() for obuf in mxt_object_show
      Input: atlas_btns - modernize the driver
      Input: ims-pcu - only expose sysfs attributes on control interface
      Input: ims-pcu - fix logic error in packet reset
      Input: ims-pcu - release data interface on disconnect
      Input: ims-pcu - fix use-after-free and double-free in disconnect
      Input: ims-pcu - fix type confusion in CDC union descriptor parsing
      Input: ims-pcu - fix firmware leak in async update
      Input: ims-pcu - fix race condition in reset_device sysfs callback
      Input: ims-pcu - validate control endpoint type
      Input: ims-pcu - fix out-of-bounds read in ims_pcu_irq() debug logging
      Input: ims-pcu - fix DMA mapping violation in line setup
      Input: ims-pcu - add response length checks
      Input: ims-pcu - fix potential infinite loop in CDC union descriptor parsing
      Input: ipaq-micro-keys - fix potential deadlock
      Input: ipaq-micro-keys - add length check in micro_key_receive
      Input: rmi4 - fix register descriptor address calculation
      Input: rmi4 - refactor register descriptor parsing
      Input: rmi4 - fix type overflow in register counts
      Input: rmi4 - fix num_subpackets overflow in register descriptor
      Input: rmi4 - initialize attn_fifo properly
      Input: rmi4 - fix memory leak in rmi_set_attn_data()
      Input: rmi4 - iterative IRQ handler
      Input: rmi4 - fix bit count in bitmap_copy()
      Input: rmi4 - fix limit in rmi_register_desc_has_subpacket()
      Input: rmi4 - use local presence map in rmi_read_register_desc()
      Input: rmi4 - refactor function allocation and registration
      Input: rmi4 - use kzalloc_flex() for struct rmi_function
      Input: rmi4 - refactor F12 probe function
      Input: rmi4 - change reg_size type to u32
      Input: rmi4 - use unaligned access helpers in F12
      Input: rmi4 - use flexible array member for IRQ masks in F12
      Input: rmi4 - use devm_kmalloc for F12 data packet buffer
      Input: rmi4 - use sizeof(*ptr) and idiomatic checks in f12 allocators
      Input: rmi4 - simplify size calculations in F12
      Input: rmi4 - propagate proper error code in F12 sensor tuning
      Input: rmi4 - update formatting in F12
      Input: stop force-feedback timer when unregistering input devices
      Input: mms114 - fix touch indexing for MMS134S and MMS136

Elliot Tester (1):
      Input: remove changelogs

Haoxiang Li (1):
      Input: synaptics-rmi4 - unregister function handlers on physical driver registration failure

Hendrik Noack (2):
      dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
      Input: Add support for Wacom W9000-series penabled touchscreens

Kris Bahnsen (1):
      Input: ads7846 - don't use scratch for tx_buf when clearing register

Petr Hodina (2):
      Input: stmfts - use client to make future code cleaner
      Input: stmfts - add optional reset GPIO support

Ranjan Kumar (1):
      Input: elan_i2c - prevent division by zero and arithmetic underflow

Ricardo Ribalda (1):
      Input: atmel_mxt_ts - set byte_offset as signed

Rosen Penev (3):
      Input: xilinx_ps2 - remove driver
      Input: apbps2 - simplify resource mapping and IRQ retrieval
      Input: ipaq-micro-keys - simplify allocation

Uwe Kleine-König (The Capable Hub) (3):
      Input: Use named initializers for arrays of i2c_device_data
      Input: iqs5xx - drop unused i2c driver_data
      Input: Drop unused assignments from pnp_device_id arrays

Vicki Pfau (2):
      Input: userio - update maintainer name
      Input: userio - allow setting other id values

Yuki Horii (1):
      Input: tsc2007 - reduce I2C transactions for Z2 read

Diffstat:
--------

 .../bindings/input/touchscreen/st,stmfts.yaml      |   4 +
 .../input/touchscreen/wacom,w9007a-lt03.yaml       |  73 ++++
 Documentation/input/userio.rst                     |  25 +-
 MAINTAINERS                                        |   2 +-
 drivers/input/ff-memless.c                         |  27 +-
 drivers/input/gameport/ns558.c                     |  46 +--
 drivers/input/input.c                              |   3 +
 drivers/input/joystick/adafruit-seesaw.c           |   2 +-
 drivers/input/joystick/as5011.c                    |   2 +-
 drivers/input/joystick/iforce/iforce-packets.c     |  18 +-
 drivers/input/joystick/qwiic-joystick.c            |   2 +-
 drivers/input/keyboard/adp5588-keys.c              |   4 +-
 drivers/input/keyboard/cap11xx.c                   |  14 +-
 drivers/input/keyboard/cypress-sf.c                |   2 +-
 drivers/input/keyboard/dlink-dir685-touchkeys.c    |   2 +-
 drivers/input/keyboard/ipaq-micro-keys.c           |  24 +-
 drivers/input/keyboard/lm8323.c                    |   4 +-
 drivers/input/keyboard/lm8333.c                    |   2 +-
 drivers/input/keyboard/max7359_keypad.c            |   2 +-
 drivers/input/keyboard/mpr121_touchkey.c           |   2 +-
 drivers/input/keyboard/qt1070.c                    |   2 +-
 drivers/input/keyboard/qt2160.c                    |   2 +-
 drivers/input/keyboard/tca8418_keypad.c            |   2 +-
 drivers/input/keyboard/tm2-touchkey.c              |   2 +-
 drivers/input/misc/Kconfig                         |  10 -
 drivers/input/misc/Makefile                        |   1 -
 drivers/input/misc/ad714x-i2c.c                    |  10 +-
 drivers/input/misc/adxl34x-i2c.c                   |   2 +-
 drivers/input/misc/apanel.c                        |   2 +-
 drivers/input/misc/atlas_btns.c                    | 109 ++---
 drivers/input/misc/atmel_captouch.c                |   2 +-
 drivers/input/misc/bma150.c                        |   6 +-
 drivers/input/misc/cma3000_d0x_i2c.c               |   2 +-
 drivers/input/misc/da7280.c                        |   2 +-
 drivers/input/misc/drv260x.c                       |   8 +-
 drivers/input/misc/drv2665.c                       |   2 +-
 drivers/input/misc/drv2667.c                       |   2 +-
 drivers/input/misc/ims-pcu.c                       | 131 +++++-
 drivers/input/misc/kxtj9.c                         |   2 +-
 drivers/input/misc/mma8450.c                       |   2 +-
 drivers/input/misc/pcap_keys.c                     | 125 ------
 drivers/input/misc/pcf8574_keypad.c                |   2 +-
 drivers/input/misc/yealink.c                       |   9 -
 drivers/input/mouse/cyapa.c                        |   2 +-
 drivers/input/mouse/elan_i2c_core.c                |  38 +-
 drivers/input/mouse/synaptics_i2c.c                |   2 +-
 drivers/input/rmi4/rmi_2d_sensor.h                 |   4 +-
 drivers/input/rmi4/rmi_bus.c                       |  34 +-
 drivers/input/rmi4/rmi_bus.h                       |   1 +
 drivers/input/rmi4/rmi_driver.c                    | 204 ++++++----
 drivers/input/rmi4/rmi_driver.h                    |  13 +-
 drivers/input/rmi4/rmi_f11.c                       |   2 +-
 drivers/input/rmi4/rmi_f12.c                       | 415 +++++++++----------
 drivers/input/rmi4/rmi_i2c.c                       |   2 +-
 drivers/input/rmi4/rmi_smbus.c                     |   2 +-
 drivers/input/serio/Kconfig                        |  10 -
 drivers/input/serio/Makefile                       |   1 -
 drivers/input/serio/apbps2.c                       |   7 +-
 drivers/input/serio/i8042-acpipnpio.h              |  56 +--
 drivers/input/serio/userio.c                       |  34 +-
 drivers/input/serio/xilinx_ps2.c                   | 363 -----------------
 drivers/input/tablet/aiptek.c                      |  31 --
 drivers/input/touchscreen/Kconfig                  |  22 +-
 drivers/input/touchscreen/Makefile                 |   2 +-
 drivers/input/touchscreen/ad7879-i2c.c             |   4 +-
 drivers/input/touchscreen/ads7846.c                | 173 +++++++-
 drivers/input/touchscreen/ar1021_i2c.c             |   2 +-
 drivers/input/touchscreen/atmel_mxt_ts.c           |  23 +-
 drivers/input/touchscreen/auo-pixcir-ts.c          |   2 +-
 drivers/input/touchscreen/bu21013_ts.c             |   2 +-
 drivers/input/touchscreen/bu21029_ts.c             |   2 +-
 drivers/input/touchscreen/cy8ctma140.c             |   2 +-
 drivers/input/touchscreen/cy8ctmg110_ts.c          |   2 +-
 drivers/input/touchscreen/cyttsp5.c                |   2 +-
 drivers/input/touchscreen/cyttsp_i2c.c             |   2 +-
 drivers/input/touchscreen/eeti_ts.c                |   2 +-
 drivers/input/touchscreen/egalax_ts.c              |   2 +-
 drivers/input/touchscreen/elants_i2c.c             |   6 +-
 drivers/input/touchscreen/exc3000.c                |   8 +-
 drivers/input/touchscreen/goodix.c                 |   5 +-
 drivers/input/touchscreen/hideep.c                 |   2 +-
 drivers/input/touchscreen/himax_hx83112b.c         |   4 +-
 drivers/input/touchscreen/hynitron-cst816x.c       |   2 +-
 drivers/input/touchscreen/ili210x.c                |   8 +-
 drivers/input/touchscreen/ilitek_ts_i2c.c          |   2 +-
 drivers/input/touchscreen/iqs5xx.c                 |   6 +-
 drivers/input/touchscreen/max11801_ts.c            |   2 +-
 drivers/input/touchscreen/melfas_mip4.c            |   2 +-
 drivers/input/touchscreen/migor_ts.c               |   2 +-
 drivers/input/touchscreen/mms114.c                 |  26 +-
 drivers/input/touchscreen/novatek-nvt-ts.c         |   4 +-
 drivers/input/touchscreen/pcap_ts.c                | 252 ------------
 drivers/input/touchscreen/pixcir_i2c_ts.c          |   4 +-
 drivers/input/touchscreen/raydium_i2c_ts.c         |   4 +-
 drivers/input/touchscreen/rohm_bu21023.c           |   2 +-
 drivers/input/touchscreen/s6sy761.c                |   2 +-
 drivers/input/touchscreen/silead.c                 |  12 +-
 drivers/input/touchscreen/sis_i2c.c                |   4 +-
 drivers/input/touchscreen/st1232.c                 |   4 +-
 drivers/input/touchscreen/stmfts.c                 | 162 +++++---
 drivers/input/touchscreen/touchwin.c               |  15 +-
 drivers/input/touchscreen/tsc2004.c                |   2 +-
 drivers/input/touchscreen/tsc2007_core.c           |   8 +-
 drivers/input/touchscreen/wacom_i2c.c              |   2 +-
 drivers/input/touchscreen/wacom_w9000.c            | 444 +++++++++++++++++++++
 drivers/input/touchscreen/wdt87xx_i2c.c            |   2 +-
 drivers/input/touchscreen/zet6223.c                |   2 +-
 drivers/input/touchscreen/zforce_ts.c              |   2 +-
 include/linux/input.h                              |   3 +
 include/uapi/linux/userio.h                        |   7 +-
 110 files changed, 1673 insertions(+), 1501 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/input/touchscreen/wacom,w9007a-lt03.yaml
 delete mode 100644 drivers/input/misc/pcap_keys.c
 delete mode 100644 drivers/input/serio/xilinx_ps2.c
 delete mode 100644 drivers/input/touchscreen/pcap_ts.c
 create mode 100644 drivers/input/touchscreen/wacom_w9000.c

Thanks.


-- 
Dmitry

^ permalink raw reply


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