From c1dd5804c97bcbf8daf769c28c36d31526d92fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Nguyen?= Date: Thu, 8 Feb 2024 16:21:59 +0100 Subject: [PATCH] csr.reg: replace csr.RegisterMap with csr.Builder. --- amaranth_soc/csr/reg.py | 424 ++++++++++++--------------- tests/test_csr_reg.py | 631 ++++++++++++++++------------------------ 2 files changed, 433 insertions(+), 622 deletions(-) diff --git a/amaranth_soc/csr/reg.py b/amaranth_soc/csr/reg.py index 5c83b7d..ea26a39 100644 --- a/amaranth_soc/csr/reg.py +++ b/amaranth_soc/csr/reg.py @@ -1,7 +1,9 @@ from collections.abc import Mapping, Sequence +from contextlib import contextmanager from amaranth import * from amaranth.lib import enum, wiring from amaranth.lib.wiring import In, Out, connect, flipped +from amaranth.utils import ceil_log2 from ..memory import MemoryMap from .bus import Element, Signature, Multiplexer @@ -9,8 +11,7 @@ __all__ = [ "FieldPort", "Field", "FieldAction", "FieldActionMap", "FieldActionArray", - "Register", "RegisterMap", - "Bridge", + "Register", "Builder", "Bridge", ] @@ -590,229 +591,203 @@ def elaborate(self, platform): return m -class RegisterMap: - """A collection of CSR registers.""" - def __init__(self): - self._registers = dict() - self._clusters = dict() - self._namespace = dict() - self._frozen = False +class Builder: + """CSR builder. - def freeze(self): - """Freeze the cluster. + A CSR builder can organize a group of registers within an address range and convert it to a + :class:`MemoryMap` for consumption by other SoC primitives. - Once the cluster is frozen, its visible state becomes immutable. Registers and clusters - cannot be added anymore. - """ - self._frozen = True + Parameters + ---------- + addr_width : :class:`int` + Address width. + data_width : :class:`int` + Data width. + granularity : :class:`int` + Granularity. Optional, defaults to 8 bits. + name : :class:`str` + Name of the address range. Optional. - def add_register(self, register, *, name): - """Add a register. + Raises + ------ + :exc:`TypeError` + If ``addr_width`` is not a positive integer. + :exc:`TypeError` + If ``data_width`` is not a positive integer. + :exc:`TypeError` + If ``granularity`` is not a positive integer. + :exc:`ValueError` + If ``granularity`` is not a divisor of ``data_width`` + :exc:`TypeError` + If ``name`` is not a string, or is empty. + """ + def __init__(self, *, addr_width, data_width, granularity=8, name=None): + if not isinstance(addr_width, int) or addr_width <= 0: + raise TypeError(f"Address width must be a positive integer, not {addr_width!r}") + if not isinstance(data_width, int) or data_width <= 0: + raise TypeError(f"Data width must be a positive integer, not {data_width!r}") + if not isinstance(granularity, int) or granularity <= 0: + raise TypeError(f"Granularity must be a positive integer, not {granularity!r}") + + if data_width != (data_width // granularity) * granularity: + raise ValueError(f"Granularity {granularity} is not a divisor of data width " + f"{data_width}") + + if name is not None and not (isinstance(name, str) and name): + raise TypeError(f"Name must be a non-empty string, not {name!r}") - Arguments - --------- - register : :class:`Register` - Register. - name : :class:`str` - Name of the register. + self._addr_width = addr_width + self._data_width = data_width + self._granularity = granularity + self._name = name - Returns - ------- - :class:`Register` - ``register``, which is added to the register map. + self._registers = dict() + self._scope_stack = [] + self._frozen = False - Raises - ------ - :exc:`ValueError` - If the register map is frozen. - :exc:`TypeError` - If ``register` is not an instance of :class:`Register`. - :exc:`TypeError` - If ``name`` is not a string. - :exc:`ValueError` - If ``name`` is already used. - """ - if self._frozen: - raise ValueError("Register map is frozen") - if not isinstance(register, Register): - raise TypeError(f"Register must be an instance of csr.Register, not {register!r}") + @property + def addr_width(self): + return self._addr_width - if not isinstance(name, str) or not name: - raise TypeError(f"Name must be a non-empty string, not {name!r}") - if name in self._namespace: - raise ValueError(f"Name '{name}' is already used by {self._namespace[name]!r}") + @property + def data_width(self): + return self._data_width - self._registers[id(register)] = register, name - self._namespace[name] = register - return register + @property + def granularity(self): + return self._granularity - def registers(self): - """Iterate local registers. + @property + def name(self): + return self._name - Yields - ------ - :class:`Register` - Register. - :class:`str` - Name of the register. + def freeze(self): + """Freeze the builder. + + Once the builder is frozen, CSR registers cannot be added anymore. """ - for register, name in self._registers.values(): - yield register, name + self._frozen = True - def add_cluster(self, cluster, *, name): - """Add a cluster of registers. + def add(self, name, reg, *, offset=None): + """Add a CSR register. Arguments --------- - cluster : :class:`RegisterMap` - Cluster of registers. name : :class:`str` - Name of the cluster. + Register name. + reg : :class:`Register` + Register. + offset : :class:`int` + Register offset. Optional. Returns ------- - :class:`RegisterMap` - ``cluster``, which is added to the register map. + :class:`Register` + ``reg``, which is added to the builder. Its name is ``name``, prefixed by the names and + indices of any parent :meth:`~Builder.Cluster` and :meth:`~Builder.Index`. Raises ------ :exc:`ValueError` - If the register map is frozen. + If the builder is frozen. :exc:`TypeError` - If ``cluster` is not an instance of :class:`RegisterMap`. + If ``name`` is not a string, or is empty. :exc:`TypeError` - If ``name`` is not a string. + If ``reg` is not an instance of :class:`Register`. :exc:`ValueError` - If ``name`` is already used. + If ``reg`` is already added to the builder. + :exc:`TypeError` + If ``offset`` is not an integer, or is negative. + :exc:`ValueError` + If ``offset`` is not a multiple of ``self.data_width // self.granularity``. """ + if not isinstance(reg, Register): + raise TypeError(f"Register must be an instance of csr.Register, not {reg!r}") if self._frozen: - raise ValueError("Register map is frozen") - if not isinstance(cluster, RegisterMap): - raise TypeError(f"Cluster must be an instance of csr.RegisterMap, not {cluster!r}") - - if not isinstance(name, str) or not name: - raise TypeError(f"Name must be a non-empty string, not {name!r}") - if name in self._namespace: - raise ValueError(f"Name '{name}' is already used by {self._namespace[name]!r}") - - self._clusters[id(cluster)] = cluster, name - self._namespace[name] = cluster - return cluster - - def clusters(self): - """Iterate local clusters of registers. - - Yields - ------ - :class:`RegisterMap` - Cluster of registers. - :class:`str` - Name of the cluster. - """ - for cluster, name in self._clusters.values(): - yield cluster, name - - def flatten(self, *, _path=()): - """Recursively iterate over all registers. - - Yields - ------ - :class:`Register` - Register. - iter(:class:`str`) - Path of the register. It contains its name, prefixed by the name of parent clusters up - to this register map. - """ - for name, assignment in self._namespace.items(): - path = (*_path, name) - if id(assignment) in self._registers: - yield assignment, path - elif id(assignment) in self._clusters: - yield from assignment.flatten(_path=path) + raise ValueError(f"Builder is frozen. Cannot add register {reg!r}") + + if name is None or not (isinstance(name, str) and name): + raise TypeError(f"Register name must be a non-empty string, not {name!r}") + + if offset is not None: + if not (isinstance(offset, int) and offset >= 0): + raise TypeError(f"Offset must be a non-negative integer, not {offset!r}") + ratio = self.data_width // self.granularity + if offset % ratio != 0: + raise ValueError(f"Offset {offset:#x} must be a multiple of {ratio:#x} bytes") + + if id(reg) in self._registers: + _, other_name, other_offset = self._registers[id(reg)] + error_msg = f"Register {reg!r} is already added with name {other_name}" + if other_offset is None: + error_msg += " at an implicit offset" else: - assert False # :nocov: + error_msg += f" at an explicit offset {other_offset:#x}" + raise ValueError(error_msg) - def get_path(self, register, *, _path=()): - """Get the path of a register. + self._registers[id(reg)] = reg, (*self._scope_stack, name), offset + return reg + + @contextmanager + def Cluster(self, name): + """Define a cluster. Arguments --------- - register : :class:`Register` - A register of the register map. - - Returns - ------- - iter(:class:`str`) - Path of the register. It contains its name, prefixed by the name of parent clusters up - to this register map. + name : :class:`str` + Cluster name. Raises ------ :exc:`TypeError` - If ``register` is not an instance of :class:`Register`. - :exc:`KeyError` - If ``register` is not in the register map. + If ``name`` is not a string, or is empty. """ - if not isinstance(register, Register): - raise TypeError(f"Register must be an instance of csr.Register, not {register!r}") - - if id(register) in self._registers: - _, name = self._registers[id(register)] - return (*_path, name) - - for cluster, name in self._clusters.values(): - try: - return cluster.get_path(register, _path=(*_path, name)) - except KeyError: - pass - - raise KeyError(register) + if not (isinstance(name, str) and name): + raise TypeError(f"Cluster name must be a non-empty string, not {name!r}") + self._scope_stack.append(name) + try: + yield + finally: + assert self._scope_stack.pop() == name - def get_register(self, path): - """Get a register. + @contextmanager + def Index(self, index): + """Define an array index. Arguments --------- - path : iter(:class:`str`) - Path of the register. It contains its name, prefixed by the name of parent clusters up - to this register map. - - Returns - ------- - :class:`Register` - The register assigned to ``path``. + index : :class:`int` + Array index. Raises ------ - :exc:`ValueError` - If ``path`` is empty. :exc:`TypeError` - If ``path`` is not composed of non-empty strings. - :exc:`KeyError` - If ``path`` is not assigned to a register. + If ``index`` is not an integer, or is negative. """ - path = tuple(path) - if not path: - raise ValueError("Path must be a non-empty iterable") - for name in path: - if not isinstance(name, str) or not name: - raise TypeError(f"Path must contain non-empty strings, not {name!r}") - - name, *rest = path - - if name in self._namespace: - assignment = self._namespace[name] - if not rest: - assert id(assignment) in self._registers - return assignment + if not (isinstance(index, int) and index >= 0): + raise TypeError(f"Array index must be a non-negative integer, not {index!r}") + self._scope_stack.append(index) + try: + yield + finally: + assert self._scope_stack.pop() == index + + def as_memory_map(self): + self.freeze() + memory_map = MemoryMap(addr_width=self.addr_width, data_width=self.data_width, + name=self.name) + for reg, reg_name, reg_offset in self._registers.values(): + if reg_offset is not None: + reg_addr = (reg_offset * self.granularity) // self.data_width else: - assert id(assignment) in self._clusters - try: - return assignment.get_register(rest) - except KeyError: - pass - - raise KeyError(path) + reg_addr = None + reg_size = (reg.element.width + self.data_width - 1) // self.data_width + # TBD: should integers be allowed inside resource names? + reg_name = tuple(str(part) for part in reg_name) + memory_map.add_resource(reg, name=reg_name, addr=reg_addr, size=reg_size, + alignment=ceil_log2(reg_size)) + memory_map.freeze() + return memory_map class Bridge(wiring.Component): @@ -820,89 +795,46 @@ class Bridge(wiring.Component): Parameters ---------- - register_map : :class:`RegisterMap` - Register map. - addr_width : :class:`int` - Address width. See :class:`Interface`. - data_width : :class:`int` - Data width. See :class:`Interface`. - alignment : log2 of :class:`int` - Register alignment. Optional, defaults to ``0``. See :class:`..memory.MemoryMap`. - name : :class:`str` - Window name. Optional. - register_addr : :class:`dict` - Register address assignments. Optional, defaults to ``None``. - register_alignment : :class:`dict` - Register alignment assignments. Optional, defaults to ``None``. + memory_map : :class:`MemoryMap` + Memory map of CSR registers. - Attributes - ---------- - register_map : :class:`RegisterMap` - Register map. + Interface attributes + -------------------- bus : :class:`Interface` - CSR bus providing access to registers. + CSR bus providing access to the contents of ``memory_map``. Raises ------ :exc:`TypeError` - If ``register_map`` is not an instance of :class:`RegisterMap`. - :exc:`TypeError` - If ``register_addr`` is a not a dict. + If ``memory_map`` is not a :class:`MemoryMap` object. + :exc:`ValueError` + If ``memory_map`` has windows. :exc:`TypeError` - If ``register_alignment`` is a not a dict. + If ``memory_map`` has resources that are not :class:`Register` objects. """ - def __init__(self, register_map, *, addr_width, data_width, alignment=0, name=None, - register_addr=None, register_alignment=None): - if not isinstance(register_map, RegisterMap): - raise TypeError(f"Register map must be an instance of RegisterMap, not " - f"{register_map!r}") - - memory_map = MemoryMap(addr_width=addr_width, data_width=data_width, alignment=alignment, - name=name) - - def traverse_path(path, obj): - progress = [] - for name in path: - if not isinstance(obj, dict): - break - progress.append(name) - obj = obj.get(name, None) - return obj, tuple(progress) - - register_map.freeze() - - for register, path in register_map.flatten(): - reg_addr, assigned = traverse_path(path, register_addr) - if assigned != path and reg_addr is not None: - raise TypeError(f"Register address assignment for the cluster {tuple(assigned)} " - f"must be a dict or None, not {reg_addr!r}") - - reg_alignment, assigned = traverse_path(path, register_alignment) - if assigned != path and reg_alignment is not None: - raise TypeError(f"Register alignment assignment for the cluster {tuple(assigned)} " - f"must be a dict or None, not {reg_alignment!r}") - - reg_size = (register.element.width + data_width - 1) // data_width - reg_name = "__".join(path) - memory_map.add_resource(register, name=(reg_name,), size=reg_size, addr=reg_addr, - alignment=reg_alignment) - - self._map = register_map + def __init__(self, memory_map): + if not isinstance(memory_map, MemoryMap): + raise TypeError(f"CSR bridge memory map must be an instance of MemoryMap, not {memory_map!r}") + if list(memory_map.windows()): + raise ValueError("CSR bridge memory map cannot have windows") + for reg, reg_name, (reg_start, reg_end) in memory_map.resources(): + if not isinstance(reg, Register): + raise TypeError(f"CSR register must be an instance of csr.Register, not {reg!r}") + + memory_map.freeze() self._mux = Multiplexer(memory_map) - - super().__init__({"bus": In(Signature(addr_width=addr_width, data_width=data_width))}) - self.bus.memory_map = self._mux.bus.memory_map - - @property - def register_map(self): - return self._map + super().__init__({ + "bus": In(Signature(addr_width=memory_map.addr_width, + data_width=memory_map.data_width)) + }) + self.bus.memory_map = memory_map def elaborate(self, platform): m = Module() m.submodules.mux = self._mux - for register, path in self.register_map.flatten(): - m.submodules["__".join(path)] = register + for reg, reg_name, _ in self.bus.memory_map.resources(): + m.submodules["__".join(reg_name)] = reg connect(m, flipped(self.bus), self._mux.bus) diff --git a/tests/test_csr_reg.py b/tests/test_csr_reg.py index 75d5e1c..f591273 100644 --- a/tests/test_csr_reg.py +++ b/tests/test_csr_reg.py @@ -8,6 +8,7 @@ from amaranth_soc.csr.reg import * from amaranth_soc.csr import action, Element +from amaranth_soc.memory import MemoryMap # The `amaranth: UnusedElaboratable=no` modeline isn't enough here. @@ -630,230 +631,260 @@ def process(): sim.run() -class RegisterMapTestCase(unittest.TestCase): - def setUp(self): - self.map = RegisterMap() +class _MockRegister(Register, access="rw"): + def __init__(self, name, width=1): + super().__init__({"a": Field(action.RW, unsigned(width))}) + self._name = name - def test_add_register(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - reg_rw_a = FooRegister() - self.assertIs(self.map.add_register(reg_rw_a, name="reg_rw_a"), reg_rw_a) + def __repr__(self): + return f"_MockRegister({self._name!r})" - def test_add_register_frozen(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - reg_rw_a = FooRegister() - self.map.freeze() - with self.assertRaisesRegex(ValueError, r"Register map is frozen"): - self.map.add_register(reg_rw_a, name="reg_rw_a") - def test_add_register_wrong_type(self): - with self.assertRaisesRegex(TypeError, - r"Register must be an instance of csr\.Register, not 'foo'"): - self.map.add_register("foo", name="foo") +class BuilderTestCase(unittest.TestCase): + def test_init(self): + # default name & granularity + regs_0 = Builder(addr_width=30, data_width=32) + self.assertEqual(regs_0.addr_width, 30) + self.assertEqual(regs_0.data_width, 32) + self.assertEqual(regs_0.granularity, 8) + self.assertEqual(regs_0.name, None) + # custom name & granularity + regs_1 = Builder(addr_width=31, data_width=32, granularity=16, name="periph") + self.assertEqual(regs_1.addr_width, 31) + self.assertEqual(regs_1.data_width, 32) + self.assertEqual(regs_1.granularity, 16) + self.assertEqual(regs_1.name, "periph") - def test_add_register_wrong_name(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - reg_rw_a = FooRegister() + def test_init_wrong_addr_width(self): with self.assertRaisesRegex(TypeError, - r"Name must be a non-empty string, not None"): - self.map.add_register(reg_rw_a, name=None) + r"Address width must be a positive integer, not 'foo'"): + Builder(addr_width="foo", data_width=32) - def test_add_register_empty_name(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - reg_rw_a = FooRegister() + def test_init_wrong_data_width(self): with self.assertRaisesRegex(TypeError, - r"Name must be a non-empty string, not ''"): - self.map.add_register(reg_rw_a, name="") - - def test_add_cluster(self): - cluster = RegisterMap() - self.assertIs(self.map.add_cluster(cluster, name="cluster"), cluster) - - def test_add_cluster_frozen(self): - self.map.freeze() - cluster = RegisterMap() - with self.assertRaisesRegex(ValueError, r"Register map is frozen"): - self.map.add_cluster(cluster, name="cluster") + r"Data width must be a positive integer, not 'foo'"): + Builder(addr_width=30, data_width="foo") - def test_add_cluster_wrong_type(self): + def test_init_wrong_granularity(self): with self.assertRaisesRegex(TypeError, - r"Cluster must be an instance of csr\.RegisterMap, not 'foo'"): - self.map.add_cluster("foo", name="foo") + r"Granularity must be a positive integer, not 'foo'"): + Builder(addr_width=30, data_width=32, granularity="foo") - def test_add_cluster_wrong_name(self): - cluster = RegisterMap() - with self.assertRaisesRegex(TypeError, - r"Name must be a non-empty string, not None"): - self.map.add_cluster(cluster, name=None) + def test_init_granularity_divisor(self): + with self.assertRaisesRegex(ValueError, + r"Granularity 7 is not a divisor of data width 32"): + Builder(addr_width=30, data_width=32, granularity=7) - def test_add_cluster_empty_name(self): - cluster = RegisterMap() + def test_init_wrong_name(self): + with self.assertRaisesRegex(TypeError, + r"Name must be a non-empty string, not 8"): + Builder(addr_width=30, data_width=32, name=8) with self.assertRaisesRegex(TypeError, r"Name must be a non-empty string, not ''"): - self.map.add_cluster(cluster, name="") - - def test_namespace_collision(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - class BarRegister(Register, access="rw"): - b: Field(action.RW, 1) - - reg_rw_a = FooRegister() - reg_rw_b = BarRegister() - cluster_0 = RegisterMap() - cluster_1 = RegisterMap() - - self.map.add_register(reg_rw_a, name="reg_rw_a") - self.map.add_cluster(cluster_0, name="cluster_0") - - with self.assertRaisesRegex(ValueError, # register/register - r"Name 'reg_rw_a' is already used by *"): - self.map.add_register(reg_rw_b, name="reg_rw_a") - with self.assertRaisesRegex(ValueError, # register/cluster - r"Name 'reg_rw_a' is already used by *"): - self.map.add_cluster(cluster_1, name="reg_rw_a") - with self.assertRaisesRegex(ValueError, # cluster/cluster - r"Name 'cluster_0' is already used by *"): - self.map.add_cluster(cluster_1, name="cluster_0") - with self.assertRaisesRegex(ValueError, # cluster/register - r"Name 'cluster_0' is already used by *"): - self.map.add_register(reg_rw_b, name="cluster_0") - - def test_iter_registers(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - class BarRegister(Register, access="rw"): - b: Field(action.RW, 1) - - reg_rw_a = FooRegister() - reg_rw_b = BarRegister() - self.map.add_register(reg_rw_a, name="reg_rw_a") - self.map.add_register(reg_rw_b, name="reg_rw_b") - - registers = list(self.map.registers()) - - self.assertEqual(len(registers), 2) - self.assertIs(registers[0][0], reg_rw_a) - self.assertEqual(registers[0][1], "reg_rw_a") - self.assertIs(registers[1][0], reg_rw_b) - self.assertEqual(registers[1][1], "reg_rw_b") - - def test_iter_clusters(self): - cluster_0 = RegisterMap() - cluster_1 = RegisterMap() - self.map.add_cluster(cluster_0, name="cluster_0") - self.map.add_cluster(cluster_1, name="cluster_1") - - clusters = list(self.map.clusters()) - - self.assertEqual(len(clusters), 2) - self.assertIs(clusters[0][0], cluster_0) - self.assertEqual(clusters[0][1], "cluster_0") - self.assertIs(clusters[1][0], cluster_1) - self.assertEqual(clusters[1][1], "cluster_1") - - def test_iter_flatten(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - class BarRegister(Register, access="rw"): - b: Field(action.RW, 1) - - reg_rw_a = FooRegister() - reg_rw_b = BarRegister() - cluster_0 = RegisterMap() - cluster_1 = RegisterMap() - - cluster_0.add_register(reg_rw_a, name="reg_rw_a") - cluster_1.add_register(reg_rw_b, name="reg_rw_b") - - self.map.add_cluster(cluster_0, name="cluster_0") - self.map.add_cluster(cluster_1, name="cluster_1") - - registers = list(self.map.flatten()) - - self.assertEqual(len(registers), 2) - self.assertIs(registers[0][0], reg_rw_a) - self.assertEqual(registers[0][1], ("cluster_0", "reg_rw_a")) - self.assertIs(registers[1][0], reg_rw_b) - self.assertEqual(registers[1][1], ("cluster_1", "reg_rw_b")) - - def test_get_path(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - class BarRegister(Register, access="rw"): - b: Field(action.RW, 1) - class BazRegister(Register, access="rw"): - c: Field(action.RW, 1) - - reg_rw_a = FooRegister() - reg_rw_b = BarRegister() - reg_rw_c = BazRegister() - - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_a, name="reg_rw_a") - - cluster_1 = RegisterMap() - cluster_1.add_register(reg_rw_c, name="reg_rw_c") + Builder(addr_width=30, data_width=32, name="") + + def test_add(self): + regs = Builder(addr_width=30, data_width=32) + ra = regs.add("a", _MockRegister("a")) + rb = regs.add("b", _MockRegister("b"), offset=4) + self.assertEqual(list(regs._registers.values()), [ + (ra, ('a',), None), + (rb, ('b',), 4), + ]) - self.map.add_cluster(cluster_0, name="cluster_0") - self.map.add_register(reg_rw_b, name="reg_rw_b") - self.map.add_cluster(cluster_1, name="cluster_1") + def test_add_cluster(self): + regs = Builder(addr_width=30, data_width=32) + with regs.Cluster("a"): + rb = regs.add("b", _MockRegister("b")) + rc = regs.add("c", _MockRegister("c")) + self.assertEqual(list(regs._registers.values()), [ + (rb, ('a', 'b'), None), + (rc, ('c',), None), + ]) - self.assertEqual(self.map.get_path(reg_rw_a), ("cluster_0", "reg_rw_a")) - self.assertEqual(self.map.get_path(reg_rw_b), ("reg_rw_b",)) - self.assertEqual(self.map.get_path(reg_rw_c), ("cluster_1", "reg_rw_c")) + def test_add_array(self): + regs = Builder(addr_width=30, data_width=32) + with regs.Index(10): + ra = regs.add("a", _MockRegister("a")) + rb = regs.add("b", _MockRegister("b")) + self.assertEqual(list(regs._registers.values()), [ + (ra, (10, 'a'), None), + (rb, ('b',), None), + ]) - def test_get_path_wrong_register(self): + def test_add_nested(self): + # cluster -> cluster & index + regs_0 = Builder(addr_width=30, data_width=32) + with regs_0.Cluster("foo"): + with regs_0.Cluster("bar"): + ra = regs_0.add("a", _MockRegister("a")) + with regs_0.Index(10): + rb = regs_0.add("b", _MockRegister("b")) + rc = regs_0.add("c", _MockRegister("c")) + self.assertEqual(list(regs_0._registers.values()), [ + (ra, ("foo", "bar", "a"), None), + (rb, ("foo", 10, "b"), None), + (rc, ("foo", "c"), None), + ]) + # index -> index & cluster + regs_1 = Builder(addr_width=8, data_width=8) + with regs_1.Index(3): + with regs_1.Index(7): + rd = regs_1.add("d", _MockRegister("d")) + re = regs_1.add("e", _MockRegister("e")) + with regs_1.Cluster("foo"): + rf = regs_1.add("f", _MockRegister("f")) + + def test_add_wrong_reg(self): + regs = Builder(addr_width=8, data_width=8) with self.assertRaisesRegex(TypeError, - r"Register must be an instance of csr\.Register, not 'foo'"): - self.map.get_path("foo") - - def test_get_path_unknown_register(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - reg_rw_a = FooRegister() - with self.assertRaises(KeyError): - self.map.get_path(reg_rw_a) - - def test_get_register(self): - class FooRegister(Register, access="rw"): - a: Field(action.RW, 1) - class BarRegister(Register, access="rw"): - b: Field(action.RW, 1) + r"Register must be an instance of csr\.Register, not 'bar'"): + regs.add("foo", "bar") - reg_rw_a = FooRegister() - reg_rw_b = BarRegister() - cluster_0 = RegisterMap() + def test_add_frozen(self): + regs = Builder(addr_width=8, data_width=8) + regs.freeze() + with self.assertRaisesRegex(ValueError, + r"Builder is frozen. Cannot add register _MockRegister\('a'\)"): + regs.add("a", _MockRegister("a")) - cluster_0.add_register(reg_rw_a, name="reg_rw_a") - self.map.add_cluster(cluster_0, name="cluster_0") - self.map.add_register(reg_rw_b, name="reg_rw_b") + def test_add_wrong_name(self): + regs = Builder(addr_width=8, data_width=8) + with self.assertRaisesRegex(TypeError, + r"Register name must be a non-empty string, not 1"): + regs.add(1, _MockRegister("a")) + with self.assertRaisesRegex(TypeError, + r"Register name must be a non-empty string, not ''"): + regs.add('', _MockRegister("a")) + with self.assertRaisesRegex(TypeError, + r"Register name must be a non-empty string, not \(\)"): + regs.add((), _MockRegister("a")) - self.assertIs(self.map.get_register(("cluster_0", "reg_rw_a")), reg_rw_a) - self.assertIs(self.map.get_register(("reg_rw_b",)), reg_rw_b) + def test_add_wrong_offset(self): + regs = Builder(addr_width=8, data_width=8) + with self.assertRaisesRegex(TypeError, + r"Offset must be a non-negative integer, not 'foo'"): + regs.add("a", _MockRegister("a"), offset="foo") + with self.assertRaisesRegex(TypeError, + r"Offset must be a non-negative integer, not -1"): + regs.add("a", _MockRegister("a"), offset=-1) + + def test_add_misaligned_offset(self): + regs = Builder(addr_width=30, data_width=32, granularity=8) + with self.assertRaisesRegex(ValueError, r"Offset 0x1 must be a multiple of 0x4 bytes"): + regs.add("a", _MockRegister("a"), offset=1) + + def test_add_twice(self): + regs = Builder(addr_width=8, data_width=8) + ra = regs.add("a", _MockRegister("a")) + rb = regs.add("b", _MockRegister("b"), offset=1) + with self.assertRaisesRegex(ValueError, + r"Register _MockRegister\('a'\) is already added with name \('a',\) at an " + r"implicit offset"): + regs.add("aa", ra) + with self.assertRaisesRegex(ValueError, + r"Register _MockRegister\('b'\) is already added with name \('b',\) at an " + r"explicit offset 0x1"): + regs.add("bb", rb) - def test_get_register_empty_path(self): - with self.assertRaisesRegex(ValueError, r"Path must be a non-empty iterable"): - self.map.get_register(()) + def test_cluster_wrong_name(self): + regs = Builder(addr_width=8, data_width=8) + with self.assertRaisesRegex(TypeError, + r"Cluster name must be a non-empty string, not -1"): + with regs.Cluster(-1): + pass + with self.assertRaisesRegex(TypeError, + r"Cluster name must be a non-empty string, not ''"): + with regs.Cluster(""): + pass + with self.assertRaisesRegex(TypeError, + r"Cluster name must be a non-empty string, not \(\)"): + with regs.Cluster(()): + pass - def test_get_register_wrong_path(self): + def test_array_wrong_index(self): + regs = Builder(addr_width=8, data_width=8) with self.assertRaisesRegex(TypeError, - r"Path must contain non-empty strings, not 0"): - self.map.get_register(("cluster_0", 0)) + r"Array index must be a non-negative integer, not 'foo'"): + with regs.Index("foo"): + pass with self.assertRaisesRegex(TypeError, - r"Path must contain non-empty strings, not ''"): - self.map.get_register(("", "reg_rw_a")) + r"Array index must be a non-negative integer, not -1"): + with regs.Index(-1): + pass - def test_get_register_unknown_path(self): - self.map.add_cluster(RegisterMap(), name="cluster_0") - with self.assertRaises(KeyError): - self.map.get_register(("reg_rw_a",)) - with self.assertRaises(KeyError): - self.map.get_register(("cluster_0", "reg_rw_a")) + def test_memory_map(self): + regs = Builder(addr_width=30, data_width=32, name="foo") + ra = regs.add("a", _MockRegister("ra")) # offset=0x0 + with regs.Cluster("b"): + rc = regs.add("c", _MockRegister("rc"), offset=0xc) + rd = regs.add("d", _MockRegister("rd"), offset=0x4) + re = regs.add("e", _MockRegister("re")) # offset=0x8 + with regs.Index(0): + rf = regs.add("f", _MockRegister("rf", width=32), offset=0x10) + with regs.Index(1): + rg = regs.add("g", _MockRegister("rg", width=48)) # offset=0x18 + + memory_map = regs.as_memory_map() + self.assertEqual(memory_map.name, "foo") + self.assertEqual(memory_map.addr_width, 30) + self.assertEqual(memory_map.data_width, 32) + self.assertEqual(memory_map.alignment, 0) + + self.assertFalse(list(memory_map.windows())) + + results = list(memory_map.resources()) + self.assertIs(results[0][0], ra) + self.assertEqual(results[0][1], ("a",)) + self.assertEqual(results[0][2], (0, 1)) + + self.assertIs(results[1][0], rc) + self.assertEqual(results[1][1], ("b", "c")) + self.assertEqual(results[1][2], (3, 4)) + + self.assertIs(results[2][0], rd) + self.assertEqual(results[2][1], ("b", "d")) + self.assertEqual(results[2][2], (1, 2)) + + self.assertIs(results[3][0], re) + self.assertEqual(results[3][1], ("b", "e")) + self.assertEqual(results[3][2], (2, 3)) + + self.assertIs(results[4][0], rf) + self.assertEqual(results[4][1], ("b", "0", "f")) + self.assertEqual(results[4][2], (4, 5)) + + self.assertIs(results[5][0], rg) + self.assertEqual(results[5][1], ("b", "1", "g")) + self.assertEqual(results[5][2], (6, 8)) + + def test_memory_map_name_conflicts(self): + # register/register + regs_0 = Builder(addr_width=8, data_width=32) + regs_0.add("a", _MockRegister("foo")) + regs_0.add("a", _MockRegister("bar")) + with self.assertRaisesRegex(ValueError, + r"Resource _MockRegister\('bar'\) cannot be added to the local namespace:" + r"\n- \('a',\) conflicts with local name \('a',\) assigned to _MockRegister\('foo'\)"): + regs_0.as_memory_map() + # register/cluster + regs_1 = Builder(addr_width=8, data_width=32) + regs_1.add("a", _MockRegister("foo")) + with regs_1.Cluster("a"): + regs_1.add("b", _MockRegister("bar")) + with self.assertRaisesRegex(ValueError, + r"Resource _MockRegister\('bar'\) cannot be added to the local namespace:" + r"\n- \('a', 'b'\) conflicts with local name \('a',\) assigned to _MockRegister\('foo'\)"): + regs_1.as_memory_map() + # cluster/register + regs_2 = Builder(addr_width=8, data_width=32) + with regs_2.Cluster("a"): + regs_2.add("b", _MockRegister("foo")) + regs_2.add("a", _MockRegister("bar")) + with self.assertRaisesRegex(ValueError, + r"Resource _MockRegister\('bar'\) cannot be added to the local namespace:" + r"\n- \('a',\) conflicts with local name \('a', 'b'\) assigned to _MockRegister\('foo'\)"): + regs_2.as_memory_map() class BridgeTestCase(unittest.TestCase): @@ -861,190 +892,38 @@ class _RWRegister(Register, access="rw"): def __init__(self, width, reset=0): super().__init__({"a": Field(action.RW, width, reset=reset)}) - def test_memory_map(self): - reg_rw_4 = self._RWRegister( 4) - reg_rw_8 = self._RWRegister( 8) - reg_rw_12 = self._RWRegister(12) - reg_rw_16 = self._RWRegister(16) - - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_12, name="reg_rw_12") - cluster_0.add_register(reg_rw_16, name="reg_rw_16") - - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") - register_map.add_register(reg_rw_8, name="reg_rw_8") - register_map.add_cluster(cluster_0, name="cluster_0") - - dut = Bridge(register_map, addr_width=16, data_width=8) - registers = list(dut.bus.memory_map.resources()) - - self.assertIs(registers[0][0], reg_rw_4) - self.assertEqual(registers[0][1], ("reg_rw_4",)) - self.assertEqual(registers[0][2], (0, 1)) - - self.assertIs(registers[1][0], reg_rw_8) - self.assertEqual(registers[1][1], ("reg_rw_8",)) - self.assertEqual(registers[1][2], (1, 2)) - - self.assertIs(registers[2][0], reg_rw_12) - self.assertEqual(registers[2][1], ("cluster_0__reg_rw_12",)) - self.assertEqual(registers[2][2], (2, 4)) - - self.assertIs(registers[3][0], reg_rw_16) - self.assertEqual(registers[3][1], ("cluster_0__reg_rw_16",)) - self.assertEqual(registers[3][2], (4, 6)) - - def test_wrong_register_map(self): + def test_wrong_memory_map(self): with self.assertRaisesRegex(TypeError, - r"Register map must be an instance of RegisterMap, not 'foo'"): - dut = Bridge("foo", addr_width=16, data_width=8) - - def test_register_addr(self): - reg_rw_4 = self._RWRegister( 4) - reg_rw_8 = self._RWRegister( 8) - reg_rw_12 = self._RWRegister(12) - reg_rw_16 = self._RWRegister(16) - - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_12, name="reg_rw_12") - cluster_0.add_register(reg_rw_16, name="reg_rw_16") - - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") - register_map.add_register(reg_rw_8, name="reg_rw_8") - register_map.add_cluster(cluster_0, name="cluster_0") - - register_addr = { - "reg_rw_4": 0x10, - "reg_rw_8": None, - "cluster_0": { - "reg_rw_12": 0x20, - "reg_rw_16": None, - }, - } - - dut = Bridge(register_map, addr_width=16, data_width=8, - register_addr=register_addr) - registers = list(dut.bus.memory_map.resources()) - - self.assertEqual(registers[0][1], ("reg_rw_4",)) - self.assertEqual(registers[0][2], (0x10, 0x11)) - - self.assertEqual(registers[1][1], ("reg_rw_8",)) - self.assertEqual(registers[1][2], (0x11, 0x12)) - - self.assertEqual(registers[2][1], ("cluster_0__reg_rw_12",)) - self.assertEqual(registers[2][2], (0x20, 0x22)) - - self.assertEqual(registers[3][1], ("cluster_0__reg_rw_16",)) - self.assertEqual(registers[3][2], (0x22, 0x24)) - - def test_register_alignment(self): - reg_rw_4 = self._RWRegister( 4) - reg_rw_8 = self._RWRegister( 8) - reg_rw_12 = self._RWRegister(12) - reg_rw_16 = self._RWRegister(16) - - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_12, name="reg_rw_12") - cluster_0.add_register(reg_rw_16, name="reg_rw_16") - - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") - register_map.add_register(reg_rw_8, name="reg_rw_8") - register_map.add_cluster(cluster_0, name="cluster_0") - - register_alignment = { - "reg_rw_4": None, - "reg_rw_8": None, - "cluster_0": { - "reg_rw_12": 3, - "reg_rw_16": None, - }, - } - - dut = Bridge(register_map, addr_width=16, data_width=8, alignment=1, - register_alignment=register_alignment) - registers = list(dut.bus.memory_map.resources()) - - self.assertEqual(registers[0][1], ("reg_rw_4",)) - self.assertEqual(registers[0][2], (0, 2)) - - self.assertEqual(registers[1][1], ("reg_rw_8",)) - self.assertEqual(registers[1][2], (2, 4)), - - self.assertEqual(registers[2][1], ("cluster_0__reg_rw_12",)) - self.assertEqual(registers[2][2], (8, 16)) - - self.assertEqual(registers[3][1], ("cluster_0__reg_rw_16",)) - self.assertEqual(registers[3][2], (16, 18)) - - def test_register_out_of_bounds(self): - reg_rw_24 = self._RWRegister(24) - register_map = RegisterMap() - register_map.add_register(reg_rw_24, name="reg_rw_24") + r"CSR bridge memory map must be an instance of MemoryMap, not 'foo'"): + Bridge("foo") + + def test_wrong_memory_map_windows(self): + memory_map_0 = MemoryMap(addr_width=1, data_width=8) + memory_map_1 = MemoryMap(addr_width=1, data_width=8) + memory_map_0.add_window(memory_map_1) with self.assertRaisesRegex(ValueError, - r"Address range 0x0\.\.0x3 out of bounds for memory map spanning " - r"range 0x0\.\.0x2 \(1 address bits\)"): - dut = Bridge(register_map, addr_width=1, data_width=8) - - def test_wrong_register_address(self): - reg_rw_4 = self._RWRegister(4) - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") + r"CSR bridge memory map cannot have windows"): + Bridge(memory_map_0) + + def test_wrong_memory_map_resource(self): + class _Reg(wiring.Component): + def __repr__(self): + return "_Reg()" + memory_map = MemoryMap(addr_width=1, data_width=8) + memory_map.add_resource(_Reg({}), name=("a",), size=1) with self.assertRaisesRegex(TypeError, - r"Register address assignment for the cluster \(\) must be a dict or None, not " - r"'foo'"): - dut = Bridge(register_map, addr_width=1, data_width=8, register_addr="foo") - - def test_wrong_cluster_address(self): - reg_rw_4 = self._RWRegister(4) - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_4, name="reg_rw_4") - register_map = RegisterMap() - register_map.add_cluster(cluster_0, name="cluster_0") - with self.assertRaisesRegex(TypeError, - r"Register address assignment for the cluster \('cluster_0',\) must be a dict or " - r"None, not 'foo'"): - dut = Bridge(register_map, addr_width=1, data_width=8, - register_addr={"cluster_0": "foo"}) - - def test_wrong_register_alignment(self): - reg_rw_4 = self._RWRegister(4) - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") - with self.assertRaisesRegex(TypeError, - r"Register alignment assignment for the cluster \(\) must be a dict or None, not " - r"'foo'"): - dut = Bridge(register_map, addr_width=1, data_width=8, register_alignment="foo") - - def test_wrong_cluster_alignment(self): - reg_rw_4 = self._RWRegister(4) - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_4, name="reg_rw_4") - register_map = RegisterMap() - register_map.add_cluster(cluster_0, name="cluster_0") - with self.assertRaisesRegex(TypeError, - r"Register alignment assignment for the cluster \('cluster_0',\) must be a dict " - r"or None, not 'foo'"): - dut = Bridge(register_map, addr_width=1, data_width=8, - register_alignment={"cluster_0": "foo"}) + r"CSR register must be an instance of csr\.Register, not _Reg\(\)"): + Bridge(memory_map) def test_sim(self): - reg_rw_4 = self._RWRegister( 4, reset=0x0) - reg_rw_8 = self._RWRegister( 8, reset=0x11) - reg_rw_16 = self._RWRegister(16, reset=0x3322) - - cluster_0 = RegisterMap() - cluster_0.add_register(reg_rw_16, name="reg_rw_16") + regs = Builder(addr_width=16, data_width=8) - register_map = RegisterMap() - register_map.add_register(reg_rw_4, name="reg_rw_4") - register_map.add_register(reg_rw_8, name="reg_rw_8") - register_map.add_cluster(cluster_0, name="cluster_0") + reg_rw_4 = regs.add("reg_rw_4", self._RWRegister(4, reset=0x0)) + reg_rw_8 = regs.add("reg_rw_8", self._RWRegister(8, reset=0x11)) + with regs.Cluster("cluster_0"): + reg_rw_16 = regs.add("reg_rw_16", self._RWRegister(16, reset=0x3322)) - dut = Bridge(register_map, addr_width=16, data_width=8) + dut = Bridge(regs.as_memory_map()) def process(): yield dut.bus.addr.eq(0)