From 4cd6f94c9bd5a731c3526610382c88c059e429cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Nguyen?= <jf@jfng.fr>
Date: Tue, 5 Dec 2023 13:12:05 +0100
Subject: [PATCH] Add annotations for memory maps, wishbone and CSR primitives.

---
 amaranth_soc/csr/bus.py      | 177 ++++++++++++++++++++-
 amaranth_soc/memory.py       | 199 ++++++++++++++++++++++--
 amaranth_soc/wishbone/bus.py | 109 ++++++++++++-
 tests/test_csr_bus.py        |  81 ++++++++++
 tests/test_memory.py         | 290 ++++++++++++++++++++++++++++-------
 tests/test_wishbone_bus.py   |  66 ++++++++
 6 files changed, 856 insertions(+), 66 deletions(-)

diff --git a/amaranth_soc/csr/bus.py b/amaranth_soc/csr/bus.py
index bf00cce..bb4919a 100644
--- a/amaranth_soc/csr/bus.py
+++ b/amaranth_soc/csr/bus.py
@@ -1,13 +1,13 @@
 from collections import defaultdict
 from amaranth import *
-from amaranth.lib import enum, wiring
+from amaranth.lib import enum, wiring, meta
 from amaranth.lib.wiring import In, Out, flipped
 from amaranth.utils import ceil_log2
 
 from ..memory import MemoryMap
 
 
-__all__ = ["Element", "Signature", "Interface", "Decoder", "Multiplexer"]
+__all__ = ["Element", "Signature", "Annotation", "Interface", "Decoder", "Multiplexer"]
 
 
 class Element(wiring.PureInterface):
@@ -113,6 +113,32 @@ def create(self, *, path=None, src_loc_at=0):
             """
             return Element(self.width, self.access, path=path, src_loc_at=1 + src_loc_at)
 
+        def annotations(self, element, /):
+            """Get annotations of a compatible CSR element.
+
+            Parameters
+            ----------
+            element : :class:`Element`
+                A CSR element compatible with this signature.
+
+            Returns
+            -------
+            iterator of :class:`meta.Annotation`
+                Annotations attached to ``element``.
+
+            Raises
+            ------
+            :exc:`TypeError`
+                If ``element`` is not an :class:`Element` object.
+            :exc:`ValueError`
+                If ``element.signature`` is not equal to ``self``.
+            """
+            if not isinstance(element, Element):
+                raise TypeError(f"Element must be a csr.Element object, not {element!r}")
+            if element.signature != self:
+                raise ValueError(f"Element signature is not equal to this signature")
+            return (*super().annotations(element), Element.Annotation(element.signature))
+
         def __eq__(self, other):
             """Compare signatures.
 
@@ -125,6 +151,64 @@ def __eq__(self, other):
         def __repr__(self):
             return f"csr.Element.Signature({self.members!r})"
 
+    class Annotation(meta.Annotation):
+        schema = {
+            "$schema": "https://json-schema.org/draft/2020-12/schema",
+            "$id": "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/element.json",
+            "type": "object",
+            "properties": {
+                "width": {
+                    "type": "integer",
+                    "minimum": 0,
+                },
+                "access": {
+                    "enum": ["r", "w", "rw"],
+                },
+            },
+            "additionalProperties": False,
+            "required": [
+                "width",
+                "access",
+            ],
+        }
+
+        """Peripheral-side CSR signature annotation.
+
+        Parameters
+        ----------
+        origin : :class:`Element.Signature`
+            The signature described by this annotation instance.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If ``origin`` is not a :class:`Element.Signature`.
+        """
+        def __init__(self, origin):
+            if not isinstance(origin, Element.Signature):
+                raise TypeError(f"Origin must be a csr.Element.Signature object, not {origin!r}")
+            self._origin = origin
+
+        @property
+        def origin(self):
+            return self._origin
+
+        def as_json(self):
+            """Translate to JSON.
+
+            Returns
+            -------
+            :class:`dict`
+                A JSON representation of :attr:`~Element.Annotation.origin`, describing its width
+                and access mode.
+            """
+            instance = {
+                "width": self.origin.width,
+                "access": self.origin.access.value,
+            }
+            self.validate(instance)
+            return instance
+
     """Peripheral-side CSR interface.
 
     A low-level interface to a single atomically readable and writable register in a peripheral.
@@ -244,6 +328,35 @@ def create(self, *, path=None, src_loc_at=0):
         return Interface(addr_width=self.addr_width, data_width=self.data_width,
                          path=path, src_loc_at=1 + src_loc_at)
 
+    def annotations(self, interface, /):
+        """Get annotations of a compatible CSR bus interface.
+
+        Parameters
+        ----------
+        interface : :class:`Interface`
+            A CSR bus interface compatible with this signature.
+
+        Returns
+        -------
+        iterator of :class:`meta.Annotation`
+            Annotations attached to ``interface``.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If ``interface`` is not an :class:`Interface` object.
+        :exc:`ValueError`
+            If ``interface.signature`` is not equal to ``self``.
+        """
+        if not isinstance(interface, Interface):
+            raise TypeError(f"Interface must be a csr.Interface object, not {interface!r}")
+        if interface.signature != self:
+            raise ValueError(f"Interface signature is not equal to this signature")
+        annotations = [*super().annotations(interface), Annotation(interface.signature)]
+        if interface._memory_map is not None:
+            annotations.append(interface._memory_map.annotation)
+        return annotations
+
     def __eq__(self, other):
         """Compare signatures.
 
@@ -257,6 +370,66 @@ def __repr__(self):
         return f"csr.Signature({self.members!r})"
 
 
+class Annotation(meta.Annotation):
+    schema = {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "$id": "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/bus.json",
+        "type": "object",
+        "properties": {
+            "addr_width": {
+                "type": "integer",
+                "minimum": 0,
+            },
+            "data_width": {
+                "type": "integer",
+                "minimum": 0,
+            },
+        },
+        "additionalProperties": False,
+        "required": [
+            "addr_width",
+            "data_width",
+        ],
+    }
+
+    """CPU-side CSR signature annotation.
+
+    Parameters
+    ----------
+    origin : :class:`Signature`
+        The signature described by this annotation instance.
+
+    Raises
+    ------
+    :exc:`TypeError`
+        If ``origin`` is not a :class:`Signature`.
+    """
+    def __init__(self, origin):
+        if not isinstance(origin, Signature):
+            raise TypeError(f"Origin must be a csr.Signature object, not {origin!r}")
+        self._origin = origin
+
+    @property
+    def origin(self):
+        return self._origin
+
+    def as_json(self):
+        """Translate to JSON.
+
+        Returns
+        -------
+        :class:`dict`
+            A JSON representation of :attr:`~Annotation.origin`, describing its address width
+            and data width.
+        """
+        instance = {
+            "addr_width": self.origin.addr_width,
+            "data_width": self.origin.data_width,
+        }
+        self.validate(instance)
+        return instance
+
+
 class Interface(wiring.PureInterface):
     """CPU-side CSR interface.
 
diff --git a/amaranth_soc/memory.py b/amaranth_soc/memory.py
index 87da2a0..d4f8894 100644
--- a/amaranth_soc/memory.py
+++ b/amaranth_soc/memory.py
@@ -1,9 +1,10 @@
 import bisect
 
+from amaranth.lib import wiring, meta
 from amaranth.utils import bits_for
 
 
-__all__ = ["ResourceInfo", "MemoryMap"]
+__all__ = ["ResourceInfo", "MemoryMap", "MemoryMapAnnotation"]
 
 
 class _RangeMap:
@@ -166,6 +167,10 @@ def __init__(self, *, addr_width, data_width, alignment=0, name=None):
         self._next_addr  = 0
         self._frozen     = False
 
+    @property
+    def annotation(self):
+        return MemoryMapAnnotation(self)
+
     @property
     def addr_width(self):
         return self._addr_width
@@ -264,7 +269,8 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
         Arguments
         ---------
         resource : object
-            Arbitrary object representing a resource.
+            Arbitrary object representing a resource. It must have a 'signature' attribute that is
+            a :class:`wiring.Signature` object.
         name : str
             Name of the resource. It must not collide with the name of other resources or windows
             present in this memory map.
@@ -284,18 +290,30 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
 
         Exceptions
         ----------
-        Raises :exn:`ValueError` if one of the following occurs:
-
-        - this memory map is frozen;
-        - the requested address and size, after alignment, would overlap with any resources or
-        windows that have already been added, or would be out of bounds;
-        - the resource has already been added to this memory map;
-        - the name of the resource is already present in the namespace of this memory map;
+        :exc:`ValueError`
+            If the memory map is frozen.
+        :exc:`AttributeError`
+            If the resource does not have a 'signature' attribute.
+        :exc:`TypeError`
+            If the resource signature is not a :class:`wiring.Signature` object.
+        :exc:`ValueError`
+            If the requested address and size, after alignment, would overlap with any resources or
+            windows that have already been added, or would be out of bounds.
+        :exc:`ValueError`
+            If the resource has already been added to this memory map.
+        :exc:`ValueError`
+            If the name of the resource is already present in the namespace of this memory map.
         """
         if self._frozen:
             raise ValueError("Memory map has been frozen. Cannot add resource {!r}"
                              .format(resource))
 
+        if not hasattr(resource, "signature"):
+            raise AttributeError(f"Resource {resource!r} must have a 'signature' attribute")
+        if not isinstance(resource.signature, wiring.Signature):
+            raise TypeError(f"Signature of resource {resource!r} must be a wiring.Signature "
+                            f"object, not {resource.signature!r}")
+
         if id(resource) in self._resources:
             _, _, addr_range = self._resources[id(resource)]
             raise ValueError("Resource {!r} is already added at address range {:#x}..{:#x}"
@@ -579,3 +597,166 @@ def decode_address(self, address):
             return assignment.decode_address((address - addr_range.start) // addr_range.step)
         else:
             assert False # :nocov:
+
+
+class MemoryMapAnnotation(meta.Annotation):
+    schema = {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "$id": "https://amaranth-lang.org/schema/amaranth-soc/0.1/memory/memory-map.json",
+        "type": "object",
+        "properties": {
+            "name": {
+                "type": "string",
+            },
+            "addr_width": {
+                "type": "integer",
+                "minimum": 0,
+            },
+            "data_width": {
+                "type": "integer",
+                "minimum": 0,
+            },
+            "alignment": {
+                "type": "integer",
+                "minimum": 0,
+            },
+            "windows": {
+                "type": "array",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "start": {
+                            "type": "integer",
+                            "minimum": 0,
+                        },
+                        "end": {
+                            "type": "integer",
+                            "minimum": 0,
+                        },
+                        "ratio": {
+                            "type": "integer",
+                            "minimum": 1,
+                        },
+                        "annotations": {
+                            "type": "object",
+                            "patternProperties": {
+                                "^.+$": { "$ref": "#" },
+                            },
+                        },
+                    },
+                    "additionalProperties": False,
+                    "required": [
+                        "start",
+                        "end",
+                        "ratio",
+                        "annotations",
+                    ],
+                },
+            },
+            "resources": {
+                "type": "array",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "name": {
+                            "type": "string",
+                        },
+                        "start": {
+                            "type": "integer",
+                            "minimum": 0,
+                        },
+                        "end": {
+                            "type": "integer",
+                            "minimum": 0,
+                        },
+                        "annotations": {
+                            "type": "object",
+                            "patternProperties": {
+                                "^.+$": { "type": "object" },
+                            },
+                        },
+                    },
+                    "additionalProperties": False,
+                    "required": [
+                        "name",
+                        "start",
+                        "end",
+                        "annotations",
+                    ],
+                },
+            },
+        },
+        "additionalProperties": False,
+        "required": [
+            "addr_width",
+            "data_width",
+            "alignment",
+            "windows",
+            "resources",
+        ],
+    }
+
+    """Memory map annotation.
+
+    Parameters
+    ----------
+    origin : :class:`MemoryMap`
+        The memory map described by this annotation instance. It is frozen as a side-effect of
+        this instantiation.
+
+    Raises
+    ------
+    :exc:`TypeError`
+        If ``origin`` is not a :class:`MemoryMap`.
+    """
+    def __init__(self, origin):
+        if not isinstance(origin, MemoryMap):
+            raise TypeError(f"Origin must be a MemoryMap object, not {origin!r}")
+        origin.freeze()
+        self._origin = origin
+
+    @property
+    def origin(self):
+        return self._origin
+
+    def as_json(self):
+        """Translate to JSON.
+
+        Returns
+        -------
+        :class:`dict`
+            A JSON representation of :attr:`~MemoryMapAnnotation.origin`, describing its address width,
+            data width, address range alignment, and a hierarchical description of its local windows
+            and resources.
+        """
+        instance = {}
+        if self.origin.name is not None:
+            instance["name"] = self.origin.name
+        instance.update({
+            "addr_width": self.origin.addr_width,
+            "data_width": self.origin.data_width,
+            "alignment": self.origin.alignment,
+            "windows": [
+                {
+                    "start": start,
+                    "end": end,
+                    "ratio": ratio,
+                    "annotations": {
+                        window.annotation.schema["$id"]: window.annotation.as_json()
+                    },
+                } for window, (start, end, ratio) in self.origin.windows()
+            ],
+            "resources": [
+                {
+                    "name": name,
+                    "start": start,
+                    "end": end,
+                    "annotations": {
+                        annotation.schema["$id"]: annotation.as_json()
+                        for annotation in resource.signature.annotations(resource)
+                    },
+                } for resource, name, (start, end) in self.origin.resources()
+            ],
+        })
+        self.validate(instance)
+        return instance
diff --git a/amaranth_soc/wishbone/bus.py b/amaranth_soc/wishbone/bus.py
index 617dace..d24d7fd 100644
--- a/amaranth_soc/wishbone/bus.py
+++ b/amaranth_soc/wishbone/bus.py
@@ -1,12 +1,15 @@
 from amaranth import *
-from amaranth.lib import enum, wiring
+from amaranth.lib import enum, wiring, meta
 from amaranth.lib.wiring import In, Out, flipped
 from amaranth.utils import exact_log2
 
 from ..memory import MemoryMap
 
 
-__all__ = ["CycleType", "BurstTypeExt", "Feature", "Signature", "Interface", "Decoder", "Arbiter"]
+__all__ = [
+    "CycleType", "BurstTypeExt", "Feature", "Signature", "Annotation", "Interface",
+    "Decoder", "Arbiter"
+]
 
 
 class CycleType(enum.Enum):
@@ -192,6 +195,35 @@ def create(self, *, path=None, src_loc_at=0):
                          granularity=self.granularity, features=self.features,
                          path=path, src_loc_at=1 + src_loc_at)
 
+    def annotations(self, interface, /):
+        """Get annotations of a compatible Wishbone bus interface.
+
+        Parameters
+        ----------
+        interface : :class:`Interface`
+            A Wishbone bus interface compatible with this signature.
+
+        Returns
+        -------
+        iterator of :class:`meta.Annotation`
+            Annotations attached to ``interface``.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If ``interface`` is not an :class:`Interface` object.
+        :exc:`ValueError`
+            If ``interface.signature`` is not equal to ``self``.
+        """
+        if not isinstance(interface, Interface):
+            raise TypeError(f"Interface must be a wishbone.Interface object, not {interface!r}")
+        if interface.signature != self:
+            raise ValueError(f"Interface signature is not equal to this signature")
+        annotations = [*super().annotations(interface), Annotation(interface.signature)]
+        if interface._memory_map is not None:
+            annotations.append(interface._memory_map.annotation)
+        return annotations
+
     def __eq__(self, other):
         """Compare signatures.
 
@@ -208,6 +240,79 @@ def __repr__(self):
         return f"wishbone.Signature({self.members!r})"
 
 
+class Annotation(meta.Annotation):
+    schema = {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "$id": "https://amaranth-lang.org/schema/amaranth-soc/0.1/wishbone/bus.json",
+        "type": "object",
+        "properties": {
+            "addr_width": {
+                "type": "integer",
+                "minimum": 0,
+            },
+            "data_width": {
+                "enum": [8, 16, 32, 64],
+            },
+            "granularity": {
+                "enum": [8, 16, 32, 64],
+            },
+            "features": {
+                "type": "array",
+                "items": {
+                    "enum": ["err", "rty", "stall", "lock", "cti", "bte"],
+                },
+                "uniqueItems": True,
+            },
+        },
+        "additionalProperties": False,
+        "required": [
+            "addr_width",
+            "data_width",
+            "granularity",
+            "features",
+        ],
+    }
+
+    """Wishbone bus signature annotation.
+
+    Parameters
+    ----------
+    origin : :class:`Signature`
+        The signature described by this annotation instance.
+
+    Raises
+    ------
+    :exc:`TypeError`
+        If ``origin`` is not a :class:`Signature`.
+    """
+    def __init__(self, origin):
+        if not isinstance(origin, Signature):
+            raise TypeError(f"Origin must be a wishbone.Signature object, not {origin!r}")
+        self._origin = origin
+
+    @property
+    def origin(self):
+        return self._origin
+
+    def as_json(self):
+        """Translate to JSON.
+
+        Returns
+        -------
+        :class:`dict`
+            A JSON representation of :attr:`~Annotation.origin`, describing its address width,
+            data width, granularity and features.
+        """
+        instance = {
+            "addr_width": self.origin.addr_width,
+            "data_width": self.origin.data_width,
+            "granularity": self.origin.granularity,
+            "features": sorted(feature.value for feature in self.origin.features),
+        }
+        self.validate(instance)
+        return instance
+
+
 class Interface(wiring.PureInterface):
     """Wishbone bus interface.
 
diff --git a/tests/test_csr_bus.py b/tests/test_csr_bus.py
index 9953779..8e8f8fa 100644
--- a/tests/test_csr_bus.py
+++ b/tests/test_csr_bus.py
@@ -59,6 +59,13 @@ def test_create(self):
         self.assertEqual(elem.r_stb.name, "foo__bar__r_stb")
         self.assertEqual(elem.signature, sig)
 
+    def test_annotations(self):
+        sig  = csr.Element.Signature(8, csr.Element.Access.RW)
+        elem = sig.create()
+        self.assertEqual([a.as_json() for a in sig.annotations(elem)], [
+            { "width": 8, "access": "rw" },
+        ]),
+
     def test_eq(self):
         self.assertEqual(csr.Element.Signature(8, "r"), csr.Element.Signature(8, "r"))
         self.assertEqual(csr.Element.Signature(8, "r"),
@@ -80,6 +87,35 @@ def test_wrong_access(self):
                 r"'wo' is not a valid Element.Access"):
             csr.Element.Signature(width=1, access="wo")
 
+    def test_annotations_wrong_type(self):
+        sig = csr.Element.Signature(8, "rw")
+        with self.assertRaisesRegex(TypeError,
+                r"Element must be a csr\.Element object, not 'foo'"):
+            sig.annotations("foo")
+
+    def test_annotations_incompatible(self):
+        sig1 = csr.Element.Signature(8, "rw")
+        elem = sig1.create()
+        sig2 = csr.Element.Signature(4, "rw")
+        with self.assertRaisesRegex(ValueError,
+                r"Element signature is not equal to this signature"):
+            sig2.annotations(elem)
+
+
+class ElementAnnotationTestCase(unittest.TestCase):
+    def test_as_json(self):
+        sig = csr.Element.Signature(8, access="rw")
+        annotation = csr.Element.Annotation(sig)
+        self.assertEqual(annotation.as_json(), {
+            "width": 8,
+            "access": "rw",
+        })
+
+    def test_wrong_origin(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Origin must be a csr.Element.Signature object, not 'foo'"):
+            csr.Element.Annotation("foo")
+
 
 class ElementTestCase(unittest.TestCase):
     def test_simple(self):
@@ -111,6 +147,22 @@ def test_create(self):
         self.assertEqual(iface.r_stb.name, "foo__bar__r_stb")
         self.assertEqual(iface.signature, sig)
 
+    def test_annotations(self):
+        sig = csr.Signature(addr_width=16, data_width=8)
+        iface = sig.create()
+        self.assertEqual([a.as_json() for a in sig.annotations(iface)], [
+            { "addr_width": 16, "data_width": 8 },
+        ])
+
+    def test_annotations_memory_map(self):
+        sig = csr.Signature(addr_width=16, data_width=8)
+        iface = sig.create()
+        iface.memory_map = MemoryMap(addr_width=16, data_width=8)
+        self.assertEqual([a.as_json() for a in sig.annotations(iface)], [
+            { "addr_width": 16, "data_width": 8 },
+            { "addr_width": 16, "data_width": 8, "alignment": 0, "windows": [], "resources": [] }
+        ])
+
     def test_eq(self):
         self.assertEqual(csr.Signature(addr_width=32, data_width=8),
                          csr.Signature(addr_width=32, data_width=8))
@@ -134,6 +186,35 @@ def test_wrong_data_width(self):
                 r"Data width must be a positive integer, not -1"):
             csr.Signature.check_parameters(addr_width=16, data_width=-1)
 
+    def test_annotations_wrong_type(self):
+        sig = csr.Signature(addr_width=8, data_width=8)
+        with self.assertRaisesRegex(TypeError,
+                r"Interface must be a csr\.Interface object, not 'foo'"):
+            sig.annotations("foo")
+
+    def test_annotations_incompatible(self):
+        sig1  = csr.Signature(addr_width=8, data_width=8)
+        iface = sig1.create()
+        sig2  = csr.Signature(addr_width=4, data_width=8)
+        with self.assertRaisesRegex(ValueError,
+                r"Interface signature is not equal to this signature"):
+            sig2.annotations(iface)
+
+
+class AnnotationTestCase(unittest.TestCase):
+    def test_as_json(self):
+        sig = csr.Signature(addr_width=16, data_width=8)
+        annotation = csr.Annotation(sig)
+        self.assertEqual(annotation.as_json(), {
+            "addr_width": 16,
+            "data_width": 8,
+        })
+
+    def test_wrong_origin(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Origin must be a csr.Signature object, not 'foo'"):
+            csr.Annotation("foo")
+
 
 class InterfaceTestCase(unittest.TestCase):
     def test_simple(self):
diff --git a/tests/test_memory.py b/tests/test_memory.py
index 790bcf6..c90e26b 100644
--- a/tests/test_memory.py
+++ b/tests/test_memory.py
@@ -1,6 +1,20 @@
+# amaranth: UnusedElaboratable=no
+
 import unittest
+from types import SimpleNamespace
+from amaranth.lib import wiring
+
+from amaranth_soc.memory import _RangeMap, ResourceInfo, MemoryMap, MemoryMapAnnotation
+from amaranth_soc import csr
+
+
+class _MockResource(wiring.PureInterface):
+    def __init__(self, name):
+        super().__init__(wiring.Signature({}))
+        self._name = name
 
-from amaranth_soc.memory import _RangeMap, ResourceInfo, MemoryMap
+    def __repr__(self):
+        return f"_MockResource({self._name!r})"
 
 
 class RangeMapTestCase(unittest.TestCase):
@@ -118,114 +132,158 @@ def test_wrong_alignment(self):
 
     def test_add_resource(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1), (0, 1))
-        self.assertEqual(memory_map.add_resource(resource="b", name="bar", size=2), (1, 3))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1), (0, 1))
+        self.assertEqual(memory_map.add_resource(resource=res2, name="bar", size=2), (1, 3))
 
     def test_add_resource_map_aligned(self):
         memory_map = MemoryMap(addr_width=16, data_width=8, alignment=1)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1), (0, 2))
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=2), (2, 4))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1), (0, 2))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=2), (2, 4))
 
     def test_add_resource_explicit_aligned(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1), (0, 1))
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=1, alignment=1), (2, 4))
-        self.assertEqual(memory_map.add_resource("c", name="baz", size=2), (4, 6))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        res3 = _MockResource("res3")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1), (0, 1))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=1, alignment=1), (2, 4))
+        self.assertEqual(memory_map.add_resource(res3, name="baz", size=2), (4, 6))
 
     def test_add_resource_addr(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1, addr=10), (10, 11))
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=2), (11, 13))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1, addr=10), (10, 11))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=2), (11, 13))
 
     def test_add_resource_size_zero(self):
         memory_map = MemoryMap(addr_width=1, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=0), (0, 1))
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=0), (1, 2))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=0), (0, 1))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=0), (1, 2))
 
     def test_add_resource_wrong_frozen(self):
         memory_map = MemoryMap(addr_width=2, data_width=8)
         memory_map.freeze()
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
-                r"Memory map has been frozen. Cannot add resource 'a'"):
-            memory_map.add_resource("a", name="foo", size=0)
+                r"Memory map has been frozen. Cannot add resource _MockResource\('res'\)"):
+            memory_map.add_resource(res, name="foo", size=0)
+
+    def test_add_resource_wrong_signature_attr(self):
+        memory_map = MemoryMap(addr_width=2, data_width=8)
+        res = SimpleNamespace()
+        with self.assertRaisesRegex(AttributeError,
+                r"Resource namespace\(\) must have a 'signature' attribute"):
+            memory_map.add_resource(res, name="foo", size=0)
+
+    def test_add_resource_wrong_signature_type(self):
+        memory_map = MemoryMap(addr_width=2, data_width=8)
+        res = SimpleNamespace(signature=1)
+        with self.assertRaisesRegex(TypeError,
+                r"Signature of resource namespace\(signature=1\) must be a wiring.Signature "
+                r"object, not 1"):
+            memory_map.add_resource(res, name="foo", size=0)
 
     def test_add_resource_wrong_name(self):
         memory_map = MemoryMap(addr_width=1, data_width=8)
+        res = _MockResource("res")
         with self.assertRaisesRegex(TypeError, r"Name must be a non-empty string, not 1"):
-            memory_map.add_resource("a", name=1, size=0)
+            memory_map.add_resource(res, name=1, size=0)
         with self.assertRaisesRegex(TypeError, r"Name must be a non-empty string, not ''"):
-            memory_map.add_resource("a", name="", size=0)
+            memory_map.add_resource(res, name="", size=0)
 
     def test_add_resource_wrong_name_conflict(self):
         memory_map = MemoryMap(addr_width=1, data_width=8)
-        memory_map.add_resource("a", name="foo", size=0)
-        with self.assertRaisesRegex(ValueError, r"Name foo is already used by 'a'"):
-            memory_map.add_resource("b", name="foo", size=0)
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        memory_map.add_resource(res1, name="foo", size=0)
+        with self.assertRaisesRegex(ValueError,
+                r"Name foo is already used by _MockResource\('res1'\)"):
+            memory_map.add_resource(res2, name="foo", size=0)
 
     def test_add_resource_wrong_address(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
                 r"Address must be a non-negative integer, not -1"):
-            memory_map.add_resource("a", name="foo", size=1, addr=-1)
+            memory_map.add_resource(res, name="foo", size=1, addr=-1)
 
     def test_add_resource_wrong_address_unaligned(self):
         memory_map = MemoryMap(addr_width=16, data_width=8, alignment=1)
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
                 r"Explicitly specified address 0x1 must be a multiple of 0x2 bytes"):
-            memory_map.add_resource("a", name="foo", size=1, addr=1)
+            memory_map.add_resource(res, name="foo", size=1, addr=1)
 
     def test_add_resource_wrong_size(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
                 r"Size must be a non-negative integer, not -1"):
-            memory_map.add_resource("a", name="foo", size=-1)
+            memory_map.add_resource(res, name="foo", size=-1)
 
     def test_add_resource_wrong_alignment(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
                 r"Alignment must be a non-negative integer, not -1"):
-            memory_map.add_resource("a", name="foo", size=1, alignment=-1)
+            memory_map.add_resource(res, name="foo", size=1, alignment=-1)
 
     def test_add_resource_wrong_out_of_bounds(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
+        res = _MockResource("res")
         with self.assertRaisesRegex(ValueError,
                 r"Address range 0x10000\.\.0x10001 out of bounds for memory map spanning "
                 r"range 0x0\.\.0x10000 \(16 address bits\)"):
-            memory_map.add_resource("a", name="foo", addr=0x10000, size=1)
+            memory_map.add_resource(res, name="foo", addr=0x10000, size=1)
         with self.assertRaisesRegex(ValueError,
                 r"Address range 0x0\.\.0x10001 out of bounds for memory map spanning "
                 r"range 0x0\.\.0x10000 \(16 address bits\)"):
-            memory_map.add_resource("a", name="foo", size=0x10001)
+            memory_map.add_resource(res, name="foo", size=0x10001)
 
     def test_add_resource_wrong_overlap(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        memory_map.add_resource("a", name="foo", size=16)
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        memory_map.add_resource(res1, name="foo", size=16)
         with self.assertRaisesRegex(ValueError,
-                r"Address range 0xa\.\.0xb overlaps with resource 'a' at 0x0\.\.0x10"):
-            memory_map.add_resource("b", name="bar", size=1, addr=10)
+                r"Address range 0xa\.\.0xb overlaps with resource _MockResource\('res1'\) at "
+                r"0x0\.\.0x10"):
+            memory_map.add_resource(res2, name="bar", size=1, addr=10)
 
     def test_add_resource_wrong_twice(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        memory_map.add_resource("a", name="foo", size=16)
+        res = _MockResource("res")
+        memory_map.add_resource(res, name="foo", size=16)
         with self.assertRaisesRegex(ValueError,
-                r"Resource 'a' is already added at address range 0x0..0x10"):
-            memory_map.add_resource("a", name="bar", size=16)
+                r"Resource _MockResource\('res'\) is already added at address range 0x0..0x10"):
+            memory_map.add_resource(res, name="bar", size=16)
 
     def test_iter_resources(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        memory_map.add_resource("a", name="foo", size=1)
-        memory_map.add_resource("b", name="bar", size=2)
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        memory_map.add_resource(res1, name="foo", size=1)
+        memory_map.add_resource(res2, name="bar", size=2)
         self.assertEqual(list(memory_map.resources()), [
-            ("a", "foo", (0, 1)),
-            ("b", "bar", (1, 3)),
+            (res1, "foo", (0, 1)),
+            (res2, "bar", (1, 3)),
         ])
 
     def test_add_window(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1), (0, 1))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1), (0, 1))
         self.assertEqual(memory_map.add_window(MemoryMap(addr_width=10, data_width=8)),
                          (0x400, 0x800, 1))
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=1), (0x800, 0x801))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=1), (0x800, 0x801))
 
     def test_add_window_sparse(self):
         memory_map = MemoryMap(addr_width=16, data_width=32)
@@ -300,22 +358,28 @@ def test_add_window_wrong_twice(self):
 
     def test_add_window_wrong_name_conflict(self):
         memory_map = MemoryMap(addr_width=2, data_width=8)
-        memory_map.add_resource("a", name="foo", size=0)
+        res = _MockResource("res")
+        memory_map.add_resource(res, name="foo", size=0)
         window = MemoryMap(addr_width=1, data_width=8, name="foo")
-        with self.assertRaisesRegex(ValueError, r"Name foo is already used by 'a'"):
+        with self.assertRaisesRegex(ValueError,
+                r"Name foo is already used by _MockResource\('res'\)"):
             memory_map.add_window(window)
 
     def test_add_window_wrong_name_conflict_subordinate(self):
         memory_map = MemoryMap(addr_width=2, data_width=8)
-        memory_map.add_resource("a", name="foo", size=0)
-        memory_map.add_resource("b", name="bar", size=0)
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        res3 = _MockResource("res3")
+        res4 = _MockResource("res4")
+        memory_map.add_resource(res1, name="foo", size=0)
+        memory_map.add_resource(res2, name="bar", size=0)
         window = MemoryMap(addr_width=1, data_width=8, name=None)
-        window.add_resource("c", name="foo", size=0)
-        window.add_resource("d", name="bar", size=0)
+        window.add_resource(res3, name="foo", size=0)
+        window.add_resource(res4, name="bar", size=0)
         with self.assertRaisesRegex(ValueError,
                 r"The following names are already used: "
-                r"bar is used by 'b'; "
-                r"foo is used by 'a'"):
+                r"bar is used by _MockResource\('res2'\); "
+                r"foo is used by _MockResource\('res1'\)"):
             memory_map.add_window(window)
 
     def test_iter_windows(self):
@@ -350,9 +414,11 @@ def test_iter_window_patterns_covered(self):
 
     def test_align_to(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
-        self.assertEqual(memory_map.add_resource("a", name="foo", size=1), (0, 1))
+        res1 = _MockResource("res1")
+        res2 = _MockResource("res2")
+        self.assertEqual(memory_map.add_resource(res1, name="foo", size=1), (0, 1))
         self.assertEqual(memory_map.align_to(10), 0x400)
-        self.assertEqual(memory_map.add_resource("b", name="bar", size=16), (0x400, 0x410))
+        self.assertEqual(memory_map.add_resource(res2, name="bar", size=16), (0x400, 0x410))
 
     def test_align_to_wrong(self):
         memory_map = MemoryMap(addr_width=16, data_width=8)
@@ -364,22 +430,22 @@ def test_align_to_wrong(self):
 class MemoryMapDiscoveryTestCase(unittest.TestCase):
     def setUp(self):
         self.root = MemoryMap(addr_width=32, data_width=32)
-        self.res1 = "res1"
+        self.res1 = _MockResource("res1")
         self.root.add_resource(self.res1, name="name1", size=16)
         self.win1 = MemoryMap(addr_width=16, data_width=32)
-        self.res2 = "res2"
+        self.res2 = _MockResource("res2")
         self.win1.add_resource(self.res2, name="name2", size=32)
-        self.res3 = "res3"
+        self.res3 = _MockResource("res3")
         self.win1.add_resource(self.res3, name="name3", size=32)
         self.root.add_window(self.win1)
-        self.res4 = "res4"
+        self.res4 = _MockResource("res4")
         self.root.add_resource(self.res4, name="name4", size=1)
         self.win2 = MemoryMap(addr_width=16, data_width=8)
-        self.res5 = "res5"
+        self.res5 = _MockResource("res5")
         self.win2.add_resource(self.res5, name="name5", size=16)
         self.root.add_window(self.win2, sparse=True)
         self.win3 = MemoryMap(addr_width=16, data_width=8, name="win3")
-        self.res6 = "res6"
+        self.res6 = _MockResource("res6")
         self.win3.add_resource(self.res6, name="name6", size=16)
         self.root.add_window(self.win3, sparse=False)
 
@@ -443,3 +509,121 @@ def test_decode_address(self):
 
     def test_decode_address_missing(self):
         self.assertIsNone(self.root.decode_address(address=0x00000100))
+
+
+class MemoryMapAnnotationTestCase(unittest.TestCase):
+    def test_origin_freeze(self):
+        memory_map = MemoryMap(addr_width=2, data_width=8)
+        res = _MockResource("res")
+        MemoryMapAnnotation(memory_map)
+        with self.assertRaisesRegex(ValueError,
+                r"Memory map has been frozen. Cannot add resource _MockResource\('res'\)"):
+            memory_map.add_resource(res, name="foo", size=0)
+
+    def test_as_json(self):
+        elem_1_1 = csr.Element(8, "rw")
+        elem_1_2 = csr.Element(4, "r")
+        mux_1 = csr.Multiplexer(addr_width=1, data_width=8, name="mux_1")
+        mux_1.add(elem_1_1, name="elem_1")
+        mux_1.add(elem_1_2, name="elem_2")
+
+        elem_2_1 = csr.Element(4, "rw")
+        elem_2_2 = csr.Element(8, "w")
+        mux_2 = csr.Multiplexer(addr_width=1, data_width=8, name="mux_2")
+        mux_2.add(elem_2_1, name="elem_1")
+        mux_2.add(elem_2_2, name="elem_2")
+
+        decoder = csr.Decoder(addr_width=2, data_width=8)
+        decoder.add(mux_1.bus)
+        decoder.add(mux_2.bus)
+
+        annotation = MemoryMapAnnotation(decoder.bus.memory_map)
+        self.assertEqual(annotation.as_json(), {
+            "addr_width": 2,
+            "data_width": 8,
+            "alignment":  0,
+            "windows": [
+                {
+                    "start": 0,
+                    "end": 2,
+                    "ratio": 1,
+                    "annotations": {
+                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/memory/memory-map.json": {
+                            "name": "mux_1",
+                            "addr_width": 1,
+                            "data_width": 8,
+                            "alignment": 0,
+                            "windows": [],
+                            "resources": [
+                                {
+                                    "name": "elem_1",
+                                    "start": 0,
+                                    "end": 1,
+                                    "annotations": {
+                                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/element.json": {
+                                            "width": 8,
+                                            "access": "rw",
+                                        },
+                                    },
+                                },
+                                {
+                                    "name": "elem_2",
+                                    "start": 1,
+                                    "end": 2,
+                                    "annotations": {
+                                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/element.json": {
+                                            "width": 4,
+                                            "access": "r",
+                                        },
+                                    },
+                                }
+                            ],
+                        },
+                    },
+                },
+                {
+                    "start": 2,
+                    "end": 4,
+                    "ratio": 1,
+                    "annotations": {
+                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/memory/memory-map.json": {
+                            "name": "mux_2",
+                            "addr_width": 1,
+                            "data_width": 8,
+                            "alignment": 0,
+                            "windows": [],
+                            "resources": [
+                                {
+                                    "name": "elem_1",
+                                    "start": 0,
+                                    "end": 1,
+                                    "annotations": {
+                                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/element.json": {
+                                            "width": 4,
+                                            "access": "rw",
+                                        },
+                                    },
+                                },
+                                {
+                                    "name": "elem_2",
+                                    "start": 1,
+                                    "end": 2,
+                                    "annotations": {
+                                        "https://amaranth-lang.org/schema/amaranth-soc/0.1/csr/element.json": {
+                                            "width": 8,
+                                            "access": "w",
+                                        },
+                                    },
+                                }
+                            ],
+                        },
+                    },
+                },
+            ],
+            "resources": []
+        })
+
+    def test_wrong_origin(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Origin must be a MemoryMap object, not 'foo'"):
+            MemoryMapAnnotation("foo")
diff --git a/tests/test_wishbone_bus.py b/tests/test_wishbone_bus.py
index 00ddb4e..3ceedba 100644
--- a/tests/test_wishbone_bus.py
+++ b/tests/test_wishbone_bus.py
@@ -79,6 +79,40 @@ def test_create(self):
         self.assertEqual(iface.granularity, 8)
         self.assertEqual(iface.signature, sig)
 
+    def test_annotations(self):
+        sig = wishbone.Signature(addr_width=30, data_width=32, granularity=8,
+                                 features={"bte", "cti"})
+        iface = sig.create()
+        self.assertEqual([a.as_json() for a in sig.annotations(iface)], [
+            {
+                "addr_width": 30,
+                "data_width": 32,
+                "granularity": 8,
+                "features": [ "bte", "cti" ],
+            },
+        ])
+
+    def test_annotations_memory_map(self):
+        sig = wishbone.Signature(addr_width=30, data_width=32, granularity=8,
+                                 features={"bte", "cti"})
+        iface = sig.create()
+        iface.memory_map = MemoryMap(addr_width=32, data_width=8)
+        self.assertEqual([a.as_json() for a in sig.annotations(iface)], [
+            {
+                "addr_width": 30,
+                "data_width": 32,
+                "granularity": 8,
+                "features": [ "bte", "cti" ],
+            },
+            {
+                "addr_width": 32,
+                "data_width": 8,
+                "alignment": 0,
+                "windows": [],
+                "resources": [],
+            },
+        ])
+
     def test_eq(self):
         self.assertEqual(wishbone.Signature(addr_width=32, data_width=8, features={"err"}),
                          wishbone.Signature(addr_width=32, data_width=8, features={"err"}))
@@ -145,6 +179,38 @@ def test_wrong_features(self):
             wishbone.Signature.check_parameters(addr_width=0, data_width=8, granularity=8,
                                                 features={"foo"})
 
+    def test_annotations_wrong_type(self):
+        sig = wishbone.Signature(addr_width=30, data_width=32, granularity=8)
+        with self.assertRaisesRegex(TypeError,
+                r"Interface must be a wishbone\.Interface object, not 'foo'"):
+            sig.annotations("foo")
+
+    def test_annotations_incompatible(self):
+        sig1  = wishbone.Signature(addr_width=30, data_width=32, granularity=8)
+        iface = sig1.create()
+        sig2  = wishbone.Signature(addr_width=32, data_width=8)
+        with self.assertRaisesRegex(ValueError,
+                r"Interface signature is not equal to this signature"):
+            sig2.annotations(iface)
+
+
+class AnnotationTestCase(unittest.TestCase):
+    def test_as_json(self):
+        sig = wishbone.Signature(addr_width=30, data_width=32, granularity=8,
+                                 features={"cti", "bte"})
+        annotation = wishbone.Annotation(sig)
+        self.assertEqual(annotation.as_json(), {
+            "addr_width": 30,
+            "data_width": 32,
+            "granularity": 8,
+            "features": [ "bte", "cti" ],
+        })
+
+    def test_wrong_origin(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Origin must be a wishbone.Signature object, not 'foo'"):
+            wishbone.Annotation("foo")
+
 
 class InterfaceTestCase(unittest.TestCase):
     def test_simple(self):