diff --git a/nmigen_soc/periph.py b/nmigen_soc/periph.py
new file mode 100644
index 0000000..6a8ad36
--- /dev/null
+++ b/nmigen_soc/periph.py
@@ -0,0 +1,59 @@
+from .memory import MemoryMap
+from . import event
+
+
+__all__ = ["PeripheralInfo"]
+
+
+class PeripheralInfo:
+    """Peripheral metadata.
+
+    A unified description of the local resources of a peripheral. It may be queried in order to
+    recover its memory windows, CSR registers and event sources.
+
+    Parameters
+    ----------
+    memory_map : :class:`MemoryMap`
+        Memory map of the peripheral.
+    irq : :class:`event.Source`
+        IRQ line of the peripheral. Optional.
+    """
+    def __init__(self, *, memory_map, irq=None):
+        if not isinstance(memory_map, MemoryMap):
+            raise TypeError("Memory map must be an instance of MemoryMap, not {!r}"
+                            .format(memory_map))
+        memory_map.freeze()
+        self._memory_map = memory_map
+
+        if irq is not None and not isinstance(irq, event.Source):
+            raise TypeError("IRQ line must be an instance of event.Source, not {!r}"
+                            .format(irq))
+        self._irq = irq
+
+    @property
+    def memory_map(self):
+        """Memory map.
+
+        Return value
+        ------------
+        A :class:`MemoryMap` describing the local address space of the peripheral.
+        """
+        return self._memory_map
+
+    @property
+    def irq(self):
+        """IRQ line.
+
+        Return value
+        ------------
+        An :class:`event.Source` used by the peripheral to request interrupts. If provided, its
+        event map describes local events.
+
+        Exceptions
+        ----------
+        Raises :exn:`NotImplementedError` if the peripheral info does not have an IRQ line.
+        """
+        if self._irq is None:
+            raise NotImplementedError("Peripheral info does not have an IRQ line"
+                                      .format(self))
+        return self._irq
diff --git a/nmigen_soc/test/test_periph.py b/nmigen_soc/test/test_periph.py
new file mode 100644
index 0000000..5499181
--- /dev/null
+++ b/nmigen_soc/test/test_periph.py
@@ -0,0 +1,50 @@
+import unittest
+
+from ..periph import PeripheralInfo
+from ..memory import MemoryMap
+from .. import event
+
+
+class PeripheralInfoTestCase(unittest.TestCase):
+    def test_memory_map(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        info = PeripheralInfo(memory_map=memory_map)
+        self.assertIs(info.memory_map, memory_map)
+
+    def test_memory_map_frozen(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        info = PeripheralInfo(memory_map=memory_map)
+        with self.assertRaisesRegex(ValueError,
+                r"Memory map has been frozen. Address width cannot be extended further"):
+            memory_map.add_resource("a", size=3, extend=True)
+
+    def test_memory_map_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Memory map must be an instance of MemoryMap, not 'foo'"):
+            info = PeripheralInfo(memory_map="foo")
+
+    def test_irq(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        irq = event.Source()
+        info = PeripheralInfo(memory_map=memory_map, irq=irq)
+        self.assertIs(info.irq, irq)
+
+    def test_irq_none(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        info = PeripheralInfo(memory_map=memory_map, irq=None)
+        with self.assertRaisesRegex(NotImplementedError,
+                r"Peripheral info does not have an IRQ line"):
+            info.irq
+
+    def test_irq_default(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        info = PeripheralInfo(memory_map=memory_map)
+        with self.assertRaisesRegex(NotImplementedError,
+                r"Peripheral info does not have an IRQ line"):
+            info.irq
+
+    def test_irq_wrong(self):
+        memory_map = MemoryMap(addr_width=1, data_width=8)
+        with self.assertRaisesRegex(TypeError,
+                r"IRQ line must be an instance of event.Source, not 'foo'"):
+            info = PeripheralInfo(memory_map=memory_map, irq="foo")