diff --git a/gpiodev/gpio.c b/gpiodev/gpio.c
new file mode 100644
index 0000000000000000000000000000000000000000..0f84fb6c502bd6d0a5f808bc299fabd4144c2909
--- /dev/null
+++ b/gpiodev/gpio.c
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * QEMU GPIO device.
+ *
+ * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
+ *
+ */
+#include "qemu/osdep.h"
+
+#include "qapi/error.h"
+#include "qemu/config-file.h"
+#include "qemu/option.h"
+#include "qemu/qemu-print.h"
+#include "qemu/help_option.h"
+
+#include "gpiodev/gpio.h"
+
+static Object *get_gpiodevs_root(void)
+{
+    return object_get_container("gpiodevs");
+}
+
+static const TypeInfo gpiodev_types_info[] = {
+    {
+        .name = TYPE_GPIODEV,
+        .parent = TYPE_OBJECT,
+        .instance_size = sizeof(Gpiodev),
+        .abstract = true,
+    },
+};
+
+DEFINE_TYPES(gpiodev_types_info);
+
+static Gpiodev *gpiodev_new(const char *id,
+                            GMainContext *gcontext,
+                            Error **errp)
+{
+    Object *obj;
+    Gpiodev *gpio = NULL;
+
+    assert(id);
+
+    obj = object_new(TYPE_GPIODEV);
+    gpio = GPIODEV(obj);
+    gpio->gcontext = gcontext;
+
+    return gpio;
+}
+
+static Gpiodev *qemu_gpiodev_new(const char *id,
+                                 GMainContext *gcontext,
+                                 Error **errp)
+{
+    Gpiodev *gpio;
+
+    gpio = gpiodev_new(id, gcontext, errp);
+    if (!gpio) {
+        return NULL;
+    }
+
+    if (!object_property_try_add_child(get_gpiodevs_root(), id, OBJECT(gpio),
+                                       errp)) {
+        object_unref(OBJECT(gpio));
+        return NULL;
+    }
+
+    object_unref(OBJECT(gpio));
+
+    return gpio;
+}
+
+typedef struct GpiodevClassFE {
+    void (*fn)(const char *name, void *opaque);
+    void *opaque;
+} GpiodevClassFE;
+
+static void
+gpiodev_class_foreach(ObjectClass *klass, void *opaque)
+{
+    GpiodevClassFE *fe = opaque;
+
+    assert(g_str_has_prefix(object_class_get_name(klass), "gpiodev-"));
+    fe->fn(object_class_get_name(klass) + 8, fe->opaque);
+}
+
+static void
+gpiodev_name_foreach(void (*fn)(const char *name, void *opaque),
+                     void *opaque)
+{
+    GpiodevClassFE fe = { .fn = fn, .opaque = opaque };
+
+    object_class_foreach(gpiodev_class_foreach, TYPE_GPIODEV, false, &fe);
+}
+
+static void
+help_string_append(const char *name, void *opaque)
+{
+    GString *str = opaque;
+
+    g_string_append_printf(str, "\n  %s", name);
+}
+
+Gpiodev *qemu_gpiodev_add(QemuOpts *opts, GMainContext *context,
+                          Error **errp)
+{
+    const char *id = qemu_opts_id(opts);
+    const char *name = qemu_opt_get(opts, "backend");
+
+    if (name && is_help_option(name)) {
+        GString *str = g_string_new("");
+
+        gpiodev_name_foreach(help_string_append, str);
+
+        qemu_printf("Available chardev backend types: %s\n", str->str);
+        g_string_free(str, true);
+        return NULL;
+    }
+
+    if (id == NULL) {
+        error_setg(errp, "gpiodev: no id specified");
+        return NULL;
+    }
+
+    return qemu_gpiodev_new(id, context, errp);
+}
+
+static QemuOptsList qemu_gpiodev_opts = {
+    .name = "gpiodev",
+    .implied_opt_name = "backend",
+    .head = QTAILQ_HEAD_INITIALIZER(qemu_gpiodev_opts.head),
+    .desc = {
+        {
+            .name = "backend",
+            .type = QEMU_OPT_STRING,
+        },
+        { /* end of list */ }
+    },
+};
+
+static void gpiodev_register_config(void)
+{
+    qemu_add_opts(&qemu_gpiodev_opts);
+}
+
+opts_init(gpiodev_register_config);
diff --git a/gpiodev/meson.build b/gpiodev/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..05aeb266bce8d905753d90d917b5f8b2d265a0cf
--- /dev/null
+++ b/gpiodev/meson.build
@@ -0,0 +1,5 @@
+gpiodev_ss.add(files(
+  'gpio.c',
+))
+
+gpiodev_ss = gpiodev_ss.apply({})
diff --git a/include/gpiodev/gpio.h b/include/gpiodev/gpio.h
new file mode 100644
index 0000000000000000000000000000000000000000..aca0a6090bb5264e1646fa5facace2d8f7cc47fd
--- /dev/null
+++ b/include/gpiodev/gpio.h
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * QEMU GPIO device.
+ *
+ * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
+ *
+ */
+#ifndef QEMU_GPIO_H
+#define QEMU_GPIO_H
+
+#include "qom/object.h"
+
+/* gpio back-end device */
+typedef struct GpioBackend GpioBackend;
+
+struct Gpiodev {
+    Object parent_obj;
+
+    GpioBackend *be;
+
+    GMainContext *gcontext;
+};
+
+struct GpiodevClass {
+    ObjectClass parent_class;
+};
+
+#define TYPE_GPIODEV "gpiodev"
+OBJECT_DECLARE_TYPE(Gpiodev, GpiodevClass, GPIODEV)
+
+Gpiodev *qemu_gpiodev_add(QemuOpts *opts, GMainContext *context,
+                          Error **errp);
+
+#endif /* QEMU_GPIO_H */
diff --git a/meson.build b/meson.build
index 8b9fda4d95e634e6faa8db5604e5e0ddc4b1eb9e..9daac5783bcf459a3ee6ff1e999582bf4e9a47cc 100644
--- a/meson.build
+++ b/meson.build
@@ -3646,6 +3646,7 @@ block_ss = ss.source_set()
 chardev_ss = ss.source_set()
 common_ss = ss.source_set()
 crypto_ss = ss.source_set()
+gpiodev_ss = ss.source_set()
 hwcore_ss = ss.source_set()
 io_ss = ss.source_set()
 qmp_ss = ss.source_set()
@@ -3735,6 +3736,7 @@ subdir('io')
 subdir('chardev')
 subdir('fsdev')
 subdir('dump')
+subdir('gpiodev')
 
 if have_block
   block_ss.add(files(
@@ -4023,11 +4025,18 @@ libhwcore = static_library('hwcore', sources: hwcore_ss.sources() + genh,
 hwcore = declare_dependency(objects: libhwcore.extract_all_objects(recursive: false))
 common_ss.add(hwcore)
 
+libgpiodev = static_library('gpiodev', gpiodev_ss.sources() + genh,
+                            dependencies: gpiodev_ss.dependencies(),
+                            build_by_default: false)
+
+gpiodev = declare_dependency(objects: libgpiodev.extract_all_objects(recursive: false),
+                             dependencies: gpiodev_ss.dependencies())
+
 ###########
 # Targets #
 ###########
 
-system_ss.add(authz, blockdev, chardev, crypto, io, qmp)
+system_ss.add(authz, blockdev, chardev, crypto, gpiodev, io, qmp)
 common_ss.add(qom, qemuutil)
 
 common_ss.add_all(when: 'CONFIG_SYSTEM_ONLY', if_true: [system_ss])
diff --git a/qemu-options.hx b/qemu-options.hx
index dc694a99a30a7d1ef1567005f59ece88d13f3455..97d293c7d3276a5835c40bb9b26e5f0c8c2a2d28 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -4081,6 +4081,15 @@ ERST
 
 DEFHEADING()
 
+DEFHEADING(GPIO device options:)
+
+DEF("gpiodev", HAS_ARG, QEMU_OPTION_gpiodev,
+    "-gpiodev help\n"
+    , QEMU_ARCH_ALL
+)
+
+DEFHEADING()
+
 #ifdef CONFIG_TPM
 DEFHEADING(TPM device options:)
 
diff --git a/system/vl.c b/system/vl.c
index 04f78466c412a2f9e38c0b187c45d09d8df873ce..67e266a93d0d53c2a7cbd49f8831ae85702c8b42 100644
--- a/system/vl.c
+++ b/system/vl.c
@@ -73,6 +73,7 @@
 #include "gdbstub/enums.h"
 #include "qemu/timer.h"
 #include "chardev/char.h"
+#include "gpiodev/gpio.h"
 #include "qemu/bitmap.h"
 #include "qemu/log.h"
 #include "system/blockdev.h"
@@ -1230,6 +1231,20 @@ static int chardev_init_func(void *opaque, QemuOpts *opts, Error **errp)
     return 0;
 }
 
+static int gpiodev_init_func(void *opaque, QemuOpts *opts, Error **errp)
+{
+    Error *local_err = NULL;
+
+    if (!qemu_gpiodev_add(opts, NULL, &local_err)) {
+        if (local_err) {
+            error_propagate(errp, local_err);
+            return -1;
+        }
+        exit(0);
+    }
+    return 0;
+}
+
 #ifdef CONFIG_VIRTFS
 static int fsdev_init_func(void *opaque, QemuOpts *opts, Error **errp)
 {
@@ -2053,6 +2068,9 @@ static void qemu_create_early_backends(void)
     qemu_opts_foreach(qemu_find_opts("chardev"),
                       chardev_init_func, NULL, &error_fatal);
 
+    qemu_opts_foreach(qemu_find_opts("gpiodev"),
+                      gpiodev_init_func, NULL, &error_fatal);
+
 #ifdef CONFIG_VIRTFS
     qemu_opts_foreach(qemu_find_opts("fsdev"),
                       fsdev_init_func, NULL, &error_fatal);
@@ -3241,6 +3259,13 @@ void qemu_init(int argc, char **argv)
                     exit(1);
                 }
                 break;
+            case QEMU_OPTION_gpiodev:
+                opts = qemu_opts_parse_noisily(qemu_find_opts("gpiodev"),
+                                               optarg, true);
+                if (!opts) {
+                    exit(1);
+                }
+                break;
             case QEMU_OPTION_fsdev:
                 olist = qemu_find_opts("fsdev");
                 if (!olist) {
