Linux Input/HID development
 help / color / mirror / Atom feed
* Re: [PATCH v3 6/9] input: keyboard: mtk-pmic-keys: add MT6392 support
From: AngeloGioacchino Del Regno @ 2026-03-18 12:39 UTC (permalink / raw)
  To: Luca Leonardo Scorcia, linux-mediatek
  Cc: Val Packett, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sen Chu, Sean Wang, Macpaul Lin, Lee Jones,
	Matthias Brugger, Linus Walleij, Liam Girdwood, Mark Brown,
	Gary Bisson, Julien Massot, Louis-Alexis Eyraud, Fabien Parent,
	Chen Zhong, linux-input, devicetree, linux-kernel, linux-pm,
	linux-arm-kernel, linux-gpio
In-Reply-To: <20260317184507.523060-7-l.scorcia@gmail.com>

Il 17/03/26 19:43, Luca Leonardo Scorcia ha scritto:
> From: Val Packett <val@packett.cool>
> 
> Add support for the MT6392 PMIC to the keys driver.
> 
> Signed-off-by: Val Packett <val@packett.cool>
> Signed-off-by: Luca Leonardo Scorcia <l.scorcia@gmail.com>
> Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>

Reviewed-by: AngeloGioacchino Del Regno <angelogioacchino.delregno@collabora.com>



^ permalink raw reply

* Re: [PATCH v3 8/9] pinctrl: mediatek: mt6397: Add support for MT6392 variant
From: AngeloGioacchino Del Regno @ 2026-03-18 12:38 UTC (permalink / raw)
  To: Luca Leonardo Scorcia, linux-mediatek
  Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Sen Chu, Sean Wang, Macpaul Lin, Lee Jones, Matthias Brugger,
	Linus Walleij, Liam Girdwood, Mark Brown, Gary Bisson,
	Julien Massot, Louis-Alexis Eyraud, Val Packett, Fabien Parent,
	Chen Zhong, linux-input, devicetree, linux-kernel, linux-pm,
	linux-arm-kernel, linux-gpio
In-Reply-To: <20260317184507.523060-9-l.scorcia@gmail.com>

Il 17/03/26 19:43, Luca Leonardo Scorcia ha scritto:
> Add support for the MT6392 pinctrl device, which is very similar to
> MT6397 with a handful of different property values and its own pins
> definition.
> 
> Update the MT6397 driver to retrieve device data from the match table and
> use it for driver init.
> 
> Signed-off-by: Luca Leonardo Scorcia <l.scorcia@gmail.com>

Reviewed-by: AngeloGioacchino Del Regno <angelogioacchino.delregno@collabora.com>


^ permalink raw reply

* Re: [PATCH WIP v2 11/11] arm64: dts: qcom: sdm845-google: Add STM FTS touchscreen support
From: Konrad Dybcio @ 2026-03-18 11:49 UTC (permalink / raw)
  To: david, Dmitry Torokhov, Maxime Coquelin, Alexandre Torgue,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Bjorn Andersson, Konrad Dybcio
  Cc: Petr Hodina, linux-input, linux-stm32, linux-arm-kernel,
	linux-kernel, Krzysztof Kozlowski, devicetree, linux-arm-msm,
	phone-devel
In-Reply-To: <20260315-stmfts5-v2-11-70bc83ee9591@ixit.cz>

On 3/15/26 7:52 PM, David Heidelberg via B4 Relay wrote:
> From: Petr Hodina <petr.hodina@protonmail.com>
> 
> Basic touchscreen connected to second i2c bus.
> 
> Signed-off-by: Petr Hodina <petr.hodina@protonmail.com>
> Co-developed-by: David Heidelberg <david@ixit.cz>
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---
>  arch/arm64/boot/dts/qcom/sdm845-google-blueline.dts | 20 +++++++++++++++++++-
>  1 file changed, 19 insertions(+), 1 deletion(-)
> 
> diff --git a/arch/arm64/boot/dts/qcom/sdm845-google-blueline.dts b/arch/arm64/boot/dts/qcom/sdm845-google-blueline.dts
> index fa89be500fb85..48d7e7f83c285 100644
> --- a/arch/arm64/boot/dts/qcom/sdm845-google-blueline.dts
> +++ b/arch/arm64/boot/dts/qcom/sdm845-google-blueline.dts
> @@ -26,7 +26,25 @@ &i2c2 {
>  
>  	status = "okay";
>  
> -	/* ST,FTS @ 49 */
> +	touchscreen@49 {
> +		compatible = "st,stmfts5";
> +		reg = <0x49>;
> +
> +		pinctrl-0 = <&touchscreen_pins &touchscreen_reset>;

s/touchscreen_pins/touchscreen_irq_n?

also: <&foo>, <&bar> (this produces the exact same compilation output
but making them separate makes more ""semantic"" sense)

> +		pinctrl-names = "default";
> +
> +		interrupt-parent = <&tlmm>;
> +		interrupts = <125 IRQ_TYPE_LEVEL_LOW>;

interrupts-extended = <&tlmm 125 IRQ...>

Konrad

^ permalink raw reply

* Re: [PATCH 1/2] Input: st1232 - read firmware version and revision
From: Michael Tretter @ 2026-03-18 11:42 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Khalid Talash
In-Reply-To: <aYX_paFYIBycCrf8@pengutronix.de>

On Fri, 06 Feb 2026 15:50:13 +0100, Michael Tretter wrote:
> On Thu, 05 Feb 2026 01:17:17 -0800, Dmitry Torokhov wrote:
> > On Fri, Jan 23, 2026 at 05:28:55PM +0100, Michael Tretter wrote:
> > > +static int st1232_ts_read_fw_version(struct st1232_ts_data *ts,
> > > +				     u8 *fw_version, u32 *fw_revision)
> > > +{
> > > +	int error;
> > > +
> > > +	/* select firmware version register */
> > > +	error = st1232_ts_read_data(ts, REG_FIRMWARE_VERSION, 1);
> > > +	if (error)
> > > +		return error;
> > > +	*fw_version = ts->read_buf[0];
> > > +
> > > +	/* select firmware revision register */
> > > +	error = st1232_ts_read_data(ts, REG_FIRMWARE_REVISION_3, 4);
> > > +	if (error)
> > > +		return error;
> > > +	*fw_revision = get_unaligned_le32(ts->read_buf);
> > 
> > Why unaligned? You are not reading at an offset and allocated memory is
> > cache line aligned.
> 
> I copied what other touchscreen drivers were doing to read the version,
> but missed that they either read at an offset or use a buffer on the
> stack.
> 
> Should I send a v2 or can you change this to le32_to_cpu() while
> applying the patch?

I just sent a v2.

Michael

^ permalink raw reply

* [PATCH v2 2/2] Input: st1232 - expose firmware version via sysfs
From: Michael Tretter @ 2026-03-18 11:41 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260318-input-st1232-firmware-version-v2-0-e844ee340d36@pengutronix.de>

Allow user space programs to read the firmware version and revision of
the touch controller from the sysfs.

Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>

---
Changes in v2:
- none
---
 drivers/input/touchscreen/st1232.c | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index cc72d70642b2..843140ebb6bf 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -69,6 +69,34 @@ struct st1232_ts_data {
 	u32 fw_revision;
 };
 
+static ssize_t fw_version_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+	return sysfs_emit(buf, "%u\n", st1232_ts->fw_version);
+}
+
+static ssize_t fw_revision_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+	return sysfs_emit(buf, "%08x\n", st1232_ts->fw_revision);
+}
+
+static DEVICE_ATTR_RO(fw_version);
+static DEVICE_ATTR_RO(fw_revision);
+
+static struct attribute *st1232_attrs[] = {
+	&dev_attr_fw_version.attr,
+	&dev_attr_fw_revision.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(st1232);
+
 static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
 			       unsigned int n)
 {
@@ -445,6 +473,7 @@ static struct i2c_driver st1232_ts_driver = {
 	.driver = {
 		.name	= ST1232_TS_NAME,
 		.of_match_table = st1232_ts_dt_ids,
+		.dev_groups = st1232_groups,
 		.probe_type	= PROBE_PREFER_ASYNCHRONOUS,
 		.pm	= pm_sleep_ptr(&st1232_ts_pm_ops),
 	},

-- 
2.47.3


^ permalink raw reply related

* [PATCH v2 1/2] Input: st1232 - read firmware version and revision
From: Michael Tretter @ 2026-03-18 11:41 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260318-input-st1232-firmware-version-v2-0-e844ee340d36@pengutronix.de>

According to the data sheet, the st1332 contains a firmware, which may
be updated. The version and revision of the firmware may be read from
the registers of the device.

Read the firmware version and revision and report it to the log when
probing the device.

Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>

---
Changes in v2:
- replace get_unaligned_le32() by le32_to_cpup
---
 drivers/input/touchscreen/st1232.c | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index 9b3901eec0a5..cc72d70642b2 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -10,6 +10,7 @@
  *	Copyright (C) 2007 Google, Inc.
  */
 
+#include <asm/byteorder.h>
 #include <linux/delay.h>
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
@@ -27,6 +28,9 @@
 #define ST1232_TS_NAME	"st1232-ts"
 #define ST1633_TS_NAME	"st1633-ts"
 
+#define REG_FIRMWARE_VERSION	0x00
+#define REG_FIRMWARE_REVISION_3	0x0C
+
 #define REG_STATUS		0x01	/* Device Status | Error Code */
 
 #define STATUS_NORMAL		0x00
@@ -61,6 +65,8 @@ struct st1232_ts_data {
 	struct list_head touch_overlay_list;
 	int read_buf_len;
 	u8 *read_buf;
+	u8 fw_version;
+	u32 fw_revision;
 };
 
 static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
@@ -110,6 +116,26 @@ static int st1232_ts_wait_ready(struct st1232_ts_data *ts)
 	return -ENXIO;
 }
 
+static int st1232_ts_read_fw_version(struct st1232_ts_data *ts,
+				     u8 *fw_version, u32 *fw_revision)
+{
+	int error;
+
+	/* select firmware version register */
+	error = st1232_ts_read_data(ts, REG_FIRMWARE_VERSION, 1);
+	if (error)
+		return error;
+	*fw_version = ts->read_buf[0];
+
+	/* select firmware revision register */
+	error = st1232_ts_read_data(ts, REG_FIRMWARE_REVISION_3, 4);
+	if (error)
+		return error;
+	*fw_revision = le32_to_cpup((__le32 *)ts->read_buf);
+
+	return 0;
+}
+
 static int st1232_ts_read_resolution(struct st1232_ts_data *ts, u16 *max_x,
 				     u16 *max_y)
 {
@@ -299,6 +325,17 @@ static int st1232_ts_probe(struct i2c_client *client)
 	if (error)
 		return error;
 
+	/* Read firmware version from the chip */
+	error = st1232_ts_read_fw_version(ts,
+					  &ts->fw_version, &ts->fw_revision);
+	if (error) {
+		dev_err(&client->dev,
+			"Failed to read firmware version: %d\n", error);
+		return error;
+	}
+	dev_dbg(&client->dev, "Detected firmware version %u, rev %08x\n",
+		ts->fw_version, ts->fw_revision);
+
 	if (ts->chip_info->have_z)
 		input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0,
 				     ts->chip_info->max_area, 0, 0);

-- 
2.47.3


^ permalink raw reply related

* [PATCH v2 0/2] Input: st1232 - add support for firmware version
From: Michael Tretter @ 2026-03-18 11:41 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash

The st1332 runs a firmware and reports the version and revision of the
firmware via the registers of the device.

Read the firmware version and revision from the respective registers,
print it to the log and report it via sysfs.

Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
Changes in v2:
- replace get_unaligned_le32() by le32_to_cpup
- Link to v1: https://patch.msgid.link/20260123-input-st1232-firmware-version-v1-0-32df7eefdafe@pengutronix.de

---
Michael Tretter (2):
      Input: st1232 - read firmware version and revision
      Input: st1232 - expose firmware version via sysfs

 drivers/input/touchscreen/st1232.c | 66 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)
---
base-commit: 2d1373e4246da3b58e1df058374ed6b101804e07
change-id: 20260123-input-st1232-firmware-version-05cdf03d85c4

Best regards,
-- 
Michael Tretter <m.tretter@pengutronix.de>


^ permalink raw reply

* Re: [PATCH 00/15] tracepoint: Avoid double static_branch evaluation at guarded call sites
From: Vineeth Remanan Pillai @ 2026-03-18 10:58 UTC (permalink / raw)
  To: Mathieu Desnoyers
  Cc: Steven Rostedt, Andrii Nakryiko, Peter Zijlstra, Dmitry Ilvokhin,
	Masami Hiramatsu, Ingo Molnar, Jens Axboe, io-uring,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Alexei Starovoitov, Daniel Borkmann, Marcelo Ricardo Leitner,
	Xin Long, Jon Maloy, Aaron Conole, Eelco Chaudron, Ilya Maximets,
	netdev, bpf, linux-sctp, tipc-discussion, dev, Oded Gabbay,
	Koby Elbaz, dri-devel, Rafael J. Wysocki, Viresh Kumar,
	Gautham R. Shenoy, Huang Rui, Mario Limonciello, Len Brown,
	Srinivas Pandruvada, linux-pm, MyungJoo Ham, Kyungmin Park,
	Chanwoo Choi, Christian König, Sumit Semwal, linaro-mm-sig,
	Eddie James, Andrew Jeffery, Joel Stanley, linux-fsi,
	David Airlie, Simona Vetter, Alex Deucher, Danilo Krummrich,
	Matthew Brost, Philipp Stanner, Harry Wentland, Leo Li, amd-gfx,
	Jiri Kosina, Benjamin Tissoires, linux-input, Wolfram Sang,
	linux-i2c, Mark Brown, Michael Hennerich, Nuno Sá, linux-spi,
	James E.J. Bottomley, Martin K. Petersen, linux-scsi, Chris Mason,
	David Sterba, linux-btrfs, linux-trace-kernel, linux-kernel
In-Reply-To: <6ca9f884-9566-4a82-9995-4c802a0bf8a0@efficios.com>

On Tue, Mar 17, 2026 at 12:02 PM Mathieu Desnoyers
<mathieu.desnoyers@efficios.com> wrote:
>
> On 2026-03-17 12:00, Steven Rostedt wrote:
> > On Fri, 13 Mar 2026 10:02:32 -0400
> > Vineeth Remanan Pillai <vineeth@bitbyteword.org> wrote:
> >
> >>>
> >>> Perhaps: call_trace_foo() ?
> >>>
> >> call_trace_foo has one collision with the tracepoint
> >> sched_update_nr_running and a function
> >> call_trace_sched_update_nr_running. I had considered this and later
> >> moved to trace_invoke_foo() because of the collision. But I can rename
> >> call_trace_sched_update_nr_running to something else if call_trace_foo
> >> is the general consensus.
> >
> > OK, then lets go with: trace_call__foo()
> >
> > The double underscore should prevent any name collisions.
> >
> > Does anyone have an objections?
> I'm OK with it.
>
Great thanks! I shall send a v2 with s/trace_invoke_foo/trace_call__foo/ soon.

Thanks,
Vineeth

^ permalink raw reply

* [PATCH] HID: huawei: fix CD30 keyboard report descriptor issue
From: Miao Li @ 2026-03-18  9:12 UTC (permalink / raw)
  To: jikos, bentiss; +Cc: linux-input, linux-kernel, limiao870622, Miao Li

From: Miao Li <limiao@kylinos.cn>

When the Huawei CD30 USB keyboard undergoes 500 reboot cycles,
initialization may fail due to a report descriptor problem.
The error log is as follows:
[pid:175,cpu0,kworker/0:1,6]usb 1-1.2.2: new low-speed USB device number 6 using xhci-hcd
[pid:175,cpu0,kworker/0:1,9]usb 1-1.2.2: New USB device found, idVendor=12d1, idProduct=109b, bcdDevice= 1.03
[pid:175,cpu0,kworker/0:1,0]usb 1-1.2.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[pid:175,cpu0,kworker/0:1,1]usb 1-1.2.2: Product: HUAWEI USB Wired Keyboard
[pid:175,cpu0,kworker/0:1,2]usb 1-1.2.2: Manufacturer: HUAWEI
[pid:175,cpu0,kworker/0:1,4]input: HUAWEI HUAWEI USB Wired Keyboard as /devices/platform/efc00000.hisi_usb/efc00000.dwc3/xhci-hcd.1.auto/usb1/1-1/1-1.2/1-1.2.2/1-1.2.2:1.0/0003:12D1:109B.0002/input/input6
[pid:175,cpu0,kworker/0:1,5]hid-generic 0003:12D1:109B.0002: input,hidraw1: USB HID v1.10 Keyboard [HUAWEI HUAWEI USB Wired Keyboard] on usb-xhci-hcd.1.auto-1.2.2/input0
[pid:175,cpu0,kworker/0:1,9]hid-generic 0003:12D1:109B.0003: collection stack underflow
[pid:175,cpu0,kworker/0:1,0]hid-generic 0003:12D1:109B.0003: item 0 0 0 12 parsing failed
[pid:175,cpu0,kworker/0:1,1]hid-generic: probe of 0003:12D1:109B.0003 failed with error -22
...
When encountering such a situation, fix it with the correct report descriptor.

Signed-off-by: Miao Li <limiao@kylinos.cn>
---
 drivers/hid/Kconfig      |  7 ++++
 drivers/hid/Makefile     |  1 +
 drivers/hid/hid-huawei.c | 82 ++++++++++++++++++++++++++++++++++++++++
 drivers/hid/hid-ids.h    |  3 ++
 4 files changed, 93 insertions(+)
 create mode 100644 drivers/hid/hid-huawei.c

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a..2fab4964d 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1435,6 +1435,13 @@ config HID_KUNIT_TEST
 
 	  If in doubt, say "N".
 
+config HID_HUAWEI
+	tristate "Huawei HID devices support"
+	depends on USB_HID
+	help
+	  Support for huawei cd30 keyboard or other hid devices
+	  that need fix-ups to work properly.
+
 endmenu
 
 source "drivers/hid/bpf/Kconfig"
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e01838239..616ff2185 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -152,6 +152,7 @@ obj-$(CONFIG_HID_ZEROPLUS)	+= hid-zpff.o
 obj-$(CONFIG_HID_ZYDACRON)	+= hid-zydacron.o
 obj-$(CONFIG_HID_VIEWSONIC)	+= hid-viewsonic.o
 obj-$(CONFIG_HID_VRC2)		+= hid-vrc2.o
+obj-$(CONFIG_HID_HUAWEI)	+= hid-huawei.o
 
 wacom-objs			:= wacom_wac.o wacom_sys.o
 obj-$(CONFIG_HID_WACOM)		+= wacom.o
diff --git a/drivers/hid/hid-huawei.c b/drivers/hid/hid-huawei.c
new file mode 100644
index 000000000..6a616bf21
--- /dev/null
+++ b/drivers/hid/hid-huawei.c
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ *  HID driver for some huawei "special" devices
+ *
+ * Copyright (c) 2026 Miao Li <limiao@kylinos.cn>
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+
+#include "hid-ids.h"
+
+static const __u8 huawei_cd30_kbd_rdesc_fixed[] = {
+	0x05, 0x01,		/* Usage Page (Generic Desktop)		*/
+	0x09, 0x80,		/* Usage (System Control)		*/
+	0xa1, 0x01,		/* Collection (Application)		*/
+	0x85, 0x01,		/*   Report ID (1)			*/
+	0x19, 0x81,		/*   Usage Minimum (System Power Down)	*/
+	0x29, 0x83,		/*   Usage Maximum (System Wake Up)	*/
+	0x15, 0x00,		/*   Logical Minimum (0)		*/
+	0x25, 0x01,		/*   Logical Maximum (1)		*/
+	0x75, 0x01,		/*   Report Size (1 bit)		*/
+	0x95, 0x03,		/*   Report Count (3)			*/
+	0x81, 0x02,		/*   Input (Data,Var,Abs)		*/
+	0x95, 0x05,		/*   Report Count (5)			*/
+	0x81, 0x01,		/*   Input (Cnst,Ary,Abs)		*/
+	0xc0,			/* End Collection			*/
+	0x05, 0x0c,		/* Usage Page (Consumer)		*/
+	0x09, 0x01,		/* Usage (Consumer Control)		*/
+	0xa1, 0x01,		/* Collection (Application)		*/
+	0x85, 0x02,		/*   Report ID (2)			*/
+	0x19, 0x00,		/*   Usage Minimum (0)			*/
+	0x2a, 0x3c, 0x02,	/*   Usage Maximum (0x023C)		*/
+	0x15, 0x00,		/*   Logical Minimum (0)		*/
+	0x26, 0x3c, 0x02,	/*   Logical Maximum (0x023C)		*/
+	0x95, 0x01,		/*   Report Count (1)			*/
+	0x75, 0x10,		/*   Report Size (16 bits)		*/
+	0x81, 0x00,		/*   Input (Data,Ary,Abs)		*/
+	0xc0			/* End Collection			*/
+};
+
+static const __u8 *huawei_report_fixup(struct hid_device *hdev, __u8 *rdesc,
+				  unsigned int *rsize)
+{
+	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+
+	switch (hdev->product) {
+	case USB_DEVICE_ID_HUAWEI_CD30KBD:
+		if (intf->cur_altsetting->desc.bInterfaceNumber == 1) {
+			if (*rsize != sizeof(huawei_cd30_kbd_rdesc_fixed) ||
+				memcmp(huawei_cd30_kbd_rdesc_fixed, rdesc,
+					sizeof(huawei_cd30_kbd_rdesc_fixed)) != 0) {
+				hid_info(hdev, "Replacing Huawei cd30 keyboard report descriptor.\n");
+				*rsize = sizeof(huawei_cd30_kbd_rdesc_fixed);
+				return huawei_cd30_kbd_rdesc_fixed;
+			}
+		}
+		break;
+	}
+
+	return rdesc;
+}
+
+static const struct hid_device_id huawei_devices[] = {
+	/* HUAWEI cd30 keyboard */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HUAWEI, USB_DEVICE_ID_HUAWEI_CD30KBD)},
+	{ }
+};
+MODULE_DEVICE_TABLE(hid, huawei_devices);
+
+static struct hid_driver huawei_driver = {
+	.name = "huawei",
+	.id_table = huawei_devices,
+	.report_fixup = huawei_report_fixup,
+};
+module_hid_driver(huawei_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Miao Li <limiao@kylinos.cn>");
+MODULE_DESCRIPTION("HID driver for some huawei \"special\" devices");
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 4ab7640b1..35df11561 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1581,4 +1581,7 @@
 #define USB_VENDOR_ID_JIELI_SDK_DEFAULT		0x4c4a
 #define USB_DEVICE_ID_JIELI_SDK_4155		0x4155
 
+#define USB_VENDOR_ID_HUAWEI			0x12d1
+#define USB_DEVICE_ID_HUAWEI_CD30KBD		0x109b
+
 #endif
-- 
2.25.1


^ permalink raw reply related

* [PATCH v3] dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
From: phucduc.bui @ 2026-03-18  8:31 UTC (permalink / raw)
  To: conor+dt, krzk+dt, robh
  Cc: conor, devicetree, dmitry.torokhov, krzk, linux-input,
	linux-kernel, marex, mingo, tglx, phucduc.bui

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
+
 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 related

* Re: [PATCH v3 1/9] dt-bindings: mfd: mt6397: Add bindings for MT6392 PMIC
From: Krzysztof Kozlowski @ 2026-03-18  7:47 UTC (permalink / raw)
  To: Luca Leonardo Scorcia
  Cc: linux-mediatek, Fabien Parent, Val Packett, Dmitry Torokhov,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Sen Chu,
	Sean Wang, Macpaul Lin, Lee Jones, Matthias Brugger,
	AngeloGioacchino Del Regno, Linus Walleij, Liam Girdwood,
	Mark Brown, Gary Bisson, Louis-Alexis Eyraud, Julien Massot,
	Chen Zhong, linux-input, devicetree, linux-kernel, linux-pm,
	linux-arm-kernel, linux-gpio
In-Reply-To: <20260317184507.523060-2-l.scorcia@gmail.com>

On Tue, Mar 17, 2026 at 06:43:04PM +0000, Luca Leonardo Scorcia wrote:
> @@ -92,6 +100,7 @@ properties:
>                - mediatek,mt6328-regulator
>                - mediatek,mt6358-regulator
>                - mediatek,mt6359-regulator
> +              - mediatek,mt6392-regulator

So this also should be dropped. I understand that we added compatibles
to MFD device children 10-15 years ago, but what is the point of doing
this still? What is the benefit? See also rules in overall-design in
writing-bindings.

>                - mediatek,mt6397-regulator
>            - items:
>                - enum:

Best regards,
Krzysztof


^ permalink raw reply

* Re: [PATCH v3 3/9] dt-bindings: regulator: Document MediaTek MT6392 PMIC Regulators
From: Krzysztof Kozlowski @ 2026-03-18  7:43 UTC (permalink / raw)
  To: Luca Leonardo Scorcia
  Cc: linux-mediatek, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sen Chu, Sean Wang, Macpaul Lin, Lee Jones,
	Matthias Brugger, AngeloGioacchino Del Regno, Linus Walleij,
	Liam Girdwood, Mark Brown, Julien Massot, Gary Bisson,
	Louis-Alexis Eyraud, Val Packett, Fabien Parent, Chen Zhong,
	linux-input, devicetree, linux-kernel, linux-pm, linux-arm-kernel,
	linux-gpio
In-Reply-To: <20260317184507.523060-4-l.scorcia@gmail.com>

On Tue, Mar 17, 2026 at 06:43:06PM +0000, Luca Leonardo Scorcia wrote:
> Add bindings for the regulators found in the MediaTek MT6392 PMIC,
> usually found in board designs using the MediaTek MT8516/MT8167 SoCs.

Please use subject prefixes matching the subsystem. You can get them for
example with 'git log --oneline -- DIRECTORY_OR_FILE' on the directory
your patch is touching. For bindings, the preferred subjects are
explained here:
https://www.kernel.org/doc/html/latest/devicetree/bindings/submitting-patches.html#i-for-patch-submitters

You already received this feedback from Mark.

> 
> Signed-off-by: Luca Leonardo Scorcia <l.scorcia@gmail.com>
> ---
>  .../regulator/mediatek,mt6392-regulator.yaml  | 318 ++++++++++++++++++
>  .../regulator/mediatek,mt6392-regulator.h     |  24 ++
>  2 files changed, 342 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/regulator/mediatek,mt6392-regulator.yaml
>  create mode 100644 include/dt-bindings/regulator/mediatek,mt6392-regulator.h
> 
> diff --git a/Documentation/devicetree/bindings/regulator/mediatek,mt6392-regulator.yaml b/Documentation/devicetree/bindings/regulator/mediatek,mt6392-regulator.yaml
> new file mode 100644
> index 000000000000..fa4aad2dcbe8
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/regulator/mediatek,mt6392-regulator.yaml
> @@ -0,0 +1,318 @@
> +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/regulator/mediatek,mt6392-regulator.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: MediaTek MT6392 Regulator
> +
> +description:
> +  Regulator node of the PMIC. This node should under the PMIC's device node.
> +  All voltage regulators provided by the PMIC are described as sub-nodes of
> +  this node.
> +
> +properties:
> +  compatible:
> +    items:
> +      - const: mediatek,mt6392-regulator

Drop compatible. Regulator nodes do not have compatibles. With this, you
can also drop example as it won't be used.

> +
> +patternProperties:
> +  "^(buck_)?v(core|proc|sys)$":

Nope, underscores are not allowed. Use only hyphens.

> +    description: Buck regulators
> +    type: object
> +    $ref: regulator.yaml#
> +    properties:
> +      regulator-allowed-modes:
> +        description: |
> +          BUCK regulators can set regulator-initial-mode and regulator-allowed-modes to
> +          values specified in dt-bindings/regulator/mediatek,mt6392-regulator.h
> +        items:
> +          enum: [0, 1]
> +    unevaluatedProperties: false
> +
> +  "^(ldo_)?v(adc18|camio|cn18|io18)$":
> +    description: LDOs with fixed 1.8V output

If fixed, then encode it in the schema - min/max microvolt.

> +    type: object
> +    $ref: regulator.yaml#
> +    properties:
> +      regulator-allowed-modes:
> +        description: |
> +          LDO regulators can set regulator-initial-mode and regulator-allowed-modes to
> +          values specified in dt-bindings/regulator/mediatek,mt6392-regulator.h
> +        items:
> +          enum: [0, 1]
> +    unevaluatedProperties: false

Best regards,
Krzysztof


^ permalink raw reply

* Re: [PATCH v3 1/9] dt-bindings: mfd: mt6397: Add bindings for MT6392 PMIC
From: Krzysztof Kozlowski @ 2026-03-18  7:37 UTC (permalink / raw)
  To: Luca Leonardo Scorcia
  Cc: linux-mediatek, Fabien Parent, Val Packett, Dmitry Torokhov,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Sen Chu,
	Sean Wang, Macpaul Lin, Lee Jones, Matthias Brugger,
	AngeloGioacchino Del Regno, Linus Walleij, Liam Girdwood,
	Mark Brown, Gary Bisson, Louis-Alexis Eyraud, Julien Massot,
	Chen Zhong, linux-input, devicetree, linux-kernel, linux-pm,
	linux-arm-kernel, linux-gpio
In-Reply-To: <20260317184507.523060-2-l.scorcia@gmail.com>

On Tue, Mar 17, 2026 at 06:43:04PM +0000, Luca Leonardo Scorcia wrote:
> From: Fabien Parent <parent.f@gmail.com>
> 
> Add the currently supported bindings for the MT6392 PMIC.
> 
> Signed-off-by: Fabien Parent <parent.f@gmail.com>
> Signed-off-by: Val Packett <val@packett.cool>
> Signed-off-by: Luca Leonardo Scorcia <l.scorcia@gmail.com>

If there is going to be new version, then please fix all the subjects:
A nit, subject: drop second/last, redundant "bindings for". The
"dt-bindings" prefix is already stating that these are bindings.
See also:
https://elixir.bootlin.com/linux/v6.17-rc3/source/Documentation/devicetree/bindings/submitting-patches.rst#L18

> ---
>  .../devicetree/bindings/mfd/mediatek,mt6397.yaml         | 9 +++++++++
>  1 file changed, 9 insertions(+)

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>

Best regards,
Krzysztof


^ permalink raw reply

* Re: [PATCH v1] Input: atlas - convert ACPI driver to a platform one
From: Dmitry Torokhov @ 2026-03-18  5:49 UTC (permalink / raw)
  To: Rafael J. Wysocki; +Cc: LKML, Linux ACPI, linux-input
In-Reply-To: <3429591.aeNJFYEL58@rafael.j.wysocki>

On Sat, Mar 14, 2026 at 12:54:58PM +0100, Rafael J. Wysocki wrote:
> From: "Rafael J. Wysocki" <rafael.j.wysocki@intel.com>
> 
> In all cases in which a struct acpi_driver is used for binding a driver
> to an ACPI device object, a corresponding platform device is created by
> the ACPI core and that device is regarded as a proper representation of
> underlying hardware.  Accordingly, a struct platform_driver should be
> used by driver code to bind to that device.  There are multiple reasons
> why drivers should not bind directly to ACPI device objects [1].
> 
> Overall, it is better to bind drivers to platform devices than to their
> ACPI companions, so convert the ACPI Atlas button driver to a platform
> one.
> 
> While this is not expected to alter functionality, it changes sysfs
> layout and so it will be visible to user space.
> 
> Link: https://lore.kernel.org/all/2396510.ElGaqSPkdT@rafael.j.wysocki/ [1]
> Signed-off-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com>

Applied, thank you.

-- 
Dmitry

^ permalink raw reply

* Re: [External] : [PATCH] Input: sentelic: fix comment typo
From: Dmitry Torokhov @ 2026-03-18  5:43 UTC (permalink / raw)
  To: Joseph Salisbury; +Cc: linux-input, linux-kernel
In-Reply-To: <e35b4f27-53c1-41c2-a321-42a34ade9839@oracle.com>

Hi Joseph,

On Mon, Mar 16, 2026 at 07:48:31PM -0400, Joseph Salisbury wrote:
> 
> 
> On 3/16/26 2:12 PM, Joseph Salisbury wrote:
> > The file contains a spelling error in a source comment (formating).
> > 
> > Typos in comments reduce readability and make text searches less reliable
> > for developers and maintainers.
> > 
> > Replace 'formating' with 'formatting' in the affected comment. This is a
> > comment-only cleanup and does not change behavior.
> > 
> > Fixes: fc69f4a6af49 ("Input: add new driver for Sentelic Finger Sensing Pad")
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Joseph Salisbury <joseph.salisbury@oracle.com>
> > ---
> >   drivers/input/mouse/sentelic.h | 2 +-
> >   1 file changed, 1 insertion(+), 1 deletion(-)
> > 
> > diff --git a/drivers/input/mouse/sentelic.h b/drivers/input/mouse/sentelic.h
> > index 02cac0e7ad63..9ba3631e3d0f 100644
> > --- a/drivers/input/mouse/sentelic.h
> > +++ b/drivers/input/mouse/sentelic.h
> > @@ -60,7 +60,7 @@
> >   #define	FSP_REG_SN1		(0x41)
> >   #define	FSP_REG_SN2		(0x42)
> > -/* Finger-sensing Pad packet formating related definitions */
> > +/* Finger-sensing Pad packet formatting related definitions */
> >   /* absolute packet type */
> >   #define	FSP_PKT_TYPE_NORMAL	(0x00)
> I inadvertently added Fixes: and Cc: stable tags.  If possible, please
> remove them as they are not appropriate for fixes to misspellings in code
> comments.  If it's not possible to remove them, I can send a v2.
> 

Sorry I did not receive the original patch but even with the typo fixed
this comment needs more work to improve readability.

Thanks.

-- 
Dmitry

^ permalink raw reply

* [PATCH] Hid: Intel-thc-hid: Intel-quickspi: Improve power management for touch devices
From: Even Xu @ 2026-03-18  3:25 UTC (permalink / raw)
  To: bentiss, jikos
  Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu,
	Rui Zhang

Enhance power management with two key improvements:
1. Hibernate support: Send POWER_OFF command when entering hibernate
   mode.
2. Conditional sleep commands: Only send POWER_SLEEP/POWER_ON commands
   during system suspend/resume when the touch device is not configured
   as a wake source, preserving Wake-on-Touch (WoT) functionality. This
   ensures proper power states while maintaining expected wake behavior.

Signed-off-by: Even Xu <even.xu@intel.com>
Tested-by: Rui Zhang <rui1.zhang@intel.com>
---
 .../intel-thc-hid/intel-quickspi/pci-quickspi.c  | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
index b6a69995692c..f669235f1883 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
@@ -753,9 +753,11 @@ static int quickspi_suspend(struct device *device)
 	if (!qsdev)
 		return -ENODEV;
 
-	ret = quickspi_set_power(qsdev, HIDSPI_SLEEP);
-	if (ret)
-		return ret;
+	if (!device_may_wakeup(qsdev->dev)) {
+		ret = quickspi_set_power(qsdev, HIDSPI_SLEEP);
+		if (ret)
+			return ret;
+	}
 
 	ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
 	if (ret)
@@ -794,9 +796,8 @@ static int quickspi_resume(struct device *device)
 	if (ret)
 		return ret;
 
-	ret = quickspi_set_power(qsdev, HIDSPI_ON);
-	if (ret)
-		return ret;
+	if (!device_may_wakeup(qsdev->dev))
+		return quickspi_set_power(qsdev, HIDSPI_ON);
 
 	return 0;
 }
@@ -855,6 +856,9 @@ static int quickspi_poweroff(struct device *device)
 	if (!qsdev)
 		return -ENODEV;
 
+	/* Ignore the return value as platform will be poweroff soon */
+	quickspi_set_power(qsdev, HIDSPI_OFF);
+
 	ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
 	if (ret)
 		return ret;
-- 
2.40.1


^ permalink raw reply related

* [PATCH] Hid: Intel-thc-hid: Intel-thc: Add more frequency support for SPI
From: Even Xu @ 2026-03-18  3:22 UTC (permalink / raw)
  To: bentiss, jikos
  Cc: srinivas.pandruvada, linux-input, linux-kernel, Even Xu,
	Rui Zhang

The Nova Lake platform enhances THC with half divider capability for
clock division, allowing more granular frequency control for the THC
SPI port.

Supported frequencies include 50MHz (125MHz/2.5), 35MHz (125MHz/3.5),
and 10MHz (125MHz/8/1.5).

Signed-off-by: Even Xu <even.xu@intel.com>
Tested-by: Rui Zhang <rui1.zhang@intel.com>
---
 .../intel-thc-hid/intel-thc/intel-thc-dev.c   | 47 +++++++++++++++++++
 .../intel-thc-hid/intel-thc/intel-thc-hw.h    |  4 ++
 2 files changed, 51 insertions(+)

diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
index d8e195189e4b..9a8449428170 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
@@ -1112,12 +1112,15 @@ int thc_port_select(struct thc_device *dev, enum thc_port_type port_type)
 EXPORT_SYMBOL_NS_GPL(thc_port_select, "INTEL_THC");
 
 #define THC_SPI_FREQUENCY_7M	7812500
+#define THC_SPI_FREQUENCY_10M	10416700
 #define THC_SPI_FREQUENCY_15M	15625000
 #define THC_SPI_FREQUENCY_17M	17857100
 #define THC_SPI_FREQUENCY_20M	20833000
 #define THC_SPI_FREQUENCY_25M	25000000
 #define THC_SPI_FREQUENCY_31M	31250000
+#define THC_SPI_FREQUENCY_35M	35714200
 #define THC_SPI_FREQUENCY_41M	41666700
+#define THC_SPI_FREQUENCY_50M	50000000
 
 #define THC_SPI_LOW_FREQUENCY	THC_SPI_FREQUENCY_17M
 
@@ -1125,21 +1128,27 @@ static u8 thc_get_spi_freq_div_val(struct thc_device *dev, u32 spi_freq_val)
 {
 	static const int frequency[] = {
 		THC_SPI_FREQUENCY_7M,
+		THC_SPI_FREQUENCY_10M,
 		THC_SPI_FREQUENCY_15M,
 		THC_SPI_FREQUENCY_17M,
 		THC_SPI_FREQUENCY_20M,
 		THC_SPI_FREQUENCY_25M,
 		THC_SPI_FREQUENCY_31M,
+		THC_SPI_FREQUENCY_35M,
 		THC_SPI_FREQUENCY_41M,
+		THC_SPI_FREQUENCY_50M,
 	};
 	static const u8 frequency_div[] = {
 		THC_SPI_FRQ_DIV_2,
 		THC_SPI_FRQ_DIV_1,
+		THC_SPI_FRQ_DIV_1,
 		THC_SPI_FRQ_DIV_7,
 		THC_SPI_FRQ_DIV_6,
 		THC_SPI_FRQ_DIV_5,
 		THC_SPI_FRQ_DIV_4,
 		THC_SPI_FRQ_DIV_3,
+		THC_SPI_FRQ_DIV_3,
+		THC_SPI_FRQ_DIV_2,
 	};
 	int size = ARRAY_SIZE(frequency);
 	u32 closest_freq;
@@ -1190,6 +1199,25 @@ int thc_spi_read_config(struct thc_device *dev, u32 spi_freq_val,
 	if (spi_freq_val < THC_SPI_LOW_FREQUENCY)
 		is_low_freq = true;
 
+	/* 10M, 35M and 50M CLK need 1.5, 3.5 and 2.5 half divider */
+	if ((freq_div == THC_SPI_FRQ_DIV_2 && spi_freq_val >=  THC_SPI_FREQUENCY_50M) ||
+		(freq_div == THC_SPI_FRQ_DIV_3 && spi_freq_val <  THC_SPI_FREQUENCY_41M) ||
+		(freq_div == THC_SPI_FRQ_DIV_1 && spi_freq_val <  THC_SPI_FREQUENCY_15M)) {
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPI_DUTYC_CFG_OFFSET,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCRF_HALF_DIV_EN,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCRF_HALF_DIV_EN);
+
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPARE_REG_OFFSET,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE);
+	} else {
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPI_DUTYC_CFG_OFFSET,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCRF_HALF_DIV_EN, 0);
+
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPARE_REG_OFFSET,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE, 0);
+	}
+
 	cfg = FIELD_PREP(THC_M_PRT_SPI_CFG_SPI_TCRF, freq_div) |
 	      FIELD_PREP(THC_M_PRT_SPI_CFG_SPI_TRMODE, io_mode) |
 	      (is_low_freq ? THC_M_PRT_SPI_CFG_SPI_LOW_FREQ_EN : 0) |
@@ -1243,6 +1271,25 @@ int thc_spi_write_config(struct thc_device *dev, u32 spi_freq_val,
 	if (spi_freq_val < THC_SPI_LOW_FREQUENCY)
 		is_low_freq = true;
 
+	/* 10M, 35M and 50M CLK need 1.5, 3.5 and 2.5 half divider */
+	if ((freq_div == THC_SPI_FRQ_DIV_2 && spi_freq_val >=  THC_SPI_FREQUENCY_50M) ||
+		(freq_div == THC_SPI_FRQ_DIV_3 && spi_freq_val <  THC_SPI_FREQUENCY_41M) ||
+		(freq_div == THC_SPI_FRQ_DIV_1 && spi_freq_val <  THC_SPI_FREQUENCY_15M)) {
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPI_DUTYC_CFG_OFFSET,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCWF_HALF_DIV_EN,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCWF_HALF_DIV_EN);
+
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPARE_REG_OFFSET,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE);
+	} else {
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPI_DUTYC_CFG_OFFSET,
+				  THC_M_PRT_SPI_DUTYC_CFG_SPI_TCWF_HALF_DIV_EN, 0);
+
+		regmap_write_bits(dev->thc_regmap, THC_M_PRT_SPARE_REG_OFFSET,
+				  THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE, 0);
+	}
+
 	cfg = FIELD_PREP(THC_M_PRT_SPI_CFG_SPI_TCWF, freq_div) |
 	      FIELD_PREP(THC_M_PRT_SPI_CFG_SPI_TWMODE, io_mode) |
 	      (is_low_freq ? THC_M_PRT_SPI_CFG_SPI_LOW_FREQ_EN : 0) |
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
index 413730f8e3f7..c6d026686b7a 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
@@ -643,6 +643,10 @@
 
 #define THC_M_PRT_SPI_DUTYC_CFG_SPI_CSA_CK_DELAY_VAL		GENMASK(3, 0)
 #define THC_M_PRT_SPI_DUTYC_CFG_SPI_CSA_CK_DELAY_EN		BIT(25)
+#define THC_M_PRT_SPI_DUTYC_CFG_SPI_TCRF_HALF_DIV_EN		BIT(30)
+#define THC_M_PRT_SPI_DUTYC_CFG_SPI_TCWF_HALF_DIV_EN		BIT(31)
+
+#define THC_M_PRT_SPARE_REG_SPI_CLK_INV_ENABLE			BIT(2)
 
 /* CS Assertion delay default value */
 #define THC_CSA_CK_DELAY_VAL_DEFAULT		4
-- 
2.40.1


^ permalink raw reply related

* [PATCH v2 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-03-18  3:08 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260318030850.289712-1-vi@endrift.com>

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)
+{
+	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)
+			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;
+	}
+
+	ns2 = switch2_get_controller(phys);
+	if (!ns2) {
+		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);
+}
+
+#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");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-03-18  3:08 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260318030850.289712-1-vi@endrift.com>

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
+ * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
+ * 2 itself does something similar, but it's quite likely.
+ *
+ * This value must be between 0 and 1024, otherwise the math below will overflow.
+ */
+#define RUMBLE_MAX 450u
+
+/*
+ * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
+ * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
+ *
+ * The units used are unknown, but the values must be between 0 and 1023.
+ */
+#define RUMBLE_HI_FREQ 0x187
+#define RUMBLE_LO_FREQ 0x112
+
 static DEFINE_MUTEX(switch2_controllers_lock);
 static LIST_HEAD(switch2_controllers);
 
@@ -3088,9 +3137,12 @@ static const uint8_t switch2_init_cmd_data[] = {
 };
 
 static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+static const uint8_t switch2_zero_data[] = { 0x00, 0x00, 0x00, 0x00 };
+#endif
 
 static const uint8_t switch2_feature_mask[] = {
-	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU,
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
 	0x00, 0x00, 0x00
 };
 
@@ -3107,6 +3159,107 @@ static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
 	return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
 }
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+static void switch2_encode_rumble(struct switch2_hd_rumble *rumble, uint8_t buffer[5])
+{
+	buffer[0] = rumble->hi_freq;
+	buffer[1] = (rumble->hi_freq >> 8) | (rumble->hi_amp << 2);
+	buffer[2] = (rumble->hi_amp >> 6) | (rumble->lo_freq << 4);
+	buffer[3] = (rumble->lo_freq >> 4) | (rumble->lo_amp << 6);
+	buffer[4] = rumble->lo_amp >> 2;
+}
+
+static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+	struct switch2_controller *ns2 = input_get_drvdata(dev);
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	guard(spinlock_irqsave)(&ns2->rumble_lock);
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+			effect->u.rumble.weak_magnitude >> 1);
+	} else {
+		ns2->rumble.hd.hi_amp = effect->u.rumble.weak_magnitude * RUMBLE_MAX >> 16;
+		ns2->rumble.hd.lo_amp = effect->u.rumble.strong_magnitude * RUMBLE_MAX >> 16;
+	}
+
+	if (ns2->hdev)
+		schedule_delayed_work(&ns2->rumble_work, 0);
+
+	return 0;
+}
+
+static void switch2_rumble_work(struct work_struct *work)
+{
+	struct switch2_controller *ns2 = container_of(to_delayed_work(work),
+						      struct switch2_controller, rumble_work);
+	unsigned long current_ms = jiffies_to_msecs(get_jiffies_64());
+	unsigned long flags;
+	bool active;
+	int ret;
+
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	ns2->rumble_buffer[0x1] = 0x50 | ns2->rumble_seq;
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble_buffer[0] = 3;
+		if (ns2->rumble.sd.amplitude == 0) {
+			ns2->rumble_buffer[2] = GC_RUMBLE_STOP;
+			ns2->rumble.sd.error = 0;
+			active = false;
+		} else {
+			if (ns2->rumble.sd.error < ns2->rumble.sd.amplitude) {
+				ns2->rumble_buffer[2] = GC_RUMBLE_ON;
+				ns2->rumble.sd.error += U16_MAX - ns2->rumble.sd.amplitude;
+			} else {
+				ns2->rumble_buffer[2] = GC_RUMBLE_OFF;
+				ns2->rumble.sd.error -= ns2->rumble.sd.amplitude;
+			}
+			active = true;
+		}
+	} else {
+		ns2->rumble_buffer[0] = 1;
+		switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x2]);
+		active = ns2->rumble.hd.hi_amp || ns2->rumble.hd.lo_amp;
+		if (ns2->ctlr_type == NS2_CTLR_TYPE_PRO) {
+			/*
+			 * The Pro Controller contains separate LRAs on each
+			 * side that can be controlled individually.
+			 */
+			ns2->rumble_buffer[0] = 2;
+			ns2->rumble_buffer[0x11] = 0x50 | ns2->rumble_seq;
+			switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x12]);
+		}
+	}
+	ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
+
+	if (active) {
+		unsigned long interval = msecs_to_jiffies(4);
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_ms;
+		else
+			ns2->last_rumble_work += interval;
+		schedule_delayed_work(&ns2->rumble_work,
+			ns2->last_rumble_work + interval - current_ms);
+	} else {
+		ns2->last_rumble_work = 0;
+	}
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	if (!ns2->hdev) {
+		cancel_delayed_work(&ns2->rumble_work);
+		ret = -ENODEV;
+	} else {
+		ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
+	}
+
+	if (ret < 0)
+		hid_dbg(ns2->hdev, "Failed to send output report ret=%d\n", ret);
+}
+#endif
+
 static int switch2_set_leds(struct switch2_controller *ns2)
 {
 	int i;
@@ -3230,6 +3383,15 @@ static int switch2_init_input(struct switch2_controller *ns2)
 		return -EINVAL;
 	}
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	input_set_capability(input, EV_FF, FF_RUMBLE);
+	ret = input_ff_create_memless(input, NULL, switch2_play_effect);
+	if (ret) {
+		input_free_device(input);
+		return ret;
+	}
+#endif
+
 	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)
@@ -3309,6 +3471,10 @@ static void switch2_controller_put(struct switch2_controller *ns2)
 	if (input)
 		input_unregister_device(input);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
+
 	if (do_free) {
 		list_del_init(&ns2->entry);
 		mutex_destroy(&ns2->lock);
@@ -3668,7 +3834,8 @@ static int switch2_init_controller(struct switch2_controller *ns2)
 		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);
+		return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS |
+			NS2_FEATURE_ANALOG | NS2_FEATURE_RUMBLE);
 	case NS2_INIT_GRIP_BUTTONS:
 		if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
 			switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
@@ -3882,6 +4049,14 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
 
 	switch2_leds_create(ns2);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
+		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+	}
+	spin_lock_init(&ns2->rumble_lock);
+	INIT_DELAYED_WORK(&ns2->rumble_work, switch2_rumble_work);
+#endif
 	hid_set_drvdata(hdev, ns2);
 
 	if (ns2->cfg)
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/3] HID: nintendo: Add unified report format support
From: Vicki Pfau @ 2026-03-18  3:08 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260318030850.289712-1-vi@endrift.com>

This adds support for the "unified" report format that all controllers also
support, which has overlapping fields for like buttons and axes between
them.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-nintendo.c | 148 +++++++++++++++++++++++++++++++++++--
 1 file changed, 143 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index 73d732ceb7116..cd32119247400 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2828,6 +2828,36 @@ static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
 #define NS2_BTN3_SR	BIT(6)
 #define NS2_BTN3_SL	BIT(7)
 
+#define NS2_BTN_U1_Y	BIT(0)
+#define NS2_BTN_U1_X	BIT(1)
+#define NS2_BTN_U1_B	BIT(2)
+#define NS2_BTN_U1_A	BIT(3)
+#define NS2_BTN_U1_SR	BIT(4)
+#define NS2_BTN_U1_SL	BIT(5)
+#define NS2_BTN_U1_R	BIT(6)
+#define NS2_BTN_U1_ZR	BIT(7)
+
+#define NS2_BTN_U2_MINUS	BIT(0)
+#define NS2_BTN_U2_PLUS		BIT(1)
+#define NS2_BTN_U2_RS		BIT(2)
+#define NS2_BTN_U2_LS		BIT(3)
+#define NS2_BTN_U2_HOME		BIT(4)
+#define NS2_BTN_U2_CAPTURE	BIT(5)
+#define NS2_BTN_U2_C		BIT(6)
+
+#define NS2_BTN_U3_DOWN		BIT(0)
+#define NS2_BTN_U3_UP		BIT(1)
+#define NS2_BTN_U3_RIGHT	BIT(2)
+#define NS2_BTN_U3_LEFT		BIT(3)
+#define NS2_BTN_U3_SR		BIT(4)
+#define NS2_BTN_U3_SL		BIT(5)
+#define NS2_BTN_U3_L		BIT(6)
+#define NS2_BTN_U3_ZL		BIT(7)
+
+#define NS2_BTN_U4_GR		BIT(0)
+#define NS2_BTN_U4_GL		BIT(1)
+#define NS2_BTN_U4_HEADSET	BIT(5)
+
 #define NS2_BTN_JCR_HOME	BIT(0)
 #define NS2_BTN_JCR_GR		BIT(2)
 #define NS2_BTN_JCR_C		NS2_BTN3_C
@@ -3073,6 +3103,22 @@ static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_unified_mappings[] = {
+	{ BTN_DPAD_LEFT,	2, NS2_BTN_U3_LEFT,	},
+	{ BTN_DPAD_UP,		2, NS2_BTN_U3_UP,	},
+	{ BTN_DPAD_DOWN,	2, NS2_BTN_U3_DOWN,	},
+	{ BTN_DPAD_RIGHT,	2, NS2_BTN_U3_RIGHT,	},
+	{ BTN_TL,		2, NS2_BTN_U3_L,	},
+	{ BTN_TL2,		2, NS2_BTN_U3_ZL,	},
+	{ BTN_SELECT,		1, NS2_BTN_U2_MINUS,	},
+	{ BTN_THUMBL,		1, NS2_BTN_U2_LS,	},
+	{ KEY_RECORD,		1, NS2_BTN_U2_CAPTURE,	},
+	{ BTN_GRIPR,		2, NS2_BTN_U3_SL,	},
+	{ BTN_GRIPR2,		2, NS2_BTN_U3_SR,	},
+	{ BTN_GRIPL,		3, NS2_BTN_U4_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,		},
@@ -3090,6 +3136,23 @@ static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TR,	0, NS2_BTN_U1_R,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_ZR	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_THUMBR,	1, NS2_BTN_U2_RS,	},
+	{ BTN_C,	1, NS2_BTN_U2_C,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME,	},
+	{ BTN_GRIPL2,	0, NS2_BTN_U1_SL,	},
+	{ BTN_GRIPL,	0, NS2_BTN_U1_SR,	},
+	{ BTN_GRIPR,	3, NS2_BTN_U4_GR,	},
+	{ /* sentinel */ },
+};
+
 static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
 	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
 	{ BTN_EAST,	0, NS2_BTNR_B,		},
@@ -3111,6 +3174,27 @@ static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_procon_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TL,	2, NS2_BTN_U3_L,	},
+	{ BTN_TR,	0, NS2_BTN_U1_R,	},
+	{ BTN_TL2,	2, NS2_BTN_U3_ZL,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_ZR,	},
+	{ BTN_SELECT,	1, NS2_BTN_U2_MINUS,	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_THUMBL,	1, NS2_BTN_U2_LS,	},
+	{ BTN_THUMBR,	1, NS2_BTN_U2_RS,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME	},
+	{ KEY_RECORD,	1, NS2_BTN_U2_CAPTURE	},
+	{ BTN_GRIPR,	3, NS2_BTN_U4_GR	},
+	{ BTN_GRIPL,	3, NS2_BTN_U4_GL	},
+	{ BTN_C,	1, NS2_BTN_U2_C		},
+	{ /* sentinel */ },
+};
+
 static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
 	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
 	{ BTN_EAST,	0, NS2_BTNR_B,		},
@@ -3128,6 +3212,23 @@ static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_gccon_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TL2,	2, NS2_BTN_U3_L,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_R,	},
+	{ BTN_TL,	2, NS2_BTN_U3_ZL	},
+	{ BTN_TR,	0, NS2_BTN_U1_ZR	},
+	{ BTN_SELECT,	1, NS2_BTN_U2_MINUS,	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME	},
+	{ KEY_RECORD,	1, NS2_BTN_U2_CAPTURE	},
+	{ BTN_C,	1, NS2_BTN_U2_C		},
+	{ /* sentinel */ },
+};
+
 static const uint8_t switch2_init_cmd_data[] = {
 	/*
 	 * The last 6 bytes of this packet are the MAC address of
@@ -3691,11 +3792,48 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
 
 	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.
-		 */
+		switch (ns2->ctlr_type) {
+		case NS2_CTLR_TYPE_JCL:
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_buttons(input, &raw_data[5],
+				ns2_left_joycon_button_unified_mappings);
+			break;
+		case NS2_CTLR_TYPE_JCR:
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[14]);
+			switch2_report_buttons(input, &raw_data[5],
+				ns2_right_joycon_button_unified_mappings);
+			break;
+		case NS2_CTLR_TYPE_GC:
+			input_report_abs(input, ABS_HAT0X,
+				!!(raw_data[7] & NS2_BTN_U3_RIGHT) -
+				!!(raw_data[7] & NS2_BTN_U3_LEFT));
+			input_report_abs(input, ABS_HAT0Y,
+				!!(raw_data[7] & NS2_BTN_U3_DOWN) -
+				!!(raw_data[7] & NS2_BTN_U3_UP));
+			switch2_report_buttons(input, &raw_data[5], ns2_gccon_unified_mappings);
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_stick(input, &ns2->stick_calib[1],
+				ABS_RX, false, ABS_RY, true, &raw_data[14]);
+			switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[0x3d]);
+			switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[0x3e]);
+			break;
+		case NS2_CTLR_TYPE_PRO:
+			input_report_abs(input, ABS_HAT0X,
+				!!(raw_data[7] & NS2_BTN_U3_RIGHT) -
+				!!(raw_data[7] & NS2_BTN_U3_LEFT));
+			input_report_abs(input, ABS_HAT0Y,
+				!!(raw_data[7] & NS2_BTN_U3_DOWN) -
+				!!(raw_data[7] & NS2_BTN_U3_UP));
+			switch2_report_buttons(input, &raw_data[5], ns2_procon_unified_mappings);
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_stick(input, &ns2->stick_calib[1],
+				ABS_RX, false, ABS_RY, true, &raw_data[14]);
+			break;
+		}
 		break;
 	case NS2_REPORT_JCL:
 		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 0/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-03-18  3:08 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

This series adds preliminary support for Switch 2 controllers. As with v1,
it does not support Bluetooth controllers and does not contain IMU or
battery support. However, several significant changes have been made:

First and foremost, the driver has been folded into hid-nintendo instead of
a separate hid-switch2 driver. This allowed reuse of some small bits of
hid-nintendo's existing support, but not a signiicant amount.

Secondly, the configuration interface API has been changed to make the
switch2_controller struct opaque. As the configuration interface is
effectively just a dumb sidechannel used for passing data that is the same
regardless of transit, there is no need for it to have any internal state
knowledge.

Thirdly, it adds support for "unified" format that had been left as a TODO
in the previous version. Though it does not directly enable said format,
it's good to have for future purposes.

It also addresses the other TODO regarding partial initialization,
guaranteeing a consistent state after setup, regardless of whether or not
an userspace process has claimed an then released control of the USB
interface.

Finally, it fixes a number of bugs, including some missing locks and race
conditions that could trigger a kernel panic if the USB configuration
interface was claimed or released from userspace.

For ease of reviewing, the series has been split into three patches, though
the bulk of the code is still in the first patch.

Vicki Pfau (3):
  HID: nintendo: Add preliminary Switch 2 controller driver
  HID: nintendo: Add rumble support for Switch 2 controllers
  HID: nintendo: Add unified report format support

 MAINTAINERS                                   |    1 +
 drivers/hid/Kconfig                           |   19 +-
 drivers/hid/hid-ids.h                         |    4 +
 drivers/hid/hid-nintendo.c                    | 1507 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  352 ++++
 8 files changed, 1953 insertions(+), 14 deletions(-)
 create mode 100644 drivers/hid/hid-nintendo.h
 create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c

-- 
2.53.0


^ permalink raw reply

* Re: [PATCH v2] dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
From: Bui Duc Phuc @ 2026-03-18  2:30 UTC (permalink / raw)
  To: Conor Dooley
  Cc: Krzysztof Kozlowski, robh, conor+dt, devicetree, dmitry.torokhov,
	krzk+dt, linux-input, linux-kernel, marex, mingo, tglx
In-Reply-To: <20260317-another-wrongdoer-5b4c56ab6027@spud>

Hi Conor, Krzysztof,

Thanks for the review.
I will remove the extra blank line in the next version.
As for the wakeup-source property, I'll keep it as a boolean as per the current
patch.

Best Regards,
Phuc

On Tue, Mar 17, 2026 at 7:14 PM Conor Dooley <conor@kernel.org> wrote:
>
> On Mon, Mar 16, 2026 at 06:10:23PM +0100, Krzysztof Kozlowski wrote:
> > On 16/03/2026 12:13, Conor Dooley wrote:
> > > On Mon, Mar 16, 2026 at 10:46:06AM +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>
> > >> ---
> > >>  .../devicetree/bindings/input/touchscreen/ti,tsc2005.yaml  | 7 +++++++
> > >>  1 file changed, 7 insertions(+)
> > >>
> > >> diff --git a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
> > >> index 7187c390b2f5..c0aae044d7d4 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: true
> >
> > I am not so sure.
> >
> > The property is multi-type, so we want to choose one - bool, IMO,
> > because device cannot wakeup the specific system idle states. Or am I
> > misinterpreting the phandles behind wakeup-source and every device can
> > be differently routed in such system?
>
> I checked before my original comment, and there there's a bout a 2:1
> ratio of defined v true. I suppose my comment can be ignored, I am just
> used to this always being true.

^ permalink raw reply

* Re: [PATCH v8 0/3] TrackPoint doubletap enablement and user control
From: Vishnu Sankar @ 2026-03-18  1:35 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: mpearson-lenovo, dmitry.torokhov, hmh, hansg, corbet,
	derekjohn.clark, linux-input, linux-kernel, ibm-acpi-devel,
	linux-doc, platform-driver-x86, vsankar
In-Reply-To: <177376698425.15640.5695107795590543274.b4-ty@linux.intel.com>

Hi Ilpo,

Thank you for accepting this.

On Wed, Mar 18, 2026 at 2:03 AM Ilpo Järvinen
<ilpo.jarvinen@linux.intel.com> wrote:
>
> On Wed, 11 Mar 2026 23:31:41 +0900, Vishnu Sankar wrote:
>
> > This patch series adds support for TrackPoint doubletap with a clear and
> > simple separation of responsibilities between drivers:
> >
> > 1. Firmware enablement (trackpoint.c):
> >    Automatically enables doubletap on capable hardware during device
> >    detection.
> >
> > [...]
>
>
> Thank you for your contribution, it has been applied to my local
> review-ilpo-next branch. Note it will show up in the public
> platform-drivers-x86/review-ilpo-next branch only once I've pushed my
> local branch there, which might take a while.
Acked.
>
> The list of commits applied:
> [1/3] input: trackpoint - Enable doubletap by default on capable devices
>       commit: 9a98ebe630cf13c1a6063afa676d1cecc44fb2c9
> [2/3] platform/x86: thinkpad_acpi: Add sysfs control for TrackPoint double-tap
>       commit: 6227cc32fa01ffbf5bef8dcc6759743a28a2ad57
> [3/3] Documentation: thinkpad-acpi - Document doubletap_enable attribute
>       commit: fa5062e99b984448b7c8ca9aea47e7fc033b6e2f
>
> --
>  i.
>


-- 

Regards,

      Vishnu Sankar

^ permalink raw reply

* RE: [PATCH 0/2] Add THC Nova Lake device IDs
From: Xu, Even @ 2026-03-18  0:48 UTC (permalink / raw)
  To: Jiri Kosina
  Cc: bentiss@kernel.org, srinivas.pandruvada@linux.intel.com,
	linux-input@vger.kernel.org, linux-kernel@vger.kernel.org
In-Reply-To: <q73969s9-n2r4-10rp-1459-3nnp0322ro9p@xreary.bet>

Thanks Jiri!

Best Regards,
Even Xu

> -----Original Message-----
> From: Jiri Kosina <jikos@kernel.org>
> Sent: Wednesday, March 18, 2026 7:01 AM
> To: Xu, Even <even.xu@intel.com>
> Cc: bentiss@kernel.org; srinivas.pandruvada@linux.intel.com; linux-
> input@vger.kernel.org; linux-kernel@vger.kernel.org
> Subject: Re: [PATCH 0/2] Add THC Nova Lake device IDs
> 
> On Tue, 17 Mar 2026, Even Xu wrote:
> 
> > Add THC Device IDs for Nova Lake platform.
> >
> > Even Xu (2):
> >   Hid: Intel-thc-hid: Intel-quicki2c: Add NVL Device IDs
> >   Hid: Intel-thc-hid: Intel-quickspi: Add NVL Device IDs
> 
> Thanks for the patches, applied to hid.git#for-7.0/upstream-fixes.
> 
> (for your next submissions, please follow the subsystem standard convention and
> use prefix 'HID:' for the shortlog, thanks!)
> 
> --
> Jiri Kosina
> SUSE Labs


^ permalink raw reply

* Re: [PATCH 0/2] Add THC Nova Lake device IDs
From: Jiri Kosina @ 2026-03-17 23:01 UTC (permalink / raw)
  To: Even Xu; +Cc: bentiss, srinivas.pandruvada, linux-input, linux-kernel
In-Reply-To: <20260317055629.3877299-1-even.xu@intel.com>

On Tue, 17 Mar 2026, Even Xu wrote:

> Add THC Device IDs for Nova Lake platform.
> 
> Even Xu (2):
>   Hid: Intel-thc-hid: Intel-quicki2c: Add NVL Device IDs
>   Hid: Intel-thc-hid: Intel-quickspi: Add NVL Device IDs

Thanks for the patches, applied to hid.git#for-7.0/upstream-fixes.

(for your next submissions, please follow the subsystem standard 
convention and use prefix 'HID:' for the shortlog, thanks!)

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply


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