diff --git a/nmigen_soc/periph.py b/nmigen_soc/periph.py index 6a8ad36..f7abd38 100644 --- a/nmigen_soc/periph.py +++ b/nmigen_soc/periph.py @@ -1,15 +1,135 @@ +from collections.abc import Mapping + +from nmigen.utils import bits_for + from .memory import MemoryMap from . import event -__all__ = ["PeripheralInfo"] +__all__ = ["ConstantValue", "ConstantBool", "ConstantInt", "ConstantMap", "PeripheralInfo"] + + +class ConstantValue: + pass + + +class ConstantBool(ConstantValue): + """Boolean constant. + + Parameters + ---------- + value : bool + Constant value. + """ + def __init__(self, value): + if not isinstance(value, bool): + raise TypeError("Value must be a bool, not {!r}".format(value)) + self._value = value + + @property + def value(self): + return self._value + + def __repr__(self): + return "ConstantBool({})".format(self.value) + + +class ConstantInt(ConstantValue): + """Integer constant. + + Parameters + ---------- + value : int + Constant value. + width : int + Width in bits. Optional. ``bits_for(value)`` by default. + signed : bool + Signedness. Optional. ``value < 0`` by default. + """ + def __init__(self, value, *, width=None, signed=None): + if not isinstance(value, int): + raise TypeError("Value must be an integer, not {!r}" + .format(value)) + self._value = value + + if width is None: + width = bits_for(value) + if not isinstance(width, int): + raise TypeError("Width must be an integer, not {!r}" + .format(width)) + if width < bits_for(value): + raise ValueError("Width must be greater than or equal to the number of bits needed to" + " represent {}".format(value)) + self._width = width + + if signed is None: + signed = value < 0 + if not isinstance(signed, bool): + raise TypeError("Signedness must be a bool, not {!r}" + .format(signed)) + self._signed = signed + + @property + def value(self): + return self._value + + @property + def width(self): + return self._width + + @property + def signed(self): + return self._signed + + def __repr__(self): + return "ConstantInt({}, width={}, signed={})".format(self.value, self.width, self.signed) + + +class ConstantMap(Mapping): + """Named constant map. + + A read-only container for named constants. Keys are iterated in ascending order. + + Parameters + ---------- + **constants : dict(str : :class:`ConstantValue`) + Named constants. + + Examples + -------- + >>> constant_map = ConstantMap(RX_FIFO_DEPTH=16) + {'RX_FIFO_DEPTH': ConstantInt(16, width=5, signed=False)} + """ + def __init__(self, **constants): + self._storage = dict() + for key, value in constants.items(): + if isinstance(value, bool): + value = ConstantBool(value) + if isinstance(value, int): + value = ConstantInt(value) + if not isinstance(value, ConstantValue): + raise TypeError("Constant value must be an instance of ConstantValue, not {!r}" + .format(value)) + self._storage[key] = value + + def __getitem__(self, key): + return self._storage[key] + + def __iter__(self): + yield from sorted(self._storage) + + def __len__(self): + return len(self._storage) + + def __repr__(self): + return repr(self._storage) class PeripheralInfo: """Peripheral metadata. A unified description of the local resources of a peripheral. It may be queried in order to - recover its memory windows, CSR registers and event sources. + recover its memory windows, CSR registers, event sources and configuration constants. Parameters ---------- @@ -17,8 +137,10 @@ class PeripheralInfo: Memory map of the peripheral. irq : :class:`event.Source` IRQ line of the peripheral. Optional. + constant_map : :class:`ConstantMap` + Constant map of the peripheral. Optional. """ - def __init__(self, *, memory_map, irq=None): + def __init__(self, *, memory_map, irq=None, constant_map=None): if not isinstance(memory_map, MemoryMap): raise TypeError("Memory map must be an instance of MemoryMap, not {!r}" .format(memory_map)) @@ -30,6 +152,13 @@ def __init__(self, *, memory_map, irq=None): .format(irq)) self._irq = irq + if constant_map is None: + constant_map = ConstantMap() + if not isinstance(constant_map, ConstantMap): + raise TypeError("Constant map must be an instance of ConstantMap, not {!r}" + .format(constant_map)) + self._constant_map = constant_map + @property def memory_map(self): """Memory map. @@ -57,3 +186,13 @@ def irq(self): raise NotImplementedError("Peripheral info does not have an IRQ line" .format(self)) return self._irq + + @property + def constant_map(self): + """Constant map. + + Return value + ------------ + A :class:`ConstantMap` containing configuration constants of the peripheral. + """ + return self._constant_map diff --git a/nmigen_soc/test/test_periph.py b/nmigen_soc/test/test_periph.py index 5499181..3130dd6 100644 --- a/nmigen_soc/test/test_periph.py +++ b/nmigen_soc/test/test_periph.py @@ -1,10 +1,100 @@ import unittest -from ..periph import PeripheralInfo +from ..periph import * from ..memory import MemoryMap from .. import event +class ConstantBoolTestCase(unittest.TestCase): + def test_init(self): + a = ConstantBool(True) + b = ConstantBool(False) + self.assertTrue(a.value) + self.assertFalse(b.value) + + def test_value_wrong(self): + with self.assertRaisesRegex(TypeError, r"Value must be a bool, not 'foo'"): + ConstantBool("foo") + + def test_repr(self): + self.assertEqual(repr(ConstantBool(True)), "ConstantBool(True)") + + +class ConstantIntTestCase(unittest.TestCase): + def test_init(self): + c = ConstantInt(5, width=8, signed=True) + self.assertEqual(c.value, 5) + self.assertEqual(c.width, 8) + self.assertEqual(c.signed, True) + + def test_init_default(self): + c = ConstantInt(5) + self.assertEqual(c.value, 5) + self.assertEqual(c.width, 3) + self.assertEqual(c.signed, False) + + def test_value_wrong(self): + with self.assertRaisesRegex(TypeError, r"Value must be an integer, not 'foo'"): + ConstantInt("foo") + + def test_width_wrong(self): + with self.assertRaisesRegex(TypeError, r"Width must be an integer, not 'foo'"): + ConstantInt(5, width="foo") + + def test_width_overflow(self): + with self.assertRaisesRegex(ValueError, + r"Width must be greater than or equal to the number of bits needed to represent 5"): + ConstantInt(5, width=1) + + def test_signed_wrong(self): + with self.assertRaisesRegex(TypeError, r"Signedness must be a bool, not 'foo'"): + ConstantInt(5, signed="foo") + + def test_repr(self): + self.assertEqual( + repr(ConstantInt(-5, width=8, signed=True)), + "ConstantInt(-5, width=8, signed=True)" + ) + + +class ConstantMapTestCase(unittest.TestCase): + def test_init(self): + constant_map = ConstantMap(A=5, B=True, C=ConstantBool(False)) + self.assertEqual( + repr(constant_map), + "{'A': ConstantInt(5, width=3, signed=False)," + " 'B': ConstantBool(True)," + " 'C': ConstantBool(False)}", + ) + + def test_init_wrong_value(self): + with self.assertRaisesRegex(TypeError, + r"Constant value must be an instance of ConstantValue, not \('foo', 'bar'\)"): + ConstantMap(A=("foo", "bar")) + + def test_getitem(self): + a = ConstantInt(1) + b = ConstantBool(False) + constant_map = ConstantMap(A=a, B=b) + self.assertIs(constant_map["A"], a) + self.assertIs(constant_map["B"], b) + + def test_iter(self): + a = ConstantInt(1) + b = ConstantBool(False) + constant_map = ConstantMap(B=b, A=a) + self.assertEqual(list(constant_map.items()), [ + ("A", a), + ("B", b), + ]) + + def test_len(self): + a = ConstantInt(1) + b = ConstantBool(False) + constant_map = ConstantMap(B=b, A=a) + self.assertEqual(len(constant_map), 2) + + class PeripheralInfoTestCase(unittest.TestCase): def test_memory_map(self): memory_map = MemoryMap(addr_width=1, data_width=8) @@ -48,3 +138,27 @@ def test_irq_wrong(self): with self.assertRaisesRegex(TypeError, r"IRQ line must be an instance of event.Source, not 'foo'"): info = PeripheralInfo(memory_map=memory_map, irq="foo") + + def test_constant_map(self): + constant_map = ConstantMap() + memory_map = MemoryMap(addr_width=1, data_width=8) + info = PeripheralInfo(memory_map=memory_map, constant_map=constant_map) + self.assertIs(info.constant_map, constant_map) + + def test_constant_map_none(self): + memory_map = MemoryMap(addr_width=1, data_width=8) + info = PeripheralInfo(memory_map=memory_map, constant_map=None) + self.assertIsInstance(info.constant_map, ConstantMap) + self.assertEqual(info.constant_map, {}) + + def test_constant_map_default(self): + memory_map = MemoryMap(addr_width=1, data_width=8) + info = PeripheralInfo(memory_map=memory_map) + self.assertIsInstance(info.constant_map, ConstantMap) + self.assertEqual(info.constant_map, {}) + + def test_constant_map_wrong(self): + memory_map = MemoryMap(addr_width=1, data_width=8) + with self.assertRaisesRegex(TypeError, + r"Constant map must be an instance of ConstantMap, not 'foo'"): + info = PeripheralInfo(memory_map=memory_map, constant_map="foo")