* [PATCH v3 10/11] HID: spi-hid: add power management implementation
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Implement HID over SPI driver power management callbacks.
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/spi-hid-acpi.c | 1 +
drivers/hid/spi-hid/spi-hid-core.c | 107 +++++++++++++++++++++++++++++++++++++
drivers/hid/spi-hid/spi-hid-of.c | 1 +
drivers/hid/spi-hid/spi-hid.h | 1 +
4 files changed, 110 insertions(+)
diff --git a/drivers/hid/spi-hid/spi-hid-acpi.c b/drivers/hid/spi-hid/spi-hid-acpi.c
index 298e3ba44d8a..15cfc4e6cc2f 100644
--- a/drivers/hid/spi-hid/spi-hid-acpi.c
+++ b/drivers/hid/spi-hid/spi-hid-acpi.c
@@ -238,6 +238,7 @@ static struct spi_driver spi_hid_acpi_driver = {
.driver = {
.name = "spi_hid_acpi",
.owner = THIS_MODULE,
+ .pm = &spi_hid_core_pm,
.acpi_match_table = spi_hid_acpi_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.dev_groups = spi_hid_groups,
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index d48175c764b9..5f7a5bb692d9 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -35,6 +35,8 @@
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/pm_wakeirq.h>
#include <linux/slab.h>
#include <linux/spi/spi.h>
#include <linux/string.h>
@@ -244,6 +246,81 @@ static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state
}
}
+static void spi_hid_suspend(struct spi_hid *shid)
+{
+ int error;
+ struct device *dev = &shid->spi->dev;
+
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_OFF)
+ return;
+
+ if (shid->hid) {
+ error = hid_driver_suspend(shid->hid, PMSG_SUSPEND);
+ if (error) {
+ dev_err(dev, "%s failed to suspend hid driver: %d",
+ __func__, error);
+ return;
+ }
+ }
+
+ disable_irq(shid->spi->irq);
+
+ clear_bit(SPI_HID_READY, &shid->flags);
+
+ if (!device_may_wakeup(dev)) {
+ set_bit(SPI_HID_RESET_PENDING, &shid->flags);
+
+ shid->ops->assert_reset(shid->ops);
+
+ error = shid->ops->power_down(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power down.", __func__);
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ return;
+ }
+
+ shid->power_state = HIDSPI_OFF;
+ }
+}
+
+static void spi_hid_resume(struct spi_hid *shid)
+{
+ int error;
+ struct device *dev = &shid->spi->dev;
+
+ guard(mutex)(&shid->power_lock);
+ if (shid->power_state == HIDSPI_ON)
+ return;
+
+ enable_irq(shid->spi->irq);
+
+ if (!device_may_wakeup(dev)) {
+ shid->ops->assert_reset(shid->ops);
+
+ shid->ops->sleep_minimal_reset_delay(shid->ops);
+
+ error = shid->ops->power_up(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power up.", __func__);
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ return;
+ }
+ shid->power_state = HIDSPI_ON;
+
+ shid->ops->deassert_reset(shid->ops);
+ }
+
+ if (shid->hid) {
+ error = hid_driver_reset_resume(shid->hid);
+ if (error)
+ dev_err(dev, "%s: failed to reset resume hid driver: %d.",
+ __func__, error);
+ }
+}
+
static void spi_hid_stop_hid(struct spi_hid *shid)
{
struct hid_device *hid = shid->hid;
@@ -1200,6 +1277,13 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
dev_err(dev, "%s: unable to request threaded IRQ.", __func__);
return error;
}
+ if (device_may_wakeup(dev)) {
+ error = dev_pm_set_wake_irq(dev, spi->irq);
+ if (error) {
+ dev_err(dev, "%s: failed to set wake IRQ.", __func__);
+ return error;
+ }
+ }
error = shid->ops->power_up(shid->ops);
if (error) {
@@ -1231,6 +1315,29 @@ void spi_hid_core_remove(struct spi_device *spi)
}
EXPORT_SYMBOL_GPL(spi_hid_core_remove);
+static int spi_hid_core_pm_suspend(struct device *dev)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ spi_hid_suspend(shid);
+
+ return 0;
+}
+
+static int spi_hid_core_pm_resume(struct device *dev)
+{
+ struct spi_hid *shid = dev_get_drvdata(dev);
+
+ spi_hid_resume(shid);
+
+ return 0;
+}
+
+const struct dev_pm_ops spi_hid_core_pm = {
+ SYSTEM_SLEEP_PM_OPS(spi_hid_core_pm_suspend, spi_hid_core_pm_resume)
+};
+EXPORT_SYMBOL_GPL(spi_hid_core_pm);
+
MODULE_DESCRIPTION("HID over SPI transport driver");
MODULE_AUTHOR("Dmitry Antipov <dmanti@microsoft.com>");
MODULE_LICENSE("GPL");
diff --git a/drivers/hid/spi-hid/spi-hid-of.c b/drivers/hid/spi-hid/spi-hid-of.c
index 651456b6906d..80c481b77149 100644
--- a/drivers/hid/spi-hid/spi-hid-of.c
+++ b/drivers/hid/spi-hid/spi-hid-of.c
@@ -227,6 +227,7 @@ static struct spi_driver spi_hid_of_driver = {
.driver = {
.name = "spi_hid_of",
.owner = THIS_MODULE,
+ .pm = &spi_hid_core_pm,
.of_match_table = spi_hid_of_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.dev_groups = spi_hid_groups,
diff --git a/drivers/hid/spi-hid/spi-hid.h b/drivers/hid/spi-hid/spi-hid.h
index f5a5f4d54beb..17b2fdf192ed 100644
--- a/drivers/hid/spi-hid/spi-hid.h
+++ b/drivers/hid/spi-hid/spi-hid.h
@@ -41,5 +41,6 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
void spi_hid_core_remove(struct spi_device *spi);
extern const struct attribute_group *spi_hid_groups[];
+extern const struct dev_pm_ops spi_hid_core_pm;
#endif /* SPI_HID_H */
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 09/11] dt-bindings: input: Document hid-over-spi DT schema
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang,
Dmitry Antipov, Jarrett Schultz
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Documentation describes the required and optional properties for
implementing Device Tree for a Microsoft G6 Touch Digitizer that
supports HID over SPI Protocol 1.0 specification.
The properties are common to HID over SPI.
Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
Signed-off-by: Jarrett Schultz <jaschultz@microsoft.com>
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
.../devicetree/bindings/input/hid-over-spi.yaml | 126 +++++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/hid-over-spi.yaml b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
new file mode 100644
index 000000000000..d1b0a2e26c32
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
@@ -0,0 +1,126 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/hid-over-spi.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: HID over SPI Devices
+
+maintainers:
+ - Benjamin Tissoires <benjamin.tissoires@redhat.com>
+ - Jiri Kosina <jkosina@suse.cz>
+
+description: |+
+ HID over SPI provides support for various Human Interface Devices over the
+ SPI bus. These devices can be for example touchpads, keyboards, touch screens
+ or sensors.
+
+ The specification has been written by Microsoft and is currently available
+ here: https://www.microsoft.com/en-us/download/details.aspx?id=103325
+
+ If this binding is used, the kernel module spi-hid will handle the
+ communication with the device and the generic hid core layer will handle the
+ protocol.
+
+allOf:
+ - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+
+properties:
+ compatible:
+ oneOf:
+ - items:
+ - enum:
+ - microsoft,g6-touch-digitizer
+ - const: hid-over-spi
+ - description: Just "hid-over-spi" alone is allowed, but not recommended.
+ const: hid-over-spi
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ GPIO specifier for the digitizer's reset pin (active low). The line must
+ be flagged with GPIO_ACTIVE_LOW.
+
+ vdd-supply:
+ description:
+ Regulator for the VDD supply voltage.
+
+ input-report-header-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Read Approval packet, listing an address of
+ the input report header to be put on the SPI bus. This address has 24
+ bits.
+
+ input-report-body-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Read Approval packet, listing an address of
+ the input report body to be put on the SPI bus. This address has 24 bits.
+
+ output-report-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 0xffffff
+ description:
+ A value to be included in the Output Report sent by the host, listing an
+ address where the output report on the SPI bus is to be written to. This
+ address has 24 bits.
+
+ read-opcode:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Value to be used in Read Approval packets. 1 byte.
+
+ write-opcode:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Value to be used in Write Approval packets. 1 byte.
+
+required:
+ - compatible
+ - interrupts
+ - reset-gpios
+ - vdd-supply
+ - input-report-header-address
+ - input-report-body-address
+ - output-report-address
+ - read-opcode
+ - write-opcode
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/gpio/gpio.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ hid@0 {
+ compatible = "microsoft,g6-touch-digitizer", "hid-over-spi";
+ reg = <0x0>;
+ interrupts-extended = <&gpio 42 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&gpio 27 GPIO_ACTIVE_LOW>;
+ vdd-supply = <&pm8350c_l3>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&ts_d6_int_bias>;
+ input-report-header-address = <0x1000>;
+ input-report-body-address = <0x1004>;
+ output-report-address = <0x2000>;
+ read-opcode = /bits/ 8 <0x0b>;
+ write-opcode = /bits/ 8 <0x02>;
+ };
+ };
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* [PATCH v3 11/11] HID: spi-hid: add panel follower support
From: Jingyuan Liang @ 2026-04-02 1:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Jingyuan Liang
In-Reply-To: <20260402-send-upstream-v3-0-6091c458d357@chromium.org>
Add support to spi-hid to be a panel follower.
Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
---
drivers/hid/spi-hid/spi-hid-core.c | 199 +++++++++++++++++++++++++++++--------
drivers/hid/spi-hid/spi-hid-core.h | 7 ++
2 files changed, 163 insertions(+), 43 deletions(-)
diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 5f7a5bb692d9..9eedd4f1cba7 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -246,21 +246,21 @@ static const char *spi_hid_power_mode_string(enum hidspi_power_state power_state
}
}
-static void spi_hid_suspend(struct spi_hid *shid)
+static int spi_hid_suspend(struct spi_hid *shid)
{
int error;
struct device *dev = &shid->spi->dev;
guard(mutex)(&shid->power_lock);
if (shid->power_state == HIDSPI_OFF)
- return;
+ return 0;
if (shid->hid) {
error = hid_driver_suspend(shid->hid, PMSG_SUSPEND);
if (error) {
dev_err(dev, "%s failed to suspend hid driver: %d",
__func__, error);
- return;
+ return error;
}
}
@@ -278,21 +278,22 @@ static void spi_hid_suspend(struct spi_hid *shid)
dev_err(dev, "%s: could not power down.", __func__);
shid->regulator_error_count++;
shid->regulator_last_error = error;
- return;
+ return error;
}
shid->power_state = HIDSPI_OFF;
}
+ return 0;
}
-static void spi_hid_resume(struct spi_hid *shid)
+static int spi_hid_resume(struct spi_hid *shid)
{
int error;
struct device *dev = &shid->spi->dev;
guard(mutex)(&shid->power_lock);
if (shid->power_state == HIDSPI_ON)
- return;
+ return 0;
enable_irq(shid->spi->irq);
@@ -306,7 +307,7 @@ static void spi_hid_resume(struct spi_hid *shid)
dev_err(dev, "%s: could not power up.", __func__);
shid->regulator_error_count++;
shid->regulator_last_error = error;
- return;
+ return error;
}
shid->power_state = HIDSPI_ON;
@@ -315,10 +316,13 @@ static void spi_hid_resume(struct spi_hid *shid)
if (shid->hid) {
error = hid_driver_reset_resume(shid->hid);
- if (error)
+ if (error) {
dev_err(dev, "%s: failed to reset resume hid driver: %d.",
__func__, error);
+ return error;
+ }
}
+ return 0;
}
static void spi_hid_stop_hid(struct spi_hid *shid)
@@ -1215,6 +1219,132 @@ const struct attribute_group *spi_hid_groups[] = {
};
EXPORT_SYMBOL_GPL(spi_hid_groups);
+/*
+ * At the end of probe we initialize the device:
+ * 0) assert reset, bias the interrupt line
+ * 1) sleep minimal reset delay
+ * 2) request IRQ
+ * 3) power up the device
+ * 4) deassert reset (high)
+ * After this we expect an IRQ with a reset response.
+ */
+static int spi_hid_dev_init(struct spi_hid *shid)
+{
+ struct spi_device *spi = shid->spi;
+ struct device *dev = &spi->dev;
+ int error;
+
+ shid->ops->assert_reset(shid->ops);
+
+ shid->ops->sleep_minimal_reset_delay(shid->ops);
+
+ error = devm_request_threaded_irq(dev, spi->irq, NULL, spi_hid_dev_irq,
+ IRQF_ONESHOT, dev_name(&spi->dev), shid);
+ if (error) {
+ dev_err(dev, "%s: unable to request threaded IRQ.", __func__);
+ return error;
+ }
+ if (device_may_wakeup(dev)) {
+ error = dev_pm_set_wake_irq(dev, spi->irq);
+ if (error) {
+ dev_err(dev, "%s: failed to set wake IRQ.", __func__);
+ return error;
+ }
+ }
+
+ error = shid->ops->power_up(shid->ops);
+ if (error) {
+ dev_err(dev, "%s: could not power up.", __func__);
+ shid->regulator_error_count++;
+ shid->regulator_last_error = error;
+ return error;
+ }
+
+ shid->ops->deassert_reset(shid->ops);
+
+ return 0;
+}
+
+static void spi_hid_panel_follower_work(struct work_struct *work)
+{
+ struct spi_hid *shid = container_of(work, struct spi_hid,
+ panel_follower_work);
+ int error;
+
+ if (!shid->desc.hid_version)
+ error = spi_hid_dev_init(shid);
+ else
+ error = spi_hid_resume(shid);
+ if (error)
+ dev_warn(&shid->spi->dev, "Power on failed: %d", error);
+ else
+ WRITE_ONCE(shid->panel_follower_work_finished, true);
+
+ /*
+ * The work APIs provide a number of memory ordering guarantees
+ * including one that says that memory writes before schedule_work()
+ * are always visible to the work function, but they don't appear to
+ * guarantee that a write that happened in the work is visible after
+ * cancel_work_sync(). We'll add a write memory barrier here to match
+ * with spi_hid_panel_unpreparing() to ensure that our write to
+ * panel_follower_work_finished is visible there.
+ */
+ smp_wmb();
+}
+
+static int spi_hid_panel_follower_resume(struct drm_panel_follower *follower)
+{
+ struct spi_hid *shid = container_of(follower, struct spi_hid, panel_follower);
+
+ /*
+ * Powering on a touchscreen can be a slow process. Queue the work to
+ * the system workqueue so we don't block the panel's power up.
+ */
+ WRITE_ONCE(shid->panel_follower_work_finished, false);
+ schedule_work(&shid->panel_follower_work);
+
+ return 0;
+}
+
+static int spi_hid_panel_follower_suspend(struct drm_panel_follower *follower)
+{
+ struct spi_hid *shid = container_of(follower, struct spi_hid, panel_follower);
+
+ cancel_work_sync(&shid->panel_follower_work);
+
+ /* Match with shid_core_panel_follower_work() */
+ smp_rmb();
+ if (!READ_ONCE(shid->panel_follower_work_finished))
+ return 0;
+
+ return spi_hid_suspend(shid);
+}
+
+static const struct drm_panel_follower_funcs
+ spi_hid_panel_follower_prepare_funcs = {
+ .panel_prepared = spi_hid_panel_follower_resume,
+ .panel_unpreparing = spi_hid_panel_follower_suspend,
+};
+
+static int spi_hid_register_panel_follower(struct spi_hid *shid)
+{
+ struct device *dev = &shid->spi->dev;
+
+ shid->panel_follower.funcs = &spi_hid_panel_follower_prepare_funcs;
+
+ /*
+ * If we're not in control of our own power up/power down then we can't
+ * do the logic to manage wakeups. Give a warning if a user thought
+ * that was possible then force the capability off.
+ */
+ if (device_can_wakeup(dev)) {
+ dev_warn(dev, "Can't wakeup if following panel\n");
+ device_set_wakeup_capable(dev, false);
+ }
+
+ return drm_panel_add_follower(dev, &shid->panel_follower);
+}
+
int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
struct spi_hid_conf *conf)
{
@@ -1234,6 +1364,7 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
shid->ops = ops;
shid->conf = conf;
set_bit(SPI_HID_RESET_PENDING, &shid->flags);
+ shid->is_panel_follower = drm_is_panel_follower(&spi->dev);
spi_set_drvdata(spi, shid);
@@ -1247,6 +1378,7 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
init_completion(&shid->output_done);
INIT_WORK(&shid->reset_work, spi_hid_reset_work);
+ INIT_WORK(&shid->panel_follower_work, spi_hid_panel_follower_work);
/*
* We need to allocate the buffer without knowing the maximum
@@ -1257,42 +1389,18 @@ int spi_hid_core_probe(struct spi_device *spi, struct spihid_ops *ops,
if (error)
return error;
- /*
- * At the end of probe we initialize the device:
- * 0) assert reset, bias the interrupt line
- * 1) sleep minimal reset delay
- * 2) request IRQ
- * 3) power up the device
- * 4) deassert reset (high)
- * After this we expect an IRQ with a reset response.
- */
-
- shid->ops->assert_reset(shid->ops);
-
- shid->ops->sleep_minimal_reset_delay(shid->ops);
-
- error = devm_request_threaded_irq(dev, spi->irq, NULL, spi_hid_dev_irq,
- IRQF_ONESHOT, dev_name(&spi->dev), shid);
- if (error) {
- dev_err(dev, "%s: unable to request threaded IRQ.", __func__);
- return error;
- }
- if (device_may_wakeup(dev)) {
- error = dev_pm_set_wake_irq(dev, spi->irq);
+ if (shid->is_panel_follower) {
+ error = spi_hid_register_panel_follower(shid);
if (error) {
- dev_err(dev, "%s: failed to set wake IRQ.", __func__);
+ dev_err(dev, "%s: could not add panel follower.", __func__);
return error;
}
+ } else {
+ error = spi_hid_dev_init(shid);
+ if (error)
+ return error;
}
- error = shid->ops->power_up(shid->ops);
- if (error) {
- dev_err(dev, "%s: could not power up.", __func__);
- return error;
- }
-
- shid->ops->deassert_reset(shid->ops);
-
dev_dbg(dev, "%s: d3 -> %s.", __func__,
spi_hid_power_mode_string(shid->power_state));
@@ -1306,6 +1414,9 @@ void spi_hid_core_remove(struct spi_device *spi)
struct device *dev = &spi->dev;
int error;
+ if (shid->is_panel_follower)
+ drm_panel_remove_follower(&shid->panel_follower);
+
spi_hid_stop_hid(shid);
shid->ops->assert_reset(shid->ops);
@@ -1319,18 +1430,20 @@ static int spi_hid_core_pm_suspend(struct device *dev)
{
struct spi_hid *shid = dev_get_drvdata(dev);
- spi_hid_suspend(shid);
+ if (shid->is_panel_follower)
+ return 0;
- return 0;
+ return spi_hid_suspend(shid);
}
static int spi_hid_core_pm_resume(struct device *dev)
{
struct spi_hid *shid = dev_get_drvdata(dev);
- spi_hid_resume(shid);
+ if (shid->is_panel_follower)
+ return 0;
- return 0;
+ return spi_hid_resume(shid);
}
const struct dev_pm_ops spi_hid_core_pm = {
diff --git a/drivers/hid/spi-hid/spi-hid-core.h b/drivers/hid/spi-hid/spi-hid-core.h
index 293e2cfcfbf7..261b2fd7f332 100644
--- a/drivers/hid/spi-hid/spi-hid-core.h
+++ b/drivers/hid/spi-hid/spi-hid-core.h
@@ -10,6 +10,8 @@
#include <linux/hid-over-spi.h>
#include <linux/spi/spi.h>
+#include <drm/drm_panel.h>
+
/* Protocol message size constants */
#define SPI_HID_READ_APPROVAL_LEN 5
#define SPI_HID_OUTPUT_HEADER_LEN 8
@@ -56,6 +58,10 @@ struct spi_hid {
struct spi_hid_input_buf *input; /* Input buffer. */
struct spi_hid_input_buf *response; /* Response buffer. */
+ struct drm_panel_follower panel_follower;
+ bool is_panel_follower;
+ bool panel_follower_work_finished;
+
u16 response_length;
u16 bufsize;
@@ -66,6 +72,7 @@ struct spi_hid {
unsigned long flags; /* device flags. */
struct work_struct reset_work;
+ struct work_struct panel_follower_work;
/* Control lock to ensure complete output transaction. */
struct mutex output_lock;
--
2.53.0.1185.g05d4b7b318-goog
^ permalink raw reply related
* Re: [PATCH v3 3/3] input: touchscreen: st1232: add system wakeup support
From: Dmitry Torokhov @ 2026-04-02 5:17 UTC (permalink / raw)
To: phucduc.bui
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Geert Uytterhoeven, Magnus Damm, Wolfram Sang, Jeff LaBundy,
Bastian Hecht, Javier Carrasco, linux-input, devicetree,
linux-renesas-soc, linux-kernel
In-Reply-To: <20260306111912.58388-4-phucduc.bui@gmail.com>
On Fri, Mar 06, 2026 at 06:19:12PM +0700, phucduc.bui@gmail.com wrote:
> From: bui duc phuc <phucduc.bui@gmail.com>
>
> The ST1232 touchscreen controller can generate an interrupt when the
> panel is touched, which may be used as a wakeup source for the system.
>
> Add support for system wakeup by initializing the device wakeup
> capability in probe() based on the "wakeup-source" device property.
> When wakeup is enabled, the driver enables IRQ wake during suspend
> so that touch events can wake the system.
>
> If wakeup is not enabled, the driver retains the existing behavior of
> disabling the IRQ and powering down the controller during suspend.
I do not believe this patch is needed: i2c core already handles
"wakeup-source" property and manages wakeup IRQ.
Thanks.
--
Dmitry
^ permalink raw reply
* [dtor-input:next] BUILD SUCCESS 4decd8f4ae06a6d82079186b6ad3fe51d4654a1d
From: kernel test robot @ 2026-04-02 2:01 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input
tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
branch HEAD: 4decd8f4ae06a6d82079186b6ad3fe51d4654a1d Input: synaptics_usb - refactor endpoint lookup
elapsed time: 1533m
configs tested: 157
configs skipped: 2
The following configs have been built successfully.
More configs may be tested in the coming days.
tested configs:
alpha allnoconfig gcc-15.2.0
alpha allyesconfig gcc-15.2.0
arc allmodconfig clang-16
arc allnoconfig gcc-15.2.0
arc allyesconfig clang-23
arc randconfig-001-20260401 clang-23
arc randconfig-002-20260401 clang-23
arm allnoconfig gcc-15.2.0
arm allyesconfig clang-16
arm randconfig-001-20260401 clang-23
arm randconfig-002-20260401 clang-23
arm randconfig-003-20260401 clang-23
arm randconfig-004-20260401 clang-23
arm64 allmodconfig clang-23
arm64 allnoconfig gcc-15.2.0
arm64 randconfig-001-20260401 gcc-15.2.0
arm64 randconfig-002-20260401 gcc-15.2.0
arm64 randconfig-003-20260401 gcc-15.2.0
arm64 randconfig-004-20260401 gcc-15.2.0
csky allmodconfig gcc-15.2.0
csky allnoconfig gcc-15.2.0
csky randconfig-001-20260401 gcc-15.2.0
csky randconfig-002-20260401 gcc-15.2.0
hexagon allmodconfig gcc-15.2.0
hexagon allnoconfig gcc-15.2.0
hexagon randconfig-001-20260401 gcc-15.2.0
hexagon randconfig-002-20260401 gcc-15.2.0
i386 allmodconfig clang-20
i386 allnoconfig gcc-15.2.0
i386 allyesconfig clang-20
i386 buildonly-randconfig-001-20260401 gcc-14
i386 buildonly-randconfig-002-20260401 gcc-14
i386 buildonly-randconfig-003-20260401 gcc-14
i386 buildonly-randconfig-004-20260401 gcc-14
i386 buildonly-randconfig-005-20260401 gcc-14
i386 buildonly-randconfig-006-20260401 gcc-14
i386 randconfig-001-20260401 gcc-14
i386 randconfig-002-20260401 gcc-14
i386 randconfig-003-20260401 gcc-14
i386 randconfig-004-20260401 gcc-14
i386 randconfig-005-20260401 gcc-14
i386 randconfig-006-20260401 gcc-14
i386 randconfig-007-20260401 gcc-14
i386 randconfig-011-20260401 clang-20
i386 randconfig-012-20260401 clang-20
i386 randconfig-013-20260401 clang-20
i386 randconfig-014-20260401 clang-20
i386 randconfig-015-20260401 clang-20
i386 randconfig-016-20260401 clang-20
i386 randconfig-017-20260401 clang-20
loongarch allmodconfig clang-23
loongarch allnoconfig gcc-15.2.0
loongarch defconfig clang-19
loongarch randconfig-001-20260401 gcc-15.2.0
loongarch randconfig-002-20260401 gcc-15.2.0
m68k allmodconfig gcc-15.2.0
m68k allnoconfig gcc-15.2.0
m68k allyesconfig clang-16
m68k defconfig clang-19
microblaze allnoconfig gcc-15.2.0
microblaze allyesconfig gcc-15.2.0
microblaze defconfig clang-19
mips allnoconfig gcc-15.2.0
mips allyesconfig gcc-15.2.0
nios2 allmodconfig clang-23
nios2 allnoconfig clang-23
nios2 defconfig clang-19
nios2 randconfig-001-20260401 gcc-15.2.0
nios2 randconfig-002-20260401 gcc-15.2.0
openrisc allmodconfig clang-23
openrisc allnoconfig clang-23
openrisc defconfig gcc-15.2.0
parisc allnoconfig clang-23
parisc allyesconfig clang-19
parisc defconfig gcc-15.2.0
parisc randconfig-001-20260401 gcc-8.5.0
parisc randconfig-002-20260401 gcc-8.5.0
parisc64 defconfig clang-19
powerpc allnoconfig clang-23
powerpc holly_defconfig clang-23
powerpc randconfig-001-20260401 gcc-8.5.0
powerpc randconfig-002-20260401 gcc-8.5.0
powerpc64 randconfig-001-20260401 gcc-8.5.0
powerpc64 randconfig-002-20260401 gcc-8.5.0
riscv allmodconfig clang-23
riscv allnoconfig clang-23
riscv allyesconfig clang-16
riscv defconfig gcc-15.2.0
riscv randconfig-001-20260401 gcc-9.5.0
riscv randconfig-002-20260401 gcc-9.5.0
s390 allmodconfig clang-19
s390 allnoconfig clang-23
s390 defconfig gcc-15.2.0
s390 randconfig-001-20260401 gcc-9.5.0
s390 randconfig-002-20260401 gcc-9.5.0
sh allmodconfig gcc-15.2.0
sh allnoconfig clang-23
sh allyesconfig clang-19
sh defconfig gcc-14
sh randconfig-001-20260401 gcc-9.5.0
sh randconfig-002-20260401 gcc-9.5.0
sparc allnoconfig clang-23
sparc defconfig gcc-15.2.0
sparc randconfig-001-20260401 clang-16
sparc randconfig-002-20260401 clang-16
sparc64 allmodconfig clang-23
sparc64 defconfig gcc-14
sparc64 randconfig-001-20260401 clang-16
sparc64 randconfig-002-20260401 clang-16
um allmodconfig clang-19
um allnoconfig clang-23
um allyesconfig gcc-15.2.0
um defconfig gcc-14
um i386_defconfig gcc-14
um randconfig-001-20260401 clang-16
um randconfig-002-20260401 clang-16
um x86_64_defconfig gcc-14
x86_64 allmodconfig clang-20
x86_64 allnoconfig clang-23
x86_64 allyesconfig clang-20
x86_64 buildonly-randconfig-001-20260401 gcc-12
x86_64 buildonly-randconfig-002-20260401 gcc-12
x86_64 buildonly-randconfig-003-20260401 gcc-12
x86_64 buildonly-randconfig-004-20260401 gcc-12
x86_64 buildonly-randconfig-005-20260401 gcc-12
x86_64 buildonly-randconfig-006-20260401 gcc-12
x86_64 defconfig gcc-14
x86_64 kexec clang-20
x86_64 randconfig-001-20260401 clang-20
x86_64 randconfig-002-20260401 clang-20
x86_64 randconfig-003-20260401 clang-20
x86_64 randconfig-004-20260401 clang-20
x86_64 randconfig-005-20260401 clang-20
x86_64 randconfig-006-20260401 clang-20
x86_64 randconfig-011-20260401 gcc-14
x86_64 randconfig-012-20260401 gcc-14
x86_64 randconfig-013-20260401 gcc-14
x86_64 randconfig-014-20260401 gcc-14
x86_64 randconfig-015-20260401 gcc-14
x86_64 randconfig-016-20260401 gcc-14
x86_64 randconfig-071-20260401 gcc-14
x86_64 randconfig-072-20260401 gcc-14
x86_64 randconfig-073-20260401 gcc-14
x86_64 randconfig-074-20260401 gcc-14
x86_64 randconfig-075-20260401 gcc-14
x86_64 randconfig-076-20260401 gcc-14
x86_64 rhel-9.4 clang-20
x86_64 rhel-9.4-bpf gcc-14
x86_64 rhel-9.4-func clang-20
x86_64 rhel-9.4-kselftests clang-20
x86_64 rhel-9.4-kunit gcc-14
x86_64 rhel-9.4-ltp gcc-14
x86_64 rhel-9.4-rust clang-20
xtensa allnoconfig clang-23
xtensa allyesconfig clang-23
xtensa randconfig-001-20260401 clang-16
xtensa randconfig-002-20260401 clang-16
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH v3 3/3] input: touchscreen: st1232: add system wakeup support
From: Geert Uytterhoeven @ 2026-04-02 6:56 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: phucduc.bui, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Magnus Damm, Wolfram Sang, Jeff LaBundy, Bastian Hecht,
Javier Carrasco, linux-input, devicetree, linux-renesas-soc,
linux-kernel
In-Reply-To: <ac37o-N5lqFMwDCC@google.com>
Hi Dmitry,
On Thu, 2 Apr 2026 at 07:17, Dmitry Torokhov <dmitry.torokhov@gmail.com> wrote:
> On Fri, Mar 06, 2026 at 06:19:12PM +0700, phucduc.bui@gmail.com wrote:
> > From: bui duc phuc <phucduc.bui@gmail.com>
> >
> > The ST1232 touchscreen controller can generate an interrupt when the
> > panel is touched, which may be used as a wakeup source for the system.
> >
> > Add support for system wakeup by initializing the device wakeup
> > capability in probe() based on the "wakeup-source" device property.
> > When wakeup is enabled, the driver enables IRQ wake during suspend
> > so that touch events can wake the system.
> >
> > If wakeup is not enabled, the driver retains the existing behavior of
> > disabling the IRQ and powering down the controller during suspend.
>
> I do not believe this patch is needed: i2c core already handles
> "wakeup-source" property and manages wakeup IRQ.
No, it is not needed, as mentioned in the cover letter of v4[1],
and as tested by me[2].
[1] https://lore.kernel.org/20260309000319.74880-1-phucduc.bui@gmail.com
[2] https://lore.kernel.org/CAMuHMdUqiaP=COTkKU_jK6Hdii+YJ5+zXnxFkOOnhLri5NakTw@mail.gmail.com
Gr{oetje,eeting}s,
Geert
--
Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org
In personal conversations with technical people, I call myself a hacker. But
when I'm talking to journalists I just say "programmer" or something like that.
-- Linus Torvalds
^ permalink raw reply
* Re: [PATCH] HID: gpd: fix report descriptor on GPD Win handheld (2f24:0137)
From: Jiri Kosina @ 2026-04-02 7:52 UTC (permalink / raw)
To: Denis Benato
Cc: Thorsten Leemhuis, honjow, Benjamin Tissoires, linux-kernel,
linux-input, Linux kernel regressions list
In-Reply-To: <76510dfe-d6da-4398-a78c-a02c596d6f75@linux.dev>
On Wed, 1 Apr 2026, Denis Benato wrote:
> > Hmmm, why does this need to be a config option? Can't this be enabled
> > unconditionally? I ask in general, as it's just another point where
> > things can go wrong. But I mainly ask because it's a regression fix –
> > and from my understanding wrt to what Linus wants we don't expect users
> > to turn some .config on to keep their hardware running (unless it can't
> > be avoided at all costs).
> >
> > Ciao, Thorsten
> >
> Perhaps the place of this, just for affected kernel series that are out
> (assuming stable has the bug, I haven't checked) is to do this on hid-generic,
> otherwise it's a change of driver managing the device?
The problem is that the device has a broken (violating spec) report
descriptor.
We got away from it before the change, but with more sanitization having
been put in place in order to deal with potentially malicious devices,
this device (correctly, in some sense), stopped working.
The way around it is to replace the report descriptor with a fixed one.
And we do it with a specific driver for now (there have been other
proposals, like request_firmware(), or it could eventually be done from
userspace / BPF, but none of it hasn't been yet implemented).
I have also been thinking about having a config option that'd link all the
HID drivers into one huge hid.ko blob for those scenarios where people
don't care about memory / disk footprint, and want everything to "just
work" automatically (thinking of distro kernels, for example).
--
Jiri Kosina
SUSE Labs
^ permalink raw reply
* [PATCH] HID: lg-g15: Add support for the Logitech G710/G710+ gaming keyboards
From: Xavier Bestel @ 2026-04-02 7:52 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Hans de Goede, linux-input, linux-kernel, Xavier Bestel
Add support for the Logitech G710 and G710+ (USB ID 046d:c24d) gaming
keyboards to the hid-lg-g15 driver.
These keyboards have 6 G-keys (G1-G6), M-keys (M1-M3, MR), a game mode
toggle, and two independent backlights: one for the main keyboard and one
for the WASD keys, each with a physical button to cycle brightness through
5 levels (0-4).
Key implementation details:
- G-keys and M-keys are reported via HID input report 0x03 (4 bytes)
using KEY_MACRO1-6, KEY_MACRO_PRESET1-3 and KEY_MACRO_RECORD_START.
- The WASD backlight LED is registered as
"g15::kbd_zoned_backlight-wasd" rather than with a
"::kbd_backlight" suffix, because UPower currently only supports a
single kbd_backlight LED per device. This follows the nomenclature
from Documentation/leds/leds-class.rst
- The G710+ firmware GET_REPORT for the backlight feature (0x08)
always returns the power-on default values, ignoring any changes
made via SET_REPORT. To work around this, the backlight brightness
is tracked in the driver cache and brightness_get returns the
cached value. M-key and game mode LEDs read back correctly.
- The physical brightness cycle buttons are handled following the
same pattern as the G15/G15v2: no key events are sent, instead the
driver cycles the cached brightness and calls
led_classdev_notify_brightness_hw_changed() from a work function,
which allows GNOME to show the brightness OSD.
- The game mode toggle is handled entirely by the firmware (it
disables the Super key and lights an indicator LED). The driver
exposes a read-only LED "g15::gamemode" with the
LED_BRIGHT_HW_CHANGED flag, and notifies userspace of state
changes via led_classdev_notify_brightness_hw_changed() by reading
back the actual firmware state on each toggle.
- Both brightness LEDs are registered with the LED_BRIGHT_HW_CHANGED
flag to enable the brightness_hw_changed sysfs attribute.
- HID_QUIRK_NOGET is set because the keyboard has buggy GET_REPORT
handling that causes timeouts on some feature reports.
- The G-keys feature report (0x09) uses 2 bytes per key rather than
1 as on the G510, so the report size calculation is adjusted
accordingly.
- Also fix a pre-existing comment typo: "f000.0000" -> "ff00.0000"
for the application report ID.
- The loop bounds in lg_g15_led_set() and lg_g510_led_set() are
tightened from "< LG_G15_LED_MAX" to "<= LG_G15_MACRO_RECORD" to
avoid iterating over the new LG_G15_GAMEMODE enum value which does
not apply to those keyboards.
Signed-off-by: Xavier Bestel <xav@bes.tel>
---
drivers/hid/hid-ids.h | 1 +
drivers/hid/hid-lg-g15.c | 393 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 384 insertions(+), 10 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index afcee13bad61..7c0f930bd014 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -904,6 +904,7 @@
#define USB_DEVICE_ID_LOGITECH_G15_V2_LCD 0xc227
#define USB_DEVICE_ID_LOGITECH_G510 0xc22d
#define USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO 0xc22e
+#define USB_DEVICE_ID_LOGITECH_G710 0xc24d
#define USB_DEVICE_ID_LOGITECH_G29_WHEEL 0xc24f
#define USB_DEVICE_ID_LOGITECH_G920_WHEEL 0xc262
#define USB_DEVICE_ID_LOGITECH_G923_XBOX_WHEEL 0xc26e
diff --git a/drivers/hid/hid-lg-g15.c b/drivers/hid/hid-lg-g15.c
index 1a88bc44ada4..8a4c4eb22c07 100644
--- a/drivers/hid/hid-lg-g15.c
+++ b/drivers/hid/hid-lg-g15.c
@@ -29,6 +29,11 @@
#define LG_G510_INPUT_MACRO_KEYS 0x03
#define LG_G510_INPUT_KBD_BACKLIGHT 0x04
+#define LG_G710_FEATURE_GAMEMODE 0x05
+#define LG_G710_FEATURE_M_KEYS_LEDS 0x06
+#define LG_G710_FEATURE_BACKLIGHT 0x08
+#define LG_G710_FEATURE_EXTRA_KEYS 0x09
+
#define LG_G13_INPUT_REPORT 0x01
#define LG_G13_FEATURE_M_KEYS_LEDS 0x05
#define LG_G13_FEATURE_BACKLIGHT_RGB 0x07
@@ -48,6 +53,7 @@ enum lg_g15_model {
LG_G15_V2,
LG_G510,
LG_G510_USB_AUDIO,
+ LG_G710,
LG_Z10,
};
@@ -59,6 +65,7 @@ enum lg_g15_led_type {
LG_G15_MACRO_PRESET2,
LG_G15_MACRO_PRESET3,
LG_G15_MACRO_RECORD,
+ LG_G15_GAMEMODE,
LG_G15_LED_MAX
};
@@ -91,7 +98,9 @@ struct lg_g15_data {
enum lg_g15_model model;
struct lg_g15_led leds[LG_G15_LED_MAX];
bool game_mode_enabled;
+ u16 pressed_keys;
bool backlight_disabled; /* true == HW backlight toggled *OFF* */
+ unsigned long brightness_changed; /* bitmask of LEDs hw-cycled */
};
/********* G13 LED functions ***********/
@@ -334,7 +343,7 @@ static int lg_g15_led_set(struct led_classdev *led_cdev,
g15->transfer_buf[1] = g15_led->led + 1;
g15->transfer_buf[2] = brightness << (g15_led->led * 4);
} else {
- for (i = LG_G15_MACRO_PRESET1; i < LG_G15_LED_MAX; i++) {
+ for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
if (i == g15_led->led)
val = brightness;
else
@@ -567,7 +576,7 @@ static int lg_g510_mkey_led_set(struct led_classdev *led_cdev,
mutex_lock(&g15->mutex);
- for (i = LG_G15_MACRO_PRESET1; i < LG_G15_LED_MAX; i++) {
+ for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
if (i == g15_led->led)
val = brightness;
else
@@ -597,6 +606,239 @@ static int lg_g510_mkey_led_set(struct led_classdev *led_cdev,
return ret;
}
+/******** G710 LED functions ********/
+
+static int lg_g710_update_game_led_brightness(struct lg_g15_data *g15)
+{
+ int ret;
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_GAMEMODE,
+ g15->transfer_buf, 8,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret != 8) {
+ hid_err(g15->hdev, "Error getting gamemode LED brightness: %d\n", ret);
+ return (ret < 0) ? ret : -EIO;
+ }
+
+ g15->leds[LG_G15_GAMEMODE].brightness =
+ !!g15->transfer_buf[1];
+
+ return 0;
+}
+
+static int lg_g710_update_mkey_led_brightness(struct lg_g15_data *g15)
+{
+ int ret;
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_M_KEYS_LEDS,
+ g15->transfer_buf, 2,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret != 2) {
+ hid_err(g15->hdev, "Error getting macro LED brightness: %d\n", ret);
+ return (ret < 0) ? ret : -EIO;
+ }
+
+ g15->leds[LG_G15_MACRO_PRESET1].brightness =
+ !!(g15->transfer_buf[1] & 0x10);
+ g15->leds[LG_G15_MACRO_PRESET2].brightness =
+ !!(g15->transfer_buf[1] & 0x20);
+ g15->leds[LG_G15_MACRO_PRESET3].brightness =
+ !!(g15->transfer_buf[1] & 0x40);
+ g15->leds[LG_G15_MACRO_RECORD].brightness =
+ !!(g15->transfer_buf[1] & 0x80);
+
+ return 0;
+}
+
+static int lg_g710_update_kbd_led_brightness(struct lg_g15_data *g15)
+{
+ int ret;
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_BACKLIGHT,
+ g15->transfer_buf, 4,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret != 4) {
+ hid_err(g15->hdev, "Error getting LED brightness: %d\n", ret);
+ return (ret < 0) ? ret : -EIO;
+ }
+
+ g15->leds[LG_G15_KBD_BRIGHTNESS].brightness = 4 - g15->transfer_buf[2];
+ g15->leds[LG_G15_LCD_BRIGHTNESS].brightness = 4 - g15->transfer_buf[1];
+
+ return 0;
+}
+
+static enum led_brightness lg_g710_led_get(struct led_classdev *led_cdev)
+{
+ struct lg_g15_led *g15_led =
+ container_of(led_cdev, struct lg_g15_led, cdev);
+ struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
+ enum led_brightness brightness;
+
+ mutex_lock(&g15->mutex);
+ /*
+ * The G710+ firmware's GET_REPORT for the backlight always returns
+ * the power-on default values, ignoring any changes made via
+ * SET_REPORT. Use the cached brightness which is kept in sync by
+ * the _set callbacks. M-key and gamemode LEDs read back correctly.
+ */
+ if (g15_led->led >= LG_G15_BRIGHTNESS_MAX && g15_led->led < LG_G15_GAMEMODE)
+ lg_g710_update_mkey_led_brightness(g15);
+ else if (g15_led->led >= LG_G15_GAMEMODE)
+ lg_g710_update_game_led_brightness(g15);
+ brightness = g15->leds[g15_led->led].brightness;
+ mutex_unlock(&g15->mutex);
+
+ return brightness;
+}
+
+static int lg_g710_mkey_led_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct lg_g15_led *g15_led =
+ container_of(led_cdev, struct lg_g15_led, cdev);
+ struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
+ u8 val, mask = 0;
+ int i, ret;
+
+ /* Ignore LED off on unregister / keyboard unplug */
+ if (led_cdev->flags & LED_UNREGISTERING)
+ return 0;
+
+ mutex_lock(&g15->mutex);
+
+ g15->transfer_buf[0] = LG_G710_FEATURE_M_KEYS_LEDS;
+
+ for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
+ if (i == g15_led->led)
+ val = brightness;
+ else
+ val = g15->leds[i].brightness;
+
+ if (val)
+ mask |= 1 << (i - LG_G15_MACRO_PRESET1 + 4);
+ }
+
+ g15->transfer_buf[1] = mask;
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_M_KEYS_LEDS,
+ g15->transfer_buf, 2,
+ HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
+ if (ret == 2) {
+ /* Success */
+ g15_led->brightness = brightness;
+ ret = 0;
+ } else {
+ hid_err(g15->hdev, "Error setting LED brightness: %d\n", ret);
+ ret = (ret < 0) ? ret : -EIO;
+ }
+
+ mutex_unlock(&g15->mutex);
+
+ return ret;
+}
+
+static int lg_g710_kbd_led_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct lg_g15_led *g15_led =
+ container_of(led_cdev, struct lg_g15_led, cdev);
+ struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
+ int ret;
+
+ /* Ignore LED off on unregister / keyboard unplug */
+ if (led_cdev->flags & LED_UNREGISTERING)
+ return 0;
+
+ mutex_lock(&g15->mutex);
+
+ g15->transfer_buf[0] = LG_G710_FEATURE_BACKLIGHT;
+ g15->transfer_buf[3] = 0;
+
+ if (g15_led->led == LG_G15_KBD_BRIGHTNESS) {
+ g15->transfer_buf[1] = 4 - g15->leds[LG_G15_LCD_BRIGHTNESS].brightness;
+ g15->transfer_buf[2] = 4 - brightness;
+ } else {
+ g15->transfer_buf[1] = 4 - brightness;
+ g15->transfer_buf[2] = 4 - g15->leds[LG_G15_KBD_BRIGHTNESS].brightness;
+ }
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_BACKLIGHT,
+ g15->transfer_buf, 4,
+ HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
+ if (ret == 4) {
+ /* Success */
+ g15_led->brightness = brightness;
+ ret = 0;
+ } else {
+ hid_err(g15->hdev, "Error setting LED brightness: %d\n", ret);
+ ret = (ret < 0) ? ret : -EIO;
+ }
+
+ mutex_unlock(&g15->mutex);
+
+ return ret;
+}
+
+/*
+ * The G710+ has separate physical keys for cycling the main keyboard backlight
+ * and the WASD backlight. The firmware handles the actual brightness change
+ * internally, but GET_REPORT always returns the power-on defaults regardless
+ * of any changes. So we must track the brightness in our cache and cycle it
+ * ourselves when a hardware brightness key press is detected.
+ *
+ * The firmware cycles brightness DOWN: 4 → 3 → 2 → 1 → 0 → 4 (in wire
+ * format where 0 = brightest, 4 = off). In user-facing terms (inverted):
+ * 4 → 3 → 2 → 1 → 0 → 4.
+ *
+ * The game mode toggle is also handled here: the firmware toggles game mode
+ * internally and updates the LED, so we read back the actual state via
+ * GET_REPORT and notify userspace of the change.
+ */
+static void lg_g710_leds_changed_work(struct work_struct *work)
+{
+ struct lg_g15_data *g15 = container_of(work, struct lg_g15_data, work);
+ enum led_brightness brightness[LG_G15_BRIGHTNESS_MAX];
+ bool changed[LG_G15_BRIGHTNESS_MAX] = {};
+ bool gamemode_changed;
+ int i;
+
+ mutex_lock(&g15->mutex);
+ for (i = 0; i < LG_G15_BRIGHTNESS_MAX; i++) {
+ if (!test_and_clear_bit(i, &g15->brightness_changed))
+ continue;
+
+ changed[i] = true;
+
+ if (g15->leds[i].brightness > 0)
+ g15->leds[i].brightness--;
+ else
+ g15->leds[i].brightness =
+ g15->leds[i].cdev.max_brightness;
+
+ brightness[i] = g15->leds[i].brightness;
+ }
+
+ gamemode_changed = test_and_clear_bit(LG_G15_GAMEMODE,
+ &g15->brightness_changed);
+ if (gamemode_changed)
+ lg_g710_update_game_led_brightness(g15);
+ mutex_unlock(&g15->mutex);
+
+ for (i = 0; i < LG_G15_BRIGHTNESS_MAX; i++) {
+ if (!changed[i])
+ continue;
+
+ led_classdev_notify_brightness_hw_changed(&g15->leds[i].cdev,
+ brightness[i]);
+ }
+
+ if (gamemode_changed)
+ led_classdev_notify_brightness_hw_changed(
+ &g15->leds[LG_G15_GAMEMODE].cdev,
+ g15->leds[LG_G15_GAMEMODE].brightness);
+}
+
/******** Generic LED functions ********/
static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
{
@@ -619,6 +861,16 @@ static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
return ret;
return lg_g510_update_mkey_led_brightness(g15);
+ case LG_G710:
+ ret = lg_g710_update_game_led_brightness(g15);
+ if (ret)
+ return ret;
+
+ ret = lg_g710_update_mkey_led_brightness(g15);
+ if (ret)
+ return ret;
+
+ return lg_g710_update_kbd_led_brightness(g15);
case LG_Z10:
/*
* Getting the LCD backlight brightness is not supported.
@@ -890,6 +1142,74 @@ static int lg_g510_leds_event(struct lg_g15_data *g15, u8 *data)
return 0;
}
+static int lg_g710_event(struct lg_g15_data *g15, u8 *data, int size)
+{
+ /*
+ * Bits 0-5: G1-G6 keys
+ * Bits 6-8: M1-M3 keys
+ * Bit 9: MR key
+ * Bit 10: WASD backlight cycle (handled as hw brightness change)
+ * Bit 11: Kbd backlight cycle (handled as hw brightness change)
+ * Bit 12: Game mode toggle (LED state change, handled by firmware)
+ */
+ static const u16 keymap[] = {
+ KEY_MACRO1,
+ KEY_MACRO2,
+ KEY_MACRO3,
+ KEY_MACRO4,
+ KEY_MACRO5,
+ KEY_MACRO6,
+ KEY_MACRO_PRESET1,
+ KEY_MACRO_PRESET2,
+ KEY_MACRO_PRESET3,
+ KEY_MACRO_RECORD_START,
+ 0, /* WASD illumination cycle - not a key event */
+ 0, /* Kbd illumination cycle - not a key event */
+ 0, /* Game mode toggle */
+ };
+ u16 pressed_keys, changed;
+ int i;
+
+ if (size != 4 || data[0] != 3)
+ return 1;
+
+ pressed_keys = (data[1] & 0x3f) | ((data[2] & 0xf0) << 2) |
+ ((data[3] & 0x7) << 10);
+ changed = pressed_keys ^ g15->pressed_keys;
+
+ for (i = 0; i < ARRAY_SIZE(keymap); i++) {
+ if (keymap[i] && (changed & BIT(i)))
+ input_report_key(g15->input, keymap[i],
+ pressed_keys & BIT(i));
+ }
+ input_sync(g15->input);
+
+ /*
+ * Detect brightness key presses (0->1 transition) and schedule
+ * the work function to cycle cached brightness and notify userspace.
+ * Bit 10 = WASD backlight (maps to LG_G15_LCD_BRIGHTNESS slot).
+ * Bit 11 = Kbd backlight (maps to LG_G15_KBD_BRIGHTNESS slot).
+ */
+ if ((changed & BIT(10)) && (pressed_keys & BIT(10))) {
+ set_bit(LG_G15_LCD_BRIGHTNESS, &g15->brightness_changed);
+ schedule_work(&g15->work);
+ }
+ if ((changed & BIT(11)) && (pressed_keys & BIT(11))) {
+ set_bit(LG_G15_KBD_BRIGHTNESS, &g15->brightness_changed);
+ schedule_work(&g15->work);
+ }
+
+ /* Game mode toggle — bit 12 is a state bit, trigger on any change */
+ if (changed & BIT(12)) {
+ set_bit(LG_G15_GAMEMODE, &g15->brightness_changed);
+ schedule_work(&g15->work);
+ }
+
+ g15->pressed_keys = pressed_keys;
+
+ return 0;
+}
+
static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *data, int size)
{
@@ -924,6 +1244,10 @@ static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
if (data[0] == LG_G510_INPUT_KBD_BACKLIGHT && size == 2)
return lg_g510_leds_event(g15, data);
break;
+ case LG_G710:
+ if (data[0] == 0x03 && size == 4)
+ return lg_g710_event(g15, data, size);
+ break;
}
return 0;
@@ -1055,6 +1379,37 @@ static int lg_g15_register_led(struct lg_g15_data *g15, int i, const char *name)
ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
}
break;
+ case LG_G710:
+ switch (i) {
+ case LG_G15_LCD_BRIGHTNESS:
+ /*
+ * The G710+ does not have a separate LCD brightness,
+ * but it does have a separate brightness for WASD keys.
+ * Do not use the ::kbd_backlight suffix here, UPower
+ * only supports one kbd_backlight LED per device.
+ */
+ g15->leds[i].cdev.name = "g15::kbd_zoned_backlight-wasd";
+ fallthrough;
+ case LG_G15_KBD_BRIGHTNESS:
+ g15->leds[i].cdev.brightness_set_blocking =
+ lg_g710_kbd_led_set;
+ g15->leds[i].cdev.brightness_get =
+ lg_g710_led_get;
+ g15->leds[i].cdev.max_brightness = 4;
+ g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
+ break;
+ default:
+ if (i != LG_G15_GAMEMODE)
+ g15->leds[i].cdev.brightness_set_blocking =
+ lg_g710_mkey_led_set;
+ g15->leds[i].cdev.brightness_get =
+ lg_g710_led_get;
+ g15->leds[i].cdev.max_brightness = 1;
+ if (i == LG_G15_GAMEMODE)
+ g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
+ }
+ ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
+ break;
}
return ret;
@@ -1079,13 +1434,16 @@ static void lg_g15_init_input_dev_core(struct hid_device *hdev, struct input_dev
static void lg_g15_init_input_dev(struct hid_device *hdev, struct input_dev *input,
const char *name)
{
+ struct lg_g15_data *g15 = hid_get_drvdata(hdev);
int i;
lg_g15_init_input_dev_core(hdev, input, name);
- /* Keys below the LCD, intended for controlling a menu on the LCD */
- for (i = 0; i < 5; i++)
- input_set_capability(input, EV_KEY, KEY_KBD_LCD_MENU1 + i);
+ if (g15->model != LG_G710) {
+ /* Keys below the LCD, intended for controlling a menu on the LCD */
+ for (i = 0; i < 5; i++)
+ input_set_capability(input, EV_KEY, KEY_KBD_LCD_MENU1 + i);
+ }
}
static void lg_g13_init_input_dev(struct hid_device *hdev,
@@ -1119,6 +1477,7 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
"g15::macro_preset2",
"g15::macro_preset3",
"g15::macro_record",
+ "g15::gamemode",
};
u8 gkeys_settings_output_report = 0;
u8 gkeys_settings_feature_report = 0;
@@ -1137,8 +1496,8 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
return ret;
/*
- * Some models have multiple interfaces, we want the interface with
- * the f000.0000 application input report.
+ * Some models have multiple interfaces, we want the interface
+ * with the ff00.0000 application input report.
*/
rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
list_for_each_entry(rep, &rep_enum->report_list, list) {
@@ -1212,6 +1571,13 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
case LG_Z10:
connect_mask = HID_CONNECT_HIDRAW;
break;
+ case LG_G710:
+ INIT_WORK(&g15->work, lg_g710_leds_changed_work);
+ hdev->quirks |= HID_QUIRK_NOGET;
+ connect_mask = HID_CONNECT_DEFAULT;
+ gkeys_settings_feature_report = LG_G710_FEATURE_EXTRA_KEYS;
+ gkeys = 6;
+ break;
}
ret = hid_hw_start(hdev, connect_mask);
@@ -1234,11 +1600,14 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
if (gkeys_settings_feature_report) {
+ int report_size = ((g15->model == LG_G710) ?
+ gkeys * 2 : gkeys) + 1;
+
g15->transfer_buf[0] = gkeys_settings_feature_report;
- memset(g15->transfer_buf + 1, 0, gkeys);
+ memset(g15->transfer_buf + 1, 0, report_size - 1);
ret = hid_hw_raw_request(g15->hdev,
gkeys_settings_feature_report,
- g15->transfer_buf, gkeys + 1,
+ g15->transfer_buf, report_size,
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
}
@@ -1327,7 +1696,7 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
goto error_hw_stop;
/* Register LED devices */
- for (i = 0; i < LG_G15_LED_MAX; i++) {
+ for (i = 0; i < LG_G15_LED_MAX - (g15->model != LG_G710); i++) {
ret = lg_g15_register_led(g15, i, led_names[i]);
if (ret)
goto error_hw_stop;
@@ -1366,6 +1735,10 @@ static const struct hid_device_id lg_g15_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO),
.driver_data = LG_G510_USB_AUDIO },
+ /* G710 or G710+ */
+ { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
+ USB_DEVICE_ID_LOGITECH_G710),
+ .driver_data = LG_G710 },
/* Z-10 speakers */
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_Z_10_SPK),
--
2.53.0
^ permalink raw reply related
* [BUG] i2c_hid_acpi: Synaptics touchpad stalls after ~6 min idle — firmware sleep notification unhandled (Dell Latitude 7420)
From: Chris @ 2026-04-02 11:06 UTC (permalink / raw)
To: linux-input; +Cc: linux-i2c
Hi,
I'd like to report a reproducible touchpad stall on a Dell Latitude
7420 that I've traced
to what appears to be an unhandled firmware sleep notification in the
i2c_hid_acpi / hid-multitouch
stack.
== Hardware ==
Machine: Dell Latitude 7420
Touchpad: DELL0A36:00 06CB:CE65 (Synaptics)
Bus: i2c-1 (i2c_designware.1, PCI 00:15.1 — Intel Tiger Lake I2C #1)
Driver: i2c_hid_acpi + hid-multitouch
Kernel: 6.17.0-19-generic (Ubuntu 24.04, Wayland/GNOME)
== Symptom ==
After approximately 6 minutes of complete idle (no touch or keyboard
input), the touchpad
stops responding entirely. The driver remains bound, all three power
layers stay active/on,
and no kernel errors are logged. IRQ 162 (intel-gpio 327, DELL0A36:00)
stops firing.
Rebinding i2c_hid_acpi recovers the device immediately.
== Reproduction ==
1. Use the touchpad normally.
2. Leave the system completely idle (no keyboard, no touch) for ~6–8 minutes.
3. Touchpad is unresponsive. IRQ 162 count frozen.
4. Recovery: echo i2c-DELL0A36:00 >
/sys/bus/i2c/drivers/i2c_hid_acpi/unbind && \
echo i2c-DELL0A36:00 > /sys/bus/i2c/drivers/i2c_hid_acpi/bind
== Trace evidence ==
I captured ftrace i2c events (filtered to adapter 1) across multiple
stall events.
The pattern is consistent across all captures:
1. Normal multitouch packets at ~140 Hz while in use:
i2c_reply: i2c-1 a=02c l=64 [20-00-03-03-<X_lo>-<X_hi>-<Y_lo>-<Y_hi>-...]
2. After finger lifted, ~30 seconds of silence, then two unusual
packets fire in
quick succession:
i2c_reply: i2c-1 a=02c l=64 [40-00-0c-06-00-00-00-00-00-00-...]
i2c_reply: i2c-1 a=02c l=64 [0f-00-00-00-00-00-00-00-00-00-...]
The first is a 64-byte report (length field 0x0040) with what appears to be
report ID 0x0c and sub-type 0x06. The second is a null/empty packet
(length field 0x000f, all-zero payload), consistent with an
end-of-transfer signal.
3. No i2c_write from the driver follows these packets.
4. ~140–380 seconds later, the same two-packet sequence fires again.
5. The IRQ then goes permanently silent — touchpad stalled.
A representative excerpt from a stall trace (timestamps are kernel
seconds since boot):
2565.349 i2c_reply [20-00-03-01-64-02-d1-01-...] ← last touch
packet (finger lift)
← 30s gap
2595.511 i2c_reply [40-00-0c-06-00-00-00-00-...] ← unknown report
(sleep notify?)
2595.522 i2c_reply [0f-00-00-00-00-00-00-00-...] ← null packet
← 378s gap
2973.423 i2c_reply [40-00-0c-06-00-00-00-00-...] ← same again
2973.435 i2c_reply [0f-00-00-00-00-00-00-00-...]
← stall; IRQ 162 frozen
The same 0x0c report appears ~6 minutes after each rebind in
subsequent captures,
with the stall following within 1–3 minutes thereafter.
== Hypothesis ==
The Synaptics firmware appears to send a proprietary power/sleep-state
notification
(report ID 0x0c) after a period of inactivity. The i2c_hid_acpi driver
does not respond
to this report — no i2c_write is observed following it. The firmware
may be waiting for
an acknowledgement or handshake that never arrives, and locks up as a result.
This is consistent with:
- The stall being time-based (not load or temperature dependent)
- Rebinding fully recovering the device (firmware reset via HID RESET command)
- The IRQ going silent with no kernel errors (driver still considers
device healthy)
- libinput occasionally logging "Touch jump detected" immediately
prior to a stall
(possibly the firmware sending corrupt data as it enters the sleep state)
== Power management note ==
The device hierarchy (PCI 00:15.1 → i2c_designware.1 →
i2c-DELL0A36:00) must all be
pinned to runtime_status=active to avoid a separate (unrelated) class
of failure where
the I2C bus autosuspends. That issue is already worked around via udev
rules. The stall
described here occurs independently, with all three layers confirmed
active throughout.
== Workaround ==
Rebind the driver after ~2 minutes of idle via a watchdog script
monitoring IRQ 162.
This preempts the firmware before it sends the 0x0c report.
== Question for maintainers ==
Is report ID 0x0c a known Synaptics vendor report that the driver
should be handling?
Should i2c_hid be sending a SET_POWER or RESET in response to an
unsolicited report of
this type? Any guidance on what the correct driver-side response
should be would be
appreciated — I'm happy to test patches.
Full ftrace dumps available on request.
Thanks,
Chris
^ permalink raw reply
* [PATCH] HID: sony: update module description
From: Rosalie Wanders @ 2026-04-02 15:59 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Rosalie Wanders, linux-input, linux-kernel
This commit updates the hid-sony module description to make it correct
with the recent hid-sony changes alongside making it more consistent.
Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
drivers/hid/Kconfig | 12 +++++++-----
drivers/hid/hid-sony.c | 6 +++---
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..b287023437ca 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1100,13 +1100,15 @@ config HID_SONY
help
Support for
- * Sony PS3 6-axis controllers
+ * Sixaxis controllers for PS3
* Buzz controllers
- * Sony PS3 Blue-ray Disk Remote Control (Bluetooth)
- * Logitech Harmony adapter for Sony Playstation 3 (Bluetooth)
- * Guitar Hero Live PS3, Wii U and PS4 guitar dongles
- * Guitar Hero PS3 and PC guitar dongles
+ * Blu-ray Disc Remote Control for PS3
+ * Logitech Harmony adapter for PS3
+ * Guitar Hero Live PS3, Wii U and PS4 guitars
+ * Guitar Hero PS3 and PC guitars
+ * Rock Band 1, 2 and 3 PS3 and Wii instruments
* Rock Band 4 PS4 and PS5 guitars
+ * DJ Hero Turntable for PS3
config SONY_FF
bool "Sony PS2/3/4 accessories force feedback support"
diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index 4b0992cfc8a7..bd4b4a470869 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 / PS5 BD devices.
+ * HID driver for Sony / PS2 / PS3 BD / PS4 / PS5 devices.
*
* Copyright (c) 1999 Andreas Gal
* Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
@@ -68,7 +68,7 @@
#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 PS3_DJH_TURNTABLE BIT(22)
#define SIXAXIS_CONTROLLER (SIXAXIS_CONTROLLER_USB | SIXAXIS_CONTROLLER_BT)
#define MOTION_CONTROLLER (MOTION_CONTROLLER_USB | MOTION_CONTROLLER_BT)
@@ -2616,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 / PS5 BD devices");
+MODULE_DESCRIPTION("HID driver for Sony / PS2 / PS3 BD / PS4 / PS5 devices");
MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification
From: Dave Carey @ 2026-04-02 18:29 UTC (permalink / raw)
To: jikos; +Cc: bentiss, linux-input, linux-kernel, Dave Carey
The Lenovo Yoga Book 9 14IAH10 (83KJ) uses a composite USB HID device
(17EF:6161) where three descriptor quirks combine to cause hid-multitouch
to incorrectly set INPUT_PROP_BUTTONPAD on both touchscreen nodes, making
libinput treat them as indirect clickpads rather than direct touchscreens.
Quirk 1: The HID_DG_TOUCHSCREEN application collection contains
HID_UP_BUTTON usages (stylus barrel buttons). The generic heuristic in
mt_touch_input_mapping() treats any touchscreen-with-buttons as a
touchpad, setting INPUT_MT_POINTER.
Quirk 2: A HID_DG_TOUCHPAD collection ("Emulated Touchpad") sets
INPUT_MT_POINTER unconditionally in mt_allocate_application().
Quirk 3: The HID_DG_BUTTONTYPE feature report (0x51) returns
MT_BUTTONTYPE_CLICKPAD, directly setting td->is_buttonpad = true.
These combine to produce INPUT_PROP_BUTTONPAD on the touchscreen input
nodes. libinput treats the devices as indirect clickpads and suppresses
direct touch events, leaving the touchscreens non-functional under
KDE/Wayland.
Additionally, the firmware resets if any USB control request is received
during the CDC ACM initialization window. The existing GET_REPORT call
in mt_check_input_mode() during probe triggers this reset.
Fix by extending MT_QUIRK_YOGABOOK9I (already defined for the earlier
Yoga Book 9i) to guard all three BUTTONPAD heuristics and skip the
HID_DG_BUTTONTYPE GET_REPORT during probe for this device.
Signed-off-by: Dave Carey <carvsdriver@gmail.com>
Tested-by: Dave Carey <carvsdriver@gmail.com>
---
drivers/hid/hid-multitouch.c | 34 +++++++++++++++++++++++++++-------
1 file changed, 27 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index e82a3c4e5..1bef32b1d 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -549,7 +549,14 @@ static void mt_feature_mapping(struct hid_device *hdev,
switch (usage->hid) {
case HID_DG_CONTACTMAX:
- mt_get_feature(hdev, field->report);
+ /*
+ * Yoga Book 9: skip GET_REPORT during probe; the firmware
+ * resets if it receives any control request before the init
+ * Output report is sent (within ~1.18s of USB enumeration).
+ * Logical maximum from the descriptor is used as the fallback.
+ */
+ if (!(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I))
+ mt_get_feature(hdev, field->report);
td->maxcontacts = field->value[0];
if (!td->maxcontacts &&
@@ -566,6 +573,10 @@ static void mt_feature_mapping(struct hid_device *hdev,
break;
}
+ /* Yoga Book 9 reports Clickpad but is a direct touchscreen */
+ if (td->mtclass.quirks & MT_QUIRK_YOGABOOK9I)
+ break;
+
mt_get_feature(hdev, field->report);
switch (field->value[usage->usage_index]) {
case MT_BUTTONTYPE_CLICKPAD:
@@ -579,7 +590,9 @@ static void mt_feature_mapping(struct hid_device *hdev,
break;
case 0xff0000c5:
/* Retrieve the Win8 blob once to enable some devices */
- if (usage->usage_index == 0)
+ /* Yoga Book 9: skip; firmware resets before init if queried */
+ if (usage->usage_index == 0 &&
+ !(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I))
mt_get_feature(hdev, field->report);
break;
}
@@ -644,8 +657,11 @@ static struct mt_application *mt_allocate_application(struct mt_device *td,
/*
* Model touchscreens providing buttons as touchpads.
+ * Yoga Book 9 has an emulated touchpad but its touch surfaces
+ * are direct screens, not indirect pointers.
*/
- if (application == HID_DG_TOUCHPAD) {
+ if (application == HID_DG_TOUCHPAD &&
+ !(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I)) {
mt_application->mt_flags |= INPUT_MT_POINTER;
td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
}
@@ -802,11 +818,15 @@ static int mt_touch_input_mapping(struct hid_device *hdev, struct hid_input *hi,
/*
* Model touchscreens providing buttons as touchpads.
+ * Skip for Yoga Book 9 which has stylus buttons inside
+ * touchscreen collections, not physical touchpad buttons.
*/
if (field->application == HID_DG_TOUCHSCREEN &&
(usage->hid & HID_USAGE_PAGE) == HID_UP_BUTTON) {
- app->mt_flags |= INPUT_MT_POINTER;
- td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
+ if (!(app->quirks & MT_QUIRK_YOGABOOK9I)) {
+ app->mt_flags |= INPUT_MT_POINTER;
+ td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
+ }
}
/* count the buttons on touchpads */
@@ -1420,7 +1440,6 @@ static int mt_touch_input_configured(struct hid_device *hdev,
*/
if (cls->quirks & MT_QUIRK_APPLE_TOUCHBAR)
app->mt_flags |= INPUT_MT_DIRECT;
-
if (cls->is_indirect)
app->mt_flags |= INPUT_MT_POINTER;
@@ -1432,7 +1451,8 @@ static int mt_touch_input_configured(struct hid_device *hdev,
/* check for clickpads */
if ((app->mt_flags & INPUT_MT_POINTER) &&
- (app->buttons_count == 1))
+ (app->buttons_count == 1) &&
+ !(app->quirks & MT_QUIRK_YOGABOOK9I))
td->is_buttonpad = true;
if (td->is_buttonpad)
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v2 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Silvan Jegen @ 2026-04-02 19:09 UTC (permalink / raw)
To: Vicki Pfau; +Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <20260318030850.289712-2-vi@endrift.com>
Hi
Thanks for the patch!
Just some comments and questions inline below.
Vicki Pfau <vi@endrift.com> wrote:
> This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
> unusual split-interface design such that input and rumble occur on the main
> HID interface, but all other communication occurs over a "configuration"
> interface. This is the case on both USB and Bluetooth, so this new driver
> uses a split-driver design with the HID interface being the "main" driver
> and the configuration interface is a secondary driver that looks up to the
> HID interface, sharing resources on a common struct.
>
> Due to using a non-standard pairing interface as well as Bluetooth
> communications being extremely limited in the kernel, a custom interface
> between userspace and the kernel will need to be design, along with bringup
> in BlueZ. That is beyond the scope of this initial patch, which only
> contains the generic HID and USB configuration interface drivers.
>
> This initial work supports general input for the Joy-Con 2, Pro Controller
> 2, and GameCube NSO controllers. IMU, rumble and battery support is not yet
> present.
>
> Signed-off-by: Vicki Pfau <vi@endrift.com>
> ---
> MAINTAINERS | 1 +
> drivers/hid/Kconfig | 11 +-
> drivers/hid/hid-ids.h | 4 +
> drivers/hid/hid-nintendo.c | 1194 ++++++++++++++++-
> drivers/hid/hid-nintendo.h | 72 +
> drivers/input/joystick/Kconfig | 11 +
> drivers/input/joystick/Makefile | 1 +
> drivers/input/joystick/nintendo-switch2-usb.c | 353 +++++
> 8 files changed, 1637 insertions(+), 10 deletions(-)
> create mode 100644 drivers/hid/hid-nintendo.h
> create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 7b277d5bf3d12..4d1a28df5fd24 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -18743,6 +18743,7 @@ F: drivers/scsi/nsp32*
>
> NINTENDO HID DRIVER
> M: Daniel J. Ogorchock <djogorchock@gmail.com>
> +M: Vicki Pfau <vi@endrift.com>
> L: linux-input@vger.kernel.org
> S: Maintained
> F: drivers/hid/hid-nintendo*
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index c1d9f7c6a5f23..1a293a6c02c26 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -826,10 +826,13 @@ config HID_NINTENDO
> depends on LEDS_CLASS
> select POWER_SUPPLY
> help
> - Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller.
> - All controllers support bluetooth, and the Pro Controller also supports
> - its USB mode. This also includes support for the Nintendo Switch Online
> - Controllers which include the NES, Genesis, SNES, and N64 controllers.
> + Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller, as
> + well as Nintendo Switch 2 Joy-Cons, Pro Controller, and NSO GameCube
> + controllers. All Switch controllers support bluetooth, and the Pro
> + Controller also supports its USB mode. This also includes support for
> + the Nintendo Switch Online Controllers which include the NES, Genesis,
> + SNES, and N64 controllers. Switch 2 controllers currently only support
> + USB mode.
>
> To compile this driver as a module, choose M here: the
> module will be called hid-nintendo.
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index 4ab7640b119ac..a794dad7980f3 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -1073,6 +1073,10 @@
> #define USB_DEVICE_ID_NINTENDO_SNESCON 0x2017
> #define USB_DEVICE_ID_NINTENDO_GENCON 0x201e
> #define USB_DEVICE_ID_NINTENDO_N64CON 0x2019
> +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR 0x2066
> +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL 0x2067
> +#define USB_DEVICE_ID_NINTENDO_NS2_PROCON 0x2069
> +#define USB_DEVICE_ID_NINTENDO_NS2_GCCON 0x2073
>
> #define USB_VENDOR_ID_NOVATEK 0x0603
> #define USB_DEVICE_ID_NOVATEK_PCT 0x0600
> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index 29008c2cc5304..4ab8d4e7558a1 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -1,11 +1,13 @@
> // SPDX-License-Identifier: GPL-2.0+
> /*
> - * HID driver for Nintendo Switch Joy-Cons and Pro Controllers
> + * HID driver for Nintendo Switch Joy-Cons and Pro Controllers, as well as
> + * Nintendo Switch 2 Joy-Cons, Pro Controller, and GameCube Controller
> *
> * Copyright (c) 2019-2021 Daniel J. Ogorchock <djogorchock@gmail.com>
> * Portions Copyright (c) 2020 Nadia Holmquist Pedersen <nadia@nhp.sh>
> * Copyright (c) 2022 Emily Strickland <linux@emily.st>
> * Copyright (c) 2023 Ryan McClelland <rymcclel@gmail.com>
> + * Copyright (c) 2026 Valve Software
> *
> * The following resources/projects were referenced for this driver:
> * https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
> @@ -13,6 +15,8 @@
> * https://github.com/FrotBot/SwitchProConLinuxUSB
> * https://github.com/MTCKC/ProconXInput
> * https://github.com/Davidobot/BetterJoyForCemu
> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
> + * https://github.com/ndeadly/switch2_controller_research
> * hid-wiimote kernel hid driver
> * hid-logitech-hidpp driver
> * hid-sony driver
> @@ -29,6 +33,7 @@
> */
>
> #include "hid-ids.h"
> +#include "hid-nintendo.h"
> #include <linux/unaligned.h>
> #include <linux/delay.h>
> #include <linux/device.h>
> @@ -41,6 +46,8 @@
> #include <linux/module.h>
> #include <linux/power_supply.h>
> #include <linux/spinlock.h>
> +#include <linux/usb.h>
> +#include "usbhid/usbhid.h"
>
> /*
> * Reference the url below for the following HID report defines:
> @@ -2614,7 +2621,7 @@ static int joycon_ctlr_handle_event(struct joycon_ctlr *ctlr, u8 *data,
> return ret;
> }
>
> -static int nintendo_hid_event(struct hid_device *hdev,
> +static int joycon_event(struct hid_device *hdev,
> struct hid_report *report, u8 *raw_data, int size)
> {
> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
> @@ -2625,7 +2632,7 @@ static int nintendo_hid_event(struct hid_device *hdev,
> return joycon_ctlr_handle_event(ctlr, raw_data, size);
> }
>
> -static int nintendo_hid_probe(struct hid_device *hdev,
> +static int joycon_probe(struct hid_device *hdev,
> const struct hid_device_id *id)
> {
> int ret;
> @@ -2729,7 +2736,7 @@ static int nintendo_hid_probe(struct hid_device *hdev,
> return ret;
> }
>
> -static void nintendo_hid_remove(struct hid_device *hdev)
> +static void joycon_remove(struct hid_device *hdev)
> {
> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
> unsigned long flags;
> @@ -2748,7 +2755,9 @@ static void nintendo_hid_remove(struct hid_device *hdev)
> hid_hw_stop(hdev);
> }
>
> -static int nintendo_hid_resume(struct hid_device *hdev)
> +#ifdef CONFIG_PM
> +
> +static int joycon_resume(struct hid_device *hdev)
> {
> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
> int ret;
> @@ -2771,7 +2780,7 @@ static int nintendo_hid_resume(struct hid_device *hdev)
> return ret;
> }
>
> -static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
> +static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
> {
> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
>
> @@ -2790,7 +2799,1120 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
> return 0;
> }
>
> +#endif
> +
> +/*
> + * =============================================================================
> + * Switch 2 support
> + * =============================================================================
> + */
> +#define NS2_BTNR_B BIT(0)
> +#define NS2_BTNR_A BIT(1)
> +#define NS2_BTNR_Y BIT(2)
> +#define NS2_BTNR_X BIT(3)
> +#define NS2_BTNR_R BIT(4)
> +#define NS2_BTNR_ZR BIT(5)
> +#define NS2_BTNR_PLUS BIT(6)
> +#define NS2_BTNR_RS BIT(7)
> +
> +#define NS2_BTNL_DOWN BIT(0)
> +#define NS2_BTNL_RIGHT BIT(1)
> +#define NS2_BTNL_LEFT BIT(2)
> +#define NS2_BTNL_UP BIT(3)
> +#define NS2_BTNL_L BIT(4)
> +#define NS2_BTNL_ZL BIT(5)
> +#define NS2_BTNL_MINUS BIT(6)
> +#define NS2_BTNL_LS BIT(7)
> +
> +#define NS2_BTN3_C BIT(4)
> +#define NS2_BTN3_SR BIT(6)
> +#define NS2_BTN3_SL BIT(7)
> +
> +#define NS2_BTN_JCR_HOME BIT(0)
> +#define NS2_BTN_JCR_GR BIT(2)
> +#define NS2_BTN_JCR_C NS2_BTN3_C
> +#define NS2_BTN_JCR_SR NS2_BTN3_SR
> +#define NS2_BTN_JCR_SL NS2_BTN3_SL
> +
> +#define NS2_BTN_JCL_CAPTURE BIT(0)
> +#define NS2_BTN_JCL_GL BIT(2)
> +#define NS2_BTN_JCL_SR NS2_BTN3_SR
> +#define NS2_BTN_JCL_SL NS2_BTN3_SL
> +
> +#define NS2_BTN_PRO_HOME BIT(0)
> +#define NS2_BTN_PRO_CAPTURE BIT(1)
> +#define NS2_BTN_PRO_GR BIT(2)
> +#define NS2_BTN_PRO_GL BIT(3)
> +#define NS2_BTN_PRO_C NS2_BTN3_C
> +
> +#define NS2_BTN_GC_HOME BIT(0)
> +#define NS2_BTN_GC_CAPTURE BIT(1)
> +#define NS2_BTN_GC_C NS2_BTN3_C
> +
> +#define NS2_TRIGGER_RANGE 4095
> +#define NS2_AXIS_MIN -32768
> +#define NS2_AXIS_MAX 32767
> +
> +#define NS2_MAX_PLAYER_ID 8
> +
> +#define NS2_MAX_INIT_RETRIES 4
> +
> +#define NS2_FLASH_ADDR_SERIAL 0x13002
> +#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB 0x130a8
> +#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB 0x130e8
> +#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB 0x13140
> +#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB 0x1fc040
> +#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB 0x1fc080
> +
> +#define NS2_FLASH_SIZE_SERIAL 0x10
> +#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9
> +#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2
> +#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11
> +
> +#define NS2_USER_CALIB_MAGIC 0xa1b2
> +
> +#define NS2_FEATURE_BUTTONS BIT(0)
> +#define NS2_FEATURE_ANALOG BIT(1)
> +#define NS2_FEATURE_IMU BIT(2)
> +#define NS2_FEATURE_MOUSE BIT(4)
> +#define NS2_FEATURE_RUMBLE BIT(5)
> +#define NS2_FEATURE_MAGNETO BIT(7)
> +
> +enum switch2_subcmd_flash {
> + NS2_SUBCMD_FLASH_READ_BLOCK = 0x01,
> + NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02,
> + NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03,
> + NS2_SUBCMD_FLASH_READ = 0x04,
> + NS2_SUBCMD_FLASH_WRITE = 0x05,
> +};
> +
> +enum switch2_subcmd_init {
> + NS2_SUBCMD_INIT_SELECT_REPORT = 0xa,
> + NS2_SUBCMD_INIT_USB = 0xd,
> +};
> +
> +enum switch2_subcmd_feature_select {
> + NS2_SUBCMD_FEATSEL_GET_INFO = 0x1,
> + NS2_SUBCMD_FEATSEL_SET_MASK = 0x2,
> + NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3,
> + NS2_SUBCMD_FEATSEL_ENABLE = 0x4,
> + NS2_SUBCMD_FEATSEL_DISABLE = 0x5,
> +};
> +
> +enum switch2_subcmd_grip {
> + NS2_SUBCMD_GRIP_GET_INFO = 0x1,
> + NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2,
> + NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3,
> +};
> +
> +enum switch2_subcmd_led {
> + NS2_SUBCMD_LED_P1 = 0x1,
> + NS2_SUBCMD_LED_P2 = 0x2,
> + NS2_SUBCMD_LED_P3 = 0x3,
> + NS2_SUBCMD_LED_P4 = 0x4,
> + NS2_SUBCMD_LED_ALL_ON = 0x5,
> + NS2_SUBCMD_LED_ALL_OFF = 0x6,
> + NS2_SUBCMD_LED_PATTERN = 0x7,
> + NS2_SUBCMD_LED_BLINK = 0x8,
> +};
> +
> +enum switch2_subcmd_fw_info {
> + NS2_SUBCMD_FW_INFO_GET = 0x1,
> +};
> +
> +enum switch2_ctlr_type {
> + NS2_CTLR_TYPE_JCL = 0x00,
> + NS2_CTLR_TYPE_JCR = 0x01,
> + NS2_CTLR_TYPE_PRO = 0x02,
> + NS2_CTLR_TYPE_GC = 0x03,
> +};
> +
> +enum switch2_report_id {
> + NS2_REPORT_UNIFIED = 0x05,
> + NS2_REPORT_JCL = 0x07,
> + NS2_REPORT_JCR = 0x08,
> + NS2_REPORT_PRO = 0x09,
> + NS2_REPORT_GC = 0x0a,
> +};
> +
> +enum switch2_init_step {
> + NS2_INIT_READ_SERIAL,
> + NS2_INIT_GET_FIRMWARE_INFO,
> + NS2_INIT_READ_FACTORY_PRIMARY_CALIB,
> + NS2_INIT_READ_FACTORY_SECONDARY_CALIB,
> + NS2_INIT_READ_FACTORY_TRIGGER_CALIB,
> + NS2_INIT_READ_USER_PRIMARY_CALIB,
> + NS2_INIT_READ_USER_SECONDARY_CALIB,
> + NS2_INIT_SET_FEATURE_MASK,
> + NS2_INIT_ENABLE_FEATURES,
> + NS2_INIT_GRIP_BUTTONS,
> + NS2_INIT_REPORT_FORMAT,
> + NS2_INIT_SET_PLAYER_LEDS,
> + NS2_INIT_INPUT,
> + NS2_INIT_FINISH,
> + NS2_INIT_DONE,
> +};
> +
> +struct switch2_version_info {
> + uint8_t major;
> + uint8_t minor;
> + uint8_t patch;
> + uint8_t ctlr_type;
> + __le32 unk;
> + int8_t dsp_major;
> + int8_t dsp_minor;
> + int8_t dsp_patch;
> + int8_t dsp_type;
> +};
> +
> +struct switch2_axis_calibration {
> + uint16_t neutral;
> + uint16_t negative;
> + uint16_t positive;
> +};
> +
> +struct switch2_stick_calibration {
> + struct switch2_axis_calibration x;
> + struct switch2_axis_calibration y;
> +};
> +
> +struct switch2_controller {
> + struct hid_device *hdev;
> + struct switch2_cfg_intf *cfg;
> +
> + char name[64];
> + char phys[64];
> + struct list_head entry;
> + struct mutex lock;
> +
> + enum switch2_ctlr_type ctlr_type;
> + enum switch2_init_step init_step;
> + struct input_dev __rcu *input;
> + char serial[NS2_FLASH_SIZE_SERIAL + 1];
> + struct switch2_version_info version;
> +
> + struct switch2_stick_calibration stick_calib[2];
> + uint8_t lt_zero;
> + uint8_t rt_zero;
> +
> + uint32_t player_id;
> + struct led_classdev leds[4];
> +};
> +
> +static DEFINE_MUTEX(switch2_controllers_lock);
> +static LIST_HEAD(switch2_controllers);
> +
> +struct switch2_ctlr_button_mapping {
> + uint32_t code;
> + int byte;
> + uint32_t bit;
> +};
> +
> +static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[] = {
> + { BTN_DPAD_LEFT, 0, NS2_BTNL_LEFT, },
> + { BTN_DPAD_UP, 0, NS2_BTNL_UP, },
> + { BTN_DPAD_DOWN, 0, NS2_BTNL_DOWN, },
> + { BTN_DPAD_RIGHT, 0, NS2_BTNL_RIGHT, },
> + { BTN_TL, 0, NS2_BTNL_L, },
> + { BTN_TL2, 0, NS2_BTNL_ZL, },
> + { BTN_SELECT, 0, NS2_BTNL_MINUS, },
> + { BTN_THUMBL, 0, NS2_BTNL_LS, },
> + { KEY_RECORD, 1, NS2_BTN_JCL_CAPTURE, },
> + { BTN_GRIPR, 1, NS2_BTN_JCL_SL, },
> + { BTN_GRIPR2, 1, NS2_BTN_JCL_SR, },
> + { BTN_GRIPL, 1, NS2_BTN_JCL_GL, },
> + { /* sentinel */ },
> +};
> +
> +static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTNR_A, },
> + { BTN_EAST, 0, NS2_BTNR_B, },
> + { BTN_NORTH, 0, NS2_BTNR_X, },
> + { BTN_WEST, 0, NS2_BTNR_Y, },
> + { BTN_TR, 0, NS2_BTNR_R, },
> + { BTN_TR2, 0, NS2_BTNR_ZR, },
> + { BTN_START, 0, NS2_BTNR_PLUS, },
> + { BTN_THUMBR, 0, NS2_BTNR_RS, },
> + { BTN_C, 1, NS2_BTN_JCR_C, },
> + { BTN_MODE, 1, NS2_BTN_JCR_HOME, },
> + { BTN_GRIPL2, 1, NS2_BTN_JCR_SL, },
> + { BTN_GRIPL, 1, NS2_BTN_JCR_SR, },
> + { BTN_GRIPR, 1, NS2_BTN_JCR_GR, },
> + { /* sentinel */ },
> +};
> +
> +static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTNR_A, },
> + { BTN_EAST, 0, NS2_BTNR_B, },
> + { BTN_NORTH, 0, NS2_BTNR_X, },
> + { BTN_WEST, 0, NS2_BTNR_Y, },
> + { BTN_TL, 1, NS2_BTNL_L, },
> + { BTN_TR, 0, NS2_BTNR_R, },
> + { BTN_TL2, 1, NS2_BTNL_ZL, },
> + { BTN_TR2, 0, NS2_BTNR_ZR, },
> + { BTN_SELECT, 1, NS2_BTNL_MINUS, },
> + { BTN_START, 0, NS2_BTNR_PLUS, },
> + { BTN_THUMBL, 1, NS2_BTNL_LS, },
> + { BTN_THUMBR, 0, NS2_BTNR_RS, },
> + { BTN_MODE, 2, NS2_BTN_PRO_HOME },
> + { KEY_RECORD, 2, NS2_BTN_PRO_CAPTURE },
> + { BTN_GRIPR, 2, NS2_BTN_PRO_GR },
> + { BTN_GRIPL, 2, NS2_BTN_PRO_GL },
> + { BTN_C, 2, NS2_BTN_PRO_C },
> + { /* sentinel */ },
> +};
> +
> +static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTNR_A, },
> + { BTN_EAST, 0, NS2_BTNR_B, },
> + { BTN_NORTH, 0, NS2_BTNR_X, },
> + { BTN_WEST, 0, NS2_BTNR_Y, },
> + { BTN_TL2, 1, NS2_BTNL_L, },
> + { BTN_TR2, 0, NS2_BTNR_R, },
> + { BTN_TL, 1, NS2_BTNL_ZL, },
> + { BTN_TR, 0, NS2_BTNR_ZR, },
> + { BTN_SELECT, 1, NS2_BTNL_MINUS, },
> + { BTN_START, 0, NS2_BTNR_PLUS, },
> + { BTN_MODE, 2, NS2_BTN_GC_HOME },
> + { KEY_RECORD, 2, NS2_BTN_GC_CAPTURE },
> + { BTN_C, 2, NS2_BTN_GC_C },
> + { /* sentinel */ },
> +};
> +
> +static const uint8_t switch2_init_cmd_data[] = {
> + /*
> + * The last 6 bytes of this packet are the MAC address of
> + * the console, but we don't need that for USB
> + */
> + 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
> +};
> +
> +static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
> +
> +static const uint8_t switch2_feature_mask[] = {
> + NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU,
> + 0x00, 0x00, 0x00
> +};
> +
> +static void switch2_init_step_done(struct switch2_controller *ns2, enum switch2_init_step step)
> +{
> + if (ns2->init_step != step)
> + return;
> +
> + ns2->init_step++;
> +}
> +
> +static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
> +{
> + return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
> +}
> +
> +static int switch2_set_leds(struct switch2_controller *ns2)
> +{
> + int i;
> + uint8_t message[8] = { 0 };
> +
> + for (i = 0; i < JC_NUM_LEDS; i++)
> + message[0] |= (!!ns2->leds[i].brightness) << i;
> +
> + if (!ns2->cfg)
> + return -ENOTCONN;
> + return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN,
> + &message, sizeof(message),
> + ns2->cfg);
> +}
> +
> +static int switch2_player_led_brightness_set(struct led_classdev *led,
> + enum led_brightness brightness)
> +{
> + struct device *dev = led->dev->parent;
> + struct hid_device *hdev = to_hid_device(dev);
> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
> +
> + if (!ns2)
> + return -ENODEV;
> +
> + guard(mutex)(&ns2->lock);
> + return switch2_set_leds(ns2);
> +}
> +
> +static void switch2_leds_create(struct switch2_controller *ns2)
> +{
> + struct hid_device *hdev = ns2->hdev;
> + struct led_classdev *led;
> + int i;
> + int player_led_pattern;
> +
> + player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS;
> + hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1);
> +
> + for (i = 0; i < JC_NUM_LEDS; i++) {
> + led = &ns2->leds[i];
> + led->brightness = joycon_player_led_patterns[player_led_pattern][i];
> + led->max_brightness = 1;
> + led->brightness_set_blocking = switch2_player_led_brightness_set;
> + led->flags = LED_CORE_SUSPENDRESUME | LED_HW_PLUGGABLE;
> + }
> +}
> +
> +static void switch2_config_buttons(struct input_dev *idev,
> + const struct switch2_ctlr_button_mapping button_mappings[])
> +{
> + const struct switch2_ctlr_button_mapping *button;
> +
> + for (button = button_mappings; button->code; button++)
> + input_set_capability(idev, EV_KEY, button->code);
> +}
> +
> +static int switch2_init_input(struct switch2_controller *ns2)
> +{
> + struct input_dev *input;
> + struct hid_device *hdev = ns2->hdev;
> + int i;
> + int ret;
> +
> + switch2_init_step_done(ns2, NS2_INIT_FINISH);
> +
> + rcu_read_lock();
> + input = rcu_dereference(ns2->input);
> + rcu_read_unlock();
> +
> + if (input)
> + return 0;
> +
> + input = devm_input_allocate_device(&hdev->dev);
> + if (!input)
> + return -ENOMEM;
> +
> + input_set_drvdata(input, ns2);
> + input->dev.parent = &hdev->dev;
> + input->id.bustype = hdev->bus;
> + input->id.vendor = hdev->vendor;
> + input->id.product = hdev->product;
> + input->id.version = hdev->version;
> + input->uniq = ns2->serial;
> + input->name = ns2->name;
> + input->phys = hdev->phys;
> +
> + switch (ns2->ctlr_type) {
> + case NS2_CTLR_TYPE_JCL:
> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + switch2_config_buttons(input, ns2_left_joycon_button_mappings);
> + break;
> + case NS2_CTLR_TYPE_JCR:
> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + switch2_config_buttons(input, ns2_right_joycon_button_mappings);
> + break;
> + case NS2_CTLR_TYPE_GC:
> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128);
> + input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128);
> + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
> + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
> + switch2_config_buttons(input, ns2_gccon_mappings);
> + break;
> + case NS2_CTLR_TYPE_PRO:
> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
> + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
> + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
> + switch2_config_buttons(input, ns2_procon_mappings);
> + break;
> + default:
> + input_free_device(input);
> + return -EINVAL;
> + }
> +
> + hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major,
> + ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
> + if (ns2->version.dsp_type >= 0)
> + hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major,
> + ns2->version.dsp_minor, ns2->version.dsp_patch);
> +
> + ret = input_register_device(input);
> + if (ret < 0) {
> + hid_err(ns2->hdev, "Failed to register input; ret=%d\n", ret);
> + return ret;
> + }
> +
> + for (i = 0; i < JC_NUM_LEDS; i++) {
> + struct led_classdev *led = &ns2->leds[i];
> + char *name = devm_kasprintf(&input->dev, GFP_KERNEL, "%s:%s:%s",
> + dev_name(&input->dev),
> + "green",
> + joycon_player_led_names[i]);
> +
> + if (!name)
> + return -ENOMEM;
> +
> + led->name = name;
> + ret = devm_led_classdev_register(&input->dev, led);
> + if (ret < 0) {
> + dev_err(&input->dev, "Failed to register player %d LED; ret=%d\n",
> + i + 1, ret);
> + input_unregister_device(input);
> + return ret;
> + }
> + }
> +
> + rcu_assign_pointer(ns2->input, input);
> + synchronize_rcu();
> + return 0;
> +}
> +
> +static struct switch2_controller *switch2_get_controller(const char *phys)
> +{
> + struct switch2_controller *ns2;
> +
> + guard(mutex)(&switch2_controllers_lock);
> + list_for_each_entry(ns2, &switch2_controllers, entry) {
> + if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0)
> + return ns2;
> + }
> + ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
> + if (!ns2)
> + return ERR_PTR(-ENOMEM);
> +
> + mutex_init(&ns2->lock);
> + INIT_LIST_HEAD(&ns2->entry);
> + list_add(&ns2->entry, &switch2_controllers);
> + strscpy(ns2->phys, phys, sizeof(ns2->phys));
> + return ns2;
> +}
> +
> +static void switch2_controller_put(struct switch2_controller *ns2)
> +{
> + struct input_dev *input;
> + bool do_free;
> +
> + guard(mutex)(&switch2_controllers_lock);
> + mutex_lock(&ns2->lock);
> +
> + rcu_read_lock();
> + input = rcu_dereference(ns2->input);
> + rcu_read_unlock();
> +
> + rcu_assign_pointer(ns2->input, NULL);
> + synchronize_rcu();
> +
> + ns2->init_step = 0;
> + do_free = !ns2->hdev && !ns2->cfg;
> + mutex_unlock(&ns2->lock);
> +
> + if (input)
> + input_unregister_device(input);
> +
> + if (do_free) {
> + list_del_init(&ns2->entry);
> + mutex_destroy(&ns2->lock);
> + kfree(ns2);
> + }
> +}
> +
> +static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib,
> + const uint8_t *data)
> +{
> + static const uint8_t UNCALIBRATED[9] = {
> + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
> + };
> + if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0)
> + return false;
> +
> + calib->x.neutral = data[0];
> + calib->x.neutral |= (data[1] & 0x0F) << 8;
> +
> + calib->y.neutral = data[1] >> 4;
> + calib->y.neutral |= data[2] << 4;
> +
> + calib->x.positive = data[3];
> + calib->x.positive |= (data[4] & 0x0F) << 8;
> +
> + calib->y.positive = data[4] >> 4;
> + calib->y.positive |= data[5] << 4;
> +
> + calib->x.negative = data[6];
> + calib->x.negative |= (data[7] & 0x0F) << 8;
> +
> + calib->y.negative = data[7] >> 4;
> + calib->y.negative |= data[8] << 4;
> +
> + return true;
> +}
> +
> +static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size,
> + uint32_t address, const uint8_t *data)
> +{
> + bool ok;
> +
> + switch (address) {
> + case NS2_FLASH_ADDR_SERIAL:
> + if (size != NS2_FLASH_SIZE_SERIAL)
> + return;
> + memcpy(ns2->serial, data, size);
> + switch2_init_step_done(ns2, NS2_INIT_READ_SERIAL);
> + break;
> + case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB:
> + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
> + return;
> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_PRIMARY_CALIB);
> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data);
> + if (ok) {
> + hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
> + ns2->stick_calib[0].x.negative,
> + ns2->stick_calib[0].x.neutral,
> + ns2->stick_calib[0].x.positive);
> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
> + ns2->stick_calib[0].y.negative,
> + ns2->stick_calib[0].y.neutral,
> + ns2->stick_calib[0].y.positive);
> + } else {
> + hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n");
> + }
> + break;
> + case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB:
> + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
> + return;
> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB);
> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data);
> + if (ok) {
> + hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n");
> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
> + ns2->stick_calib[1].x.negative,
> + ns2->stick_calib[1].x.neutral,
> + ns2->stick_calib[1].x.positive);
> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
> + ns2->stick_calib[1].y.negative,
> + ns2->stick_calib[1].y.neutral,
> + ns2->stick_calib[1].y.positive);
> + } else {
> + hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n");
> + }
> + break;
> + case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB:
> + if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB)
> + return;
> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB);
> + if (data[0] != 0xFF && data[1] != 0xFF) {
> + ns2->lt_zero = data[0];
> + ns2->rt_zero = data[1];
> +
> + hid_dbg(ns2->hdev, "Got factory trigger calibration:\n");
> + hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero);
> + hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero);
> + } else {
> + hid_dbg(ns2->hdev, "Factory trigger calibration not present\n");
> + }
> + break;
> + case NS2_FLASH_ADDR_USER_PRIMARY_CALIB:
> + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
> + return;
> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_PRIMARY_CALIB);
> + if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
> + hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
> + break;
> + }
> +
> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
> + if (ok) {
> + hid_dbg(ns2->hdev, "Got user primary stick calibration:\n");
> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
> + ns2->stick_calib[0].x.negative,
> + ns2->stick_calib[0].x.neutral,
> + ns2->stick_calib[0].x.positive);
> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
> + ns2->stick_calib[0].y.negative,
> + ns2->stick_calib[0].y.neutral,
> + ns2->stick_calib[0].y.positive);
> + } else {
> + hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
> + }
> + break;
> + case NS2_FLASH_ADDR_USER_SECONDARY_CALIB:
> + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
> + return;
> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB);
> + if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
> + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
> + break;
> + }
> +
> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
> + if (ok) {
> + hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n");
> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
> + ns2->stick_calib[1].x.negative,
> + ns2->stick_calib[1].x.neutral,
> + ns2->stick_calib[1].x.positive);
> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
> + ns2->stick_calib[1].y.negative,
> + ns2->stick_calib[1].y.neutral,
> + ns2->stick_calib[1].y.positive);
> + } else {
> + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
> + }
> + break;
> + }
> +}
> +
> +static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes,
> + const struct switch2_ctlr_button_mapping button_mappings[])
> +{
> + const struct switch2_ctlr_button_mapping *button;
> +
> + for (button = button_mappings; button->code; button++)
> + input_report_key(input, button->code, bytes[button->byte] & button->bit);
> +}
> +
> +static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib,
> + int axis, bool invert, int value)
> +{
> + if (calib && calib->neutral && calib->negative && calib->positive) {
> + value -= calib->neutral;
> + value *= NS2_AXIS_MAX + 1;
> + if (value < 0)
> + value /= calib->negative;
> + else
> + value /= calib->positive;
> + } else {
> + value = (value - 2048) * 16;
> + }
> +
> + if (invert)
> + value = -value;
> + input_report_abs(input, axis,
> + clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX));
> +}
> +
> +static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib,
> + int x, bool invert_x, int y, bool invert_y, const uint8_t *data)
> +{
> + switch2_report_axis(input, &calib->x, x, invert_x, data[0] | ((data[1] & 0x0F) << 8));
> + switch2_report_axis(input, &calib->y, y, invert_y, (data[1] >> 4) | (data[2] << 4));
> +}
> +
> +static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data)
> +{
> + int value = (NS2_TRIGGER_RANGE + 1) * (data - zero) / (232 - zero);
> +
> + input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE));
> +}
> +
> +static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data,
> + int size)
> +{
> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
> + struct input_dev *input;
> +
> + if (report->type != HID_INPUT_REPORT)
> + return 0;
> +
> + if (size < 15)
> + return -EINVAL;
> +
> + guard(rcu)();
> + input = rcu_dereference(ns2->input);
> +
> + if (!input)
> + return 0;
> +
> + switch (report->id) {
> + case NS2_REPORT_UNIFIED:
> + /*
> + * TODO
> + * This won't be sent unless the report type gets changed via command
> + * 03-0A, but we should support it at some point regardless.
> + */
> + break;
> + case NS2_REPORT_JCL:
> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
> + ABS_Y, true, &raw_data[6]);
> + switch2_report_buttons(input, &raw_data[3], ns2_left_joycon_button_mappings);
> + break;
> + case NS2_REPORT_JCR:
> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
> + ABS_Y, true, &raw_data[6]);
> + switch2_report_buttons(input, &raw_data[3], ns2_right_joycon_button_mappings);
> + break;
> + case NS2_REPORT_GC:
> + input_report_abs(input, ABS_HAT0X,
> + !!(raw_data[4] & NS2_BTNL_RIGHT) -
> + !!(raw_data[4] & NS2_BTNL_LEFT));
> + input_report_abs(input, ABS_HAT0Y,
> + !!(raw_data[4] & NS2_BTNL_DOWN) -
> + !!(raw_data[4] & NS2_BTNL_UP));
> + switch2_report_buttons(input, &raw_data[3], ns2_gccon_mappings);
> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
> + ABS_Y, true, &raw_data[6]);
> + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
> + ABS_RY, true, &raw_data[9]);
> + switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]);
> + switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]);
> + break;
> + case NS2_REPORT_PRO:
> + input_report_abs(input, ABS_HAT0X,
> + !!(raw_data[4] & NS2_BTNL_RIGHT) -
> + !!(raw_data[4] & NS2_BTNL_LEFT));
> + input_report_abs(input, ABS_HAT0Y,
> + !!(raw_data[4] & NS2_BTNL_DOWN) -
> + !!(raw_data[4] & NS2_BTNL_UP));
> + switch2_report_buttons(input, &raw_data[3], ns2_procon_mappings);
> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
> + ABS_Y, true, &raw_data[6]);
> + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
> + ABS_RY, true, &raw_data[9]);
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + input_sync(input);
> + return 0;
> +}
> +
> +static int switch2_features_enable(struct switch2_controller *ns2, int features)
> +{
> + __le32 feature_bits = __cpu_to_le32(features);
> +
> + if (!ns2->cfg)
> + return -ENOTCONN;
> + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE,
> + &feature_bits, sizeof(feature_bits),
> + ns2->cfg);
> +}
> +
> +static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address,
> + uint8_t size)
> +{
> + uint8_t message[8] = { size, 0x7e };
> +
> + if (!ns2->cfg)
> + return -ENOTCONN;
> + *(__le32 *)&message[4] = __cpu_to_le32(address);
> + return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message,
> + sizeof(message), ns2->cfg);
> +}
> +
> +static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id)
> +{
> + int i;
> + int player_led_pattern = player_id % JC_NUM_LED_PATTERNS;
> +
> + for (i = 0; i < JC_NUM_LEDS; i++)
> + ns2->leds[i].brightness = joycon_player_led_patterns[player_led_pattern][i];
> +
> + return switch2_set_leds(ns2);
> +}
> +
> +static int switch2_set_report_format(struct switch2_controller *ns2, enum switch2_report_id fmt)
> +{
> + __le32 format_id = __cpu_to_le32(fmt);
> +
> + if (!ns2->cfg)
> + return -ENOTCONN;
> + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_SELECT_REPORT,
> + &format_id, sizeof(format_id),
> + ns2->cfg);
> +}
> +
> +static int switch2_init_controller(struct switch2_controller *ns2)
This is now a recursive call while in v1 it wasn't. I think I preferred
the non-recursive version as there was one place where init_step
state was changed while now I am not sure where it happens (and whether
there is a code path where we end up in an infinite recursion)
What is the advantage of the recursive version compared to the
non-recursive one?
> +{
> + if (ns2->init_step == NS2_INIT_DONE)
> + return 0;
> +
> + if (!ns2->cfg)
> + return -ENOTCONN;
> +
> + switch (ns2->init_step) {
> + case NS2_INIT_READ_SERIAL:
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL,
> + NS2_FLASH_SIZE_SERIAL);
> + case NS2_INIT_GET_FIRMWARE_INFO:
> + return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET,
> + NULL, 0, ns2->cfg);
> + case NS2_INIT_READ_FACTORY_PRIMARY_CALIB:
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB,
> + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
> + case NS2_INIT_READ_FACTORY_SECONDARY_CALIB:
> + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB);
> + return switch2_init_controller(ns2);
> + }
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB,
> + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
> + case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
> + if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB);
> + return switch2_init_controller(ns2);
> + }
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
> + NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);
> + case NS2_INIT_READ_USER_PRIMARY_CALIB:
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB,
> + NS2_FLASH_SIZE_USER_AXIS_CALIB);
> + case NS2_INIT_READ_USER_SECONDARY_CALIB:
> + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB);
> + return switch2_init_controller(ns2);
> + }
> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB,
> + NS2_FLASH_SIZE_USER_AXIS_CALIB);
> + case NS2_INIT_SET_FEATURE_MASK:
> + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
> + switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
> + case NS2_INIT_ENABLE_FEATURES:
> + return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG);
> + case NS2_INIT_GRIP_BUTTONS:
> + if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
> + switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
> + return switch2_init_controller(ns2);
> + }
> + return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS,
> + switch2_one_data, sizeof(switch2_one_data),
> + ns2->cfg);
> + case NS2_INIT_REPORT_FORMAT:
> + switch (ns2->ctlr_type) {
> + case NS2_CTLR_TYPE_JCL:
> + return switch2_set_report_format(ns2, NS2_REPORT_JCL);
> + case NS2_CTLR_TYPE_JCR:
> + return switch2_set_report_format(ns2, NS2_REPORT_JCR);
> + case NS2_CTLR_TYPE_PRO:
> + return switch2_set_report_format(ns2, NS2_REPORT_PRO);
> + case NS2_CTLR_TYPE_GC:
> + return switch2_set_report_format(ns2, NS2_REPORT_GC);
> + default:
> + switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT);
> + return switch2_init_controller(ns2);
> + }
> + case NS2_INIT_SET_PLAYER_LEDS:
> + return switch2_set_player_id(ns2, ns2->player_id);
> + case NS2_INIT_INPUT:
> + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
> + switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
> + case NS2_INIT_FINISH:
> + if (ns2->hdev)
If this is not set we skip the switch2_init_input call but don't error
out. Is this intentional (are we expecting ns2->hdev to be populated at
a later time and this step retried, perhaps)?
> + return switch2_init_input(ns2);
> + break;
> + default:
> + WARN_ON_ONCE(1);
> + break;
> + }
> + return 0;
> +}
> +
> +int switch2_receive_command(struct switch2_controller *ns2,
> + const uint8_t *message, size_t length)
> +{
> + const struct switch2_cmd_header *header;
> + int ret = 0;
> +
> + if (length < 8)
> + return -EINVAL;
> +
> + print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false);
> +
> + guard(mutex)(&ns2->lock);
> +
> + header = (const struct switch2_cmd_header *)message;
> + if (!(header->flags & NS2_FLAG_OK)) {
> + ret = -EIO;
> + goto exit;
> + }
> + message = &message[8];
> + switch (header->command) {
> + case NS2_CMD_FLASH:
> + if (header->subcommand == NS2_SUBCMD_FLASH_READ) {
> + uint8_t read_size;
> + uint32_t read_address;
> +
> + if (length < 16) {
> + ret = -EINVAL;
> + goto exit;
> + }
> + read_size = message[0];
> + read_address = __le32_to_cpu(*(__le32 *)&message[4]);
> + if (length < read_size + 16) {
> + ret = -EINVAL;
> + goto exit;
> + }
> + switch2_handle_flash_read(ns2, read_size, read_address, &message[8]);
> + }
> + break;
> + case NS2_CMD_INIT:
> + if (header->subcommand == NS2_SUBCMD_INIT_USB)
> + switch2_init_step_done(ns2, NS2_INIT_INPUT);
> + else if (header->subcommand == NS2_SUBCMD_INIT_SELECT_REPORT)
> + switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT);
> + break;
> + case NS2_CMD_GRIP:
> + if (header->subcommand == NS2_SUBCMD_GRIP_ENABLE_BUTTONS)
> + switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
> + break;
> + case NS2_CMD_LED:
> + if (header->subcommand == NS2_SUBCMD_LED_PATTERN)
> + switch2_init_step_done(ns2, NS2_INIT_SET_PLAYER_LEDS);
> + break;
> + case NS2_CMD_FEATSEL:
> + if (header->subcommand == NS2_SUBCMD_FEATSEL_SET_MASK)
> + switch2_init_step_done(ns2, NS2_INIT_SET_FEATURE_MASK);
> + else if (header->subcommand == NS2_SUBCMD_FEATSEL_ENABLE)
> + switch2_init_step_done(ns2, NS2_INIT_ENABLE_FEATURES);
> + break;
> + case NS2_CMD_FW_INFO:
> + if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) {
> + if (length < sizeof(ns2->version)) {
> + ret = -EINVAL;
> + goto exit;
> + }
> + memcpy(&ns2->version, message, sizeof(ns2->version));
> + ns2->ctlr_type = ns2->version.ctlr_type;
> + switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
> + }
> + break;
> + default:
> + break;
> + }
> +
> +exit:
> + if (ns2->init_step < NS2_INIT_DONE)
> + switch2_init_controller(ns2);
> +
> + return ret;
> +}
> +EXPORT_SYMBOL_GPL(switch2_receive_command);
> +
> +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg)
> +{
> + struct switch2_controller *ns2 = switch2_get_controller(phys);
> +
> + if (IS_ERR(ns2))
> + return PTR_ERR(ns2);
> +
> + cfg->parent = ns2;
> +
> + guard(mutex)(&ns2->lock);
> + WARN_ON(ns2->cfg);
> + ns2->cfg = cfg;
> +
> + if (ns2->hdev)
> + return switch2_init_controller(ns2);
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg);
> +
> +void switch2_controller_detach_cfg(struct switch2_controller *ns2)
> +{
> + mutex_lock(&ns2->lock);
> + WARN_ON(ns2 != ns2->cfg->parent);
> + ns2->cfg = NULL;
> + mutex_unlock(&ns2->lock);
> + switch2_controller_put(ns2);
> +}
> +EXPORT_SYMBOL_GPL(switch2_controller_detach_cfg);
> +
> +static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> + struct switch2_controller *ns2;
> + struct usb_device *udev;
> + char phys[64];
> + int ret;
> +
> + if (!hid_is_usb(hdev))
> + return -ENODEV;
> +
> + udev = hid_to_usb_dev(hdev);
> + if (usb_make_path(udev, phys, sizeof(phys)) < 0)
> + return -EINVAL;
> +
> + ret = hid_parse(hdev);
> + if (ret) {
> + hid_err(hdev, "parse failed %d\n", ret);
> + return ret;
> + }
> +
> + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
> + if (ret) {
> + hid_err(hdev, "hw_start failed %d\n", ret);
> + return ret;
> + }
> +
> + ret = hid_hw_open(hdev);
> + if (ret) {
> + hid_err(hdev, "hw_open failed %d\n", ret);
> + goto err_stop;
> + }
For the Switch 1 controllers we are calling hid_device_io_start after
hid_hw_open. Is this not necessary in this case?
> +
> + ns2 = switch2_get_controller(phys);
> + if (!ns2) {
switch2_get_controller returns an err pointer in case of ENOMEM, not
NULL so I think this check has to be changed.
> + ret = -ENOMEM;
> + goto err_close;
> + }
> +
> + guard(mutex)(&ns2->lock);
> + WARN_ON(ns2->hdev);
> + ns2->hdev = hdev;
> + switch (hdev->product | (hdev->vendor << 16)) {
> + default:
> + strscpy(ns2->name, hdev->name, sizeof(ns2->name));
> + break;
> + /* Some controllers have slightly wrong names so we override them */
> + case USB_DEVICE_ID_NINTENDO_NS2_JOYCONR | (USB_VENDOR_ID_NINTENDO << 16):
> + /* Missing the "2" in the name */
> + strscpy(ns2->name, "Nintendo Joy-Con 2 (R)", sizeof(ns2->name));
> + break;
> + case USB_DEVICE_ID_NINTENDO_NS2_GCCON | (USB_VENDOR_ID_NINTENDO << 16):
> + /* Has "Nintendo" in the name twice */
> + strscpy(ns2->name, "Nintendo GameCube Controller", sizeof(ns2->name));
> + break;
> + }
> +
> + ns2->player_id = U32_MAX;
> + ret = ida_alloc(&nintendo_player_id_allocator, GFP_KERNEL);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret);
> + else
> + ns2->player_id = ret;
> +
> + switch2_leds_create(ns2);
> +
> + hid_set_drvdata(hdev, ns2);
> +
> + if (ns2->cfg)
> + return switch2_init_controller(ns2);
> +
> + return 0;
> +
> +err_close:
> + hid_hw_close(hdev);
> +err_stop:
> + hid_hw_stop(hdev);
> +
> + return ret;
> +}
> +
> +static void switch2_remove(struct hid_device *hdev)
> +{
> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
> +
> + hid_hw_close(hdev);
> + mutex_lock(&ns2->lock);
> + WARN_ON(ns2->hdev != hdev);
> + ns2->hdev = NULL;
> + mutex_unlock(&ns2->lock);
> + ida_free(&nintendo_player_id_allocator, ns2->player_id);
> + switch2_controller_put(ns2);
> + hid_hw_stop(hdev);
> +}
> +
> static const struct hid_device_id nintendo_hid_devices[] = {
> + /* Switch devices */
> { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> USB_DEVICE_ID_NINTENDO_PROCON) },
> { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> @@ -2813,10 +3935,69 @@ static const struct hid_device_id nintendo_hid_devices[] = {
> USB_DEVICE_ID_NINTENDO_GENCON) },
> { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_NINTENDO,
> USB_DEVICE_ID_NINTENDO_N64CON) },
> + /* Switch 2 devices */
> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> + USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> + USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> + USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
> + USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
> { }
> };
> MODULE_DEVICE_TABLE(hid, nintendo_hid_devices);
>
> +static bool nintendo_is_switch2(struct hid_device *hdev)
> +{
> + return hdev->vendor == USB_VENDOR_ID_NINTENDO &&
> + hdev->product >= USB_DEVICE_ID_NINTENDO_NS2_JOYCONR;
> +}
> +
> +static void nintendo_hid_remove(struct hid_device *hdev)
> +{
> + if (nintendo_is_switch2(hdev))
> + switch2_remove(hdev);
> + else
> + joycon_remove(hdev);
> +}
> +
> +static int nintendo_hid_event(struct hid_device *hdev,
> + struct hid_report *report, u8 *raw_data, int size)
> +{
> + if (nintendo_is_switch2(hdev))
> + return switch2_event(hdev, report, raw_data, size);
> + else
> + return joycon_event(hdev, report, raw_data, size);
> +}
> +
> +static int nintendo_hid_probe(struct hid_device *hdev,
> + const struct hid_device_id *id)
> +{
> + if (nintendo_is_switch2(hdev))
> + return switch2_probe(hdev, id);
> + else
> + return joycon_probe(hdev, id);
> +}
> +
> +#ifdef CONFIG_PM
> +static int nintendo_hid_resume(struct hid_device *hdev)
> +{
> + if (nintendo_is_switch2(hdev))
> + return 0;
> + else
> + return joycon_resume(hdev);
> +}
> +
> +static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
> +{
> + if (nintendo_is_switch2(hdev))
> + return 0;
> + else
> + return joycon_suspend(hdev, message);
> +}
> +#endif
> +
> static struct hid_driver nintendo_hid_driver = {
> .name = "nintendo",
> .id_table = nintendo_hid_devices,
> @@ -2844,4 +4025,5 @@ MODULE_LICENSE("GPL");
> MODULE_AUTHOR("Ryan McClelland <rymcclel@gmail.com>");
> MODULE_AUTHOR("Emily Strickland <linux@emily.st>");
> MODULE_AUTHOR("Daniel J. Ogorchock <djogorchock@gmail.com>");
> +MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
> MODULE_DESCRIPTION("Driver for Nintendo Switch Controllers");
> diff --git a/drivers/hid/hid-nintendo.h b/drivers/hid/hid-nintendo.h
> new file mode 100644
> index 0000000000000..7aff22f302661
> --- /dev/null
> +++ b/drivers/hid/hid-nintendo.h
> @@ -0,0 +1,72 @@
> +/* SPDX-License-Identifier: GPL-2.0+ */
> +/*
> + * HID driver for Nintendo Switch 2 controllers
> + *
> + * Copyright (c) 2025 Valve Software
> + *
> + * This driver is based on the following work:
> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
> + * https://github.com/ndeadly/switch2_controller_research
> + */
> +
> +#ifndef __HID_NINTENDO_H
> +#define __HID_NINTENDO_H
> +
> +#include <linux/bits.h>
> +
> +#define NS2_FLAG_OK BIT(0)
> +#define NS2_FLAG_NACK BIT(2)
> +
> +enum switch2_cmd {
> + NS2_CMD_NFC = 0x01,
> + NS2_CMD_FLASH = 0x02,
> + NS2_CMD_INIT = 0x03,
> + NS2_CMD_GRIP = 0x08,
> + NS2_CMD_LED = 0x09,
> + NS2_CMD_VIBRATE = 0x0a,
> + NS2_CMD_BATTERY = 0x0b,
> + NS2_CMD_FEATSEL = 0x0c,
> + NS2_CMD_FW_UPD = 0x0d,
> + NS2_CMD_FW_INFO = 0x10,
> + NS2_CMD_BT_PAIR = 0x15,
> +};
> +
> +enum switch2_direction {
> + NS2_DIR_IN = 0x00,
> + NS2_DIR_OUT = 0x90,
> +};
> +
> +enum switch2_transport {
> + NS2_TRANS_USB = 0x00,
> + NS2_TRANS_BT = 0x01,
> +};
> +
> +struct switch2_cmd_header {
> + uint8_t command;
> + uint8_t flags;
> + uint8_t transport;
> + uint8_t subcommand;
> + uint8_t unk1;
> + uint8_t length;
> + uint16_t unk2;
> +};
> +static_assert(sizeof(struct switch2_cmd_header) == 8);
> +
> +struct device;
> +struct switch2_controller;
> +struct switch2_cfg_intf {
> + struct switch2_controller *parent;
> + struct device *dev;
> +
> + int (*send_command)(enum switch2_cmd command, uint8_t subcommand,
> + const void *message, size_t length,
> + struct switch2_cfg_intf *intf);
> +};
> +
> +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg);
> +void switch2_controller_detach_cfg(struct switch2_controller *controller);
> +
> +int switch2_receive_command(struct switch2_controller *controller,
> + const uint8_t *message, size_t length);
> +
> +#endif
> diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
> index 7755e5b454d2c..868262c6ccd9a 100644
> --- a/drivers/input/joystick/Kconfig
> +++ b/drivers/input/joystick/Kconfig
> @@ -422,4 +422,15 @@ config JOYSTICK_SEESAW
> To compile this driver as a module, choose M here: the module will be
> called adafruit-seesaw.
>
> +config JOYSTICK_NINTENDO_SWITCH2_USB
> + tristate "Wired Nintendo Switch 2 controller support"
> + depends on HID_NINTENDO
> + depends on USB
> + help
> + Say Y here if you want to enable support for wired Nintendo Switch 2
> + controllers.
> +
> + To compile this driver as a module, choose M here: the
> + module will be called nintendo-switch2-usb.
> +
> endif
> diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
> index 9976f596a9208..8f92900ae8856 100644
> --- a/drivers/input/joystick/Makefile
> +++ b/drivers/input/joystick/Makefile
> @@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER) += sidewinder.o
> obj-$(CONFIG_JOYSTICK_SPACEBALL) += spaceball.o
> obj-$(CONFIG_JOYSTICK_SPACEORB) += spaceorb.o
> obj-$(CONFIG_JOYSTICK_STINGER) += stinger.o
> +obj-$(CONFIG_JOYSTICK_NINTENDO_SWITCH2_USB) += nintendo-switch2-usb.o
> obj-$(CONFIG_JOYSTICK_TMDC) += tmdc.o
> obj-$(CONFIG_JOYSTICK_TURBOGRAFX) += turbografx.o
> obj-$(CONFIG_JOYSTICK_TWIDJOY) += twidjoy.o
> diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
> new file mode 100644
> index 0000000000000..ebd89d852e21a
> --- /dev/null
> +++ b/drivers/input/joystick/nintendo-switch2-usb.c
> @@ -0,0 +1,353 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * USB driver for Nintendo Switch 2 controllers configuration interface
> + *
> + * Copyright (c) 2025 Valve Software
> + *
> + * This driver is based on the following work:
> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
> + * https://github.com/ndeadly/switch2_controller_research
> + */
> +
> +#include "../../hid/hid-ids.h"
> +#include "../../hid/hid-nintendo.h"
> +#include <linux/module.h>
> +#include <linux/usb/input.h>
> +
> +#define NS2_BULK_SIZE 64
> +#define NS2_IN_URBS 2
> +#define NS2_OUT_URBS 4
> +
> +static struct usb_driver switch2_usb;
> +
> +struct switch2_urb {
> + struct urb *urb;
> + uint8_t *data;
> + bool active;
> +};
> +
> +struct switch2_usb {
> + struct switch2_cfg_intf cfg;
> + struct usb_device *udev;
> +
> + struct switch2_urb bulk_in[NS2_IN_URBS];
> + struct usb_anchor bulk_in_anchor;
> + spinlock_t bulk_in_lock;
> +
> + struct switch2_urb bulk_out[NS2_OUT_URBS];
> + struct usb_anchor bulk_out_anchor;
> + spinlock_t bulk_out_lock;
> +
> + int message_in;
> + struct work_struct message_in_work;
> +};
> +
> +static void switch2_bulk_in(struct urb *urb)
> +{
> + struct switch2_usb *ns2_usb = urb->context;
> + int i;
> + bool schedule = false;
> + unsigned long flags;
> +
> + switch (urb->status) {
> + case 0:
> + schedule = true;
> + break;
> + case -ECONNRESET:
> + case -ENOENT:
> + case -ESHUTDOWN:
> + dev_dbg(&ns2_usb->udev->dev, "shutting down input urb: %d\n", urb->status);
> + return;
> + default:
> + dev_dbg(&ns2_usb->udev->dev, "unknown input urb status: %d\n", urb->status);
> + break;
> + }
> +
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + for (i = 0; i < NS2_IN_URBS; i++) {
> + int err;
> + struct switch2_urb *ns2_urb;
> +
> + if (ns2_usb->bulk_in[i].urb == urb) {
> + ns2_usb->message_in = i;
> + continue;
> + }
> +
> + if (ns2_usb->bulk_in[i].active)
> + continue;
> +
> + ns2_urb = &ns2_usb->bulk_in[i];
> + usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor);
> + err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
> + if (err) {
> + usb_unanchor_urb(ns2_urb->urb);
> + dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
> + } else {
> + ns2_urb->active = true;
> + }
> + }
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + if (schedule)
> + schedule_work(&ns2_usb->message_in_work);
> +}
> +
> +static void switch2_bulk_out(struct urb *urb)
> +{
> + struct switch2_usb *ns2_usb = urb->context;
> + int i;
> +
> + guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock);
> +
> + switch (urb->status) {
> + case 0:
> + break;
> + case -ECONNRESET:
> + case -ENOENT:
> + case -ESHUTDOWN:
> + dev_dbg(&ns2_usb->udev->dev, "shutting down output urb: %d\n", urb->status);
> + return;
> + default:
> + dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
> + return;
> + }
> +
> + for (i = 0; i < NS2_OUT_URBS; i++) {
> + if (ns2_usb->bulk_out[i].urb != urb)
> + continue;
> +
> + ns2_usb->bulk_out[i].active = false;
> + break;
> + }
> +}
> +
> +static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand,
> + const void *message, size_t size, struct switch2_cfg_intf *cfg)
> +{
> + struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg;
> + struct switch2_urb *urb = NULL;
> + int i;
> + int ret;
> + unsigned long flags;
> +
> + struct switch2_cmd_header header = {
> + command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size
> + };
> +
> + if (WARN_ON(size > 56))
> + return -EINVAL;
> +
> + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
> + for (i = 0; i < NS2_OUT_URBS; i++) {
> + if (ns2_usb->bulk_out[i].active)
> + continue;
> +
> + urb = &ns2_usb->bulk_out[i];
> + urb->active = true;
> + break;
> + }
> + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
> +
> + if (!urb) {
> + dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n");
> + return -ENOBUFS;
> + }
> +
> + memcpy(urb->data, &header, sizeof(header));
> + if (message && size)
> + memcpy(&urb->data[8], message, size);
> + urb->urb->transfer_buffer_length = size + sizeof(header);
> +
> + print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data,
> + size + sizeof(header), false);
> +
> + usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor);
> + ret = usb_submit_urb(urb->urb, GFP_ATOMIC);
> + if (ret) {
> + if (ret != -ENODEV)
> + dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret);
> + urb->active = false;
> + usb_unanchor_urb(urb->urb);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void switch2_usb_message_in_work(struct work_struct *work)
> +{
> + struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work);
> + struct switch2_urb *urb;
> + int err;
> + unsigned long flags;
> +
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + urb = &ns2_usb->bulk_in[ns2_usb->message_in];
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + err = switch2_receive_command(ns2_usb->cfg.parent, urb->urb->transfer_buffer,
> + urb->urb->actual_length);
> + if (err)
> + dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err);
> +
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + urb->active = false;
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +}
> +
> +static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
> +{
> + struct switch2_usb *ns2_usb;
> + struct usb_device *udev;
> + struct usb_endpoint_descriptor *bulk_in, *bulk_out;
> + char phys[64];
> + int ret;
> + int i;
> +
> + udev = interface_to_usbdev(intf);
> + if (usb_make_path(udev, phys, sizeof(phys)) < 0)
> + return -EINVAL;
> +
> + ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL);
> + if (ret) {
> + dev_err(&intf->dev, "failed to find bulk EPs\n");
> + return ret;
> + }
> +
> + ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL);
> + if (!ns2_usb)
> + return -ENOMEM;
> +
> + ns2_usb->udev = udev;
> + for (i = 0; i < NS2_IN_URBS; i++) {
> + ns2_usb->bulk_in[i].urb = usb_alloc_urb(0, GFP_KERNEL);
> + if (!ns2_usb->bulk_in[i].urb) {
> + ret = -ENOMEM;
> + goto err_free_in;
> + }
> +
> + ns2_usb->bulk_in[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
> + &ns2_usb->bulk_in[i].urb->transfer_dma);
> + if (!ns2_usb->bulk_in[i].data) {
> + ret = -ENOMEM;
> + goto err_free_in;
> + }
> +
> + usb_fill_bulk_urb(ns2_usb->bulk_in[i].urb, udev,
> + usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
> + ns2_usb->bulk_in[i].data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
> + ns2_usb->bulk_in[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
> + }
> +
> + for (i = 0; i < NS2_OUT_URBS; i++) {
> + ns2_usb->bulk_out[i].urb = usb_alloc_urb(0, GFP_KERNEL);
> + if (!ns2_usb->bulk_out[i].urb) {
> + ret = -ENOMEM;
> + goto err_free_out;
> + }
> +
> + ns2_usb->bulk_out[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
> + &ns2_usb->bulk_out[i].urb->transfer_dma);
> + if (!ns2_usb->bulk_out[i].data) {
> + ret = -ENOMEM;
> + goto err_free_out;
> + }
> +
> + usb_fill_bulk_urb(ns2_usb->bulk_out[i].urb, udev,
> + usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
> + ns2_usb->bulk_out[i].data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
> + ns2_usb->bulk_out[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
> + }
> +
> + ns2_usb->bulk_in[0].active = true;
> + ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
> + if (ret < 0)
> + goto err_free_out;
> +
> + init_usb_anchor(&ns2_usb->bulk_out_anchor);
> + spin_lock_init(&ns2_usb->bulk_out_lock);
> + init_usb_anchor(&ns2_usb->bulk_in_anchor);
> + spin_lock_init(&ns2_usb->bulk_in_lock);
> + INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work);
> +
> + usb_set_intfdata(intf, ns2_usb);
> +
> + ns2_usb->cfg.dev = &ns2_usb->udev->dev;
> + ns2_usb->cfg.send_command = switch2_usb_send_cmd;
> +
> + ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
> + if (ret < 0)
> + goto err_kill_urb;
> +
> + return 0;
> +
> +err_kill_urb:
> + usb_kill_urb(ns2_usb->bulk_in[0].urb);
> +err_free_out:
> + for (i = 0; i < NS2_OUT_URBS; i++) {
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
> + ns2_usb->bulk_out[i].urb->transfer_dma);
> + usb_free_urb(ns2_usb->bulk_out[i].urb);
> + }
> +err_free_in:
> + for (i = 0; i < NS2_IN_URBS; i++) {
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
> + ns2_usb->bulk_in[i].urb->transfer_dma);
> + usb_free_urb(ns2_usb->bulk_in[i].urb);
> + }
> + devm_kfree(&intf->dev, ns2_usb);
> +
> + return ret;
> +}
> +
> +static void switch2_usb_disconnect(struct usb_interface *intf)
> +{
> + struct switch2_usb *ns2_usb = usb_get_intfdata(intf);
> + unsigned long flags;
> + int i;
> +
> + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
> + usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
> + for (i = 0; i < NS2_OUT_URBS; i++) {
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
> + ns2_usb->bulk_out[i].urb->transfer_dma);
> + usb_free_urb(ns2_usb->bulk_out[i].urb);
> + }
> + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
> +
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
> + cancel_work_sync(&ns2_usb->message_in_work);
> + for (i = 0; i < NS2_IN_URBS; i++) {
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
> + ns2_usb->bulk_in[i].urb->transfer_dma);
> + usb_free_urb(ns2_usb->bulk_in[i].urb);
> + }
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + switch2_controller_detach_cfg(ns2_usb->cfg.parent);
As we have allocated ns2_usb with devm_kzalloc, don't we need to free it
with devm_kfree again?
Cheers,
Silvan
> +}
> +
> +#define SWITCH2_CONTROLLER(vend, prod) \
> + USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0)
> +
> +static const struct usb_device_id switch2_usb_devices[] = {
> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
> + { }
> +};
> +MODULE_DEVICE_TABLE(usb, switch2_usb_devices);
> +
> +static struct usb_driver switch2_usb = {
> + .name = "switch2",
> + .id_table = switch2_usb_devices,
> + .probe = switch2_usb_probe,
> + .disconnect = switch2_usb_disconnect,
> +};
> +module_usb_driver(switch2_usb);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
> +MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
^ permalink raw reply
* Re: [PATCH v2 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Silvan Jegen @ 2026-04-02 19:11 UTC (permalink / raw)
To: Vicki Pfau; +Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <20260318030850.289712-3-vi@endrift.com>
Hi!
Just one typo below.
Vicki Pfau <vi@endrift.com> wrote:
> This adds rumble support for both the "HD Rumble" linear resonant actuator
> type as used in the Joy-Cons and Pro Controller, as well as the eccentric
> rotating mass type used in the GameCube controller. Note that since there's
> currently no API for exposing full control of LRAs with evdev, it only
> simulates a basic rumble for now.
>
> Signed-off-by: Vicki Pfau <vi@endrift.com>
> ---
> drivers/hid/Kconfig | 8 +-
> drivers/hid/hid-nintendo.c | 179 ++++++++++++++++++++++++++++++++++++-
> 2 files changed, 181 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index 1a293a6c02c26..d8ce7451d8578 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -842,10 +842,10 @@ config NINTENDO_FF
> depends on HID_NINTENDO
> select INPUT_FF_MEMLESS
> help
> - Say Y here if you have a Nintendo Switch controller and want to enable
> - force feedback support for it. This works for both joy-cons, the pro
> - controller, and the NSO N64 controller. For the pro controller, both
> - rumble motors can be controlled individually.
> + Say Y here if you have a Nintendo Switch or Switch 2 controller and want
> + to enable force feedback support for it. This works for Joy-Cons, the Pro
> + Controllers, and the NSO N64 and GameCube controller. For the Pro
> + Controller, both rumble motors can be controlled individually.
>
> config HID_NTI
> tristate "NTI keyboard adapters"
> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index 4ab8d4e7558a1..73d732ceb7116 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -2976,6 +2976,18 @@ struct switch2_stick_calibration {
> struct switch2_axis_calibration y;
> };
>
> +struct switch2_hd_rumble {
> + uint16_t hi_freq : 10;
> + uint16_t hi_amp : 10;
> + uint16_t lo_freq : 10;
> + uint16_t lo_amp : 10;
> +} __packed;
> +
> +struct switch2_erm_rumble {
> + uint16_t error;
> + uint16_t amplitude;
> +};
> +
> struct switch2_controller {
> struct hid_device *hdev;
> struct switch2_cfg_intf *cfg;
> @@ -2997,8 +3009,45 @@ struct switch2_controller {
>
> uint32_t player_id;
> struct led_classdev leds[4];
> +
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> + spinlock_t rumble_lock;
> + uint8_t rumble_seq;
> + union {
> + struct switch2_hd_rumble hd;
> + struct switch2_erm_rumble sd;
> + } rumble;
> + unsigned long last_rumble_work;
> + struct delayed_work rumble_work;
> + uint8_t rumble_buffer[64];
> +#endif
> +};
> +
> +enum gc_rumble {
> + GC_RUMBLE_OFF = 0,
> + GC_RUMBLE_ON = 1,
> + GC_RUMBLE_STOP = 2,
> };
>
> +/*
> + * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
> + * and also leaves your hands feeling like melted jelly, so we set a semi-abitrary scaling factor
s/abitrary/arbitrary/
Cheers,
Silvan
^ permalink raw reply
* [dtor-input:next] BUILD SUCCESS f13b7800929df0df0ba2407226ba1b627b482c92
From: kernel test robot @ 2026-04-02 19:39 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input
tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
branch HEAD: f13b7800929df0df0ba2407226ba1b627b482c92 Input: usbtouchscreen - refactor endpoint lookup
elapsed time: 742m
configs tested: 182
configs skipped: 2
The following configs have been built successfully.
More configs may be tested in the coming days.
tested configs:
alpha allnoconfig gcc-15.2.0
alpha allyesconfig gcc-15.2.0
alpha defconfig gcc-15.2.0
arc allmodconfig clang-16
arc allnoconfig gcc-15.2.0
arc allyesconfig clang-23
arc defconfig gcc-15.2.0
arc randconfig-001-20260402 gcc-11.5.0
arc randconfig-002-20260402 gcc-11.5.0
arm allnoconfig gcc-15.2.0
arm allyesconfig clang-16
arm defconfig gcc-15.2.0
arm orion5x_defconfig clang-23
arm randconfig-001-20260402 gcc-11.5.0
arm randconfig-002-20260402 gcc-11.5.0
arm randconfig-003-20260402 gcc-11.5.0
arm randconfig-004-20260402 gcc-11.5.0
arm spear13xx_defconfig gcc-15.2.0
arm64 allmodconfig clang-23
arm64 allnoconfig gcc-15.2.0
arm64 defconfig gcc-15.2.0
arm64 randconfig-001-20260402 gcc-15.2.0
arm64 randconfig-002-20260402 gcc-15.2.0
arm64 randconfig-003-20260402 gcc-15.2.0
arm64 randconfig-004-20260402 gcc-15.2.0
csky allmodconfig gcc-15.2.0
csky allnoconfig gcc-15.2.0
csky defconfig gcc-15.2.0
csky randconfig-001-20260402 gcc-15.2.0
csky randconfig-002-20260402 gcc-15.2.0
hexagon allmodconfig gcc-15.2.0
hexagon allnoconfig gcc-15.2.0
hexagon defconfig gcc-15.2.0
hexagon randconfig-001-20260402 clang-18
hexagon randconfig-001-20260403 clang-23
hexagon randconfig-002-20260402 clang-18
hexagon randconfig-002-20260403 clang-23
i386 allmodconfig clang-20
i386 allnoconfig gcc-15.2.0
i386 allyesconfig clang-20
i386 buildonly-randconfig-001-20260402 clang-20
i386 buildonly-randconfig-001-20260403 gcc-14
i386 buildonly-randconfig-002-20260402 clang-20
i386 buildonly-randconfig-002-20260403 gcc-14
i386 buildonly-randconfig-003-20260402 clang-20
i386 buildonly-randconfig-003-20260403 gcc-14
i386 buildonly-randconfig-004-20260402 clang-20
i386 buildonly-randconfig-004-20260403 gcc-14
i386 buildonly-randconfig-005-20260402 clang-20
i386 buildonly-randconfig-005-20260403 gcc-14
i386 buildonly-randconfig-006-20260402 clang-20
i386 buildonly-randconfig-006-20260403 gcc-14
i386 defconfig gcc-15.2.0
i386 randconfig-001-20260402 clang-20
i386 randconfig-002-20260402 clang-20
i386 randconfig-003-20260402 clang-20
i386 randconfig-004-20260402 clang-20
i386 randconfig-005-20260402 clang-20
i386 randconfig-006-20260402 clang-20
i386 randconfig-007-20260402 clang-20
i386 randconfig-011-20260402 clang-20
i386 randconfig-012-20260402 clang-20
i386 randconfig-013-20260402 clang-20
i386 randconfig-014-20260402 clang-20
i386 randconfig-015-20260402 clang-20
i386 randconfig-016-20260402 clang-20
i386 randconfig-017-20260402 clang-20
loongarch allmodconfig clang-23
loongarch allnoconfig gcc-15.2.0
loongarch defconfig clang-19
loongarch randconfig-001-20260402 clang-18
loongarch randconfig-001-20260403 clang-23
loongarch randconfig-002-20260402 clang-18
loongarch randconfig-002-20260403 clang-23
m68k allmodconfig gcc-15.2.0
m68k allnoconfig gcc-15.2.0
m68k allyesconfig clang-16
m68k defconfig clang-19
m68k stmark2_defconfig gcc-15.2.0
microblaze allnoconfig gcc-15.2.0
microblaze allyesconfig gcc-15.2.0
microblaze defconfig clang-19
mips allmodconfig gcc-15.2.0
mips allnoconfig gcc-15.2.0
mips allyesconfig gcc-15.2.0
nios2 allmodconfig clang-23
nios2 allnoconfig clang-23
nios2 defconfig clang-19
nios2 randconfig-001-20260402 clang-18
nios2 randconfig-001-20260403 clang-23
nios2 randconfig-002-20260402 clang-18
nios2 randconfig-002-20260403 clang-23
openrisc allmodconfig clang-23
openrisc allnoconfig clang-23
openrisc defconfig gcc-15.2.0
parisc allmodconfig gcc-15.2.0
parisc allnoconfig clang-23
parisc allyesconfig clang-19
parisc defconfig gcc-15.2.0
parisc randconfig-001-20260402 clang-20
parisc randconfig-002-20260402 clang-20
parisc64 defconfig clang-19
powerpc allmodconfig gcc-15.2.0
powerpc allnoconfig clang-23
powerpc randconfig-001-20260402 clang-20
powerpc randconfig-002-20260402 clang-20
powerpc64 randconfig-001-20260402 clang-20
powerpc64 randconfig-002-20260402 clang-20
riscv allmodconfig clang-23
riscv allnoconfig clang-23
riscv allyesconfig clang-16
riscv defconfig gcc-15.2.0
riscv randconfig-001-20260402 clang-23
riscv randconfig-002-20260402 clang-23
s390 allmodconfig clang-19
s390 allnoconfig clang-23
s390 allyesconfig gcc-15.2.0
s390 defconfig gcc-15.2.0
s390 randconfig-001-20260402 clang-23
s390 randconfig-002-20260402 clang-23
sh allmodconfig gcc-15.2.0
sh allnoconfig clang-23
sh allyesconfig clang-19
sh defconfig gcc-14
sh randconfig-001-20260402 clang-23
sh randconfig-002-20260402 clang-23
sparc allnoconfig clang-23
sparc defconfig gcc-15.2.0
sparc randconfig-001-20260402 gcc-14
sparc randconfig-002-20260402 gcc-14
sparc64 allmodconfig clang-23
sparc64 defconfig gcc-14
sparc64 randconfig-001-20260402 gcc-14
sparc64 randconfig-002-20260402 gcc-14
um allmodconfig clang-19
um allnoconfig clang-23
um allyesconfig gcc-15.2.0
um defconfig gcc-14
um i386_defconfig gcc-14
um randconfig-001-20260402 gcc-14
um randconfig-002-20260402 gcc-14
um x86_64_defconfig gcc-14
x86_64 allmodconfig clang-20
x86_64 allnoconfig clang-23
x86_64 allyesconfig clang-20
x86_64 buildonly-randconfig-001-20260402 clang-20
x86_64 buildonly-randconfig-002-20260402 clang-20
x86_64 buildonly-randconfig-003-20260402 clang-20
x86_64 buildonly-randconfig-004-20260402 clang-20
x86_64 buildonly-randconfig-005-20260402 clang-20
x86_64 buildonly-randconfig-006-20260402 clang-20
x86_64 defconfig gcc-14
x86_64 kexec clang-20
x86_64 randconfig-001-20260402 gcc-14
x86_64 randconfig-002-20260402 gcc-14
x86_64 randconfig-003-20260402 gcc-14
x86_64 randconfig-004-20260402 gcc-14
x86_64 randconfig-005-20260402 gcc-14
x86_64 randconfig-006-20260402 gcc-14
x86_64 randconfig-011-20260402 clang-20
x86_64 randconfig-012-20260402 clang-20
x86_64 randconfig-013-20260402 clang-20
x86_64 randconfig-014-20260402 clang-20
x86_64 randconfig-015-20260402 clang-20
x86_64 randconfig-016-20260402 clang-20
x86_64 randconfig-071-20260402 clang-20
x86_64 randconfig-072-20260402 clang-20
x86_64 randconfig-073-20260402 clang-20
x86_64 randconfig-074-20260402 clang-20
x86_64 randconfig-075-20260402 clang-20
x86_64 randconfig-076-20260402 clang-20
x86_64 rhel-9.4 clang-20
x86_64 rhel-9.4-bpf gcc-14
x86_64 rhel-9.4-func clang-20
x86_64 rhel-9.4-kselftests clang-20
x86_64 rhel-9.4-kunit gcc-14
x86_64 rhel-9.4-ltp gcc-14
x86_64 rhel-9.4-rust clang-20
xtensa allnoconfig clang-23
xtensa allyesconfig clang-23
xtensa randconfig-001-20260402 gcc-14
xtensa randconfig-002-20260402 gcc-14
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH 0/2] PCI: Remove no_pci_devices
From: Heiner Kallweit @ 2026-04-02 22:15 UTC (permalink / raw)
To: Dmitry Torokhov, Bjorn Helgaas
Cc: open list:HID CORE LAYER, linux-pci@vger.kernel.org
Remove the last user of no_pci_devices(), and then the function itself.
Heiner Kallweit (2):
input: pc110pad: change PCI check to get rid of orphaned
no_pci_devices
PCI: Remove no_pci_devices
drivers/input/mouse/pc110pad.c | 2 +-
drivers/pci/probe.c | 17 -----------------
include/linux/pci.h | 3 ---
3 files changed, 1 insertion(+), 21 deletions(-)
--
2.53.0
^ permalink raw reply
* [PATCH 1/2] input: pc110pad: change PCI check to get rid of orphaned no_pci_devices
From: Heiner Kallweit @ 2026-04-02 22:17 UTC (permalink / raw)
To: Dmitry Torokhov, Bjorn Helgaas
Cc: open list:HID CORE LAYER, linux-pci@vger.kernel.org
In-Reply-To: <dd9ea7c1-44f5-4364-a3d5-885b1c99159f@gmail.com>
As a prerequisite for removing no_pci_devices(), replace its usage here
with an equivalent check for presence of a PCI bus.
Signed-off-by: Heiner Kallweit <hkallweit1@gmail.com>
---
drivers/input/mouse/pc110pad.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/input/mouse/pc110pad.c b/drivers/input/mouse/pc110pad.c
index efa58049f..c7a6df210 100644
--- a/drivers/input/mouse/pc110pad.c
+++ b/drivers/input/mouse/pc110pad.c
@@ -91,7 +91,7 @@ static int __init pc110pad_init(void)
{
int err;
- if (!no_pci_devices())
+ if (pci_find_next_bus(NULL))
return -ENODEV;
if (!request_region(pc110pad_io, 4, "pc110pad")) {
--
2.53.0
^ permalink raw reply related
* [PATCH 2/2] PCI: Remove no_pci_devices
From: Heiner Kallweit @ 2026-04-02 22:18 UTC (permalink / raw)
To: Dmitry Torokhov, Bjorn Helgaas
Cc: open list:HID CORE LAYER, linux-pci@vger.kernel.org
In-Reply-To: <dd9ea7c1-44f5-4364-a3d5-885b1c99159f@gmail.com>
After having removed the last usage of no_pci_devices(), this function
can be removed.
Signed-off-by: Heiner Kallweit <hkallweit1@gmail.com>
---
drivers/pci/probe.c | 17 -----------------
include/linux/pci.h | 3 ---
2 files changed, 20 deletions(-)
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index bccc7a4bd..19d73f613 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -67,23 +67,6 @@ static struct resource *get_pci_domain_busn_res(int domain_nr)
return &r->res;
}
-/*
- * Some device drivers need know if PCI is initiated.
- * Basically, we think PCI is not initiated when there
- * is no device to be found on the pci_bus_type.
- */
-int no_pci_devices(void)
-{
- struct device *dev;
- int no_devices;
-
- dev = bus_find_next_device(&pci_bus_type, NULL);
- no_devices = (dev == NULL);
- put_device(dev);
- return no_devices;
-}
-EXPORT_SYMBOL(no_pci_devices);
-
/*
* PCI Bus Class
*/
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 8861eeb43..cc98713e3 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1201,8 +1201,6 @@ extern const struct bus_type pci_bus_type;
/* Do NOT directly access these two variables, unless you are arch-specific PCI
* code, or PCI core code. */
extern struct list_head pci_root_buses; /* List of all known PCI buses */
-/* Some device drivers need know if PCI is initiated */
-int no_pci_devices(void);
void pcibios_resource_survey_bus(struct pci_bus *bus);
void pcibios_bus_add_device(struct pci_dev *pdev);
@@ -2140,7 +2138,6 @@ static inline struct pci_dev *pci_get_base_class(unsigned int class,
static inline int pci_dev_present(const struct pci_device_id *ids)
{ return 0; }
-#define no_pci_devices() (1)
#define pci_dev_put(dev) do { } while (0)
static inline void pci_set_master(struct pci_dev *dev) { }
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v2 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-04-03 1:17 UTC (permalink / raw)
To: Silvan Jegen
Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <3GWQPE79MJ7Y0.2LOOIA8A83N7R@homearch.localdomain>
Hi,
Replies inline
On 4/2/26 12:09 PM, Silvan Jegen wrote:
> Hi
>
> Thanks for the patch!
>
> Just some comments and questions inline below.
>
> Vicki Pfau <vi@endrift.com> wrote:
>> This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
>> unusual split-interface design such that input and rumble occur on the main
>> HID interface, but all other communication occurs over a "configuration"
>> interface. This is the case on both USB and Bluetooth, so this new driver
>> uses a split-driver design with the HID interface being the "main" driver
>> and the configuration interface is a secondary driver that looks up to the
>> HID interface, sharing resources on a common struct.
>>
>> Due to using a non-standard pairing interface as well as Bluetooth
>> communications being extremely limited in the kernel, a custom interface
>> between userspace and the kernel will need to be design, along with bringup
>> in BlueZ. That is beyond the scope of this initial patch, which only
>> contains the generic HID and USB configuration interface drivers.
>>
>> This initial work supports general input for the Joy-Con 2, Pro Controller
>> 2, and GameCube NSO controllers. IMU, rumble and battery support is not yet
>> present.
>>
>> Signed-off-by: Vicki Pfau <vi@endrift.com>
>> ---
>> MAINTAINERS | 1 +
>> drivers/hid/Kconfig | 11 +-
>> drivers/hid/hid-ids.h | 4 +
>> drivers/hid/hid-nintendo.c | 1194 ++++++++++++++++-
>> drivers/hid/hid-nintendo.h | 72 +
>> drivers/input/joystick/Kconfig | 11 +
>> drivers/input/joystick/Makefile | 1 +
>> drivers/input/joystick/nintendo-switch2-usb.c | 353 +++++
>> 8 files changed, 1637 insertions(+), 10 deletions(-)
>> create mode 100644 drivers/hid/hid-nintendo.h
>> create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c
>>
>> diff --git a/MAINTAINERS b/MAINTAINERS
>> index 7b277d5bf3d12..4d1a28df5fd24 100644
>> --- a/MAINTAINERS
>> +++ b/MAINTAINERS
>> @@ -18743,6 +18743,7 @@ F: drivers/scsi/nsp32*
>>
>> NINTENDO HID DRIVER
>> M: Daniel J. Ogorchock <djogorchock@gmail.com>
>> +M: Vicki Pfau <vi@endrift.com>
>> L: linux-input@vger.kernel.org
>> S: Maintained
>> F: drivers/hid/hid-nintendo*
>> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
>> index c1d9f7c6a5f23..1a293a6c02c26 100644
>> --- a/drivers/hid/Kconfig
>> +++ b/drivers/hid/Kconfig
>> @@ -826,10 +826,13 @@ config HID_NINTENDO
>> depends on LEDS_CLASS
>> select POWER_SUPPLY
>> help
>> - Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller.
>> - All controllers support bluetooth, and the Pro Controller also supports
>> - its USB mode. This also includes support for the Nintendo Switch Online
>> - Controllers which include the NES, Genesis, SNES, and N64 controllers.
>> + Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller, as
>> + well as Nintendo Switch 2 Joy-Cons, Pro Controller, and NSO GameCube
>> + controllers. All Switch controllers support bluetooth, and the Pro
>> + Controller also supports its USB mode. This also includes support for
>> + the Nintendo Switch Online Controllers which include the NES, Genesis,
>> + SNES, and N64 controllers. Switch 2 controllers currently only support
>> + USB mode.
>>
>> To compile this driver as a module, choose M here: the
>> module will be called hid-nintendo.
>> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
>> index 4ab7640b119ac..a794dad7980f3 100644
>> --- a/drivers/hid/hid-ids.h
>> +++ b/drivers/hid/hid-ids.h
>> @@ -1073,6 +1073,10 @@
>> #define USB_DEVICE_ID_NINTENDO_SNESCON 0x2017
>> #define USB_DEVICE_ID_NINTENDO_GENCON 0x201e
>> #define USB_DEVICE_ID_NINTENDO_N64CON 0x2019
>> +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR 0x2066
>> +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL 0x2067
>> +#define USB_DEVICE_ID_NINTENDO_NS2_PROCON 0x2069
>> +#define USB_DEVICE_ID_NINTENDO_NS2_GCCON 0x2073
>>
>> #define USB_VENDOR_ID_NOVATEK 0x0603
>> #define USB_DEVICE_ID_NOVATEK_PCT 0x0600
>> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
>> index 29008c2cc5304..4ab8d4e7558a1 100644
>> --- a/drivers/hid/hid-nintendo.c
>> +++ b/drivers/hid/hid-nintendo.c
>> @@ -1,11 +1,13 @@
>> // SPDX-License-Identifier: GPL-2.0+
>> /*
>> - * HID driver for Nintendo Switch Joy-Cons and Pro Controllers
>> + * HID driver for Nintendo Switch Joy-Cons and Pro Controllers, as well as
>> + * Nintendo Switch 2 Joy-Cons, Pro Controller, and GameCube Controller
>> *
>> * Copyright (c) 2019-2021 Daniel J. Ogorchock <djogorchock@gmail.com>
>> * Portions Copyright (c) 2020 Nadia Holmquist Pedersen <nadia@nhp.sh>
>> * Copyright (c) 2022 Emily Strickland <linux@emily.st>
>> * Copyright (c) 2023 Ryan McClelland <rymcclel@gmail.com>
>> + * Copyright (c) 2026 Valve Software
>> *
>> * The following resources/projects were referenced for this driver:
>> * https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
>> @@ -13,6 +15,8 @@
>> * https://github.com/FrotBot/SwitchProConLinuxUSB
>> * https://github.com/MTCKC/ProconXInput
>> * https://github.com/Davidobot/BetterJoyForCemu
>> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
>> + * https://github.com/ndeadly/switch2_controller_research
>> * hid-wiimote kernel hid driver
>> * hid-logitech-hidpp driver
>> * hid-sony driver
>> @@ -29,6 +33,7 @@
>> */
>>
>> #include "hid-ids.h"
>> +#include "hid-nintendo.h"
>> #include <linux/unaligned.h>
>> #include <linux/delay.h>
>> #include <linux/device.h>
>> @@ -41,6 +46,8 @@
>> #include <linux/module.h>
>> #include <linux/power_supply.h>
>> #include <linux/spinlock.h>
>> +#include <linux/usb.h>
>> +#include "usbhid/usbhid.h"
>>
>> /*
>> * Reference the url below for the following HID report defines:
>> @@ -2614,7 +2621,7 @@ static int joycon_ctlr_handle_event(struct joycon_ctlr *ctlr, u8 *data,
>> return ret;
>> }
>>
>> -static int nintendo_hid_event(struct hid_device *hdev,
>> +static int joycon_event(struct hid_device *hdev,
>> struct hid_report *report, u8 *raw_data, int size)
>> {
>> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
>> @@ -2625,7 +2632,7 @@ static int nintendo_hid_event(struct hid_device *hdev,
>> return joycon_ctlr_handle_event(ctlr, raw_data, size);
>> }
>>
>> -static int nintendo_hid_probe(struct hid_device *hdev,
>> +static int joycon_probe(struct hid_device *hdev,
>> const struct hid_device_id *id)
>> {
>> int ret;
>> @@ -2729,7 +2736,7 @@ static int nintendo_hid_probe(struct hid_device *hdev,
>> return ret;
>> }
>>
>> -static void nintendo_hid_remove(struct hid_device *hdev)
>> +static void joycon_remove(struct hid_device *hdev)
>> {
>> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
>> unsigned long flags;
>> @@ -2748,7 +2755,9 @@ static void nintendo_hid_remove(struct hid_device *hdev)
>> hid_hw_stop(hdev);
>> }
>>
>> -static int nintendo_hid_resume(struct hid_device *hdev)
>> +#ifdef CONFIG_PM
>> +
>> +static int joycon_resume(struct hid_device *hdev)
>> {
>> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
>> int ret;
>> @@ -2771,7 +2780,7 @@ static int nintendo_hid_resume(struct hid_device *hdev)
>> return ret;
>> }
>>
>> -static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
>> +static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
>> {
>> struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
>>
>> @@ -2790,7 +2799,1120 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
>> return 0;
>> }
>>
>> +#endif
>> +
>> +/*
>> + * =============================================================================
>> + * Switch 2 support
>> + * =============================================================================
>> + */
>> +#define NS2_BTNR_B BIT(0)
>> +#define NS2_BTNR_A BIT(1)
>> +#define NS2_BTNR_Y BIT(2)
>> +#define NS2_BTNR_X BIT(3)
>> +#define NS2_BTNR_R BIT(4)
>> +#define NS2_BTNR_ZR BIT(5)
>> +#define NS2_BTNR_PLUS BIT(6)
>> +#define NS2_BTNR_RS BIT(7)
>> +
>> +#define NS2_BTNL_DOWN BIT(0)
>> +#define NS2_BTNL_RIGHT BIT(1)
>> +#define NS2_BTNL_LEFT BIT(2)
>> +#define NS2_BTNL_UP BIT(3)
>> +#define NS2_BTNL_L BIT(4)
>> +#define NS2_BTNL_ZL BIT(5)
>> +#define NS2_BTNL_MINUS BIT(6)
>> +#define NS2_BTNL_LS BIT(7)
>> +
>> +#define NS2_BTN3_C BIT(4)
>> +#define NS2_BTN3_SR BIT(6)
>> +#define NS2_BTN3_SL BIT(7)
>> +
>> +#define NS2_BTN_JCR_HOME BIT(0)
>> +#define NS2_BTN_JCR_GR BIT(2)
>> +#define NS2_BTN_JCR_C NS2_BTN3_C
>> +#define NS2_BTN_JCR_SR NS2_BTN3_SR
>> +#define NS2_BTN_JCR_SL NS2_BTN3_SL
>> +
>> +#define NS2_BTN_JCL_CAPTURE BIT(0)
>> +#define NS2_BTN_JCL_GL BIT(2)
>> +#define NS2_BTN_JCL_SR NS2_BTN3_SR
>> +#define NS2_BTN_JCL_SL NS2_BTN3_SL
>> +
>> +#define NS2_BTN_PRO_HOME BIT(0)
>> +#define NS2_BTN_PRO_CAPTURE BIT(1)
>> +#define NS2_BTN_PRO_GR BIT(2)
>> +#define NS2_BTN_PRO_GL BIT(3)
>> +#define NS2_BTN_PRO_C NS2_BTN3_C
>> +
>> +#define NS2_BTN_GC_HOME BIT(0)
>> +#define NS2_BTN_GC_CAPTURE BIT(1)
>> +#define NS2_BTN_GC_C NS2_BTN3_C
>> +
>> +#define NS2_TRIGGER_RANGE 4095
>> +#define NS2_AXIS_MIN -32768
>> +#define NS2_AXIS_MAX 32767
>> +
>> +#define NS2_MAX_PLAYER_ID 8
>> +
>> +#define NS2_MAX_INIT_RETRIES 4
>> +
>> +#define NS2_FLASH_ADDR_SERIAL 0x13002
>> +#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB 0x130a8
>> +#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB 0x130e8
>> +#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB 0x13140
>> +#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB 0x1fc040
>> +#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB 0x1fc080
>> +
>> +#define NS2_FLASH_SIZE_SERIAL 0x10
>> +#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9
>> +#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2
>> +#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11
>> +
>> +#define NS2_USER_CALIB_MAGIC 0xa1b2
>> +
>> +#define NS2_FEATURE_BUTTONS BIT(0)
>> +#define NS2_FEATURE_ANALOG BIT(1)
>> +#define NS2_FEATURE_IMU BIT(2)
>> +#define NS2_FEATURE_MOUSE BIT(4)
>> +#define NS2_FEATURE_RUMBLE BIT(5)
>> +#define NS2_FEATURE_MAGNETO BIT(7)
>> +
>> +enum switch2_subcmd_flash {
>> + NS2_SUBCMD_FLASH_READ_BLOCK = 0x01,
>> + NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02,
>> + NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03,
>> + NS2_SUBCMD_FLASH_READ = 0x04,
>> + NS2_SUBCMD_FLASH_WRITE = 0x05,
>> +};
>> +
>> +enum switch2_subcmd_init {
>> + NS2_SUBCMD_INIT_SELECT_REPORT = 0xa,
>> + NS2_SUBCMD_INIT_USB = 0xd,
>> +};
>> +
>> +enum switch2_subcmd_feature_select {
>> + NS2_SUBCMD_FEATSEL_GET_INFO = 0x1,
>> + NS2_SUBCMD_FEATSEL_SET_MASK = 0x2,
>> + NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3,
>> + NS2_SUBCMD_FEATSEL_ENABLE = 0x4,
>> + NS2_SUBCMD_FEATSEL_DISABLE = 0x5,
>> +};
>> +
>> +enum switch2_subcmd_grip {
>> + NS2_SUBCMD_GRIP_GET_INFO = 0x1,
>> + NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2,
>> + NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3,
>> +};
>> +
>> +enum switch2_subcmd_led {
>> + NS2_SUBCMD_LED_P1 = 0x1,
>> + NS2_SUBCMD_LED_P2 = 0x2,
>> + NS2_SUBCMD_LED_P3 = 0x3,
>> + NS2_SUBCMD_LED_P4 = 0x4,
>> + NS2_SUBCMD_LED_ALL_ON = 0x5,
>> + NS2_SUBCMD_LED_ALL_OFF = 0x6,
>> + NS2_SUBCMD_LED_PATTERN = 0x7,
>> + NS2_SUBCMD_LED_BLINK = 0x8,
>> +};
>> +
>> +enum switch2_subcmd_fw_info {
>> + NS2_SUBCMD_FW_INFO_GET = 0x1,
>> +};
>> +
>> +enum switch2_ctlr_type {
>> + NS2_CTLR_TYPE_JCL = 0x00,
>> + NS2_CTLR_TYPE_JCR = 0x01,
>> + NS2_CTLR_TYPE_PRO = 0x02,
>> + NS2_CTLR_TYPE_GC = 0x03,
>> +};
>> +
>> +enum switch2_report_id {
>> + NS2_REPORT_UNIFIED = 0x05,
>> + NS2_REPORT_JCL = 0x07,
>> + NS2_REPORT_JCR = 0x08,
>> + NS2_REPORT_PRO = 0x09,
>> + NS2_REPORT_GC = 0x0a,
>> +};
>> +
>> +enum switch2_init_step {
>> + NS2_INIT_READ_SERIAL,
>> + NS2_INIT_GET_FIRMWARE_INFO,
>> + NS2_INIT_READ_FACTORY_PRIMARY_CALIB,
>> + NS2_INIT_READ_FACTORY_SECONDARY_CALIB,
>> + NS2_INIT_READ_FACTORY_TRIGGER_CALIB,
>> + NS2_INIT_READ_USER_PRIMARY_CALIB,
>> + NS2_INIT_READ_USER_SECONDARY_CALIB,
>> + NS2_INIT_SET_FEATURE_MASK,
>> + NS2_INIT_ENABLE_FEATURES,
>> + NS2_INIT_GRIP_BUTTONS,
>> + NS2_INIT_REPORT_FORMAT,
>> + NS2_INIT_SET_PLAYER_LEDS,
>> + NS2_INIT_INPUT,
>> + NS2_INIT_FINISH,
>> + NS2_INIT_DONE,
>> +};
>> +
>> +struct switch2_version_info {
>> + uint8_t major;
>> + uint8_t minor;
>> + uint8_t patch;
>> + uint8_t ctlr_type;
>> + __le32 unk;
>> + int8_t dsp_major;
>> + int8_t dsp_minor;
>> + int8_t dsp_patch;
>> + int8_t dsp_type;
>> +};
>> +
>> +struct switch2_axis_calibration {
>> + uint16_t neutral;
>> + uint16_t negative;
>> + uint16_t positive;
>> +};
>> +
>> +struct switch2_stick_calibration {
>> + struct switch2_axis_calibration x;
>> + struct switch2_axis_calibration y;
>> +};
>> +
>> +struct switch2_controller {
>> + struct hid_device *hdev;
>> + struct switch2_cfg_intf *cfg;
>> +
>> + char name[64];
>> + char phys[64];
>> + struct list_head entry;
>> + struct mutex lock;
>> +
>> + enum switch2_ctlr_type ctlr_type;
>> + enum switch2_init_step init_step;
>> + struct input_dev __rcu *input;
>> + char serial[NS2_FLASH_SIZE_SERIAL + 1];
>> + struct switch2_version_info version;
>> +
>> + struct switch2_stick_calibration stick_calib[2];
>> + uint8_t lt_zero;
>> + uint8_t rt_zero;
>> +
>> + uint32_t player_id;
>> + struct led_classdev leds[4];
>> +};
>> +
>> +static DEFINE_MUTEX(switch2_controllers_lock);
>> +static LIST_HEAD(switch2_controllers);
>> +
>> +struct switch2_ctlr_button_mapping {
>> + uint32_t code;
>> + int byte;
>> + uint32_t bit;
>> +};
>> +
>> +static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[] = {
>> + { BTN_DPAD_LEFT, 0, NS2_BTNL_LEFT, },
>> + { BTN_DPAD_UP, 0, NS2_BTNL_UP, },
>> + { BTN_DPAD_DOWN, 0, NS2_BTNL_DOWN, },
>> + { BTN_DPAD_RIGHT, 0, NS2_BTNL_RIGHT, },
>> + { BTN_TL, 0, NS2_BTNL_L, },
>> + { BTN_TL2, 0, NS2_BTNL_ZL, },
>> + { BTN_SELECT, 0, NS2_BTNL_MINUS, },
>> + { BTN_THUMBL, 0, NS2_BTNL_LS, },
>> + { KEY_RECORD, 1, NS2_BTN_JCL_CAPTURE, },
>> + { BTN_GRIPR, 1, NS2_BTN_JCL_SL, },
>> + { BTN_GRIPR2, 1, NS2_BTN_JCL_SR, },
>> + { BTN_GRIPL, 1, NS2_BTN_JCL_GL, },
>> + { /* sentinel */ },
>> +};
>> +
>> +static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings[] = {
>> + { BTN_SOUTH, 0, NS2_BTNR_A, },
>> + { BTN_EAST, 0, NS2_BTNR_B, },
>> + { BTN_NORTH, 0, NS2_BTNR_X, },
>> + { BTN_WEST, 0, NS2_BTNR_Y, },
>> + { BTN_TR, 0, NS2_BTNR_R, },
>> + { BTN_TR2, 0, NS2_BTNR_ZR, },
>> + { BTN_START, 0, NS2_BTNR_PLUS, },
>> + { BTN_THUMBR, 0, NS2_BTNR_RS, },
>> + { BTN_C, 1, NS2_BTN_JCR_C, },
>> + { BTN_MODE, 1, NS2_BTN_JCR_HOME, },
>> + { BTN_GRIPL2, 1, NS2_BTN_JCR_SL, },
>> + { BTN_GRIPL, 1, NS2_BTN_JCR_SR, },
>> + { BTN_GRIPR, 1, NS2_BTN_JCR_GR, },
>> + { /* sentinel */ },
>> +};
>> +
>> +static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
>> + { BTN_SOUTH, 0, NS2_BTNR_A, },
>> + { BTN_EAST, 0, NS2_BTNR_B, },
>> + { BTN_NORTH, 0, NS2_BTNR_X, },
>> + { BTN_WEST, 0, NS2_BTNR_Y, },
>> + { BTN_TL, 1, NS2_BTNL_L, },
>> + { BTN_TR, 0, NS2_BTNR_R, },
>> + { BTN_TL2, 1, NS2_BTNL_ZL, },
>> + { BTN_TR2, 0, NS2_BTNR_ZR, },
>> + { BTN_SELECT, 1, NS2_BTNL_MINUS, },
>> + { BTN_START, 0, NS2_BTNR_PLUS, },
>> + { BTN_THUMBL, 1, NS2_BTNL_LS, },
>> + { BTN_THUMBR, 0, NS2_BTNR_RS, },
>> + { BTN_MODE, 2, NS2_BTN_PRO_HOME },
>> + { KEY_RECORD, 2, NS2_BTN_PRO_CAPTURE },
>> + { BTN_GRIPR, 2, NS2_BTN_PRO_GR },
>> + { BTN_GRIPL, 2, NS2_BTN_PRO_GL },
>> + { BTN_C, 2, NS2_BTN_PRO_C },
>> + { /* sentinel */ },
>> +};
>> +
>> +static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
>> + { BTN_SOUTH, 0, NS2_BTNR_A, },
>> + { BTN_EAST, 0, NS2_BTNR_B, },
>> + { BTN_NORTH, 0, NS2_BTNR_X, },
>> + { BTN_WEST, 0, NS2_BTNR_Y, },
>> + { BTN_TL2, 1, NS2_BTNL_L, },
>> + { BTN_TR2, 0, NS2_BTNR_R, },
>> + { BTN_TL, 1, NS2_BTNL_ZL, },
>> + { BTN_TR, 0, NS2_BTNR_ZR, },
>> + { BTN_SELECT, 1, NS2_BTNL_MINUS, },
>> + { BTN_START, 0, NS2_BTNR_PLUS, },
>> + { BTN_MODE, 2, NS2_BTN_GC_HOME },
>> + { KEY_RECORD, 2, NS2_BTN_GC_CAPTURE },
>> + { BTN_C, 2, NS2_BTN_GC_C },
>> + { /* sentinel */ },
>> +};
>> +
>> +static const uint8_t switch2_init_cmd_data[] = {
>> + /*
>> + * The last 6 bytes of this packet are the MAC address of
>> + * the console, but we don't need that for USB
>> + */
>> + 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
>> +};
>> +
>> +static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
>> +
>> +static const uint8_t switch2_feature_mask[] = {
>> + NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU,
>> + 0x00, 0x00, 0x00
>> +};
>> +
>> +static void switch2_init_step_done(struct switch2_controller *ns2, enum switch2_init_step step)
>> +{
>> + if (ns2->init_step != step)
>> + return;
>> +
>> + ns2->init_step++;
>> +}
>> +
>> +static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
>> +{
>> + return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
>> +}
>> +
>> +static int switch2_set_leds(struct switch2_controller *ns2)
>> +{
>> + int i;
>> + uint8_t message[8] = { 0 };
>> +
>> + for (i = 0; i < JC_NUM_LEDS; i++)
>> + message[0] |= (!!ns2->leds[i].brightness) << i;
>> +
>> + if (!ns2->cfg)
>> + return -ENOTCONN;
>> + return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN,
>> + &message, sizeof(message),
>> + ns2->cfg);
>> +}
>> +
>> +static int switch2_player_led_brightness_set(struct led_classdev *led,
>> + enum led_brightness brightness)
>> +{
>> + struct device *dev = led->dev->parent;
>> + struct hid_device *hdev = to_hid_device(dev);
>> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
>> +
>> + if (!ns2)
>> + return -ENODEV;
>> +
>> + guard(mutex)(&ns2->lock);
>> + return switch2_set_leds(ns2);
>> +}
>> +
>> +static void switch2_leds_create(struct switch2_controller *ns2)
>> +{
>> + struct hid_device *hdev = ns2->hdev;
>> + struct led_classdev *led;
>> + int i;
>> + int player_led_pattern;
>> +
>> + player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS;
>> + hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1);
>> +
>> + for (i = 0; i < JC_NUM_LEDS; i++) {
>> + led = &ns2->leds[i];
>> + led->brightness = joycon_player_led_patterns[player_led_pattern][i];
>> + led->max_brightness = 1;
>> + led->brightness_set_blocking = switch2_player_led_brightness_set;
>> + led->flags = LED_CORE_SUSPENDRESUME | LED_HW_PLUGGABLE;
>> + }
>> +}
>> +
>> +static void switch2_config_buttons(struct input_dev *idev,
>> + const struct switch2_ctlr_button_mapping button_mappings[])
>> +{
>> + const struct switch2_ctlr_button_mapping *button;
>> +
>> + for (button = button_mappings; button->code; button++)
>> + input_set_capability(idev, EV_KEY, button->code);
>> +}
>> +
>> +static int switch2_init_input(struct switch2_controller *ns2)
>> +{
>> + struct input_dev *input;
>> + struct hid_device *hdev = ns2->hdev;
>> + int i;
>> + int ret;
>> +
>> + switch2_init_step_done(ns2, NS2_INIT_FINISH);
>> +
>> + rcu_read_lock();
>> + input = rcu_dereference(ns2->input);
>> + rcu_read_unlock();
>> +
>> + if (input)
>> + return 0;
>> +
>> + input = devm_input_allocate_device(&hdev->dev);
>> + if (!input)
>> + return -ENOMEM;
>> +
>> + input_set_drvdata(input, ns2);
>> + input->dev.parent = &hdev->dev;
>> + input->id.bustype = hdev->bus;
>> + input->id.vendor = hdev->vendor;
>> + input->id.product = hdev->product;
>> + input->id.version = hdev->version;
>> + input->uniq = ns2->serial;
>> + input->name = ns2->name;
>> + input->phys = hdev->phys;
>> +
>> + switch (ns2->ctlr_type) {
>> + case NS2_CTLR_TYPE_JCL:
>> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + switch2_config_buttons(input, ns2_left_joycon_button_mappings);
>> + break;
>> + case NS2_CTLR_TYPE_JCR:
>> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + switch2_config_buttons(input, ns2_right_joycon_button_mappings);
>> + break;
>> + case NS2_CTLR_TYPE_GC:
>> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128);
>> + input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128);
>> + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
>> + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
>> + switch2_config_buttons(input, ns2_gccon_mappings);
>> + break;
>> + case NS2_CTLR_TYPE_PRO:
>> + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
>> + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
>> + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
>> + switch2_config_buttons(input, ns2_procon_mappings);
>> + break;
>> + default:
>> + input_free_device(input);
>> + return -EINVAL;
>> + }
>> +
>> + hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major,
>> + ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
>> + if (ns2->version.dsp_type >= 0)
>> + hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major,
>> + ns2->version.dsp_minor, ns2->version.dsp_patch);
>> +
>> + ret = input_register_device(input);
>> + if (ret < 0) {
>> + hid_err(ns2->hdev, "Failed to register input; ret=%d\n", ret);
>> + return ret;
>> + }
>> +
>> + for (i = 0; i < JC_NUM_LEDS; i++) {
>> + struct led_classdev *led = &ns2->leds[i];
>> + char *name = devm_kasprintf(&input->dev, GFP_KERNEL, "%s:%s:%s",
>> + dev_name(&input->dev),
>> + "green",
>> + joycon_player_led_names[i]);
>> +
>> + if (!name)
>> + return -ENOMEM;
>> +
>> + led->name = name;
>> + ret = devm_led_classdev_register(&input->dev, led);
>> + if (ret < 0) {
>> + dev_err(&input->dev, "Failed to register player %d LED; ret=%d\n",
>> + i + 1, ret);
>> + input_unregister_device(input);
>> + return ret;
>> + }
>> + }
>> +
>> + rcu_assign_pointer(ns2->input, input);
>> + synchronize_rcu();
>> + return 0;
>> +}
>> +
>> +static struct switch2_controller *switch2_get_controller(const char *phys)
>> +{
>> + struct switch2_controller *ns2;
>> +
>> + guard(mutex)(&switch2_controllers_lock);
>> + list_for_each_entry(ns2, &switch2_controllers, entry) {
>> + if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0)
>> + return ns2;
>> + }
>> + ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
>> + if (!ns2)
>> + return ERR_PTR(-ENOMEM);
>> +
>> + mutex_init(&ns2->lock);
>> + INIT_LIST_HEAD(&ns2->entry);
>> + list_add(&ns2->entry, &switch2_controllers);
>> + strscpy(ns2->phys, phys, sizeof(ns2->phys));
>> + return ns2;
>> +}
>> +
>> +static void switch2_controller_put(struct switch2_controller *ns2)
>> +{
>> + struct input_dev *input;
>> + bool do_free;
>> +
>> + guard(mutex)(&switch2_controllers_lock);
>> + mutex_lock(&ns2->lock);
>> +
>> + rcu_read_lock();
>> + input = rcu_dereference(ns2->input);
>> + rcu_read_unlock();
>> +
>> + rcu_assign_pointer(ns2->input, NULL);
>> + synchronize_rcu();
>> +
>> + ns2->init_step = 0;
>> + do_free = !ns2->hdev && !ns2->cfg;
>> + mutex_unlock(&ns2->lock);
>> +
>> + if (input)
>> + input_unregister_device(input);
>> +
>> + if (do_free) {
>> + list_del_init(&ns2->entry);
>> + mutex_destroy(&ns2->lock);
>> + kfree(ns2);
>> + }
>> +}
>> +
>> +static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib,
>> + const uint8_t *data)
>> +{
>> + static const uint8_t UNCALIBRATED[9] = {
>> + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
>> + };
>> + if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0)
>> + return false;
>> +
>> + calib->x.neutral = data[0];
>> + calib->x.neutral |= (data[1] & 0x0F) << 8;
>> +
>> + calib->y.neutral = data[1] >> 4;
>> + calib->y.neutral |= data[2] << 4;
>> +
>> + calib->x.positive = data[3];
>> + calib->x.positive |= (data[4] & 0x0F) << 8;
>> +
>> + calib->y.positive = data[4] >> 4;
>> + calib->y.positive |= data[5] << 4;
>> +
>> + calib->x.negative = data[6];
>> + calib->x.negative |= (data[7] & 0x0F) << 8;
>> +
>> + calib->y.negative = data[7] >> 4;
>> + calib->y.negative |= data[8] << 4;
>> +
>> + return true;
>> +}
>> +
>> +static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size,
>> + uint32_t address, const uint8_t *data)
>> +{
>> + bool ok;
>> +
>> + switch (address) {
>> + case NS2_FLASH_ADDR_SERIAL:
>> + if (size != NS2_FLASH_SIZE_SERIAL)
>> + return;
>> + memcpy(ns2->serial, data, size);
>> + switch2_init_step_done(ns2, NS2_INIT_READ_SERIAL);
>> + break;
>> + case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB:
>> + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
>> + return;
>> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_PRIMARY_CALIB);
>> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data);
>> + if (ok) {
>> + hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
>> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
>> + ns2->stick_calib[0].x.negative,
>> + ns2->stick_calib[0].x.neutral,
>> + ns2->stick_calib[0].x.positive);
>> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
>> + ns2->stick_calib[0].y.negative,
>> + ns2->stick_calib[0].y.neutral,
>> + ns2->stick_calib[0].y.positive);
>> + } else {
>> + hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n");
>> + }
>> + break;
>> + case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB:
>> + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
>> + return;
>> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB);
>> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data);
>> + if (ok) {
>> + hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n");
>> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
>> + ns2->stick_calib[1].x.negative,
>> + ns2->stick_calib[1].x.neutral,
>> + ns2->stick_calib[1].x.positive);
>> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
>> + ns2->stick_calib[1].y.negative,
>> + ns2->stick_calib[1].y.neutral,
>> + ns2->stick_calib[1].y.positive);
>> + } else {
>> + hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n");
>> + }
>> + break;
>> + case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB:
>> + if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB)
>> + return;
>> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB);
>> + if (data[0] != 0xFF && data[1] != 0xFF) {
>> + ns2->lt_zero = data[0];
>> + ns2->rt_zero = data[1];
>> +
>> + hid_dbg(ns2->hdev, "Got factory trigger calibration:\n");
>> + hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero);
>> + hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero);
>> + } else {
>> + hid_dbg(ns2->hdev, "Factory trigger calibration not present\n");
>> + }
>> + break;
>> + case NS2_FLASH_ADDR_USER_PRIMARY_CALIB:
>> + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
>> + return;
>> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_PRIMARY_CALIB);
>> + if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
>> + hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
>> + break;
>> + }
>> +
>> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
>> + if (ok) {
>> + hid_dbg(ns2->hdev, "Got user primary stick calibration:\n");
>> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
>> + ns2->stick_calib[0].x.negative,
>> + ns2->stick_calib[0].x.neutral,
>> + ns2->stick_calib[0].x.positive);
>> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
>> + ns2->stick_calib[0].y.negative,
>> + ns2->stick_calib[0].y.neutral,
>> + ns2->stick_calib[0].y.positive);
>> + } else {
>> + hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
>> + }
>> + break;
>> + case NS2_FLASH_ADDR_USER_SECONDARY_CALIB:
>> + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
>> + return;
>> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB);
>> + if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
>> + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
>> + break;
>> + }
>> +
>> + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
>> + if (ok) {
>> + hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n");
>> + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
>> + ns2->stick_calib[1].x.negative,
>> + ns2->stick_calib[1].x.neutral,
>> + ns2->stick_calib[1].x.positive);
>> + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
>> + ns2->stick_calib[1].y.negative,
>> + ns2->stick_calib[1].y.neutral,
>> + ns2->stick_calib[1].y.positive);
>> + } else {
>> + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
>> + }
>> + break;
>> + }
>> +}
>> +
>> +static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes,
>> + const struct switch2_ctlr_button_mapping button_mappings[])
>> +{
>> + const struct switch2_ctlr_button_mapping *button;
>> +
>> + for (button = button_mappings; button->code; button++)
>> + input_report_key(input, button->code, bytes[button->byte] & button->bit);
>> +}
>> +
>> +static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib,
>> + int axis, bool invert, int value)
>> +{
>> + if (calib && calib->neutral && calib->negative && calib->positive) {
>> + value -= calib->neutral;
>> + value *= NS2_AXIS_MAX + 1;
>> + if (value < 0)
>> + value /= calib->negative;
>> + else
>> + value /= calib->positive;
>> + } else {
>> + value = (value - 2048) * 16;
>> + }
>> +
>> + if (invert)
>> + value = -value;
>> + input_report_abs(input, axis,
>> + clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX));
>> +}
>> +
>> +static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib,
>> + int x, bool invert_x, int y, bool invert_y, const uint8_t *data)
>> +{
>> + switch2_report_axis(input, &calib->x, x, invert_x, data[0] | ((data[1] & 0x0F) << 8));
>> + switch2_report_axis(input, &calib->y, y, invert_y, (data[1] >> 4) | (data[2] << 4));
>> +}
>> +
>> +static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data)
>> +{
>> + int value = (NS2_TRIGGER_RANGE + 1) * (data - zero) / (232 - zero);
>> +
>> + input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE));
>> +}
>> +
>> +static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data,
>> + int size)
>> +{
>> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
>> + struct input_dev *input;
>> +
>> + if (report->type != HID_INPUT_REPORT)
>> + return 0;
>> +
>> + if (size < 15)
>> + return -EINVAL;
>> +
>> + guard(rcu)();
>> + input = rcu_dereference(ns2->input);
>> +
>> + if (!input)
>> + return 0;
>> +
>> + switch (report->id) {
>> + case NS2_REPORT_UNIFIED:
>> + /*
>> + * TODO
>> + * This won't be sent unless the report type gets changed via command
>> + * 03-0A, but we should support it at some point regardless.
>> + */
>> + break;
>> + case NS2_REPORT_JCL:
>> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
>> + ABS_Y, true, &raw_data[6]);
>> + switch2_report_buttons(input, &raw_data[3], ns2_left_joycon_button_mappings);
>> + break;
>> + case NS2_REPORT_JCR:
>> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
>> + ABS_Y, true, &raw_data[6]);
>> + switch2_report_buttons(input, &raw_data[3], ns2_right_joycon_button_mappings);
>> + break;
>> + case NS2_REPORT_GC:
>> + input_report_abs(input, ABS_HAT0X,
>> + !!(raw_data[4] & NS2_BTNL_RIGHT) -
>> + !!(raw_data[4] & NS2_BTNL_LEFT));
>> + input_report_abs(input, ABS_HAT0Y,
>> + !!(raw_data[4] & NS2_BTNL_DOWN) -
>> + !!(raw_data[4] & NS2_BTNL_UP));
>> + switch2_report_buttons(input, &raw_data[3], ns2_gccon_mappings);
>> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
>> + ABS_Y, true, &raw_data[6]);
>> + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
>> + ABS_RY, true, &raw_data[9]);
>> + switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]);
>> + switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]);
>> + break;
>> + case NS2_REPORT_PRO:
>> + input_report_abs(input, ABS_HAT0X,
>> + !!(raw_data[4] & NS2_BTNL_RIGHT) -
>> + !!(raw_data[4] & NS2_BTNL_LEFT));
>> + input_report_abs(input, ABS_HAT0Y,
>> + !!(raw_data[4] & NS2_BTNL_DOWN) -
>> + !!(raw_data[4] & NS2_BTNL_UP));
>> + switch2_report_buttons(input, &raw_data[3], ns2_procon_mappings);
>> + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
>> + ABS_Y, true, &raw_data[6]);
>> + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
>> + ABS_RY, true, &raw_data[9]);
>> + break;
>> + default:
>> + return -EINVAL;
>> + }
>> +
>> + input_sync(input);
>> + return 0;
>> +}
>> +
>> +static int switch2_features_enable(struct switch2_controller *ns2, int features)
>> +{
>> + __le32 feature_bits = __cpu_to_le32(features);
>> +
>> + if (!ns2->cfg)
>> + return -ENOTCONN;
>> + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE,
>> + &feature_bits, sizeof(feature_bits),
>> + ns2->cfg);
>> +}
>> +
>> +static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address,
>> + uint8_t size)
>> +{
>> + uint8_t message[8] = { size, 0x7e };
>> +
>> + if (!ns2->cfg)
>> + return -ENOTCONN;
>> + *(__le32 *)&message[4] = __cpu_to_le32(address);
>> + return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message,
>> + sizeof(message), ns2->cfg);
>> +}
>> +
>> +static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id)
>> +{
>> + int i;
>> + int player_led_pattern = player_id % JC_NUM_LED_PATTERNS;
>> +
>> + for (i = 0; i < JC_NUM_LEDS; i++)
>> + ns2->leds[i].brightness = joycon_player_led_patterns[player_led_pattern][i];
>> +
>> + return switch2_set_leds(ns2);
>> +}
>> +
>> +static int switch2_set_report_format(struct switch2_controller *ns2, enum switch2_report_id fmt)
>> +{
>> + __le32 format_id = __cpu_to_le32(fmt);
>> +
>> + if (!ns2->cfg)
>> + return -ENOTCONN;
>> + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_SELECT_REPORT,
>> + &format_id, sizeof(format_id),
>> + ns2->cfg);
>> +}
>> +
>> +static int switch2_init_controller(struct switch2_controller *ns2)
>
> This is now a recursive call while in v1 it wasn't. I think I preferred
> the non-recursive version as there was one place where init_step
> state was changed while now I am not sure where it happens (and whether
> there is a code path where we end up in an infinite recursion)
>
> What is the advantage of the recursive version compared to the
> non-recursive one?
>
The old version incremented the step regardless of whether or not it could confirm it had happened. Since the confirmation is now handled with an external step, calling into switch2_init_step_done, the loop condition would become somewhat complicated. I replaced it with explicit tail calls since that make the control flow simplier, and it is always matched with a call to switch2_init_step_done to ensure that the state is always advanced. As such, the number recursive calls is explicitly limited, and the fact that they're tail calls should mean that it doesn't increase the stack depth (unless there's something I don't know about how the kernel is compiled, which is possible in the wake of things like retpoline protections). I suppose I could replace it with a loop, but the condition would be the same so the only real difference would be a `while (ns2->init_step < NS2_INIT_DONE)` instead of the tail calls; the tail calls themselves would just become break statements. There would be no functional difference.
>> +{
>> + if (ns2->init_step == NS2_INIT_DONE)
>> + return 0;
>> +
>> + if (!ns2->cfg)
>> + return -ENOTCONN;
>> +
>> + switch (ns2->init_step) {
>> + case NS2_INIT_READ_SERIAL:
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL,
>> + NS2_FLASH_SIZE_SERIAL);
>> + case NS2_INIT_GET_FIRMWARE_INFO:
>> + return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET,
>> + NULL, 0, ns2->cfg);
>> + case NS2_INIT_READ_FACTORY_PRIMARY_CALIB:
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB,
>> + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
>> + case NS2_INIT_READ_FACTORY_SECONDARY_CALIB:
>> + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
>> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB);
>> + return switch2_init_controller(ns2);
>> + }
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB,
>> + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
>> + case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
>> + if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
>> + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB);
>> + return switch2_init_controller(ns2);
>> + }
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
>> + NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);
>> + case NS2_INIT_READ_USER_PRIMARY_CALIB:
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB,
>> + NS2_FLASH_SIZE_USER_AXIS_CALIB);
>> + case NS2_INIT_READ_USER_SECONDARY_CALIB:
>> + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
>> + switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB);
>> + return switch2_init_controller(ns2);
>> + }
>> + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB,
>> + NS2_FLASH_SIZE_USER_AXIS_CALIB);
>> + case NS2_INIT_SET_FEATURE_MASK:
>> + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
>> + switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
>> + case NS2_INIT_ENABLE_FEATURES:
>> + return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG);
>> + case NS2_INIT_GRIP_BUTTONS:
>> + if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
>> + switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
>> + return switch2_init_controller(ns2);
>> + }
>> + return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS,
>> + switch2_one_data, sizeof(switch2_one_data),
>> + ns2->cfg);
>> + case NS2_INIT_REPORT_FORMAT:
>> + switch (ns2->ctlr_type) {
>> + case NS2_CTLR_TYPE_JCL:
>> + return switch2_set_report_format(ns2, NS2_REPORT_JCL);
>> + case NS2_CTLR_TYPE_JCR:
>> + return switch2_set_report_format(ns2, NS2_REPORT_JCR);
>> + case NS2_CTLR_TYPE_PRO:
>> + return switch2_set_report_format(ns2, NS2_REPORT_PRO);
>> + case NS2_CTLR_TYPE_GC:
>> + return switch2_set_report_format(ns2, NS2_REPORT_GC);
>> + default:
>> + switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT);
>> + return switch2_init_controller(ns2);
>> + }
>> + case NS2_INIT_SET_PLAYER_LEDS:
>> + return switch2_set_player_id(ns2, ns2->player_id);
>> + case NS2_INIT_INPUT:
>> + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
>> + switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
>> + case NS2_INIT_FINISH:
>> + if (ns2->hdev)
>
> If this is not set we skip the switch2_init_input call but don't error
> out. Is this intentional (are we expecting ns2->hdev to be populated at
> a later time and this step retried, perhaps)?
This is intentional, yes. There are a handful of places this can get called, including the function that sets ns2->hdev in the first place (switch2_probe). It's to ensure the steps start as soon as possible (when the cfg pointer gets set) but it can't finish until the hdev gets set, which can be either before or after, depending on the ordering the interfaces get enumerated.
>
>> + return switch2_init_input(ns2);
>> + break;
>> + default:
>> + WARN_ON_ONCE(1);
>> + break;
>> + }
>> + return 0;
>> +}
>> +
>> +int switch2_receive_command(struct switch2_controller *ns2,
>> + const uint8_t *message, size_t length)
>> +{
>> + const struct switch2_cmd_header *header;
>> + int ret = 0;
>> +
>> + if (length < 8)
>> + return -EINVAL;
>> +
>> + print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false);
>> +
>> + guard(mutex)(&ns2->lock);
>> +
>> + header = (const struct switch2_cmd_header *)message;
>> + if (!(header->flags & NS2_FLAG_OK)) {
>> + ret = -EIO;
>> + goto exit;
>> + }
>> + message = &message[8];
>> + switch (header->command) {
>> + case NS2_CMD_FLASH:
>> + if (header->subcommand == NS2_SUBCMD_FLASH_READ) {
>> + uint8_t read_size;
>> + uint32_t read_address;
>> +
>> + if (length < 16) {
>> + ret = -EINVAL;
>> + goto exit;
>> + }
>> + read_size = message[0];
>> + read_address = __le32_to_cpu(*(__le32 *)&message[4]);
>> + if (length < read_size + 16) {
>> + ret = -EINVAL;
>> + goto exit;
>> + }
>> + switch2_handle_flash_read(ns2, read_size, read_address, &message[8]);
>> + }
>> + break;
>> + case NS2_CMD_INIT:
>> + if (header->subcommand == NS2_SUBCMD_INIT_USB)
>> + switch2_init_step_done(ns2, NS2_INIT_INPUT);
>> + else if (header->subcommand == NS2_SUBCMD_INIT_SELECT_REPORT)
>> + switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT);
>> + break;
>> + case NS2_CMD_GRIP:
>> + if (header->subcommand == NS2_SUBCMD_GRIP_ENABLE_BUTTONS)
>> + switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
>> + break;
>> + case NS2_CMD_LED:
>> + if (header->subcommand == NS2_SUBCMD_LED_PATTERN)
>> + switch2_init_step_done(ns2, NS2_INIT_SET_PLAYER_LEDS);
>> + break;
>> + case NS2_CMD_FEATSEL:
>> + if (header->subcommand == NS2_SUBCMD_FEATSEL_SET_MASK)
>> + switch2_init_step_done(ns2, NS2_INIT_SET_FEATURE_MASK);
>> + else if (header->subcommand == NS2_SUBCMD_FEATSEL_ENABLE)
>> + switch2_init_step_done(ns2, NS2_INIT_ENABLE_FEATURES);
>> + break;
>> + case NS2_CMD_FW_INFO:
>> + if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) {
>> + if (length < sizeof(ns2->version)) {
>> + ret = -EINVAL;
>> + goto exit;
>> + }
>> + memcpy(&ns2->version, message, sizeof(ns2->version));
>> + ns2->ctlr_type = ns2->version.ctlr_type;
>> + switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
>> + }
>> + break;
>> + default:
>> + break;
>> + }
>> +
>> +exit:
>> + if (ns2->init_step < NS2_INIT_DONE)
>> + switch2_init_controller(ns2);
>> +
>> + return ret;
>> +}
>> +EXPORT_SYMBOL_GPL(switch2_receive_command);
>> +
>> +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg)
>> +{
>> + struct switch2_controller *ns2 = switch2_get_controller(phys);
>> +
>> + if (IS_ERR(ns2))
>> + return PTR_ERR(ns2);
>> +
>> + cfg->parent = ns2;
>> +
>> + guard(mutex)(&ns2->lock);
>> + WARN_ON(ns2->cfg);
>> + ns2->cfg = cfg;
>> +
>> + if (ns2->hdev)
>> + return switch2_init_controller(ns2);
>> + return 0;
>> +}
>> +EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg);
>> +
>> +void switch2_controller_detach_cfg(struct switch2_controller *ns2)
>> +{
>> + mutex_lock(&ns2->lock);
>> + WARN_ON(ns2 != ns2->cfg->parent);
>> + ns2->cfg = NULL;
>> + mutex_unlock(&ns2->lock);
>> + switch2_controller_put(ns2);
>> +}
>> +EXPORT_SYMBOL_GPL(switch2_controller_detach_cfg);
>> +
>> +static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
>> +{
>> + struct switch2_controller *ns2;
>> + struct usb_device *udev;
>> + char phys[64];
>> + int ret;
>> +
>> + if (!hid_is_usb(hdev))
>> + return -ENODEV;
>> +
>> + udev = hid_to_usb_dev(hdev);
>> + if (usb_make_path(udev, phys, sizeof(phys)) < 0)
>> + return -EINVAL;
>> +
>> + ret = hid_parse(hdev);
>> + if (ret) {
>> + hid_err(hdev, "parse failed %d\n", ret);
>> + return ret;
>> + }
>> +
>> + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
>> + if (ret) {
>> + hid_err(hdev, "hw_start failed %d\n", ret);
>> + return ret;
>> + }
>> +
>> + ret = hid_hw_open(hdev);
>> + if (ret) {
>> + hid_err(hdev, "hw_open failed %d\n", ret);
>> + goto err_stop;
>> + }
>
> For the Switch 1 controllers we are calling hid_device_io_start after
> hid_hw_open. Is this not necessary in this case?
Since we don't do any HID I/O during probe it's not needed; the startup configuration is on the cfg interface instead.
>
>> +
>> + ns2 = switch2_get_controller(phys);
>> + if (!ns2) {
>
> switch2_get_controller returns an err pointer in case of ENOMEM, not
> NULL so I think this check has to be changed.
Fixed locally, thanks. I'll send that out with v4 (this was actually v3 but I thought it was v2, oops).
>
>> + ret = -ENOMEM;
>> + goto err_close;
>> + }
>> +
>> + guard(mutex)(&ns2->lock);
>> + WARN_ON(ns2->hdev);
>> + ns2->hdev = hdev;
>> + switch (hdev->product | (hdev->vendor << 16)) {
>> + default:
>> + strscpy(ns2->name, hdev->name, sizeof(ns2->name));
>> + break;
>> + /* Some controllers have slightly wrong names so we override them */
>> + case USB_DEVICE_ID_NINTENDO_NS2_JOYCONR | (USB_VENDOR_ID_NINTENDO << 16):
>> + /* Missing the "2" in the name */
>> + strscpy(ns2->name, "Nintendo Joy-Con 2 (R)", sizeof(ns2->name));
>> + break;
>> + case USB_DEVICE_ID_NINTENDO_NS2_GCCON | (USB_VENDOR_ID_NINTENDO << 16):
>> + /* Has "Nintendo" in the name twice */
>> + strscpy(ns2->name, "Nintendo GameCube Controller", sizeof(ns2->name));
>> + break;
>> + }
>> +
>> + ns2->player_id = U32_MAX;
>> + ret = ida_alloc(&nintendo_player_id_allocator, GFP_KERNEL);
>> + if (ret < 0)
>> + hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret);
>> + else
>> + ns2->player_id = ret;
>> +
>> + switch2_leds_create(ns2);
>> +
>> + hid_set_drvdata(hdev, ns2);
>> +
>> + if (ns2->cfg)
>> + return switch2_init_controller(ns2);
>> +
>> + return 0;
>> +
>> +err_close:
>> + hid_hw_close(hdev);
>> +err_stop:
>> + hid_hw_stop(hdev);
>> +
>> + return ret;
>> +}
>> +
>> +static void switch2_remove(struct hid_device *hdev)
>> +{
>> + struct switch2_controller *ns2 = hid_get_drvdata(hdev);
>> +
>> + hid_hw_close(hdev);
>> + mutex_lock(&ns2->lock);
>> + WARN_ON(ns2->hdev != hdev);
>> + ns2->hdev = NULL;
>> + mutex_unlock(&ns2->lock);
>> + ida_free(&nintendo_player_id_allocator, ns2->player_id);
>> + switch2_controller_put(ns2);
>> + hid_hw_stop(hdev);
>> +}
>> +
>> static const struct hid_device_id nintendo_hid_devices[] = {
>> + /* Switch devices */
>> { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> USB_DEVICE_ID_NINTENDO_PROCON) },
>> { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> @@ -2813,10 +3935,69 @@ static const struct hid_device_id nintendo_hid_devices[] = {
>> USB_DEVICE_ID_NINTENDO_GENCON) },
>> { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_NINTENDO,
>> USB_DEVICE_ID_NINTENDO_N64CON) },
>> + /* Switch 2 devices */
>> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> + USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
>> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> + USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
>> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> + USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
>> + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
>> + USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
>> { }
>> };
>> MODULE_DEVICE_TABLE(hid, nintendo_hid_devices);
>>
>> +static bool nintendo_is_switch2(struct hid_device *hdev)
>> +{
>> + return hdev->vendor == USB_VENDOR_ID_NINTENDO &&
>> + hdev->product >= USB_DEVICE_ID_NINTENDO_NS2_JOYCONR;
>> +}
>> +
>> +static void nintendo_hid_remove(struct hid_device *hdev)
>> +{
>> + if (nintendo_is_switch2(hdev))
>> + switch2_remove(hdev);
>> + else
>> + joycon_remove(hdev);
>> +}
>> +
>> +static int nintendo_hid_event(struct hid_device *hdev,
>> + struct hid_report *report, u8 *raw_data, int size)
>> +{
>> + if (nintendo_is_switch2(hdev))
>> + return switch2_event(hdev, report, raw_data, size);
>> + else
>> + return joycon_event(hdev, report, raw_data, size);
>> +}
>> +
>> +static int nintendo_hid_probe(struct hid_device *hdev,
>> + const struct hid_device_id *id)
>> +{
>> + if (nintendo_is_switch2(hdev))
>> + return switch2_probe(hdev, id);
>> + else
>> + return joycon_probe(hdev, id);
>> +}
>> +
>> +#ifdef CONFIG_PM
>> +static int nintendo_hid_resume(struct hid_device *hdev)
>> +{
>> + if (nintendo_is_switch2(hdev))
>> + return 0;
>> + else
>> + return joycon_resume(hdev);
>> +}
>> +
>> +static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
>> +{
>> + if (nintendo_is_switch2(hdev))
>> + return 0;
>> + else
>> + return joycon_suspend(hdev, message);
>> +}
>> +#endif
>> +
>> static struct hid_driver nintendo_hid_driver = {
>> .name = "nintendo",
>> .id_table = nintendo_hid_devices,
>> @@ -2844,4 +4025,5 @@ MODULE_LICENSE("GPL");
>> MODULE_AUTHOR("Ryan McClelland <rymcclel@gmail.com>");
>> MODULE_AUTHOR("Emily Strickland <linux@emily.st>");
>> MODULE_AUTHOR("Daniel J. Ogorchock <djogorchock@gmail.com>");
>> +MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
>> MODULE_DESCRIPTION("Driver for Nintendo Switch Controllers");
>> diff --git a/drivers/hid/hid-nintendo.h b/drivers/hid/hid-nintendo.h
>> new file mode 100644
>> index 0000000000000..7aff22f302661
>> --- /dev/null
>> +++ b/drivers/hid/hid-nintendo.h
>> @@ -0,0 +1,72 @@
>> +/* SPDX-License-Identifier: GPL-2.0+ */
>> +/*
>> + * HID driver for Nintendo Switch 2 controllers
>> + *
>> + * Copyright (c) 2025 Valve Software
>> + *
>> + * This driver is based on the following work:
>> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
>> + * https://github.com/ndeadly/switch2_controller_research
>> + */
>> +
>> +#ifndef __HID_NINTENDO_H
>> +#define __HID_NINTENDO_H
>> +
>> +#include <linux/bits.h>
>> +
>> +#define NS2_FLAG_OK BIT(0)
>> +#define NS2_FLAG_NACK BIT(2)
>> +
>> +enum switch2_cmd {
>> + NS2_CMD_NFC = 0x01,
>> + NS2_CMD_FLASH = 0x02,
>> + NS2_CMD_INIT = 0x03,
>> + NS2_CMD_GRIP = 0x08,
>> + NS2_CMD_LED = 0x09,
>> + NS2_CMD_VIBRATE = 0x0a,
>> + NS2_CMD_BATTERY = 0x0b,
>> + NS2_CMD_FEATSEL = 0x0c,
>> + NS2_CMD_FW_UPD = 0x0d,
>> + NS2_CMD_FW_INFO = 0x10,
>> + NS2_CMD_BT_PAIR = 0x15,
>> +};
>> +
>> +enum switch2_direction {
>> + NS2_DIR_IN = 0x00,
>> + NS2_DIR_OUT = 0x90,
>> +};
>> +
>> +enum switch2_transport {
>> + NS2_TRANS_USB = 0x00,
>> + NS2_TRANS_BT = 0x01,
>> +};
>> +
>> +struct switch2_cmd_header {
>> + uint8_t command;
>> + uint8_t flags;
>> + uint8_t transport;
>> + uint8_t subcommand;
>> + uint8_t unk1;
>> + uint8_t length;
>> + uint16_t unk2;
>> +};
>> +static_assert(sizeof(struct switch2_cmd_header) == 8);
>> +
>> +struct device;
>> +struct switch2_controller;
>> +struct switch2_cfg_intf {
>> + struct switch2_controller *parent;
>> + struct device *dev;
>> +
>> + int (*send_command)(enum switch2_cmd command, uint8_t subcommand,
>> + const void *message, size_t length,
>> + struct switch2_cfg_intf *intf);
>> +};
>> +
>> +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg);
>> +void switch2_controller_detach_cfg(struct switch2_controller *controller);
>> +
>> +int switch2_receive_command(struct switch2_controller *controller,
>> + const uint8_t *message, size_t length);
>> +
>> +#endif
>> diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
>> index 7755e5b454d2c..868262c6ccd9a 100644
>> --- a/drivers/input/joystick/Kconfig
>> +++ b/drivers/input/joystick/Kconfig
>> @@ -422,4 +422,15 @@ config JOYSTICK_SEESAW
>> To compile this driver as a module, choose M here: the module will be
>> called adafruit-seesaw.
>>
>> +config JOYSTICK_NINTENDO_SWITCH2_USB
>> + tristate "Wired Nintendo Switch 2 controller support"
>> + depends on HID_NINTENDO
>> + depends on USB
>> + help
>> + Say Y here if you want to enable support for wired Nintendo Switch 2
>> + controllers.
>> +
>> + To compile this driver as a module, choose M here: the
>> + module will be called nintendo-switch2-usb.
>> +
>> endif
>> diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
>> index 9976f596a9208..8f92900ae8856 100644
>> --- a/drivers/input/joystick/Makefile
>> +++ b/drivers/input/joystick/Makefile
>> @@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER) += sidewinder.o
>> obj-$(CONFIG_JOYSTICK_SPACEBALL) += spaceball.o
>> obj-$(CONFIG_JOYSTICK_SPACEORB) += spaceorb.o
>> obj-$(CONFIG_JOYSTICK_STINGER) += stinger.o
>> +obj-$(CONFIG_JOYSTICK_NINTENDO_SWITCH2_USB) += nintendo-switch2-usb.o
>> obj-$(CONFIG_JOYSTICK_TMDC) += tmdc.o
>> obj-$(CONFIG_JOYSTICK_TURBOGRAFX) += turbografx.o
>> obj-$(CONFIG_JOYSTICK_TWIDJOY) += twidjoy.o
>> diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
>> new file mode 100644
>> index 0000000000000..ebd89d852e21a
>> --- /dev/null
>> +++ b/drivers/input/joystick/nintendo-switch2-usb.c
>> @@ -0,0 +1,353 @@
>> +// SPDX-License-Identifier: GPL-2.0+
>> +/*
>> + * USB driver for Nintendo Switch 2 controllers configuration interface
>> + *
>> + * Copyright (c) 2025 Valve Software
>> + *
>> + * This driver is based on the following work:
>> + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
>> + * https://github.com/ndeadly/switch2_controller_research
>> + */
>> +
>> +#include "../../hid/hid-ids.h"
>> +#include "../../hid/hid-nintendo.h"
>> +#include <linux/module.h>
>> +#include <linux/usb/input.h>
>> +
>> +#define NS2_BULK_SIZE 64
>> +#define NS2_IN_URBS 2
>> +#define NS2_OUT_URBS 4
>> +
>> +static struct usb_driver switch2_usb;
>> +
>> +struct switch2_urb {
>> + struct urb *urb;
>> + uint8_t *data;
>> + bool active;
>> +};
>> +
>> +struct switch2_usb {
>> + struct switch2_cfg_intf cfg;
>> + struct usb_device *udev;
>> +
>> + struct switch2_urb bulk_in[NS2_IN_URBS];
>> + struct usb_anchor bulk_in_anchor;
>> + spinlock_t bulk_in_lock;
>> +
>> + struct switch2_urb bulk_out[NS2_OUT_URBS];
>> + struct usb_anchor bulk_out_anchor;
>> + spinlock_t bulk_out_lock;
>> +
>> + int message_in;
>> + struct work_struct message_in_work;
>> +};
>> +
>> +static void switch2_bulk_in(struct urb *urb)
>> +{
>> + struct switch2_usb *ns2_usb = urb->context;
>> + int i;
>> + bool schedule = false;
>> + unsigned long flags;
>> +
>> + switch (urb->status) {
>> + case 0:
>> + schedule = true;
>> + break;
>> + case -ECONNRESET:
>> + case -ENOENT:
>> + case -ESHUTDOWN:
>> + dev_dbg(&ns2_usb->udev->dev, "shutting down input urb: %d\n", urb->status);
>> + return;
>> + default:
>> + dev_dbg(&ns2_usb->udev->dev, "unknown input urb status: %d\n", urb->status);
>> + break;
>> + }
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
>> + for (i = 0; i < NS2_IN_URBS; i++) {
>> + int err;
>> + struct switch2_urb *ns2_urb;
>> +
>> + if (ns2_usb->bulk_in[i].urb == urb) {
>> + ns2_usb->message_in = i;
>> + continue;
>> + }
>> +
>> + if (ns2_usb->bulk_in[i].active)
>> + continue;
>> +
>> + ns2_urb = &ns2_usb->bulk_in[i];
>> + usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor);
>> + err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
>> + if (err) {
>> + usb_unanchor_urb(ns2_urb->urb);
>> + dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
>> + } else {
>> + ns2_urb->active = true;
>> + }
>> + }
>> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
>> +
>> + if (schedule)
>> + schedule_work(&ns2_usb->message_in_work);
>> +}
>> +
>> +static void switch2_bulk_out(struct urb *urb)
>> +{
>> + struct switch2_usb *ns2_usb = urb->context;
>> + int i;
>> +
>> + guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock);
>> +
>> + switch (urb->status) {
>> + case 0:
>> + break;
>> + case -ECONNRESET:
>> + case -ENOENT:
>> + case -ESHUTDOWN:
>> + dev_dbg(&ns2_usb->udev->dev, "shutting down output urb: %d\n", urb->status);
>> + return;
>> + default:
>> + dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
>> + return;
>> + }
>> +
>> + for (i = 0; i < NS2_OUT_URBS; i++) {
>> + if (ns2_usb->bulk_out[i].urb != urb)
>> + continue;
>> +
>> + ns2_usb->bulk_out[i].active = false;
>> + break;
>> + }
>> +}
>> +
>> +static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand,
>> + const void *message, size_t size, struct switch2_cfg_intf *cfg)
>> +{
>> + struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg;
>> + struct switch2_urb *urb = NULL;
>> + int i;
>> + int ret;
>> + unsigned long flags;
>> +
>> + struct switch2_cmd_header header = {
>> + command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size
>> + };
>> +
>> + if (WARN_ON(size > 56))
>> + return -EINVAL;
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
>> + for (i = 0; i < NS2_OUT_URBS; i++) {
>> + if (ns2_usb->bulk_out[i].active)
>> + continue;
>> +
>> + urb = &ns2_usb->bulk_out[i];
>> + urb->active = true;
>> + break;
>> + }
>> + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
>> +
>> + if (!urb) {
>> + dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n");
>> + return -ENOBUFS;
>> + }
>> +
>> + memcpy(urb->data, &header, sizeof(header));
>> + if (message && size)
>> + memcpy(&urb->data[8], message, size);
>> + urb->urb->transfer_buffer_length = size + sizeof(header);
>> +
>> + print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data,
>> + size + sizeof(header), false);
>> +
>> + usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor);
>> + ret = usb_submit_urb(urb->urb, GFP_ATOMIC);
>> + if (ret) {
>> + if (ret != -ENODEV)
>> + dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret);
>> + urb->active = false;
>> + usb_unanchor_urb(urb->urb);
>> + return ret;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +static void switch2_usb_message_in_work(struct work_struct *work)
>> +{
>> + struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work);
>> + struct switch2_urb *urb;
>> + int err;
>> + unsigned long flags;
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
>> + urb = &ns2_usb->bulk_in[ns2_usb->message_in];
>> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
>> +
>> + err = switch2_receive_command(ns2_usb->cfg.parent, urb->urb->transfer_buffer,
>> + urb->urb->actual_length);
>> + if (err)
>> + dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err);
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
>> + urb->active = false;
>> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
>> +}
>> +
>> +static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
>> +{
>> + struct switch2_usb *ns2_usb;
>> + struct usb_device *udev;
>> + struct usb_endpoint_descriptor *bulk_in, *bulk_out;
>> + char phys[64];
>> + int ret;
>> + int i;
>> +
>> + udev = interface_to_usbdev(intf);
>> + if (usb_make_path(udev, phys, sizeof(phys)) < 0)
>> + return -EINVAL;
>> +
>> + ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL);
>> + if (ret) {
>> + dev_err(&intf->dev, "failed to find bulk EPs\n");
>> + return ret;
>> + }
>> +
>> + ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL);
>> + if (!ns2_usb)
>> + return -ENOMEM;
>> +
>> + ns2_usb->udev = udev;
>> + for (i = 0; i < NS2_IN_URBS; i++) {
>> + ns2_usb->bulk_in[i].urb = usb_alloc_urb(0, GFP_KERNEL);
>> + if (!ns2_usb->bulk_in[i].urb) {
>> + ret = -ENOMEM;
>> + goto err_free_in;
>> + }
>> +
>> + ns2_usb->bulk_in[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
>> + &ns2_usb->bulk_in[i].urb->transfer_dma);
>> + if (!ns2_usb->bulk_in[i].data) {
>> + ret = -ENOMEM;
>> + goto err_free_in;
>> + }
>> +
>> + usb_fill_bulk_urb(ns2_usb->bulk_in[i].urb, udev,
>> + usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
>> + ns2_usb->bulk_in[i].data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
>> + ns2_usb->bulk_in[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
>> + }
>> +
>> + for (i = 0; i < NS2_OUT_URBS; i++) {
>> + ns2_usb->bulk_out[i].urb = usb_alloc_urb(0, GFP_KERNEL);
>> + if (!ns2_usb->bulk_out[i].urb) {
>> + ret = -ENOMEM;
>> + goto err_free_out;
>> + }
>> +
>> + ns2_usb->bulk_out[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
>> + &ns2_usb->bulk_out[i].urb->transfer_dma);
>> + if (!ns2_usb->bulk_out[i].data) {
>> + ret = -ENOMEM;
>> + goto err_free_out;
>> + }
>> +
>> + usb_fill_bulk_urb(ns2_usb->bulk_out[i].urb, udev,
>> + usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
>> + ns2_usb->bulk_out[i].data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
>> + ns2_usb->bulk_out[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
>> + }
>> +
>> + ns2_usb->bulk_in[0].active = true;
>> + ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
>> + if (ret < 0)
>> + goto err_free_out;
>> +
>> + init_usb_anchor(&ns2_usb->bulk_out_anchor);
>> + spin_lock_init(&ns2_usb->bulk_out_lock);
>> + init_usb_anchor(&ns2_usb->bulk_in_anchor);
>> + spin_lock_init(&ns2_usb->bulk_in_lock);
>> + INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work);
>> +
>> + usb_set_intfdata(intf, ns2_usb);
>> +
>> + ns2_usb->cfg.dev = &ns2_usb->udev->dev;
>> + ns2_usb->cfg.send_command = switch2_usb_send_cmd;
>> +
>> + ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
>> + if (ret < 0)
>> + goto err_kill_urb;
>> +
>> + return 0;
>> +
>> +err_kill_urb:
>> + usb_kill_urb(ns2_usb->bulk_in[0].urb);
>> +err_free_out:
>> + for (i = 0; i < NS2_OUT_URBS; i++) {
>> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
>> + ns2_usb->bulk_out[i].urb->transfer_dma);
>> + usb_free_urb(ns2_usb->bulk_out[i].urb);
>> + }
>> +err_free_in:
>> + for (i = 0; i < NS2_IN_URBS; i++) {
>> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
>> + ns2_usb->bulk_in[i].urb->transfer_dma);
>> + usb_free_urb(ns2_usb->bulk_in[i].urb);
>> + }
>> + devm_kfree(&intf->dev, ns2_usb);
>> +
>> + return ret;
>> +}
>> +
>> +static void switch2_usb_disconnect(struct usb_interface *intf)
>> +{
>> + struct switch2_usb *ns2_usb = usb_get_intfdata(intf);
>> + unsigned long flags;
>> + int i;
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
>> + usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
>> + for (i = 0; i < NS2_OUT_URBS; i++) {
>> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
>> + ns2_usb->bulk_out[i].urb->transfer_dma);
>> + usb_free_urb(ns2_usb->bulk_out[i].urb);
>> + }
>> + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
>> +
>> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
>> + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
>> + cancel_work_sync(&ns2_usb->message_in_work);
>> + for (i = 0; i < NS2_IN_URBS; i++) {
>> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
>> + ns2_usb->bulk_in[i].urb->transfer_dma);
>> + usb_free_urb(ns2_usb->bulk_in[i].urb);
>> + }
>> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
>> +
>> + switch2_controller_detach_cfg(ns2_usb->cfg.parent);
>
> As we have allocated ns2_usb with devm_kzalloc, don't we need to free it
> with devm_kfree again?
Devres will automatically free it when the device is freed. As this is the callback for the device going away, it's freed directly after this function exits. That's why I'm using devm_kzalloc instead of kzalloc.
>
> Cheers,
> Silvan
>
>> +}
>> +
>> +#define SWITCH2_CONTROLLER(vend, prod) \
>> + USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0)
>> +
>> +static const struct usb_device_id switch2_usb_devices[] = {
>> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
>> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
>> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
>> + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
>> + { }
>> +};
>> +MODULE_DEVICE_TABLE(usb, switch2_usb_devices);
>> +
>> +static struct usb_driver switch2_usb = {
>> + .name = "switch2",
>> + .id_table = switch2_usb_devices,
>> + .probe = switch2_usb_probe,
>> + .disconnect = switch2_usb_disconnect,
>> +};
>> +module_usb_driver(switch2_usb);
>> +
>> +MODULE_LICENSE("GPL");
>> +MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
>> +MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
>
>
Vicki
^ permalink raw reply
* Re: [PATCH v3] dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
From: Bui Duc Phuc @ 2026-04-03 3:35 UTC (permalink / raw)
To: Rob Herring
Cc: conor+dt, krzk+dt, conor, devicetree, dmitry.torokhov, krzk,
linux-input, linux-kernel, marex, mingo, tglx
In-Reply-To: <20260326141551.GA2304345-robh@kernel.org>
Hi Rob, Krzysztof, Conor,
Thanks for the discussion.
The core schema already defines "wakeup-source" as a common property
(using oneOf for boolean or phandle-array), so defining the type here is
redundant.
https://github.com/devicetree-org/dt-schema/blob/main/dtschema/schemas/wakeup-source.yaml
I'll update it to "wakeup-source: true" in v4.
Best regards,
Phuc
On Thu, Mar 26, 2026 at 9:15 PM Rob Herring <robh@kernel.org> wrote:
>
> On Wed, Mar 18, 2026 at 03:31:24PM +0700, phucduc.bui@gmail.com wrote:
> > From: bui duc phuc <phucduc.bui@gmail.com>
> >
> > Document the "wakeup-source" property for the ti,tsc2005 touchscreen
> > controllers to allow the device to wake the system from suspend.
> >
> > Signed-off-by: bui duc phuc <phucduc.bui@gmail.com>
> > ---
> >
> > changes:
> > v3: Remove blank lines
> > v2: Revise the commit content and remove patch1 related to I2C and SPI
> > wakeup handling
> >
> > .../devicetree/bindings/input/touchscreen/ti,tsc2005.yaml | 5 +++++
> > 1 file changed, 5 insertions(+)
> >
> > diff --git a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
> > index 7187c390b2f5..a9842509c1fe 100644
> > --- a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
> > +++ b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
> > @@ -55,6 +55,9 @@ properties:
> > touchscreen-size-x: true
> > touchscreen-size-y: true
> >
> > + wakeup-source:
> > + type: boolean
>
> wakeup-source already has a defined type.
>
> wakeup-source: true
>
> > +
> > allOf:
> > - $ref: touchscreen.yaml#
> > - if:
> > @@ -97,6 +100,7 @@ examples:
> >
> > ti,x-plate-ohms = <280>;
> > ti,esd-recovery-timeout-ms = <8000>;
> > + wakeup-source;
> > };
> > };
> > - |
> > @@ -124,5 +128,6 @@ examples:
> >
> > ti,x-plate-ohms = <280>;
> > ti,esd-recovery-timeout-ms = <8000>;
> > + wakeup-source;
> > };
> > };
> > --
> > 2.43.0
> >
^ permalink raw reply
* [PATCH v4] dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
From: phucduc.bui @ 2026-04-03 4:07 UTC (permalink / raw)
To: robh
Cc: conor+dt, conor, devicetree, dmitry.torokhov, krzk+dt, krzk,
linux-input, linux-kernel, marex, mingo, phucduc.bui, tglx
From: bui duc phuc <phucduc.bui@gmail.com>
Document the "wakeup-source" property for the ti,tsc2005 touchscreen
controllers to allow the device to wake the system from suspend.
Signed-off-by: bui duc phuc <phucduc.bui@gmail.com>
---
changes:
v4: Drop redundant "type: boolean" for wakeup-source to use the core
definition from dt-schema (as suggested by Rob Herring).
v3: Remove blank lines (suggested by Conor).
v2: Revise the commit content and remove patch1 related to I2C and SPI
wakeup handling
.../devicetree/bindings/input/touchscreen/ti,tsc2005.yaml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
index 7187c390b2f5..c06a85b74533 100644
--- a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
+++ b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
@@ -55,6 +55,8 @@ properties:
touchscreen-size-x: true
touchscreen-size-y: true
+ wakeup-source: true
+
allOf:
- $ref: touchscreen.yaml#
- if:
@@ -97,6 +99,7 @@ examples:
ti,x-plate-ohms = <280>;
ti,esd-recovery-timeout-ms = <8000>;
+ wakeup-source;
};
};
- |
@@ -124,5 +127,6 @@ examples:
ti,x-plate-ohms = <280>;
ti,esd-recovery-timeout-ms = <8000>;
+ wakeup-source;
};
};
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v4 3/4] Input: aw86938 - add driver for Awinic AW86938
From: Luca Weiss @ 2026-04-03 8:23 UTC (permalink / raw)
To: Dmitry Torokhov, Luca Weiss
Cc: Griffin Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Bjorn Andersson, Konrad Dybcio, linux-input,
devicetree, linux-kernel, linux-arm-msm
In-Reply-To: <ac1DclNOl3ZA5bUg@google.com>
On Wed Apr 1, 2026 at 6:11 PM CEST, Dmitry Torokhov wrote:
> On Wed, Apr 01, 2026 at 04:44:47PM +0200, Luca Weiss wrote:
>> Hi Dmitry,
>>
>> On Wed Mar 4, 2026 at 5:56 AM CET, Dmitry Torokhov wrote:
>> > On Mon, Mar 02, 2026 at 11:50:27AM +0100, Griffin Kroah-Hartman wrote:
>> >> Add support for the I2C-connected Awinic AW86938 LRA haptic driver.
>> >>
>> >> The AW86938 has a similar but slightly different register layout. In
>> >> particular, the boost mode register values.
>> >> The AW86938 also has some extra features that aren't implemented
>> >> in this driver yet.
>> >>
>> >> Signed-off-by: Griffin Kroah-Hartman <griffin.kroah@fairphone.com>
>> >
>> > Applied, thank you.
>>
>> I'm curious, where did you apply these patches? linux-next doesn't have
>> it and I don't see it in your kernel.org repo either.
>> https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git/
>>
>> Did this slip through the cracks or will these patches still appear
>> there?
>
> My bad, I think there was a conflict with Dan Carpenter's patch and as a
> result the series got stuck in my internal queue. My apologies.
>
> Should be out in 'next' branch now.
>
> Thanks.
Thanks, appreciate it!
Regards
Luca
^ permalink raw reply
* [PATCH 0/2] HID: multitouch: Add support for Dell Pro Rugged 12 Tablet RA02260
From: buingoc67 @ 2026-04-03 9:28 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, hmtheboy154
From: hmtheboy154 <buingoc67@gmail.com>
This patch series adds multitouch and stylus support for the eGalax
I2C touchscreen found in the Dell Pro Rugged 12 Tablet RA02260.
hmtheboy154 (2):
HID: multitouch: rename MT_CLS_EGALAX_P80H84 to MT_CLS_EGALAX_WIN8
HID: multitouch: add support for Dell Pro Rugged 12 Tablet RA02260
drivers/hid/hid-ids.h | 1 +
drivers/hid/hid-multitouch.c | 10 +++++++---
2 files changed, 8 insertions(+), 3 deletions(-)
--
2.53.0
^ permalink raw reply
* [PATCH 1/2] HID: multitouch: rename MT_CLS_EGALAX_P80H84 to MT_CLS_EGALAX_WIN8
From: buingoc67 @ 2026-04-03 9:28 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, hmtheboy154
In-Reply-To: <20260403092848.10223-1-buingoc67@gmail.com>
From: hmtheboy154 <buingoc67@gmail.com>
The Dell Pro Rugged 12 Tablet RA02260 uses an i2c touchscreen from
eGalax that exposes the same HID_GROUP_MULTITOUCH_WIN_8 as the
P80H84 and also requires the same quirks to make multitouch work.
Rename the class to MT_CLS_EGALAX_WIN8 for more general use.
Signed-off-by: hmtheboy154 <buingoc67@gmail.com>
---
drivers/hid/hid-multitouch.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index e82a3c4e5b44..9e46028746fd 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -235,7 +235,7 @@ static void mt_post_parse(struct mt_device *td, struct mt_application *app);
#define MT_CLS_SMART_TECH 0x0113
#define MT_CLS_APPLE_TOUCHBAR 0x0114
#define MT_CLS_YOGABOOK9I 0x0115
-#define MT_CLS_EGALAX_P80H84 0x0116
+#define MT_CLS_EGALAX_WIN8 0x0116
#define MT_CLS_SIS 0x0457
#define MT_DEFAULT_MAXCONTACT 10
@@ -450,7 +450,7 @@ static const struct mt_class mt_classes[] = {
MT_QUIRK_YOGABOOK9I,
.export_all_inputs = true
},
- { .name = MT_CLS_EGALAX_P80H84,
+ { .name = MT_CLS_EGALAX_WIN8,
.quirks = MT_QUIRK_ALWAYS_VALID |
MT_QUIRK_IGNORE_DUPLICATES |
MT_QUIRK_CONTACT_CNT_ACCURATE,
@@ -2246,7 +2246,7 @@ static const struct hid_device_id mt_devices[] = {
{ .driver_data = MT_CLS_EGALAX_SERIAL,
MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000) },
- { .driver_data = MT_CLS_EGALAX_P80H84,
+ { .driver_data = MT_CLS_EGALAX_WIN8,
HID_DEVICE(HID_BUS_ANY, HID_GROUP_MULTITOUCH_WIN_8,
USB_VENDOR_ID_DWAV,
USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002) },
--
2.53.0
^ permalink raw reply related
* [PATCH 2/2] HID: multitouch: add support for Dell Pro Rugged 12 Tablet RA02260
From: buingoc67 @ 2026-04-03 9:28 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, hmtheboy154
In-Reply-To: <20260403092848.10223-1-buingoc67@gmail.com>
From: hmtheboy154 <buingoc67@gmail.com>
Tested both multitouch & stylus.
Signed-off-by: hmtheboy154 <buingoc67@gmail.com>
---
drivers/hid/hid-ids.h | 1 +
drivers/hid/hid-multitouch.c | 4 ++++
2 files changed, 5 insertions(+)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index afcee13bad61..2d40d33a6a90 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -447,6 +447,7 @@
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_A001 0xa001
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000 0xc000
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002 0xc002
+#define I2C_DEVICE_ID_DELL_PRO_RUGGED_12_RA02260 0xc005
#define USB_VENDOR_ID_EDIFIER 0x2d99
#define USB_DEVICE_ID_EDIFIER_QR30 0xa101 /* EDIFIER Hal0 2.0 SE */
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index 9e46028746fd..228fcffc3423 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -2250,6 +2250,10 @@ static const struct hid_device_id mt_devices[] = {
HID_DEVICE(HID_BUS_ANY, HID_GROUP_MULTITOUCH_WIN_8,
USB_VENDOR_ID_DWAV,
USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002) },
+ { .driver_data = MT_CLS_EGALAX_WIN8,
+ HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
+ USB_VENDOR_ID_DWAV,
+ I2C_DEVICE_ID_DELL_PRO_RUGGED_12_RA02260) },
/* Elan devices */
{ .driver_data = MT_CLS_WIN_8_FORCE_MULTI_INPUT,
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v3 3/3] input: touchscreen: st1232: add system wakeup support
From: Bui Duc Phuc @ 2026-04-03 11:39 UTC (permalink / raw)
To: Geert Uytterhoeven
Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Magnus Damm, Wolfram Sang, Jeff LaBundy, Bastian Hecht,
Javier Carrasco, linux-input, devicetree, linux-renesas-soc,
linux-kernel
In-Reply-To: <CAMuHMdW6y4MkCYR-rgn=FA38ZUE_X=3oQWNOvfdyMo=D5_xoxA@mail.gmail.com>
Hi Dmitry, Geert,
Thank you, Dmitry, for the review and the explanation. You are
absolutely right; I realized the I2C core handles this automatically,
which is tại sao I dropped those changes in the v4 series [1] as Geert
mentioned.
Thank you, Geert, for pointing that out and for your support.
While working on this, I also noticed similar redundant wakeup
handling in the mpr121 driver and sent a cleanup patch to remove
it [2].
[1] https://lore.kernel.org/20260309000319.74880-1-phucduc.bui@gmail.com
[2] https://lore.kernel.org/all/20260309071413.92709-1-phucduc.bui@gmail.com/
Thanks,
Phuc
On Thu, Apr 2, 2026 at 1:56 PM Geert Uytterhoeven <geert@linux-m68k.org> wrote:
>
> Hi Dmitry,
>
> On Thu, 2 Apr 2026 at 07:17, Dmitry Torokhov <dmitry.torokhov@gmail.com> wrote:
> > On Fri, Mar 06, 2026 at 06:19:12PM +0700, phucduc.bui@gmail.com wrote:
> > > From: bui duc phuc <phucduc.bui@gmail.com>
> > >
> > > The ST1232 touchscreen controller can generate an interrupt when the
> > > panel is touched, which may be used as a wakeup source for the system.
> > >
> > > Add support for system wakeup by initializing the device wakeup
> > > capability in probe() based on the "wakeup-source" device property.
> > > When wakeup is enabled, the driver enables IRQ wake during suspend
> > > so that touch events can wake the system.
> > >
> > > If wakeup is not enabled, the driver retains the existing behavior of
> > > disabling the IRQ and powering down the controller during suspend.
> >
> > I do not believe this patch is needed: i2c core already handles
> > "wakeup-source" property and manages wakeup IRQ.
>
> No, it is not needed, as mentioned in the cover letter of v4[1],
> and as tested by me[2].
>
> [1] https://lore.kernel.org/20260309000319.74880-1-phucduc.bui@gmail.com
> [2] https://lore.kernel.org/CAMuHMdUqiaP=COTkKU_jK6Hdii+YJ5+zXnxFkOOnhLri5NakTw@mail.gmail.com
>
> Gr{oetje,eeting}s,
>
> Geert
>
> --
> Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org
>
> In personal conversations with technical people, I call myself a hacker. But
> when I'm talking to journalists I just say "programmer" or something like that.
> -- Linus Torvalds
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox