* [PATCH 2/2] HID: input: Add HID_BATTERY_QUIRK_DYNAMIC for Elan touchscreens
From: Hans de Goede @ 2026-02-28 14:52 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Hans de Goede, Dmitry Torokhov, linux-input, ggrundik
In-Reply-To: <20260228145258.76937-1-johannes.goede@oss.qualcomm.com>
Elan touchscreens have a HID-battery device for the stylus which is always
there even if there is no stylus.
This is causing upower to report an empty battery for the stylus and some
desktop-environments will show a notification about this, which is quite
annoying.
Because of this the HID-battery is being ignored on all Elan I2c and USB
touchscreens, but this causes there to be no battery reporting for
the stylus at all.
This adds a new HID_BATTERY_QUIRK_DYNAMIC and uses these for the Elan
touchscreens.
This new quirks causes the present value of the battery to start at 0,
which will make userspace ignore it and only sets present to 1 after
receiving a battery input report which only happens when the stylus
gets in range.
Reported-by: ggrundik@gmail.com
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221118
Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
---
drivers/hid/hid-input.c | 14 +++++++++++---
include/linux/hid.h | 1 +
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index 67ca1e88ce13..8fc20df99b97 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -354,6 +354,7 @@ static enum power_supply_property hidinput_battery_props[] = {
#define HID_BATTERY_QUIRK_FEATURE (1 << 1) /* ask for feature report */
#define HID_BATTERY_QUIRK_IGNORE (1 << 2) /* completely ignore the battery */
#define HID_BATTERY_QUIRK_AVOID_QUERY (1 << 3) /* do not query the battery */
+#define HID_BATTERY_QUIRK_DYNAMIC (1 << 4) /* report present only after life signs */
static const struct hid_device_id hid_battery_quirks[] = {
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_APPLE,
@@ -398,8 +399,8 @@ static const struct hid_device_id hid_battery_quirks[] = {
* Elan HID touchscreens seem to all report a non present battery,
* set HID_BATTERY_QUIRK_IGNORE for all Elan I2C and USB HID devices.
*/
- { HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_IGNORE },
- { HID_USB_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_IGNORE },
+ { HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_DYNAMIC },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_DYNAMIC },
{}
};
@@ -456,11 +457,14 @@ static int hidinput_get_battery_property(struct power_supply *psy,
int ret = 0;
switch (prop) {
- case POWER_SUPPLY_PROP_PRESENT:
case POWER_SUPPLY_PROP_ONLINE:
val->intval = 1;
break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = dev->battery_present;
+ break;
+
case POWER_SUPPLY_PROP_CAPACITY:
if (dev->battery_status != HID_BATTERY_REPORTED &&
!dev->battery_avoid_query) {
@@ -573,6 +577,8 @@ static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
if (quirks & HID_BATTERY_QUIRK_AVOID_QUERY)
dev->battery_avoid_query = true;
+ dev->battery_present = (quirks & HID_BATTERY_QUIRK_DYNAMIC) ? false : true;
+
dev->battery = power_supply_register(&dev->dev, psy_desc, &psy_cfg);
if (IS_ERR(dev->battery)) {
error = PTR_ERR(dev->battery);
@@ -628,6 +634,7 @@ static void hidinput_update_battery(struct hid_device *dev, unsigned int usage,
return;
if (hidinput_update_battery_charge_status(dev, usage, value)) {
+ dev->battery_present = true;
power_supply_changed(dev->battery);
return;
}
@@ -643,6 +650,7 @@ static void hidinput_update_battery(struct hid_device *dev, unsigned int usage,
if (dev->battery_status != HID_BATTERY_REPORTED ||
capacity != dev->battery_capacity ||
ktime_after(ktime_get_coarse(), dev->battery_ratelimit_time)) {
+ dev->battery_present = true;
dev->battery_capacity = capacity;
dev->battery_status = HID_BATTERY_REPORTED;
dev->battery_ratelimit_time =
diff --git a/include/linux/hid.h b/include/linux/hid.h
index dce862cafbbd..d9b54f0e8671 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -682,6 +682,7 @@ struct hid_device {
__s32 battery_charge_status;
enum hid_battery_status battery_status;
bool battery_avoid_query;
+ bool battery_present;
ktime_t battery_ratelimit_time;
#endif
--
2.52.0
^ permalink raw reply related
* [PATCH 1/2] HID: input: Drop Asus UX550* touchscreen ignore battery quirks
From: Hans de Goede @ 2026-02-28 14:52 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Hans de Goede, Dmitry Torokhov, linux-input
Drop the Asus UX550* touchscreen ignore battery quirks, there is a blanket
HID_BATTERY_QUIRK_IGNORE for all USB_VENDOR_ID_ELAN USB touchscreens now,
so these are just a duplicate of those.
Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
---
drivers/hid/hid-ids.h | 2 --
drivers/hid/hid-input.c | 4 ----
2 files changed, 6 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..fa728b4153fb 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -446,8 +446,6 @@
#define USB_DEVICE_ID_TOSHIBA_CLICK_L9W 0x0401
#define USB_DEVICE_ID_HP_X2 0x074d
#define USB_DEVICE_ID_HP_X2_10_COVER 0x0755
-#define USB_DEVICE_ID_ASUS_UX550VE_TOUCHSCREEN 0x2544
-#define USB_DEVICE_ID_ASUS_UX550_TOUCHSCREEN 0x2706
#define I2C_DEVICE_ID_CHROMEBOOK_TROGDOR_POMPOM 0x2F81
#define USB_VENDOR_ID_ELECOM 0x056e
diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index 2633fcd8f910..67ca1e88ce13 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -386,10 +386,6 @@ static const struct hid_device_id hid_battery_quirks[] = {
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_DINOVO_EDGE_KBD),
HID_BATTERY_QUIRK_IGNORE },
- { HID_USB_DEVICE(USB_VENDOR_ID_ELAN, USB_DEVICE_ID_ASUS_UX550_TOUCHSCREEN),
- HID_BATTERY_QUIRK_IGNORE },
- { HID_USB_DEVICE(USB_VENDOR_ID_ELAN, USB_DEVICE_ID_ASUS_UX550VE_TOUCHSCREEN),
- HID_BATTERY_QUIRK_IGNORE },
{ HID_USB_DEVICE(USB_VENDOR_ID_UGEE, USB_DEVICE_ID_UGEE_XPPEN_TABLET_DECO_L),
HID_BATTERY_QUIRK_AVOID_QUERY },
{ HID_USB_DEVICE(USB_VENDOR_ID_UGEE, USB_DEVICE_ID_UGEE_XPPEN_TABLET_DECO_PRO_MW),
--
2.52.0
^ permalink raw reply related
* [PATCH 7/7] HID: asus: do not try to initialize the backlight if the enpoint doesn't support it
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
Avoid possibly printing a warning about the inability to initialize the
backlight if the hid endpoint doesn't support it.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index bcf3b0c7c758..6e9a3a3d5616 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -1309,7 +1309,8 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
- asus_kbd_register_leds(hdev))
+ (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) &&
+ (asus_kbd_register_leds(hdev)))
hid_warn(hdev, "Failed to initialize backlight.\n");
/*
--
2.53.0
^ permalink raw reply related
* [PATCH 6/7] HID: asus: do not abort probe when not necessary
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
In order to avoid dereferencing a NULL pointer asus_probe is aborted early
and control of some asus devices is transferred over hid-generic after
erroring out even when such NULL dereference cannot happen: only early
abort when the NULL dereference can happen.
Also make the code shorter and more adherent to coding standards
removing square brackets enclosing single-line if-else statements.
Fixes: d3af6ca9a8c3 ("HID: asus: fix UAF via HID_CLAIMED_INPUT validation")
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 25 ++++++++++---------------
1 file changed, 10 insertions(+), 15 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 61705fd10d90..bcf3b0c7c758 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -1325,22 +1325,17 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
* were freed during registration due to no usages being mapped,
* leaving drvdata->input pointing to freed memory.
*/
- if (!drvdata->input || !(hdev->claimed & HID_CLAIMED_INPUT)) {
- hid_err(hdev, "Asus input not registered\n");
- ret = -ENOMEM;
- goto err_stop_hw;
- }
-
- if (drvdata->tp) {
- drvdata->input->name = "Asus TouchPad";
- } else {
- drvdata->input->name = "Asus Keyboard";
- }
+ if (drvdata->input && (hdev->claimed & HID_CLAIMED_INPUT)) {
+ if (drvdata->tp)
+ drvdata->input->name = "Asus TouchPad";
+ else
+ drvdata->input->name = "Asus Keyboard";
- if (drvdata->tp) {
- ret = asus_start_multitouch(hdev);
- if (ret)
- goto err_stop_hw;
+ if (drvdata->tp) {
+ ret = asus_start_multitouch(hdev);
+ if (ret)
+ goto err_stop_hw;
+ }
}
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH 5/7] HID: asus: simplify and improve asus_kbd_set_report()
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
Make the function shorter and easier to read using __free, and also fix a
misaligned comment closing tag.
The __free macro from cleanup.h is already used in the driver, but its
include is missing: add it.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 48731b48523d..61705fd10d90 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -21,6 +21,7 @@
*/
#include <linux/acpi.h>
+#include <linux/cleanup.h>
#include <linux/dmi.h>
#include <linux/hid.h>
#include <linux/module.h>
@@ -464,23 +465,18 @@ static int asus_raw_event(struct hid_device *hdev,
static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
{
- unsigned char *dmabuf;
int ret;
- dmabuf = kmemdup(buf, buf_size, GFP_KERNEL);
+ u8 *dmabuf __free(kfree) = kmemdup(buf, buf_size, GFP_KERNEL);
if (!dmabuf)
return -ENOMEM;
/*
* The report ID should be set from the incoming buffer due to LED and key
* interfaces having different pages
- */
- ret = hid_hw_raw_request(hdev, buf[0], dmabuf,
- buf_size, HID_FEATURE_REPORT,
- HID_REQ_SET_REPORT);
- kfree(dmabuf);
-
- return ret;
+ */
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, HID_FEATURE_REPORT,
+ HID_REQ_SET_REPORT);
}
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
--
2.53.0
^ permalink raw reply related
* [PATCH 4/7] HID: asus: make asus_resume adhere to linux kernel coding standards
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
Linux kernel coding standars requires functions opening brackets to be in
a newline: move the opening bracket of asus_resume in its own line.
Fixes: 546edbd26cff ("HID: hid-asus: reset the backlight brightness level on resume")
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 2b4411399571..48731b48523d 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -1171,7 +1171,8 @@ static int asus_start_multitouch(struct hid_device *hdev)
return 0;
}
-static int __maybe_unused asus_resume(struct hid_device *hdev) {
+static int __maybe_unused asus_resume(struct hid_device *hdev)
+{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
int ret = 0;
--
2.53.0
^ permalink raw reply related
* [PATCH 3/7] HID: asus: fix compiler warning about unused variables
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
In the function asus_has_report_id there are 3 unused variables clang
warns about: remove them.
Fixes:0919db9f3583 ("HID: asus: always fully initialize devices")
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index fbcf38b15290..2b4411399571 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -723,10 +723,9 @@ static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct)
static bool asus_has_report_id(struct hid_device *hdev, u16 report_id)
{
- int t, f, u, err = 0;
struct hid_report *report;
- for (t = HID_INPUT_REPORT; t <= HID_FEATURE_REPORT; t++) {
+ for (int t = HID_INPUT_REPORT; t <= HID_FEATURE_REPORT; t++) {
list_for_each_entry(report, &hdev->report_enum[t].report_list, list) {
if (report->id == report_id)
return true;
--
2.53.0
^ permalink raw reply related
* [PATCH 2/7] HID: asus: add xg mobile 2022 external hardware support
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
XG mobile station 2022 has a different PID than the 2023 model: add it
that model to hid-asus.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 3 +++
drivers/hid/hid-ids.h | 1 +
2 files changed, 4 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 5fcb06b16167..fbcf38b15290 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -1515,6 +1515,9 @@ static const struct hid_device_id asus_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X),
QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_ROG_ALLY_XPAD },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
+ USB_DEVICE_ID_ASUSTEK_XGM_2022),
+ },
{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_XGM_2023),
},
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 4ab7640b119a..5e85921049e0 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -229,6 +229,7 @@
#define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X 0x1b4c
#define USB_DEVICE_ID_ASUSTEK_ROG_CLAYMORE_II_KEYBOARD 0x196b
#define USB_DEVICE_ID_ASUSTEK_FX503VD_KEYBOARD 0x1869
+#define USB_DEVICE_ID_ASUSTEK_XGM_2022 0x1970
#define USB_DEVICE_ID_ASUSTEK_XGM_2023 0x1a9a
#define USB_VENDOR_ID_ATEN 0x0557
--
2.53.0
^ permalink raw reply related
* [PATCH 1/7] HID: asus: fix code style of comments and brackets
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>
In asus_raw_event there is code that would violate checkpatch script
such as unaligned closing comments and superfluous graph parenthesis:
fix these stylistic issues.
Also remove an empty comment spanning two lines.
This commit does not change runtime behavior.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index ffbfaff9f117..5fcb06b16167 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -20,9 +20,6 @@
* Copyright (c) 2016 Frederik Wenigwieser <frederik.wenigwieser@gmail.com>
*/
-/*
- */
-
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/hid.h>
@@ -359,7 +356,7 @@ static int asus_event(struct hid_device *hdev, struct hid_field *field,
struct hid_usage *usage, __s32 value)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
-
+
if ((usage->hid & HID_USAGE_PAGE) == HID_UP_ASUSVENDOR &&
(usage->hid & HID_USAGE) != 0x00 &&
(usage->hid & HID_USAGE) != 0xff && !usage->type) {
@@ -448,21 +445,18 @@ static int asus_raw_event(struct hid_device *hdev,
/*
* G713 and G733 send these codes on some keypresses, depending on
* the key pressed it can trigger a shutdown event if not caught.
- */
- if (data[0] == 0x02 && data[1] == 0x30) {
+ */
+ if (data[0] == 0x02 && data[1] == 0x30)
return -1;
- }
}
if (drvdata->quirks & QUIRK_ROG_CLAYMORE_II_KEYBOARD) {
/*
* CLAYMORE II keyboard sends this packet when it goes to sleep
* this causes the whole system to go into suspend.
- */
-
- if(size == 2 && data[0] == 0x02 && data[1] == 0x00) {
+ */
+ if (size == 2 && data[0] == 0x02 && data[1] == 0x00)
return -1;
- }
}
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH 0/7] HID: asus: increase robustness of the driver
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
To: linux-kernel
Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schyboll, Denis Benato, Denis Benato
Hi all,
Previous asus-wmi maintainer and asus-linux developer has become less
active in the project and left me in charge of advancing the support
for ASUS equipement on Linux.
I am preparing to send a patchset of his revised work to support ASUS
ROG Ally handhelds devices and since that work is also useful in
expanding support for 2025 models it is important to improve the
hid-asus driver as future patches will build on top of these changes.
Cheers,
Denis
Denis Benato (7):
HID: asus: fix code style of comments and brackets
HID: asus: add xg mobile 2022 external hardware support
HID: asus: fix compiler warning about unused variables
HID: asus: make asus_resume adhere to linux kernel coding standards
HID: asus: simplify and improve asus_kbd_set_report()
HID: asus: do not abort probe when not necessary
HID: asus: do not try to initialize the backlight if the enpoint
doesn't support it
drivers/hid/hid-asus.c | 67 ++++++++++++++++++------------------------
drivers/hid/hid-ids.h | 1 +
2 files changed, 29 insertions(+), 39 deletions(-)
--
2.53.0
^ permalink raw reply
* Re: [PATCH v2 0/3] Input: add initial support for Goodix GTX8 touchscreen ICs
From: Hans de Goede @ 2026-02-28 11:23 UTC (permalink / raw)
To: Aelin Reidel, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Neil Armstrong, Henrik Rydberg
Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
~postmarketos/upstreaming, Piyush Raj Chouhan,
Alexander Koskovich
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>
Hi,
On 28-Feb-26 02:56, Aelin Reidel wrote:
> These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
> and gesture events.
>
> This driver is derived from the Goodix gtx8_driver_linux available at
> [1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
> Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.
>
> The current implementation only supports Normandy and Yellowstone type
> ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
> don't have a device with GTX8 over SPI at hand. Adding support for SPI
> should be fairly easy in the future, since the code uses a regmap.
>
> Support for advanced features like:
> - Firmware updates
> - Stylus events
> - Gesture events
> - Nanjing IC support
> is not included in current version.
>
> The current support requires a previously flashed firmware to be
> present.
>
> As I did not have access to datasheets for these ICs, I extracted the
> addresses from a couple of config files using a small tool [2]. The
> addresses are identical for the same IC families in all configs I
> observed, however not all of them make sense and I stubbed out firmware
> request support due to this.
>
> I've taken a lot of inspiration from the goodix_berlin driver, but the
> Berlin and GTX8 series of touchscreen ICs differ quite a bit. The driver
> architecture is the same overall, i.e. the power-up sequence and general
> concepts are the mostly same, but it is very clear that they are
> different generations when looking at it in more detail.
Right, this answers my main question about this driver which was:
"why another goodix driver?" (this would be the third one).
I've also compared this driver with the original goodix.c touchscreen
driver (which I know well) and the protocol is somewhat closer
to the original goodix.c driver then it is to goodix_berlin, but still
different enough that having a separate driver is the best option IMHO.
...
> From what I can tell, the evolution seems to be:
> Normandy -> Yellowstone -> Berlin
> since Normandy and Yellowstone are already quite different (especially
> with the way checksums work) and Yellowstone has a couple of things
> (checksum, fw_version) that appear similar to Berlin series ICs.
You forgot the original goodix.c driver, adding that it seems
the evolution is:
GTx1/GTx2/GTx6 -> Normandy -> Yellowstone -> Berlin
With GTx1/GTx2/GTx6 having no checksum at all (and 16 bit
registers) and some of the original GTx1/GTx2/GTx6 don't have
nvram for the firmware, so Linux must upload firmware every boot.
Anyways I agree that these are different enough from the existing
goodix and goodix_berlin drivers, so based on that (and only on that):
Acked-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
Regards,
Hans
^ permalink raw reply
* [PATCH] HID: sony: add support for more instruments
From: Rosalie Wanders @ 2026-02-28 6:05 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Rosalie Wanders, Sanjay Govind, Brenton Simpson, linux-input,
linux-kernel
This patch adds support for the following instruments:
* Rock Band 1/2/3 Wii/PS3 instruments
* Rock Band 3 PS3 Pro instruments
* DJ Hero Turntable
Wii and PS3 instruments are the same besides the vendor and product ID.
This patch also fixes the mappings for the existing Guitar Hero
instruments.
Co-developed-by: Sanjay Govind <sanjay.govind9@gmail.com>
Signed-off-by: Sanjay Govind <sanjay.govind9@gmail.com>
Co-developed-by: Brenton Simpson <appsforartists@google.com>
Signed-off-by: Brenton Simpson <appsforartists@google.com>
Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
drivers/hid/hid-ids.h | 28 ++++-
drivers/hid/hid-sony.c | 278 ++++++++++++++++++++++++++++++++++-------
2 files changed, 259 insertions(+), 47 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 3e299a30dcde..b0bb34fe000b 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -664,6 +664,19 @@
#define USB_DEVICE_ID_UGCI_FLYING 0x0020
#define USB_DEVICE_ID_UGCI_FIGHTING 0x0030
+#define USB_VENDOR_ID_HARMONIX 0x1bad
+#define USB_DEVICE_ID_HARMONIX_WII_RB1_GUITAR 0x0004
+#define USB_DEVICE_ID_HARMONIX_WII_RB2_GUITAR 0x3010
+#define USB_DEVICE_ID_HARMONIX_WII_RB1_DRUMS 0x0005
+#define USB_DEVICE_ID_HARMONIX_WII_RB2_DRUMS 0x3110
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_DRUMS_MODE 0x3138
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MUSTANG_GUITAR 0x3430
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_SQUIRE_GUITAR 0x3530
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_MUSTANG_MODE 0x3438
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_SQUIRE_MODE 0x3538
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD 0x3330
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE 0x3338
+
#define USB_VENDOR_ID_HP 0x03f0
#define USB_PRODUCT_ID_HP_ELITE_PRESENTER_MOUSE_464A 0x464a
#define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0A4A 0x0a4a
@@ -1298,8 +1311,19 @@
#define USB_DEVICE_ID_SONY_WIRELESS_BUZZ_CONTROLLER 0x1000
#define USB_VENDOR_ID_SONY_RHYTHM 0x12ba
-#define USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE_DONGLE 0x074b
-#define USB_DEVICE_ID_SONY_PS3_GUITAR_DONGLE 0x0100
+#define USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE 0x074b
+#define USB_DEVICE_ID_SONY_PS3_GH_GUITAR 0x0100
+#define USB_DEVICE_ID_SONY_PS3_GH_DRUMS 0x0120
+#define USB_DEVICE_ID_SONY_PS3_DJH_TURNTABLE 0x0140
+#define USB_DEVICE_ID_SONY_PS3_RB_GUITAR 0x0200
+#define USB_DEVICE_ID_SONY_PS3_RB_DRUMS 0x0210
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_DRUMS_MODE 0x0218
+#define USB_DEVICE_ID_SONY_PS3_RB3_MUSTANG_GUITAR 0x2430
+#define USB_DEVICE_ID_SONY_PS3_RB3_SQUIRE_GUITAR 0x2530
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_MUSTANG_MODE 0x2438
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_SQUIRE_MODE 0x2538
+#define USB_DEVICE_ID_SONY_PS3_RB3_KEYBOARD 0x2330
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_KEYBOARD_MODE 0x2338
#define USB_VENDOR_ID_SINO_LITE 0x1345
#define USB_DEVICE_ID_SINO_LITE_CONTROLLER 0x3008
diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index a89af14e4acc..4b0992cfc8a7 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * HID driver for Sony / PS2 / PS3 / PS4 BD devices.
+ * HID driver for Sony / PS2 / PS3 / PS4 / PS5 BD devices.
*
* Copyright (c) 1999 Andreas Gal
* Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
@@ -12,9 +12,10 @@
* Copyright (c) 2014-2016 Frank Praznik <frank.praznik@gmail.com>
* Copyright (c) 2018 Todd Kelner
* Copyright (c) 2020-2021 Pascal Giard <pascal.giard@etsmtl.ca>
- * Copyright (c) 2020 Sanjay Govind <sanjay.govind9@gmail.com>
+ * Copyright (c) 2020-2026 Sanjay Govind <sanjay.govind9@gmail.com>
* Copyright (c) 2021 Daniel Nguyen <daniel.nguyen.1@ens.etsmtl.ca>
* Copyright (c) 2026 Rosalie Wanders <rosalie@mailbox.org>
+ * Copyright (c) 2026 Brenton Simpson <appsforartists@google.com>
*/
/*
@@ -59,12 +60,15 @@
#define NSG_MR5U_REMOTE_BT BIT(11)
#define NSG_MR7U_REMOTE_BT BIT(12)
#define SHANWAN_GAMEPAD BIT(13)
-#define GH_GUITAR_CONTROLLER BIT(14)
-#define GHL_GUITAR_PS3WIIU BIT(15)
-#define GHL_GUITAR_PS4 BIT(16)
-#define RB4_GUITAR_PS4_USB BIT(17)
-#define RB4_GUITAR_PS4_BT BIT(18)
-#define RB4_GUITAR_PS5 BIT(19)
+#define INSTRUMENT BIT(14)
+#define GH_GUITAR_TILT BIT(15)
+#define GHL_GUITAR_PS3WIIU BIT(16)
+#define GHL_GUITAR_PS4 BIT(17)
+#define RB4_GUITAR_PS4_USB BIT(18)
+#define RB4_GUITAR_PS4_BT BIT(19)
+#define RB4_GUITAR_PS5 BIT(20)
+#define RB3_PS3_PRO_INSTRUMENT BIT(21)
+#define PS3_DJH_TURNTABLE BIT(22)
#define SIXAXIS_CONTROLLER (SIXAXIS_CONTROLLER_USB | SIXAXIS_CONTROLLER_BT)
#define MOTION_CONTROLLER (MOTION_CONTROLLER_USB | MOTION_CONTROLLER_BT)
@@ -87,6 +91,10 @@
#define GHL_GUITAR_POKE_INTERVAL 8 /* In seconds */
#define GUITAR_TILT_USAGE 44
+#define TURNTABLE_EFFECTS_KNOB_USAGE 44
+#define TURNTABLE_PLATTER_BUTTONS_USAGE 45
+#define TURNTABLE_CROSS_FADER_USAGE 46
+
/* Magic data taken from GHLtarUtility:
* https://github.com/ghlre/GHLtarUtility/blob/master/PS3Guitar.cs
* Note: The Wii U and PS3 dongles happen to share the same!
@@ -102,6 +110,13 @@ static const char ghl_ps4_magic_data[] = {
0x30, 0x02, 0x08, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00
};
+/* Rock Band 3 PS3 Pro Instruments require sending a report
+ * once an instrument is connected to its dongle.
+ * We need to retry sending these reports,
+ * but to avoid doing this too often we delay the retries
+ */
+#define RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL 8 /* In seconds */
+
/* PS/3 Motion controller */
static const u8 motion_rdesc[] = {
0x05, 0x01, /* Usage Page (Desktop), */
@@ -427,20 +442,25 @@ static const unsigned int rb4_absmap[] = {
[0x31] = ABS_Y,
};
-static const unsigned int rb4_keymap[] = {
- [0x1] = BTN_WEST, /* Square */
- [0x2] = BTN_SOUTH, /* Cross */
- [0x3] = BTN_EAST, /* Circle */
- [0x4] = BTN_NORTH, /* Triangle */
- [0x5] = BTN_TL, /* L1 */
- [0x6] = BTN_TR, /* R1 */
- [0x7] = BTN_TL2, /* L2 */
- [0x8] = BTN_TR2, /* R2 */
- [0x9] = BTN_SELECT, /* Share */
- [0xa] = BTN_START, /* Options */
- [0xb] = BTN_THUMBL, /* L3 */
- [0xc] = BTN_THUMBR, /* R3 */
- [0xd] = BTN_MODE, /* PS */
+static const unsigned int ps3_turntable_absmap[] = {
+ [0x32] = ABS_X,
+ [0x35] = ABS_Y,
+};
+
+static const unsigned int instrument_keymap[] = {
+ [0x1] = BTN_WEST,
+ [0x2] = BTN_SOUTH,
+ [0x3] = BTN_EAST,
+ [0x4] = BTN_NORTH,
+ [0x5] = BTN_TL,
+ [0x6] = BTN_TR,
+ [0x7] = BTN_TL2,
+ [0x8] = BTN_TR2,
+ [0x9] = BTN_SELECT,
+ [0xa] = BTN_START,
+ [0xb] = BTN_THUMBL,
+ [0xc] = BTN_THUMBR,
+ [0xd] = BTN_MODE,
};
static enum power_supply_property sony_battery_props[] = {
@@ -490,6 +510,7 @@ struct motion_output_report_02 {
#define SIXAXIS_REPORT_0xF2_SIZE 17
#define SIXAXIS_REPORT_0xF5_SIZE 8
#define MOTION_REPORT_0x02_SIZE 49
+#define PRO_INSTRUMENT_0x00_SIZE 8
#define SENSOR_SUFFIX " Motion Sensors"
#define TOUCHPAD_SUFFIX " Touchpad"
@@ -539,6 +560,9 @@ struct sony_sc {
/* GH Live */
struct urb *ghl_urb;
struct timer_list ghl_poke_timer;
+
+ /* Rock Band 3 Pro Instruments */
+ unsigned long rb3_pro_poke_jiffies;
};
static void sony_set_leds(struct sony_sc *sc);
@@ -610,35 +634,108 @@ static int ghl_init_urb(struct sony_sc *sc, struct usb_device *usbdev,
return 0;
}
-static int gh_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+
+
+/*
+ * Sending HID_REQ_SET_REPORT enables the full report. Without this
+ * Rock Band 3 Pro instruments only report navigation events
+ */
+static int rb3_pro_instrument_enable_full_report(struct sony_sc *sc)
+{
+ struct hid_device *hdev = sc->hdev;
+ static const u8 report[] = { 0x00, 0xE9, 0x00, 0x89, 0x1B,
+ 0x00, 0x00, 0x00, 0x02, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0x00, 0x00,
+ 0x00, 0x00, 0x89, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xE9, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00 };
+ u8 *buf;
+ int ret;
+
+ buf = kmemdup(report, sizeof(report), GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, buf[0], buf, sizeof(report),
+ HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
+
+ kfree(buf);
+
+ return ret;
+}
+
+static int djh_turntable_mapping(struct hid_device *hdev, struct hid_input *hi,
struct hid_field *field, struct hid_usage *usage,
unsigned long **bit, int *max)
{
if ((usage->hid & HID_USAGE_PAGE) == HID_UP_MSVENDOR) {
unsigned int abs = usage->hid & HID_USAGE;
- if (abs == GUITAR_TILT_USAGE) {
+ if (abs == TURNTABLE_CROSS_FADER_USAGE) {
+ hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RX);
+ return 1;
+ } else if (abs == TURNTABLE_EFFECTS_KNOB_USAGE) {
hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RY);
return 1;
+ } else if (abs == TURNTABLE_PLATTER_BUTTONS_USAGE) {
+ hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RZ);
+ return 1;
}
+ } else if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
+ unsigned int abs = usage->hid & HID_USAGE;
+
+ if (abs >= ARRAY_SIZE(ps3_turntable_absmap))
+ return -1;
+
+ abs = ps3_turntable_absmap[abs];
+
+ hid_map_usage_clear(hi, usage, bit, max, EV_ABS, abs);
+ return 1;
}
return 0;
}
-static int rb4_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+static int instrument_mapping(struct hid_device *hdev, struct hid_input *hi,
struct hid_field *field, struct hid_usage *usage,
unsigned long **bit, int *max)
{
if ((usage->hid & HID_USAGE_PAGE) == HID_UP_BUTTON) {
unsigned int key = usage->hid & HID_USAGE;
- if (key >= ARRAY_SIZE(rb4_keymap))
+ if (key >= ARRAY_SIZE(instrument_keymap))
return 0;
- key = rb4_keymap[key];
+ key = instrument_keymap[key];
hid_map_usage_clear(hi, usage, bit, max, EV_KEY, key);
return 1;
- } else if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
+ }
+
+ return 0;
+}
+
+static int gh_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+ struct hid_field *field, struct hid_usage *usage,
+ unsigned long **bit, int *max)
+{
+ if ((usage->hid & HID_USAGE_PAGE) == HID_UP_MSVENDOR) {
+ unsigned int abs = usage->hid & HID_USAGE;
+
+ if (abs == GUITAR_TILT_USAGE) {
+ hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RY);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int rb4_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+ struct hid_field *field, struct hid_usage *usage,
+ unsigned long **bit, int *max)
+{
+ if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
unsigned int abs = usage->hid & HID_USAGE;
/* Let the HID parser deal with the HAT. */
@@ -1052,6 +1149,18 @@ static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
return 1;
}
+ /* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
+ * sending full reports, and 0x02 when only sending navigation.
+ */
+ if ((sc->quirks & RB3_PS3_PRO_INSTRUMENT) && rd[24] == 0x02) {
+ /* Only attempt to enable report every 8 seconds */
+ if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
+ sc->rb3_pro_poke_jiffies = jiffies +
+ (RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL * HZ);
+ rb3_pro_instrument_enable_full_report(sc);
+ }
+ }
+
if (sc->defer_initialization) {
sc->defer_initialization = 0;
sony_schedule_work(sc, SONY_WORKER_STATE);
@@ -1065,6 +1174,7 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
unsigned long **bit, int *max)
{
struct sony_sc *sc = hid_get_drvdata(hdev);
+ int ret;
if (sc->quirks & BUZZ_CONTROLLER) {
unsigned int key = usage->hid & HID_USAGE;
@@ -1098,9 +1208,19 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
if (sc->quirks & SIXAXIS_CONTROLLER)
return sixaxis_mapping(hdev, hi, field, usage, bit, max);
- if (sc->quirks & GH_GUITAR_CONTROLLER)
+ /* INSTRUMENT quirk is used as a base mapping for instruments */
+ if (sc->quirks & INSTRUMENT) {
+ ret = instrument_mapping(hdev, hi, field, usage, bit, max);
+ if (ret != 0)
+ return ret;
+ }
+
+ if (sc->quirks & GH_GUITAR_TILT)
return gh_guitar_mapping(hdev, hi, field, usage, bit, max);
+ if (sc->quirks & PS3_DJH_TURNTABLE)
+ return djh_turntable_mapping(hdev, hi, field, usage, bit, max);
+
if (sc->quirks & (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT))
return rb4_guitar_mapping(hdev, hi, field, usage, bit, max);
@@ -2060,6 +2180,19 @@ static int sony_input_configured(struct hid_device *hdev,
}
sony_init_output_report(sc, sixaxis_send_output_report);
+ } else if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+ /*
+ * Rock Band 3 PS3 Pro Instruments also do not handle HID Output
+ * Reports on the interrupt EP like they should, so we need to force
+ * HID output reports to use HID_REQ_SET_REPORT on the Control EP.
+ *
+ * There is also another issue about HID Output Reports via USB,
+ * these instruments do not want the report_id as part of the data
+ * packet, so we have to discard buf[0] when sending the actual
+ * control message, even for numbered reports.
+ */
+ hdev->quirks |= HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP;
+ hdev->quirks |= HID_QUIRK_SKIP_OUTPUT_REPORT_ID;
} else if (sc->quirks & SIXAXIS_CONTROLLER_USB) {
/*
* The Sony Sixaxis does not handle HID Output Reports on the
@@ -2227,6 +2360,10 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
goto err;
}
+ if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+ sc->rb3_pro_poke_jiffies = 0;
+ }
+
if (sc->quirks & (GHL_GUITAR_PS3WIIU | GHL_GUITAR_PS4)) {
if (!hid_is_usb(hdev)) {
ret = -EINVAL;
@@ -2364,35 +2501,86 @@ static const struct hid_device_id sony_devices[] = {
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_SMK, USB_DEVICE_ID_SMK_NSG_MR7U_REMOTE),
.driver_data = NSG_MR7U_REMOTE_BT },
/* Guitar Hero Live PS3 and Wii U guitar dongles */
- { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE_DONGLE),
- .driver_data = GHL_GUITAR_PS3WIIU | GH_GUITAR_CONTROLLER },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE),
+ .driver_data = GHL_GUITAR_PS3WIIU | GH_GUITAR_TILT | INSTRUMENT },
/* Guitar Hero PC Guitar Dongle */
{ HID_USB_DEVICE(USB_VENDOR_ID_REDOCTANE, USB_DEVICE_ID_REDOCTANE_GUITAR_DONGLE),
- .driver_data = GH_GUITAR_CONTROLLER },
- /* Guitar Hero PS3 World Tour Guitar Dongle */
- { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GUITAR_DONGLE),
- .driver_data = GH_GUITAR_CONTROLLER },
+ .driver_data = GH_GUITAR_TILT | INSTRUMENT },
+ /* Guitar Hero PS3 Guitar Dongle */
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GH_GUITAR),
+ .driver_data = GH_GUITAR_TILT | INSTRUMENT },
+ /* Guitar Hero PS3 Drum Dongle */
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GH_DRUMS),
+ .driver_data = INSTRUMENT },
+ /* DJ Hero PS3 Guitar Dongle */
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_DJH_TURNTABLE),
+ .driver_data = PS3_DJH_TURNTABLE | INSTRUMENT },
/* Guitar Hero Live PS4 guitar dongles */
{ HID_USB_DEVICE(USB_VENDOR_ID_REDOCTANE, USB_DEVICE_ID_REDOCTANE_PS4_GHLIVE_DONGLE),
- .driver_data = GHL_GUITAR_PS4 | GH_GUITAR_CONTROLLER },
+ .driver_data = GHL_GUITAR_PS4 | GH_GUITAR_TILT | INSTRUMENT },
+ /* Rock Band 1 Wii instruments */
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB1_GUITAR),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB1_DRUMS),
+ .driver_data = INSTRUMENT },
+ /* Rock Band 2 Wii instruments */
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB2_GUITAR),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB2_DRUMS),
+ .driver_data = INSTRUMENT },
+ /* Rock Band 3 Wii instruments */
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_DRUMS_MODE),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MUSTANG_GUITAR),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_SQUIRE_GUITAR),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_MUSTANG_MODE),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_SQUIRE_MODE),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE),
+ .driver_data = INSTRUMENT },
+ /* Rock Band 3 PS3 instruments */
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB_GUITAR),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB_DRUMS),
+ .driver_data = INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_DRUMS_MODE),
+ .driver_data = INSTRUMENT },
+ /* Rock Band 3 PS3 Pro instruments */
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MUSTANG_GUITAR),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_SQUIRE_GUITAR),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_MUSTANG_MODE),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_SQUIRE_MODE),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_KEYBOARD),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_KEYBOARD_MODE),
+ .driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
/* Rock Band 4 PS4 guitars */
{ HID_USB_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS4_RIFFMASTER),
- .driver_data = RB4_GUITAR_PS4_USB },
+ .driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS4_GIBSON_SG),
- .driver_data = RB4_GUITAR_PS4_USB },
+ .driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS4_GIBSON_SG_DONGLE),
- .driver_data = RB4_GUITAR_PS4_USB },
+ .driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS4_JAGUAR),
- .driver_data = RB4_GUITAR_PS4_BT },
+ .driver_data = RB4_GUITAR_PS4_BT | INSTRUMENT },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_MADCATZ, USB_DEVICE_ID_MADCATZ_PS4_STRATOCASTER),
- .driver_data = RB4_GUITAR_PS4_BT },
+ .driver_data = RB4_GUITAR_PS4_BT | INSTRUMENT },
/* Rock Band 4 PS5 guitars */
{ HID_USB_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS5_RIFFMASTER),
- .driver_data = RB4_GUITAR_PS5 },
+ .driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS5_GIBSON_SG),
- .driver_data = RB4_GUITAR_PS5 },
+ .driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS5_GIBSON_SG_DONGLE),
- .driver_data = RB4_GUITAR_PS5 },
+ .driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
{ }
};
MODULE_DEVICE_TABLE(hid, sony_devices);
@@ -2428,5 +2616,5 @@ static void __exit sony_exit(void)
module_init(sony_init);
module_exit(sony_exit);
-MODULE_DESCRIPTION("HID driver for Sony / PS2 / PS3 / PS4 BD devices");
+MODULE_DESCRIPTION("HID driver for Sony / PS2 / PS3 / PS4 / PS5 BD devices");
MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH] HID: hid-lenovo-go-s: Remove unneeded semicolon
From: Chen Ni @ 2026-02-28 3:43 UTC (permalink / raw)
To: derekjohn.clark
Cc: mpearson-lenovo, jikos, bentiss, linux-input, linux-kernel,
Chen Ni
Remove unnecessary semicolons reported by Coccinelle/coccicheck and the
semantic patch at scripts/coccinelle/misc/semicolon.cocci.
Signed-off-by: Chen Ni <nichen@iscas.ac.cn>
---
drivers/hid/hid-lenovo-go-s.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index cacc5bd5ed2b..d1eb067509f6 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -1102,7 +1102,7 @@ static void hid_gos_brightness_set(struct led_classdev *led_cdev,
default:
dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n",
ret);
- };
+ }
}
#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \
--
2.25.1
^ permalink raw reply related
* [PATCH] HID: hid-lenovo-go: Remove unneeded semicolon
From: Chen Ni @ 2026-02-28 3:39 UTC (permalink / raw)
To: derekjohn.clark
Cc: mpearson-lenovo, jikos, bentiss, linux-input, linux-kernel,
Chen Ni
Remove unnecessary semicolons after switch statements and function
bodies. Most issues were reported by Coccinelle/coccicheck using the
semantic patch at scripts/coccinelle/misc/semicolon.cocci. Additional
instances found during manual code review were also fixed.
Signed-off-by: Chen Ni <nichen@iscas.ac.cn>
---
drivers/hid/hid-lenovo-go.c | 52 ++++++++++++++++++-------------------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 6972d13802e2..77e3823447e5 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -455,7 +455,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
case FEATURE_IMU_BYPASS:
switch (cmd_rep->device_type) {
case LEFT_CONTROLLER:
@@ -466,7 +466,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
break;
case FEATURE_LIGHT_ENABLE:
drvdata.rgb_en = cmd_rep->data[0];
@@ -481,7 +481,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
break;
case FEATURE_TOUCHPAD_ENABLE:
drvdata.tp_en = cmd_rep->data[0];
@@ -515,7 +515,7 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
break;
case RUMBLE_MODE:
switch (cmd_rep->device_type) {
@@ -527,7 +527,7 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
case TP_VIBRATION_ENABLE:
drvdata.tp_vibration_en = cmd_rep->data[0];
return 0;
@@ -625,7 +625,7 @@ static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep)
return 0;
default:
return -EINVAL;
- };
+ }
}
static int hid_go_set_event_return(struct command_report *cmd_rep)
@@ -699,14 +699,14 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
default:
ret = -EINVAL;
break;
- };
+ }
break;
case OS_MODE_DATA:
ret = hid_go_os_mode_cfg_event(cmd_rep);
break;
default:
goto passthrough;
- };
+ }
dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
GO_PACKET_SIZE, data);
@@ -925,7 +925,7 @@ static ssize_t feature_status_store(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
if (ret < 0)
return ret;
@@ -1013,7 +1013,7 @@ static ssize_t feature_status_show(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
count = sysfs_emit(buf, "%u\n", i);
break;
case FEATURE_FPS_SWITCH_STATUS:
@@ -1032,7 +1032,7 @@ static ssize_t feature_status_show(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
return count;
}
@@ -1070,7 +1070,7 @@ static ssize_t feature_status_options(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
if (count)
buf[count - 1] = '\n';
@@ -1111,7 +1111,7 @@ static ssize_t motor_config_store(struct device *dev,
ret = sysfs_match_string(intensity_text, buf);
val = ret;
break;
- };
+ }
if (ret < 0)
return ret;
@@ -1161,7 +1161,7 @@ static ssize_t motor_config_show(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
if (i >= ARRAY_SIZE(enabled_status_text))
return -EINVAL;
@@ -1177,7 +1177,7 @@ static ssize_t motor_config_show(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
if (i >= ARRAY_SIZE(rumble_mode_text))
return -EINVAL;
@@ -1197,7 +1197,7 @@ static ssize_t motor_config_show(struct device *dev,
count = sysfs_emit(buf, "%s\n", intensity_text[i]);
break;
- };
+ }
return count;
}
@@ -1232,7 +1232,7 @@ static ssize_t motor_config_options(struct device *dev,
enabled_status_text[i]);
}
break;
- };
+ }
if (count)
buf[count - 1] = '\n';
@@ -1333,7 +1333,7 @@ static ssize_t device_status_show(struct device *dev,
break;
default:
return -EINVAL;
- };
+ }
if (i >= ARRAY_SIZE(cal_status_text))
return -EINVAL;
@@ -1459,7 +1459,7 @@ static int rgb_attr_show(void)
index = drvdata.rgb_profile + 3;
return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
-};
+}
static ssize_t rgb_effect_store(struct device *dev,
struct device_attribute *attr, const char *buf,
@@ -1489,7 +1489,7 @@ static ssize_t rgb_effect_store(struct device *dev,
drvdata.rgb_effect = effect;
return count;
-};
+}
static ssize_t rgb_effect_show(struct device *dev,
struct device_attribute *attr, char *buf)
@@ -1552,7 +1552,7 @@ static ssize_t rgb_speed_store(struct device *dev,
drvdata.rgb_speed = val;
return count;
-};
+}
static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
char *buf)
@@ -1594,7 +1594,7 @@ static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
drvdata.rgb_mode = val;
return count;
-};
+}
static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
char *buf)
@@ -1609,7 +1609,7 @@ static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
return -EINVAL;
return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
-};
+}
static ssize_t rgb_mode_index_show(struct device *dev,
struct device_attribute *attr, char *buf)
@@ -1649,7 +1649,7 @@ static ssize_t rgb_profile_store(struct device *dev,
drvdata.rgb_profile = val;
return count;
-};
+}
static ssize_t rgb_profile_show(struct device *dev,
struct device_attribute *attr, char *buf)
@@ -1665,7 +1665,7 @@ static ssize_t rgb_profile_show(struct device *dev,
return -EINVAL;
return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
-};
+}
static ssize_t rgb_profile_range_show(struct device *dev,
struct device_attribute *attr, char *buf)
@@ -1704,7 +1704,7 @@ static void hid_go_brightness_set(struct led_classdev *led_cdev,
break;
default:
dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
- };
+ }
}
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
--
2.25.1
^ permalink raw reply related
* [PATCH v2 2/3] Input: add support for Goodix GTX8 Touchscreen ICs
From: Aelin Reidel @ 2026-02-28 1:56 UTC (permalink / raw)
To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Hans de Goede, Neil Armstrong, Henrik Rydberg
Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
~postmarketos/upstreaming, Aelin Reidel, Piyush Raj Chouhan,
Alexander Koskovich
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>
Add initial support for the Goodix GTX8 touchscreen ICs.
These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
and gesture events.
This driver is derived from the Goodix gtx8_driver_linux available at
[1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.
The current implementation only supports Normandy and Yellowstone type
ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
don't have a device with GTX8 over SPI at hand. Adding support for SPI
should be fairly easy in the future, since the code uses a regmap.
Support for advanced features like:
- Firmware updates
- Stylus events
- Gesture events
- Nanjing IC support
is not included in current version.
The current support requires a previously flashed firmware to be
present.
As I did not have access to datasheets for these ICs, I extracted the
addresses from a couple of config files using a small tool [2]. The
addresses are identical for the same IC families in all configs I
observed, however not all of them make sense and I stubbed out firmware
request support due to this.
[1] https://github.com/goodix/gtx8_driver_linux
[2] https://github.com/sm7150-mainline/goodix-cfg-bin
Tested-by: Piyush Raj Chouhan <pc1598@mainlining.org>
Tested-by: Alexander Koskovich <AKoskovich@pm.me>
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
drivers/input/touchscreen/Kconfig | 15 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/goodix_gtx8.c | 563 ++++++++++++++++++++++++++++++++
drivers/input/touchscreen/goodix_gtx8.h | 141 ++++++++
4 files changed, 720 insertions(+)
diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 7d5b72ee07fa1313da39a625b5129a0459720865..099ccd3679383dcf037bc7c6e6a3dbf0741722b4 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -429,6 +429,21 @@ config TOUCHSCREEN_GOODIX_BERLIN_SPI
To compile this driver as a module, choose M here: the
module will be called goodix_berlin_spi.
+config TOUCHSCREEN_GOODIX_GTX8
+ tristate "Goodix GTX8 touchscreen"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say Y here if you have a Goodix GTX8 IC connected to
+ your system via I2C. This driver supports Normandy and
+ Yellowstone ICs like the GT9886 and GT9896.
+ They are commonly found in mobile phones.
+
+ if unsure, say N.
+
+ To compile this driver as a module, choose M here: the
+ module will be called goodix_gtx8.
+
config TOUCHSCREEN_HIDEEP
tristate "HiDeep Touch IC"
depends on I2C
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index ab9abd151078831a4b22d6998e00ef74fe01c356..9bcb8f01ea785dcbe2a22bd3293601dd4259ba1d 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -48,6 +48,7 @@ obj-$(CONFIG_TOUCHSCREEN_GOODIX) += goodix_ts.o
obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_CORE) += goodix_berlin_core.o
obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_I2C) += goodix_berlin_i2c.o
obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_SPI) += goodix_berlin_spi.o
+obj-$(CONFIG_TOUCHSCREEN_GOODIX_GTX8) += goodix_gtx8.o
obj-$(CONFIG_TOUCHSCREEN_HIDEEP) += hideep.o
obj-$(CONFIG_TOUCHSCREEN_HIMAX_HX852X) += himax_hx852x.o
obj-$(CONFIG_TOUCHSCREEN_HYNITRON_CSTXXX) += hynitron_cstxxx.o
diff --git a/drivers/input/touchscreen/goodix_gtx8.c b/drivers/input/touchscreen/goodix_gtx8.c
new file mode 100644
index 0000000000000000000000000000000000000000..3fbebfa8b75672866788adf89d83be16e32abf81
--- /dev/null
+++ b/drivers/input/touchscreen/goodix_gtx8.c
@@ -0,0 +1,563 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for Goodix GTX8 Touchscreens
+ *
+ * Copyright (c) 2019 - 2020 Goodix, Inc.
+ * Copyright (C) 2023 Linaro Ltd.
+ * Copyright (c) 2025 Aelin Reidel <aelin@mainlining.org>
+ *
+ * Based on gtx8_driver_linux vendor driver and goodix_berlin kernel driver.
+ *
+ * The driver currently relies on the pre-flashed firmware and only supports
+ * Normandy / Yellowstone ICs.
+ * Pen support is also missing.
+ */
+#include <linux/bitfield.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/unaligned.h>
+
+#include "goodix_gtx8.h"
+
+static const struct regmap_config goodix_gtx8_regmap_conf = {
+ .reg_bits = 16,
+ .val_bits = 8,
+ .max_raw_read = I2C_MAX_TRANSFER_SIZE,
+ .max_raw_write = I2C_MAX_TRANSFER_SIZE,
+};
+
+/* vendor & product left unassigned here, should probably be updated from fw info */
+static const struct input_id goodix_gtx8_input_id = {
+ .bustype = BUS_I2C,
+};
+
+static bool goodix_gtx8_checksum_valid_normandy(const u8 *data, int size)
+{
+ u8 cal_checksum = 0;
+ int i;
+
+ if (size < GOODIX_GTX8_CHECKSUM_SIZE)
+ return false;
+
+ for (i = 0; i < size; i++)
+ cal_checksum += data[i];
+
+ return cal_checksum == 0;
+}
+
+static bool goodix_gtx8_checksum_valid_yellowstone(const u8 *data, int size)
+{
+ u16 cal_checksum = 0;
+ u16 r_checksum;
+ int i;
+
+ if (size < GOODIX_GTX8_CHECKSUM_SIZE)
+ return false;
+
+ for (i = 0; i < size - GOODIX_GTX8_CHECKSUM_SIZE; i++)
+ cal_checksum += data[i];
+
+ r_checksum = get_unaligned_be16(&data[i]);
+
+ return cal_checksum == r_checksum;
+}
+
+static int goodix_gtx8_get_remaining_contacts(struct goodix_gtx8_core *cd,
+ int n)
+{
+ size_t offset = cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
+ GOODIX_GTX8_CHECKSUM_SIZE;
+ u32 addr = cd->ic_data->touch_data_addr + offset;
+ int error;
+
+ error = regmap_raw_read(cd->regmap, addr, &cd->event_buffer[offset],
+ (n - 1) * GOODIX_GTX8_TOUCH_SIZE);
+ if (error) {
+ dev_err_ratelimited(cd->dev, "failed to get touch data, %d\n",
+ error);
+ return error;
+ }
+
+ return 0;
+}
+
+static void goodix_gtx8_report_state(struct goodix_gtx8_core *cd, u8 touch_num,
+ union goodix_gtx8_touch *touch_data)
+{
+ union goodix_gtx8_touch *t;
+ int i;
+ u8 finger_id;
+
+ for (i = 0; i < touch_num; i++) {
+ t = &touch_data[i];
+
+ if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+ input_mt_slot(cd->input_dev, t->normandy.finger_id);
+ input_mt_report_slot_state(cd->input_dev,
+ MT_TOOL_FINGER, true);
+
+ touchscreen_report_pos(cd->input_dev, &cd->props,
+ __le16_to_cpu(t->normandy.x),
+ __le16_to_cpu(t->normandy.y),
+ true);
+ input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
+ t->normandy.w);
+ } else {
+ finger_id = FIELD_GET(
+ GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE,
+ t->yellowstone.finger_id);
+ input_mt_slot(cd->input_dev, finger_id);
+ input_mt_report_slot_state(cd->input_dev,
+ MT_TOOL_FINGER, true);
+
+ touchscreen_report_pos(cd->input_dev, &cd->props,
+ __be16_to_cpu(t->yellowstone.x),
+ __be16_to_cpu(t->yellowstone.y),
+ true);
+ input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
+ t->yellowstone.w);
+ }
+ }
+
+ input_mt_sync_frame(cd->input_dev);
+ input_sync(cd->input_dev);
+}
+
+static void goodix_gtx8_touch_handler(struct goodix_gtx8_core *cd, u8 touch_num,
+ union goodix_gtx8_touch *touch_data)
+{
+ int error;
+
+ touch_num = FIELD_GET(GOODIX_GTX8_TOUCH_COUNT_MASK, touch_num);
+
+ if (touch_num > GOODIX_GTX8_MAX_TOUCH) {
+ dev_warn(cd->dev, "invalid touch num %d\n", touch_num);
+ return;
+ }
+
+ if (touch_num > 1) {
+ /* read additional contact data if more than 1 touch event */
+ error = goodix_gtx8_get_remaining_contacts(cd, touch_num);
+ if (error)
+ return;
+ }
+
+ if (touch_num) {
+ /*
+ * Normandy checksum is for the entire read buffer,
+ * Yellowstone is only for the touch data (since header
+ * has a separate checksum)
+ */
+ if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+ int len = GOODIX_GTX8_HEADER_SIZE_NORMANDY +
+ touch_num * GOODIX_GTX8_TOUCH_SIZE +
+ GOODIX_GTX8_CHECKSUM_SIZE;
+ if (!goodix_gtx8_checksum_valid_normandy(
+ cd->event_buffer, len)) {
+ dev_err(cd->dev,
+ "touch data checksum error: %*ph\n",
+ len, cd->event_buffer);
+ return;
+ }
+ } else {
+ int len = touch_num * GOODIX_GTX8_TOUCH_SIZE +
+ GOODIX_GTX8_CHECKSUM_SIZE;
+ if (!goodix_gtx8_checksum_valid_yellowstone(
+ (u8 *)touch_data, len)) {
+ dev_err(cd->dev,
+ "touch data checksum error: %*ph\n",
+ len, (u8 *)touch_data);
+ return;
+ }
+ }
+ }
+
+ goodix_gtx8_report_state(cd, touch_num, touch_data);
+}
+
+static irqreturn_t goodix_gtx8_irq(int irq, void *data)
+{
+ struct goodix_gtx8_core *cd = data;
+ struct goodix_gtx8_event_normandy *ev_normandy;
+ struct goodix_gtx8_event_yellowstone *ev_yellowstone;
+ union goodix_gtx8_touch *touch_data;
+ int error;
+ u8 status, touch_num;
+
+ error = regmap_raw_read(
+ cd->regmap, cd->ic_data->touch_data_addr, cd->event_buffer,
+ cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
+ GOODIX_GTX8_CHECKSUM_SIZE);
+ if (error) {
+ dev_warn_ratelimited(
+ cd->dev, "failed to get event head data: %d\n", error);
+ goto out;
+ }
+
+ /*
+ * Both IC types have the same data in the header, just at different
+ * offsets
+ */
+ if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+ ev_normandy =
+ (struct goodix_gtx8_event_normandy *)cd->event_buffer;
+ status = ev_normandy->hdr.status;
+ touch_num = ev_normandy->hdr.touch_num;
+ touch_data = (union goodix_gtx8_touch *)ev_normandy->data;
+ } else {
+ ev_yellowstone = (struct goodix_gtx8_event_yellowstone *)
+ cd->event_buffer;
+ status = ev_yellowstone->hdr.status;
+ touch_num = ev_yellowstone->hdr.touch_num;
+ touch_data = (union goodix_gtx8_touch *)ev_yellowstone->data;
+ }
+
+ if (status == 0)
+ goto out;
+
+ /* Yellowstone ICs have a checksum for the header */
+ if (cd->ic_data->ic_type == IC_TYPE_YELLOWSTONE &&
+ !goodix_gtx8_checksum_valid_yellowstone(
+ cd->event_buffer, GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE)) {
+ dev_warn_ratelimited(cd->dev,
+ "touch head checksum error: %*ph\n",
+ (int)GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
+ cd->event_buffer);
+ goto out_clear;
+ }
+
+ if (status & GOODIX_GTX8_TOUCH_EVENT)
+ goodix_gtx8_touch_handler(cd, touch_num, touch_data);
+
+ if (status & GOODIX_GTX8_REQUEST_EVENT) {
+ /*
+ * All configs seen so far either set the firmware request
+ * address to 0 (Normandy) or have it equal the touch data
+ * address (Yellowstone). Neither seems correct, and this
+ * is not testable. Therefore it is currently omitted.
+ */
+ dev_dbg(cd->dev, "received request event, ignoring\n");
+ }
+
+out_clear:
+ /* Clear up status field */
+ regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);
+
+out:
+ return IRQ_HANDLED;
+}
+
+static int goodix_gtx8_input_dev_config(struct goodix_gtx8_core *cd)
+{
+ struct input_dev *input_dev;
+ int error;
+
+ input_dev = devm_input_allocate_device(cd->dev);
+ if (!input_dev)
+ return -ENOMEM;
+
+ cd->input_dev = input_dev;
+ input_set_drvdata(input_dev, cd);
+
+ input_dev->name = "Goodix GTX8 Capacitive TouchScreen";
+ input_dev->phys = "input/ts";
+
+ input_dev->id = goodix_gtx8_input_id;
+
+ input_set_abs_params(cd->input_dev, ABS_MT_POSITION_X, 0, SZ_64K - 1, 0,
+ 0);
+ input_set_abs_params(cd->input_dev, ABS_MT_POSITION_Y, 0, SZ_64K - 1, 0,
+ 0);
+ input_set_abs_params(cd->input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
+
+ touchscreen_parse_properties(cd->input_dev, true, &cd->props);
+
+ error = input_mt_init_slots(cd->input_dev, GOODIX_GTX8_MAX_TOUCH,
+ INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+ if (error)
+ return error;
+
+ error = input_register_device(cd->input_dev);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static int goodix_gtx8_read_version(struct goodix_gtx8_core *cd)
+{
+ int error;
+
+ /*
+ * The vendor driver reads a whole lot more data to calculate and
+ * verify a checksum. Without documentation, we don't know what
+ * most of that data is, so we only read the parts we know about
+ * and instead ensure their values are as expected
+ */
+ error = regmap_raw_read(cd->regmap, cd->ic_data->fw_version_addr,
+ &cd->fw_version, sizeof(cd->fw_version));
+ if (error) {
+ dev_err(cd->dev, "error reading fw version, %d\n", error);
+ return error;
+ }
+
+ /*
+ * Since we don't verify the checksum, do a basic check that the
+ * product ID meets expectations
+ */
+ if (memcmp(cd->fw_version.product_id, cd->ic_data->product_id,
+ sizeof(cd->fw_version.product_id))) {
+ dev_err(cd->dev, "unexpected product ID, got: %c%c%c%c\n",
+ cd->fw_version.product_id[0],
+ cd->fw_version.product_id[1],
+ cd->fw_version.product_id[2],
+ cd->fw_version.product_id[3]);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int goodix_gtx8_dev_confirm(struct goodix_gtx8_core *cd)
+{
+ u8 rx_buf[1];
+ int retry = 3;
+ int error;
+
+ while (retry--) {
+ /*
+ * test_addr appears to always be the touch_data_addr for
+ * Normandy, but it doesn't really matter since all we
+ * need is a valid address
+ */
+ error = regmap_raw_read(cd->regmap,
+ cd->ic_data->touch_data_addr, rx_buf,
+ sizeof(rx_buf));
+
+ if (!error)
+ return 0;
+
+ usleep_range(5000, 5100);
+ }
+
+ dev_err(cd->dev, "device confirm failed\n");
+
+ return -EINVAL;
+}
+
+static int goodix_gtx8_power_on(struct goodix_gtx8_core *cd)
+{
+ int error;
+
+ error = regulator_enable(cd->vddio);
+ if (error) {
+ dev_err(cd->dev, "Failed to enable VDDIO: %d\n", error);
+ return error;
+ }
+
+ error = regulator_enable(cd->avdd);
+ if (error) {
+ dev_err(cd->dev, "Failed to enable AVDD: %d\n", error);
+ goto err_vddio_disable;
+ }
+
+ /* Vendors usually configure the power on delay as 300ms */
+ msleep(GOODIX_GTX8_POWER_ON_DELAY_MS);
+
+ gpiod_set_value_cansleep(cd->reset_gpio, 0);
+
+ /* Vendor waits 5ms for firmware to initialize */
+ usleep_range(5000, 5100);
+
+ error = goodix_gtx8_dev_confirm(cd);
+ if (error)
+ goto err_dev_reset;
+
+ /* Vendor waits 100ms for firmware to fully boot */
+ msleep(GOODIX_GTX8_NORMAL_RESET_DELAY_MS);
+
+ return 0;
+
+err_dev_reset:
+ gpiod_set_value_cansleep(cd->reset_gpio, 1);
+ regulator_disable(cd->avdd);
+err_vddio_disable:
+ regulator_disable(cd->vddio);
+ return error;
+}
+
+static void goodix_gtx8_power_off(struct goodix_gtx8_core *cd)
+{
+ gpiod_set_value_cansleep(cd->reset_gpio, 1);
+ regulator_disable(cd->avdd);
+ regulator_disable(cd->vddio);
+}
+
+static int goodix_gtx8_suspend(struct device *dev)
+{
+ struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
+
+ disable_irq(cd->irq);
+ goodix_gtx8_power_off(cd);
+
+ return 0;
+}
+
+static int goodix_gtx8_resume(struct device *dev)
+{
+ struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
+ int error;
+
+ error = goodix_gtx8_power_on(cd);
+ if (error)
+ return error;
+
+ enable_irq(cd->irq);
+
+ return 0;
+}
+
+EXPORT_GPL_SIMPLE_DEV_PM_OPS(goodix_gtx8_pm_ops, goodix_gtx8_suspend,
+ goodix_gtx8_resume);
+
+static void goodix_gtx8_power_off_act(void *data)
+{
+ struct goodix_gtx8_core *cd = data;
+
+ goodix_gtx8_power_off(cd);
+}
+
+static int goodix_gtx8_probe(struct i2c_client *client)
+{
+ struct goodix_gtx8_core *cd;
+ struct regmap *regmap;
+ int error;
+
+ cd = devm_kzalloc(&client->dev, sizeof(*cd), GFP_KERNEL);
+ if (!cd)
+ return -ENOMEM;
+
+ regmap = devm_regmap_init_i2c(client, &goodix_gtx8_regmap_conf);
+ if (IS_ERR(regmap))
+ return PTR_ERR(regmap);
+
+ cd->dev = &client->dev;
+ cd->irq = client->irq;
+ cd->regmap = regmap;
+ cd->ic_data = i2c_get_match_data(client);
+
+ cd->event_buffer =
+ devm_kzalloc(cd->dev, cd->ic_data->event_size, GFP_KERNEL);
+ if (!cd->event_buffer)
+ return -ENOMEM;
+
+ cd->reset_gpio =
+ devm_gpiod_get_optional(cd->dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(cd->reset_gpio))
+ return dev_err_probe(cd->dev, PTR_ERR(cd->reset_gpio),
+ "Failed to request reset GPIO\n");
+
+ cd->avdd = devm_regulator_get(cd->dev, "avdd");
+ if (IS_ERR(cd->avdd))
+ return dev_err_probe(cd->dev, PTR_ERR(cd->avdd),
+ "Failed to request AVDD regulator\n");
+
+ cd->vddio = devm_regulator_get(cd->dev, "vddio");
+ if (IS_ERR(cd->vddio))
+ return dev_err_probe(cd->dev, PTR_ERR(cd->vddio),
+ "Failed to request VDDIO regulator\n");
+
+ error = goodix_gtx8_power_on(cd);
+ if (error) {
+ dev_err(cd->dev, "failed power on");
+ return error;
+ }
+
+ error = devm_add_action_or_reset(cd->dev, goodix_gtx8_power_off_act,
+ cd);
+ if (error)
+ return error;
+
+ error = goodix_gtx8_read_version(cd);
+ if (error) {
+ dev_err(cd->dev, "failed to get version info");
+ return error;
+ }
+
+ error = goodix_gtx8_input_dev_config(cd);
+ if (error) {
+ dev_err(cd->dev, "failed to set input device");
+ return error;
+ }
+
+ error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
+ goodix_gtx8_irq, IRQF_ONESHOT,
+ "goodix-gtx8", cd);
+ if (error) {
+ dev_err(cd->dev, "request threaded IRQ failed: %d\n", error);
+ return error;
+ }
+
+ dev_set_drvdata(cd->dev, cd);
+
+ dev_dbg(cd->dev,
+ "Goodix GT%c%c%c%c Touchscreen Controller, Version %d.%d.%d.%d\n",
+ cd->fw_version.product_id[0], cd->fw_version.product_id[1],
+ cd->fw_version.product_id[2], cd->fw_version.product_id[3],
+ cd->fw_version.fw_version[0], cd->fw_version.fw_version[1],
+ cd->fw_version.fw_version[2], cd->fw_version.fw_version[3]);
+
+ return 0;
+}
+
+static const struct goodix_gtx8_ic_data gt9886_data = {
+ .event_size = GOODIX_GTX8_EVENT_SIZE_NORMANDY,
+ .fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY,
+ .header_size = GOODIX_GTX8_HEADER_SIZE_NORMANDY,
+ .ic_type = IC_TYPE_NORMANDY,
+ .product_id = { '9', '8', '8', '6' },
+ .touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY,
+};
+
+static const struct goodix_gtx8_ic_data gt9896_data = {
+ .event_size = GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE,
+ .fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE,
+ .header_size = GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
+ .ic_type = IC_TYPE_YELLOWSTONE,
+ .product_id = { '9', '8', '9', '6' },
+ .touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE,
+};
+
+static const struct i2c_device_id goodix_gtx8_i2c_id[] = {
+ { .name = "gt9886", .driver_data = (long)>9886_data },
+ { .name = "gt9896", .driver_data = (long)>9896_data },
+ {},
+};
+MODULE_DEVICE_TABLE(i2c, goodix_gtx8_i2c_id);
+
+static const struct of_device_id goodix_gtx8_of_match[] = {
+ { .compatible = "goodix,gt9886", .data = >9886_data },
+ { .compatible = "goodix,gt9896", .data = >9896_data },
+ {},
+};
+MODULE_DEVICE_TABLE(of, goodix_gtx8_of_match);
+
+static struct i2c_driver goodix_gtx8_driver = {
+ .probe = goodix_gtx8_probe,
+ .id_table = goodix_gtx8_i2c_id,
+ .driver = {
+ .name = "goodix-gtx8",
+ .of_match_table = of_match_ptr(goodix_gtx8_of_match),
+ .pm = pm_sleep_ptr(&goodix_gtx8_pm_ops),
+ },
+};
+module_i2c_driver(goodix_gtx8_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Goodix GTX8 Touchscreen driver");
+MODULE_AUTHOR("Aelin Reidel <aelin@mainlining.org>");
diff --git a/drivers/input/touchscreen/goodix_gtx8.h b/drivers/input/touchscreen/goodix_gtx8.h
new file mode 100644
index 0000000000000000000000000000000000000000..9b13cfce38720b35bb11f0d7f56d671b31664ade
--- /dev/null
+++ b/drivers/input/touchscreen/goodix_gtx8.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __GOODIX_GTX8_H__
+#define __GOODIX_GTX8_H__
+
+#include <linux/pm.h>
+
+#define GOODIX_GTX8_NORMAL_RESET_DELAY_MS 100
+#define GOODIX_GTX8_POWER_ON_DELAY_MS 300
+
+#define GOODIX_GTX8_TOUCH_EVENT BIT(7)
+#define GOODIX_GTX8_REQUEST_EVENT BIT(6)
+#define GOODIX_GTX8_TOUCH_COUNT_MASK GENMASK(3, 0)
+#define GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE GENMASK(7, 4)
+
+#define GOODIX_GTX8_MAX_TOUCH 10
+#define GOODIX_GTX8_CHECKSUM_SIZE sizeof(u16)
+
+#define GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY 0x4535
+#define GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE 0x4022
+#define GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY 0x4100
+#define GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE 0x4180
+
+#define I2C_MAX_TRANSFER_SIZE 256
+
+enum goodix_gtx8_ic_type {
+ IC_TYPE_NORMANDY,
+ IC_TYPE_YELLOWSTONE,
+};
+
+struct goodix_gtx8_ic_data {
+ size_t event_size;
+ /*
+ * This is technically not the firmware version address
+ * referenced in the vendor driver, but rather the
+ * address of the product ID part. The meaning of the
+ * other parts is unknown and they are therefore omitted
+ * for now.
+ */
+ int fw_version_addr;
+ size_t header_size;
+ enum goodix_gtx8_ic_type ic_type;
+ char product_id[4];
+ int touch_data_addr;
+};
+
+struct goodix_gtx8_header_normandy {
+ u8 status;
+ /* Only the lower 4 bits are actually used */
+ u8 touch_num;
+};
+#define GOODIX_GTX8_HEADER_SIZE_NORMANDY \
+ sizeof(struct goodix_gtx8_header_normandy)
+
+struct goodix_gtx8_header_yellowstone {
+ u8 status;
+ /* Most likely unused */
+ u8 __unknown1;
+ /* Only the lower 4 bits are actually used */
+ u8 touch_num;
+ /* Most likely unused */
+ u8 __unknown2[3];
+ __le16 checksum;
+} __packed __aligned(1);
+#define GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE \
+ sizeof(struct goodix_gtx8_header_yellowstone)
+
+struct goodix_gtx8_touch_normandy {
+ u8 finger_id;
+ __le16 x;
+ __le16 y;
+ u8 w;
+ u8 __unknown[2];
+} __packed __aligned(1);
+
+struct goodix_gtx8_touch_yellowstone {
+ /*
+ * Only the upper 4 bits are used, lower 4 bits are
+ * probably the sensor ID.
+ */
+ u8 finger_id;
+ u8 __unknown1;
+ __be16 x;
+ __be16 y;
+ /*
+ * Vendor driver claims that this is a single __be16,
+ * but testing shows that it likely isn't.
+ */
+ u8 __unknown2;
+ u8 w;
+} __packed __aligned(1);
+
+union goodix_gtx8_touch {
+ struct goodix_gtx8_touch_normandy normandy;
+ struct goodix_gtx8_touch_yellowstone yellowstone;
+};
+#define GOODIX_GTX8_TOUCH_SIZE sizeof(union goodix_gtx8_touch)
+
+struct goodix_gtx8_event_normandy {
+ struct goodix_gtx8_header_normandy hdr;
+ /* The data below is u16 aligned */
+ u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
+ GOODIX_GTX8_CHECKSUM_SIZE];
+};
+#define GOODIX_GTX8_EVENT_SIZE_NORMANDY \
+ sizeof(struct goodix_gtx8_event_normandy)
+
+struct goodix_gtx8_event_yellowstone {
+ struct goodix_gtx8_header_yellowstone hdr;
+ /* The data below is u16 aligned */
+ u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
+ GOODIX_GTX8_CHECKSUM_SIZE];
+};
+#define GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE \
+ sizeof(struct goodix_gtx8_event_yellowstone)
+
+struct goodix_gtx8_fw_version {
+ /* 4 digits IC number */
+ char product_id[4];
+ /* Most likely unused */
+ u8 __unknown[4];
+ /* Four components version number */
+ u8 fw_version[4];
+};
+
+struct goodix_gtx8_core {
+ struct device *dev;
+ struct regmap *regmap;
+ struct regulator *avdd;
+ struct regulator *vddio;
+ struct gpio_desc *reset_gpio;
+ struct touchscreen_properties props;
+ struct goodix_gtx8_fw_version fw_version;
+ struct input_dev *input_dev;
+ int irq;
+ const struct goodix_gtx8_ic_data *ic_data;
+ u8 *event_buffer;
+};
+
+extern const struct dev_pm_ops goodix_gtx8_pm_ops;
+
+#endif
--
2.53.0
^ permalink raw reply related
* [PATCH v2 0/3] Input: add initial support for Goodix GTX8 touchscreen ICs
From: Aelin Reidel @ 2026-02-28 1:56 UTC (permalink / raw)
To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Hans de Goede, Neil Armstrong, Henrik Rydberg
Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
~postmarketos/upstreaming, Aelin Reidel, Piyush Raj Chouhan,
Alexander Koskovich
These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
and gesture events.
This driver is derived from the Goodix gtx8_driver_linux available at
[1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.
The current implementation only supports Normandy and Yellowstone type
ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
don't have a device with GTX8 over SPI at hand. Adding support for SPI
should be fairly easy in the future, since the code uses a regmap.
Support for advanced features like:
- Firmware updates
- Stylus events
- Gesture events
- Nanjing IC support
is not included in current version.
The current support requires a previously flashed firmware to be
present.
As I did not have access to datasheets for these ICs, I extracted the
addresses from a couple of config files using a small tool [2]. The
addresses are identical for the same IC families in all configs I
observed, however not all of them make sense and I stubbed out firmware
request support due to this.
I've taken a lot of inspiration from the goodix_berlin driver, but the
Berlin and GTX8 series of touchscreen ICs differ quite a bit. The driver
architecture is the same overall, i.e. the power-up sequence and general
concepts are the mostly same, but it is very clear that they are
different generations when looking at it in more detail.
Some of the differences:
- There is no equivalent to the bootoption reg that I can find in the
public GTX8 drivers
- Firmware version struct layout is different yet again
- GTX8 does not expose IC information at runtime as far as I can tell
- The checksum method differs yet again
- The vendor driver reads only 1 touch upfront rather than 2
- Register addresses are 16-bit on GTX8 and 32-bit on Berlin
- Firmware requests don't appear to really exist on GTX8
From what I can tell, the evolution seems to be:
Normandy -> Yellowstone -> Berlin
since Normandy and Yellowstone are already quite different (especially
with the way checksums work) and Yellowstone has a couple of things
(checksum, fw_version) that appear similar to Berlin series ICs.
I've tried to make the Berlin driver work for GTX8 ICs before, but
they're so different (and I lack documentation for registers to perhaps
make some parts work on GTX8) that I'd rather support these ICs in a new
and tiny driver. I hope that makes sense. I took heavy inspiration from
the Berlin driver, but the only parts that are really common between
them are very trivial things like e.g. the input dev config or power on,
which I don't think are worth putting in a separate header.
[1] https://github.com/goodix/gtx8_driver_linux
[2] https://github.com/sm7150-mainline/goodix-cfg-bin
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
Changes in v2:
- Fix compilation issues found by Intel's kernel test robot
- Add Alexander's T-b to the driver patch
- Link to v1: https://lore.kernel.org/r/20260218-gtx8-v1-0-0d575b3dedc5@mainlining.org
Changes in v1 (post-RFC):
- Drop RFC prefix, the series has been tested enough and works well
as-is
- Update my name and email address
- Add some reasoning for a new driver to the cover letter
- Add Rob's R-b on the dt-bindings patch
- Add Piyush's T-b to the driver patch
- Link to RFC: https://lore.kernel.org/r/20250918-gtx8-v1-0-cba879c84775@mainlining.org
---
Aelin Reidel (3):
dt-bindings: input: document Goodix GTX8 Touchscreen ICs
Input: add support for Goodix GTX8 Touchscreen ICs
MAINTAINERS: add an entry for Goodix GTX8 Touchscreen driver
.../bindings/input/touchscreen/goodix,gt9886.yaml | 71 +++
MAINTAINERS | 7 +
drivers/input/touchscreen/Kconfig | 15 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/goodix_gtx8.c | 563 +++++++++++++++++++++
drivers/input/touchscreen/goodix_gtx8.h | 141 ++++++
6 files changed, 798 insertions(+)
---
base-commit: 3fa5e5702a82d259897bd7e209469bc06368bf31
change-id: 20250918-gtx8-59a50ccd78a5
Best regards,
--
Aelin Reidel <aelin@mainlining.org>
^ permalink raw reply
* [PATCH v2 1/3] dt-bindings: input: document Goodix GTX8 Touchscreen ICs
From: Aelin Reidel @ 2026-02-28 1:56 UTC (permalink / raw)
To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Hans de Goede, Neil Armstrong, Henrik Rydberg
Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
~postmarketos/upstreaming, Aelin Reidel
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>
Document the Goodix GT9886 and GT9896 which are part of the GTX8 series
of Touchscreen controller ICs from Goodix.
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
.../bindings/input/touchscreen/goodix,gt9886.yaml | 71 ++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml b/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6307495c2746313cfc32cdbb701455d1596be435
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/goodix,gt9886.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Goodix GTX8 series touchscreen controller
+
+maintainers:
+ - Aelin Reidel <aelin@mainlining.org>
+
+allOf:
+ - $ref: touchscreen.yaml#
+
+properties:
+ compatible:
+ enum:
+ - goodix,gt9886
+ - goodix,gt9896
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ avdd-supply:
+ description: Analog power supply regulator on AVDD pin
+
+ vddio-supply:
+ description: power supply regulator on VDDIO pin
+
+ touchscreen-inverted-x: true
+ touchscreen-inverted-y: true
+ touchscreen-size-x: true
+ touchscreen-size-y: true
+ touchscreen-swapped-x-y: true
+
+additionalProperties: false
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - avdd-supply
+ - touchscreen-size-x
+ - touchscreen-size-y
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/gpio/gpio.h>
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ touchscreen@5d {
+ compatible = "goodix,gt9886";
+ reg = <0x5d>;
+ interrupt-parent = <&gpio>;
+ interrupts = <9 IRQ_TYPE_LEVEL_LOW>;
+ reset-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
+ avdd-supply = <&ts_avdd>;
+ touchscreen-size-x = <1080>;
+ touchscreen-size-y = <2340>;
+ };
+ };
+
+...
--
2.53.0
^ permalink raw reply related
* [PATCH v2 3/3] MAINTAINERS: add an entry for Goodix GTX8 Touchscreen driver
From: Aelin Reidel @ 2026-02-28 1:56 UTC (permalink / raw)
To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Hans de Goede, Neil Armstrong, Henrik Rydberg
Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
~postmarketos/upstreaming, Aelin Reidel
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>
Add MAINTAINERS entry for the Goodix GTX8 Touchscreen IC driver.
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 14899f1de77ed2e8a583cf7b0fea25725c8534cb..c76f9fbe51f929f7eded37760cb5c83dfa337d0b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10849,6 +10849,13 @@ M: Maud Spierings <maudspierings@gocontroll.com>
S: Maintained
F: Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
+GOODIX GTX8 TOUCHSCREEN
+M: Aelin Reidel <aelin@mainlining.org>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
+F: drivers/input/touchscreen/goodix_gtx8*
+
GOODIX TOUCHSCREEN
M: Hans de Goede <hansg@kernel.org>
L: linux-input@vger.kernel.org
--
2.53.0
^ permalink raw reply related
* [PATCH v3 18/18] HID: steelseries: Document sysfs ABI
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Add Documentation/ABI/testing/sysfs-driver-hid-steelseries documenting
the sysfs attributes and LED class device exposed by the driver:
- bt_enabled, bt_device_connected: read-only Bluetooth radio state
- inactive_time: read/write auto-shutoff timer in minutes
- bt_auto_enable: read/write Bluetooth radio power-on behavior
- <dev>::micmute/brightness: mic mute LED brightness via LED class
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
.../ABI/testing/sysfs-driver-hid-steelseries | 87 +++++++++++++++++++
1 file changed, 87 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
new file mode 100644
index 000000000000..7b8d29282ed6
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
@@ -0,0 +1,87 @@
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_enabled
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RO) Whether the Bluetooth radio on the headset is currently
+ enabled.
+
+ * 0 = Bluetooth radio off
+ * 1 = Bluetooth radio on
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_device_connected
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RO) Whether a Bluetooth device is currently connected to
+ the headset.
+
+ * 0 = no Bluetooth device connected
+ * 1 = Bluetooth device connected
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/inactive_time
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Auto-shutoff timer for the headset, in minutes. A
+ value of 0 disables the timer. The maximum accepted value
+ is device-specific.
+
+ The encoding sent to the firmware varies by device family:
+ the Arctis 9 converts the value to seconds, the Nova 3P
+ rounds down to its nearest supported discrete step, and the
+ Nova Pro maps to six firmware-defined level indices. For all
+ other devices the value is sent in minutes directly.
+
+ Reading the attribute returns the last value reported by the
+ firmware. Writing immediately sends the new timeout to the
+ device.
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis 1 Wireless, Arctis 7, Arctis 7+,
+ Arctis 9, Arctis Nova 3P, Arctis Nova 5, Arctis Nova 5X,
+ Arctis Nova 7, Arctis Nova 7P, Arctis Nova 7 Gen2,
+ Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_auto_enable
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Whether the headset automatically enables its
+ Bluetooth radio on power-on.
+
+ * 0 = Bluetooth radio stays off at power-on
+ * 1 = Bluetooth radio activates automatically at power-on
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7, Arctis Nova 7P,
+ Arctis Nova 7 Gen2
+
+What: /sys/class/leds/<dev>::micmute/brightness
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Brightness of the microphone mute status LED.
+ <dev> is the HID device node name (e.g.
+ 0003:1038:12AE.0001).
+
+ * 0 = off
+ * 1 = low
+ * 2 = medium
+ * 3 = high
+
+ Supported on: Arctis Nova 5, Arctis Nova 5X, Arctis Nova 7,
+ Arctis Nova 7P, Arctis Nova 7 Gen2
--
2.53.0
^ permalink raw reply related
* [PATCH v3 17/18] HID: steelseries: Add mic mute LED brightness control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Register the microphone mute LED as an LED class device named
"<device>::micmute", following the standard LED naming convention. The
brightness range is 0-3 representing off, low, medium, and high.
On the Arctis Nova 5 family, the discrete levels map to non-linear
hardware values expected by the firmware (0, 1, 4, 10). The Nova 7
family uses a direct linear mapping. On the Nova 7 Gen2, the current
brightness is recovered from the 0xa0 settings poll response.
Registration is guarded by a LEDS_CLASS module compatibility check
analogous to the existing SND guard.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 119 ++++++++++++++++++++++++++++++++--
1 file changed, 114 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index a794af01e15a..dcd34c61cccd 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -33,6 +33,7 @@
#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_CAP_INACTIVE_TIME BIT(10)
#define SS_CAP_BT_AUTO_ENABLE BIT(11)
+#define SS_CAP_MIC_MUTE_BRIGHTNESS BIT(12)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -42,6 +43,7 @@
#define SS_SETTING_BT_CALL_DUCKING 3
#define SS_SETTING_INACTIVE_TIME 4
#define SS_SETTING_BT_AUTO_ENABLE 5
+#define SS_SETTING_MIC_MUTE_BRIGHTNESS 6
struct steelseries_device;
@@ -100,6 +102,12 @@ struct steelseries_device {
bool bt_device_connected;
u8 inactive_timeout;
bool bt_auto_enable;
+ u8 mic_mute_brightness;
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ struct led_classdev *mic_mute_led;
+#endif
spinlock_t lock;
bool removed;
@@ -606,6 +614,14 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_INACTIVE_TIME:
cmd = 0xa3;
break;
+ case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+ cmd = 0xae;
+ /* Hardware uses non-linear values: 0=off, 1=low, 4=medium, 10=high */
+ if (value == 2)
+ value = 0x04;
+ else if (value == 3)
+ value = 0x0a;
+ break;
default:
return -EINVAL;
}
@@ -650,6 +666,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_BT_AUTO_ENABLE:
cmd = 0xb2;
break;
+ case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+ cmd = 0xae;
+ break;
default:
return -EINVAL;
}
@@ -1005,6 +1024,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
break;
case 0xa0:
sd->inactive_timeout = data[1];
+ sd->mic_mute_brightness = data[2];
sd->bt_auto_enable = data[3];
sd->bt_call_ducking = data[4];
break;
@@ -1020,6 +1040,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0xa3:
sd->inactive_timeout = data[1];
break;
+ case 0xae:
+ sd->mic_mute_brightness = data[1];
+ break;
case 0xb2:
sd->bt_auto_enable = data[1];
break;
@@ -1129,7 +1152,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
- SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
+ SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME |
+ SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -1143,7 +1167,7 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_INACTIVE_TIME,
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -1158,7 +1182,7 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1172,7 +1196,7 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1189,7 +1213,7 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.sidetone_max = 3,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1970,6 +1994,82 @@ static void steelseries_snd_unregister(struct steelseries_device *sd)
#endif
+/*
+ * Mic mute LED
+ */
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+
+#define SS_MIC_MUTE_BRIGHTNESS_MAX 3
+
+static int steelseries_mic_mute_led_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct device *dev = led_cdev->dev->parent;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ unsigned long flags;
+ int ret;
+
+ if (brightness > SS_MIC_MUTE_BRIGHTNESS_MAX)
+ brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->removed) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_MUTE_BRIGHTNESS,
+ brightness);
+ if (ret)
+ return ret;
+
+ sd->mic_mute_brightness = brightness;
+
+ return 0;
+}
+
+static enum led_brightness
+steelseries_mic_mute_led_brightness_get(struct led_classdev *led_cdev)
+{
+ struct device *dev = led_cdev->dev->parent;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+ return sd->mic_mute_brightness;
+}
+
+static int steelseries_mic_mute_led_register(struct steelseries_device *sd)
+{
+ struct hid_device *hdev = sd->hdev;
+ struct led_classdev *led;
+ size_t name_size;
+ char *name;
+
+ name_size = strlen(dev_name(&hdev->dev)) + 16;
+
+ led = devm_kzalloc(&hdev->dev, sizeof(*led) + name_size, GFP_KERNEL);
+ if (!led)
+ return -ENOMEM;
+
+ name = (void *)(&led[1]);
+ snprintf(name, name_size, "%s::micmute", dev_name(&hdev->dev));
+ led->name = name;
+ led->brightness = 0;
+ led->max_brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+ led->brightness_get = steelseries_mic_mute_led_brightness_get;
+ led->brightness_set_blocking = steelseries_mic_mute_led_brightness_set;
+
+ sd->mic_mute_led = led;
+
+ return devm_led_classdev_register(&hdev->dev, led);
+}
+
+#endif
+
static int steelseries_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
@@ -2175,6 +2275,15 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register sound card: %d\n", ret);
#endif
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ if (info->capabilities & SS_CAP_MIC_MUTE_BRIGHTNESS) {
+ ret = steelseries_mic_mute_led_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register mic mute LED: %d\n", ret);
+ }
+#endif
+
INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
INIT_DELAYED_WORK(&sd->settings_work, steelseries_settings_work_handler);
--
2.53.0
^ permalink raw reply related
* [PATCH v3 16/18] HID: steelseries: Add Bluetooth auto-enable sysfs attribute
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Expose the Bluetooth auto-enable setting as a read/write sysfs boolean
attribute (bt_auto_enable). When enabled, the headset activates its
Bluetooth radio automatically on power-on. Currently supported on the
Arctis Nova 7, Nova 7P, and Nova 7 Gen2 via write command 0xb2.
On the Nova 7 Gen2, the current value is recovered from the 0xa0 device
settings response alongside inactive timeout and call audio ducking.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 65 ++++++++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f076a0ef8af1..a794af01e15a 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -32,6 +32,7 @@
#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_CAP_INACTIVE_TIME BIT(10)
+#define SS_CAP_BT_AUTO_ENABLE BIT(11)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -40,6 +41,7 @@
#define SS_SETTING_VOLUME_LIMITER 2
#define SS_SETTING_BT_CALL_DUCKING 3
#define SS_SETTING_INACTIVE_TIME 4
+#define SS_SETTING_BT_AUTO_ENABLE 5
struct steelseries_device;
@@ -97,6 +99,7 @@ struct steelseries_device {
bool bt_enabled;
bool bt_device_connected;
u8 inactive_timeout;
+ bool bt_auto_enable;
spinlock_t lock;
bool removed;
@@ -644,6 +647,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_INACTIVE_TIME:
cmd = 0xa3;
break;
+ case SS_SETTING_BT_AUTO_ENABLE:
+ cmd = 0xb2;
+ break;
default:
return -EINVAL;
}
@@ -999,6 +1005,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
break;
case 0xa0:
sd->inactive_timeout = data[1];
+ sd->bt_auto_enable = data[3];
sd->bt_call_ducking = data[4];
break;
case 0x37:
@@ -1013,6 +1020,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0xa3:
sd->inactive_timeout = data[1];
break;
+ case 0xb2:
+ sd->bt_auto_enable = data[1];
+ break;
case 0xb3:
sd->bt_call_ducking = data[1];
break;
@@ -1147,7 +1157,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1160,7 +1171,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1176,7 +1188,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.sidetone_max = 3,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1416,14 +1429,53 @@ static ssize_t inactive_time_store(struct device *dev,
return count;
}
+static ssize_t bt_auto_enable_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->bt_auto_enable);
+}
+
+static ssize_t bt_auto_enable_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ bool value;
+ int ret;
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ ret = kstrtobool(buf, &value);
+ if (ret)
+ return ret;
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_AUTO_ENABLE, value);
+ if (ret)
+ return ret;
+
+ sd->bt_auto_enable = value;
+
+ return count;
+}
+
static DEVICE_ATTR_RO(bt_enabled);
static DEVICE_ATTR_RO(bt_device_connected);
static DEVICE_ATTR_RW(inactive_time);
+static DEVICE_ATTR_RW(bt_auto_enable);
static struct attribute *steelseries_headset_attrs[] = {
&dev_attr_bt_enabled.attr,
&dev_attr_bt_device_connected.attr,
&dev_attr_inactive_time.attr,
+ &dev_attr_bt_auto_enable.attr,
NULL,
};
@@ -1447,6 +1499,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
if (attr == &dev_attr_inactive_time.attr)
return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
+ if (attr == &dev_attr_bt_auto_enable.attr)
+ return (caps & SS_CAP_BT_AUTO_ENABLE) ? attr->mode : 0;
return 0;
}
@@ -2106,7 +2160,8 @@ static int steelseries_probe(struct hid_device *hdev,
}
if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_INACTIVE_TIME)) {
+ SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE)) {
ret = sysfs_create_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
if (ret)
@@ -2189,7 +2244,7 @@ static void steelseries_remove(struct hid_device *hdev)
if (interface_num == sd->info->sync_interface) {
if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_INACTIVE_TIME))
+ SS_CAP_INACTIVE_TIME | SS_CAP_BT_AUTO_ENABLE))
sysfs_remove_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
--
2.53.0
^ permalink raw reply related
* [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Expose the headset auto-shutoff timer as a read/write sysfs attribute
(inactive_time), in minutes. Writing the attribute immediately sends the
new value to the device; reading it returns the last value reported by
the firmware.
The wire encoding differs per family:
- Arctis 1: HID feature report 0x06/0x53 with the value in minutes
- Arctis 7: HID feature report 0x06/0x51; split into its own write
function as the command byte differs from the Arctis 1
- Arctis 9: converts minutes to seconds in a big-endian u16
- Nova 3P: rounds down to the nearest value in a discrete set
{0,1,5,10,15,30,45,60,75,90} before sending command 0xa3
- Nova 5/7: output report with command 0xa3, no rounding required
- Nova Pro: maps minutes to six firmware-defined level indices via
command 0xc1
The inactive_time_max field is added to the device info struct to
enforce the per-device maximum at write time.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 183 +++++++++++++++++++++++++++++++---
1 file changed, 167 insertions(+), 16 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index bb9abbb0b6f8..f076a0ef8af1 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -31,6 +31,7 @@
#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_CAP_BT_CALL_DUCKING BIT(9)
+#define SS_CAP_INACTIVE_TIME BIT(10)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -38,6 +39,7 @@
#define SS_SETTING_MIC_VOLUME 1
#define SS_SETTING_VOLUME_LIMITER 2
#define SS_SETTING_BT_CALL_DUCKING 3
+#define SS_SETTING_INACTIVE_TIME 4
struct steelseries_device;
@@ -51,6 +53,7 @@ struct steelseries_device_info {
u8 sidetone_max;
u8 mic_volume_min;
u8 mic_volume_max;
+ u8 inactive_time_max;
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -93,6 +96,7 @@ struct steelseries_device {
bool bt_enabled;
bool bt_device_connected;
+ u8 inactive_timeout;
spinlock_t lock;
bool removed;
@@ -476,6 +480,37 @@ static int steelseries_arctis_1_write_setting(struct hid_device *hdev,
return steelseries_send_feature_report(hdev, data,
sizeof(data));
}
+ case SS_SETTING_INACTIVE_TIME: {
+ const u8 data[] = { 0x06, 0x53, value };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int steelseries_arctis_7_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ if (value == 0) {
+ const u8 data[] = { 0x06, 0x35 };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ } else {
+ const u8 data[] = { 0x06, 0x35, 0x01, 0x00, value };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ }
+ case SS_SETTING_INACTIVE_TIME: {
+ const u8 data[] = { 0x06, 0x51, value };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
default:
return -EINVAL;
}
@@ -490,11 +525,30 @@ static int steelseries_arctis_9_write_setting(struct hid_device *hdev,
return steelseries_send_feature_report(hdev, data, sizeof(data));
}
+ case SS_SETTING_INACTIVE_TIME: {
+ u16 seconds = (u16)value * 60;
+ const u8 data[] = { 0x04, 0x00, seconds >> 8, seconds & 0xff };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
default:
return -EINVAL;
}
}
+static u8 steelseries_arctis_nova_3p_round_inactive_time(u8 minutes)
+{
+ static const u8 supported[] = { 0, 1, 5, 10, 15, 30, 45, 60, 75, 90 };
+ int i;
+
+ for (i = ARRAY_SIZE(supported) - 1; i > 0; i--) {
+ if (minutes >= supported[i])
+ return supported[i];
+ }
+
+ return 0;
+}
+
static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
u8 setting, u8 value)
{
@@ -510,6 +564,10 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ value = steelseries_arctis_nova_3p_round_inactive_time(value);
+ break;
default:
return -EINVAL;
}
@@ -542,6 +600,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_VOLUME_LIMITER:
cmd = 0x27;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ break;
default:
return -EINVAL;
}
@@ -580,6 +641,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_BT_CALL_DUCKING:
cmd = 0xb3;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ break;
default:
return -EINVAL;
}
@@ -612,6 +676,24 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xc1;
+ /* Map minutes to firmware level */
+ if (value >= 45)
+ value = 6; /* 60 min */
+ else if (value >= 23)
+ value = 5; /* 30 min */
+ else if (value >= 13)
+ value = 4; /* 15 min */
+ else if (value >= 8)
+ value = 3; /* 10 min */
+ else if (value >= 3)
+ value = 2; /* 5 min */
+ else if (value > 0)
+ value = 1; /* 1 min */
+ else
+ value = 0; /* disabled */
+ break;
default:
return -EINVAL;
}
@@ -916,6 +998,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
sd->volume_limiter = data[3];
break;
case 0xa0:
+ sd->inactive_timeout = data[1];
sd->bt_call_ducking = data[4];
break;
case 0x37:
@@ -927,6 +1010,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x3a:
sd->volume_limiter = data[1];
break;
+ case 0xa3:
+ sd->inactive_timeout = data[1];
+ break;
case 0xb3:
sd->bt_call_ducking = data[1];
break;
@@ -936,11 +1022,13 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
static void steelseries_arctis_nova_pro_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 10)
+ if (size < 13)
return;
- if (data[0] == 0x06 && data[1] == 0xb0)
+ if (data[0] == 0x06 && data[1] == 0xb0) {
sd->mic_volume = data[9];
+ sd->inactive_timeout = data[12];
+ }
}
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
@@ -970,9 +1058,10 @@ static const struct steelseries_device_info srws1_info = { };
static const struct steelseries_device_info arctis_1_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 18,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_1_request_status,
.parse_status = steelseries_arctis_1_parse_status,
.write_setting = steelseries_arctis_1_write_setting,
@@ -980,19 +1069,23 @@ static const struct steelseries_device_info arctis_1_info = {
static const struct steelseries_device_info arctis_7_info = {
.sync_interface = 5,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 18,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_7_request_status,
.parse_status = steelseries_arctis_7_parse_status,
- .write_setting = steelseries_arctis_1_write_setting,
+ .write_setting = steelseries_arctis_7_write_setting,
};
static const struct steelseries_device_info arctis_7_plus_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_7_plus_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1000,9 +1093,11 @@ static const struct steelseries_device_info arctis_7_plus_info = {
static const struct steelseries_device_info arctis_9_info = {
.sync_interface = 0,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 61,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_9_request_status,
.parse_status = steelseries_arctis_9_parse_status,
.write_setting = steelseries_arctis_9_write_setting,
@@ -1010,10 +1105,12 @@ static const struct steelseries_device_info arctis_9_info = {
static const struct steelseries_device_info arctis_nova_3p_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 14,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_nova_3p_request_status,
.parse_status = steelseries_arctis_nova_3p_parse_status,
.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -1022,10 +1119,11 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
- SS_CAP_VOLUME_LIMITER,
+ SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1034,10 +1132,12 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5x_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1047,10 +1147,11 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1059,9 +1160,10 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1074,9 +1176,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.sidetone_max = 3,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -1088,11 +1191,12 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_min = 1,
.mic_volume_max = 10,
+ .inactive_time_max = 60,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
.parse_settings = steelseries_arctis_nova_pro_parse_settings,
@@ -1271,12 +1375,55 @@ static ssize_t bt_device_connected_show(struct device *dev,
return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
}
+static ssize_t inactive_time_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->inactive_timeout);
+}
+
+static ssize_t inactive_time_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ unsigned int value;
+ int ret;
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ ret = kstrtouint(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ if (value > sd->info->inactive_time_max)
+ return -EINVAL;
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_INACTIVE_TIME,
+ value);
+ if (ret)
+ return ret;
+
+ sd->inactive_timeout = value;
+
+ return count;
+}
+
static DEVICE_ATTR_RO(bt_enabled);
static DEVICE_ATTR_RO(bt_device_connected);
+static DEVICE_ATTR_RW(inactive_time);
static struct attribute *steelseries_headset_attrs[] = {
&dev_attr_bt_enabled.attr,
&dev_attr_bt_device_connected.attr,
+ &dev_attr_inactive_time.attr,
NULL,
};
@@ -1298,6 +1445,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
if (attr == &dev_attr_bt_device_connected.attr)
return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
+ if (attr == &dev_attr_inactive_time.attr)
+ return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
return 0;
}
@@ -1956,7 +2105,8 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register battery: %d\n", ret);
}
- if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED)) {
+ if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_INACTIVE_TIME)) {
ret = sysfs_create_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
if (ret)
@@ -2038,7 +2188,8 @@ static void steelseries_remove(struct hid_device *hdev)
}
if (interface_num == sd->info->sync_interface) {
- if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED))
+ if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_INACTIVE_TIME))
sysfs_remove_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
--
2.53.0
^ permalink raw reply related
* [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Expose Bluetooth call audio ducking behavior as a writable ALSA
enumerated mixer control ("Bluetooth Call Audio Ducking"), with three
options: off, lower game audio by 12 dB, or mute game audio entirely.
On the Arctis Nova 7 Gen2, this setting is stored alongside inactive
timeout and Bluetooth auto-enable in a dedicated device configuration
block. The settings request is expanded to also send a 0x00/0xa0 device
query in addition to the existing 0x00/0x20 audio settings query.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 120 ++++++++++++++++++++++++++++++++--
1 file changed, 114 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 47ffec481571..bb9abbb0b6f8 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -30,12 +30,14 @@
#define SS_CAP_SIDETONE BIT(6)
#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_CAP_VOLUME_LIMITER BIT(8)
+#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
#define SS_SETTING_MIC_VOLUME 1
#define SS_SETTING_VOLUME_LIMITER 2
+#define SS_SETTING_BT_CALL_DUCKING 3
struct steelseries_device;
@@ -80,12 +82,14 @@ struct steelseries_device {
struct snd_ctl_elem_id sidetone_id;
struct snd_ctl_elem_id mic_volume_id;
struct snd_ctl_elem_id volume_limiter_id;
+ struct snd_ctl_elem_id bt_call_ducking_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
u8 mic_volume;
bool volume_limiter;
+ u8 bt_call_ducking;
bool bt_enabled;
bool bt_device_connected;
@@ -573,6 +577,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_VOLUME_LIMITER:
cmd = 0x3a;
break;
+ case SS_SETTING_BT_CALL_DUCKING:
+ cmd = 0xb3;
+ break;
default:
return -EINVAL;
}
@@ -883,15 +890,23 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hdev)
{
- const u8 data[] = { 0x00, 0x20 };
+ const u8 audio_data[] = { 0x00, 0x20 };
+ const u8 device_data[] = { 0x00, 0xa0 };
+ int ret;
- return steelseries_send_output_report(hdev, data, sizeof(data));
+ ret = steelseries_send_output_report(hdev, audio_data, sizeof(audio_data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_output_report(hdev, device_data, sizeof(device_data));
}
static void steelseries_arctis_nova_7_gen2_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 4)
+ if (size < 5)
return;
switch (data[0]) {
@@ -900,6 +915,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
sd->sidetone = data[2];
sd->volume_limiter = data[3];
break;
+ case 0xa0:
+ sd->bt_call_ducking = data[4];
+ break;
case 0x37:
sd->mic_volume = data[1];
break;
@@ -909,6 +927,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x3a:
sd->volume_limiter = data[1];
break;
+ case 0xb3:
+ sd->bt_call_ducking = data[1];
+ break;
}
}
@@ -1025,7 +1046,8 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1036,7 +1058,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
@@ -1050,7 +1073,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
@@ -1557,6 +1581,70 @@ static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
.put = steelseries_volume_limiter_put,
};
+static const char *const bt_call_ducking_texts[] = {
+ "Off",
+ "Lower Volume (-12dB)",
+ "Mute Game",
+};
+
+static int steelseries_bt_call_ducking_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(bt_call_ducking_texts),
+ bt_call_ducking_texts);
+}
+
+static int steelseries_bt_call_ducking_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.enumerated.item[0] = sd->bt_call_ducking;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_bt_call_ducking_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ u8 new_value;
+ int ret;
+
+ new_value = ucontrol->value.enumerated.item[0];
+ if (new_value >= ARRAY_SIZE(bt_call_ducking_texts))
+ return -EINVAL;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->bt_call_ducking == new_value) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_CALL_DUCKING,
+ new_value);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->bt_call_ducking = new_value;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_bt_call_ducking_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Bluetooth Call Audio Ducking",
+ .info = steelseries_bt_call_ducking_info,
+ .get = steelseries_bt_call_ducking_get,
+ .put = steelseries_bt_call_ducking_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1644,6 +1732,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->volume_limiter_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_BT_CALL_DUCKING) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new ducking_ctl = steelseries_bt_call_ducking_control;
+
+ ducking_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ ducking_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&ducking_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->bt_call_ducking_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1677,6 +1780,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_sidetone;
u8 old_mic_volume;
bool old_volume_limiter;
+ u8 old_bt_call_ducking;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1694,6 +1798,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_sidetone = sd->sidetone;
old_mic_volume = sd->mic_volume;
old_volume_limiter = sd->volume_limiter;
+ old_bt_call_ducking = sd->bt_call_ducking;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1763,6 +1868,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->volume_limiter != old_volume_limiter)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->volume_limiter_id);
+ if (sd->bt_call_ducking != old_bt_call_ducking)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->bt_call_ducking_id);
}
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH v3 13/18] HID: steelseries: Add volume limiter ALSA mixer control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Expose the maximum output volume cap as a writable ALSA boolean mixer
control ("Volume Limiter").
The Nova 7 family uses command 0x3a for this setting whereas the Nova 5
family uses 0x27, so a dedicated steelseries_arctis_nova_7_write_setting()
is introduced and the Nova 7, Nova 7P, and Nova 7 Gen2 entries are
updated to use it instead of the Nova 5 handler.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 145 +++++++++++++++++++++++++++++++---
1 file changed, 136 insertions(+), 9 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 1339f965f67f..47ffec481571 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -29,11 +29,13 @@
#define SS_CAP_EXTERNAL_CONFIG BIT(5)
#define SS_CAP_SIDETONE BIT(6)
#define SS_CAP_MIC_VOLUME BIT(7)
+#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
#define SS_SETTING_MIC_VOLUME 1
+#define SS_SETTING_VOLUME_LIMITER 2
struct steelseries_device;
@@ -77,11 +79,13 @@ struct steelseries_device {
struct snd_ctl_elem_id mic_muted_id;
struct snd_ctl_elem_id sidetone_id;
struct snd_ctl_elem_id mic_volume_id;
+ struct snd_ctl_elem_id volume_limiter_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
u8 mic_volume;
+ bool volume_limiter;
bool bt_enabled;
bool bt_device_connected;
@@ -531,6 +535,44 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_VOLUME_LIMITER:
+ cmd = 0x27;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ data[0] = 0x00;
+ data[1] = cmd;
+ data[2] = value;
+
+ ret = steelseries_send_output_report(hdev, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ const u8 save[] = { 0x00, 0x09 };
+ u8 cmd;
+ int ret;
+ u8 data[3];
+
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ cmd = 0x39;
+ break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
+ case SS_SETTING_VOLUME_LIMITER:
+ cmd = 0x3a;
+ break;
default:
return -EINVAL;
}
@@ -849,13 +891,14 @@ static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hd
static void steelseries_arctis_nova_7_gen2_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 3)
+ if (size < 4)
return;
switch (data[0]) {
case 0x20:
sd->mic_volume = data[1];
sd->sidetone = data[2];
+ sd->volume_limiter = data[3];
break;
case 0x37:
sd->mic_volume = data[1];
@@ -863,6 +906,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x39:
sd->sidetone = data[1];
break;
+ case 0x3a:
+ sd->volume_limiter = data[1];
+ break;
}
}
@@ -954,7 +1000,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+ SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -966,7 +1013,7 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -978,23 +1025,23 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -1003,14 +1050,14 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
.parse_settings = steelseries_arctis_nova_7_gen2_parse_settings,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
@@ -1450,6 +1497,66 @@ static const struct snd_kcontrol_new steelseries_mic_volume_control = {
.put = steelseries_mic_volume_put,
};
+static int steelseries_volume_limiter_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_volume_limiter_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->volume_limiter;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_volume_limiter_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ bool new_volume_limiter;
+ int ret;
+
+ new_volume_limiter = !!ucontrol->value.integer.value[0];
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->volume_limiter == new_volume_limiter) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_VOLUME_LIMITER,
+ new_volume_limiter ? 1 : 0);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->volume_limiter = new_volume_limiter;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Volume Limiter",
+ .info = steelseries_volume_limiter_info,
+ .get = steelseries_volume_limiter_get,
+ .put = steelseries_volume_limiter_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1522,6 +1629,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->mic_volume_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_VOLUME_LIMITER) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new vol_lim_ctl = steelseries_volume_limiter_control;
+
+ vol_lim_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ vol_lim_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&vol_lim_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->volume_limiter_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1554,6 +1676,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
bool old_mic_muted;
u8 old_sidetone;
u8 old_mic_volume;
+ bool old_volume_limiter;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1570,6 +1693,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_mic_muted = sd->mic_muted;
old_sidetone = sd->sidetone;
old_mic_volume = sd->mic_volume;
+ old_volume_limiter = sd->volume_limiter;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1636,6 +1760,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->mic_volume != old_mic_volume)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->mic_volume_id);
+ if (sd->volume_limiter != old_volume_limiter)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->volume_limiter_id);
}
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH v3 12/18] HID: steelseries: Add mic volume ALSA mixer control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>
Expose microphone gain as a writable ALSA integer mixer control
("Mic Volume"). The valid range is per-device; mic_volume_min and
mic_volume_max are added to the device info struct to accommodate models
with a non-zero minimum (e.g. the Arctis Nova Pro, which has a range
of 1-10).
The write command (0x37) is added to the Nova 3P, Nova 5, and Nova Pro
write handlers. On the Nova Pro, the current mic volume is recovered
from the 0x06/0xb0 settings response via a new parse_settings hook.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 141 ++++++++++++++++++++++++++++++++--
1 file changed, 134 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 2bdf772432d0..1339f965f67f 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -28,10 +28,12 @@
#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
#define SS_CAP_EXTERNAL_CONFIG BIT(5)
#define SS_CAP_SIDETONE BIT(6)
+#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
+#define SS_SETTING_MIC_VOLUME 1
struct steelseries_device;
@@ -43,6 +45,8 @@ struct steelseries_device_info {
u8 async_interface;
u8 sidetone_max;
+ u8 mic_volume_min;
+ u8 mic_volume_max;
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -72,10 +76,12 @@ struct steelseries_device {
struct snd_ctl_elem_id chatmix_game_id;
struct snd_ctl_elem_id mic_muted_id;
struct snd_ctl_elem_id sidetone_id;
+ struct snd_ctl_elem_id mic_volume_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
+ u8 mic_volume;
bool bt_enabled;
bool bt_device_connected;
@@ -493,6 +499,9 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -519,6 +528,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -548,6 +560,9 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -839,14 +854,28 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
switch (data[0]) {
case 0x20:
+ sd->mic_volume = data[1];
sd->sidetone = data[2];
break;
+ case 0x37:
+ sd->mic_volume = data[1];
+ break;
case 0x39:
sd->sidetone = data[1];
break;
}
}
+static void steelseries_arctis_nova_pro_parse_settings(
+ struct steelseries_device *sd, u8 *data, int size)
+{
+ if (size < 10)
+ return;
+
+ if (data[0] == 0x06 && data[1] == 0xb0)
+ sd->mic_volume = data[9];
+}
+
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
@@ -914,9 +943,10 @@ static const struct steelseries_device_info arctis_9_info = {
static const struct steelseries_device_info arctis_nova_3p_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 14,
.request_status = steelseries_arctis_nova_3p_request_status,
.parse_status = steelseries_arctis_nova_3p_parse_status,
.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -924,9 +954,10 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 15,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -934,9 +965,11 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 15,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5x_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -944,9 +977,11 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -954,10 +989,12 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -965,8 +1002,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.async_interface = 5,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE,
+ SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.sidetone_max = 3,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -978,11 +1017,14 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_SIDETONE,
+ SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .mic_volume_min = 1,
+ .mic_volume_max = 10,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
+ .parse_settings = steelseries_arctis_nova_pro_parse_settings,
.write_setting = steelseries_arctis_nova_pro_write_setting,
};
@@ -1343,6 +1385,71 @@ static const struct snd_kcontrol_new steelseries_sidetone_control = {
.put = steelseries_sidetone_put,
};
+static int steelseries_mic_volume_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = sd->info->mic_volume_min;
+ uinfo->value.integer.max = sd->info->mic_volume_max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_mic_volume_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->mic_volume;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_mic_volume_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ u8 new_mic_volume;
+ int ret;
+
+ new_mic_volume = ucontrol->value.integer.value[0];
+ if (new_mic_volume < sd->info->mic_volume_min ||
+ new_mic_volume > sd->info->mic_volume_max)
+ return -EINVAL;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->mic_volume == new_mic_volume) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_VOLUME,
+ new_mic_volume);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->mic_volume = new_mic_volume;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_mic_volume_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Mic Volume",
+ .info = steelseries_mic_volume_info,
+ .get = steelseries_mic_volume_get,
+ .put = steelseries_mic_volume_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1400,6 +1507,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->sidetone_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_MIC_VOLUME) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new mic_vol_ctl = steelseries_mic_volume_control;
+
+ mic_vol_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ mic_vol_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&mic_vol_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->mic_volume_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1431,6 +1553,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_chatmix_game;
bool old_mic_muted;
u8 old_sidetone;
+ u8 old_mic_volume;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1446,6 +1569,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_chatmix_game = sd->chatmix_game;
old_mic_muted = sd->mic_muted;
old_sidetone = sd->sidetone;
+ old_mic_volume = sd->mic_volume;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1509,6 +1633,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->sidetone != old_sidetone)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->sidetone_id);
+ if (sd->mic_volume != old_mic_volume)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->mic_volume_id);
}
return 0;
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox