* [PATCH 1/3] Input: adafruit-seesaw - switch to using dev_err_probe()
2026-03-21 20:24 [PATCH 0/3] Input: adafruit-seesaw: use dev_err_probe and add IRQ support charles.embedded
@ 2026-03-21 20:24 ` charles.embedded
2026-03-21 20:24 ` [PATCH 2/3] Input: adafruit-seesaw - add interrupt support charles.embedded
2026-03-21 20:24 ` [PATCH 3/3] dt-bindings: input: adafruit-seesaw-gamepad: fix interrupt polarity charles.embedded
2 siblings, 0 replies; 6+ messages in thread
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
From: Charles Dias <charlesdias.cd@outlook.com>
Use dev_err_probe() instead of dev_err() in seesaw_probe function
to improve error handling.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
drivers/input/joystick/adafruit-seesaw.c | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/drivers/input/joystick/adafruit-seesaw.c b/drivers/input/joystick/adafruit-seesaw.c
index c248c15b849d..177b42446e9b 100644
--- a/drivers/input/joystick/adafruit-seesaw.c
+++ b/drivers/input/joystick/adafruit-seesaw.c
@@ -277,17 +277,12 @@ static int seesaw_probe(struct i2c_client *client)
SEESAW_JOYSTICK_FUZZ, SEESAW_JOYSTICK_FLAT);
err = sparse_keymap_setup(seesaw->input_dev, seesaw_buttons_new, NULL);
- if (err) {
- dev_err(&client->dev,
- "failed to set up input device keymap: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to set up input device keymap\n");
err = input_setup_polling(seesaw->input_dev, seesaw_poll);
- if (err) {
- dev_err(&client->dev, "failed to set up polling: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to set up polling\n");
input_set_poll_interval(seesaw->input_dev,
SEESAW_GAMEPAD_POLL_INTERVAL_MS);
@@ -295,10 +290,8 @@ static int seesaw_probe(struct i2c_client *client)
input_set_min_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MIN);
err = input_register_device(seesaw->input_dev);
- if (err) {
- dev_err(&client->dev, "failed to register joystick: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to register joystick\n");
return 0;
}
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH 2/3] Input: adafruit-seesaw - add interrupt support
2026-03-21 20:24 [PATCH 0/3] Input: adafruit-seesaw: use dev_err_probe and add IRQ support charles.embedded
2026-03-21 20:24 ` [PATCH 1/3] Input: adafruit-seesaw - switch to using dev_err_probe() charles.embedded
@ 2026-03-21 20:24 ` charles.embedded
2026-03-23 5:12 ` Dmitry Torokhov
2026-03-21 20:24 ` [PATCH 3/3] dt-bindings: input: adafruit-seesaw-gamepad: fix interrupt polarity charles.embedded
2 siblings, 1 reply; 6+ messages in thread
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
From: Charles Dias <charlesdias.cd@outlook.com>
Use IRQ-driven reporting for button events when an interrupt is
described in DTS. Joystick axis values still have no interrupt source,
so axis reporting continues to use polling.
When the DTS specifies an interrupt, the IRQ thread calls only
seesaw_report_buttons(), avoiding unnecessary ADC reads on each button
event. The polling path always calls seesaw_report_axes(), and calls
seesaw_report_buttons() only when no IRQ is configured. This avoids
concurrent access to button_state between the IRQ thread and the poll
timer.
When no interrupt is available, the driver falls back to fully polled
operation.
Also remove the now-unused seesaw_data structure. Axis values are read
directly in seesaw_report_axes(), and button_state already lives in the
driver data, where it persists across calls for state comparison.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
drivers/input/joystick/adafruit-seesaw.c | 141 +++++++++++++++++------
1 file changed, 103 insertions(+), 38 deletions(-)
diff --git a/drivers/input/joystick/adafruit-seesaw.c b/drivers/input/joystick/adafruit-seesaw.c
index 177b42446e9b..ed8896c639ce 100644
--- a/drivers/input/joystick/adafruit-seesaw.c
+++ b/drivers/input/joystick/adafruit-seesaw.c
@@ -11,8 +11,10 @@
* Product page: https://www.adafruit.com/product/5743
* Firmware and hardware sources: https://github.com/adafruit/Adafruit_Seesaw
*
- * TODO:
- * - Add interrupt support
+ * Interrupt support is available when the DTS defines an interrupt for the
+ * device. Button events are then driven by the seesaw INT line, while joystick
+ * axes are always polled since the seesaw ADC has no interrupt source.
+ * Without an interrupt, the driver falls back to fully polling mode.
*/
#include <linux/unaligned.h>
@@ -21,6 +23,7 @@
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
+#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
@@ -31,12 +34,14 @@
#define SEESAW_GPIO_DIRCLR_BULK 0x0103
#define SEESAW_GPIO_BULK 0x0104
#define SEESAW_GPIO_BULK_SET 0x0105
+#define SEESAW_GPIO_INTENSET 0x0108
#define SEESAW_GPIO_PULLENSET 0x010b
#define SEESAW_STATUS_HW_ID 0x0001
#define SEESAW_STATUS_SWRST 0x007f
#define SEESAW_ADC_OFFSET 0x07
+#define SEESAW_ADC_REG(ch) (SEESAW_ADC_BASE | (SEESAW_ADC_OFFSET + (ch)))
#define SEESAW_BUTTON_A 0x05
#define SEESAW_BUTTON_B 0x01
@@ -67,12 +72,6 @@ struct seesaw_gamepad {
u32 button_state;
};
-struct seesaw_data {
- u16 x;
- u16 y;
- u32 button_state;
-};
-
static const struct key_entry seesaw_buttons_new[] = {
{ KE_KEY, SEESAW_BUTTON_A, .keycode = BTN_SOUTH },
{ KE_KEY, SEESAW_BUTTON_B, .keycode = BTN_EAST },
@@ -142,39 +141,61 @@ static int seesaw_register_write_u32(struct i2c_client *client, u16 reg,
return 0;
}
-static int seesaw_read_data(struct i2c_client *client, struct seesaw_data *data)
+static int seesaw_report_buttons(struct seesaw_gamepad *private)
{
- __be16 adc_data;
+ struct input_dev *input = private->input_dev;
+ unsigned long changed;
+ u32 button_state;
__be32 read_buf;
- int err;
+ int err, i;
- err = seesaw_register_read(client, SEESAW_GPIO_BULK,
+ err = seesaw_register_read(private->i2c_client, SEESAW_GPIO_BULK,
&read_buf, sizeof(read_buf));
if (err)
return err;
- data->button_state = ~be32_to_cpu(read_buf);
+ button_state = ~be32_to_cpu(read_buf) & SEESAW_BUTTON_MASK;
+ changed = private->button_state ^ button_state;
+ private->button_state = button_state;
+
+ for_each_set_bit(i, &changed, fls(SEESAW_BUTTON_MASK)) {
+ if (!sparse_keymap_report_event(input, i,
+ button_state & BIT(i), false))
+ dev_err_ratelimited(&input->dev,
+ "failed to report keymap event");
+ }
+
+ input_sync(input);
+ return 0;
+}
+
+static int seesaw_report_axes(struct seesaw_gamepad *private)
+{
+ struct input_dev *input = private->input_dev;
+ __be16 adc_data;
+ int err;
- err = seesaw_register_read(client,
- SEESAW_ADC_BASE |
- (SEESAW_ADC_OFFSET + SEESAW_ANALOG_X),
+ err = seesaw_register_read(private->i2c_client,
+ SEESAW_ADC_REG(SEESAW_ANALOG_X),
&adc_data, sizeof(adc_data));
if (err)
return err;
+
/*
* ADC reads left as max and right as 0, must be reversed since kernel
* expects reports in opposite order.
*/
- data->x = SEESAW_JOYSTICK_MAX_AXIS - be16_to_cpu(adc_data);
+ input_report_abs(input, ABS_X,
+ SEESAW_JOYSTICK_MAX_AXIS - be16_to_cpu(adc_data));
- err = seesaw_register_read(client,
- SEESAW_ADC_BASE |
- (SEESAW_ADC_OFFSET + SEESAW_ANALOG_Y),
+ err = seesaw_register_read(private->i2c_client,
+ SEESAW_ADC_REG(SEESAW_ANALOG_Y),
&adc_data, sizeof(adc_data));
if (err)
return err;
- data->y = be16_to_cpu(adc_data);
+ input_report_abs(input, ABS_Y, be16_to_cpu(adc_data));
+ input_sync(input);
return 0;
}
@@ -182,42 +203,72 @@ static int seesaw_read_data(struct i2c_client *client, struct seesaw_data *data)
static int seesaw_open(struct input_dev *input)
{
struct seesaw_gamepad *private = input_get_drvdata(input);
+ int err;
private->button_state = 0;
+ if (private->i2c_client->irq) {
+ /*
+ * Read and report current button state before enabling the
+ * edge-triggered IRQ. This deasserts any pending INT already
+ * latched by the chip since probe(), preventing the IRQ line
+ * from being stuck low on the first open.
+ */
+ err = seesaw_report_buttons(private);
+ if (err)
+ return err;
+
+ enable_irq(private->i2c_client->irq);
+ }
+
return 0;
}
+static void seesaw_close(struct input_dev *input)
+{
+ struct seesaw_gamepad *private = input_get_drvdata(input);
+
+ if (private->i2c_client->irq)
+ disable_irq(private->i2c_client->irq);
+}
+
static void seesaw_poll(struct input_dev *input)
{
struct seesaw_gamepad *private = input_get_drvdata(input);
- struct seesaw_data data;
- unsigned long changed;
- int err, i;
+ int err;
- err = seesaw_read_data(private->i2c_client, &data);
+ err = seesaw_report_axes(private);
if (err) {
dev_err_ratelimited(&input->dev,
- "failed to read joystick state: %d\n", err);
+ "failed to read joystick axes: %d\n", err);
return;
}
- input_report_abs(input, ABS_X, data.x);
- input_report_abs(input, ABS_Y, data.y);
+ /*
+ * In interrupt mode, buttons are reported exclusively by
+ * seesaw_irq_thread() to avoid concurrent access to button_state.
+ */
+ if (!private->i2c_client->irq) {
+ err = seesaw_report_buttons(private);
+ if (err)
+ dev_err_ratelimited(&input->dev,
+ "failed to read button state: %d\n", err);
+ }
+}
- data.button_state &= SEESAW_BUTTON_MASK;
- changed = private->button_state ^ data.button_state;
- private->button_state = data.button_state;
+static irqreturn_t seesaw_irq_thread(int irq, void *dev_id)
+{
+ struct seesaw_gamepad *private = dev_id;
+ int err;
- for_each_set_bit(i, &changed, fls(SEESAW_BUTTON_MASK)) {
- if (!sparse_keymap_report_event(input, i,
- data.button_state & BIT(i),
- false))
- dev_err_ratelimited(&input->dev,
- "failed to report keymap event");
+ err = seesaw_report_buttons(private);
+ if (err) {
+ dev_err_ratelimited(&private->input_dev->dev,
+ "failed to read button state: %d\n", err);
+ return IRQ_NONE;
}
- input_sync(input);
+ return IRQ_HANDLED;
}
static int seesaw_probe(struct i2c_client *client)
@@ -268,6 +319,7 @@ static int seesaw_probe(struct i2c_client *client)
seesaw->input_dev->name = "Adafruit Seesaw Gamepad";
seesaw->input_dev->phys = "i2c/" SEESAW_DEVICE_NAME;
seesaw->input_dev->open = seesaw_open;
+ seesaw->input_dev->close = seesaw_close;
input_set_drvdata(seesaw->input_dev, seesaw);
input_set_abs_params(seesaw->input_dev, ABS_X,
0, SEESAW_JOYSTICK_MAX_AXIS,
@@ -289,6 +341,19 @@ static int seesaw_probe(struct i2c_client *client)
input_set_max_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MAX);
input_set_min_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MIN);
+ if (client->irq) {
+ err = seesaw_register_write_u32(client, SEESAW_GPIO_INTENSET, SEESAW_BUTTON_MASK);
+ if (err)
+ return dev_err_probe(&client->dev, err,
+ "failed to enable hardware interrupts\n");
+
+ err = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ seesaw_irq_thread, IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ SEESAW_DEVICE_NAME, seesaw);
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to request IRQ\n");
+ }
+
err = input_register_device(seesaw->input_dev);
if (err)
return dev_err_probe(&client->dev, err, "failed to register joystick\n");
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH 3/3] dt-bindings: input: adafruit-seesaw-gamepad: fix interrupt polarity
2026-03-21 20:24 [PATCH 0/3] Input: adafruit-seesaw: use dev_err_probe and add IRQ support charles.embedded
2026-03-21 20:24 ` [PATCH 1/3] Input: adafruit-seesaw - switch to using dev_err_probe() charles.embedded
2026-03-21 20:24 ` [PATCH 2/3] Input: adafruit-seesaw - add interrupt support charles.embedded
@ 2026-03-21 20:24 ` charles.embedded
2 siblings, 0 replies; 6+ messages in thread
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
From: Charles Dias <charlesdias.cd@outlook.com>
The INT line is open-drain and asserts low on button GPIO changes, so
the binding should describe a falling-edge trigger rather than rising
edge. Also update the example to use IRQ_TYPE_EDGE_FALLING and add
interrupt-parent, and clarify that the driver can fall back to polling
when no IRQ is wired.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
.../bindings/input/adafruit,seesaw-gamepad.yaml | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml b/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
index 5e86f6de6978..f0ebb326bf74 100644
--- a/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
+++ b/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
@@ -25,6 +25,11 @@ description: |
SE -> Select
X, A, B, Y -> Digital action buttons
+ The gamepad exposes button events through the seesaw GPIO block and joystick
+ axes through the seesaw ADC block. If the optional IRQ pin is wired, button
+ presses can be interrupt-driven while joystick axes remain polled. Without an
+ IRQ, the driver falls back to fully polled operation.
+
Datasheet: https://cdn-learn.adafruit.com/downloads/pdf/gamepad-qt.pdf
Product page: https://www.adafruit.com/product/5743
Arduino Driver: https://github.com/adafruit/Adafruit_Seesaw
@@ -39,7 +44,9 @@ properties:
interrupts:
maxItems: 1
description:
- The gamepad's IRQ pin triggers a rising edge if interrupts are enabled.
+ Optional interrupt from the gamepad's open-drain INT pin. The device
+ asserts INT low on button GPIO changes when interrupts are enabled in the
+ seesaw firmware, so the host should typically use a falling-edge trigger.
required:
- compatible
@@ -57,7 +64,8 @@ examples:
joystick@50 {
compatible = "adafruit,seesaw-gamepad";
- interrupts = <18 IRQ_TYPE_EDGE_RISING>;
+ interrupt-parent = <&gpio1>;
+ interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
reg = <0x50>;
};
};
^ permalink raw reply related [flat|nested] 6+ messages in thread