* [PATCH v11 0/3] HID: nintendo: Add preliminary Switch 2 controller
From: Vicki Pfau @ 2026-07-02 21:46 UTC (permalink / raw)
To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Vicki Pfau, Silvan Jegen
This series adds preliminary support for Switch 2 controllers using the
same split-driver model as previous versions. This is a minor iteration on
v10, fixing some small errors pointed out by sashiko. The remaining issues
sashiko found in v10 appear to be purely imagined. Hopefully it doesn't
come up with new things this time.
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 | 1680 ++++++++++++++++-
drivers/hid/hid-nintendo.h | 72 +
drivers/input/joystick/Kconfig | 11 +
drivers/input/joystick/Makefile | 1 +
drivers/input/joystick/nintendo-switch2-usb.c | 468 +++++
8 files changed, 2215 insertions(+), 41 deletions(-)
create mode 100644 drivers/hid/hid-nintendo.h
create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c
--
2.54.0
^ permalink raw reply
* [PATCH v11 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-07-02 21:47 UTC (permalink / raw)
To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260702214704.1859350-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 | 211 ++++++++++++++++++++++++++++++++++++-
2 files changed, 213 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 19c77c323ec9..851eed76c236 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -859,10 +859,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 e21c36921832..a36f4fd9a1da 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -37,6 +37,7 @@
#include <linux/unaligned.h>
#include <linux/delay.h>
#include <linux/device.h>
+#include <linux/devm-helpers.h>
#include <linux/kernel.h>
#include <linux/hid.h>
#include <linux/idr.h>
@@ -2989,6 +2990,7 @@ enum switch2_init_step {
NS2_INIT_READ_USER_SECONDARY_CALIB,
NS2_INIT_SET_FEATURE_MASK,
NS2_INIT_ENABLE_FEATURES,
+ NS2_INIT_ENABLE_RUMBLE,
NS2_INIT_GRIP_BUTTONS,
NS2_INIT_REPORT_FORMAT,
NS2_INIT_INPUT,
@@ -3020,6 +3022,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;
@@ -3043,8 +3057,45 @@ struct switch2_controller {
uint32_t player_id;
struct led_classdev *leds;
+
+#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;
+ uint64_t last_rumble_work;
+ struct delayed_work rumble_work;
+ uint8_t *rumble_buffer;
+#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-arbitrary 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);
@@ -3136,7 +3187,7 @@ static const uint8_t switch2_init_cmd_data[] = {
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,
+ NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
0x00, 0x00, 0x00
};
@@ -3209,6 +3260,125 @@ static void switch2_kref_put(struct kref *refcount)
kfree(ns2);
}
+#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);
+ unsigned long flags;
+
+ if (effect->type != FF_RUMBLE)
+ return 0;
+
+ spin_lock_irqsave(&ns2->rumble_lock, flags);
+ if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+ ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+ (uint16_t) (effect->u.rumble.weak_magnitude >> 1));
+ } else {
+ ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+ ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+ 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;
+ }
+ spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+ 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 flags;
+ bool active;
+ int ret = 0;
+
+ 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;
+ spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+ if (active) {
+ unsigned long interval = msecs_to_jiffies(4);
+ uint64_t current_jiffies = get_jiffies_64();
+
+ if (!ns2->last_rumble_work)
+ ns2->last_rumble_work = current_jiffies;
+ else
+ ns2->last_rumble_work += interval;
+
+ /* Reschedule a little early to make sure the buffer never underruns */
+ interval -= msecs_to_jiffies(2);
+ if (ns2->last_rumble_work + interval >= current_jiffies)
+ schedule_delayed_work(&ns2->rumble_work,
+ ns2->last_rumble_work + interval - current_jiffies);
+ else
+ schedule_delayed_work(&ns2->rumble_work, 0);
+ } else {
+ ns2->last_rumble_work = 0;
+ }
+
+ mutex_lock(&ns2->lock);
+ if (!ns2->hdev) {
+ cancel_delayed_work(&ns2->rumble_work);
+ } else {
+ ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
+ /*
+ * Don't log on ENODEV, ESHUTDOWN, or EPROTO, which can happen
+ * mid-hotplug. Also cancel any further work on ENODEV or
+ * ESHUTDOWN as they're clear indications that the endpoint
+ * is dead.
+ */
+ if (ret == -ENODEV || ret == -ESHUTDOWN)
+ cancel_delayed_work(&ns2->rumble_work);
+ else if (ret < 0 && ret != -EPROTO)
+ hid_warn_ratelimited(ns2->hdev,
+ "Failed to send output report ret=%d\n", ret);
+ }
+ mutex_unlock(&ns2->lock);
+}
+#endif
+
static int switch2_set_leds(struct switch2_controller *ns2)
{
int i;
@@ -3332,6 +3502,26 @@ static int switch2_init_input(struct switch2_controller *ns2)
return -EINVAL;
}
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+ ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
+ if (!ns2->rumble_buffer) {
+ input_free_device(input);
+ return -ENOMEM;
+ }
+ ret = devm_delayed_work_autocancel(&input->dev, &ns2->rumble_work, switch2_rumble_work);
+ if (ret < 0) {
+ input_free_device(input);
+ return ret;
+ }
+
+ 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)
@@ -3765,7 +3955,16 @@ 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_ENABLE_RUMBLE:
+ /*
+ * It is unclear what this packet is supposed to be for, but it
+ * appears to be needed for rumble to work reliably. The reply
+ * data indicates it might be a query of some sort, but we
+ * ignore the reply so long as it doesn't return an error.
+ */
+ return ns2->cfg->send_command(0x11, 1, NULL, 0, ns2->cfg);
case NS2_INIT_GRIP_BUTTONS:
if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
switch2_init_step_done(ns2, ns2->init_step);
@@ -3878,6 +4077,10 @@ int switch2_receive_command(struct switch2_controller *ns2,
switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
}
break;
+ case 0x11:
+ if (header->subcommand == 1)
+ switch2_init_step_done(ns2, NS2_INIT_ENABLE_RUMBLE);
+ break;
default:
break;
}
@@ -3997,6 +4200,10 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
else
ns2->player_id = ret;
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+ spin_lock_init(&ns2->rumble_lock);
+#endif
+
ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
if (ret) {
hid_err(hdev, "hw_start failed %d\n", ret);
--
2.54.0
^ permalink raw reply related
* [PATCH v11 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-07-02 21:47 UTC (permalink / raw)
To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260702214704.1859350-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 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.
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 | 1278 ++++++++++++++++-
drivers/hid/hid-nintendo.h | 72 +
drivers/input/joystick/Kconfig | 11 +
drivers/input/joystick/Makefile | 1 +
drivers/input/joystick/nintendo-switch2-usb.c | 468 ++++++
8 files changed, 1836 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 4ecd282f8f52..778982ab298e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19051,6 +19051,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 f9bcaeb66385..19c77c323ec9 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -843,10 +843,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 1059922baaac..9ba62b8fb894 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1103,6 +1103,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 e7302ec01ff1..e21c36921832 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:
@@ -2662,7 +2669,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);
@@ -2673,7 +2680,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;
@@ -2777,7 +2784,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;
@@ -2796,7 +2803,7 @@ static void nintendo_hid_remove(struct hid_device *hdev)
hid_hw_stop(hdev);
}
-static int nintendo_hid_resume(struct hid_device *hdev)
+static int joycon_resume(struct hid_device *hdev)
{
struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
int ret;
@@ -2819,7 +2826,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);
@@ -2838,7 +2845,1208 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
return 0;
}
+/*
+ * =============================================================================
+ * 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_INPUT,
+ NS2_INIT_SET_PLAYER_LEDS,
+ 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;
+ struct kref refcount;
+
+ 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;
+ int init_retries;
+ 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;
+};
+
+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 int switch2_init_controller(struct switch2_controller *ns2);
+
+static void switch2_init_step_done(struct switch2_controller *ns2, enum switch2_init_step step)
+{
+ if (ns2->init_step != step)
+ return;
+
+ ns2->init_retries = 0;
+ 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 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) {
+ if (kref_get_unless_zero(&ns2->refcount))
+ return ns2;
+ }
+ }
+ ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
+ if (!ns2)
+ return ERR_PTR(-ENOMEM);
+
+ kref_init(&ns2->refcount);
+ 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;
+
+ 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;
+ mutex_unlock(&ns2->lock);
+
+ if (input)
+ input_unregister_device(input);
+}
+
+static void switch2_kref_put(struct kref *refcount)
+{
+ struct switch2_controller *ns2 = container_of(refcount,
+ struct switch2_controller, refcount);
+
+ guard(mutex)(&switch2_controllers_lock);
+ list_del_init(&ns2->entry);
+ mutex_destroy(&ns2->lock);
+ kfree(ns2);
+}
+
+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 input_dev *input = to_input_dev(dev);
+ struct switch2_controller *ns2 = input_get_drvdata(input);
+
+ if (!ns2)
+ return -ENODEV;
+
+ guard(mutex)(&ns2->lock);
+ return switch2_set_leds(ns2);
+}
+
+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_input_ref(struct input_dev *input)
+{
+ struct switch2_controller *ns2 = input_get_drvdata(input);
+
+ kref_get(&ns2->refcount);
+
+ return 0;
+}
+
+static void switch2_input_deref(struct input_dev *input)
+{
+ struct switch2_controller *ns2 = input_get_drvdata(input);
+
+ kref_put(&ns2->refcount, switch2_kref_put);
+}
+
+static int switch2_init_input(struct switch2_controller *ns2)
+{
+ struct input_dev *input;
+ struct hid_device *hdev = ns2->hdev;
+ int player_led_pattern;
+ int i;
+ int ret;
+
+ rcu_read_lock();
+ input = rcu_dereference(ns2->input);
+ rcu_read_unlock();
+
+ if (input) {
+ switch2_init_step_done(ns2, NS2_INIT_INPUT);
+ return 0;
+ }
+
+ input = input_allocate_device();
+ if (!input)
+ return -ENOMEM;
+
+ input_set_drvdata(input, ns2);
+ input->open = switch2_input_ref;
+ input->close = switch2_input_deref;
+ 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);
+ input_free_device(input);
+ return ret;
+ }
+
+ player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS;
+ hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1);
+
+ ns2->leds = devm_kcalloc(&input->dev, JC_NUM_LEDS, sizeof(*ns2->leds), GFP_KERNEL);
+ if (!ns2->leds) {
+ hid_err(ns2->hdev, "Failed to allocate LEDs\n");
+ input_unregister_device(input);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < JC_NUM_LEDS; i++) {
+ struct led_classdev *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 | LED_RETAIN_AT_SHUTDOWN;
+ char *name = devm_kasprintf(&input->dev, GFP_KERNEL, "%s:%s:%s",
+ dev_name(&input->dev),
+ "green",
+ joycon_player_led_names[i]);
+
+ if (!name) {
+ dev_err(&input->dev, "Failed to allocate name for player %d LED; ret=%d\n",
+ i + 1, ret);
+ break;
+ }
+
+ 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);
+ break;
+ }
+ }
+
+ rcu_assign_pointer(ns2->input, input);
+ synchronize_rcu();
+
+ switch2_init_step_done(ns2, NS2_INIT_INPUT);
+ return switch2_init_controller(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 (ns2->hdev) {
+ 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 (ns2->hdev) {
+ 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];
+
+ if (ns2->hdev) {
+ 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 if (ns2->hdev) {
+ 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 (get_unaligned_le16((__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+ if (ns2->hdev)
+ hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+ break;
+ }
+
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
+ if (ns2->hdev) {
+ 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 (get_unaligned_le16((__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+ if (ns2->hdev)
+ hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+ break;
+ }
+
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
+ if (ns2->hdev) {
+ 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);
+
+ if (zero != 232)
+ value /= (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;
+ put_unaligned_le32(address, &message[4]);
+ 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);
+}
+
+int switch2_init_controller(struct switch2_controller *ns2)
+{
+ if (ns2->init_step == NS2_INIT_DONE)
+ return 0;
+
+ if (!ns2->cfg)
+ return -ENOTCONN;
+
+ if (ns2->init_retries > NS2_MAX_INIT_RETRIES) {
+ if (ns2->init_retries == NS2_MAX_INIT_RETRIES + 1) {
+ if (ns2->cfg)
+ dev_err(ns2->cfg->dev, "Failed to configure controller\n");
+ ns2->init_retries++;
+ }
+ return -EIO;
+ }
+
+ ns2->init_retries++;
+ 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_step);
+ 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_step);
+ 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_step);
+ 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_step);
+ 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_step);
+ return switch2_init_controller(ns2);
+ }
+ case NS2_INIT_INPUT:
+ if (ns2->hdev)
+ return switch2_init_input(ns2);
+ break;
+ case NS2_INIT_SET_PLAYER_LEDS:
+ return switch2_set_player_id(ns2, ns2->player_id);
+ case NS2_INIT_FINISH:
+ return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
+ switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
+ 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);
+
+ mutex_lock(&ns2->lock);
+
+ header = (const struct switch2_cmd_header *)message;
+ if (!(header->flags & NS2_FLAG_OK)) {
+ if (ns2->cfg)
+ dev_warn(ns2->cfg->dev, "Packet error %02x replying to command %x:%x",
+ header->flags, header->command, header->subcommand);
+ ret = -EIO;
+ goto exit;
+ }
+ message = &message[8];
+ length -= 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 < 8) {
+ ret = -EINVAL;
+ goto exit;
+ }
+ read_size = message[0];
+ read_address = get_unaligned_le32(&message[4]);
+ if (length < read_size + 8) {
+ 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_FINISH);
+ 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);
+
+ mutex_unlock(&ns2->lock);
+ 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);
+ int ret = 0;
+
+ if (IS_ERR(ns2))
+ return PTR_ERR(ns2);
+
+ mutex_lock(&ns2->lock);
+ if (ns2->cfg) {
+ ret = -EBUSY;
+ goto out;
+ }
+ cfg->parent = ns2;
+ ns2->cfg = cfg;
+
+ if (ns2->hdev)
+ ret = switch2_init_controller(ns2);
+
+ if (ret < 0) {
+ cfg->parent = NULL;
+ ns2->cfg = NULL;
+ }
+
+out:
+ mutex_unlock(&ns2->lock);
+
+ if (ret < 0)
+ kref_put(&ns2->refcount, switch2_kref_put);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg);
+
+void switch2_controller_detach_cfg(struct switch2_controller *ns2)
+{
+ mutex_lock(&ns2->lock);
+ if (!ns2->cfg || WARN_ON(ns2 != ns2->cfg->parent)) {
+ mutex_unlock(&ns2->lock);
+ return;
+ }
+ ns2->cfg->parent = NULL;
+ ns2->cfg = NULL;
+ mutex_unlock(&ns2->lock);
+ switch2_controller_put(ns2);
+ kref_put(&ns2->refcount, switch2_kref_put);
+}
+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;
+ }
+
+ ns2 = switch2_get_controller(phys);
+ if (IS_ERR(ns2))
+ return PTR_ERR(ns2);
+
+ mutex_lock(&ns2->lock);
+ if (ns2->hdev) {
+ mutex_unlock(&ns2->lock);
+ hid_err(hdev,
+ "Second hdev tried to claim same controller, first=%p vs second=%p\n",
+ ns2->hdev, hdev);
+ kref_put(&ns2->refcount, switch2_kref_put);
+ return -EBUSY;
+ }
+ ns2->hdev = hdev;
+ hid_set_drvdata(hdev, ns2);
+
+ 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;
+
+ ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (ret) {
+ hid_err(hdev, "hw_start failed %d\n", ret);
+ goto err_cleanup;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "hw_open failed %d\n", ret);
+ goto err_stop;
+ }
+
+ ret = 0;
+ if (ns2->cfg)
+ ret = switch2_init_controller(ns2);
+
+ if (!ret) {
+ mutex_unlock(&ns2->lock);
+ return 0;
+ }
+
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+err_cleanup:
+ ida_free(&nintendo_player_id_allocator, ns2->player_id);
+ ns2->hdev = NULL;
+ mutex_unlock(&ns2->lock);
+ switch2_controller_put(ns2);
+ kref_put(&ns2->refcount, switch2_kref_put);
+
+ return ret;
+}
+
+static void switch2_remove(struct hid_device *hdev)
+{
+ struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+
+ switch2_controller_put(ns2);
+ mutex_lock(&ns2->lock);
+ ns2->hdev = NULL;
+ ida_free(&nintendo_player_id_allocator, ns2->player_id);
+ mutex_unlock(&ns2->lock);
+ kref_put(&ns2->refcount, switch2_kref_put);
+ hid_hw_close(hdev);
+ 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,
@@ -2863,10 +4071,67 @@ static const struct hid_device_id nintendo_hid_devices[] = {
USB_DEVICE_ID_NINTENDO_N64CON) },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_HORI,
USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) },
+ /* 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);
+}
+
+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);
+}
+
static struct hid_driver nintendo_hid_driver = {
.name = "nintendo",
.id_table = nintendo_hid_devices,
@@ -2894,4 +4159,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 000000000000..7aff22f30266
--- /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 7755e5b454d2..868262c6ccd9 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 9976f596a920..8f92900ae885 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 000000000000..a6999a0a26ae
--- /dev/null
+++ b/drivers/input/joystick/nintendo-switch2-usb.c
@@ -0,0 +1,468 @@
+// 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;
+
+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:
+ break;
+ 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) {
+ 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;
+
+ /*
+ * 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) || ns2_usb->shutdown)
+ 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:
+ 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_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);
+ 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) || ns2_usb->shutdown)
+ 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;
+
+ ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
+ if (ret < 0)
+ goto err_free_out;
+
+ return 0;
+
+err_free_out:
+ 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_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+ usb_free_urb(urb);
+ }
+err_free_in:
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ ns2_usb->shutdown = true;
+ spin_unlock_irqrestore(&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++) {
+ 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_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+ usb_free_urb(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;
+ struct urb *urb;
+ uint8_t *data;
+ int i;
+
+ /* Prevent any further IN URBs from being scheduled */
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ ns2_usb->shutdown = true;
+ spin_unlock_irqrestore(&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++) {
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ urb = ns2_usb->bulk_in[i].urb;
+ data = ns2_usb->bulk_in[i].data;
+ ns2_usb->bulk_in[i].urb = NULL;
+ ns2_usb->bulk_in[i].data = NULL;
+ spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+ usb_free_urb(urb);
+ }
+
+ /*
+ * 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.
+ */
+ switch2_controller_detach_cfg(ns2_usb->cfg.parent);
+
+ 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;
+ ns2_usb->bulk_out[i].urb = NULL;
+ ns2_usb->bulk_out[i].data = NULL;
+ spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+ usb_free_urb(urb);
+ }
+}
+
+#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");
--
2.54.0
^ permalink raw reply related
* [PATCH v11 3/3] HID: nintendo: Add unified report format support
From: Vicki Pfau @ 2026-07-02 21:47 UTC (permalink / raw)
To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260702214704.1859350-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 | 151 +++++++++++++++++++++++++++++++++++--
1 file changed, 146 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index a36f4fd9a1da..ca2126f85bb3 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2873,6 +2873,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
@@ -3121,6 +3151,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, },
@@ -3138,6 +3184,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, },
@@ -3159,6 +3222,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, },
@@ -3176,6 +3260,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
@@ -3802,11 +3903,51 @@ 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.
- */
+ if (size < 0x3f)
+ return -EINVAL;
+
+ 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.54.0
^ permalink raw reply related
* Re: [PATCH v11 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: sashiko-bot @ 2026-07-02 22:04 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702214704.1859350-2-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Concurrent URB completion and device disconnect paths cause a Use-After-Free due to missing synchronization.
- [High] Immediate resubmission of failed bulk IN URBs causes an interrupt storm.
--
Note: The format of this review has been altered to use a free-form summary of findings rather than inline patch quoting due to recitation restrictions.
commit 77b4d4df4f2d3eefe6970b5429bc05e1a86ecc7e
Author: Vicki Pfau <vi@endrift.com>
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.
[Severity: High]
In switch2_bulk_in(), could this cause a use-after-free during device
disconnect?
When switch2_usb_disconnect() executes concurrently with an URB completion,
it calls usb_kill_anchored_urbs() to cancel in-flight URBs. However, the USB
core unanchors the URB just before invoking the completion handler. Thus,
usb_kill_anchored_urbs() misses the URB being processed:
switch2_usb_disconnect()
usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
cancel_work_sync(&ns2_usb->message_in_work);
If switch2_bulk_in() is preempted before it schedules work:
switch2_bulk_in()
...
if (schedule)
schedule_work(&ns2_usb->message_in_work);
the disconnect path will complete and free ns2_usb via devres. When the
completion handler resumes, it attempts to access the freed context. Does
this missing synchronization need to be addressed?
[Severity: High]
Can an interrupt storm occur if a bulk IN URB fails persistently?
In switch2_bulk_in(), if an URB completes with an error like -EPIPE or
-EPROTO, the schedule boolean is false, but the URB state is set to
NS2_URB_FREE. The driver then immediately evaluates it for resubmission:
switch2_bulk_in()
...
usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor);
err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
Since the device isn't shutting down and the anchor is empty, the exact same
failing URB is resubmitted instantly. This will likely fail again right
away, re-invoking the handler and creating an infinite loop. Should the
resubmission logic be delayed or halted upon encountering persistent
errors?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702214704.1859350-1-vi@endrift.com?part=1
^ permalink raw reply
* Re: [PATCH v11 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: sashiko-bot @ 2026-07-02 22:04 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260702214704.1859350-3-vi@endrift.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] Use-After-Free of rumble_work and controller state due to devm lifecycle mismatch
Pre-existing issues:
- [High] Circular reference in LED devm registration causes permanent memory leak and Use-After-Free
--
commit 24b9a46372beedd9b4a7b187b2ec2e8fa3058997
Author: Vicki Pfau <vi@endrift.com>
HID: nintendo: Add rumble support for Switch 2 controllers
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.
> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index e21c369218325..a36f4fd9a1da5 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
[ ... ]
> @@ -3209,6 +3260,125 @@ static void switch2_kref_put(struct kref *refcount)
> kfree(ns2);
> }
>
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
[ ... ]
> +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 flags;
> + bool active;
> + int ret = 0;
> +
> + spin_lock_irqsave(&ns2->rumble_lock, flags);
[Severity: High]
If the device is unplugged and `ns2` is manually freed, can this work
function execute concurrently and attempt to acquire `ns2->rumble_lock`
on freed memory?
> + ns2->rumble_buffer[0x1] = 0x50 | ns2->rumble_seq;
[ ... ]
> @@ -3332,6 +3502,26 @@ static int switch2_init_input(struct switch2_controller *ns2)
> return -EINVAL;
> }
>
[Severity: High]
This is a pre-existing issue, but does registering the LED devices with
devm create a circular reference by binding the LED devres to the input
device while simultaneously setting it as the parent?
The driver core's device_add() function automatically takes a reference to
the parent device (&input->dev). Since devres is attached to the same
parent, it waits for the parent's refcount to drop to 0 before unregistering
the LEDs, which means neither can be released.
Does this permanently leak the input device, LED devices, and devm
allocations?
Furthermore, because `ns2` is freed manually upon device removal, could
lingering LED sysfs attributes trigger a use-after-free crash if a user
writes to them, causing switch2_player_led_brightness_set() to dereference
the freed `ns2` pointer?
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> + ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
> + if (!ns2->rumble_buffer) {
> + input_free_device(input);
> + return -ENOMEM;
> + }
> + ret = devm_delayed_work_autocancel(&input->dev, &ns2->rumble_work, switch2_rumble_work);
[Severity: High]
Does binding the rumble work lifecycle to &input->dev cause a
use-after-free when `ns2` is manually freed?
If a user unplugs the controller while holding the input device open,
`ns2` is freed in the removal path, but the devres cleanup for the input
device will run later.
Will this result in the devres cleanup unconditionally calling
cancel_delayed_work_sync(&ns2->rumble_work) on the already-freed
`ns2` pointer?
> + if (ret < 0) {
> + input_free_device(input);
> + return ret;
> + }
> +
> + input_set_capability(input, EV_FF, FF_RUMBLE);
> + ret = input_ff_create_memless(input, NULL, switch2_play_effect);
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260702214704.1859350-1-vi@endrift.com?part=2
^ permalink raw reply
* [PATCH 01/10] HID: steam: Update documentation
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
Mark myself as the maintainer, as well as adding myself as an author.
It also makes some minor updates to comments, such as correcly calling the
left menu key view and retroactively renaming the original Steam Controller
as Steam Controller (2015), in preparation for support for the 2026 model.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
MAINTAINERS | 6 ++++++
drivers/hid/hid-steam.c | 13 +++++++------
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/MAINTAINERS b/MAINTAINERS
index 4ecd282f8f52..2ed29e970023 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11500,6 +11500,12 @@ F: drivers/hid/hid-sensor-*
F: drivers/iio/*/hid-*
F: include/linux/hid-sensor-*
+HID STEAM CONTROLLER
+M: Vicki Pfau <vi@endrift.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-steam.c
+
HID VRC-2 CAR CONTROLLER DRIVER
M: Marcus Folkesson <marcus.folkesson@gmail.com>
L: linux-input@vger.kernel.org
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 197126d6e081..a854d6360a0e 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -48,6 +48,7 @@
MODULE_DESCRIPTION("HID driver for Valve Steam Controller");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Rodrigo Rivas Costa <rodrigorivascosta@gmail.com>");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
static bool lizard_mode = true;
@@ -1413,9 +1414,9 @@ static inline s16 steam_le16(u8 *data)
* 9.1 | BTN_DPAD_RIGHT | left-pad right
* 9.2 | BTN_DPAD_LEFT | left-pad left
* 9.3 | BTN_DPAD_DOWN | left-pad down
- * 9.4 | BTN_SELECT | menu left
+ * 9.4 | BTN_SELECT | view
* 9.5 | BTN_MODE | steam logo
- * 9.6 | BTN_START | menu right
+ * 9.6 | BTN_START | menu
* 9.7 | BTN_GRIPL | left back lever
* 10.0 | BTN_GRIPR | right back lever
* 10.1 | -- | left-pad clicked
@@ -1541,9 +1542,9 @@ static void steam_do_input_event(struct steam_device *steam,
* 9.1 | BTN_DPAD_RIGHT | left-pad right
* 9.2 | BTN_DPAD_LEFT | left-pad left
* 9.3 | BTN_DPAD_DOWN | left-pad down
- * 9.4 | BTN_SELECT | menu left
+ * 9.4 | BTN_SELECT | view
* 9.5 | BTN_MODE | steam logo
- * 9.6 | BTN_START | menu right
+ * 9.6 | BTN_START | menu
* 9.7 | BTN_GRIPL2 | left bottom grip button
* 10.0 | BTN_GRIPR2 | right bottom grip button
* 10.1 | BTN_THUMB | left pad pressed
@@ -1850,11 +1851,11 @@ MODULE_PARM_DESC(lizard_mode,
"Enable mouse and keyboard emulation (lizard mode) when the gamepad is not in use");
static const struct hid_device_id steam_controllers[] = {
- { /* Wired Steam Controller */
+ { /* Wired Steam Controller (2015) */
HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
USB_DEVICE_ID_STEAM_CONTROLLER)
},
- { /* Wireless Steam Controller */
+ { /* Wireless Steam Controller (2015) */
HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS),
.driver_data = STEAM_QUIRK_WIRELESS
--
2.54.0
^ permalink raw reply related
* [PATCH 02/10] HID: steam: Refactor and clean up report parsing
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
This switches from a parsing style where each button or axis is parsed
individually out of a report using !!(byte & BIT(x)) style. This commit
switches it to a mostly unified approach of defining a list of individual
mappings in an array and passing it to a function that handles all of the
extraction. Theoretically this is more lines, but in practice it results in
(subjectively) cleaner code. Some exceptions still need to be made for
things like handling the lizard mode toggle key, but in general there's a
lot less manual code.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 211 ++++++++++++++++++++++++----------------
1 file changed, 128 insertions(+), 83 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index a854d6360a0e..1b0367c98eac 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -43,6 +43,7 @@
#include <linux/rcupdate.h>
#include <linux/delay.h>
#include <linux/power_supply.h>
+#include <linux/unaligned.h>
#include "hid-ids.h"
MODULE_DESCRIPTION("HID driver for Valve Steam Controller");
@@ -1355,13 +1356,45 @@ static void steam_do_connect_event(struct steam_device *steam, bool connected)
* Clamp the values to 32767..-32767 so that the range is
* symmetrical and can be negated safely.
*/
-static inline s16 steam_le16(u8 *data)
+static inline s16 steam_le16(const u8 *data)
{
- s16 x = (s16) le16_to_cpup((__le16 *)data);
+ s16 x = (s16) get_unaligned_le16((__le16 *)data);
return x == -32768 ? -32767 : x;
}
+struct steam_button_mapping {
+ int code;
+ u8 byte;
+ u8 bit;
+};
+
+struct steam_axis_mapping {
+ int code;
+ s8 sign;
+ u8 byte;
+};
+
+static void steam_map_buttons(struct input_dev *input,
+ const struct steam_button_mapping *mappings, const u8 *data)
+{
+ const struct steam_button_mapping *mapping;
+
+ for (mapping = mappings; mapping->code; mapping++)
+ input_report_key(input, mapping->code,
+ data[mapping->byte] & BIT(mapping->bit));
+}
+
+static void steam_map_axes(struct input_dev *input,
+ const struct steam_axis_mapping *mappings, const u8 *data)
+{
+ const struct steam_axis_mapping *mapping;
+
+ for (mapping = mappings; mapping->sign; mapping++)
+ input_report_abs(input, mapping->code,
+ mapping->sign * steam_le16(&data[mapping->byte]));
+}
+
/*
* The size for this message payload is 60.
* The known values are:
@@ -1428,18 +1461,42 @@ static inline s16 steam_le16(u8 *data)
* 10.7 | -- | lpad_and_joy
*/
+static const struct steam_button_mapping steam_controller_button_mappings[] = {
+ { BTN_TR2, 8, 0 },
+ { BTN_TL2, 8, 1 },
+ { BTN_TR, 8, 2 },
+ { BTN_TL, 8, 3 },
+ { BTN_Y, 8, 4 },
+ { BTN_B, 8, 5 },
+ { BTN_X, 8, 6 },
+ { BTN_A, 8, 7 },
+ { BTN_SELECT, 9, 4 },
+ { BTN_MODE, 9, 5 },
+ { BTN_START, 9, 6 },
+ { BTN_GRIPL, 9, 7 },
+ { BTN_GRIPR, 10, 0 },
+ { BTN_THUMBR, 10, 2 },
+ { BTN_THUMBL, 10, 6 },
+ { BTN_THUMB2, 10, 4 },
+ { BTN_DPAD_UP, 9, 0 },
+ { BTN_DPAD_RIGHT, 9, 1 },
+ { BTN_DPAD_LEFT, 9, 2 },
+ { BTN_DPAD_DOWN, 9, 3 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_controller_axis_mappings[] = {
+ { ABS_RX, 1, 20 },
+ { ABS_RY, -1, 22 },
+ { /* sentinel */ },
+};
+
static void steam_do_input_event(struct steam_device *steam,
struct input_dev *input, u8 *data)
{
- /* 24 bits of buttons */
- u8 b8, b9, b10;
s16 x, y;
bool lpad_touched, lpad_and_joy;
- b8 = data[8];
- b9 = data[9];
- b10 = data[10];
-
input_report_abs(input, ABS_HAT2Y, data[11]);
input_report_abs(input, ABS_HAT2X, data[12]);
@@ -1451,8 +1508,8 @@ static void steam_do_input_event(struct steam_device *steam,
* joystick values.
* (lpad_touched || lpad_and_joy) tells if the lpad is really touched.
*/
- lpad_touched = b10 & BIT(3);
- lpad_and_joy = b10 & BIT(7);
+ lpad_touched = data[10] & BIT(3);
+ lpad_and_joy = data[10] & BIT(7);
x = steam_le16(data + 16);
y = -steam_le16(data + 18);
@@ -1468,31 +1525,10 @@ static void steam_do_input_event(struct steam_device *steam,
input_report_abs(input, ABS_HAT0X, 0);
input_report_abs(input, ABS_HAT0Y, 0);
}
+ input_report_key(input, BTN_THUMB, lpad_touched || lpad_and_joy);
- input_report_abs(input, ABS_RX, steam_le16(data + 20));
- input_report_abs(input, ABS_RY, -steam_le16(data + 22));
-
- input_event(input, EV_KEY, BTN_TR2, !!(b8 & BIT(0)));
- input_event(input, EV_KEY, BTN_TL2, !!(b8 & BIT(1)));
- input_event(input, EV_KEY, BTN_TR, !!(b8 & BIT(2)));
- input_event(input, EV_KEY, BTN_TL, !!(b8 & BIT(3)));
- input_event(input, EV_KEY, BTN_Y, !!(b8 & BIT(4)));
- input_event(input, EV_KEY, BTN_B, !!(b8 & BIT(5)));
- input_event(input, EV_KEY, BTN_X, !!(b8 & BIT(6)));
- input_event(input, EV_KEY, BTN_A, !!(b8 & BIT(7)));
- input_event(input, EV_KEY, BTN_SELECT, !!(b9 & BIT(4)));
- input_event(input, EV_KEY, BTN_MODE, !!(b9 & BIT(5)));
- input_event(input, EV_KEY, BTN_START, !!(b9 & BIT(6)));
- input_event(input, EV_KEY, BTN_GRIPL, !!(b9 & BIT(7)));
- input_event(input, EV_KEY, BTN_GRIPR, !!(b10 & BIT(0)));
- input_event(input, EV_KEY, BTN_THUMBR, !!(b10 & BIT(2)));
- input_event(input, EV_KEY, BTN_THUMBL, !!(b10 & BIT(6)));
- input_event(input, EV_KEY, BTN_THUMB, lpad_touched || lpad_and_joy);
- input_event(input, EV_KEY, BTN_THUMB2, !!(b10 & BIT(4)));
- input_event(input, EV_KEY, BTN_DPAD_UP, !!(b9 & BIT(0)));
- input_event(input, EV_KEY, BTN_DPAD_RIGHT, !!(b9 & BIT(1)));
- input_event(input, EV_KEY, BTN_DPAD_LEFT, !!(b9 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_DOWN, !!(b9 & BIT(3)));
+ steam_map_buttons(input, steam_controller_button_mappings, data);
+ steam_map_axes(input, steam_controller_axis_mappings, data);
input_sync(input);
}
@@ -1595,23 +1631,67 @@ static void steam_do_input_event(struct steam_device *steam,
* 15.6 | -- | unknown
* 15.7 | -- | unknown
*/
+
+static const struct steam_button_mapping steam_deck_button_mappings[] = {
+ { BTN_TR2, 8, 0 },
+ { BTN_TL2, 8, 1 },
+ { BTN_TR, 8, 2 },
+ { BTN_TL, 8, 3 },
+ { BTN_Y, 8, 4 },
+ { BTN_B, 8, 5 },
+ { BTN_X, 8, 6 },
+ { BTN_A, 8, 7 },
+ { BTN_SELECT, 9, 4 },
+ { BTN_MODE, 9, 5 },
+ { BTN_START, 9, 6 },
+ { BTN_GRIPL2, 9, 7 },
+ { BTN_GRIPR2, 10, 0 },
+ { BTN_THUMBL, 10, 6 },
+ { BTN_THUMBR, 11, 2 },
+ { BTN_DPAD_UP, 9, 0 },
+ { BTN_DPAD_RIGHT, 9, 1 },
+ { BTN_DPAD_LEFT, 9, 2 },
+ { BTN_DPAD_DOWN, 9, 3 },
+ { BTN_THUMB, 10, 1 },
+ { BTN_THUMB2, 10, 2 },
+ { BTN_GRIPL, 13, 1 },
+ { BTN_GRIPR, 13, 2 },
+ { BTN_BASE, 14, 2 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_deck_axis_mappings[] = {
+ { ABS_X, 1, 48 },
+ { ABS_Y, -1, 50 },
+ { ABS_RX, 1, 52 },
+ { ABS_RY, -1, 54 },
+ { ABS_HAT2Y, 1, 44 },
+ { ABS_HAT2X, 1, 46 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_deck_imu_mappings[] = {
+ { ABS_X, 1, 24 },
+ { ABS_Z, -1, 26 },
+ { ABS_Y, 1, 28 },
+ { ABS_RX, 1, 30 },
+ { ABS_RZ, -1, 32 },
+ { ABS_RY, 1, 34 },
+ { /* sentinel */ },
+};
+
static void steam_do_deck_input_event(struct steam_device *steam,
struct input_dev *input, u8 *data)
{
- u8 b8, b9, b10, b11, b13, b14;
+ bool start_pressed;
bool lpad_touched, rpad_touched;
- b8 = data[8];
- b9 = data[9];
- b10 = data[10];
- b11 = data[11];
- b13 = data[13];
- b14 = data[14];
+ start_pressed = data[9] & BIT(6);
- if (!(b9 & BIT(6)) && steam->did_mode_switch) {
+ if (!start_pressed && steam->did_mode_switch) {
steam->did_mode_switch = false;
cancel_delayed_work(&steam->mode_switch);
- } else if (!steam->client_opened && (b9 & BIT(6)) && !steam->did_mode_switch) {
+ } else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
steam->did_mode_switch = true;
schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
}
@@ -1619,8 +1699,8 @@ static void steam_do_deck_input_event(struct steam_device *steam,
if (!steam->gamepad_mode && lizard_mode)
return;
- lpad_touched = b10 & BIT(3);
- rpad_touched = b10 & BIT(4);
+ lpad_touched = data[10] & BIT(3);
+ rpad_touched = data[10] & BIT(4);
if (lpad_touched) {
input_report_abs(input, ABS_HAT0X, steam_le16(data + 16));
@@ -1638,38 +1718,8 @@ static void steam_do_deck_input_event(struct steam_device *steam,
input_report_abs(input, ABS_HAT1Y, 0);
}
- input_report_abs(input, ABS_X, steam_le16(data + 48));
- input_report_abs(input, ABS_Y, -steam_le16(data + 50));
- input_report_abs(input, ABS_RX, steam_le16(data + 52));
- input_report_abs(input, ABS_RY, -steam_le16(data + 54));
-
- input_report_abs(input, ABS_HAT2Y, steam_le16(data + 44));
- input_report_abs(input, ABS_HAT2X, steam_le16(data + 46));
-
- input_event(input, EV_KEY, BTN_TR2, !!(b8 & BIT(0)));
- input_event(input, EV_KEY, BTN_TL2, !!(b8 & BIT(1)));
- input_event(input, EV_KEY, BTN_TR, !!(b8 & BIT(2)));
- input_event(input, EV_KEY, BTN_TL, !!(b8 & BIT(3)));
- input_event(input, EV_KEY, BTN_Y, !!(b8 & BIT(4)));
- input_event(input, EV_KEY, BTN_B, !!(b8 & BIT(5)));
- input_event(input, EV_KEY, BTN_X, !!(b8 & BIT(6)));
- input_event(input, EV_KEY, BTN_A, !!(b8 & BIT(7)));
- input_event(input, EV_KEY, BTN_SELECT, !!(b9 & BIT(4)));
- input_event(input, EV_KEY, BTN_MODE, !!(b9 & BIT(5)));
- input_event(input, EV_KEY, BTN_START, !!(b9 & BIT(6)));
- input_event(input, EV_KEY, BTN_GRIPL2, !!(b9 & BIT(7)));
- input_event(input, EV_KEY, BTN_GRIPR2, !!(b10 & BIT(0)));
- input_event(input, EV_KEY, BTN_THUMBL, !!(b10 & BIT(6)));
- input_event(input, EV_KEY, BTN_THUMBR, !!(b11 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_UP, !!(b9 & BIT(0)));
- input_event(input, EV_KEY, BTN_DPAD_RIGHT, !!(b9 & BIT(1)));
- input_event(input, EV_KEY, BTN_DPAD_LEFT, !!(b9 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_DOWN, !!(b9 & BIT(3)));
- input_event(input, EV_KEY, BTN_THUMB, !!(b10 & BIT(1)));
- input_event(input, EV_KEY, BTN_THUMB2, !!(b10 & BIT(2)));
- input_event(input, EV_KEY, BTN_GRIPL, !!(b13 & BIT(1)));
- input_event(input, EV_KEY, BTN_GRIPR, !!(b13 & BIT(2)));
- input_event(input, EV_KEY, BTN_BASE, !!(b14 & BIT(2)));
+ steam_map_buttons(input, steam_deck_button_mappings, data);
+ steam_map_axes(input, steam_deck_axis_mappings, data);
input_sync(input);
}
@@ -1690,12 +1740,7 @@ static void steam_do_deck_sensors_event(struct steam_device *steam,
return;
input_event(sensors, EV_MSC, MSC_TIMESTAMP, steam->sensor_timestamp_us);
- input_report_abs(sensors, ABS_X, steam_le16(data + 24));
- input_report_abs(sensors, ABS_Z, -steam_le16(data + 26));
- input_report_abs(sensors, ABS_Y, steam_le16(data + 28));
- input_report_abs(sensors, ABS_RX, steam_le16(data + 30));
- input_report_abs(sensors, ABS_RZ, -steam_le16(data + 32));
- input_report_abs(sensors, ABS_RY, steam_le16(data + 34));
+ steam_map_axes(sensors, steam_deck_imu_mappings, data);
input_sync(sensors);
}
--
2.54.0
^ permalink raw reply related
* [PATCH 03/10] HID: steam: Rename some constants that got renamed upstream
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
SETTING_MOUSE_POINTER_ENABLED was renamed to SETTING_LIZARD_MODE upstream.
SETTING_GYRO_MODE was renamed to SETTING_IMU_MODE in an older commit, but
the associated enum was overlooked.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 1b0367c98eac..add9f4f27fff 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -151,7 +151,7 @@ enum {
SETTING_USB_DEBUG_MODE,
SETTING_LEFT_TRACKPAD_MODE,
SETTING_RIGHT_TRACKPAD_MODE,
- SETTING_MOUSE_POINTER_ENABLED,
+ SETTING_LIZARD_MODE,
/* 10 */
SETTING_DPAD_DEADZONE,
@@ -261,14 +261,14 @@ enum {
ATTRIB_STR_UNIT_SERIAL,
};
-/* Values for GYRO_MODE (bitmask) */
+/* Values for IMU_MODE (bitmask) */
enum {
- SETTING_GYRO_MODE_OFF = 0,
- SETTING_GYRO_MODE_STEERING = BIT(0),
- SETTING_GYRO_MODE_TILT = BIT(1),
- SETTING_GYRO_MODE_SEND_ORIENTATION = BIT(2),
- SETTING_GYRO_MODE_SEND_RAW_ACCEL = BIT(3),
- SETTING_GYRO_MODE_SEND_RAW_GYRO = BIT(4),
+ SETTING_IMU_MODE_OFF = 0,
+ SETTING_IMU_MODE_STEERING = BIT(0),
+ SETTING_IMU_MODE_TILT = BIT(1),
+ SETTING_IMU_MODE_SEND_ORIENTATION = BIT(2),
+ SETTING_IMU_MODE_SEND_RAW_ACCEL = BIT(3),
+ SETTING_IMU_MODE_SEND_RAW_GYRO = BIT(4),
};
/* Trackpad modes */
--
2.54.0
^ permalink raw reply related
* [PATCH 05/10] HID: steam: Coalesce rumble packets
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
The Steam Deck resets the haptic pattern every time it receives a rumble
packet, leading to weird discontinuities or sometimes cutting out entirely.
Instead of overloading the interface, Steam interally rate-limits sending
these packets, so we should too.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 34653ad383ac..01b64194ec97 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -343,6 +343,7 @@ struct steam_device {
bool did_mode_switch;
bool gamepad_mode;
struct work_struct rumble_work;
+ struct delayed_work coalesce_rumble_work;
u16 rumble_left;
u16 rumble_right;
unsigned int sensor_timestamp_us;
@@ -603,10 +604,26 @@ static void steam_haptic_rumble_cb(struct work_struct *work)
{
struct steam_device *steam = container_of(work, struct steam_device,
rumble_work);
+
+ guard(mutex)(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
}
+static void steam_coalesce_rumble_cb(struct work_struct *work)
+{
+ struct steam_device *steam = container_of(to_delayed_work(work),
+ struct steam_device,
+ coalesce_rumble_work);
+
+ guard(mutex)(&steam->report_mutex);
+ steam_haptic_rumble(steam, 0, steam->rumble_left,
+ steam->rumble_right, 2, 0);
+
+ if (steam->rumble_left || steam->rumble_right)
+ schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
+}
+
#ifdef CONFIG_STEAM_FF
static int steam_play_effect(struct input_dev *dev, void *data,
struct ff_effect *effect)
@@ -616,6 +633,14 @@ static int steam_play_effect(struct input_dev *dev, void *data,
steam->rumble_left = effect->u.rumble.strong_magnitude;
steam->rumble_right = effect->u.rumble.weak_magnitude;
+ /*
+ * The interface gets somewhat overloaded when too many rumble
+ * packets are sent in a row, so Steam throttles it to 20 Hz
+ */
+ if (delayed_work_pending(&steam->coalesce_rumble_work))
+ return 0;
+
+ schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
return schedule_work(&steam->rumble_work);
}
#endif
@@ -1360,6 +1385,7 @@ static int steam_probe(struct hid_device *hdev,
INIT_DELAYED_WORK(&steam->mode_switch, steam_mode_switch_cb);
INIT_LIST_HEAD(&steam->list);
INIT_WORK(&steam->rumble_work, steam_haptic_rumble_cb);
+ INIT_DELAYED_WORK(&steam->coalesce_rumble_work, steam_coalesce_rumble_cb);
steam->sensor_timestamp_us = 0;
if (steam->quirks & STEAM_QUIRK_DECK)
steam->sensor_update_rate_us = 4000;
@@ -1426,6 +1452,7 @@ static int steam_probe(struct hid_device *hdev,
cancel_work_sync(&steam->work_connect);
cancel_delayed_work_sync(&steam->mode_switch);
cancel_work_sync(&steam->rumble_work);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
cancel_work_sync(&steam->unregister_work);
return ret;
@@ -1444,6 +1471,7 @@ static void steam_remove(struct hid_device *hdev)
cancel_delayed_work_sync(&steam->mode_switch);
cancel_work_sync(&steam->work_connect);
cancel_work_sync(&steam->rumble_work);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
cancel_work_sync(&steam->unregister_work);
steam->client_hdev = NULL;
steam->client_opened = 0;
--
2.54.0
^ permalink raw reply related
* [PATCH 06/10] HID: steam: Fully unregister controller when hidraw is opened
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
To avoid conflicts between anything touching the hidraw and the driver we
had previously detached the evdev nodes when the hidraw is opened. However,
this isn't sufficient to avoid FEATURE reports from conflicting, so we
change to fully unregistering the controller internally, leaving only the
hidraw active until it's closed.
This also unifies the unregister and connect callbacks, as now the logic
between these two callbacks is identical.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 58 ++++++++++++-----------------------------
1 file changed, 16 insertions(+), 42 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 01b64194ec97..546916da31f9 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -348,7 +348,6 @@ struct steam_device {
u16 rumble_right;
unsigned int sensor_timestamp_us;
unsigned int sensor_update_rate_us;
- struct work_struct unregister_work;
};
static int steam_recv_report(struct steam_device *steam,
@@ -1144,38 +1143,41 @@ static int steam_register(struct steam_device *steam)
static void steam_unregister(struct steam_device *steam)
{
+ if (!steam->serial_no[0])
+ return;
+
+ hid_info(steam->hdev, "Steam Controller '%s' disconnected",
+ steam->serial_no);
steam_battery_unregister(steam);
steam_sensors_unregister(steam);
steam_input_unregister(steam);
- if (steam->serial_no[0]) {
- hid_info(steam->hdev, "Steam Controller '%s' disconnected",
- steam->serial_no);
- mutex_lock(&steam_devices_lock);
- list_del_init(&steam->list);
- mutex_unlock(&steam_devices_lock);
- steam->serial_no[0] = 0;
- }
+ mutex_lock(&steam_devices_lock);
+ list_del_init(&steam->list);
+ mutex_unlock(&steam_devices_lock);
+ steam->serial_no[0] = 0;
}
static void steam_work_connect_cb(struct work_struct *work)
{
struct steam_device *steam = container_of(work, struct steam_device,
work_connect);
+
unsigned long flags;
bool connected;
+ bool opened;
int ret;
spin_lock_irqsave(&steam->lock, flags);
+ opened = steam->client_opened;
connected = steam->connected;
spin_unlock_irqrestore(&steam->lock, flags);
- if (connected) {
+ if (connected && !opened) {
ret = steam_register(steam);
- if (ret) {
+ if (ret)
hid_err(steam->hdev,
"%s:steam_register failed with error %d\n",
__func__, ret);
- }
} else {
steam_unregister(steam);
}
@@ -1209,31 +1211,6 @@ static void steam_mode_switch_cb(struct work_struct *work)
}
}
-static void steam_work_unregister_cb(struct work_struct *work)
-{
- struct steam_device *steam = container_of(work, struct steam_device,
- unregister_work);
- unsigned long flags;
- bool connected;
- bool opened;
-
- spin_lock_irqsave(&steam->lock, flags);
- opened = steam->client_opened;
- connected = steam->connected;
- spin_unlock_irqrestore(&steam->lock, flags);
-
- if (connected) {
- if (opened) {
- steam_sensors_unregister(steam);
- steam_input_unregister(steam);
- } else {
- steam_set_lizard_mode(steam, lizard_mode);
- steam_input_register(steam);
- steam_sensors_register(steam);
- }
- }
-}
-
static bool steam_is_valve_interface(struct hid_device *hdev)
{
struct hid_report_enum *rep_enum;
@@ -1279,7 +1256,7 @@ static int steam_client_ll_open(struct hid_device *hdev)
steam->client_opened++;
spin_unlock_irqrestore(&steam->lock, flags);
- schedule_work(&steam->unregister_work);
+ schedule_work(&steam->work_connect);
return 0;
}
@@ -1294,7 +1271,7 @@ static void steam_client_ll_close(struct hid_device *hdev)
steam->client_opened--;
spin_unlock_irqrestore(&steam->lock, flags);
- schedule_work(&steam->unregister_work);
+ schedule_work(&steam->work_connect);
}
static int steam_client_ll_raw_request(struct hid_device *hdev,
@@ -1391,7 +1368,6 @@ static int steam_probe(struct hid_device *hdev,
steam->sensor_update_rate_us = 4000;
else
steam->sensor_update_rate_us = 9000;
- INIT_WORK(&steam->unregister_work, steam_work_unregister_cb);
/*
* With the real steam controller interface, do not connect hidraw.
@@ -1453,7 +1429,6 @@ static int steam_probe(struct hid_device *hdev,
cancel_delayed_work_sync(&steam->mode_switch);
cancel_work_sync(&steam->rumble_work);
cancel_delayed_work_sync(&steam->coalesce_rumble_work);
- cancel_work_sync(&steam->unregister_work);
return ret;
}
@@ -1472,7 +1447,6 @@ static void steam_remove(struct hid_device *hdev)
cancel_work_sync(&steam->work_connect);
cancel_work_sync(&steam->rumble_work);
cancel_delayed_work_sync(&steam->coalesce_rumble_work);
- cancel_work_sync(&steam->unregister_work);
steam->client_hdev = NULL;
steam->client_opened = 0;
if (steam->quirks & STEAM_QUIRK_WIRELESS) {
--
2.54.0
^ permalink raw reply related
* [PATCH 04/10] HID: steam: Add support for sensor events on the Steam Controller (2015)
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
Sensor support was added for the Steam Deck previously, but Steam
Controller sensor events were never added. This adds that missing support,
bringing Steam Controller support much closer to feature parity with things
like SDL and Steam itself.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 214 ++++++++++++++++++++++++++++++++--------
1 file changed, 175 insertions(+), 39 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index add9f4f27fff..34653ad383ac 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -70,13 +70,14 @@ static LIST_HEAD(steam_devices);
/* Joystick runs are about 5 mm and 32768 units */
#define STEAM_DECK_JOYSTICK_RESOLUTION 6553
/* Accelerometer has 16 bit resolution and a range of +/- 2g */
-#define STEAM_DECK_ACCEL_RES_PER_G 16384
-#define STEAM_DECK_ACCEL_RANGE 32768
+#define STEAM_ACCEL_RES_PER_G 16384
+#define STEAM_ACCEL_RANGE 32768
+#define STEAM_ACCEL_FUZZ 128
#define STEAM_DECK_ACCEL_FUZZ 32
/* Gyroscope has 16 bit resolution and a range of +/- 2000 dps */
-#define STEAM_DECK_GYRO_RES_PER_DPS 16
-#define STEAM_DECK_GYRO_RANGE 32768
-#define STEAM_DECK_GYRO_FUZZ 1
+#define STEAM_GYRO_RES_PER_DPS 16
+#define STEAM_GYRO_RANGE 32768
+#define STEAM_GYRO_FUZZ 0
#define STEAM_PAD_FUZZ 256
@@ -255,6 +256,31 @@ enum
ID_CONTROLLER_DECK_STATE = 9
};
+/* Read-only attributes */
+enum {
+ ATTRIB_UNIQUE_ID, // deprecated
+ ATTRIB_PRODUCT_ID,
+ ATTRIB_PRODUCT_REVISON, // deprecated
+ ATTRIB_CAPABILITIES = ATTRIB_PRODUCT_REVISON, // intentional aliasing
+ ATTRIB_FIRMWARE_VERSION, // deprecated
+ ATTRIB_FIRMWARE_BUILD_TIME,
+ ATTRIB_RADIO_FIRMWARE_BUILD_TIME,
+ ATTRIB_RADIO_DEVICE_ID0,
+ ATTRIB_RADIO_DEVICE_ID1,
+ ATTRIB_DONGLE_FIRMWARE_BUILD_TIME,
+ ATTRIB_HW_ID, // AKA BOARD_REVISION,
+ ATTRIB_BOOTLOADER_BUILD_TIME,
+ ATTRIB_CONNECTION_INTERVAL_IN_US,
+ ATTRIB_SECONDARY_FIRMWARE_BUILD_TIME,
+ ATTRIB_SECONDARY_BOOTLOADER_BUILD_TIME,
+ ATTRIB_SECONDARY_HW_ID, // AKA BOARD_REVISION,
+ ATTRIB_STREAMING,
+ ATTRIB_TRACKPAD_ID,
+ ATTRIB_SECONDARY_TRACKPAD_ID,
+
+ ATTRIB_COUNT
+};
+
/* String attribute identifiers */
enum {
ATTRIB_STR_BOARD_SERIAL,
@@ -284,6 +310,11 @@ enum {
TRACKPAD_GESTURE_KEYBOARD,
};
+struct steam_controller_attribute {
+ unsigned char tag;
+ __le32 value;
+} __packed;
+
/* Pad identifiers for the deck */
#define STEAM_PAD_LEFT 0
#define STEAM_PAD_RIGHT 1
@@ -315,6 +346,7 @@ struct steam_device {
u16 rumble_left;
u16 rumble_right;
unsigned int sensor_timestamp_us;
+ unsigned int sensor_update_rate_us;
struct work_struct unregister_work;
};
@@ -468,6 +500,38 @@ static int steam_get_serial(struct steam_device *steam)
return ret;
}
+static int steam_get_attributes(struct steam_device *steam)
+{
+ int ret = 0;
+ u8 cmd[] = {ID_GET_ATTRIBUTES_VALUES, 0};
+ u8 reply[64] = {};
+ u8 size;
+ int i;
+ struct steam_controller_attribute *attr;
+
+ guard(mutex)(&steam->report_mutex);
+ ret = steam_send_report(steam, cmd, sizeof(cmd));
+ if (ret < 0)
+ return ret;
+ ret = steam_recv_report(steam, reply, sizeof(reply));
+ if (ret < 0)
+ return ret;
+ if (reply[0] != ID_GET_ATTRIBUTES_VALUES || reply[1] < 2)
+ return -EIO;
+
+ size = min(reply[1], sizeof(reply) - 2);
+ for (i = 0; i + sizeof(*attr) <= size; i += sizeof(*attr)) {
+ attr = (struct steam_controller_attribute *)&reply[i];
+ if (attr->tag == ATTRIB_CONNECTION_INTERVAL_IN_US) {
+ steam->sensor_update_rate_us = get_unaligned_le32(&attr->value);
+ hid_dbg(steam->hdev, "Sensor update rate: %uus\n",
+ steam->sensor_update_rate_us);
+ }
+ }
+
+ return 0;
+}
+
/*
* This command requests the wireless adaptor to post an event
* with the connection status. Useful if this driver is loaded when
@@ -626,6 +690,42 @@ static void steam_input_close(struct input_dev *dev)
}
}
+static int steam_sensor_open(struct input_dev *dev)
+{
+ struct steam_device *steam = input_get_drvdata(dev);
+ unsigned long flags;
+ bool client_opened;
+
+ spin_lock_irqsave(&steam->lock, flags);
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+ if (client_opened)
+ return 0;
+
+ guard(mutex)(&steam->report_mutex);
+ steam_write_settings(steam, SETTING_IMU_MODE,
+ SETTING_IMU_MODE_SEND_RAW_ACCEL | SETTING_IMU_MODE_SEND_RAW_GYRO,
+ 0);
+
+ return 0;
+}
+
+static void steam_sensor_close(struct input_dev *dev)
+{
+ struct steam_device *steam = input_get_drvdata(dev);
+ unsigned long flags;
+ bool client_opened;
+
+ spin_lock_irqsave(&steam->lock, flags);
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+ if (client_opened)
+ return;
+
+ guard(mutex)(&steam->report_mutex);
+ steam_write_settings(steam, SETTING_IMU_MODE, 0, 0);
+}
+
static enum power_supply_property steam_battery_props[] = {
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_SCOPE,
@@ -839,9 +939,6 @@ static int steam_sensors_register(struct steam_device *steam)
struct input_dev *sensors;
int ret;
- if (!(steam->quirks & STEAM_QUIRK_DECK))
- return 0;
-
rcu_read_lock();
sensors = rcu_dereference(steam->sensors);
rcu_read_unlock();
@@ -856,8 +953,14 @@ static int steam_sensors_register(struct steam_device *steam)
input_set_drvdata(sensors, steam);
sensors->dev.parent = &hdev->dev;
+ if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+ sensors->open = steam_sensor_open;
+ sensors->close = steam_sensor_close;
+ }
- sensors->name = "Steam Deck Motion Sensors";
+ sensors->name = steam->quirks & STEAM_QUIRK_DECK ?
+ "Steam Deck Motion Sensors" :
+ "Steam Controller Motion Sensors";
sensors->phys = hdev->phys;
sensors->uniq = steam->serial_no;
sensors->id.bustype = hdev->bus;
@@ -869,25 +972,34 @@ static int steam_sensors_register(struct steam_device *steam)
__set_bit(EV_MSC, sensors->evbit);
__set_bit(MSC_TIMESTAMP, sensors->mscbit);
- input_set_abs_params(sensors, ABS_X, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_set_abs_params(sensors, ABS_Y, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_set_abs_params(sensors, ABS_Z, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_abs_set_res(sensors, ABS_X, STEAM_DECK_ACCEL_RES_PER_G);
- input_abs_set_res(sensors, ABS_Y, STEAM_DECK_ACCEL_RES_PER_G);
- input_abs_set_res(sensors, ABS_Z, STEAM_DECK_ACCEL_RES_PER_G);
-
- input_set_abs_params(sensors, ABS_RX, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_set_abs_params(sensors, ABS_RY, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_set_abs_params(sensors, ABS_RZ, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_abs_set_res(sensors, ABS_RX, STEAM_DECK_GYRO_RES_PER_DPS);
- input_abs_set_res(sensors, ABS_RY, STEAM_DECK_GYRO_RES_PER_DPS);
- input_abs_set_res(sensors, ABS_RZ, STEAM_DECK_GYRO_RES_PER_DPS);
+ if (steam->quirks & STEAM_QUIRK_DECK) {
+ input_set_abs_params(sensors, ABS_X, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Y, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Z, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ } else {
+ input_set_abs_params(sensors, ABS_X, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Y, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Z, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ }
+ input_abs_set_res(sensors, ABS_X, STEAM_ACCEL_RES_PER_G);
+ input_abs_set_res(sensors, ABS_Y, STEAM_ACCEL_RES_PER_G);
+ input_abs_set_res(sensors, ABS_Z, STEAM_ACCEL_RES_PER_G);
+
+ input_set_abs_params(sensors, ABS_RX, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_RY, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_RZ, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_abs_set_res(sensors, ABS_RX, STEAM_GYRO_RES_PER_DPS);
+ input_abs_set_res(sensors, ABS_RY, STEAM_GYRO_RES_PER_DPS);
+ input_abs_set_res(sensors, ABS_RZ, STEAM_GYRO_RES_PER_DPS);
ret = input_register_device(sensors);
if (ret)
@@ -918,9 +1030,6 @@ static void steam_sensors_unregister(struct steam_device *steam)
{
struct input_dev *sensors;
- if (!(steam->quirks & STEAM_QUIRK_DECK))
- return;
-
rcu_read_lock();
sensors = rcu_dereference(steam->sensors);
rcu_read_unlock();
@@ -968,6 +1077,12 @@ static int steam_register(struct steam_device *steam)
strscpy(steam->serial_no, "XXXXXXXXXX",
sizeof(steam->serial_no));
+ ret = steam_get_attributes(steam);
+ if (ret < 0)
+ hid_err(steam->hdev,
+ "%s:steam_get_attributes failed with error %d\n",
+ __func__, ret);
+
hid_info(steam->hdev, "Steam Controller '%s' connected",
steam->serial_no);
@@ -1246,6 +1361,10 @@ static int steam_probe(struct hid_device *hdev,
INIT_LIST_HEAD(&steam->list);
INIT_WORK(&steam->rumble_work, steam_haptic_rumble_cb);
steam->sensor_timestamp_us = 0;
+ if (steam->quirks & STEAM_QUIRK_DECK)
+ steam->sensor_update_rate_us = 4000;
+ else
+ steam->sensor_update_rate_us = 9000;
INIT_WORK(&steam->unregister_work, steam_work_unregister_cb);
/*
@@ -1491,6 +1610,16 @@ static const struct steam_axis_mapping steam_controller_axis_mappings[] = {
{ /* sentinel */ },
};
+static const struct steam_axis_mapping steam_controller_imu_mappings[] = {
+ { ABS_X, 1, 28 },
+ { ABS_Z, -1, 30 },
+ { ABS_Y, 1, 32 },
+ { ABS_RX, 1, 34 },
+ { ABS_RZ, 1, 36 },
+ { ABS_RY, 1, 38 },
+ { /* sentinel */ },
+};
+
static void steam_do_input_event(struct steam_device *steam,
struct input_dev *input, u8 *data)
{
@@ -1533,6 +1662,17 @@ static void steam_do_input_event(struct steam_device *steam,
input_sync(input);
}
+static void steam_do_sensors_event(struct steam_device *steam,
+ struct input_dev *sensors, u8 *data)
+{
+ steam->sensor_timestamp_us += steam->sensor_update_rate_us;
+
+ input_event(sensors, EV_MSC, MSC_TIMESTAMP, steam->sensor_timestamp_us);
+ steam_map_axes(sensors, steam_controller_imu_mappings, data);
+
+ input_sync(sensors);
+}
+
/*
* The size for this message payload is 56.
* The known values are:
@@ -1727,14 +1867,7 @@ static void steam_do_deck_input_event(struct steam_device *steam,
static void steam_do_deck_sensors_event(struct steam_device *steam,
struct input_dev *sensors, u8 *data)
{
- /*
- * The deck input report is received every 4 ms on average,
- * with a jitter of +/- 4 ms even though the USB descriptor claims
- * that it uses 1 kHz.
- * Since the HID report does not include a sensor timestamp,
- * use a fixed increment here.
- */
- steam->sensor_timestamp_us += 4000;
+ steam->sensor_timestamp_us += steam->sensor_update_rate_us;
if (!steam->gamepad_mode && lizard_mode)
return;
@@ -1819,6 +1952,9 @@ static int steam_raw_event(struct hid_device *hdev,
input = rcu_dereference(steam->input);
if (likely(input))
steam_do_input_event(steam, input, data);
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_sensors_event(steam, sensors, data);
rcu_read_unlock();
break;
case ID_CONTROLLER_DECK_STATE:
--
2.54.0
^ permalink raw reply related
* [PATCH 07/10] HID: steam: Rearrange deinitialization sequence
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
This fixes a narrow window during the deinitialization where callbacks
could still be scheduled during cleanup that would then have a dangling
pointer to the now-freed steam struct.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 546916da31f9..9d1fe9792101 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1443,18 +1443,18 @@ static void steam_remove(struct hid_device *hdev)
}
hid_destroy_device(steam->client_hdev);
- cancel_delayed_work_sync(&steam->mode_switch);
- cancel_work_sync(&steam->work_connect);
- cancel_work_sync(&steam->rumble_work);
- cancel_delayed_work_sync(&steam->coalesce_rumble_work);
steam->client_hdev = NULL;
+ hid_hw_close(hdev);
+ cancel_work_sync(&steam->work_connect);
steam->client_opened = 0;
if (steam->quirks & STEAM_QUIRK_WIRELESS) {
hid_info(hdev, "Steam wireless receiver disconnected");
}
- hid_hw_close(hdev);
- hid_hw_stop(hdev);
steam_unregister(steam);
+ cancel_work_sync(&steam->rumble_work);
+ cancel_delayed_work_sync(&steam->mode_switch);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
+ hid_hw_stop(hdev);
}
static void steam_do_connect_event(struct steam_device *steam, bool connected)
--
2.54.0
^ permalink raw reply related
* [PATCH 08/10] HID: steam: Improve logging and other cleanup
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
Adds more logging as appropriate, reindents an enum to match surrounding
style, as well as cleaning up some places where we can use guard() instead
of doing locking and unlocking manually.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 56 ++++++++++++++++++++++++-----------------
1 file changed, 33 insertions(+), 23 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 9d1fe9792101..593151709cf1 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -246,14 +246,14 @@ enum {
/* Input report identifiers */
enum
{
- ID_CONTROLLER_STATE = 1,
- ID_CONTROLLER_DEBUG = 2,
- ID_CONTROLLER_WIRELESS = 3,
- ID_CONTROLLER_STATUS = 4,
- ID_CONTROLLER_DEBUG2 = 5,
- ID_CONTROLLER_SECONDARY_STATE = 6,
- ID_CONTROLLER_BLE_STATE = 7,
- ID_CONTROLLER_DECK_STATE = 9
+ ID_CONTROLLER_STATE = 1,
+ ID_CONTROLLER_DEBUG = 2,
+ ID_CONTROLLER_WIRELESS = 3,
+ ID_CONTROLLER_STATUS = 4,
+ ID_CONTROLLER_DEBUG2 = 5,
+ ID_CONTROLLER_SECONDARY_STATE = 6,
+ ID_CONTROLLER_BLE_STATE = 7,
+ ID_CONTROLLER_DECK_STATE = 9,
};
/* Read-only attributes */
@@ -379,9 +379,16 @@ static int steam_recv_report(struct steam_device *steam,
ret = hid_hw_raw_request(steam->hdev, 0x00,
buf, hid_report_len(r) + 1,
HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
- if (ret > 0)
- memcpy(data, buf + 1, min(size, ret - 1));
+ if (ret > 0) {
+ ret = min(size, ret - 1);
+ memcpy(data, buf + 1, ret);
+ }
kfree(buf);
+
+ if (ret < 0)
+ hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
+ else
+ hid_dbg(steam->hdev, "Received report %*ph\n", ret, data);
return ret;
}
@@ -409,6 +416,8 @@ static int steam_send_report(struct steam_device *steam,
/* The report ID is always 0 */
memcpy(buf + 1, cmd, size);
+ hid_dbg(steam->hdev, "Sending report %*ph\n", size, cmd);
+
/*
* Sometimes the wireless controller fails with EPIPE
* when sending a feature report.
@@ -481,22 +490,21 @@ static int steam_get_serial(struct steam_device *steam)
u8 cmd[] = {ID_GET_STRING_ATTRIBUTE, sizeof(steam->serial_no), ATTRIB_STR_UNIT_SERIAL};
u8 reply[3 + STEAM_SERIAL_LEN + 1];
- mutex_lock(&steam->report_mutex);
+ guard(mutex)(&steam->report_mutex);
ret = steam_send_report(steam, cmd, sizeof(cmd));
if (ret < 0)
- goto out;
+ return ret;
ret = steam_recv_report(steam, reply, sizeof(reply));
if (ret < 0)
- goto out;
+ return ret;
if (reply[0] != ID_GET_STRING_ATTRIBUTE || reply[1] < 1 ||
reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
- ret = -EIO;
- goto out;
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
+ return -EIO;
}
reply[3 + STEAM_SERIAL_LEN] = 0;
strscpy(steam->serial_no, reply + 3, reply[1]);
-out:
- mutex_unlock(&steam->report_mutex);
return ret;
}
@@ -516,8 +524,11 @@ static int steam_get_attributes(struct steam_device *steam)
ret = steam_recv_report(steam, reply, sizeof(reply));
if (ret < 0)
return ret;
- if (reply[0] != ID_GET_ATTRIBUTES_VALUES || reply[1] < 2)
+ if (reply[0] != ID_GET_ATTRIBUTES_VALUES || reply[1] < 2) {
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
return -EIO;
+ }
size = min(reply[1], sizeof(reply) - 2);
for (i = 0; i + sizeof(*attr) <= size; i += sizeof(*attr)) {
@@ -539,11 +550,8 @@ static int steam_get_attributes(struct steam_device *steam)
*/
static inline int steam_request_conn_status(struct steam_device *steam)
{
- int ret;
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
- mutex_unlock(&steam->report_mutex);
- return ret;
+ guard(mutex)(&steam->report_mutex);
+ return steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
}
/*
@@ -1193,6 +1201,7 @@ static void steam_mode_switch_cb(struct work_struct *work)
return;
steam->gamepad_mode = !steam->gamepad_mode;
+ hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, steam->gamepad_mode);
if (steam->gamepad_mode)
steam_set_lizard_mode(steam, false);
else {
@@ -1834,6 +1843,7 @@ static void steam_do_deck_input_event(struct steam_device *steam,
steam->did_mode_switch = false;
cancel_delayed_work(&steam->mode_switch);
} else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
+ hid_dbg(steam->hdev, "%s: doing mode switch\n", __func__);
steam->did_mode_switch = true;
schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
}
--
2.54.0
^ permalink raw reply related
* [PATCH 09/10] HID: steam: Reject short reads
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Vicki Pfau, Yousef Alhouseen, syzbot+75f3f9bff8c510602d36
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
Steam Controller FEATURE reports encode the size of the message in the
message itself. Previously we were trusting that the size reported matched
the size we actually read, leading to a potential issue with short reads.
Instead, we should actually verify the length of the read.
Fixes: c164d6abf384 ("HID: add driver for Valve Steam Controller")
Reported-by: syzbot+75f3f9bff8c510602d36@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=75f3f9bff8c510602d36
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 593151709cf1..e97431bc2828 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -389,6 +389,12 @@ static int steam_recv_report(struct steam_device *steam,
hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
else
hid_dbg(steam->hdev, "Received report %*ph\n", ret, data);
+
+ if (ret >= 2 && data[1] > ret + 2) {
+ hid_err(steam->hdev, "%s: expected %u bytes, read %i\n",
+ __func__, data[1] + 2, ret);
+ return -EPROTO;
+ }
return ret;
}
--
2.54.0
^ permalink raw reply related
* [PATCH 10/10] HID: steam: Retry send/recv reports if stale
From: Vicki Pfau @ 2026-07-02 22:21 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau, Yousef Alhouseen
In-Reply-To: <20260702222145.1863104-1-vi@endrift.com>
Sometimes recv report will reply with a stale result from a previous send
report. Instead of failing out, we should retry them, as they generally
reply correctly after three tries, give or take.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 54 +++++++++++++++++++++++++++++++----------
1 file changed, 41 insertions(+), 13 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index e97431bc2828..8d036b341253 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -486,6 +486,43 @@ static int steam_write_settings(struct steam_device *steam,
return steam_recv_report(steam, cmd, 2 + cmd[1]);
}
+static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
+ u8 *reply, int rsize)
+{
+ unsigned int retries = 5;
+ int ret;
+
+ guard(mutex)(&steam->report_mutex);
+ do {
+ ret = steam_send_report(steam, cmd, csize);
+ if (ret < 0)
+ return ret;
+ ret = steam_recv_report(steam, reply, rsize);
+ /*
+ * Sometimes this can fail on the first few tries on the Steam
+ * Controller (2015). It appears to be a firmware bug, and Steam
+ * itself just retries, so we should also retry a few times to
+ * see if we get it.
+ */
+ if (ret == -EPROTO)
+ continue;
+ if (ret < 0) {
+ hid_err(steam->hdev, "%s: error reading reply (%*ph)\n",
+ __func__, csize, cmd);
+ return ret;
+ }
+ if (reply[0] == cmd[0] && reply[1] >= 1)
+ break;
+ if (retries > 0)
+ continue;
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ rsize, reply);
+ return -EPROTO;
+ } while (retries--);
+
+ return ret;
+}
+
static int steam_get_serial(struct steam_device *steam)
{
/*
@@ -496,15 +533,10 @@ static int steam_get_serial(struct steam_device *steam)
u8 cmd[] = {ID_GET_STRING_ATTRIBUTE, sizeof(steam->serial_no), ATTRIB_STR_UNIT_SERIAL};
u8 reply[3 + STEAM_SERIAL_LEN + 1];
- guard(mutex)(&steam->report_mutex);
- ret = steam_send_report(steam, cmd, sizeof(cmd));
+ ret = steam_exchange_report(steam, cmd, sizeof(cmd), reply, sizeof(reply));
if (ret < 0)
return ret;
- ret = steam_recv_report(steam, reply, sizeof(reply));
- if (ret < 0)
- return ret;
- if (reply[0] != ID_GET_STRING_ATTRIBUTE || reply[1] < 1 ||
- reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
+ if (reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
(int)sizeof(reply), reply);
return -EIO;
@@ -523,14 +555,10 @@ static int steam_get_attributes(struct steam_device *steam)
int i;
struct steam_controller_attribute *attr;
- guard(mutex)(&steam->report_mutex);
- ret = steam_send_report(steam, cmd, sizeof(cmd));
- if (ret < 0)
- return ret;
- ret = steam_recv_report(steam, reply, sizeof(reply));
+ ret = steam_exchange_report(steam, cmd, sizeof(cmd), reply, sizeof(reply));
if (ret < 0)
return ret;
- if (reply[0] != ID_GET_ATTRIBUTES_VALUES || reply[1] < 2) {
+ if (reply[1] < 2) {
hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
(int)sizeof(reply), reply);
return -EIO;
--
2.54.0
^ permalink raw reply related
* [PATCH v6 0/4] Input: Add support for TouchNetix aXiom touchscreen
From: Marco Felsch @ 2026-07-02 22:29 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
Mamta Shukla, Krzysztof Kozlowski
Hi,
this adds the support for the TouchNetix aXiom touchcontroller family.
The following features are added:
- I2C communication
- Input event handling
- Touchcontroller firmware (AXFW) updates
- Touchcontroller config (TH2CFGBIN) updates
- Poll or IRQ support
Many thanks for Dmitry's input on my v5. I included all changes
requested from him.
Regards,
Marco
Changes in v7:
- Link to v6: https://lore.kernel.org/all/20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@pengutronix.de/
- Rebased on top of 7.2-rc1
- Fix U33 rev.6 usage (Mamta)
- Fix bootloader mode handling, required to disable the IRQ (Andrew)
- Fix bug in usage table population which led to a nullptr bug (Andrew)
Changes in v6:
- Link to v5: https://lore.kernel.org/r/20260111-v6-10-topic-touchscreen-axiom-v5-0-f94e0ae266cb@pengutronix.de
- Fix update POLL mode
- Fix max. input values
- Resolve some "TODO" and "Downstream" comments (Andrew)
- Add 0d, xl fw_variant (Andrew)
- Use 2sec timeout for u02 swreset (Andrew)
- Fix IRQ-runmode input-device registration after FW update
- Drop legacy ALC firmware support, since no fw sanity check could be performed
which is bad for user experience if they provided the wrong file accidentally.
- drop BLP_PRE_MODE
- make AXIOM_U31 rev.1 required
- axiom_usage_supported: drop iter and instead use new usage_table_by_baseaddr
- axiom_axfw_fw_write: drop goto error handling
- adapt comments
- rework axiom_i2c_probe (Dmitry)
- rework axiom_power_*_device and axiom_parse_firmware (Dmitry)
- rework axiom_register_input_dev (Dmitry)
- rework axiom_register_fwl (Dmitry)
- rework axiom_cfg_fw_cancel (Dmitry)
- rework axiom_cfg_fw_write (Dmitry)
- rework axiom_verify_volatile_mem (Dmitry)
- rework axiom_write_cfg_chunk (Dmitry)
- rework axiom_cfg_fw_prepare (Dmitry)
- rework axiom_axfw_fw_* functions (Dmitry)
- rework axiom_blp_reset (Dmitry)
- rework axiom_blp_write_chunk (Dmitry)
- rework axiom_enter_bootloader_mode (Dmitry)
- rework axiom_axfw_fw_prepare (Dmitry)
- rework regmap_read/write (Dmitry)
- rework axiom_u34_rev1_process_report (Dmitry)
- rework axiom_cdu_wait_idle (Dmitry)
- rework axiom_u64_cds_enabled (Dmitry)
- rework axiom_u42_get_touchslots (Dmitry)
- rework axiom_u33_read (s/ret/error/) (Dmitry)
- rework axiom_u31_device_discover (Dmitry)
- simple s/ret/error/ (Dmitry)
- rework u04 handling (Dmitry)
- rework u02 handling (Dmitry)
- align function name accordingly (Dmitry)
- fix indentation (Dmitry)
Changes in v5:
- Link to v4: https://lore.kernel.org/r/20260106-v6-10-topic-touchscreen-axiom-v4-0-9e9b69c84926@pengutronix.de
- fix sysfs documentation description indentation and date
Changes in v4:
- Link to v3: https://lore.kernel.org/r/20250821-v6-10-topic-touchscreen-axiom-v3-0-940ccee6dba3@pengutronix.de
- rebased on top of v6.19-rc1
- collect r-b tags
Changes in v3:
- Link to v2: https://lore.kernel.org/r/20250529-v6-10-topic-touchscreen-axiom-v2-0-a5edb105a600@pengutronix.de
- firmware: fix commit message (Russ)
- dt-bindings: Add ack from Krzysztof
- dt-bindings: make use of GPIO_ACTIVE_LOW (Krzysztof)
- dt-bindings: drop 'panel: true' property (Krzysztof)
- driver: make use of sysfs_emit (Greg)
- driver: s/WARN()/dev_warn()/ to not take down the system (Greg)
- driver: fix build dependency error by adding "depends on DRM || !DRM"
- driver: harmonize usage printing to u%02X
Changes in v2:
- Link to v1: https://lore.kernel.org/r/20241119-v6-10-topic-touchscreen-axiom-v1-0-6124925b9718@pengutronix.de
- Rework the firmware-duplicate handling -> expose the error to the
userspace
- Drop Krzysztof Kozlowski ACK and RB
- Add panel-follower support
- Add sysfs-driver-input-touchnetix-axiom documentation
- Add support for new firmware 4.8.9
- Add support to handle 2D and 3D firmware
---
Kamel Bouhara (2):
dt-bindings: vendor-prefixes: Add TouchNetix AS
dt-bindings: input: Add TouchNetix axiom touchscreen
Marco Felsch (2):
firmware_loader: expand firmware error codes with up-to-date error
Input: Add TouchNetix aXiom I2C Touchscreen support
.../testing/sysfs-driver-input-touchnetix-axiom | 80 +
.../input/touchscreen/touchnetix,ax54a.yaml | 62 +
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
drivers/base/firmware_loader/sysfs_upload.c | 1 +
drivers/input/touchscreen/Kconfig | 17 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/touchnetix_axiom.c | 3141 ++++++++++++++++++++
include/linux/firmware.h | 2 +
lib/test_firmware.c | 1 +
9 files changed, 3307 insertions(+)
---
base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
change-id: 20240704-v6-10-topic-touchscreen-axiom-105761e81011
Best regards,
--
Marco Felsch <m.felsch@pengutronix.de>
^ permalink raw reply
* [PATCH v6 2/4] dt-bindings: vendor-prefixes: Add TouchNetix AS
From: Marco Felsch @ 2026-07-02 22:29 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add vendor prefix for TouchNetix AS (https://www.touchnetix.com/products/).
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Acked-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e7cf0ca1436713ae44f1950259f006..b8450050d7299cea2b5cf6f57fe71f5ead1140c8 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1715,6 +1715,8 @@ patternProperties:
description: Toradex AG
"^toshiba,.*":
description: Toshiba Corporation
+ "^touchnetix,.*":
+ description: TouchNetix AS
"^toumaz,.*":
description: Toumaz
"^tpk,.*":
--
2.47.3
^ permalink raw reply related
* [PATCH v6 3/4] dt-bindings: input: Add TouchNetix axiom touchscreen
From: Marco Felsch @ 2026-07-02 22:29 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add the TouchNetix axiom I2C touchscreen device tree bindings
documentation.
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
.../input/touchscreen/touchnetix,ax54a.yaml | 62 ++++++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d388c41a75dd4d6d6d0e6de0eaef4d493d439a90
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/touchnetix,ax54a.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: TouchNetix Axiom series touchscreen controller
+
+maintainers:
+ - Marco Felsch <kernel@pengutronix.de>
+
+allOf:
+ - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+ - $ref: /schemas/input/input.yaml#
+
+properties:
+ compatible:
+ const: touchnetix,ax54a
+
+ reg:
+ enum: [ 0x66, 0x67 ]
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ vdda-supply:
+ description: Analog power supply regulator on VDDA pin
+
+ vddi-supply:
+ description: I/O power supply regulator on VDDI pin
+
+required:
+ - compatible
+ - reg
+ - vdda-supply
+ - vddi-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ touchscreen@66 {
+ compatible = "touchnetix,ax54a";
+ reg = <0x66>;
+ interrupt-parent = <&gpio2>;
+ interrupts = <2 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>;
+ vdda-supply = <&vdda_reg>;
+ vddi-supply = <&vddi_reg>;
+ poll-interval = <20>;
+ };
+ };
+...
--
2.47.3
^ permalink raw reply related
* [PATCH v6 4/4] Input: Add TouchNetix aXiom I2C Touchscreen support
From: Marco Felsch @ 2026-07-02 22:29 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Mamta Shukla,
Marco Felsch
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de>
This adds the initial support for the TouchNetix AX54A touchcontroller
which is part of TouchNetix's aXiom touchscreen controller family.
The TouchNetix aXiom family provides two physical interfaces: SPI and
I2C. This patch covers only the I2C interface.
Apart the input event handling the driver supports firmware updates too.
One firmware interface handles the touchcontroller firmware (AXFW)
update the other handles the touchcontroller configuration (TH2CFGBIN)
update.
Co-developed-by: Mamta Shukla <mamta.shukla@leica-geosystems.com>
Signed-off-by: Mamta Shukla <mamta.shukla@leica-geosystems.com>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
.../testing/sysfs-driver-input-touchnetix-axiom | 80 +
drivers/input/touchscreen/Kconfig | 17 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/touchnetix_axiom.c | 3141 ++++++++++++++++++++
4 files changed, 3239 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom b/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom
new file mode 100644
index 0000000000000000000000000000000000000000..8262673630557bf1e595a97ec23e66c1c5370f71
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-input-touchnetix-axiom
@@ -0,0 +1,80 @@
+What: /sys/bus/i2c/devices/xxx/fw_major
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the firmware major version provided by the touchscreen.
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/fw_minor
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the firmware minor version provided by the touchscreen.
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/fw_rc
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the firmware release canidate version provided by the touchscreen.
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/fw_status
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the firmware status provided by the touchscreen. It may
+ be either "release" or "engineering".
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/fw_variant
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the firmware variant provided by the touchscreen. It may
+ be either: "0d", "2d", "3d", "force", "xl" or "unknown".
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/device_id
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the touchscreen device id, for example: "54" for the AX54A.
+
+ Access: Read
+
+ Valid values: Represented as string
+
+What: /sys/bus/i2c/devices/xxx/device_state
+Date: Jan 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Reports the touchscreen device current runtime state. The
+ following values are reported:
+
+ discovery: Device is in discovery mode.
+ tcp: Device is in touch-control-protocol (tcp) mode. This is
+ the normal working mode.
+ th2cfg-update: Device is in configuration update mode.
+ bootloader: Device is in bootloader mode, used for firmware
+ updates.
+ unknown: Device mode is unknown.
+
+ Access: Read
+
+ Valid values: Represented as string
diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 9b9ae8ac3f7fd3b9e948083fcd05afabdcd69bca..399db93c337de61a0d759fd667de1fffa10aacb4 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -828,6 +828,23 @@ config TOUCHSCREEN_MIGOR
To compile this driver as a module, choose M here: the
module will be called migor_ts.
+config TOUCHSCREEN_TOUCHNETIX_AXIOM
+ tristate "TouchNetix aXiom based touchscreen controllers"
+ # We need to call into panel code so if DRM=m, this can't be 'y'
+ depends on DRM || !DRM
+ depends on I2C
+ select CRC16
+ select CRC32
+ select REGMAP_I2C
+ help
+ Say Y here if you have a axiom touchscreen connected to
+ your system.
+
+ If unsure, say N.
+
+ To compile this driver as a module, choose M here: the
+ module will be called touchnetix_axiom.
+
config TOUCHSCREEN_TOUCHRIGHT
tristate "Touchright serial touchscreen"
select SERIO
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index bfd9de83389d8833dbe7cc2641cb24493cdf3807..0d139fff701aa6466d2ecd6f6b27fcc8006a0575 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -88,6 +88,7 @@ obj-$(CONFIG_TOUCHSCREEN_SUR40) += sur40.o
obj-$(CONFIG_TOUCHSCREEN_SURFACE3_SPI) += surface3_spi.o
obj-$(CONFIG_TOUCHSCREEN_TI_AM335X_TSC) += ti_am335x_tsc.o
obj-$(CONFIG_TOUCHSCREEN_TOUCHIT213) += touchit213.o
+obj-$(CONFIG_TOUCHSCREEN_TOUCHNETIX_AXIOM) += touchnetix_axiom.o
obj-$(CONFIG_TOUCHSCREEN_TOUCHRIGHT) += touchright.o
obj-$(CONFIG_TOUCHSCREEN_TOUCHWIN) += touchwin.o
obj-$(CONFIG_TOUCHSCREEN_TS4800) += ts4800-ts.o
diff --git a/drivers/input/touchscreen/touchnetix_axiom.c b/drivers/input/touchscreen/touchnetix_axiom.c
new file mode 100644
index 0000000000000000000000000000000000000000..b945ccda326bdd726420e2a26021ba8529d6e8b8
--- /dev/null
+++ b/drivers/input/touchscreen/touchnetix_axiom.c
@@ -0,0 +1,3141 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * TouchNetix aXiom Touchscreen Driver
+ *
+ * Copyright (C) 2024 Pengutronix
+ *
+ * Marco Felsch <kernel@pengutronix.de>
+ */
+
+#include <drm/drm_panel.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/completion.h>
+#include <linux/crc16.h>
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/time.h>
+#include <linux/unaligned.h>
+
+/*
+ * Short introduction for developers:
+ * The programming manual is written based on u(sages):
+ * - Max. 0xff usages possible
+ * - A usage is a group of registers (0x00 ... 0xff)
+ * - The usage base address must be discovered (FW dependent)
+ * - Partial RW usage access is allowed
+ * - Each usage has a revision (FW dependent)
+ * - Only u31 is always at address 0x0 (used for discovery)
+ *
+ * E.x. Reading register 0x01 for usage u03 with baseaddr 0x20 results in the
+ * following physical 16bit I2C address: 0x2001.
+ *
+ * Note the datasheet specifies the usage numbers in hex and the internal
+ * offsets in decimal. Keep it that way to make it more developer friendly.
+ */
+#define AXIOM_U01 0x01
+#define AXIOM_U01_REV1_REPORTTYPE_REG 0
+#define AXIOM_U01_REV1_REPORTTYPE_HELLO 0
+#define AXIOM_U01_REV1_REPORTTYPE_HEARTBEAT 1
+#define AXIOM_U01_REV1_REPORTTYPE_OPCOMPLETE 3
+
+#define AXIOM_U02 0x02
+#define AXIOM_U02_REV1_COMMAND_REG 0
+#define AXIOM_U02_REV1_CMD_HARDRESET 0x0001
+#define AXIOM_U02_REV1_CMD_SOFTRESET 0x0002
+#define AXIOM_U02_REV1_CMD_STOP 0x0005
+#define AXIOM_U02_REV1_CMD_SAVEVLTLCFG2NVM 0x0007
+#define AXIOM_U02_REV1_PARAM1_SAVEVLTLCFG2NVM 0xb10c
+#define AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM 0xc0de
+#define AXIOM_U02_REV1_CMD_HANDSHAKENVM 0x0008
+#define AXIOM_U02_REV1_CMD_COMPUTECRCS 0x0009
+#define AXIOM_U02_REV1_CMD_FILLCONFIG 0x000a
+#define AXIOM_U02_REV1_PARAM0_FILLCONFIG 0x5555
+#define AXIOM_U02_REV1_PARAM1_FILLCONFIG 0xaaaa
+#define AXIOM_U02_REV1_PARAM2_FILLCONFIG_ZERO 0xa55a
+#define AXIOM_U02_REV1_CMD_ENTERBOOTLOADER 0x000b
+#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1 0x5555
+#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2 0xaaaa
+#define AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3 0xa55a
+#define AXIOM_U02_REV1_RESP_SUCCESS 0x0000
+
+struct axiom_u02_rev1_system_manager_msg {
+ union {
+ __le16 command;
+ __le16 response;
+ };
+ __le16 parameters[3];
+};
+
+#define AXIOM_U04 0x04
+
+#define AXIOM_U05 0x05 /* CDU */
+
+#define AXIOM_U22 0x22 /* CDU */
+
+#define AXIOM_U31 0x31
+#define AXIOM_U31_REV1_PAGE0 0x0000
+#define AXIOM_U31_REV1_DEVICE_ID_LOW_REG (AXIOM_U31_REV1_PAGE0 + 0)
+#define AXIOM_U31_REV1_DEVICE_ID_HIGH_REG (AXIOM_U31_REV1_PAGE0 + 1)
+#define AXIOM_U31_REV1_MODE_MASK BIT(7)
+#define AXIOM_U31_REV1_MODE_BLP 1
+#define AXIOM_U31_REV1_DEVICE_ID_HIGH_MASK GENMASK(6, 0)
+#define AXIOM_U31_REV1_RUNTIME_FW_MIN_REG (AXIOM_U31_REV1_PAGE0 + 2)
+#define AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG (AXIOM_U31_REV1_PAGE0 + 3)
+#define AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG (AXIOM_U31_REV1_PAGE0 + 4)
+#define AXIOM_U31_REV1_RUNTIME_FW_STATUS BIT(7)
+#define AXIOM_U31_REV1_RUNTIME_FW_VARIANT GENMASK(6, 0)
+#define AXIOM_U31_REV1_JEDEC_ID_LOW_REG (AXIOM_U31_REV1_PAGE0 + 8)
+#define AXIOM_U31_REV1_JEDEC_ID_HIGH_REG (AXIOM_U31_REV1_PAGE0 + 9)
+#define AXIOM_U31_REV1_NUM_USAGES_REG (AXIOM_U31_REV1_PAGE0 + 10)
+#define AXIOM_U31_REV1_RUNTIME_FW_RC_REG (AXIOM_U31_REV1_PAGE0 + 11)
+#define AXIOM_U31_REV1_RUNTIME_FW_RC_MASK GENMASK(7, 4)
+#define AXIOM_U31_REV1_SILICON_REV_MASK GENMASK(3, 0)
+
+#define AXIOM_U31_REV1_PAGE1 0x0100
+#define AXIOM_U31_REV1_OFFSET_TYPE_MASK BIT(7)
+#define AXIOM_U31_REV1_MAX_OFFSET_MASK GENMASK(6, 0)
+
+#define AXIOM_U32 0x32
+
+struct axiom_u31_usage_table_entry {
+ u8 usage_num;
+ u8 start_page;
+ u8 num_pages;
+ u8 max_offset;
+ u8 uifrevision;
+ u8 reserved;
+} __packed;
+
+#define AXIOM_U33 0x33
+
+struct axiom_u33_rev2 {
+ __le32 runtime_crc;
+ __le32 runtime_nvm_crc;
+ __le32 bootloader_crc;
+ __le32 nvltlusageconfig_crc;
+ __le32 vltusageconfig_crc;
+ __le32 u22_sequencedata_crc;
+ __le32 u43_hotspots_crc;
+ __le32 u93_profiles_crc;
+ __le32 u94_deltascalemap_crc;
+ __le32 runtimehash_crc;
+};
+
+struct axiom_u33_rev3 {
+ __le32 runtime_crc;
+ __le32 runtime_nvm_crc;
+ __le32 bootloader_crc;
+ __le32 nvltlusageconfig_crc;
+ __le32 vltusageconfig_crc;
+ __le32 u22_sequencedata_crc;
+ __le32 u43_hotspots_crc;
+ __le32 u77_dod_data_crc;
+ __le32 u93_profiles_crc;
+ __le32 u94_deltascalemap_crc;
+ __le32 runtimehash_crc;
+};
+
+struct axiom_u33_rev6 {
+ __le32 runtime_crc;
+ __le32 runtime_nvm_crc;
+ __le32 bootloader_crc;
+ __le32 nvltlusageconfig_crc;
+ __le32 vltusageconfig_crc;
+ __le32 reserved[3];
+ __le32 u93_profiles_crc;
+ __le32 u94_deltascalemap_crc;
+ __le32 runtimehash_crc;
+};
+
+#define AXIOM_U34 0x34
+#define AXIOM_U34_REV1_OVERFLOW_MASK BIT(7)
+#define AXIOM_U34_REV1_REPORTLENGTH_MASK GENMASK(6, 0)
+#define AXIOM_U34_REV1_PREAMBLE_BYTES 2
+#define AXIOM_U34_REV1_POSTAMBLE_BYTES 4
+
+#define AXIOM_U36 0x36
+
+#define AXIOM_U41 0x41
+#define AXIOM_U41_REV2_TARGETSTATUS_REG 0
+#define AXIOM_U41_REV2_X_REG(id) ((4 * (id)) + 2)
+#define AXIOM_U41_REV2_Y_REG(id) ((4 * (id)) + 4)
+#define AXIOM_U41_REV2_Z_REG(id) ((id) + 42)
+
+#define AXIOM_U42 0x42
+#define AXIOM_U42_REV1_REPORT_ID_CONTAINS(id) ((id) + 2)
+#define AXIOM_U42_REV1_REPORT_ID_TOUCH 1 /* Touch, Proximity, Hover */
+
+#define AXIOM_U42_REV4_REPORT_ID_CONTAINS(id) ((id) + 8)
+#define AXIOM_U42_REV4_REPORT_ID_TOUCH 1 /* Touch, Proximity, Hover */
+
+#define AXIOM_U43 0x43 /* CDU */
+
+#define AXIOM_U64 0x64
+#define AXIOM_U64_REV2_ENABLECDSPROCESSING_REG 0
+#define AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK BIT(0)
+
+#define AXIOM_U77 0x77 /* CDU */
+#define AXIOM_U82 0x82
+#define AXIOM_U93 0x93 /* CDU */
+#define AXIOM_U94 0x94 /* CDU */
+
+/*
+ * Axiom CDU usage structure copied from downstream CDU_Common.py. Downstream
+ * doesn't mention any revision. According downstream all CDU register windows
+ * are 56 byte wide (8 byte header + 48 byte data).
+ */
+#define AXIOM_CDU_CMD_STORE 0x0002
+#define AXIOM_CDU_CMD_COMMIT 0x0003
+#define AXIOM_CDU_PARAM0_COMMIT 0xb10c
+#define AXIOM_CDU_PARAM1_COMMIT 0xc0de
+
+#define AXIOM_CDU_RESP_SUCCESS 0x0000
+#define AXIOM_CDU_MAX_DATA_BYTES 48
+
+struct axiom_cdu_usage {
+ union {
+ __le16 command;
+ __le16 response;
+ };
+ __le16 parameters[3];
+ u8 data[AXIOM_CDU_MAX_DATA_BYTES];
+};
+
+/*
+ * u01 for the bootloader protocol (BLP)
+ *
+ * Values taken from Bootloader.py [1] which had a comment that documentation
+ * values are out dated. The BLP does not have different versions according the
+ * documentation python helper.
+ *
+ * [1] https://github.com/TouchNetix/axiom_pylib
+ */
+#define AXIOM_U01_BLP_COMMAND_REG 0x0100
+#define AXIOM_U01_BLP_COMMAND_RESET BIT(1)
+#define AXIOM_U01_BLP_SATUS_REG 0x0100
+#define AXIOM_U01_BLP_STATUS_BUSY BIT(0)
+#define AXIOM_U01_BLP_FIFO_REG 0x0102
+#define AXIOM_U01_BLP_FIFO_CHK_SIZE_BYTES 255
+
+#define AXIOM_PROX_LEVEL -128
+#define AXIOM_STARTUP_TIME_MS 110
+
+#define AXIOM_USAGE_BASEADDR_MASK GENMASK(15, 8)
+#define AXIOM_MAX_USAGES 256 /* u00 - uFF */
+#define AXIOM_MAX_BASEADDR 256
+/*
+ * The devices have a 16bit ADC but Touchnetix used the lower two bits for other
+ * information.
+ */
+#define AXIOM_MAX_XY (65535 - 3)
+#define AXIOM_DEFAULT_POLL_INTERVAL_MS 10
+#define AXIOM_PAGE_BYTE_LEN 256
+#define AXIOM_MAX_XFERLEN 0x7fff
+#define AXIOM_MAX_TOUCHSLOTS 10
+#define AXIOM_MAX_TOUCHSLOTS_MASK GENMASK(9, 0)
+
+/* aXiom firmware (.axfw) */
+#define AXIOM_FW_AXFW_SIGNATURE "AXFW"
+#define AXIOM_FW_AXFW_FILE_FMT_VER 0x0200
+
+struct axiom_fw_axfw_hdr {
+ u8 signature[4];
+ __le32 file_crc32;
+ __le16 file_format_ver;
+ __le16 device_id;
+ u8 variant;
+ u8 minor_ver;
+ u8 major_ver;
+ u8 rc_ver;
+ u8 status;
+ __le16 silicon_ver;
+ u8 silicon_rev;
+ __le32 fw_crc32;
+} __packed;
+
+struct axiom_fw_axfw_chunk_hdr {
+ u8 internal[6]; /* no description */
+ __be16 payload_length;
+};
+
+/* aXiom config (.th2cfgbin) */
+#define AXIOM_FW_CFG_SIGNATURE 0x20071969
+
+struct axiom_fw_cfg_hdr {
+ __be32 signature;
+ __le16 file_format_ver;
+ __le16 tcp_file_rev_major;
+ __le16 tcp_file_rev_minor;
+ __le16 tcp_file_rev_patch;
+ u8 tcp_version;
+} __packed;
+
+struct axiom_fw_cfg_chunk_hdr {
+ u8 usage_num;
+ u8 usage_rev;
+ u8 reserved;
+ __le16 usage_length;
+} __packed;
+
+struct axiom_fw_cfg_chunk {
+ u8 usage_num;
+ u8 usage_rev;
+ u16 usage_length;
+ const u8 *usage_content;
+};
+
+enum axiom_fw_type {
+ AXIOM_FW_AXFW,
+ AXIOM_FW_CFG,
+ AXIOM_FW_NUM
+};
+
+enum axiom_crc_type {
+ AXIOM_CRC_CUR,
+ AXIOM_CRC_NEW,
+ AXIOM_CRC_NUM
+};
+
+struct axiom_data;
+
+struct axiom_usage_info {
+ unsigned char usage_num; /* uXX number (XX in hex) */
+ unsigned int rev_num; /* rev.X (X in dec) */
+ bool is_cdu;
+ bool is_ro;
+
+ /* Optional hooks */
+ int (*process_report)(struct axiom_data *ts, const u8 *buf, size_t bufsize);
+};
+
+enum axiom_runmode {
+ AXIOM_DISCOVERY_MODE,
+ AXIOM_TCP_MODE,
+ AXIOM_TCP_CFG_UPDATE_MODE,
+ AXIOM_BLP_MODE,
+};
+
+struct axiom_data {
+ struct input_dev *input;
+ struct device *dev;
+
+ struct gpio_desc *reset_gpio;
+ struct regulator_bulk_data supplies[2];
+ unsigned int num_supplies;
+
+ struct regmap *regmap;
+ struct touchscreen_properties prop;
+ bool irq_setup_done;
+ u32 poll_interval;
+
+ struct drm_panel_follower panel_follower;
+ bool is_panel_follower;
+
+ enum axiom_runmode mode;
+ /*
+ * Two completion types to support firmware updates
+ * in irq and poll mode.
+ */
+ struct axiom_completion {
+ struct completion completion;
+ bool poll_done;
+ } nvm_write, boot_complete;
+
+ /* Lock to protect both firmware interfaces */
+ struct mutex fwupdate_lock;
+ struct axiom_firmware {
+ /* Lock to protect cancel */
+ struct mutex lock;
+ bool cancel;
+ struct fw_upload *fwl;
+ } fw[AXIOM_FW_NUM];
+
+ unsigned int fw_major;
+ unsigned int fw_minor;
+ unsigned int fw_rc;
+ unsigned int fw_status;
+ unsigned int fw_variant;
+ u16 device_id;
+ u16 jedec_id;
+ u8 silicon_rev;
+
+ /* CRCs we need to check during a config update */
+ struct axiom_crc {
+ u32 runtime;
+ u32 vltusageconfig;
+ u32 nvltlusageconfig;
+ u32 u22_sequencedata;
+ u32 u43_hotspots;
+ u32 u77_dod_data;
+ u32 u93_profiles;
+ u32 u94_deltascalemap;
+ } crc[AXIOM_CRC_NUM];
+
+ bool cds_enabled;
+ unsigned long enabled_slots;
+ unsigned int num_slots;
+
+ unsigned int max_report_byte_len;
+ /* Used for access by usage number */
+ struct axiom_usage_table_entry {
+ bool populated;
+ unsigned int baseaddr;
+ unsigned int size_bytes;
+ const struct axiom_usage_info *info;
+ } usage_table[AXIOM_MAX_USAGES];
+ /* Used for access by usage base address */
+ struct axiom_usage_table_entry *usage_table_by_baseaddr[AXIOM_MAX_BASEADDR];
+};
+
+static int axiom_u01_rev1_process_report(struct axiom_data *ts, const u8 *buf,
+ size_t bufsize);
+static int axiom_u34_rev1_process_report(struct axiom_data *ts, const u8 *_buf,
+ size_t bufsize);
+static int axiom_u41_rev2_process_report(struct axiom_data *ts, const u8 *buf,
+ size_t bufsize);
+
+#define AXIOM_USAGE(num, rev) \
+ { \
+ .usage_num = num, \
+ .rev_num = rev, \
+ }
+
+#define AXIOM_RO_USAGE(num, rev) \
+ { \
+ .usage_num = num, \
+ .rev_num = rev, \
+ .is_ro = true, \
+ }
+
+#define AXIOM_CDU_USAGE(num, rev) \
+ { \
+ .usage_num = num, \
+ .rev_num = rev, \
+ .is_cdu = true, \
+ }
+
+#define AXIOM_REPORT_USAGE(num, rev, func) \
+ { \
+ .usage_num = num, \
+ .rev_num = rev, \
+ .process_report = func, \
+ }
+
+#define AXIOM_USAGE_REV_UNUSED (-1)
+
+/*
+ * All usages used by driver must be added to this list to ensure the correct
+ * communictation with the devices. The list can contain multiple entries of the
+ * same usage to handle different usage revisions.
+ *
+ * Note:
+ * During a th2cfgbin update the driver may use usages not listed here.
+ * Therefore the th2cfgbin update compares the current running FW again the
+ * th2cfgbin targets FW.
+ */
+static const struct axiom_usage_info driver_required_usages[] = {
+ AXIOM_REPORT_USAGE(AXIOM_U01, 1, axiom_u01_rev1_process_report),
+ AXIOM_REPORT_USAGE(AXIOM_U01, 3, axiom_u01_rev1_process_report),
+ AXIOM_USAGE(AXIOM_U02, 1),
+ AXIOM_USAGE(AXIOM_U02, 2),
+ AXIOM_USAGE(AXIOM_U04, 1),
+ AXIOM_RO_USAGE(AXIOM_U31, 1),
+ AXIOM_RO_USAGE(AXIOM_U33, 2),
+ AXIOM_RO_USAGE(AXIOM_U33, 3),
+ AXIOM_RO_USAGE(AXIOM_U33, 6),
+ AXIOM_REPORT_USAGE(AXIOM_U34, 1, axiom_u34_rev1_process_report),
+ AXIOM_REPORT_USAGE(AXIOM_U41, 2, axiom_u41_rev2_process_report),
+ AXIOM_REPORT_USAGE(AXIOM_U41, 4, axiom_u41_rev2_process_report),
+ AXIOM_REPORT_USAGE(AXIOM_U41, 5, axiom_u41_rev2_process_report),
+ AXIOM_USAGE(AXIOM_U42, 1),
+ AXIOM_USAGE(AXIOM_U42, 4),
+ AXIOM_USAGE(AXIOM_U42, 7),
+ AXIOM_USAGE(AXIOM_U64, 2),
+ AXIOM_USAGE(AXIOM_U64, 4),
+ { /* sentinel */ }
+};
+
+/*
+ * All usages below are unused but the driver needs to know the type (ro, cdu)
+ * to handle them correctly. Unfortunately the type is not discoverable. Once
+ * a usage is actually used, it must be shifted to driver_required_usages and
+ * the revision must be set accordingly.
+ */
+static const struct axiom_usage_info driver_additional_usages[] = {
+ AXIOM_CDU_USAGE(AXIOM_U05, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_CDU_USAGE(AXIOM_U22, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_RO_USAGE(AXIOM_U32, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_RO_USAGE(AXIOM_U36, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_CDU_USAGE(AXIOM_U43, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_CDU_USAGE(AXIOM_U77, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_RO_USAGE(AXIOM_U82, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_CDU_USAGE(AXIOM_U93, AXIOM_USAGE_REV_UNUSED),
+ AXIOM_CDU_USAGE(AXIOM_U94, AXIOM_USAGE_REV_UNUSED),
+ { /* sentinel */ }
+};
+
+/************************ Common helpers **************************************/
+
+static void axiom_enable_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+
+ if (client->irq && ts->irq_setup_done)
+ enable_irq(client->irq);
+}
+
+static void axiom_disable_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+
+ if (client->irq && ts->irq_setup_done)
+ disable_irq(client->irq);
+}
+
+static void axiom_set_runmode(struct axiom_data *ts, enum axiom_runmode mode)
+{
+ ts->mode = mode;
+}
+
+static enum axiom_runmode axiom_get_runmode(struct axiom_data *ts)
+{
+ return ts->mode;
+}
+
+static const char *axiom_runmode_to_string(struct axiom_data *ts)
+{
+ switch (ts->mode) {
+ case AXIOM_DISCOVERY_MODE: return "discovery";
+ case AXIOM_TCP_MODE: return "tcp";
+ case AXIOM_TCP_CFG_UPDATE_MODE: return "th2cfg-update";
+ case AXIOM_BLP_MODE: return "bootlaoder";
+ default: return "unknown";
+ }
+}
+
+static bool axiom_skip_usage_check(struct axiom_data *ts)
+{
+ switch (ts->mode) {
+ case AXIOM_TCP_CFG_UPDATE_MODE:
+ case AXIOM_DISCOVERY_MODE:
+ case AXIOM_BLP_MODE:
+ return true;
+ case AXIOM_TCP_MODE:
+ default:
+ return false;
+ }
+}
+
+static unsigned int axiom_usage_baseaddr(struct axiom_data *ts,
+ unsigned char usage_num)
+{
+ return ts->usage_table[usage_num].baseaddr;
+}
+
+static unsigned int axiom_usage_size(struct axiom_data *ts,
+ unsigned char usage_num)
+{
+ return ts->usage_table[usage_num].size_bytes;
+}
+
+static int axiom_usage_rev(struct axiom_data *ts, unsigned char usage_num)
+{
+ struct axiom_usage_table_entry *entry = &ts->usage_table[usage_num];
+
+ if (!entry->info)
+ return -EINVAL;
+
+ return entry->info->rev_num;
+}
+
+static bool axiom_driver_supports_usage(struct axiom_data *ts,
+ unsigned char usage_num)
+{
+ const struct axiom_usage_info *iter = driver_required_usages;
+ struct device *dev = ts->dev;
+ int rev;
+
+ /*
+ * Some features depend on the current running firmware. Don't print an
+ * error if the usage for an optional feature is missing.
+ */
+ if (!ts->usage_table[usage_num].populated) {
+ dev_dbg(dev, "u%02X is not supported by the current firmware\n",
+ usage_num);
+ return false;
+ }
+
+ rev = axiom_usage_rev(ts, usage_num);
+ if (rev < 0) {
+ dev_warn(dev, "Driver doesn't support u%02X yet\n", usage_num);
+ return false;
+ }
+
+ for (; iter; iter++) {
+ if (iter->usage_num != usage_num)
+ continue;
+
+ if (iter->rev_num == rev)
+ return true;
+ }
+
+ dev_warn(dev, "Driver doesn't support u%02X rev.%d yet\n",
+ usage_num, rev);
+
+ return false;
+}
+
+static bool axiom_usage_entry_is_report(struct axiom_u31_usage_table_entry *entry)
+{
+ return entry->num_pages == 0;
+}
+
+static unsigned int axiom_get_usage_size_bytes(struct axiom_u31_usage_table_entry *entry)
+{
+ unsigned char max_offset;
+
+ max_offset = FIELD_GET(AXIOM_U31_REV1_MAX_OFFSET_MASK,
+ entry->max_offset) + 1;
+ max_offset *= 2;
+
+ if (axiom_usage_entry_is_report(entry))
+ return max_offset;
+
+ if (FIELD_GET(AXIOM_U31_REV1_OFFSET_TYPE_MASK, entry->max_offset))
+ return (entry->num_pages - 1) * AXIOM_PAGE_BYTE_LEN + max_offset;
+
+ return max_offset;
+}
+
+static void axiom_dump_usage_entry(struct device *dev,
+ struct axiom_u31_usage_table_entry *entry)
+{
+ unsigned int page_len, total_len;
+
+ total_len = axiom_get_usage_size_bytes(entry);
+
+ if (total_len > AXIOM_PAGE_BYTE_LEN)
+ page_len = AXIOM_PAGE_BYTE_LEN;
+ else
+ page_len = total_len;
+
+ if (axiom_usage_entry_is_report(entry))
+ dev_dbg(dev,
+ "u%02X rev.%d total-len:%u [REPORT]\n",
+ entry->usage_num, entry->uifrevision, total_len);
+ else
+ dev_dbg(dev,
+ "u%02X rev.%d first-page:%#02x page-len:%u num-pages:%u total-len:%u\n",
+ entry->usage_num, entry->uifrevision, entry->start_page, page_len,
+ entry->num_pages, total_len);
+}
+
+static const struct axiom_usage_info *
+axiom_get_usage_info(struct axiom_u31_usage_table_entry *query)
+{
+ const struct axiom_usage_info *info = driver_required_usages;
+ bool required = false;
+ bool found = false;
+
+ for (; info->usage_num; info++) {
+ /* Skip all usages not used by the driver */
+ if (query->usage_num != info->usage_num)
+ continue;
+
+ /* The usage is used so we need to mark it as required */
+ required = true;
+
+ /* Continue with the next usage if the revision doesn't match */
+ if (query->uifrevision != info->rev_num)
+ continue;
+
+ found = true;
+ break;
+ }
+
+ if (found)
+ return info;
+
+ /* Return an error if not found but required */
+ if (required)
+ return ERR_PTR(-EINVAL);
+
+ info = driver_additional_usages;
+ for (; info->usage_num; info++) {
+ if (query->usage_num != info->usage_num)
+ continue;
+
+ /*
+ * No need to check the revision since these usages are not
+ * used actually but the driver needs the type information.
+ */
+ return info;
+ }
+
+ /* No info found */
+ return NULL;
+}
+
+static bool axiom_usage_supported(struct axiom_data *ts, unsigned int regaddr)
+{
+ struct axiom_usage_table_entry *entry;
+ struct device *dev = ts->dev;
+ unsigned char baseaddr;
+
+ if (axiom_skip_usage_check(ts))
+ return true;
+
+ baseaddr = FIELD_GET(AXIOM_USAGE_BASEADDR_MASK, regaddr);
+ entry = ts->usage_table_by_baseaddr[baseaddr];
+
+ dev_dbg(dev, "Checking support for i2c-reg:%#04x (reg-baseaddr:%#02x)\n",
+ regaddr, baseaddr);
+
+ /*
+ * Ensure that no one messed with the driver e.g.:
+ * - by not retrieving the baseaddr properly, or
+ * - by making use of an usage without adding it to
+ * driver_required_usages[].
+ */
+ if (!entry) {
+ dev_warn(dev, "Device doesn't support this baseaddr, driver bug!\n");
+ return false;
+ }
+
+ if (!entry->info) {
+ dev_warn(dev, "Driver doesn't support this usage, driver bug!\n");
+ return false;
+ }
+
+ return true;
+}
+
+static int axiom_process_report(struct axiom_data *ts, unsigned char usage_num,
+ const u8 *buf, size_t buflen);
+
+static unsigned long axiom_wait_for_completion_timeout(struct axiom_data *ts,
+ struct axiom_completion *x,
+ long timeout)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+ unsigned long poll_timeout;
+
+ if (client->irq)
+ return wait_for_completion_timeout(&x->completion, timeout);
+
+ /*
+ * Only firmware update cases do wait for completion. Since they require
+ * the input device to be closed, the poller is not running. So we need
+ * to do the polling manually.
+ */
+ poll_timeout = timeout / 10;
+
+ /*
+ * Very basic and not very accurate but it does the job because there
+ * are no known timeout constraints. Poll via axiom_process_report()
+ * and without the help of axiom_poll() since the input device may not
+ * be available yet.
+ */
+ do {
+ axiom_process_report(ts, AXIOM_U34, NULL, 0);
+ fsleep(jiffies_to_usecs(poll_timeout));
+ if (x->poll_done)
+ break;
+ timeout -= poll_timeout;
+ } while (timeout > 0);
+
+ x->poll_done = false;
+
+ return timeout > 0 ? timeout : 0;
+}
+
+static void axiom_complete(struct axiom_data *ts, struct axiom_completion *x)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+
+ if (client->irq)
+ complete(&x->completion);
+ else
+ x->poll_done = true;
+}
+
+/*************************** Usage handling ***********************************/
+/*
+ * Wrapper functions to handle the usage access. Wrappers are used to add
+ * different revision handling later on more easily.
+ */
+static int axiom_u02_wait_idle(struct axiom_data *ts)
+{
+ unsigned int reg;
+ int error, ret;
+ u16 cmd;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U02);
+ reg += AXIOM_U02_REV1_COMMAND_REG;
+
+ /*
+ * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
+ * the last command successfully completed and the device is idle.
+ */
+ error = read_poll_timeout(regmap_raw_read, ret,
+ ret || cmd == AXIOM_U02_REV1_RESP_SUCCESS,
+ 10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
+ ts->regmap, reg, &cmd, 2);
+ if (error) {
+ dev_err(ts->dev, "Poll u02 timedout with: %#x\n", cmd);
+ return error;
+ }
+
+ return 0;
+}
+
+static int axiom_u02_send_msg(struct axiom_data *ts,
+ const struct axiom_u02_rev1_system_manager_msg *msg,
+ bool validate_response)
+{
+ unsigned int reg;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U02);
+ reg += AXIOM_U02_REV1_COMMAND_REG;
+
+ error = regmap_raw_write(ts->regmap, reg, msg, sizeof(*msg));
+ if (error)
+ return error;
+
+ if (!validate_response)
+ return 0;
+
+ return axiom_u02_wait_idle(ts);
+}
+
+static int axiom_u02_rev1_send_single_cmd(struct axiom_data *ts, u16 cmd)
+{
+ struct axiom_u02_rev1_system_manager_msg msg = {
+ .command = cpu_to_le16(cmd)
+ };
+
+ return axiom_u02_send_msg(ts, &msg, true);
+}
+
+static int axiom_u02_handshakenvm(struct axiom_data *ts)
+{
+ return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_HANDSHAKENVM);
+}
+
+static int axiom_u02_computecrc(struct axiom_data *ts)
+{
+ return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_COMPUTECRCS);
+}
+
+static int axiom_u02_stop(struct axiom_data *ts)
+{
+ return axiom_u02_rev1_send_single_cmd(ts, AXIOM_U02_REV1_CMD_STOP);
+}
+
+static int axiom_u02_save_config(struct axiom_data *ts)
+{
+ struct axiom_u02_rev1_system_manager_msg msg;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_SAVEVLTLCFG2NVM);
+ msg.parameters[0] = 0; /* Don't care */
+ msg.parameters[1] = cpu_to_le16(AXIOM_U02_REV1_PARAM1_SAVEVLTLCFG2NVM);
+ msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_SAVEVLTLCFG2NVM);
+
+ error = axiom_u02_send_msg(ts, &msg, false);
+ if (error)
+ return error;
+
+ /* Downstream axcfg.py waits for 2sec without checking U01 response */
+ if (!axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
+ msecs_to_jiffies(2 * MSEC_PER_SEC))) {
+ dev_err(ts->dev, "Error save volatile config timedout\n");
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static int axiom_u02_swreset(struct axiom_data *ts)
+{
+ struct axiom_u02_rev1_system_manager_msg msg = {
+ .command = cpu_to_le16(AXIOM_U02_REV1_CMD_SOFTRESET),
+ };
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ error = axiom_u02_send_msg(ts, &msg, false);
+ if (error)
+ return error;
+
+ /*
+ * Downstream axcfg.py waits for 1sec without checking U01 HELLO. Tests
+ * showed that waiting for the HELLO message isn't enough therefore we
+ * need to add the additional fsleep(1sec).
+ * Touchnetix said that the boot can take up to 2sec if all self tests
+ * are enabled, so wait 2sec for the HELLO message.
+ */
+ if (!axiom_wait_for_completion_timeout(ts, &ts->boot_complete,
+ msecs_to_jiffies(2 * MSEC_PER_SEC))) {
+ dev_err(ts->dev, "Error swreset timedout\n");
+ error = -ETIMEDOUT;
+ }
+
+ fsleep(USEC_PER_SEC);
+
+ return error;
+}
+
+static int axiom_u02_fillconfig(struct axiom_data *ts)
+{
+ struct axiom_u02_rev1_system_manager_msg msg;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_FILLCONFIG);
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_FILLCONFIG);
+ msg.parameters[1] = cpu_to_le16(AXIOM_U02_REV1_PARAM1_FILLCONFIG);
+ msg.parameters[2] = cpu_to_le16(AXIOM_U02_REV1_PARAM2_FILLCONFIG_ZERO);
+
+ return axiom_u02_send_msg(ts, &msg, true);
+}
+
+static int axiom_u02_enter_bootloader(struct axiom_data *ts)
+{
+ struct axiom_u02_rev1_system_manager_msg msg = { };
+ struct device *dev = ts->dev;
+ unsigned int val;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U02))
+ return -EINVAL;
+
+ msg.command = cpu_to_le16(AXIOM_U02_REV1_CMD_ENTERBOOTLOADER);
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY1);
+ error = axiom_u02_send_msg(ts, &msg, true);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key1: %d\n", error);
+ return error;
+ }
+
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY2);
+ error = axiom_u02_send_msg(ts, &msg, true);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key2: %d\n", error);
+ return error;
+ }
+
+ /* No need to wait since we wait afterwards anyway */
+ msg.parameters[0] = cpu_to_le16(AXIOM_U02_REV1_PARAM0_ENTERBOOLOADER_KEY3);
+ error = axiom_u02_send_msg(ts, &msg, false);
+ if (error) {
+ dev_err(dev, "Failed to send bootloader-key3: %d\n", error);
+ return error;
+ }
+
+ /* Sleep before the first read to give the device time */
+ fsleep(250 * USEC_PER_MSEC);
+
+ /* Wait till the device reports it is in bootloader mode */
+ error = regmap_read_poll_timeout(ts->regmap,
+ AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, val,
+ FIELD_GET(AXIOM_U31_REV1_MODE_MASK, val) ==
+ AXIOM_U31_REV1_MODE_BLP,
+ 250 * USEC_PER_MSEC, USEC_PER_SEC);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static u8 *axiom_u04_get(struct axiom_data *ts)
+{
+ unsigned int bufsize;
+ unsigned int reg;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U04))
+ return ERR_PTR(-EINVAL);
+
+ bufsize = axiom_usage_size(ts, AXIOM_U04);
+ u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
+ if (!buf)
+ return ERR_PTR(-ENOMEM);
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U04);
+ error = regmap_raw_read(ts->regmap, reg, buf, bufsize);
+ if (error)
+ return ERR_PTR(error);
+
+ return_ptr(buf);
+}
+
+static int axiom_u04_set(struct axiom_data *ts, u8 *buf)
+{
+ unsigned int bufsize;
+ unsigned int reg;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U04))
+ return -EINVAL;
+
+ bufsize = axiom_usage_size(ts, AXIOM_U04);
+ reg = axiom_usage_baseaddr(ts, AXIOM_U04);
+ return regmap_raw_write(ts->regmap, reg, buf, bufsize);
+}
+
+/*
+ * U31 revision must be always rev.1 else the whole self discovery mechanism
+ * fall apart.
+ */
+static int axiom_u31_parse_device_info(struct axiom_data *ts)
+{
+ struct regmap *regmap = ts->regmap;
+ unsigned int id_low, id_high, val;
+ int error;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &id_high);
+ if (error)
+ return error;
+ id_high = FIELD_GET(AXIOM_U31_REV1_DEVICE_ID_HIGH_MASK, id_high);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_LOW_REG, &id_low);
+ if (error)
+ return error;
+ ts->device_id = id_high << 8 | id_low;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MAJ_REG, &val);
+ if (error)
+ return error;
+ ts->fw_major = val;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_MIN_REG, &val);
+ if (error)
+ return error;
+ ts->fw_minor = val;
+
+ /* All other fields are not allowed to be read in BLP mode */
+ if (axiom_get_runmode(ts) == AXIOM_BLP_MODE)
+ return 0;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_RC_REG, &val);
+ if (error)
+ return error;
+ ts->fw_rc = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_RC_MASK, val);
+ ts->silicon_rev = FIELD_GET(AXIOM_U31_REV1_SILICON_REV_MASK, val);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_RUNTIME_FW_STATUS_REG, &val);
+ if (error)
+ return error;
+ ts->fw_status = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_STATUS, val);
+ ts->fw_variant = FIELD_GET(AXIOM_U31_REV1_RUNTIME_FW_VARIANT, val);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_HIGH_REG, &val);
+ if (error)
+ return error;
+ ts->jedec_id = val << 8;
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_JEDEC_ID_LOW_REG, &val);
+ if (error)
+ return error;
+ ts->jedec_id |= val;
+
+ return 0;
+}
+
+static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc);
+
+static int axiom_u31_device_discover(struct axiom_data *ts)
+{
+ struct axiom_u31_usage_table_entry *entry;
+ struct regmap *regmap = ts->regmap;
+ unsigned int mode, num_usages;
+ struct device *dev = ts->dev;
+ unsigned int i;
+ int error;
+
+ axiom_set_runmode(ts, AXIOM_DISCOVERY_MODE);
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG, &mode);
+ if (error) {
+ dev_err(dev, "Failed to read MODE\n");
+ return error;
+ }
+
+ mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
+ if (mode == AXIOM_U31_REV1_MODE_BLP)
+ axiom_set_runmode(ts, AXIOM_BLP_MODE);
+
+ error = axiom_u31_parse_device_info(ts);
+ if (error) {
+ dev_err(dev, "Failed to parse device info\n");
+ return error;
+ }
+
+ /* All other fields are not allowed to be read in BLP mode */
+ if (axiom_get_runmode(ts) == AXIOM_BLP_MODE) {
+ dev_info(dev, "Device in Bootloader mode, firmware upload required\n");
+ return -EACCES;
+ }
+
+ error = regmap_read(regmap, AXIOM_U31_REV1_NUM_USAGES_REG, &num_usages);
+ if (error) {
+ dev_err(dev, "Failed to read NUM_USAGES\n");
+ return error;
+ }
+
+ struct axiom_u31_usage_table_entry *u31_usage_table __free(kfree) =
+ kcalloc(num_usages, sizeof(*u31_usage_table), GFP_KERNEL);
+ if (!u31_usage_table)
+ return -ENOMEM;
+
+ error = regmap_raw_read(regmap, AXIOM_U31_REV1_PAGE1, u31_usage_table,
+ array_size(num_usages, sizeof(*u31_usage_table)));
+ if (error) {
+ dev_err(dev, "Failed to read NUM_USAGES\n");
+ return error;
+ }
+
+ /*
+ * axiom_u31_device_discover() is call after fw update too, so ensure
+ * that the usage_table is cleared.
+ */
+ memset(ts->usage_table, 0, sizeof(ts->usage_table));
+ memset(ts->usage_table_by_baseaddr, 0, sizeof(ts->usage_table_by_baseaddr));
+
+ for (i = 0, entry = u31_usage_table; i < num_usages; i++, entry++) {
+ unsigned char idx = entry->usage_num;
+ const struct axiom_usage_info *info;
+ bool skip_usage_add = false;
+ unsigned int size_bytes;
+
+ axiom_dump_usage_entry(dev, entry);
+
+ /*
+ * Verify that the driver used usages are supported. Don't abort
+ * yet if a usage isn't supported to allow the user to dump the
+ * actual usage table but skip adding it to the usage_table.
+ */
+ info = axiom_get_usage_info(entry);
+ if (IS_ERR(info)) {
+ dev_info(dev, "Required usage u%02X isn't supported for rev.%u\n",
+ entry->usage_num, entry->uifrevision);
+ error = -EACCES;
+ skip_usage_add = true;
+ }
+
+ if (skip_usage_add)
+ continue;
+
+ size_bytes = axiom_get_usage_size_bytes(entry);
+
+ ts->usage_table[idx].baseaddr =
+ FIELD_PREP(AXIOM_USAGE_BASEADDR_MASK, entry->start_page);
+ ts->usage_table[idx].size_bytes = size_bytes;
+ ts->usage_table[idx].populated = true;
+ ts->usage_table[idx].info = info;
+ /* Reports are indirectly accessed, no baseaddr */
+ if (!axiom_usage_entry_is_report(entry))
+ ts->usage_table_by_baseaddr[entry->start_page] =
+ &ts->usage_table[idx];
+
+ if (axiom_usage_entry_is_report(entry) &&
+ ts->max_report_byte_len < size_bytes)
+ ts->max_report_byte_len = size_bytes;
+ }
+
+ /*
+ * Each usage support up to 256-byte. The 257-byte would point to the
+ * next usage.
+ */
+ if (ts->max_report_byte_len > AXIOM_PAGE_BYTE_LEN) {
+ dev_err(dev, "Invalid report length (%u-byte) detected\n",
+ ts->max_report_byte_len);
+ error = -EACCES;
+ }
+
+ if (error)
+ return error;
+
+ /* From now on we are in TCP mode to include usage revision checks */
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static int axiom_u33_read(struct axiom_data *ts, struct axiom_crc *crc)
+{
+ struct device *dev = ts->dev;
+ unsigned int reg;
+ int error;
+
+ if (!axiom_driver_supports_usage(ts, AXIOM_U33))
+ return -EINVAL;
+
+ /* Reset previous stored values */
+ memset(crc, 0, sizeof(*crc));
+
+ if (axiom_usage_rev(ts, AXIOM_U33) == 2) {
+ struct axiom_u33_rev2 val;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U33);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ crc->runtime = le32_to_cpu(val.runtime_crc);
+ crc->vltusageconfig = le32_to_cpu(val.vltusageconfig_crc);
+ crc->nvltlusageconfig = le32_to_cpu(val.nvltlusageconfig_crc);
+ crc->u22_sequencedata = le32_to_cpu(val.u22_sequencedata_crc);
+ crc->u43_hotspots = le32_to_cpu(val.u43_hotspots_crc);
+ crc->u93_profiles = le32_to_cpu(val.u93_profiles_crc);
+ crc->u94_deltascalemap = le32_to_cpu(val.u94_deltascalemap_crc);
+ } else if (axiom_usage_rev(ts, AXIOM_U33) == 3) {
+ struct axiom_u33_rev3 val;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U33);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ crc->runtime = le32_to_cpu(val.runtime_crc);
+ crc->vltusageconfig = le32_to_cpu(val.vltusageconfig_crc);
+ crc->nvltlusageconfig = le32_to_cpu(val.nvltlusageconfig_crc);
+ crc->u22_sequencedata = le32_to_cpu(val.u22_sequencedata_crc);
+ crc->u43_hotspots = le32_to_cpu(val.u43_hotspots_crc);
+ crc->u77_dod_data = le32_to_cpu(val.u77_dod_data_crc);
+ crc->u93_profiles = le32_to_cpu(val.u93_profiles_crc);
+ crc->u94_deltascalemap = le32_to_cpu(val.u94_deltascalemap_crc);
+ } else if (axiom_usage_rev(ts, AXIOM_U33) == 6) {
+ struct axiom_u33_rev6 val;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U33);
+ error = regmap_raw_read(ts->regmap, reg, &val, sizeof(val));
+ if (error) {
+ dev_err(dev, "Failed to read u33\n");
+ return error;
+ }
+
+ crc->runtime = le32_to_cpu(val.runtime_crc);
+ crc->vltusageconfig = le32_to_cpu(val.vltusageconfig_crc);
+ crc->nvltlusageconfig = le32_to_cpu(val.nvltlusageconfig_crc);
+ crc->u93_profiles = le32_to_cpu(val.u93_profiles_crc);
+ crc->u94_deltascalemap = le32_to_cpu(val.u94_deltascalemap_crc);
+ }
+
+ return 0;
+}
+
+static bool axiom_u42_touch_enabled(struct axiom_data *ts, const u8 *buf,
+ unsigned int touch_num)
+{
+ switch (axiom_usage_rev(ts, AXIOM_U42)) {
+ case 1:
+ return buf[AXIOM_U42_REV1_REPORT_ID_CONTAINS(touch_num)] ==
+ AXIOM_U42_REV1_REPORT_ID_TOUCH;
+ case 4:
+ return buf[AXIOM_U42_REV4_REPORT_ID_CONTAINS(touch_num)] ==
+ AXIOM_U42_REV4_REPORT_ID_TOUCH;
+ case 7:
+ return buf[AXIOM_U42_REV4_REPORT_ID_CONTAINS(touch_num)] ==
+ AXIOM_U42_REV4_REPORT_ID_TOUCH;
+ default:
+ /* Should never happen */
+ return false;
+ }
+}
+
+static bool axiom_u42_get_touchslots(struct axiom_data *ts)
+{
+ unsigned int bufsize;
+ unsigned int reg;
+ int error, i;
+
+ bufsize = axiom_usage_size(ts, AXIOM_U42);
+ u8 *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL);
+ if (!buf)
+ return false;
+
+ reg = axiom_usage_baseaddr(ts, AXIOM_U42);
+ error = regmap_raw_read(ts->regmap, reg, buf, bufsize);
+ if (error) {
+ dev_warn(ts->dev, "Failed to read u42\n");
+ return false;
+ }
+
+ ts->enabled_slots = 0;
+ ts->num_slots = 0;
+
+ for (i = 0; i < AXIOM_MAX_TOUCHSLOTS; i++) {
+ if (axiom_u42_touch_enabled(ts, buf, i)) {
+ ts->enabled_slots |= BIT(i);
+ ts->num_slots++;
+ }
+ }
+
+ return true;
+}
+
+static void axiom_get_touchslots(struct axiom_data *ts)
+{
+ if (!axiom_driver_supports_usage(ts, AXIOM_U42) ||
+ !axiom_u42_get_touchslots(ts)) {
+ dev_warn(ts->dev, "Use default touchslots num\n");
+ ts->enabled_slots = AXIOM_MAX_TOUCHSLOTS_MASK;
+ ts->num_slots = AXIOM_MAX_TOUCHSLOTS;
+ }
+}
+
+static void axiom_u64_cds_enabled(struct axiom_data *ts)
+{
+ unsigned int reg, val;
+ int error;
+
+ ts->cds_enabled = false;
+
+ if (axiom_driver_supports_usage(ts, AXIOM_U64)) {
+ reg = axiom_usage_baseaddr(ts, AXIOM_U64);
+ reg += AXIOM_U64_REV2_ENABLECDSPROCESSING_REG;
+
+ error = regmap_read(ts->regmap, reg, &val);
+ if (!error) {
+ val = FIELD_GET(AXIOM_U64_REV2_ENABLECDSPROCESSING_MASK, val);
+ ts->cds_enabled = val ? true : false;
+ }
+ }
+}
+
+static int axiom_cdu_wait_idle(struct axiom_data *ts, u8 cdu_usage_num)
+{
+ unsigned int reg;
+ int error, ret;
+ u16 cmd;
+
+ reg = axiom_usage_baseaddr(ts, cdu_usage_num);
+
+ /*
+ * Missing regmap_raw_read_poll_timeout for now. RESP_SUCCESS means that
+ * the last command successfully completed and the device is idle.
+ */
+ error = read_poll_timeout(regmap_raw_read, ret,
+ ret || cmd == AXIOM_CDU_RESP_SUCCESS,
+ 10 * USEC_PER_MSEC, 1 * USEC_PER_SEC, false,
+ ts->regmap, reg, &cmd, 2);
+ if (error) {
+ dev_err(ts->dev, "Poll CDU u%02X timedout with: %#x\n",
+ cdu_usage_num, cmd);
+ return error;
+ }
+
+ return 0;
+}
+
+/*********************** Report usage handling ********************************/
+
+static int axiom_process_report(struct axiom_data *ts, unsigned char usage_num,
+ const u8 *buf, size_t buflen)
+{
+ struct axiom_usage_table_entry *entry = &ts->usage_table[usage_num];
+
+ /* Skip processing if not in TCP mode */
+ if ((axiom_get_runmode(ts) != AXIOM_TCP_MODE) &&
+ (axiom_get_runmode(ts) != AXIOM_TCP_CFG_UPDATE_MODE))
+ return 0;
+
+ /* May happen if an unsupported usage was requested */
+ if (!entry) {
+ dev_info(ts->dev, "Unsupported usage U%x request\n", usage_num);
+ return 0;
+ }
+
+ /* Supported report usages need to have a process_report hook */
+ if (!entry->info || !entry->info->process_report)
+ return -EINVAL;
+
+ return entry->info->process_report(ts, buf, buflen);
+}
+
+/* Make use of datasheet method 1 - single transfer read */
+static int axiom_u34_rev1_process_report(struct axiom_data *ts,
+ const u8 *_buf, size_t bufsize)
+{
+ unsigned int reg = axiom_usage_baseaddr(ts, AXIOM_U34);
+ struct regmap *regmap = ts->regmap;
+ u8 buf[AXIOM_PAGE_BYTE_LEN] = { };
+ struct device *dev = ts->dev;
+ unsigned char report_usage;
+ u16 crc_report, crc_calc;
+ unsigned int len;
+ u8 *payload;
+ int error;
+
+ error = regmap_raw_read(regmap, reg, buf, ts->max_report_byte_len);
+ if (error)
+ return error;
+
+ /* TODO: Add overflow statistics */
+
+ /* REPORTLENGTH is in uint16 */
+ len = FIELD_GET(AXIOM_U34_REV1_REPORTLENGTH_MASK, buf[0]);
+ len *= 2;
+
+ /*
+ * The device keeps the IRQ asserted till the I2C-STOP signal was
+ * received and optional longer (up to 40us). This can trigger the ISR
+ * a 2nd time albeit the device data is not ready yet. In such case the
+ * device sends 0-length reports. Don't treat this as error.
+ */
+ if (len == 0) {
+ dev_dbg_ratelimited(dev, "0-length report received\n");
+ return 0;
+ }
+
+ /*
+ * The CRC16 value can be queried at the last two bytes of the report.
+ * The value itself is covering the complete report excluding the CRC16
+ * value at the end.
+ */
+ crc_report = get_unaligned_le16(&buf[len - 2]);
+ crc_calc = crc16(0, buf, (len - 2));
+
+ if (crc_calc != crc_report) {
+ dev_err_ratelimited(dev, "CRC16 mismatch!\n");
+ return -EINVAL;
+ }
+
+ report_usage = buf[1];
+ payload = &buf[AXIOM_U34_REV1_PREAMBLE_BYTES];
+ len -= AXIOM_U34_REV1_PREAMBLE_BYTES - AXIOM_U34_REV1_POSTAMBLE_BYTES;
+
+ switch (report_usage) {
+ case AXIOM_U01:
+ case AXIOM_U41:
+ /*
+ * axiom_driver_supports_usage() is not required since the
+ * correct .process_report() hooks are assigned during the
+ * discovery.
+ */
+ return axiom_process_report(ts, report_usage, payload, len);
+ default:
+ dev_dbg(dev, "Unsupported report u%02X received\n",
+ report_usage);
+ }
+
+ return 0;
+}
+
+static void axiom_u41_rev2_decode_target(const u8 *buf, u8 id,
+ u16 *x, u16 *y, s8 *z)
+{
+ u16 val;
+
+ val = get_unaligned_le16(&buf[AXIOM_U41_REV2_X_REG(id)]);
+ val &= AXIOM_MAX_XY;
+ *x = val;
+
+ val = get_unaligned_le16(&buf[AXIOM_U41_REV2_Y_REG(id)]);
+ val &= AXIOM_MAX_XY;
+ *y = val;
+
+ *z = buf[AXIOM_U41_REV2_Z_REG(id)];
+}
+
+static int axiom_u41_rev2_process_report(struct axiom_data *ts,
+ const u8 *buf, size_t bufsize)
+{
+ struct input_dev *input = ts->input;
+ unsigned char id;
+ u16 targets;
+
+ /*
+ * The input registration can be postponed but the touchscreen FW is
+ * sending u41 reports regardless.
+ */
+ if (!input)
+ return 0;
+
+ targets = get_unaligned_le16(&buf[AXIOM_U41_REV2_TARGETSTATUS_REG]);
+
+ for_each_set_bit(id, &ts->enabled_slots, AXIOM_MAX_TOUCHSLOTS) {
+ bool present;
+ u16 x, y;
+ s8 z;
+
+ axiom_u41_rev2_decode_target(buf, id, &x, &y, &z);
+
+ present = targets & BIT(id);
+ /* Ignore possible jitters */
+ if (z == AXIOM_PROX_LEVEL)
+ present = false;
+
+ dev_dbg(ts->dev, "id:%u x:%u y:%u z:%d present:%u",
+ id, x, y, z, present);
+
+ input_mt_slot(input, id);
+ if (input_mt_report_slot_state(input, MT_TOOL_FINGER, present))
+ touchscreen_report_pos(input, &ts->prop, x, y, true);
+
+ if (!present)
+ continue;
+
+ input_report_abs(input, ABS_MT_DISTANCE, z < 0 ? -z : 0);
+ if (ts->cds_enabled)
+ input_report_abs(input, ABS_MT_PRESSURE, z >= 0 ? z : 0);
+ }
+
+ input_sync(input);
+
+ return 0;
+}
+
+static int axiom_u01_rev1_process_report(struct axiom_data *ts,
+ const u8 *buf, size_t bufsize)
+{
+ switch (buf[AXIOM_U01_REV1_REPORTTYPE_REG]) {
+ case AXIOM_U01_REV1_REPORTTYPE_HELLO:
+ dev_dbg(ts->dev, "u01 HELLO received\n");
+ axiom_complete(ts, &ts->boot_complete);
+ return 0;
+ case AXIOM_U01_REV1_REPORTTYPE_HEARTBEAT:
+ dev_dbg_ratelimited(ts->dev, "u01 HEARTBEAT received\n");
+ return 0;
+ case AXIOM_U01_REV1_REPORTTYPE_OPCOMPLETE:
+ dev_dbg(ts->dev, "u01 OPCOMPLETE received\n");
+ axiom_u02_handshakenvm(ts);
+ axiom_complete(ts, &ts->nvm_write);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+/**************************** Regmap handling *********************************/
+
+#define AXIOM_CMD_HDR_DIR_MASK BIT(15)
+#define AXIOM_CMD_HDR_READ 1
+#define AXIOM_CMD_HDR_WRITE 0
+#define AXIOM_CMD_HDR_LEN_MASK GENMASK(14, 0)
+
+struct axiom_cmd_header {
+ __le16 target_address;
+ __le16 xferlen;
+};
+
+/* Custom regmap read/write handling is required due to the aXiom protocol */
+static int axiom_regmap_read(void *context, const void *reg_buf, size_t reg_size,
+ void *val_buf, size_t val_size)
+{
+ struct device *dev = context;
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+ struct axiom_cmd_header hdr;
+ struct i2c_msg xfer[2];
+ u16 xferlen, addr;
+ int ret;
+
+ if (val_size > AXIOM_MAX_XFERLEN) {
+ dev_err(ts->dev, "Exceed max xferlen: %zu > %u\n",
+ val_size, AXIOM_MAX_XFERLEN);
+ return -EINVAL;
+ }
+
+ addr = *((u16 *)reg_buf);
+ hdr.target_address = cpu_to_le16(addr);
+ xferlen = FIELD_PREP(AXIOM_CMD_HDR_DIR_MASK, AXIOM_CMD_HDR_READ) |
+ FIELD_PREP(AXIOM_CMD_HDR_LEN_MASK, val_size);
+ hdr.xferlen = cpu_to_le16(xferlen);
+
+ if (!axiom_usage_supported(ts, addr))
+ return -EINVAL;
+
+ xfer[0].addr = i2c->addr;
+ xfer[0].flags = 0;
+ xfer[0].len = sizeof(hdr);
+ xfer[0].buf = (u8 *)&hdr;
+
+ xfer[1].addr = i2c->addr;
+ xfer[1].flags = I2C_M_RD;
+ xfer[1].len = val_size;
+ xfer[1].buf = val_buf;
+
+ ret = i2c_transfer(i2c->adapter, xfer, ARRAY_SIZE(xfer));
+ if (likely(ret == ARRAY_SIZE(xfer)))
+ return 0;
+
+ return ret < 0 ? ret : -EIO;
+}
+
+static int axiom_regmap_write(void *context, const void *data, size_t count)
+{
+ struct device *dev = context;
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+ struct axiom_cmd_header hdr;
+ size_t val_size, msg_size;
+ u16 xferlen, addr;
+ int ret;
+
+ val_size = count - sizeof(addr);
+ if (val_size > AXIOM_MAX_XFERLEN) {
+ dev_err(ts->dev, "Exceed max xferlen: %zu > %u\n",
+ val_size, AXIOM_MAX_XFERLEN);
+ return -EINVAL;
+ }
+
+ addr = *((u16 *)data);
+ hdr.target_address = cpu_to_le16(addr);
+ xferlen = FIELD_PREP(AXIOM_CMD_HDR_DIR_MASK, AXIOM_CMD_HDR_WRITE) |
+ FIELD_PREP(AXIOM_CMD_HDR_LEN_MASK, val_size);
+ hdr.xferlen = cpu_to_le16(xferlen);
+
+ if (!axiom_usage_supported(ts, addr))
+ return -EINVAL;
+
+ msg_size = sizeof(hdr) + val_size;
+ u8 *buf __free(kfree) = kzalloc(msg_size, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ memcpy(buf, &hdr, sizeof(hdr));
+ memcpy(&buf[sizeof(hdr)], &((char *)data)[2], val_size);
+
+ ret = i2c_master_send(i2c, buf, msg_size);
+ if (likely(ret == msg_size))
+ return 0;
+
+ return ret < 0 ? ret : -EIO;
+}
+
+static const struct regmap_config axiom_i2c_regmap_config = {
+ .reg_bits = 16,
+ .val_bits = 8,
+ .read = axiom_regmap_read,
+ .write = axiom_regmap_write,
+};
+
+/************************ FW update handling **********************************/
+
+static int axiom_update_input_dev(struct axiom_data *ts);
+
+static enum fw_upload_err __axiom_axfw_fw_prepare(struct axiom_data *ts,
+ struct axiom_firmware *afw,
+ const u8 *data, u32 size)
+{
+ u8 major_ver, minor_ver, rc_ver, status, variant;
+ u32 fw_file_crc32, crc32_calc;
+ struct device *dev = ts->dev;
+ unsigned int signature_len;
+ u16 fw_file_format_ver;
+ u16 fw_file_device_id;
+
+ if (size < sizeof(struct axiom_fw_axfw_hdr)) {
+ dev_err(dev, "Invalid AXFW file size\n");
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+ }
+
+ signature_len = strlen(AXIOM_FW_AXFW_SIGNATURE);
+ if (strncmp(data, AXIOM_FW_AXFW_SIGNATURE, signature_len)) {
+ dev_err(dev, "No AXFW signature, abort firmware update\n");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ fw_file_crc32 = get_unaligned_le32(&data[signature_len]);
+ crc32_calc = crc32(~0, &data[8], size - 8) ^ 0xffffffff;
+ if (fw_file_crc32 != crc32_calc) {
+ dev_err(dev, "AXFW CRC32 doesn't match (fw:%#x calc:%#x)\n",
+ fw_file_crc32, crc32_calc);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ data += signature_len + sizeof(fw_file_crc32);
+ fw_file_format_ver = get_unaligned_le16(data);
+ if (fw_file_format_ver != AXIOM_FW_AXFW_FILE_FMT_VER) {
+ dev_err(dev, "Invalid AXFW file format version: %04x",
+ fw_file_format_ver);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ data += sizeof(fw_file_format_ver);
+ fw_file_device_id = get_unaligned_le16(data);
+ if (fw_file_device_id != ts->device_id) {
+ dev_err(dev, "Invalid AXFW target device (fw:%#04x dev:%#04x)\n",
+ fw_file_device_id, ts->device_id);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ /*
+ * The afterward fw duplication check requires that the devices
+ * discovery ran successfully at least once (-> device is in TCP mode).
+ * This is not always ensured because:
+ * * The usage discovery failed either during the probe or after the
+ * firmware update.
+ * * The device came up in bootloader mode because no valid firmware
+ * was found by the bootloader.
+ * * Downloading the firmware failed in between and the device is still
+ * in bootloader mode, or
+ */
+ if (axiom_get_runmode(ts) != AXIOM_TCP_MODE)
+ return FW_UPLOAD_ERR_NONE;
+
+ data += sizeof(fw_file_device_id);
+ variant = *data++;
+ minor_ver = *data++;
+ major_ver = *data++;
+ rc_ver = *data++;
+ status = *data++;
+
+ if (major_ver == ts->fw_major && minor_ver == ts->fw_minor &&
+ rc_ver == ts->fw_rc && status == ts->fw_status &&
+ variant == ts->fw_variant) {
+ return FW_UPLOAD_ERR_DUPLICATE;
+ }
+
+ dev_info(dev, "Detected AXFW %02u.%02u.%02u (%s)\n",
+ major_ver, minor_ver, rc_ver,
+ status ? "production" : "engineering");
+
+ guard(mutex)(&afw->lock);
+ return afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err axiom_axfw_fw_prepare(struct fw_upload *fw_upload,
+ const u8 *data, u32 size)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
+ enum fw_upload_err ret;
+
+ scoped_guard(mutex, &afw->lock) {
+ afw->cancel = false;
+ }
+
+ mutex_lock(&ts->fwupdate_lock);
+
+ ret = __axiom_axfw_fw_prepare(ts, afw, data, size);
+
+ /*
+ * In FW_UPLOAD_ERR_NONE case the complete handler will release the
+ * lock.
+ */
+ if (ret != FW_UPLOAD_ERR_NONE)
+ mutex_unlock(&ts->fwupdate_lock);
+
+ return ret;
+}
+
+static int axiom_enter_bootloader_mode(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ int error;
+
+ error = axiom_u02_wait_idle(ts);
+ if (error)
+ goto err_out;
+
+ error = axiom_u02_enter_bootloader(ts);
+ if (error) {
+ dev_err(dev, "Failed to enter bootloader mode\n");
+ goto err_out;
+ }
+
+ axiom_set_runmode(ts, AXIOM_BLP_MODE);
+ return 0;
+
+err_out:
+ return error;
+}
+
+static int axoim_blp_wait_ready(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ unsigned int reg;
+ int tmp, ret;
+ u8 buf[4];
+
+ reg = AXIOM_U01_BLP_SATUS_REG;
+
+ /* BLP busy poll requires to read 4 bytes! */
+ ret = read_poll_timeout(regmap_raw_read, tmp,
+ tmp || !(buf[2] & AXIOM_U01_BLP_STATUS_BUSY),
+ 10 * USEC_PER_MSEC, 5 * USEC_PER_SEC, false,
+ ts->regmap, reg, &buf, 4);
+ if (ret)
+ dev_err(dev, "Bootloader wait processing packets failed %d\n", ret);
+
+ return ret;
+}
+
+static int axiom_blp_write_chunk(struct axiom_data *ts,
+ const u8 *data, u16 length)
+{
+ unsigned int chunk_size = AXIOM_U01_BLP_FIFO_CHK_SIZE_BYTES;
+ unsigned int reg = AXIOM_U01_BLP_FIFO_REG;
+ struct device *dev = ts->dev;
+ unsigned int pos = 0;
+ int error;
+
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+
+ /*
+ * TODO: Downstream does this chunk transfers. Verify if this is
+ * required if one fw-chunk <= AXIOM_MAX_XFERLEN
+ */
+ while (pos < length) {
+ u16 len;
+
+ len = chunk_size;
+ if ((pos + chunk_size) > length)
+ len = length - pos;
+
+ error = regmap_raw_write(ts->regmap, reg, &data[pos], len);
+ if (error) {
+ dev_err(dev, "Bootloader download AXFW chunk failed %d\n", error);
+ return error;
+ }
+
+ pos += len;
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+ }
+
+ return 0;
+}
+
+static int axiom_blp_reset(struct axiom_data *ts)
+{
+ __le16 reset_cmd = cpu_to_le16(AXIOM_U01_BLP_COMMAND_RESET);
+ unsigned int reg = AXIOM_U01_BLP_COMMAND_REG;
+ struct device *dev = ts->dev;
+ unsigned int attempts = 20;
+ unsigned int mode;
+ int error;
+
+ error = axoim_blp_wait_ready(ts);
+ if (error)
+ return error;
+
+ /*
+ * For some reason this write fail with -ENXIO. Skip checking the return
+ * code (which is also done by the downstream axfw.py tool and poll u31
+ * instead.
+ */
+ regmap_raw_write(ts->regmap, reg, &reset_cmd, sizeof(reset_cmd));
+
+ do {
+ error = regmap_read(ts->regmap, AXIOM_U31_REV1_DEVICE_ID_HIGH_REG,
+ &mode);
+ if (!error)
+ break;
+
+ fsleep(250 * USEC_PER_MSEC);
+ } while (attempts--);
+
+ if (error) {
+ dev_err(dev, "Failed to read MODE after BLP reset: %d\n", error);
+ return error;
+ }
+
+ mode = FIELD_GET(AXIOM_U31_REV1_MODE_MASK, mode);
+ if (mode == AXIOM_U31_REV1_MODE_BLP) {
+ dev_err(dev, "Device still in BLP mode, abort\n");
+ return -EINVAL;
+ }
+
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ return 0;
+}
+
+static void axiom_lock_input_device(struct axiom_data *ts)
+{
+ if (!ts->input)
+ return;
+
+ mutex_lock(&ts->input->mutex);
+}
+
+static void axiom_unlock_input_device(struct axiom_data *ts)
+{
+ if (!ts->input)
+ return;
+
+ mutex_unlock(&ts->input->mutex);
+}
+
+static void axiom_unregister_input_dev(struct axiom_data *ts)
+{
+ if (ts->input)
+ input_unregister_device(ts->input);
+
+ ts->input = NULL;
+}
+
+static enum fw_upload_err axiom_unlock_input_return_hw_error(struct axiom_data *ts)
+{
+ axiom_unlock_input_device(ts);
+ return FW_UPLOAD_ERR_HW_ERROR;
+}
+
+static enum fw_upload_err axiom_axfw_fw_write(struct fw_upload *fw_upload,
+ const u8 *data, u32 offset,
+ u32 size, u32 *written)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
+ struct device *dev = ts->dev;
+ int error;
+
+ /*
+ * According Touchnetix the IRQ pin gets reconfigured in bootloader
+ * mode. Which can cause spurious IRQs. Therefore the IRQ should be
+ * disabled on the host.
+ *
+ * See the cleanup routine for the balanced enable.
+ */
+ if (axiom_get_runmode(ts) != AXIOM_BLP_MODE)
+ axiom_disable_irq(ts);
+
+ /* Done before cancel check due to cleanup based put */
+ error = pm_runtime_resume_and_get(ts->dev);
+ if (error)
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ scoped_guard(mutex, &afw->lock) {
+ if (afw->cancel)
+ return FW_UPLOAD_ERR_CANCELED;
+ }
+
+ axiom_lock_input_device(ts);
+
+ if (ts->input && input_device_enabled(ts->input)) {
+ dev_err(dev, "Input device not idle, abort AXFW update\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ /* Set the pointer to the first fw chunk */
+ data += sizeof(struct axiom_fw_axfw_hdr);
+ size -= sizeof(struct axiom_fw_axfw_hdr);
+ *written += sizeof(struct axiom_fw_axfw_hdr);
+
+ if (axiom_get_runmode(ts) != AXIOM_BLP_MODE) {
+ error = axiom_enter_bootloader_mode(ts);
+ if (error)
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ while (size) {
+ u16 chunk_len, len;
+
+ chunk_len = get_unaligned_be16(&data[6]);
+ len = chunk_len + sizeof(struct axiom_fw_axfw_chunk_hdr);
+
+ /*
+ * The bootlaoder FW can handle the complete chunk incl. the
+ * header.
+ */
+ error = axiom_blp_write_chunk(ts, data, len);
+ if (error) {
+ /*
+ * Tests showed that the bootloader mode must be exited
+ * if an invalid chunk was received by the bootloader fw
+ * since all following attempts to download valid chunks
+ * will fail. Try do so via axiom_blp_reset() but tests
+ * also showed that this may fail too. So inform the
+ * user and hope that the full power-cycle helps. To get
+ * the device back into a working mode where the device
+ * accepts data again.
+ */
+ if (axiom_blp_reset(ts))
+ dev_warn(dev, "Couldn't recover device, device requires power-cycle\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ size -= len;
+ *written += len;
+ data += len;
+ }
+
+ error = axiom_blp_reset(ts);
+ if (error) {
+ dev_err(dev, "BLP reset failed\n");
+ return axiom_unlock_input_return_hw_error(ts);
+ }
+
+ error = axiom_u31_device_discover(ts);
+ /* Unlock before the input device gets unregistered/updated */
+ axiom_unlock_input_device(ts);
+ if (error) {
+ /*
+ * This is critical and we need to avoid that the user-space can
+ * still use the input-dev.
+ */
+ axiom_unregister_input_dev(ts);
+ dev_err(dev, "Device discovery failed after AXFW firmware update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ error = axiom_update_input_dev(ts);
+ if (error) {
+ dev_err(dev, "Input device update failed after AXFW firmware update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ dev_info(dev, "AXFW update successful\n");
+
+ return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err axiom_fw_poll_complete(struct fw_upload *fw_upload)
+{
+ return FW_UPLOAD_ERR_NONE;
+}
+
+static void axiom_axfw_fw_cancel(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_AXFW];
+
+ guard(mutex)(&afw->lock);
+ afw->cancel = true;
+}
+
+static void axiom_axfw_fw_handle_irq(struct axiom_data *ts)
+{
+ struct i2c_client *client = to_i2c_client(ts->dev);
+ struct irq_data *data;
+
+ if (!ts->irq_setup_done)
+ return;
+
+ data = irq_get_irq_data(client->irq);
+ /*
+ * The cleanup is called in both cases: success and failure. In case
+ * of failure the device may still be in bootloader mode. Don't enable
+ * the IRQ in such case to avoid a possible IRQ storm because of the
+ * unknown IRQ pin behavior in bootloader mode.
+ *
+ * Furthermore if the IRQ is enabled at this point this means that the
+ * firmware update was necessary to get the device in a working state
+ * because the probe() aborted due to firmware incompatibilities. Skip
+ * the enable step if the IRQ is not disabled to not mess with the
+ * refcount.
+ */
+ if (irqd_irq_disabled(data) &&
+ axiom_get_runmode(ts) != AXIOM_BLP_MODE)
+ axiom_enable_irq(ts);
+}
+
+static void axiom_axfw_fw_cleanup(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+
+ mutex_unlock(&ts->fwupdate_lock);
+ axiom_axfw_fw_handle_irq(ts);
+ pm_runtime_mark_last_busy(ts->dev);
+ pm_runtime_put_sync_autosuspend(ts->dev);
+}
+
+static const struct fw_upload_ops axiom_axfw_fw_upload_ops = {
+ .prepare = axiom_axfw_fw_prepare,
+ .write = axiom_axfw_fw_write,
+ .poll_complete = axiom_fw_poll_complete,
+ .cancel = axiom_axfw_fw_cancel,
+ .cleanup = axiom_axfw_fw_cleanup,
+};
+
+static int axiom_set_new_crcs(struct axiom_data *ts,
+ const struct axiom_fw_cfg_chunk *cfg)
+{
+ struct axiom_crc *crc = &ts->crc[AXIOM_CRC_NEW];
+ const u32 *u33_data = (const u32 *)cfg->usage_content;
+ unsigned int i;
+
+ /*
+ * Provide some debug output of u33, because the th2cfg chunks caused
+ * a few headaches already.
+ */
+ for (i = 0; i < cfg->usage_length / sizeof(u32); i++)
+ dev_dbg(ts->dev, "u33 th2cfgbin chunk[%u]:%#x\n", i,
+ get_unaligned_le32(&u33_data[i]));
+
+ switch (cfg->usage_rev) {
+ case 2:
+ crc->runtime = get_unaligned_le32(u33_data);
+ crc->nvltlusageconfig = get_unaligned_le32(&u33_data[3]);
+ crc->vltusageconfig = get_unaligned_le32(&u33_data[4]);
+ crc->u22_sequencedata = get_unaligned_le32(&u33_data[5]);
+ crc->u43_hotspots = get_unaligned_le32(&u33_data[6]);
+ crc->u93_profiles = get_unaligned_le32(&u33_data[7]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[8]);
+ return 0;
+ case 3:
+ crc->runtime = get_unaligned_le32(u33_data);
+ crc->nvltlusageconfig = get_unaligned_le32(&u33_data[3]);
+ crc->vltusageconfig = get_unaligned_le32(&u33_data[4]);
+ crc->u22_sequencedata = get_unaligned_le32(&u33_data[5]);
+ crc->u43_hotspots = get_unaligned_le32(&u33_data[6]);
+ crc->u77_dod_data = get_unaligned_le32(&u33_data[7]);
+ crc->u93_profiles = get_unaligned_le32(&u33_data[8]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[9]);
+ return 0;
+ case 6:
+ crc->runtime = get_unaligned_le32(u33_data);
+ crc->nvltlusageconfig = get_unaligned_le32(&u33_data[3]);
+ crc->vltusageconfig = get_unaligned_le32(&u33_data[4]);
+ crc->u93_profiles = get_unaligned_le32(&u33_data[8]);
+ crc->u94_deltascalemap = get_unaligned_le32(&u33_data[9]);
+ return 0;
+ default:
+ dev_err(ts->dev, "The driver doesn't support u33 revision %u\n",
+ cfg->usage_rev);
+ return -EINVAL;
+ }
+}
+
+static unsigned int axiom_cfg_fw_prepare_chunk(struct axiom_fw_cfg_chunk *chunk,
+ const u8 *data)
+{
+ chunk->usage_num = data[0];
+ chunk->usage_rev = data[1];
+ chunk->usage_length = get_unaligned_le16(&data[3]);
+ chunk->usage_content = &data[5];
+
+ return chunk->usage_length + sizeof(struct axiom_fw_cfg_chunk_hdr);
+}
+
+/*
+ * To overcome buggy firmware we need to check if a given usage is used by the
+ * current running firmware. Return true if the usage is unused/not populated
+ * by the firmware since we can't perform the actual check.
+ */
+#define axiom_usage_crc_match(_ts, _usage_num, _cur, _new, _field) \
+ (!_ts->usage_table[_usage_num].populated || (_cur->_field == _new->_field))
+
+static bool axiom_cfg_fw_update_required(struct axiom_data *ts)
+{
+ struct axiom_crc *cur, *new;
+
+ cur = &ts->crc[AXIOM_CRC_CUR];
+ new = &ts->crc[AXIOM_CRC_NEW];
+
+ if (cur->nvltlusageconfig != new->nvltlusageconfig ||
+ !axiom_usage_crc_match(ts, AXIOM_U22, cur, new, u22_sequencedata) ||
+ !axiom_usage_crc_match(ts, AXIOM_U43, cur, new, u43_hotspots) ||
+ !axiom_usage_crc_match(ts, AXIOM_U93, cur, new, u93_profiles) ||
+ !axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap)) {
+ return true;
+ }
+
+ return false;
+}
+
+static enum fw_upload_err __axiom_cfg_fw_prepare(struct axiom_data *ts,
+ struct axiom_firmware *afw,
+ const u8 *data, u32 size)
+{
+ u32 cur_runtime_crc, fw_runtime_crc;
+ struct axiom_fw_cfg_chunk chunk;
+ struct device *dev = ts->dev;
+ u32 signature;
+ int error;
+
+ if (axiom_get_runmode(ts) != AXIOM_TCP_MODE) {
+ dev_err(dev, "Device not in TCP mode, abort TH2CFG update\n");
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ if (size < sizeof(struct axiom_fw_cfg_hdr)) {
+ dev_err(dev, "Invalid TH2CFG file size\n");
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+ }
+
+ signature = get_unaligned_be32(data);
+ if (signature != AXIOM_FW_CFG_SIGNATURE) {
+ dev_err(dev, "Invalid TH2CFG signature\n");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ /* Skip to the first fw chunk */
+ data += sizeof(struct axiom_fw_cfg_hdr);
+ size -= sizeof(struct axiom_fw_cfg_hdr);
+
+ /*
+ * Search for u33 which contains the CRC information and perform only
+ * the runtime-crc check.
+ */
+ while (size) {
+ unsigned int chunk_len;
+
+ chunk_len = axiom_cfg_fw_prepare_chunk(&chunk, data);
+ if (chunk.usage_num == AXIOM_U33)
+ break;
+
+ data += chunk_len;
+ size -= chunk_len;
+ }
+
+ if (size == 0) {
+ dev_err(dev, "Failed to find the u33 entry in TH2CFG\n");
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ error = axiom_set_new_crcs(ts, &chunk);
+ if (error)
+ return FW_UPLOAD_ERR_FW_INVALID;
+
+ /*
+ * Nothing to do if the CRCs are the same. TODO: Must be extended once
+ * the CDU update is added.
+ */
+ if (!axiom_cfg_fw_update_required(ts))
+ return FW_UPLOAD_ERR_DUPLICATE;
+
+ cur_runtime_crc = ts->crc[AXIOM_CRC_CUR].runtime;
+ fw_runtime_crc = ts->crc[AXIOM_CRC_NEW].runtime;
+ if (cur_runtime_crc != fw_runtime_crc) {
+ dev_err(dev, "TH2CFG and device runtime CRC doesn't match: %#x != %#x\n",
+ fw_runtime_crc, cur_runtime_crc);
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+ guard(mutex)(&afw->lock);
+ return afw->cancel ? FW_UPLOAD_ERR_CANCELED : FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err axiom_cfg_fw_prepare(struct fw_upload *fw_upload,
+ const u8 *data, u32 size)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
+ enum fw_upload_err ret;
+
+ scoped_guard(mutex, &afw->lock) {
+ afw->cancel = false;
+ }
+
+ mutex_lock(&ts->fwupdate_lock);
+
+ ret = __axiom_cfg_fw_prepare(ts, afw, data, size);
+
+ /*
+ * In FW_UPLOAD_ERR_NONE case the complete handler will release the
+ * lock.
+ */
+ if (ret != FW_UPLOAD_ERR_NONE)
+ mutex_unlock(&ts->fwupdate_lock);
+
+ return ret;
+}
+
+static int axiom_zero_volatile_mem(struct axiom_data *ts)
+{
+ int error;
+
+ /* Zero out the volatile memory except for the user content in u04 */
+ u8 *buf __free(kfree) = axiom_u04_get(ts);
+ if (IS_ERR(buf))
+ return PTR_ERR(buf);
+
+ error = axiom_u02_fillconfig(ts);
+ if (error)
+ return error;
+
+ error = axiom_u04_set(ts, buf);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static bool axiom_skip_cfg_chunk(struct axiom_data *ts,
+ const struct axiom_fw_cfg_chunk *chunk)
+{
+ u8 usage_num = chunk->usage_num;
+
+ if (!ts->usage_table[usage_num].populated) {
+ dev_warn(ts->dev, "Unknown usage chunk for u%02X\n", usage_num);
+ return true;
+ }
+
+ /* Skip read-only usages */
+ if (ts->usage_table[usage_num].info &&
+ ts->usage_table[usage_num].info->is_ro) {
+ return true;
+ }
+
+ return false;
+}
+
+static int axiom_write_cdu_usage(struct axiom_data *ts,
+ const struct axiom_fw_cfg_chunk *chunk)
+{
+ struct axiom_cdu_usage cdu = { };
+ struct device *dev = ts->dev;
+ unsigned int remaining;
+ unsigned int reg;
+ unsigned int pos;
+ int error;
+
+ pos = 0;
+ remaining = chunk->usage_length;
+ cdu.command = cpu_to_le16(AXIOM_CDU_CMD_STORE);
+ reg = axiom_usage_baseaddr(ts, chunk->usage_num);
+
+ while (remaining) {
+ unsigned int size;
+
+ cdu.parameters[1] = cpu_to_le16(pos);
+
+ size = remaining;
+ if (size > AXIOM_CDU_MAX_DATA_BYTES)
+ size = AXIOM_CDU_MAX_DATA_BYTES;
+
+ memset(cdu.data, 0, sizeof(cdu.data));
+ memcpy(cdu.data, &chunk->usage_content[pos], size);
+
+ error = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
+ if (error) {
+ dev_err(dev, "Failed to write CDU u%02X\n",
+ chunk->usage_num);
+ return error;
+ }
+
+ error = axiom_cdu_wait_idle(ts, chunk->usage_num);
+ if (error) {
+ dev_err(dev, "CDU write wait-idle failed\n");
+ return error;
+ }
+
+ remaining -= size;
+ pos += size;
+ }
+
+ /*
+ * TODO: Check if we really need to send 48 zero bytes of data like
+ * downstream does.
+ */
+ memset(&cdu, 0, sizeof(cdu));
+ cdu.command = cpu_to_le16(AXIOM_CDU_CMD_COMMIT);
+ cdu.parameters[0] = cpu_to_le16(AXIOM_CDU_PARAM0_COMMIT);
+ cdu.parameters[1] = cpu_to_le16(AXIOM_CDU_PARAM1_COMMIT);
+
+ error = regmap_raw_write(ts->regmap, reg, &cdu, sizeof(cdu));
+ if (error) {
+ dev_err(dev, "Failed to commit CDU u%02X to NVM\n",
+ chunk->usage_num);
+ return error;
+ }
+
+ if (!axiom_wait_for_completion_timeout(ts, &ts->nvm_write,
+ msecs_to_jiffies(5 * MSEC_PER_SEC))) {
+ dev_err(ts->dev, "Error CDU u%02X commit timedout\n",
+ chunk->usage_num);
+ return -ETIMEDOUT;
+ }
+
+ error = axiom_cdu_wait_idle(ts, chunk->usage_num);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static int axiom_write_cfg_chunk(struct axiom_data *ts,
+ const struct axiom_fw_cfg_chunk *chunk)
+{
+ unsigned int reg;
+ int error;
+
+ dev_dbg(ts->dev, "Write th2cfg-chunk - u%02X.rev%u length:%u\n",
+ chunk->usage_num, chunk->usage_rev, chunk->usage_length);
+
+ if (ts->usage_table[chunk->usage_num].info &&
+ ts->usage_table[chunk->usage_num].info->is_cdu) {
+ error = axiom_write_cdu_usage(ts, chunk);
+ if (error)
+ return error;
+ } else {
+ reg = axiom_usage_baseaddr(ts, chunk->usage_num);
+ error = regmap_raw_write(ts->regmap, reg, chunk->usage_content,
+ chunk->usage_length);
+ if (error)
+ return error;
+ }
+
+ error = axiom_u02_wait_idle(ts);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static int axiom_verify_volatile_mem(struct axiom_data *ts)
+{
+ int error;
+
+ error = axiom_u02_computecrc(ts);
+ if (error)
+ return error;
+
+ /* Query the new CRCs after they are re-computed */
+ error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
+ if (error)
+ return error;
+
+ return ts->crc[AXIOM_CRC_CUR].vltusageconfig ==
+ ts->crc[AXIOM_CRC_NEW].vltusageconfig ? 0 : -EINVAL;
+}
+
+static int axiom_verify_crcs(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ struct axiom_crc *cur, *new;
+
+ cur = &ts->crc[AXIOM_CRC_CUR];
+ new = &ts->crc[AXIOM_CRC_NEW];
+
+ if (new->vltusageconfig != cur->vltusageconfig) {
+ dev_err(dev, "VLTUSAGECONFIG CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->vltusageconfig, new->vltusageconfig);
+ return -EINVAL;
+ } else if (new->nvltlusageconfig != cur->nvltlusageconfig) {
+ dev_err(dev, "NVLTUSAGECONFIG CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->nvltlusageconfig, new->nvltlusageconfig);
+ return -EINVAL;
+ } else if (!axiom_usage_crc_match(ts, AXIOM_U22, cur, new, u22_sequencedata)) {
+ dev_err(dev, "U22_SEQUENCEDATA CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->u22_sequencedata, new->u22_sequencedata);
+ return -EINVAL;
+ } else if (!axiom_usage_crc_match(ts, AXIOM_U43, cur, new, u43_hotspots)) {
+ dev_err(dev, "U43_HOTSPOTS CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->u43_hotspots, new->u43_hotspots);
+ return -EINVAL;
+ } else if (!axiom_usage_crc_match(ts, AXIOM_U93, cur, new, u93_profiles)) {
+ dev_err(dev, "U93_PROFILES CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->u93_profiles, new->u93_profiles);
+ return -EINVAL;
+ } else if (!axiom_usage_crc_match(ts, AXIOM_U94, cur, new, u94_deltascalemap)) {
+ dev_err(dev, "U94_DELTASCALEMAP CRC32 mismatch (dev:%#x != fw:%#x)\n",
+ cur->u94_deltascalemap, new->u94_deltascalemap);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static enum fw_upload_err axiom_cfg_fw_write(struct fw_upload *fw_upload,
+ const u8 *data, u32 offset,
+ u32 size, u32 *written)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
+ struct device *dev = ts->dev;
+ int error;
+
+ /* Done before cancel check due to cleanup based put */
+ error = pm_runtime_resume_and_get(ts->dev);
+ if (error)
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ scoped_guard(mutex, &afw->lock) {
+ if (afw->cancel)
+ return FW_UPLOAD_ERR_CANCELED;
+ }
+
+ axiom_lock_input_device(ts);
+
+ if (ts->input && input_device_enabled(ts->input)) {
+ dev_err(dev, "Input device not idle, abort TH2CFG update\n");
+ axiom_unlock_input_device(ts);
+ return FW_UPLOAD_ERR_HW_ERROR;
+ }
+
+ error = axiom_u02_stop(ts);
+ if (error)
+ goto err_swreset;
+
+ error = axiom_zero_volatile_mem(ts);
+ if (error)
+ goto err_swreset;
+
+ /* Skip to the first fw chunk */
+ data += sizeof(struct axiom_fw_cfg_hdr);
+ size -= sizeof(struct axiom_fw_cfg_hdr);
+ *written += sizeof(struct axiom_fw_cfg_hdr);
+
+ axiom_set_runmode(ts, AXIOM_TCP_CFG_UPDATE_MODE);
+
+ while (size) {
+ struct axiom_fw_cfg_chunk chunk;
+ unsigned int chunk_len;
+
+ chunk_len = axiom_cfg_fw_prepare_chunk(&chunk, data);
+ if (axiom_skip_cfg_chunk(ts, &chunk)) {
+ dev_dbg(dev, "Skip TH2CFG usage u%02X\n", chunk.usage_num);
+ } else {
+ error = axiom_write_cfg_chunk(ts, &chunk);
+ if (error) {
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+ goto err_swreset;
+ }
+ }
+
+ data += chunk_len;
+ size -= chunk_len;
+ *written += chunk_len;
+ }
+
+ axiom_set_runmode(ts, AXIOM_TCP_MODE);
+
+ /* Ensure that the chunks are written correctly */
+ error = axiom_verify_volatile_mem(ts);
+ if (error) {
+ dev_err(dev, "Failed to verify written config, abort\n");
+ goto err_swreset;
+ }
+
+ error = axiom_u02_save_config(ts);
+ if (error)
+ goto err_swreset;
+
+ /*
+ * (Re)start the device with the new config. Start the device AE
+ * either via u02 SW_RESET or u02 CMD_START, the behavior is the same
+ * according Touchnetix.
+ */
+ error = axiom_u02_swreset(ts);
+ if (error) {
+ dev_err(dev, "Soft reset failed\n");
+ goto err_unlock;
+ }
+
+ error = axiom_u33_read(ts, &ts->crc[AXIOM_CRC_CUR]);
+ if (error)
+ goto err_unlock;
+
+ if (axiom_verify_crcs(ts))
+ goto err_unlock;
+
+ /* Unlock before the input device gets unregistered */
+ axiom_unlock_input_device(ts);
+
+ error = axiom_update_input_dev(ts);
+ if (error) {
+ dev_err(dev, "Input device update failed after TH2CFG firmware update\n");
+ goto err_out;
+ }
+
+ dev_info(dev, "TH2CFG update successful\n");
+
+ return FW_UPLOAD_ERR_NONE;
+
+err_swreset:
+ axiom_u02_swreset(ts);
+err_unlock:
+ axiom_unlock_input_device(ts);
+err_out:
+ return error == -ETIMEDOUT ? FW_UPLOAD_ERR_TIMEOUT : FW_UPLOAD_ERR_HW_ERROR;
+}
+
+static void axiom_cfg_fw_cancel(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+ struct axiom_firmware *afw = &ts->fw[AXIOM_FW_CFG];
+
+ guard(mutex)(&afw->lock);
+ afw->cancel = true;
+}
+
+static void axiom_cfg_fw_cleanup(struct fw_upload *fw_upload)
+{
+ struct axiom_data *ts = fw_upload->dd_handle;
+
+ mutex_unlock(&ts->fwupdate_lock);
+ pm_runtime_mark_last_busy(ts->dev);
+ pm_runtime_put_sync_autosuspend(ts->dev);
+}
+
+static const struct fw_upload_ops axiom_cfg_fw_upload_ops = {
+ .prepare = axiom_cfg_fw_prepare,
+ .write = axiom_cfg_fw_write,
+ .poll_complete = axiom_fw_poll_complete,
+ .cancel = axiom_cfg_fw_cancel,
+ .cleanup = axiom_cfg_fw_cleanup,
+};
+
+static void axiom_remove_axfw_fwl_action(void *data)
+{
+ struct axiom_data *ts = data;
+
+ firmware_upload_unregister(ts->fw[AXIOM_FW_AXFW].fwl);
+}
+
+static void axiom_remove_cfg_fwl_action(void *data)
+{
+ struct axiom_data *ts = data;
+
+ firmware_upload_unregister(ts->fw[AXIOM_FW_CFG].fwl);
+}
+
+static int axiom_register_fwl(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ struct fw_upload *fwl;
+ char *fw_name;
+ int error;
+
+ if (!IS_ENABLED(CONFIG_FW_UPLOAD)) {
+ dev_dbg(dev, "axfw and th2cfgbin update disabled\n");
+ return 0;
+ }
+
+ mutex_init(&ts->fw[AXIOM_FW_AXFW].lock);
+ fw_name = kasprintf(GFP_KERNEL, "i2c:%s.axfw", dev_name(dev));
+ fwl = firmware_upload_register(THIS_MODULE, ts->dev, fw_name,
+ &axiom_axfw_fw_upload_ops, ts);
+ kfree(fw_name);
+ if (IS_ERR(fwl))
+ return dev_err_probe(dev, PTR_ERR(fwl),
+ "Failed to register firmware upload\n");
+
+ error = devm_add_action_or_reset(dev, axiom_remove_axfw_fwl_action, ts);
+ if (error)
+ return error;
+
+ ts->fw[AXIOM_FW_AXFW].fwl = fwl;
+
+ mutex_init(&ts->fw[AXIOM_FW_CFG].lock);
+ fw_name = kasprintf(GFP_KERNEL, "i2c:%s.th2cfgbin", dev_name(dev));
+ fwl = firmware_upload_register(THIS_MODULE, ts->dev, fw_name,
+ &axiom_cfg_fw_upload_ops, ts);
+ kfree(fw_name);
+ if (IS_ERR(fwl))
+ return dev_err_probe(dev, PTR_ERR(fwl),
+ "Failed to register cfg firmware upload\n");
+
+ error = devm_add_action_or_reset(dev, axiom_remove_cfg_fwl_action, ts);
+ if (error)
+ return error;
+
+ ts->fw[AXIOM_FW_CFG].fwl = fwl;
+
+ return 0;
+}
+
+/************************* Device handlig *************************************/
+
+#define AXIOM_SIMPLE_FW_DEVICE_ATTR(attr) \
+ static ssize_t \
+ fw_ ## attr ## _show(struct device *dev, \
+ struct device_attribute *_attr, char *buf) \
+ { \
+ struct i2c_client *i2c = to_i2c_client(dev); \
+ struct axiom_data *ts = i2c_get_clientdata(i2c); \
+ \
+ return sysfs_emit(buf, "%u\n", ts->fw_##attr); \
+ } \
+ static DEVICE_ATTR_RO(fw_##attr)
+
+AXIOM_SIMPLE_FW_DEVICE_ATTR(major);
+AXIOM_SIMPLE_FW_DEVICE_ATTR(minor);
+AXIOM_SIMPLE_FW_DEVICE_ATTR(rc);
+
+static ssize_t fw_status_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+ const char *val;
+
+ if (ts->fw_status)
+ val = "production";
+ else
+ val = "engineering";
+
+ return sysfs_emit(buf, "%s\n", val);
+}
+static DEVICE_ATTR_RO(fw_status);
+
+static ssize_t fw_variant_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+ const char *val;
+
+ switch (ts->fw_variant) {
+ case 0:
+ val = "3d";
+ break;
+ case 1:
+ val = "2d";
+ break;
+ case 2:
+ val = "force";
+ break;
+ case 3:
+ val = "0d";
+ break;
+ case 4:
+ val = "xl";
+ break;
+ default:
+ val = "unknown";
+ break;
+ }
+
+ return sysfs_emit(buf, "%s\n", val);
+}
+static DEVICE_ATTR_RO(fw_variant);
+
+static ssize_t device_id_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+
+ return sysfs_emit(buf, "%u\n", ts->device_id);
+}
+static DEVICE_ATTR_RO(device_id);
+
+static ssize_t device_state_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+ struct axiom_data *ts = i2c_get_clientdata(i2c);
+
+ return sysfs_emit(buf, "%s\n", axiom_runmode_to_string(ts));
+}
+static DEVICE_ATTR_RO(device_state);
+
+static struct attribute *axiom_attrs[] = {
+ &dev_attr_fw_major.attr,
+ &dev_attr_fw_minor.attr,
+ &dev_attr_fw_rc.attr,
+ &dev_attr_fw_status.attr,
+ &dev_attr_fw_variant.attr,
+ &dev_attr_device_id.attr,
+ &dev_attr_device_state.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(axiom);
+
+static void axiom_poll(struct input_dev *input)
+{
+ struct axiom_data *ts = input_get_drvdata(input);
+
+ axiom_process_report(ts, AXIOM_U34, NULL, 0);
+}
+
+static irqreturn_t axiom_irq(int irq, void *dev_id)
+{
+ struct axiom_data *ts = dev_id;
+
+ axiom_process_report(ts, AXIOM_U34, NULL, 0);
+
+ return IRQ_HANDLED;
+}
+
+static int axiom_input_open(struct input_dev *dev)
+{
+ struct axiom_data *ts = input_get_drvdata(dev);
+
+ return pm_runtime_resume_and_get(ts->dev);
+}
+
+static void axiom_input_close(struct input_dev *dev)
+{
+ struct axiom_data *ts = input_get_drvdata(dev);
+
+ pm_runtime_mark_last_busy(ts->dev);
+ pm_runtime_put_sync_autosuspend(ts->dev);
+}
+
+static int axiom_register_input_dev(struct axiom_data *ts,
+ bool update_in_process)
+{
+ struct device *dev = ts->dev;
+ struct i2c_client *client = to_i2c_client(dev);
+ struct input_dev *input;
+ int error;
+
+ input = input_allocate_device();
+ if (!input) {
+ dev_err(dev, "Failed to allocate input driver data\n");
+ return -ENOMEM;
+ }
+
+ input->dev.parent = dev;
+ input->name = "TouchNetix aXiom Touchscreen";
+ input->id.bustype = BUS_I2C;
+ input->id.vendor = ts->jedec_id;
+ input->id.product = ts->device_id;
+ input->id.version = ts->silicon_rev;
+
+ /* Either follow the panel or the open user count, not both */
+ if (!ts->is_panel_follower) {
+ input->open = axiom_input_open;
+ input->close = axiom_input_close;
+ }
+
+ axiom_u64_cds_enabled(ts);
+ input_set_abs_params(input, ABS_MT_POSITION_X, 0, AXIOM_MAX_XY, 0, 0);
+ input_set_abs_params(input, ABS_MT_POSITION_Y, 0, AXIOM_MAX_XY, 0, 0);
+ input_set_abs_params(input, ABS_MT_DISTANCE, 0, 127, 0, 0);
+ if (ts->cds_enabled)
+ input_set_abs_params(input, ABS_MT_PRESSURE, 0, 127, 0, 0);
+
+ touchscreen_parse_properties(input, true, &ts->prop);
+
+ axiom_get_touchslots(ts);
+ if (!ts->num_slots && update_in_process) {
+ input_free_device(input);
+ /*
+ * Skip input device registration but don't throw an error to
+ * not abort the update since some FW updates require a
+ * following CFG update to re-initialize the touchslot handling.
+ */
+ if (update_in_process) {
+ dev_info(dev, "No touchslots found after FW or CFG update, skip registering input device\n");
+ return 0;
+ }
+
+ dev_err(dev, "Error firmware has no touchslots enabled\n");
+ return -EINVAL;
+ }
+
+ error = input_mt_init_slots(input, ts->num_slots, INPUT_MT_DIRECT);
+ if (error) {
+ input_free_device(input);
+ dev_err(dev, "Failed to init mt slots\n");
+ return error;
+ }
+
+ /*
+ * Ensure that the IRQ setup is done only once since the handler belong
+ * to the i2c-dev whereas the input-poller belong to the input-dev. The
+ * input-dev can get unregistered during a firmware update to reflect
+ * the new firmware state. Therefore the input-poller setup must be done
+ * always.
+ */
+ if (client->irq) {
+ if (!ts->irq_setup_done) {
+ error = devm_request_threaded_irq(dev, client->irq,
+ NULL, axiom_irq,
+ IRQF_ONESHOT,
+ dev_name(dev), ts);
+ if (error) {
+ dev_err(dev, "Failed to request IRQ\n");
+ return error;
+ }
+ ts->irq_setup_done = true;
+ }
+ } else {
+ error = input_setup_polling(input, axiom_poll);
+ if (error) {
+ input_free_device(input);
+ dev_err(dev, "Setup polling mode failed\n");
+ return error;
+ }
+
+ input_set_poll_interval(input, ts->poll_interval);
+ }
+
+ input_set_drvdata(input, ts);
+ ts->input = input;
+
+ error = input_register_device(input);
+ if (error) {
+ input_free_device(input);
+ ts->input = NULL;
+ dev_err(dev, "Failed to register input device\n");
+ };
+
+ return error;
+}
+
+static int axiom_update_input_dev(struct axiom_data *ts)
+{
+ axiom_unregister_input_dev(ts);
+
+ return axiom_register_input_dev(ts, true);
+}
+
+static int axiom_parse_firmware(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ struct gpio_desc *gpio;
+ int error;
+
+ ts->supplies[0].supply = "vddi";
+ ts->supplies[1].supply = "vdda";
+ ts->num_supplies = ARRAY_SIZE(ts->supplies);
+
+ error = devm_regulator_bulk_get(dev, ts->num_supplies, ts->supplies);
+ if (error)
+ return dev_err_probe(dev, error,
+ "Failed to get power supplies\n");
+
+ gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(gpio))
+ return dev_err_probe(dev, PTR_ERR(gpio),
+ "Failed to get reset GPIO\n");
+ ts->reset_gpio = gpio;
+
+ ts->poll_interval = AXIOM_DEFAULT_POLL_INTERVAL_MS;
+ device_property_read_u32(dev, "poll-interval", &ts->poll_interval);
+
+ return 0;
+}
+
+static int axiom_power_up_device(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+ int error;
+
+ error = regulator_bulk_enable(ts->num_supplies, ts->supplies);
+ if (error) {
+ dev_err(dev, "Failed to enable power supplies\n");
+ return error;
+ }
+
+ gpiod_set_value_cansleep(ts->reset_gpio, 1);
+ fsleep(2000);
+ gpiod_set_value_cansleep(ts->reset_gpio, 0);
+
+ fsleep(AXIOM_STARTUP_TIME_MS);
+
+ return 0;
+}
+
+static void axiom_power_down_device(struct axiom_data *ts)
+{
+ regulator_bulk_disable(ts->num_supplies, ts->supplies);
+}
+
+static int axiom_panel_prepared(struct drm_panel_follower *follower)
+{
+ struct axiom_data *ts = container_of(follower, struct axiom_data,
+ panel_follower);
+
+ return pm_runtime_resume_and_get(ts->dev);
+}
+
+static int axiom_panel_unpreparing(struct drm_panel_follower *follower)
+{
+ struct axiom_data *ts = container_of(follower, struct axiom_data,
+ panel_follower);
+
+ return pm_runtime_put_sync_suspend(ts->dev);
+}
+
+static const struct drm_panel_follower_funcs axiom_panel_follower_funcs = {
+ .panel_prepared = axiom_panel_prepared,
+ .panel_unpreparing = axiom_panel_unpreparing,
+};
+
+static int axiom_register_panel_follower(struct axiom_data *ts)
+{
+ struct device *dev = ts->dev;
+
+ if (!drm_is_panel_follower(dev))
+ return 0;
+
+ if (device_can_wakeup(dev)) {
+ dev_warn(dev, "Can't follow panel if marked as wakup device\n");
+ return 0;
+ }
+
+ ts->panel_follower.funcs = &axiom_panel_follower_funcs;
+ ts->is_panel_follower = true;
+
+ return devm_drm_panel_add_follower(dev, &ts->panel_follower);
+}
+
+static int axiom_i2c_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct axiom_data *ts;
+ int error;
+
+ ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
+ if (!ts)
+ return dev_err_probe(dev, -ENOMEM,
+ "Failed to allocate driver data\n");
+
+ ts->regmap = devm_regmap_init_i2c(client, &axiom_i2c_regmap_config);
+ if (IS_ERR(ts->regmap))
+ return dev_err_probe(dev, PTR_ERR(ts->regmap),
+ "Failed to initialize regmap\n");
+
+ i2c_set_clientdata(client, ts);
+ ts->dev = dev;
+
+ init_completion(&ts->boot_complete.completion);
+ init_completion(&ts->nvm_write.completion);
+ mutex_init(&ts->fwupdate_lock);
+
+ error = axiom_register_fwl(ts);
+ if (error)
+ return error;
+
+ error = axiom_parse_firmware(ts);
+ if (error)
+ return error;
+
+ error = axiom_power_up_device(ts);
+ if (error)
+ return dev_err_probe(dev, error, "Failed to power-on device\n");
+
+ pm_runtime_set_autosuspend_delay(dev, 10 * MSEC_PER_SEC);
+ pm_runtime_use_autosuspend(dev);
+ pm_runtime_set_active(dev);
+ pm_runtime_get_noresume(dev);
+ error = devm_pm_runtime_enable(dev);
+ if (error)
+ return dev_err_probe(dev, error, "Failed to enable pm-runtime\n");
+
+ error = axiom_register_panel_follower(ts);
+ if (error)
+ return dev_err_probe(dev, error, "Failed to register panel follower\n");
+
+ error = axiom_u31_device_discover(ts);
+ /*
+ * Register the device to allow FW updates in case that the current FW
+ * doesn't support the required driver usages or if the device is in
+ * bootloader mode.
+ *
+ * The U02 usage has to be supported, since this is the usage which is
+ * used to put the device into bootloader mode!
+ *
+ * TODO: Add support for the emergency bootloader mode enter sequence.
+ * The emergency enter is done by toggling the nRESET pin 5-times in a
+ * row.
+ */
+ if (error) {
+ if (IS_ENABLED(CONFIG_FW_UPLOAD) &&
+ axiom_driver_supports_usage(ts, AXIOM_U02) &&
+ (axiom_get_runmode(ts) == AXIOM_DISCOVERY_MODE ||
+ axiom_get_runmode(ts) == AXIOM_BLP_MODE)) {
+ dev_warn(dev, "Device discovery failed, wait for user fw update\n");
+ pm_runtime_mark_last_busy(dev);
+ pm_runtime_put_sync_autosuspend(dev);
+ return 0;
+ }
+ pm_runtime_put_sync(dev);
+ return dev_err_probe(dev, error, "Device discovery failed\n");
+ }
+
+ error = axiom_register_input_dev(ts, false);
+ pm_runtime_mark_last_busy(dev);
+ pm_runtime_put_sync_autosuspend(dev);
+ if (error) {
+ if (!IS_ENABLED(CONFIG_FW_UPLOAD))
+ return dev_err_probe(dev, error, "Failed to register input device\n");
+
+ dev_warn(dev, "Failed to register the input device, wait for user fw update\n");
+ }
+
+ return 0;
+}
+
+static void axiom_i2c_remove(struct i2c_client *client)
+{
+ struct axiom_data *ts = i2c_get_clientdata(client);
+
+ axiom_unregister_input_dev(ts);
+}
+
+static int axiom_runtime_suspend(struct device *dev)
+{
+ struct axiom_data *ts = dev_get_drvdata(dev);
+
+ axiom_disable_irq(ts);
+ axiom_power_down_device(ts);
+
+ return 0;
+}
+
+static int axiom_runtime_resume(struct device *dev)
+{
+ struct axiom_data *ts = dev_get_drvdata(dev);
+ int error;
+
+ error = axiom_power_up_device(ts);
+ if (error)
+ return error;
+
+ axiom_enable_irq(ts);
+
+ return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(axiom_pm_ops, axiom_runtime_suspend,
+ axiom_runtime_resume, NULL);
+
+static const struct i2c_device_id axiom_i2c_id_table[] = {
+ { "ax54a" },
+ { },
+};
+MODULE_DEVICE_TABLE(i2c, axiom_i2c_id_table);
+
+static const struct of_device_id axiom_of_match[] = {
+ { .compatible = "touchnetix,ax54a", },
+ { }
+};
+MODULE_DEVICE_TABLE(of, axiom_of_match);
+
+static struct i2c_driver axiom_i2c_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .dev_groups = axiom_groups,
+ .pm = pm_ptr(&axiom_pm_ops),
+ .of_match_table = axiom_of_match,
+ },
+ .id_table = axiom_i2c_id_table,
+ .probe = axiom_i2c_probe,
+ .remove = axiom_i2c_remove,
+};
+module_i2c_driver(axiom_i2c_driver);
+
+MODULE_DESCRIPTION("TouchNetix aXiom touchscreen I2C bus driver");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related
* [PATCH v6 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Marco Felsch @ 2026-07-02 22:29 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de>
Add FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the firmware_loader
framework that the update is not required. This can be the case if the
user provided firmware matches the current running firmware.
Sync lib/test_firmware.c accordingly.
Reviewed-by: Russ Weight <russ.weight@linux.dev>
Reviewed-by: Luis Chamberlain <mcgrof@kernel.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
drivers/base/firmware_loader/sysfs_upload.c | 1 +
include/linux/firmware.h | 2 ++
lib/test_firmware.c | 1 +
3 files changed, 4 insertions(+)
diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
index efc33294212fd82bb1a8c426d7430ef96c6620b2..82c4b1fedf4cdd0b41fd9acb0cfc1688d726a6fd 100644
--- a/drivers/base/firmware_loader/sysfs_upload.c
+++ b/drivers/base/firmware_loader/sysfs_upload.c
@@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
[FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
[FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
[FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
+ [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
};
static const char *fw_upload_progress(struct device *dev,
diff --git a/include/linux/firmware.h b/include/linux/firmware.h
index 0fa3b027f02f16ffc4a28d4209c3af9319dd8bea..86a2f010e0c1e833d8456cadf21743b9f86dbde5 100644
--- a/include/linux/firmware.h
+++ b/include/linux/firmware.h
@@ -29,6 +29,7 @@ struct firmware {
* @FW_UPLOAD_ERR_RW_ERROR: read or write to HW failed, see kernel log
* @FW_UPLOAD_ERR_WEAROUT: FLASH device is approaching wear-out, wait & retry
* @FW_UPLOAD_ERR_FW_INVALID: invalid firmware file
+ * @FW_UPLOAD_ERR_DUPLICATE: firmware is already up to date (duplicate)
* @FW_UPLOAD_ERR_MAX: Maximum error code marker
*/
enum fw_upload_err {
@@ -41,6 +42,7 @@ enum fw_upload_err {
FW_UPLOAD_ERR_RW_ERROR,
FW_UPLOAD_ERR_WEAROUT,
FW_UPLOAD_ERR_FW_INVALID,
+ FW_UPLOAD_ERR_DUPLICATE,
FW_UPLOAD_ERR_MAX
};
diff --git a/lib/test_firmware.c b/lib/test_firmware.c
index 7459bba65444d1d153667ab3fc447c375418e938..a822d9641eb4c4db01d9966f76f080ede31027f3 100644
--- a/lib/test_firmware.c
+++ b/lib/test_firmware.c
@@ -1134,6 +1134,7 @@ static const char * const fw_upload_err_str[] = {
[FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
[FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
[FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
+ [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
};
static void upload_err_inject_error(struct test_firmware_upload *tst,
--
2.47.3
^ permalink raw reply related
* Re: [PATCH v6 0/4] Input: Add support for TouchNetix aXiom touchscreen
From: Marco Felsch @ 2026-07-02 22:31 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, Mamta Shukla,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v6-0-1aa50ba3bc5a@pengutronix.de>
Hi,
Please ignore this patch series, for some reasons b4 made it v6 albeit
this should be v7. I will sind a new series with the correct version!
Regards,
Marco
On 26-07-03, Marco Felsch wrote:
> Hi,
>
> this adds the support for the TouchNetix aXiom touchcontroller family.
>
> The following features are added:
> - I2C communication
> - Input event handling
> - Touchcontroller firmware (AXFW) updates
> - Touchcontroller config (TH2CFGBIN) updates
> - Poll or IRQ support
>
> Many thanks for Dmitry's input on my v5. I included all changes
> requested from him.
>
> Regards,
> Marco
>
> Changes in v7:
> - Link to v6: https://lore.kernel.org/all/20260303-v6-10-topic-touchscreen-axiom-v6-0-8ac755add12b@pengutronix.de/
> - Rebased on top of 7.2-rc1
> - Fix U33 rev.6 usage (Mamta)
> - Fix bootloader mode handling, required to disable the IRQ (Andrew)
> - Fix bug in usage table population which led to a nullptr bug (Andrew)
>
> Changes in v6:
> - Link to v5: https://lore.kernel.org/r/20260111-v6-10-topic-touchscreen-axiom-v5-0-f94e0ae266cb@pengutronix.de
> - Fix update POLL mode
> - Fix max. input values
> - Resolve some "TODO" and "Downstream" comments (Andrew)
> - Add 0d, xl fw_variant (Andrew)
> - Use 2sec timeout for u02 swreset (Andrew)
> - Fix IRQ-runmode input-device registration after FW update
> - Drop legacy ALC firmware support, since no fw sanity check could be performed
> which is bad for user experience if they provided the wrong file accidentally.
> - drop BLP_PRE_MODE
> - make AXIOM_U31 rev.1 required
> - axiom_usage_supported: drop iter and instead use new usage_table_by_baseaddr
> - axiom_axfw_fw_write: drop goto error handling
> - adapt comments
> - rework axiom_i2c_probe (Dmitry)
> - rework axiom_power_*_device and axiom_parse_firmware (Dmitry)
> - rework axiom_register_input_dev (Dmitry)
> - rework axiom_register_fwl (Dmitry)
> - rework axiom_cfg_fw_cancel (Dmitry)
> - rework axiom_cfg_fw_write (Dmitry)
> - rework axiom_verify_volatile_mem (Dmitry)
> - rework axiom_write_cfg_chunk (Dmitry)
> - rework axiom_cfg_fw_prepare (Dmitry)
> - rework axiom_axfw_fw_* functions (Dmitry)
> - rework axiom_blp_reset (Dmitry)
> - rework axiom_blp_write_chunk (Dmitry)
> - rework axiom_enter_bootloader_mode (Dmitry)
> - rework axiom_axfw_fw_prepare (Dmitry)
> - rework regmap_read/write (Dmitry)
> - rework axiom_u34_rev1_process_report (Dmitry)
> - rework axiom_cdu_wait_idle (Dmitry)
> - rework axiom_u64_cds_enabled (Dmitry)
> - rework axiom_u42_get_touchslots (Dmitry)
> - rework axiom_u33_read (s/ret/error/) (Dmitry)
> - rework axiom_u31_device_discover (Dmitry)
> - simple s/ret/error/ (Dmitry)
> - rework u04 handling (Dmitry)
> - rework u02 handling (Dmitry)
> - align function name accordingly (Dmitry)
> - fix indentation (Dmitry)
>
> Changes in v5:
> - Link to v4: https://lore.kernel.org/r/20260106-v6-10-topic-touchscreen-axiom-v4-0-9e9b69c84926@pengutronix.de
> - fix sysfs documentation description indentation and date
>
> Changes in v4:
> - Link to v3: https://lore.kernel.org/r/20250821-v6-10-topic-touchscreen-axiom-v3-0-940ccee6dba3@pengutronix.de
> - rebased on top of v6.19-rc1
> - collect r-b tags
>
> Changes in v3:
> - Link to v2: https://lore.kernel.org/r/20250529-v6-10-topic-touchscreen-axiom-v2-0-a5edb105a600@pengutronix.de
> - firmware: fix commit message (Russ)
> - dt-bindings: Add ack from Krzysztof
> - dt-bindings: make use of GPIO_ACTIVE_LOW (Krzysztof)
> - dt-bindings: drop 'panel: true' property (Krzysztof)
> - driver: make use of sysfs_emit (Greg)
> - driver: s/WARN()/dev_warn()/ to not take down the system (Greg)
> - driver: fix build dependency error by adding "depends on DRM || !DRM"
> - driver: harmonize usage printing to u%02X
>
> Changes in v2:
> - Link to v1: https://lore.kernel.org/r/20241119-v6-10-topic-touchscreen-axiom-v1-0-6124925b9718@pengutronix.de
> - Rework the firmware-duplicate handling -> expose the error to the
> userspace
> - Drop Krzysztof Kozlowski ACK and RB
> - Add panel-follower support
> - Add sysfs-driver-input-touchnetix-axiom documentation
> - Add support for new firmware 4.8.9
> - Add support to handle 2D and 3D firmware
>
> ---
> Kamel Bouhara (2):
> dt-bindings: vendor-prefixes: Add TouchNetix AS
> dt-bindings: input: Add TouchNetix axiom touchscreen
>
> Marco Felsch (2):
> firmware_loader: expand firmware error codes with up-to-date error
> Input: Add TouchNetix aXiom I2C Touchscreen support
>
> .../testing/sysfs-driver-input-touchnetix-axiom | 80 +
> .../input/touchscreen/touchnetix,ax54a.yaml | 62 +
> .../devicetree/bindings/vendor-prefixes.yaml | 2 +
> drivers/base/firmware_loader/sysfs_upload.c | 1 +
> drivers/input/touchscreen/Kconfig | 17 +
> drivers/input/touchscreen/Makefile | 1 +
> drivers/input/touchscreen/touchnetix_axiom.c | 3141 ++++++++++++++++++++
> include/linux/firmware.h | 2 +
> lib/test_firmware.c | 1 +
> 9 files changed, 3307 insertions(+)
> ---
> base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
> change-id: 20240704-v6-10-topic-touchscreen-axiom-105761e81011
>
> Best regards,
> --
> Marco Felsch <m.felsch@pengutronix.de>
>
>
--
#gernperDu
#CallMeByMyFirstName
Pengutronix e.K. | |
Steuerwalder Str. 21 | https://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-9 |
^ permalink raw reply
* [PATCH v7 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
Add FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the firmware_loader
framework that the update is not required. This can be the case if the
user provided firmware matches the current running firmware.
Sync lib/test_firmware.c accordingly.
Reviewed-by: Russ Weight <russ.weight@linux.dev>
Reviewed-by: Luis Chamberlain <mcgrof@kernel.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
drivers/base/firmware_loader/sysfs_upload.c | 1 +
include/linux/firmware.h | 2 ++
lib/test_firmware.c | 1 +
3 files changed, 4 insertions(+)
diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
index efc33294212fd82bb1a8c426d7430ef96c6620b2..82c4b1fedf4cdd0b41fd9acb0cfc1688d726a6fd 100644
--- a/drivers/base/firmware_loader/sysfs_upload.c
+++ b/drivers/base/firmware_loader/sysfs_upload.c
@@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
[FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
[FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
[FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
+ [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
};
static const char *fw_upload_progress(struct device *dev,
diff --git a/include/linux/firmware.h b/include/linux/firmware.h
index 0fa3b027f02f16ffc4a28d4209c3af9319dd8bea..86a2f010e0c1e833d8456cadf21743b9f86dbde5 100644
--- a/include/linux/firmware.h
+++ b/include/linux/firmware.h
@@ -29,6 +29,7 @@ struct firmware {
* @FW_UPLOAD_ERR_RW_ERROR: read or write to HW failed, see kernel log
* @FW_UPLOAD_ERR_WEAROUT: FLASH device is approaching wear-out, wait & retry
* @FW_UPLOAD_ERR_FW_INVALID: invalid firmware file
+ * @FW_UPLOAD_ERR_DUPLICATE: firmware is already up to date (duplicate)
* @FW_UPLOAD_ERR_MAX: Maximum error code marker
*/
enum fw_upload_err {
@@ -41,6 +42,7 @@ enum fw_upload_err {
FW_UPLOAD_ERR_RW_ERROR,
FW_UPLOAD_ERR_WEAROUT,
FW_UPLOAD_ERR_FW_INVALID,
+ FW_UPLOAD_ERR_DUPLICATE,
FW_UPLOAD_ERR_MAX
};
diff --git a/lib/test_firmware.c b/lib/test_firmware.c
index 7459bba65444d1d153667ab3fc447c375418e938..a822d9641eb4c4db01d9966f76f080ede31027f3 100644
--- a/lib/test_firmware.c
+++ b/lib/test_firmware.c
@@ -1134,6 +1134,7 @@ static const char * const fw_upload_err_str[] = {
[FW_UPLOAD_ERR_RW_ERROR] = "read-write-error",
[FW_UPLOAD_ERR_WEAROUT] = "flash-wearout",
[FW_UPLOAD_ERR_FW_INVALID] = "firmware-invalid",
+ [FW_UPLOAD_ERR_DUPLICATE] = "firmware-duplicate",
};
static void upload_err_inject_error(struct test_firmware_upload *tst,
--
2.47.3
^ permalink raw reply related
* [PATCH v7 2/4] dt-bindings: vendor-prefixes: Add TouchNetix AS
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add vendor prefix for TouchNetix AS (https://www.touchnetix.com/products/).
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Acked-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e7cf0ca1436713ae44f1950259f006..b8450050d7299cea2b5cf6f57fe71f5ead1140c8 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1715,6 +1715,8 @@ patternProperties:
description: Toradex AG
"^toshiba,.*":
description: Toshiba Corporation
+ "^touchnetix,.*":
+ description: TouchNetix AS
"^toumaz,.*":
description: Toumaz
"^tpk,.*":
--
2.47.3
^ permalink raw reply related
* [PATCH v7 3/4] dt-bindings: input: Add TouchNetix axiom touchscreen
From: Marco Felsch @ 2026-07-02 22:32 UTC (permalink / raw)
To: andrew.thomas, Luis Chamberlain, Russ Weight, Greg Kroah-Hartman,
Rafael J. Wysocki, Andrew Morton, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov, Kamel Bouhara,
Marco Felsch, Henrik Rydberg, Danilo Krummrich, Danilo Krummrich
Cc: linux-kernel, devicetree, linux-input, kernel, Marco Felsch,
Krzysztof Kozlowski
In-Reply-To: <20260703-v6-10-topic-touchscreen-axiom-v7-0-0d2a550a7ee8@pengutronix.de>
From: Kamel Bouhara <kamel.bouhara@bootlin.com>
Add the TouchNetix axiom I2C touchscreen device tree bindings
documentation.
Signed-off-by: Kamel Bouhara <kamel.bouhara@bootlin.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
---
.../input/touchscreen/touchnetix,ax54a.yaml | 62 ++++++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d388c41a75dd4d6d6d0e6de0eaef4d493d439a90
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/touchnetix,ax54a.yaml
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/touchnetix,ax54a.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: TouchNetix Axiom series touchscreen controller
+
+maintainers:
+ - Marco Felsch <kernel@pengutronix.de>
+
+allOf:
+ - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+ - $ref: /schemas/input/input.yaml#
+
+properties:
+ compatible:
+ const: touchnetix,ax54a
+
+ reg:
+ enum: [ 0x66, 0x67 ]
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ vdda-supply:
+ description: Analog power supply regulator on VDDA pin
+
+ vddi-supply:
+ description: I/O power supply regulator on VDDI pin
+
+required:
+ - compatible
+ - reg
+ - vdda-supply
+ - vddi-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ touchscreen@66 {
+ compatible = "touchnetix,ax54a";
+ reg = <0x66>;
+ interrupt-parent = <&gpio2>;
+ interrupts = <2 IRQ_TYPE_EDGE_FALLING>;
+ reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>;
+ vdda-supply = <&vdda_reg>;
+ vddi-supply = <&vddi_reg>;
+ poll-interval = <20>;
+ };
+ };
+...
--
2.47.3
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox