On 6/25/26 10:54 AM, Shenwei Wang wrote:
From: Shenwei Wang <[email protected]>

On an AMP platform, the system may include multiple processors:
        - MCUs running an RTOS
        - An MPU running Linux

These processors communicate via the RPMSG protocol.
The driver implements the standard GPIO interface, allowing
the Linux side to control GPIO controllers which reside in
the remote processor via RPMSG protocol.

Cc: Bartosz Golaszewski <[email protected]>
Cc: Andrew Lunn <[email protected]>
Signed-off-by: Shenwei Wang <[email protected]>
---
  drivers/gpio/Kconfig      |  17 ++
  drivers/gpio/Makefile     |   1 +
  drivers/gpio/gpio-rpmsg.c | 568 ++++++++++++++++++++++++++++++++++++++
  3 files changed, 586 insertions(+)
  create mode 100644 drivers/gpio/gpio-rpmsg.c

diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index 020e51e30317..4ad299fe3c6f 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1917,6 +1917,23 @@ config GPIO_SODAVILLE
endmenu +menu "RPMSG GPIO drivers"
+       depends on RPMSG
+
+config GPIO_RPMSG
+       tristate "Generic RPMSG GPIO support"
+       depends on OF && REMOTEPROC
+       select GPIOLIB_IRQCHIP
+       default REMOTEPROC
+       help
+         Say yes here to support the generic GPIO functions over the RPMSG
+         bus. Currently supported devices: i.MX7ULP, i.MX8ULP, i.MX8x, and
+         i.MX9x.

The support would depend on if the right firmware is loaded/running on the given
remote core. Also if you want to make this generic, then any vendor should be 
able
to make a firmware that implements this protocol and make use of this driver.
Suggest dropping this NXP specific device list.

+
+         If unsure, say N.
+
+endmenu
+
  menu "SPI GPIO expanders"
        depends on SPI_MASTER
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index b267598b517d..ee75c0e65b8b 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -157,6 +157,7 @@ obj-$(CONFIG_GPIO_RDC321X)          += gpio-rdc321x.o
  obj-$(CONFIG_GPIO_REALTEK_OTTO)               += gpio-realtek-otto.o
  obj-$(CONFIG_GPIO_REG)                        += gpio-reg.o
  obj-$(CONFIG_GPIO_ROCKCHIP)   += gpio-rockchip.o
+obj-$(CONFIG_GPIO_RPMSG)               += gpio-rpmsg.o
  obj-$(CONFIG_GPIO_RTD)                        += gpio-rtd.o
  obj-$(CONFIG_ARCH_SA1100)             += gpio-sa1100.o
  obj-$(CONFIG_GPIO_SAMA5D2_PIOBU)      += gpio-sama5d2-piobu.o
diff --git a/drivers/gpio/gpio-rpmsg.c b/drivers/gpio/gpio-rpmsg.c
new file mode 100644
index 000000000000..332e2925a830
--- /dev/null
+++ b/drivers/gpio/gpio-rpmsg.c
@@ -0,0 +1,568 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2026 NXP
+ *
+ * The driver exports a standard gpiochip interface to control
+ * the GPIO controllers via RPMSG on a remote processor.
+ */
+
+#include <linux/completion.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/driver.h>
+#include <linux/init.h>
+#include <linux/irqdomain.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/remoteproc.h>
+#include <linux/rpmsg.h>
+#include <linux/virtio_gpio.h>
+
+#define GPIOS_PER_PORT_DEFAULT         32
+#define RPMSG_TIMEOUT                  1000
+
+/* Additional commands beyond virtio-gpio */
+#define VIRTIO_GPIO_MSG_SET_WAKEUP     0x0010
+
+/* GPIO Receive MSG Type */
+#define GPIO_RPMSG_REPLY       1
+#define GPIO_RPMSG_NOTIFY      2
+
+#define CHAN_NAME_PREFIX       "rpmsg-io-"
+#define GPIO_COMPAT_STR                "rpmsg-gpio"
+
+struct rpmsg_gpio_response {
+       __u8 type;
+       union {
+               /* command reply */
+               struct {
+                       __u8 status;
+                       __u8 value;
+               };
+
+               /* interrupt notification */
+               struct {
+                       __u8 line;
+                       __u8 trigger; /* rising/falling/high/low */
+               };
+       };
+};
+
+struct rpmsg_gpio_line {
+       u8 irq_shutdown;
+       u8 irq_unmask;
+       u8 irq_mask;
+       u32 irq_wake_enable;
+       u32 irq_type;
+};
+
+struct rpmsg_gpio_port {
+       struct gpio_chip gc;
+       struct rpmsg_device *rpdev;
+       struct virtio_gpio_request *send_msg;
+       struct rpmsg_gpio_response *recv_msg;
+       struct completion cmd_complete;
+       struct mutex lock;
+       u32 ngpios;
+       u32 idx;
+       struct rpmsg_gpio_line lines[GPIOS_PER_PORT_DEFAULT];
+};
+
+static int rpmsg_gpio_send_message(struct rpmsg_gpio_port *port)
+{
+       int ret;
+
+       reinit_completion(&port->cmd_complete);
+
+       ret = rpmsg_send(port->rpdev->ept, port->send_msg, 
sizeof(*port->send_msg));
+       if (ret) {
+               dev_err(&port->rpdev->dev, "rpmsg_send failed: cmd=%d ret=%d\n",
+                       port->send_msg->type, ret);
+               return ret;
+       }
+
+       ret = wait_for_completion_timeout(&port->cmd_complete,
+                                         msecs_to_jiffies(RPMSG_TIMEOUT));
+       if (ret == 0) {
+               dev_err(&port->rpdev->dev, "rpmsg_send timeout! cmd=%d\n",
+                       port->send_msg->type);
+               return -ETIMEDOUT;
+       }
+
+       if (unlikely(port->recv_msg->status != VIRTIO_GPIO_STATUS_OK)) {
+               dev_err(&port->rpdev->dev, "remote core replies an error: 
cmd=%d!\n",
+                       port->send_msg->type);
+               return -EINVAL;
+       }
+
+       return 0;
+}
+
+static struct virtio_gpio_request *
+rpmsg_gpio_msg_prepare(struct rpmsg_gpio_port *port, u16 line, u16 cmd, u32 
val)
+{
+       struct virtio_gpio_request *msg = port->send_msg;
+
+       msg->type = cmd;
+       msg->gpio = line;
+       msg->value = val;
+
+       return msg;
+}
+
+static int rpmsg_gpio_get(struct gpio_chip *gc, unsigned int line)
+{
+       struct rpmsg_gpio_port *port = gpiochip_get_data(gc);
+       int ret;
+
+       guard(mutex)(&port->lock);
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_GET_VALUE, 0);
+
+       ret = rpmsg_gpio_send_message(port);
+       return ret ? ret : port->recv_msg->value;
+}
+
+static int rpmsg_gpio_get_direction(struct gpio_chip *gc, unsigned int line)
+{
+       struct rpmsg_gpio_port *port = gpiochip_get_data(gc);
+       int ret;
+
+       guard(mutex)(&port->lock);
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_GET_DIRECTION, 0);
+
+       ret = rpmsg_gpio_send_message(port);
+       if (ret)
+               return ret;
+
+       switch (port->recv_msg->value) {
+       case VIRTIO_GPIO_DIRECTION_IN:
+               return GPIO_LINE_DIRECTION_IN;
+       case VIRTIO_GPIO_DIRECTION_OUT:
+               return GPIO_LINE_DIRECTION_OUT;
+       default:
+               break;
+       }
+
+       return -EINVAL;
+}
+
+static int rpmsg_gpio_direction_input(struct gpio_chip *gc, unsigned int line)
+{
+       struct rpmsg_gpio_port *port = gpiochip_get_data(gc);
+
+       guard(mutex)(&port->lock);
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_SET_DIRECTION,
+                              VIRTIO_GPIO_DIRECTION_IN);
+
+       return rpmsg_gpio_send_message(port);
+}
+
+static int rpmsg_gpio_set(struct gpio_chip *gc, unsigned int line, int val)
+{
+       struct rpmsg_gpio_port *port = gpiochip_get_data(gc);
+
+       guard(mutex)(&port->lock);
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_SET_VALUE, val);
+
+       return rpmsg_gpio_send_message(port);
+}
+
+static int rpmsg_gpio_direction_output(struct gpio_chip *gc, unsigned int 
line, int val)
+{
+       struct rpmsg_gpio_port *port = gpiochip_get_data(gc);
+       int ret;
+
+       guard(mutex)(&port->lock);
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_SET_DIRECTION,
+                              VIRTIO_GPIO_DIRECTION_OUT);
+
+       ret = rpmsg_gpio_send_message(port);
+       if (ret)
+               return ret;
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_SET_VALUE, val);
+
+       return rpmsg_gpio_send_message(port);
+}
+
+static int gpio_rpmsg_irq_set_type(struct irq_data *d, u32 type)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       switch (type) {
+       case IRQ_TYPE_EDGE_RISING:
+               type = VIRTIO_GPIO_IRQ_TYPE_EDGE_RISING;
+               irq_set_handler_locked(d, handle_simple_irq);
+               break;
+       case IRQ_TYPE_EDGE_FALLING:
+               type = VIRTIO_GPIO_IRQ_TYPE_EDGE_FALLING;
+               irq_set_handler_locked(d, handle_simple_irq);
+               break;
+       case IRQ_TYPE_EDGE_BOTH:
+               type = VIRTIO_GPIO_IRQ_TYPE_EDGE_BOTH;
+               irq_set_handler_locked(d, handle_simple_irq);
+               break;
+       case IRQ_TYPE_LEVEL_LOW:
+               type = VIRTIO_GPIO_IRQ_TYPE_LEVEL_LOW;
+               irq_set_handler_locked(d, handle_level_irq);
+               break;
+       case IRQ_TYPE_LEVEL_HIGH:
+               type = VIRTIO_GPIO_IRQ_TYPE_LEVEL_HIGH;
+               irq_set_handler_locked(d, handle_level_irq);
+               break;
+       default:
+               dev_err(&port->rpdev->dev, "unsupported irq type: %u\n", type);
+               return -EINVAL;
+       }
+
+       port->lines[line].irq_type = type;
+
+       return 0;
+}
+
+static int gpio_rpmsg_irq_set_wake(struct irq_data *d, u32 enable)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       port->lines[line].irq_wake_enable = enable;
+
+       return 0;
+}
+
+/*
+ * This unmask/mask function is invoked in two situations:
+ *   - when an interrupt is being set up, and
+ *   - after an interrupt has occurred.
+ *
+ * The GPIO driver does not access hardware registers directly.
+ * Instead, it caches all relevant information locally, and then sends
+ * the accumulated state to the remote system at this stage.
+ */
+static void gpio_rpmsg_unmask_irq(struct irq_data *d)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       port->lines[line].irq_unmask = 1;
+}
+
+static void gpio_rpmsg_mask_irq(struct irq_data *d)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       /*
+        * When an interrupt occurs, the remote system masks the interrupt
+        * and then sends a notification to Linux. After Linux processes
+        * that notification, it sends an RPMsg command back to the remote
+        * system to unmask the interrupt again.
+        */
+       port->lines[line].irq_mask = 1;
+}
+
+static void gpio_rpmsg_irq_shutdown(struct irq_data *d)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       port->lines[line].irq_shutdown = 1;
+}
+
+static void gpio_rpmsg_irq_bus_lock(struct irq_data *d)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+
+       mutex_lock(&port->lock);
+}
+
+static void gpio_rpmsg_irq_bus_sync_unlock(struct irq_data *d)
+{
+       struct rpmsg_gpio_port *port = irq_data_get_irq_chip_data(d);
+       u32 line = d->hwirq;
+
+       rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_SET_WAKEUP,
+                              port->lines[line].irq_wake_enable);
+       rpmsg_gpio_send_message(port);
+
+       /*
+        * For mask irq, do nothing here.
+        * The remote system will mask interrupt after an interrupt occurs,
+        * and then send a notification to Linux system. After Linux system
+        * handles the notification, it sends an rpmsg back to the remote
+        * system to unmask this interrupt again.
+        */
+       if (port->lines[line].irq_mask && !port->lines[line].irq_unmask) {
+               port->lines[line].irq_mask = 0;
+               mutex_unlock(&port->lock);
+               return;
+       }
+
+       if (port->lines[line].irq_shutdown) {
+               rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_IRQ_TYPE,
+                                      VIRTIO_GPIO_IRQ_TYPE_NONE);
+               port->lines[line].irq_shutdown = 0;
+       } else {
+               rpmsg_gpio_msg_prepare(port, line, VIRTIO_GPIO_MSG_IRQ_TYPE,
+                                      port->lines[line].irq_type);
+
+               if (port->lines[line].irq_unmask)
+                       port->lines[line].irq_unmask = 0;
+       }
+
+       rpmsg_gpio_send_message(port);
+       mutex_unlock(&port->lock);
+}
+
+static const struct irq_chip gpio_rpmsg_irq_chip = {
+       .irq_mask = gpio_rpmsg_mask_irq,
+       .irq_unmask = gpio_rpmsg_unmask_irq,
+       .irq_set_wake = gpio_rpmsg_irq_set_wake,
+       .irq_set_type = gpio_rpmsg_irq_set_type,
+       .irq_shutdown = gpio_rpmsg_irq_shutdown,
+       .irq_bus_lock = gpio_rpmsg_irq_bus_lock,
+       .irq_bus_sync_unlock = gpio_rpmsg_irq_bus_sync_unlock,
+       .flags = IRQCHIP_IMMUTABLE,
+};
+
+static int rpmsg_gpiochip_register(struct rpmsg_device *rpdev,
+                                  struct device_node *np, const char *name)
+{
+       struct rpmsg_gpio_port *port;
+       struct gpio_irq_chip *girq;
+       struct gpio_chip *gc;
+       int ret;
+
+       port = devm_kzalloc(&rpdev->dev, sizeof(*port), GFP_KERNEL);
+       if (!port)
+               return -ENOMEM;
+
+       ret = of_property_read_u32(np, "reg", &port->idx);
+       if (ret)
+               return ret;
+
+       ret = devm_mutex_init(&rpdev->dev, &port->lock);
+       if (ret)
+               return ret;
+
+       ret = of_property_read_u32(np, "ngpios", &port->ngpios);
+       if (ret || port->ngpios > GPIOS_PER_PORT_DEFAULT)
+               port->ngpios = GPIOS_PER_PORT_DEFAULT;
+
+       port->send_msg = devm_kzalloc(&rpdev->dev,
+                                     sizeof(*port->send_msg),
+                                     GFP_KERNEL);
+
+       port->recv_msg = devm_kzalloc(&rpdev->dev,
+                                     sizeof(*port->recv_msg),
+                                     GFP_KERNEL);
+       if (!port->send_msg || !port->recv_msg)
+               return -ENOMEM;
+
+       init_completion(&port->cmd_complete);
+       port->rpdev = rpdev;
+
+       gc = &port->gc;
+       gc->owner = THIS_MODULE;
+       gc->parent = &rpdev->dev;
+       gc->fwnode = of_fwnode_handle(np);
+       gc->ngpio = port->ngpios;
+       gc->base = -1;
+       gc->label = devm_kasprintf(&rpdev->dev, GFP_KERNEL, "%s-gpio%d",
+                                  name, port->idx);
+
+       gc->direction_input = rpmsg_gpio_direction_input;
+       gc->direction_output = rpmsg_gpio_direction_output;
+       gc->get_direction = rpmsg_gpio_get_direction;
+       gc->get = rpmsg_gpio_get;
+       gc->set = rpmsg_gpio_set;
+
+       girq = &gc->irq;
+       gpio_irq_chip_set_chip(girq, &gpio_rpmsg_irq_chip);
+       girq->parent_handler = NULL;
+       girq->num_parents = 0;
+       girq->parents = NULL;
+       girq->chip->name = devm_kstrdup(&rpdev->dev, gc->label, GFP_KERNEL);
+
+       dev_set_drvdata(&rpdev->dev, port);
+
+       return devm_gpiochip_add_data(&rpdev->dev, gc, port);
+}
+
+static const char *rpmsg_get_rproc_node_name(struct rpmsg_device *rpdev)
+{
+       const char *name = NULL;
+       struct device_node *np;
+       struct rproc *rproc;
+
+       rproc = rproc_get_by_child(&rpdev->dev);
+       if (!rproc)
+               return NULL;
+
+       np = of_node_get(rproc->dev.of_node);
+       if (!np && rproc->dev.parent)
+               np = of_node_get(rproc->dev.parent->of_node);
+
+       if (np) {
+               name = devm_kstrdup(&rpdev->dev, np->name, GFP_KERNEL);
+               of_node_put(np);
+       }
+
+       return name;
+}
+
+static struct device_node *
+rpmsg_find_child_by_compat_reg(struct device_node *parent, const char *compat, 
u32 idx)
+{
+       struct device_node *child;
+       u32 reg;
+
+       for_each_available_child_of_node(parent, child) {
+               if (!of_device_is_compatible(child, compat))
+                       continue;
+
+               if (of_property_read_u32(child, "reg", &reg))
+                       continue;
+
+               if (reg == idx)
+                       return child;
+       }
+
+       return NULL;
+}
+
+static struct device_node *
+rpmsg_get_channel_ofnode(struct rpmsg_device *rpdev, const char *compat, u32 
idx)
+{
+       struct device_node *np_chan = NULL, *np;
+       struct rproc *rproc;
+
+       rproc = rproc_get_by_child(&rpdev->dev);
+       if (!rproc)
+               return NULL;
+
+       np = of_node_get(rproc->dev.of_node);
+       if (!np && rproc->dev.parent)
+               np = of_node_get(rproc->dev.parent->of_node);
+
+       if (np)
+               np_chan = rpmsg_find_child_by_compat_reg(np, compat, idx);
+
+       return np_chan;
+}
+
+static int rpmsg_get_gpio_index(const char *name, const char *prefix)
+{
+       const char *p;
+       int base = 10;
+       int val;
+
+       if (!name)
+               return -EINVAL;
+
+       /* Ensure correct prefix */
+       if (!str_has_prefix(name, prefix))
+               return -EINVAL;
+
+       /* Find last '-' */
+       p = strrchr(name, '-');
+
+       if (!p || *(p + 1) == '\0')
+               return -EINVAL;
+
+       if (p[1] == '0' && (p[2] == 'x' || p[2] == 'X'))
+               base = 16;
+
+       if (kstrtoint(p + 1, base, &val))
+               return -EINVAL;
+
+       return val;
+}
+
+static int rpmsg_gpio_channel_callback(struct rpmsg_device *rpdev, void *data,
+                                      int len, void *priv, u32 src)
+{
+       struct rpmsg_gpio_response *msg = data;
+       struct rpmsg_gpio_port *port = NULL;
+
+       port = dev_get_drvdata(&rpdev->dev);
+
+       if (!port) {
+               dev_err(&rpdev->dev, "port is null\n");
+               return -EINVAL;
+       }
+
+       if (msg->type == GPIO_RPMSG_REPLY) {
+               *port->recv_msg = *msg;
+               complete(&port->cmd_complete);
+       } else if (msg->type == GPIO_RPMSG_NOTIFY) {
+               generic_handle_domain_irq_safe(port->gc.irq.domain, msg->line);
+       } else {
+               dev_err(&rpdev->dev, "wrong message type (0x%x)\n", msg->type);
+       }
+
+       return 0;
+}
+
+static int rpmsg_gpio_channel_probe(struct rpmsg_device *rpdev)
+{
+       struct device *dev = &rpdev->dev;
+       struct device_node *np;
+       const char *rproc_name;
+       int idx;
+
+       idx = rpmsg_get_gpio_index(rpdev->id.name, CHAN_NAME_PREFIX);
+       if (idx < 0)
+               return -EINVAL;
+
+       if (!dev->of_node) {
+               np = rpmsg_get_channel_ofnode(rpdev, GPIO_COMPAT_STR, idx);
+               if (!np)
+                       return -ENODEV;

This seems to imply that DT nodes are required. RPMSG is a discoverable
bus with a nameservice that can bind/probe new devices. While then optionally
binding to a DT node when available so sub-devices can be described in DT is
fine, I don't see why it should be required.

+
+               dev->of_node = np;
+               set_primary_fwnode(dev, of_fwnode_handle(np));
+               return -EPROBE_DEFER;
+       }
+
+       rproc_name = rpmsg_get_rproc_node_name(rpdev);
+
+       return rpmsg_gpiochip_register(rpdev, dev->of_node, rproc_name);
+}
+
+static const struct of_device_id rpmsg_gpio_dt_ids[] = {
+       { .compatible = GPIO_COMPAT_STR },
+       { /* sentinel */ }
+};
+
+static struct rpmsg_device_id rpmsg_gpio_channel_id_table[] = {
+       { .name = CHAN_NAME_PREFIX },
+       { },
+};
+MODULE_DEVICE_TABLE(rpmsg, rpmsg_gpio_channel_id_table);
+
+static struct rpmsg_driver rpmsg_gpio_channel_client = {
+       .callback       = rpmsg_gpio_channel_callback,
+       .id_table       = rpmsg_gpio_channel_id_table,
+       .probe          = rpmsg_gpio_channel_probe,
+       .drv            = {
+               .name   = KBUILD_MODNAME,
+               .of_match_table = rpmsg_gpio_dt_ids,

Does this line actually do anything anymore? Maybe it did when
this was a platform_driver, but this is a rpmsg_driver and
will probe though .id_table matches.

Andrew

+       },
+};
+module_rpmsg_driver(rpmsg_gpio_channel_client);
+
+MODULE_AUTHOR("Shenwei Wang <[email protected]>");
+MODULE_DESCRIPTION("generic rpmsg gpio driver");
+MODULE_LICENSE("GPL");


Reply via email to