mirror of
https://github.com/armbian/build.git
synced 2025-08-15 15:46:58 +02:00
468 lines
15 KiB
Diff
468 lines
15 KiB
Diff
From 63cf9c45ac695a062d47213b5bc962785b6f9147 Mon Sep 17 00:00:00 2001
|
|
From: Teguh Sobirin <teguh@sobir.in>
|
|
Date: Thu, 20 Feb 2025 14:50:30 +0800
|
|
Subject: [PATCH] input: Add driver for RSInput Gamepad
|
|
|
|
Signed-off-by: Teguh Sobirin <teguh@sobir.in>
|
|
---
|
|
drivers/input/joystick/Kconfig | 4 +
|
|
drivers/input/joystick/Makefile | 1 +
|
|
drivers/input/joystick/rsinput.c | 418 +++++++++++++++++++++++++++++++
|
|
3 files changed, 423 insertions(+)
|
|
create mode 100644 drivers/input/joystick/rsinput.c
|
|
|
|
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
|
|
index 7755e5b454d2..0da3c0f44ecf 100644
|
|
--- a/drivers/input/joystick/Kconfig
|
|
+++ b/drivers/input/joystick/Kconfig
|
|
@@ -383,6 +383,10 @@ config JOYSTICK_QWIIC
|
|
To compile this driver as a module, choose M here: the
|
|
module will be called qwiic-joystick.
|
|
|
|
+config JOYSTICK_RSINPUT
|
|
+ tristate "UART Based gamepad driver that found in AYN and Retroid Pocket products"
|
|
+ depends on SERIAL_DEV_BUS
|
|
+
|
|
config JOYSTICK_FSIA6B
|
|
tristate "FlySky FS-iA6B RC Receiver"
|
|
select SERIO
|
|
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
|
|
index 9976f596a920..3de503e29489 100644
|
|
--- a/drivers/input/joystick/Makefile
|
|
+++ b/drivers/input/joystick/Makefile
|
|
@@ -28,6 +28,7 @@ obj-$(CONFIG_JOYSTICK_N64) += n64joy.o
|
|
obj-$(CONFIG_JOYSTICK_PSXPAD_SPI) += psxpad-spi.o
|
|
obj-$(CONFIG_JOYSTICK_PXRC) += pxrc.o
|
|
obj-$(CONFIG_JOYSTICK_QWIIC) += qwiic-joystick.o
|
|
+obj-$(CONFIG_JOYSTICK_RSINPUT) += rsinput.o
|
|
obj-$(CONFIG_JOYSTICK_SEESAW) += adafruit-seesaw.o
|
|
obj-$(CONFIG_JOYSTICK_SENSEHAT) += sensehat-joystick.o
|
|
obj-$(CONFIG_JOYSTICK_SIDEWINDER) += sidewinder.o
|
|
diff --git a/drivers/input/joystick/rsinput.c b/drivers/input/joystick/rsinput.c
|
|
new file mode 100644
|
|
index 000000000000..4a7096407712
|
|
--- /dev/null
|
|
+++ b/drivers/input/joystick/rsinput.c
|
|
@@ -0,0 +1,418 @@
|
|
+// SPDX-License-Identifier: GPL-2.0-only
|
|
+/*
|
|
+ * RSInput Gamepad Driver
|
|
+ *
|
|
+ * Copyright (C) 2024 Teguh Sobirin <teguh@sobir.in>
|
|
+ *
|
|
+ */
|
|
+#define DEBUG
|
|
+
|
|
+#include <linux/errno.h>
|
|
+#include <linux/gpio/consumer.h>
|
|
+#include <linux/init.h>
|
|
+#include <linux/input.h>
|
|
+#include <linux/kernel.h>
|
|
+#include <linux/module.h>
|
|
+#include <linux/of.h>
|
|
+#include <linux/serdev.h>
|
|
+#include <linux/slab.h>
|
|
+#include <uapi/linux/sched/types.h>
|
|
+
|
|
+#define FRAME_HEAD_1 0xA5
|
|
+#define FRAME_HEAD_2 0xD3
|
|
+#define FRAME_HEAD_3 0x5A
|
|
+#define FRAME_HEAD_4 0x3D
|
|
+
|
|
+#define CMD_COMMOD 0x01
|
|
+#define CMD_STATUS 0x02
|
|
+
|
|
+#define DATA_COMMOD_VERSION 0x02
|
|
+#define DATA_COMMOD_SET_PAR 0x05
|
|
+
|
|
+#define FRAME_POS_SEQ 4
|
|
+#define FRAME_POS_CMD 5
|
|
+#define FRAME_POS_LEN_L 6
|
|
+#define FRAME_POS_LEN_H 7
|
|
+#define FRAME_POS_DATA_1 8
|
|
+#define FRAME_POS_DATA_2 9
|
|
+#define FRAME_POS_DATA_3 10
|
|
+#define FRAME_POS_DATA_4 11
|
|
+#define FRAME_POS_DATA_5 12
|
|
+#define FRAME_POS_DATA_6 13
|
|
+#define FRAME_POS_DATA_7 14
|
|
+#define FRAME_POS_DATA_8 15
|
|
+#define FRAME_POS_DATA_9 16
|
|
+#define FRAME_POS_DATA_10 17
|
|
+#define FRAME_POS_DATA_11 18
|
|
+#define FRAME_POS_DATA_12 19
|
|
+#define FRAME_POS_DATA_13 20
|
|
+#define FRAME_POS_DATA_14 21
|
|
+
|
|
+#define MCU_PKT_SIZE_MIN 9
|
|
+
|
|
+#define MCU_VERSION_MAX_LEN 64
|
|
+
|
|
+struct rsinput_driver {
|
|
+ struct serdev_device *serdev;
|
|
+ struct input_dev *input;
|
|
+ struct gpio_desc *boot_gpio;
|
|
+ struct gpio_desc *enable_gpio;
|
|
+ struct gpio_desc *reset_gpio;
|
|
+ uint8_t rx_buf[256];
|
|
+ uint8_t sequence_number;
|
|
+};
|
|
+
|
|
+static const unsigned int keymap[] = {
|
|
+ BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT,
|
|
+ BTN_NORTH, BTN_WEST, BTN_EAST, BTN_SOUTH,
|
|
+ BTN_TL, BTN_TR, BTN_SELECT, BTN_START,
|
|
+ BTN_THUMBL, BTN_THUMBR, BTN_MODE, BTN_BACK
|
|
+};
|
|
+
|
|
+static uint8_t compute_checksum(const uint8_t *data, size_t len) {
|
|
+ uint8_t checksum = 0;
|
|
+
|
|
+ for (size_t i = FRAME_POS_SEQ; i < len - 1; i++) {
|
|
+ checksum ^= data[i];
|
|
+ }
|
|
+
|
|
+ return checksum;
|
|
+}
|
|
+
|
|
+static int rsinput_send_command(struct rsinput_driver *drv, uint8_t cmd, const uint8_t *data, size_t len) {
|
|
+ uint8_t frame[256];
|
|
+ uint8_t checksum = 0;
|
|
+ size_t frame_len = 0;
|
|
+
|
|
+ frame[frame_len++] = FRAME_HEAD_1;
|
|
+ frame[frame_len++] = FRAME_HEAD_2;
|
|
+ frame[frame_len++] = FRAME_HEAD_3;
|
|
+ frame[frame_len++] = FRAME_HEAD_4;
|
|
+
|
|
+ frame[frame_len++] = drv->sequence_number;
|
|
+ drv->sequence_number++;
|
|
+
|
|
+ frame[frame_len++] = cmd;
|
|
+
|
|
+ frame[frame_len++] = len & 0xFF;
|
|
+ frame[frame_len++] = (len >> 8) & 0xFF;
|
|
+
|
|
+ if (data && len) {
|
|
+ memcpy(&frame[frame_len], data, len);
|
|
+ frame_len += len;
|
|
+ }
|
|
+
|
|
+ checksum = compute_checksum(frame, frame_len + 1);
|
|
+ frame[frame_len++] = checksum;
|
|
+
|
|
+ return serdev_device_write_buf(drv->serdev, frame, frame_len);
|
|
+}
|
|
+
|
|
+static int rsinput_init_commands(struct rsinput_driver *drv) {
|
|
+ int error;
|
|
+
|
|
+ msleep(100);
|
|
+ uint8_t version_request[] = {DATA_COMMOD_VERSION};
|
|
+ error = rsinput_send_command(drv, CMD_COMMOD, version_request, sizeof(version_request));
|
|
+ if (error < 0) {
|
|
+ dev_err(&drv->serdev->dev, "Failed to request MCU version: %d\n", error);
|
|
+ return error;
|
|
+ }
|
|
+
|
|
+ msleep(100);
|
|
+ uint8_t mcu_params[] = {
|
|
+ DATA_COMMOD_SET_PAR,
|
|
+ 0x01,
|
|
+ 0x00, 0x00, 0x00, 0x28,
|
|
+ 0x00, 0x00, 0x00, 0x07
|
|
+ };
|
|
+ error = rsinput_send_command(drv, CMD_COMMOD, mcu_params, sizeof(mcu_params));
|
|
+ if (error < 0) {
|
|
+ dev_err(&drv->serdev->dev, "Failed to set MCU parameters: %d\n", error);
|
|
+ return error;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void handle_cmd_commod(struct rsinput_driver *drv, const uint8_t *data, size_t payload_length) {
|
|
+ switch (data[FRAME_POS_DATA_1]) {
|
|
+ case DATA_COMMOD_VERSION:
|
|
+ if (payload_length >= 1) {
|
|
+ char mcu_version[MCU_VERSION_MAX_LEN] = {0};
|
|
+ size_t version_length = payload_length;
|
|
+ if (version_length > MCU_VERSION_MAX_LEN - 1) {
|
|
+ version_length = MCU_VERSION_MAX_LEN - 1;
|
|
+ }
|
|
+ memcpy(mcu_version, &data[FRAME_POS_DATA_1], version_length);
|
|
+ mcu_version[version_length] = '\0';
|
|
+ dev_info(&drv->serdev->dev, "MCU Version: %s\n", mcu_version);
|
|
+ } else {
|
|
+ dev_err(&drv->serdev->dev, "Invalid MCU version response length\n");
|
|
+ }
|
|
+ break;
|
|
+ case DATA_COMMOD_SET_PAR:
|
|
+ dev_info(&drv->serdev->dev, "MCU parameters set successfully\n");
|
|
+ break;
|
|
+ default:
|
|
+ dev_warn(&drv->serdev->dev, "Unhandled CMD_COMMOD sub-command: 0x%02x\n", data[FRAME_POS_DATA_1]);
|
|
+ break;
|
|
+ }
|
|
+}
|
|
+
|
|
+static void handle_cmd_status(struct rsinput_driver *drv, const uint8_t *data, size_t payload_length) {
|
|
+ if (payload_length >= 6) {
|
|
+ static unsigned long prev_states;
|
|
+ unsigned long keys = data[FRAME_POS_DATA_1] | (data[FRAME_POS_DATA_2] << 8);
|
|
+ unsigned long current_states = keys, changes;
|
|
+ int i;
|
|
+
|
|
+ bitmap_xor(&changes, ¤t_states, &prev_states, ARRAY_SIZE(keymap));
|
|
+
|
|
+ for_each_set_bit(i, &changes, ARRAY_SIZE(keymap)) {
|
|
+ input_report_key(drv->input, keymap[i], (current_states & BIT(i)));
|
|
+ }
|
|
+
|
|
+ input_report_abs(drv->input, ABS_HAT2X,
|
|
+ 0x650 - (data[FRAME_POS_DATA_3] | (data[FRAME_POS_DATA_4] << 8)));
|
|
+ input_report_abs(drv->input, ABS_HAT2Y,
|
|
+ 0x650 - (data[FRAME_POS_DATA_5] | (data[FRAME_POS_DATA_6] << 8)));
|
|
+ input_report_abs(drv->input, ABS_X,
|
|
+ -(int16_t)(data[FRAME_POS_DATA_7] | (data[FRAME_POS_DATA_8] << 8)));
|
|
+ input_report_abs(drv->input, ABS_Y,
|
|
+ -(int16_t)(data[FRAME_POS_DATA_9] | (data[FRAME_POS_DATA_10] << 8)));
|
|
+ input_report_abs(drv->input, ABS_RX,
|
|
+ -(int16_t)(data[FRAME_POS_DATA_11] | (data[FRAME_POS_DATA_12] << 8)));
|
|
+ input_report_abs(drv->input, ABS_RY,
|
|
+ -(int16_t)(data[FRAME_POS_DATA_13] | (data[FRAME_POS_DATA_14] << 8)));
|
|
+
|
|
+ input_sync(drv->input);
|
|
+ prev_states = keys;
|
|
+
|
|
+ } else {
|
|
+ dev_warn(&drv->serdev->dev, "Invalid CMD_STATUS response length\n");
|
|
+ }
|
|
+}
|
|
+
|
|
+static void rsinput_process_data(struct rsinput_driver *drv, const uint8_t *data, size_t len) {
|
|
+ while (len >= MCU_PKT_SIZE_MIN) {
|
|
+ uint16_t payload_length = data[FRAME_POS_LEN_L] | (data[FRAME_POS_LEN_H] << 8);
|
|
+ size_t frame_length = MCU_PKT_SIZE_MIN + payload_length;
|
|
+
|
|
+ if (len < frame_length) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ uint8_t received_checksum = data[frame_length - 1];
|
|
+ uint8_t computed_checksum = compute_checksum(data, frame_length);
|
|
+ if (computed_checksum != received_checksum) {
|
|
+ data += frame_length;
|
|
+ len -= frame_length;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ switch (data[FRAME_POS_CMD]) {
|
|
+ case CMD_COMMOD:
|
|
+ handle_cmd_commod(drv, data, payload_length);
|
|
+ break;
|
|
+ case CMD_STATUS:
|
|
+ handle_cmd_status(drv, data, payload_length);
|
|
+ break;
|
|
+ default:
|
|
+ dev_warn(&drv->serdev->dev, "Unhandled command: 0x%02X\n", data[FRAME_POS_CMD]);
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ data += frame_length;
|
|
+ len -= frame_length;
|
|
+ }
|
|
+
|
|
+ if (len > 0) {
|
|
+ dev_warn(&drv->serdev->dev, "Trailing bytes after processing: %zu\n", len);
|
|
+ }
|
|
+}
|
|
+
|
|
+static size_t rsinput_rx(struct serdev_device *serdev, const u8 *data, size_t count) {
|
|
+ struct rsinput_driver *drv = serdev_device_get_drvdata(serdev);
|
|
+ uint8_t received_checksum, computed_checksum;
|
|
+
|
|
+ if (!drv || !data || count == 0) {
|
|
+ dev_warn_ratelimited(&serdev->dev, "Invalid RX data\n");
|
|
+ goto error;
|
|
+ }
|
|
+
|
|
+ if (count > sizeof(drv->rx_buf)) {
|
|
+ dev_warn_ratelimited(&serdev->dev, "RX buffer overflow\n");
|
|
+ goto error;
|
|
+ }
|
|
+
|
|
+ memcpy(drv->rx_buf, data, count);
|
|
+
|
|
+ if (count < MCU_PKT_SIZE_MIN) {
|
|
+ dev_warn_ratelimited(&serdev->dev, "Frame too short for checksum validation\n");
|
|
+ goto error;
|
|
+ }
|
|
+
|
|
+ received_checksum = drv->rx_buf[count - 1];
|
|
+
|
|
+ computed_checksum = compute_checksum(drv->rx_buf, count);
|
|
+
|
|
+ if (computed_checksum != received_checksum) {
|
|
+ rsinput_init_commands(drv);
|
|
+ goto error;
|
|
+ }
|
|
+
|
|
+ rsinput_process_data(drv, drv->rx_buf, count);
|
|
+
|
|
+error:
|
|
+ return count;
|
|
+}
|
|
+
|
|
+static const struct serdev_device_ops rsinput_rx_ops = {
|
|
+ .receive_buf = rsinput_rx,
|
|
+};
|
|
+
|
|
+static int rsinput_probe(struct serdev_device *serdev) {
|
|
+ struct rsinput_driver *drv;
|
|
+ u32 gamepad_bus = 0;
|
|
+ u32 gamepad_vid = 0;
|
|
+ u32 gamepad_pid = 0;
|
|
+ u32 gamepad_rev = 0;
|
|
+ int error;
|
|
+
|
|
+ drv = devm_kzalloc(&serdev->dev, sizeof(*drv), GFP_KERNEL);
|
|
+ if (!drv)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ drv->boot_gpio =
|
|
+ devm_gpiod_get_optional(&serdev->dev, "boot", GPIOD_OUT_HIGH);
|
|
+ if (IS_ERR(drv->boot_gpio)) {
|
|
+ error = PTR_ERR(drv->boot_gpio);
|
|
+ dev_warn(&serdev->dev, "Unable to get boot gpio: %d\n", error);
|
|
+ }
|
|
+
|
|
+ drv->enable_gpio =
|
|
+ devm_gpiod_get_optional(&serdev->dev, "enable", GPIOD_OUT_HIGH);
|
|
+ if (IS_ERR(drv->enable_gpio)) {
|
|
+ error = PTR_ERR(drv->enable_gpio);
|
|
+ dev_warn(&serdev->dev, "Unable to get enable gpio: %d\n", error);
|
|
+ }
|
|
+
|
|
+ drv->reset_gpio =
|
|
+ devm_gpiod_get_optional(&serdev->dev, "reset", GPIOD_OUT_HIGH);
|
|
+ if (IS_ERR(drv->reset_gpio)) {
|
|
+ error = PTR_ERR(drv->reset_gpio);
|
|
+ dev_warn(&serdev->dev, "Unable to get reset gpio: %d\n", error);
|
|
+ }
|
|
+
|
|
+ if (drv->boot_gpio)
|
|
+ gpiod_set_value_cansleep(drv->boot_gpio, 0);
|
|
+
|
|
+ if (drv->reset_gpio)
|
|
+ gpiod_set_value_cansleep(drv->reset_gpio, 0);
|
|
+
|
|
+ msleep(100);
|
|
+
|
|
+ if (drv->enable_gpio)
|
|
+ gpiod_set_value_cansleep(drv->enable_gpio, 1);
|
|
+
|
|
+ if (drv->reset_gpio)
|
|
+ gpiod_set_value_cansleep(drv->reset_gpio, 1);
|
|
+
|
|
+ msleep(100);
|
|
+
|
|
+ error = serdev_device_open(serdev);
|
|
+ if (error)
|
|
+ return dev_err_probe(&serdev->dev, error, "Unable to open UART device\n");
|
|
+
|
|
+ drv->serdev = serdev;
|
|
+ drv->sequence_number = 0;
|
|
+
|
|
+ serdev_device_set_drvdata(serdev, drv);
|
|
+
|
|
+ error = serdev_device_set_baudrate(serdev, 115200);
|
|
+ if (error < 0)
|
|
+ return dev_err_probe(&serdev->dev, error, "Failed to set up host baud rate\n");
|
|
+
|
|
+ serdev_device_set_flow_control(serdev, false);
|
|
+
|
|
+ drv->input = devm_input_allocate_device(&serdev->dev);
|
|
+ if (!drv->input)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ drv->input->phys = "rsinput-gamepad/input0";
|
|
+
|
|
+ error = device_property_read_string(&serdev->dev, "gamepad-name", &drv->input->name);
|
|
+ if (error) {
|
|
+ drv->input->name = "RSInput Gamepad";
|
|
+ }
|
|
+
|
|
+ device_property_read_u32(&serdev->dev, "gamepad-bus", &gamepad_bus);
|
|
+ device_property_read_u32(&serdev->dev, "gamepad-vid", &gamepad_vid);
|
|
+ device_property_read_u32(&serdev->dev, "gamepad-pid", &gamepad_pid);
|
|
+ device_property_read_u32(&serdev->dev, "gamepad-rev", &gamepad_rev);
|
|
+
|
|
+ drv->input->id.bustype = (u16)gamepad_bus;
|
|
+ drv->input->id.vendor = (u16)gamepad_vid;
|
|
+ drv->input->id.product = (u16)gamepad_pid;
|
|
+ drv->input->id.version = (u16)gamepad_rev;
|
|
+
|
|
+ __set_bit(EV_KEY, drv->input->evbit);
|
|
+ for (int i = 0; i < ARRAY_SIZE(keymap); i++)
|
|
+ input_set_capability(drv->input, EV_KEY, keymap[i]);
|
|
+
|
|
+ __set_bit(EV_ABS, drv->input->evbit);
|
|
+ for (int i = ABS_X; i <= ABS_RZ; i++)
|
|
+ input_set_abs_params(drv->input, i, -0x580, 0x580,
|
|
+ 0, 0);
|
|
+
|
|
+ input_set_abs_params(drv->input, ABS_HAT2X, 0, 1830, 0, 30);
|
|
+ input_set_abs_params(drv->input, ABS_HAT2Y, 0, 1830, 0, 30);
|
|
+
|
|
+ error = input_register_device(drv->input);
|
|
+ if (error)
|
|
+ return error;
|
|
+
|
|
+ serdev_device_set_client_ops(serdev, &rsinput_rx_ops);
|
|
+
|
|
+ error = rsinput_init_commands(drv);
|
|
+ if (error < 0) {
|
|
+ serdev_device_close(serdev);
|
|
+ return error;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void rsinput_remove(struct serdev_device *serdev) {
|
|
+ struct rsinput_driver *drv = serdev_device_get_drvdata(serdev);
|
|
+
|
|
+ serdev_device_close(serdev);
|
|
+ input_unregister_device(drv->input);
|
|
+ if (drv->enable_gpio)
|
|
+ gpiod_set_value_cansleep(drv->enable_gpio, 0);
|
|
+
|
|
+ if (drv->reset_gpio)
|
|
+ gpiod_set_value_cansleep(drv->reset_gpio, 0);
|
|
+}
|
|
+
|
|
+static const struct of_device_id rsinput_of_match[] = {
|
|
+ { .compatible = "gamepad,rsinput" },
|
|
+ { /* sentinel */ }
|
|
+};
|
|
+MODULE_DEVICE_TABLE(of, rsinput_of_match);
|
|
+
|
|
+static struct serdev_device_driver rsinput_driver = {
|
|
+ .probe = rsinput_probe,
|
|
+ .remove = rsinput_remove,
|
|
+ .driver = {
|
|
+ .name = "rsinput",
|
|
+ .of_match_table = rsinput_of_match,
|
|
+ },
|
|
+};
|
|
+
|
|
+module_serdev_device_driver(rsinput_driver);
|
|
+
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_DESCRIPTION("RSInput Gamepad Driver");
|
|
+MODULE_AUTHOR("Teguh Sobirin <teguh@sobir.in>");
|
|
--
|
|
2.34.1
|
|
|