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):