drivers: gpio: add VIRTIO GPIO driver

Add a driver for the VIRTIO GPIO device (virtio spec 1.3, section 5.18),
letting a Zephyr guest drive GPIO lines exposed by a hypervisor or a
vhost-user backend.

The number of lines is read from the device configuration space at init,
and interrupts use the event virtqueue when the device offers
VIRTIO_GPIO_F_IRQ. Every operation is a round trip to the device, so the
GPIO API can only be used from a thread.

Assisted-by: Claude Code:opus-4.8
Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
diff --git a/drivers/gpio/CMakeLists.txt b/drivers/gpio/CMakeLists.txt
index 38289df..b035d33 100644
--- a/drivers/gpio/CMakeLists.txt
+++ b/drivers/gpio/CMakeLists.txt
@@ -132,6 +132,7 @@
 zephyr_library_sources_ifdef(CONFIG_GPIO_TEST gpio_test.c)
 zephyr_library_sources_ifdef(CONFIG_GPIO_TLA2528 gpio_tla2528.c)
 zephyr_library_sources_ifdef(CONFIG_GPIO_TLE9104 gpio_tle9104.c)
+zephyr_library_sources_ifdef(CONFIG_GPIO_VIRTIO gpio_virtio.c)
 zephyr_library_sources_ifdef(CONFIG_GPIO_WCH_CH5XX gpio_wch_ch5xx.c)
 zephyr_library_sources_ifdef(CONFIG_GPIO_WCH_GPIO wch_gpio_ch32v00x.c)
 zephyr_library_sources_ifdef(CONFIG_GPIO_XEC gpio_mchp_xec.c)
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index 0688716..058bcb5 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -212,6 +212,7 @@
 source "drivers/gpio/Kconfig.test"
 source "drivers/gpio/Kconfig.tla2528"
 source "drivers/gpio/Kconfig.tle9104"
+source "drivers/gpio/Kconfig.virtio"
 source "drivers/gpio/Kconfig.wch_ch32v00x"
 source "drivers/gpio/Kconfig.wch_ch5xx"
 source "drivers/gpio/Kconfig.xec"
diff --git a/drivers/gpio/Kconfig.virtio b/drivers/gpio/Kconfig.virtio
new file mode 100644
index 0000000..518d458
--- /dev/null
+++ b/drivers/gpio/Kconfig.virtio
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+config GPIO_VIRTIO
+	bool "VIRTIO GPIO driver"
+	default y
+	depends on DT_HAS_VIRTIO_GPIO_ENABLED
+	select VIRTIO
+	help
+	  Enable driver for VIRTIO GPIO devices.
diff --git a/drivers/gpio/gpio_virtio.c b/drivers/gpio/gpio_virtio.c
new file mode 100644
index 0000000..4f50b67
--- /dev/null
+++ b/drivers/gpio/gpio_virtio.c
@@ -0,0 +1,691 @@
+/*
+ * SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * Driver for the VIRTIO GPIO device (virtio spec 1.3, section 5.18).
+ *
+ * Every operation is a round trip to the virtio device, so none of the GPIO API
+ * calls can be issued from an ISR, including from the interrupt callbacks this
+ * driver fires; they return -EWOULDBLOCK there. Callbacks needing to touch the
+ * port must defer the work to a thread.
+ */
+
+#define DT_DRV_COMPAT virtio_gpio
+
+#include <zephyr/device.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/drivers/gpio/gpio_utils.h>
+#include <zephyr/drivers/virtio.h>
+#include <zephyr/drivers/virtio/virtqueue.h>
+#include <zephyr/kernel.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/sys/minmax.h>
+#include <zephyr/sys/util.h>
+
+LOG_MODULE_REGISTER(gpio_virtio, CONFIG_GPIO_LOG_LEVEL);
+
+#define VIRTIO_GPIO_REQUESTQ 0
+#define VIRTIO_GPIO_EVENTQ   1
+
+#define VIRTIO_GPIO_F_IRQ 0
+
+#define VIRTIO_GPIO_MSG_GET_DIRECTION 0x0002
+#define VIRTIO_GPIO_MSG_SET_DIRECTION 0x0003
+#define VIRTIO_GPIO_MSG_GET_VALUE     0x0004
+#define VIRTIO_GPIO_MSG_SET_VALUE     0x0005
+#define VIRTIO_GPIO_MSG_IRQ_TYPE      0x0006
+
+#define VIRTIO_GPIO_STATUS_OK 0x0
+
+#define VIRTIO_GPIO_DIRECTION_NONE 0x00
+#define VIRTIO_GPIO_DIRECTION_OUT  0x01
+#define VIRTIO_GPIO_DIRECTION_IN   0x02
+
+#define VIRTIO_GPIO_IRQ_TYPE_NONE         0x00
+#define VIRTIO_GPIO_IRQ_TYPE_EDGE_RISING  0x01
+#define VIRTIO_GPIO_IRQ_TYPE_EDGE_FALLING 0x02
+#define VIRTIO_GPIO_IRQ_TYPE_EDGE_BOTH    0x03
+#define VIRTIO_GPIO_IRQ_TYPE_LEVEL_HIGH   0x04
+#define VIRTIO_GPIO_IRQ_TYPE_LEVEL_LOW    0x08
+
+#define VIRTIO_GPIO_IRQ_STATUS_VALID 0x1
+
+struct virtio_gpio_config {
+	uint16_t ngpio;
+	uint8_t padding[2];
+	uint32_t gpio_names_size;
+} __packed;
+
+struct virtio_gpio_request {
+	uint16_t type;
+	uint16_t gpio;
+	uint32_t value;
+} __packed;
+
+struct virtio_gpio_response {
+	uint8_t status;
+	uint8_t value;
+} __packed;
+
+struct virtio_gpio_irq_request {
+	uint16_t gpio;
+} __packed;
+
+struct virtio_gpio_irq_response {
+	uint8_t status;
+} __packed;
+
+/** Per line state of the buffer chain sitting in the event virtqueue */
+struct gpio_virtio_irq_line {
+	const struct device *dev;
+	struct virtio_gpio_irq_request req;
+	struct virtio_gpio_irq_response res;
+	gpio_pin_t pin;
+	bool queued;
+	bool enabled;
+};
+
+struct gpio_virtio_config {
+	struct gpio_driver_config common;
+	const struct device *vdev;
+	struct gpio_virtio_irq_line *irq_lines;
+	uint16_t max_pins;
+};
+
+struct gpio_virtio_data {
+	struct gpio_driver_data common;
+	struct virtq *requestq;
+	struct virtq *eventq;
+	/* serializes the single in-flight request and the state tracked below */
+	struct k_sem lock;
+	struct k_sem done;
+	uint32_t res_len;
+	struct virtio_gpio_request req;
+	struct virtio_gpio_response res;
+	struct k_spinlock irq_lock;
+	sys_slist_t callbacks;
+	/* lines given a direction, i.e. the ones that have a value to report */
+	gpio_port_pins_t configured;
+	gpio_port_value_t out_state;
+	uint16_t ngpio;
+	bool irq_supported;
+};
+
+static void gpio_virtio_request_cb(void *opaque, uint32_t used_len)
+{
+	struct gpio_virtio_data *data = opaque;
+
+	data->res_len = used_len;
+	k_sem_give(&data->done);
+}
+
+/**
+ * @brief Run a single request/response transaction on the request virtqueue
+ *
+ * Caller must hold data->lock.
+ *
+ * @param dev virtio GPIO device
+ * @param type one of the VIRTIO_GPIO_MSG_* request types
+ * @param pin line the request applies to
+ * @param value request payload, meaning depends on @p type
+ * @param res_value where the response payload is stored, can be NULL
+ * @return 0 on success or negative error code on failure
+ */
+static int gpio_virtio_request(const struct device *dev, uint16_t type, gpio_pin_t pin,
+			       uint32_t value, uint8_t *res_value)
+{
+	const struct gpio_virtio_config *cfg = dev->config;
+	struct gpio_virtio_data *data = dev->data;
+	struct virtq_buf bufs[] = {
+		{.addr = &data->req, .len = sizeof(data->req)},
+		{.addr = &data->res, .len = sizeof(data->res)},
+	};
+	int ret;
+
+	data->req.type = sys_cpu_to_le16(type);
+	data->req.gpio = sys_cpu_to_le16(pin);
+	data->req.value = sys_cpu_to_le32(value);
+	data->res_len = 0;
+
+	/*
+	 * The lock keeps a single request in flight, and the transport returns
+	 * the descriptors before completing it, so the queue never runs out.
+	 */
+	ret = virtq_add_buffer_chain(data->requestq, bufs, ARRAY_SIZE(bufs), 1,
+				     gpio_virtio_request_cb, data, K_NO_WAIT);
+	if (ret != 0) {
+		LOG_ERR("failed to queue request %u for pin %u: %d", type, pin, ret);
+		return ret;
+	}
+
+	virtio_notify_virtqueue(cfg->vdev, VIRTIO_GPIO_REQUESTQ);
+
+	k_sem_take(&data->done, K_FOREVER);
+
+	if (data->res_len != sizeof(data->res)) {
+		LOG_ERR("response of incorrect length (%u : %u)", data->res_len,
+			(uint32_t)sizeof(data->res));
+		return -EIO;
+	}
+
+	if (data->res.status != VIRTIO_GPIO_STATUS_OK) {
+		LOG_DBG("request %u rejected for pin %u", type, pin);
+		return -EIO;
+	}
+
+	if (res_value != NULL) {
+		*res_value = data->res.value;
+	}
+
+	return 0;
+}
+
+/* Caller must hold data->lock */
+static int gpio_virtio_set_value(const struct device *dev, gpio_pin_t pin, bool value)
+{
+	struct gpio_virtio_data *data = dev->data;
+	int ret;
+
+	ret = gpio_virtio_request(dev, VIRTIO_GPIO_MSG_SET_VALUE, pin, value, NULL);
+	if (ret == 0) {
+		WRITE_BIT(data->out_state, pin, value);
+	}
+
+	return ret;
+}
+
+static int gpio_virtio_pin_configure(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags)
+{
+	struct gpio_virtio_data *data = dev->data;
+	uint8_t dir;
+	int ret;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	if (pin >= data->ngpio) {
+		return -EINVAL;
+	}
+
+	if ((flags & (GPIO_SINGLE_ENDED | GPIO_PULL_UP | GPIO_PULL_DOWN)) != 0) {
+		return -ENOTSUP;
+	}
+
+	/* the protocol carries a single direction per line */
+	if ((flags & GPIO_INPUT) != 0 && (flags & GPIO_OUTPUT) != 0) {
+		return -ENOTSUP;
+	}
+
+	k_sem_take(&data->lock, K_FOREVER);
+
+	if ((flags & GPIO_OUTPUT) != 0) {
+		if ((flags & (GPIO_OUTPUT_INIT_HIGH | GPIO_OUTPUT_INIT_LOW)) != 0) {
+			/* drive the requested level before enabling the output */
+			ret = gpio_virtio_set_value(dev, pin, (flags & GPIO_OUTPUT_INIT_HIGH) != 0);
+			if (ret != 0) {
+				goto out;
+			}
+		}
+		dir = VIRTIO_GPIO_DIRECTION_OUT;
+	} else if ((flags & GPIO_INPUT) != 0) {
+		dir = VIRTIO_GPIO_DIRECTION_IN;
+	} else {
+		dir = VIRTIO_GPIO_DIRECTION_NONE;
+	}
+
+	ret = gpio_virtio_request(dev, VIRTIO_GPIO_MSG_SET_DIRECTION, pin, dir, NULL);
+	if (ret == 0) {
+		WRITE_BIT(data->configured, pin, dir != VIRTIO_GPIO_DIRECTION_NONE);
+	}
+
+out:
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+
+static int gpio_virtio_port_get_raw(const struct device *dev, gpio_port_value_t *value)
+{
+	struct gpio_virtio_data *data = dev->data;
+	gpio_port_pins_t pins;
+	gpio_port_value_t val = 0;
+	int ret = 0;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	k_sem_take(&data->lock, K_FOREVER);
+
+	/*
+	 * The protocol has no bulk read, and a line left at the default
+	 * VIRTIO_GPIO_DIRECTION_NONE has no value to report, so only the lines
+	 * given a direction through this driver are read back.
+	 */
+	pins = data->configured;
+
+	while (pins != 0) {
+		gpio_pin_t pin = find_lsb_set(pins) - 1;
+		uint8_t pin_val;
+
+		ret = gpio_virtio_request(dev, VIRTIO_GPIO_MSG_GET_VALUE, pin, 0, &pin_val);
+		if (ret != 0) {
+			goto out;
+		}
+
+		WRITE_BIT(val, pin, pin_val);
+		pins &= ~BIT(pin);
+	}
+
+	*value = val;
+
+out:
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+
+/* Caller must hold data->lock */
+static int gpio_virtio_set_masked(const struct device *dev, gpio_port_pins_t mask,
+				  gpio_port_value_t value)
+{
+	struct gpio_virtio_data *data = dev->data;
+	int ret = 0;
+
+	mask &= GPIO_PORT_PIN_MASK_FROM_NGPIOS(data->ngpio);
+
+	while (mask != 0) {
+		gpio_pin_t pin = find_lsb_set(mask) - 1;
+
+		ret = gpio_virtio_set_value(dev, pin, (value & BIT(pin)) != 0);
+		if (ret != 0) {
+			break;
+		}
+
+		mask &= ~BIT(pin);
+	}
+
+	return ret;
+}
+
+static int gpio_virtio_port_set_masked_raw(const struct device *dev, gpio_port_pins_t mask,
+					   gpio_port_value_t value)
+{
+	struct gpio_virtio_data *data = dev->data;
+	int ret;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	k_sem_take(&data->lock, K_FOREVER);
+	ret = gpio_virtio_set_masked(dev, mask, value);
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+
+static int gpio_virtio_port_set_bits_raw(const struct device *dev, gpio_port_pins_t pins)
+{
+	return gpio_virtio_port_set_masked_raw(dev, pins, pins);
+}
+
+static int gpio_virtio_port_clear_bits_raw(const struct device *dev, gpio_port_pins_t pins)
+{
+	return gpio_virtio_port_set_masked_raw(dev, pins, 0);
+}
+
+static int gpio_virtio_port_toggle_bits(const struct device *dev, gpio_port_pins_t pins)
+{
+	struct gpio_virtio_data *data = dev->data;
+	int ret;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	/* the lock keeps the shadow state consistent across the update */
+	k_sem_take(&data->lock, K_FOREVER);
+	ret = gpio_virtio_set_masked(dev, pins, ~data->out_state);
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+
+#ifdef CONFIG_GPIO_GET_DIRECTION
+static int gpio_virtio_port_get_direction(const struct device *dev, gpio_port_pins_t map,
+					  gpio_port_pins_t *inputs, gpio_port_pins_t *outputs)
+{
+	struct gpio_virtio_data *data = dev->data;
+	gpio_port_pins_t in = 0;
+	gpio_port_pins_t out = 0;
+	int ret = 0;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	map &= GPIO_PORT_PIN_MASK_FROM_NGPIOS(data->ngpio);
+
+	k_sem_take(&data->lock, K_FOREVER);
+
+	while (map != 0) {
+		gpio_pin_t pin = find_lsb_set(map) - 1;
+		uint8_t dir;
+
+		ret = gpio_virtio_request(dev, VIRTIO_GPIO_MSG_GET_DIRECTION, pin, 0, &dir);
+		if (ret != 0) {
+			goto out;
+		}
+
+		if (dir == VIRTIO_GPIO_DIRECTION_IN) {
+			in |= BIT(pin);
+		} else if (dir == VIRTIO_GPIO_DIRECTION_OUT) {
+			out |= BIT(pin);
+		}
+
+		map &= ~BIT(pin);
+	}
+
+	if (inputs != NULL) {
+		*inputs = in;
+	}
+
+	if (outputs != NULL) {
+		*outputs = out;
+	}
+
+out:
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+#endif /* CONFIG_GPIO_GET_DIRECTION */
+
+static void gpio_virtio_event_cb(void *opaque, uint32_t used_len);
+
+/* Caller must hold data->irq_lock */
+static int gpio_virtio_arm_irq(const struct device *dev, struct gpio_virtio_irq_line *line)
+{
+	const struct gpio_virtio_config *cfg = dev->config;
+	struct gpio_virtio_data *data = dev->data;
+	struct virtq_buf bufs[] = {
+		{.addr = &line->req, .len = sizeof(line->req)},
+		{.addr = &line->res, .len = sizeof(line->res)},
+	};
+	int ret;
+
+	if (line->queued) {
+		return 0;
+	}
+
+	line->req.gpio = sys_cpu_to_le16(line->pin);
+
+	ret = virtq_add_buffer_chain(data->eventq, bufs, ARRAY_SIZE(bufs), 1, gpio_virtio_event_cb,
+				     line, K_NO_WAIT);
+	if (ret != 0) {
+		LOG_ERR("failed to queue event buffer for pin %u: %d", line->pin, ret);
+		return ret;
+	}
+
+	line->queued = true;
+	virtio_notify_virtqueue(cfg->vdev, VIRTIO_GPIO_EVENTQ);
+
+	return 0;
+}
+
+static void gpio_virtio_event_cb(void *opaque, uint32_t used_len)
+{
+	struct gpio_virtio_irq_line *line = opaque;
+	const struct device *dev = line->dev;
+	struct gpio_virtio_data *data = dev->data;
+	k_spinlock_key_t key;
+	bool fire;
+
+	key = k_spin_lock(&data->irq_lock);
+	line->queued = false;
+	/*
+	 * A buffer handed back with an invalid status only means the device
+	 * dropped it because the interrupt got disabled, it is not an event.
+	 */
+	fire = line->enabled && used_len == sizeof(line->res) &&
+	       line->res.status == VIRTIO_GPIO_IRQ_STATUS_VALID;
+	k_spin_unlock(&data->irq_lock, key);
+
+	if (fire) {
+		gpio_fire_callbacks(&data->callbacks, dev, BIT(line->pin));
+	}
+
+	/* re-arm only once the event has been reported, to keep them ordered */
+	key = k_spin_lock(&data->irq_lock);
+	if (line->enabled) {
+		(void)gpio_virtio_arm_irq(dev, line);
+	}
+	k_spin_unlock(&data->irq_lock, key);
+}
+
+static int gpio_virtio_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin,
+					       enum gpio_int_mode mode, enum gpio_int_trig trig)
+{
+	const struct gpio_virtio_config *cfg = dev->config;
+	struct gpio_virtio_data *data = dev->data;
+	struct gpio_virtio_irq_line *line;
+	k_spinlock_key_t key;
+	uint8_t type;
+	int ret;
+
+	if (k_is_in_isr()) {
+		return -EWOULDBLOCK;
+	}
+
+	if (pin >= data->ngpio) {
+		return -EINVAL;
+	}
+
+	if (!data->irq_supported) {
+		return -ENOTSUP;
+	}
+
+	line = &cfg->irq_lines[pin];
+
+	switch (mode) {
+	case GPIO_INT_MODE_DISABLED:
+		type = VIRTIO_GPIO_IRQ_TYPE_NONE;
+		break;
+	case GPIO_INT_MODE_EDGE:
+		switch (trig & GPIO_INT_TRIG_BOTH) {
+		case GPIO_INT_TRIG_LOW:
+			type = VIRTIO_GPIO_IRQ_TYPE_EDGE_FALLING;
+			break;
+		case GPIO_INT_TRIG_HIGH:
+			type = VIRTIO_GPIO_IRQ_TYPE_EDGE_RISING;
+			break;
+		default:
+			type = VIRTIO_GPIO_IRQ_TYPE_EDGE_BOTH;
+			break;
+		}
+		break;
+	case GPIO_INT_MODE_LEVEL:
+		switch (trig & GPIO_INT_TRIG_BOTH) {
+		case GPIO_INT_TRIG_LOW:
+			type = VIRTIO_GPIO_IRQ_TYPE_LEVEL_LOW;
+			break;
+		case GPIO_INT_TRIG_HIGH:
+			type = VIRTIO_GPIO_IRQ_TYPE_LEVEL_HIGH;
+			break;
+		default:
+			return -ENOTSUP;
+		}
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	if (type == VIRTIO_GPIO_IRQ_TYPE_NONE) {
+		/*
+		 * Stop reporting events first, the buffer still queued on the
+		 * device is handed back once it processes the request.
+		 */
+		key = k_spin_lock(&data->irq_lock);
+		line->enabled = false;
+		k_spin_unlock(&data->irq_lock, key);
+	}
+
+	k_sem_take(&data->lock, K_FOREVER);
+	ret = gpio_virtio_request(dev, VIRTIO_GPIO_MSG_IRQ_TYPE, pin, type, NULL);
+	k_sem_give(&data->lock);
+
+	if (ret != 0 || type == VIRTIO_GPIO_IRQ_TYPE_NONE) {
+		return ret;
+	}
+
+	/* the buffer may only be queued once the interrupt is enabled */
+	key = k_spin_lock(&data->irq_lock);
+	line->enabled = true;
+	ret = gpio_virtio_arm_irq(dev, line);
+	k_spin_unlock(&data->irq_lock, key);
+
+	return ret;
+}
+
+static int gpio_virtio_manage_callback(const struct device *dev, struct gpio_callback *callback,
+				       bool set)
+{
+	struct gpio_virtio_data *data = dev->data;
+
+	return gpio_manage_callback(&data->callbacks, callback, set);
+}
+
+static DEVICE_API(gpio, gpio_virtio_api) = {
+	.pin_configure = gpio_virtio_pin_configure,
+	.port_get_raw = gpio_virtio_port_get_raw,
+	.port_set_masked_raw = gpio_virtio_port_set_masked_raw,
+	.port_set_bits_raw = gpio_virtio_port_set_bits_raw,
+	.port_clear_bits_raw = gpio_virtio_port_clear_bits_raw,
+	.port_toggle_bits = gpio_virtio_port_toggle_bits,
+	.pin_interrupt_configure = gpio_virtio_pin_interrupt_configure,
+	.manage_callback = gpio_virtio_manage_callback,
+#ifdef CONFIG_GPIO_GET_DIRECTION
+	.port_get_direction = gpio_virtio_port_get_direction,
+#endif
+};
+
+static uint16_t gpio_virtio_enum_queues_cb(uint16_t q_index, uint16_t q_size_max, void *opaque)
+{
+	struct gpio_virtio_data *data = opaque;
+
+	switch (q_index) {
+	case VIRTIO_GPIO_REQUESTQ:
+		/* a single request is in flight at a time, and takes two descriptors */
+		return min(2, q_size_max);
+	case VIRTIO_GPIO_EVENTQ:
+		/* every line may have a buffer chain of its own queued up */
+		return min(NHPOT(2 * data->ngpio), q_size_max);
+	default:
+		return 0;
+	}
+}
+
+static int gpio_virtio_init(const struct device *dev)
+{
+	const struct gpio_virtio_config *cfg = dev->config;
+	struct gpio_virtio_data *data = dev->data;
+	volatile struct virtio_gpio_config *devcfg;
+	uint16_t ngpio;
+	int ret;
+
+	if (!device_is_ready(cfg->vdev)) {
+		LOG_ERR_DEVICE_NOT_READY(cfg->vdev);
+		return -ENODEV;
+	}
+
+	k_sem_init(&data->lock, 1, 1);
+	k_sem_init(&data->done, 0, 1);
+
+	data->irq_supported = virtio_read_device_feature_bit(cfg->vdev, VIRTIO_GPIO_F_IRQ);
+	if (data->irq_supported &&
+	    virtio_write_driver_feature_bit(cfg->vdev, VIRTIO_GPIO_F_IRQ, true) != 0) {
+		LOG_WRN("could not enable the IRQ feature");
+		data->irq_supported = false;
+	}
+
+	ret = virtio_commit_feature_bits(cfg->vdev);
+	if (ret != 0) {
+		LOG_ERR("virtio_commit_feature_bits failed: %d", ret);
+		return ret;
+	}
+
+	devcfg = virtio_get_device_specific_config(cfg->vdev);
+	if (devcfg == NULL) {
+		LOG_ERR("could not get device-specific config");
+		return -ENODEV;
+	}
+
+	ngpio = sys_le16_to_cpu(devcfg->ngpio);
+	if (ngpio == 0) {
+		LOG_ERR("device exposes no GPIO lines");
+		return -ENODEV;
+	}
+
+	if (ngpio > cfg->max_pins) {
+		LOG_WRN("device exposes %u lines, only the first %u are usable", ngpio,
+			cfg->max_pins);
+		ngpio = cfg->max_pins;
+	}
+
+	data->ngpio = ngpio;
+
+	ret = virtio_init_virtqueues(cfg->vdev, data->irq_supported ? 2 : 1,
+				     gpio_virtio_enum_queues_cb, data);
+	if (ret != 0) {
+		LOG_ERR("virtio_init_virtqueues failed: %d", ret);
+		return ret;
+	}
+
+	data->requestq = virtio_get_virtqueue(cfg->vdev, VIRTIO_GPIO_REQUESTQ);
+	if (data->requestq == NULL) {
+		LOG_ERR("failed to get the request virtqueue");
+		return -ENODEV;
+	}
+
+	if (data->irq_supported) {
+		data->eventq = virtio_get_virtqueue(cfg->vdev, VIRTIO_GPIO_EVENTQ);
+		if (data->eventq == NULL) {
+			LOG_ERR("failed to get the event virtqueue");
+			return -ENODEV;
+		}
+
+		for (gpio_pin_t pin = 0; pin < ngpio; pin++) {
+			cfg->irq_lines[pin].dev = dev;
+			cfg->irq_lines[pin].pin = pin;
+		}
+	}
+
+	virtio_finalize_init(cfg->vdev);
+
+	LOG_DBG("%u lines, interrupts %s", ngpio,
+		data->irq_supported ? "supported" : "unsupported");
+
+	return 0;
+}
+
+#define GPIO_VIRTIO_DEFINE(inst)                                                                   \
+	static struct gpio_virtio_irq_line                                                         \
+		gpio_virtio_irq_lines_##inst[DT_INST_PROP(inst, ngpios)];                          \
+	static struct gpio_virtio_data gpio_virtio_data_##inst;                                    \
+	static const struct gpio_virtio_config gpio_virtio_config_##inst = {                       \
+		.common = GPIO_COMMON_CONFIG_FROM_DT_INST(inst),                                   \
+		.vdev = DEVICE_DT_GET(DT_INST_PARENT(inst)),                                       \
+		.irq_lines = gpio_virtio_irq_lines_##inst,                                         \
+		.max_pins = DT_INST_PROP(inst, ngpios),                                            \
+	};                                                                                         \
+	DEVICE_DT_INST_DEFINE(inst, gpio_virtio_init, NULL, &gpio_virtio_data_##inst,              \
+			      &gpio_virtio_config_##inst, POST_KERNEL, CONFIG_GPIO_INIT_PRIORITY,  \
+			      &gpio_virtio_api);
+
+DT_INST_FOREACH_STATUS_OKAY(GPIO_VIRTIO_DEFINE)
diff --git a/dts/bindings/gpio/virtio,gpio.yaml b/dts/bindings/gpio/virtio,gpio.yaml
new file mode 100644
index 0000000..72ee2e8
--- /dev/null
+++ b/dts/bindings/gpio/virtio,gpio.yaml
@@ -0,0 +1,36 @@
+# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+description: |
+  VIRTIO GPIO device (ID:41)
+
+compatible: "virtio,gpio"
+
+include: [gpio-controller.yaml, base.yaml]
+
+properties:
+  ngpios:
+    max: 32
+
+  "#gpio-cells":
+    const: 2
+
+gpio-cells:
+  - pin
+  - flags
+
+examples:
+  - |
+    virtio_mmio0: virtio_mmio@a000000 {
+            compatible = "virtio,mmio";
+            reg = <0xa000000 0x200>;
+            interrupts = <0 16 4>;
+            status = "okay";
+
+            virtio_gpio0: virtio-gpio {
+                    compatible = "virtio,gpio";
+                    gpio-controller;
+                    #gpio-cells = <2>;
+                    status = "okay";
+            };
+    };
diff --git a/tests/drivers/build_all/gpio/tests.yaml b/tests/drivers/build_all/gpio/tests.yaml
index 32a5592..adc2025 100644
--- a/tests/drivers/build_all/gpio/tests.yaml
+++ b/tests/drivers/build_all/gpio/tests.yaml
@@ -76,3 +76,11 @@
       - nrf54l15dk/nrf54l15/cpuflpr/xip
     depends_on: gpio
     extra_args: DTC_OVERLAY_FILE="nrf54l15dk_nrf54l15_cpuflpr.overlay"
+
+  drivers.gpio.build.virtio:
+    platform_allow:
+      - qemu_cortex_a53
+      - qemu_riscv64
+    integration_platforms:
+      - qemu_cortex_a53
+    extra_args: DTC_OVERLAY_FILE="virtio.overlay"
diff --git a/tests/drivers/build_all/gpio/virtio.overlay b/tests/drivers/build_all/gpio/virtio.overlay
new file mode 100644
index 0000000..0488b57
--- /dev/null
+++ b/tests/drivers/build_all/gpio/virtio.overlay
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Application overlay for testing the VIRTIO GPIO driver build
+ */
+
+&virtio_mmio1 {
+	status = "okay";
+
+	test_gpio_virtio: virtio-gpio {
+		compatible = "virtio,gpio";
+		gpio-controller;
+		#gpio-cells = <2>;
+		status = "okay";
+	};
+};