diff --git a/repos/dde_linux/src/include/lx_emul/pin.h b/repos/dde_linux/src/include/lx_emul/pin.h
new file mode 100644
index 0000000000..74bb5dbb43
--- /dev/null
+++ b/repos/dde_linux/src/include/lx_emul/pin.h
@@ -0,0 +1,50 @@
+/*
+ * \brief Lx_emul support for accessing GPIO pins
+ * \author Norman Feske
+ * \date 2021-11-02
+ */
+
+/*
+ * Copyright (C) 2021 Genode Labs GmbH
+ *
+ * This file is distributed under the terms of the GNU General Public License
+ * version 2.
+ */
+
+#ifndef _LX_EMUL__PIN_H_
+#define _LX_EMUL__PIN_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Set output state of GPIO pin
+ *
+ * \pin_name GPIO name used as label for corresponding 'Pin_control' session
+ */
+void lx_emul_pin_control(char const *pin_name, bool enabled);
+
+/**
+ * Request interrupt backed by an IRQ session
+ */
+void lx_emul_pin_irq_unmask(unsigned gic_irq, unsigned pin_irq,
+ char const *pin_name);
+
+/**
+ * Return pin IRQ number of most recently occurred pin interrupt
+ *
+ * This function is meant to be called by the PIO driver's interrupt handler.
+ */
+unsigned lx_emul_pin_last_irq(void);
+
+/**
+ * Acknowledge GPIO interrupt
+ */
+void lx_emul_pin_irq_ack(unsigned pin_irq);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _LX_EMUL__PIN_H_ */
diff --git a/repos/dde_linux/src/lib/lx_emul/pin.cc b/repos/dde_linux/src/lib/lx_emul/pin.cc
new file mode 100644
index 0000000000..7fb683dbfd
--- /dev/null
+++ b/repos/dde_linux/src/lib/lx_emul/pin.cc
@@ -0,0 +1,231 @@
+/*
+ * \brief Lx_emul backend for accessing GPIO pins
+ * \author Norman Feske
+ * \date 2021-11-02
+ */
+
+/*
+ * Copyright (C) 2021 Genode Labs GmbH
+ *
+ * This file is distributed under the terms of the GNU General Public License
+ * version 2.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+
+#include
+#include
+
+namespace {
+
+ using namespace Genode;
+
+ class Global_irq_controller : Noncopyable
+ {
+ private:
+
+ Lx_kit::Env &_env;
+
+ public:
+
+ struct Number { unsigned value; };
+
+ Global_irq_controller(Lx_kit::Env &env) : _env(env) { }
+
+ void trigger_irq(Number number)
+ {
+ /*
+ * Mirrored from 'Lx_kit::Device::Irq::handle'
+ */
+ _env.last_irq = number.value;
+ _env.scheduler.unblock_irq_handler();
+ _env.scheduler.schedule();
+ }
+ };
+
+ using Pin_name = Session_label;
+ using Gic_irq_number = Global_irq_controller::Number;
+
+ struct Pin_irq_number { unsigned value; };
+
+ struct Irq_info
+ {
+ Gic_irq_number gic_irq_number;
+ Pin_irq_number pin_irq_number;
+ };
+
+ struct Pin_irq_handler : Interface
+ {
+ virtual void handle_pin_irq(Irq_info) = 0;
+ };
+
+ struct Pin : Interface
+ {
+ using Name = Session_label;
+
+ Env &_env;
+
+ Pin_irq_handler &_pin_irq_handler;
+
+ Irq_info _irq_info { };
+
+ Name const name;
+
+ Constructible _control { };
+ Constructible _irq { };
+
+ Io_signal_handler _irq_handler { _env.ep(), *this, &Pin::_handle_irq };
+
+ void _handle_irq()
+ {
+ _pin_irq_handler.handle_pin_irq(_irq_info);
+ }
+
+ Pin(Env &env, Name const &name, Pin_irq_handler &pin_irq_handler)
+ :
+ _env(env), _pin_irq_handler(pin_irq_handler), name(name)
+ { }
+
+ void control(bool enabled)
+ {
+ if (_irq.constructed()) {
+ error("attempt to drive interrupt pin ", name, " as output");
+ return;
+ }
+
+ if (!_control.constructed())
+ _control.construct(_env, name.string());
+
+ _control->state(enabled);
+ }
+
+ void associate_with_gic_and_unmask_irq(Irq_info irq_info)
+ {
+ _control.destruct();
+
+ if (!_irq.constructed()) {
+ _irq_info = irq_info;
+ _irq.construct(_env, _irq_info.pin_irq_number.value);
+ _irq->sigh(_irq_handler);
+ _irq->ack_irq();
+ }
+ }
+
+ void ack_matching_irq(Pin_irq_number ack_pin)
+ {
+ if (ack_pin.value != _irq_info.pin_irq_number.value)
+ return;
+
+ if (_irq.constructed())
+ _irq->ack_irq();
+ }
+ };
+
+ struct Pins : private Pin_irq_handler
+ {
+ Env &_env;
+ Allocator &_alloc;
+
+ Global_irq_controller &_gic;
+
+ Registry > _registry { };
+
+ Pin_irq_number last_irq { };
+
+ Pins(Env &env, Allocator &alloc, Global_irq_controller &gic)
+ :
+ _env(env), _alloc(alloc), _gic(gic)
+ { }
+
+ template
+ void with_pin(Pin::Name const &name, FN const &fn)
+ {
+
+ Pin_irq_handler &pin_irq_handler = *this;
+
+ /*
+ * Construct 'Pin' object on demand, apply 'fn' if constructed
+ */
+ for (unsigned i = 0; i < 2; i++) {
+
+ bool done = false;
+ _registry.for_each([&] (Pin &pin) {
+ if (pin.name == name) {
+ fn(pin);
+ done = true;
+ }
+ });
+ if (done)
+ break;
+
+ new (_alloc) Registered(_registry, _env, name, pin_irq_handler);
+
+ /* ... apply 'fn' in second iteration */
+ }
+ }
+
+ void handle_pin_irq(Irq_info irq_info) override
+ {
+ last_irq = irq_info.pin_irq_number;
+
+ _gic.trigger_irq(irq_info.gic_irq_number);
+ }
+
+ void irq_ack(Pin_irq_number ack_pin_number)
+ {
+ _registry.for_each([&] (Pin &pin) {
+ pin.ack_matching_irq(ack_pin_number); });
+ }
+ };
+};
+
+
+static Pins &pins()
+{
+ static Global_irq_controller gic { Lx_kit::env() };
+
+ static Pins pins { Lx_kit::env().env, Lx_kit::env().heap, gic };
+
+ return pins;
+}
+
+
+extern "C" void lx_emul_pin_control(char const *pin_name, bool enabled)
+{
+ pins().with_pin(pin_name, [&] (Pin &pin) {
+ pin.control(enabled); });
+}
+
+
+extern "C" void lx_emul_pin_irq_unmask(unsigned gic_irq, unsigned pin_irq,
+ char const *pin_name)
+{
+ /*
+ * Translate GIC IRQ number as known by the Linux kernel into the
+ * physical IRQ number expected by 'Lx_kit::Env::last_irq'.
+ */
+ gic_irq += 32;
+
+ pins().with_pin(pin_name, [&] (Pin &pin) {
+
+ Irq_info const irq_info { .gic_irq_number = { gic_irq },
+ .pin_irq_number = { pin_irq } };
+
+ pin.associate_with_gic_and_unmask_irq(irq_info);
+ });
+}
+
+
+extern "C" void lx_emul_pin_irq_ack(unsigned pin_irq)
+{
+ pins().irq_ack( Pin_irq_number { pin_irq } );
+}
+
+
+extern "C" unsigned lx_emul_pin_last_irq(void)
+{
+ return pins().last_irq.value;
+}