mirror of
https://github.com/torvalds/linux.git
synced 2026-11-06 21:58:01 +01:00
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:
parent
3774b1dd8c
commit
9417132df6
|
|
@ -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*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
493
drivers/hid/hid-nintendo-switch2-usb.c
Normal file
493
drivers/hid/hid-nintendo-switch2-usb.c
Normal 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
73
drivers/hid/hid-nintendo.h
Normal file
73
drivers/hid/hid-nintendo.h
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user