HID: nintendo: Add preliminary Switch 2 controller driver

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 designed, 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.

Reviewed-by: Silvan Jegen <s.jegen@gmail.com>
Tested-by: Silvan Jegen <s.jegen@gmail.com>
Signed-off-by: Vicki Pfau <vi@endrift.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
This commit is contained in:
Vicki Pfau 2026-10-05 23:16:07 -07:00 committed by Jiri Kosina
parent 3774b1dd8c
commit 9417132df6
7 changed files with 1864 additions and 10 deletions

View File

@ -19364,6 +19364,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*

View File

@ -869,10 +869,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.
@ -887,6 +890,17 @@ config NINTENDO_FF
controller, and the NSO N64 controller. For the pro controller, both
rumble motors can be controlled individually.
config 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 hid-nintendo-switch2-usb.
config HID_NTI
tristate "NTI keyboard adapters"
help

View File

@ -96,6 +96,7 @@ obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o
obj-$(CONFIG_HID_MSI) += hid-msi.o
obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o
obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o
obj-$(CONFIG_NINTENDO_SWITCH2_USB) += hid-nintendo-switch2-usb.o
obj-$(CONFIG_HID_NTI) += hid-nti.o
obj-$(CONFIG_HID_NTRIG) += hid-ntrig.o
obj-$(CONFIG_HID_NVIDIA_SHIELD) += hid-nvidia-shield.o

View File

@ -1116,6 +1116,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

View File

@ -0,0 +1,493 @@
// 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-ids.h"
#include "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;
enum switch2_urb_state {
NS2_URB_FREE,
NS2_URB_OUT,
NS2_URB_IN,
};
struct switch2_urb {
struct urb *urb;
uint8_t *data;
enum switch2_urb_state state;
};
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;
bool shutdown;
spinlock_t bulk_in_lock;
struct switch2_urb bulk_out[NS2_OUT_URBS];
struct usb_anchor bulk_out_anchor;
spinlock_t bulk_out_lock;
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:
case -EPIPE:
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);
if (ns2_usb->shutdown)
schedule = false;
for (i = 0; i < NS2_IN_URBS; i++) {
int err;
struct switch2_urb *ns2_urb;
if (ns2_usb->bulk_in[i].urb == urb) {
if (schedule) {
ns2_usb->bulk_in[i].state = NS2_URB_IN;
continue;
} else {
ns2_usb->bulk_in[i].state = NS2_URB_FREE;
}
}
if (ns2_usb->bulk_in[i].state != NS2_URB_FREE)
continue;
if (ns2_usb->shutdown)
continue;
/*
* We want exactly one bulk in URB scheduled at a time, so only
* reschedule this immediately if nothing else is scheduled
* currently.
*/
if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor))
continue;
ns2_urb = &ns2_usb->bulk_in[i];
if (!ns2_urb)
continue;
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->state = NS2_URB_OUT;
}
}
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;
case -EPIPE:
break;
default:
dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
break;
}
for (i = 0; i < NS2_OUT_URBS; i++) {
if (ns2_usb->bulk_out[i].urb != urb)
continue;
ns2_usb->bulk_out[i].state = NS2_URB_FREE;
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].state != NS2_URB_FREE)
continue;
urb = &ns2_usb->bulk_out[i];
urb->state = NS2_URB_OUT;
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_KERNEL);
if (ret) {
if (ret != -ENODEV)
dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret);
spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
urb->state = NS2_URB_FREE;
spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
usb_unanchor_urb(urb->urb);
return ret;
}
return 0;
}
static void switch2_usb_free_in_urbs(struct switch2_usb *ns2_usb)
{
struct urb *urb;
uint8_t *data;
int i;
unsigned long flags;
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
ns2_usb->shutdown = true;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
cancel_work_sync(&ns2_usb->message_in_work);
usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
for (i = 0; i < NS2_IN_URBS; i++) {
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
urb = ns2_usb->bulk_in[i].urb;
data = ns2_usb->bulk_in[i].data;
if (!urb) {
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
continue;
}
ns2_usb->bulk_in[i].urb = NULL;
ns2_usb->bulk_in[i].data = NULL;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
usb_kill_urb(urb);
usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
usb_free_urb(urb);
}
}
static void switch2_usb_free_out_urbs(struct switch2_usb *ns2_usb)
{
struct urb *urb;
uint8_t *data;
int i;
unsigned long flags;
usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
for (i = 0; i < NS2_OUT_URBS; i++) {
spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
urb = ns2_usb->bulk_out[i].urb;
data = ns2_usb->bulk_out[i].data;
if (!urb) {
spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
continue;
}
ns2_usb->bulk_out[i].urb = NULL;
ns2_usb->bulk_out[i].data = NULL;
spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
usb_kill_urb(urb);
usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
usb_free_urb(urb);
}
}
static void switch2_usb_shutdown(struct switch2_cfg_intf *intf)
{
struct switch2_usb *ns2_usb = (struct switch2_usb *)intf;
int i;
unsigned long flags;
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
ns2_usb->shutdown = true;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
cancel_work_sync(&ns2_usb->message_in_work);
usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
for (i = 0; i < NS2_IN_URBS; i++)
ns2_usb->bulk_in[i].state = NS2_URB_FREE;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
}
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;
int i;
unsigned long flags;
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
for (i = 0; i < NS2_IN_URBS; i++) {
urb = &ns2_usb->bulk_in[i];
if (urb->state != NS2_URB_IN)
continue;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
if (ns2_usb->cfg.parent) {
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);
} else {
dev_err(&ns2_usb->udev->dev,
"Got message before controller is fully set up; discarding\n");
}
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
if (!urb->urb)
/* We're shutting down and this URB was freed */
break;
urb->state = NS2_URB_FREE;
/*
* We want exactly one bulk in URB scheduled at a time, so only
* reschedule this immediately if nothing else is scheduled
* currently.
*/
if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor))
continue;
usb_anchor_urb(urb->urb, &ns2_usb->bulk_in_anchor);
err = usb_submit_urb(urb->urb, GFP_ATOMIC);
if (err) {
usb_unanchor_urb(urb->urb);
dev_dbg(&ns2_usb->udev->dev,
"failed to queue input urb: %d\n", err);
} else {
urb->state = NS2_URB_OUT;
}
}
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;
struct urb *urb;
uint8_t *data;
char phys[64];
int ret;
int i;
unsigned long flags;
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;
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);
ns2_usb->udev = udev;
for (i = 0; i < NS2_IN_URBS; i++) {
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
ret = -ENOMEM;
goto err_free_in;
}
data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
&urb->transfer_dma);
if (!data) {
usb_free_urb(urb);
ret = -ENOMEM;
goto err_free_in;
}
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
usb_fill_bulk_urb(urb, udev,
usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ns2_usb->bulk_in[i].urb = urb;
ns2_usb->bulk_in[i].data = data;
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
}
for (i = 0; i < NS2_OUT_URBS; i++) {
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
ret = -ENOMEM;
goto err_free_out;
}
data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
&urb->transfer_dma);
if (!data) {
usb_free_urb(urb);
ret = -ENOMEM;
goto err_free_out;
}
spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
usb_fill_bulk_urb(urb, udev,
usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ns2_usb->bulk_out[i].urb = urb;
ns2_usb->bulk_out[i].data = data;
spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
}
usb_set_intfdata(intf, ns2_usb);
ns2_usb->cfg.dev = &ns2_usb->udev->dev;
ns2_usb->cfg.send_command = switch2_usb_send_cmd;
spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
ns2_usb->bulk_in[0].state = NS2_URB_OUT;
usb_anchor_urb(ns2_usb->bulk_in[0].urb, &ns2_usb->bulk_in_anchor);
ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
if (ret < 0)
goto err_free_out;
ns2_usb->cfg.shutdown = switch2_usb_shutdown;
ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
if (ret < 0)
goto err_free_out;
return 0;
err_free_out:
switch2_usb_free_out_urbs(ns2_usb);
err_free_in:
switch2_usb_free_in_urbs(ns2_usb);
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);
switch2_usb_free_in_urbs(ns2_usb);
/*
* We need to detach *before* we kill the out URBs to make sure no
* further URBs get scheduled by the HID endpoint in the meantime.
*/
if (ns2_usb->cfg.parent)
switch2_controller_detach_cfg(ns2_usb->cfg.parent);
switch2_usb_free_out_urbs(ns2_usb);
}
#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 = "nintendo-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");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
/* 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);
void (*shutdown)(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