periph.intc: add GenericInterruptController
authorJean-François Nguyen <jf@lambdaconcept.com>
Wed, 25 Mar 2020 16:41:41 +0000 (17:41 +0100)
committerJean-François Nguyen <jf@lambdaconcept.com>
Wed, 25 Mar 2020 16:41:41 +0000 (17:41 +0100)
lambdasoc/periph/intc.py [new file with mode: 0644]
lambdasoc/test/test_periph_intc.py [new file with mode: 0644]

diff --git a/lambdasoc/periph/intc.py b/lambdasoc/periph/intc.py
new file mode 100644 (file)
index 0000000..5d75ff9
--- /dev/null
@@ -0,0 +1,114 @@
+from nmigen import *
+from . import Peripheral, IRQLine
+
+
+__all__ = ["InterruptController", "GenericInterruptController"]
+
+
+class InterruptController(Peripheral):
+    """Interrupt controller base class."""
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.__irq_lines = set()
+        self.__irq_map   = dict()
+
+    def add_irq(self, line, index):
+        """Add an IRQ line.
+
+        Parameters
+        ----------
+        line : :class:`IRQLine`
+            IRQ line.
+        index : int
+            IRQ index.
+
+        Exceptions
+        ----------
+        Raises :exn:`ValueError` if ``line`` is added twice, or if ``index`` is already used.
+        """
+        if not isinstance(line, IRQLine):
+            raise TypeError("IRQ line must be an instance of IRQLine, not {!r}"
+                            .format(line))
+        if not isinstance(index, int) or index < 0:
+            raise ValueError("IRQ index must be a non-negative integer, not {!r}"
+                             .format(index))
+        if line in self.__irq_lines:
+            raise ValueError("IRQ line {!r} has already been mapped to IRQ index {}"
+                             .format(line, self.find_index(line)))
+        if index in self.__irq_map:
+            raise ValueError("IRQ index {} has already been mapped to IRQ line {!r}"
+                             .format(index, self.__irq_map[index]))
+        self.__irq_lines.add(line)
+        self.__irq_map[index] = line
+
+    def iter_irqs(self):
+        """Iterate IRQ lines.
+
+        Yield values
+        ------------
+        A tuple ``index, line`` describing an IRQ line and its index.
+        """
+        yield from sorted(self.__irq_map.items())
+
+    def find_index(self, line):
+        """Find the index at which an IRQ line is mapped.
+
+        Parameters
+        ----------
+        line : :class:`IRQLine`
+            IRQ line.
+
+        Return value
+        ------------
+        The index at which ``line`` is mapped, if present.
+
+        Exceptions
+        ----------
+        Raises :exn:`KeyError` if ``line`` is not present.
+        """
+        for irq_index, irq_line in self.iter_irqs():
+            if line is irq_line:
+                return irq_index
+        raise KeyError(line)
+
+
+class GenericInterruptController(InterruptController, Elaboratable):
+    """Generic interrupt controller.
+
+    An interrupt "controller" acting as a passthrough for IRQ lines. Useful for CPU cores that do
+    interrupt management themselves.
+
+    Parameters
+    ----------
+    width : int
+        Output width.
+
+    Attributes
+    ----------
+    width : int
+        Output width.
+    ip : Signal, out
+        Pending IRQs.
+    """
+    def __init__(self, *, width):
+        super().__init__(src_loc_at=2)
+        if not isinstance(width, int) or width <= 0:
+            raise ValueError("Width must be a strictly positive integer, not {!r}"
+                             .format(width))
+        self.width = width
+        self.ip    = Signal(width)
+
+    def add_irq(self, line, index):
+        __doc__ = InterruptController.add_irq.__doc__
+        if not isinstance(index, int) or index not in range(0, self.width):
+            raise ValueError("IRQ index must be an integer ranging from 0 to {}, not {!r}"
+                             .format(self.width - 1, index))
+        super().add_irq(line, index)
+
+    def elaborate(self, platform):
+        m = Module()
+
+        for irq_index, irq_line in self.iter_irqs():
+            m.d.comb += self.ip[irq_index].eq(irq_line)
+
+        return m
diff --git a/lambdasoc/test/test_periph_intc.py b/lambdasoc/test/test_periph_intc.py
new file mode 100644 (file)
index 0000000..add61e9
--- /dev/null
@@ -0,0 +1,108 @@
+#nmigen: UnusedElaboratable=no
+
+import unittest
+
+from nmigen import *
+from nmigen.back.pysim import *
+
+from ..periph import IRQLine
+from ..periph.intc import *
+
+
+class InterruptControllerTestCase(unittest.TestCase):
+    def test_add_irq_wrong_line(self):
+        intc = InterruptController()
+        with self.assertRaisesRegex(TypeError,
+                r"IRQ line must be an instance of IRQLine, not 'foo'"):
+            intc.add_irq("foo", 0)
+
+    def test_add_irq_wrong_index(self):
+        intc = InterruptController()
+        with self.assertRaisesRegex(ValueError,
+                r"IRQ index must be a non-negative integer, not 'bar'"):
+            intc.add_irq(IRQLine(name="foo"), "bar")
+        with self.assertRaisesRegex(ValueError,
+                r"IRQ index must be a non-negative integer, not -1"):
+            intc.add_irq(IRQLine(name="foo"), -1)
+
+    def test_add_irq_line_twice(self):
+        intc = InterruptController()
+        line = IRQLine()
+        with self.assertRaisesRegex(ValueError,
+                r"IRQ line \(sig line\) has already been mapped to IRQ index 0"):
+            intc.add_irq(line, 0)
+            intc.add_irq(line, 1)
+
+    def test_add_irq_index_twice(self):
+        intc = InterruptController()
+        line_0 = IRQLine()
+        line_1 = IRQLine()
+        with self.assertRaisesRegex(ValueError,
+                r"IRQ index 0 has already been mapped to IRQ line \(sig line_0\)"):
+            intc.add_irq(line_0, 0)
+            intc.add_irq(line_1, 0)
+
+    def test_iter_irqs(self):
+        intc = InterruptController()
+        line_0 = IRQLine()
+        line_1 = IRQLine()
+        intc.add_irq(line_0, 0)
+        intc.add_irq(line_1, 1)
+        self.assertEqual(list(intc.iter_irqs()), [
+            (0, line_0),
+            (1, line_1),
+        ])
+
+    def test_find_index(self):
+        intc = InterruptController()
+        line_0 = IRQLine()
+        line_1 = IRQLine()
+        intc.add_irq(line_0, 0)
+        intc.add_irq(line_1, 1)
+        self.assertEqual(intc.find_index(line_0), 0)
+        self.assertEqual(intc.find_index(line_1), 1)
+
+    def test_find_index_absent(self):
+        intc = InterruptController()
+        line = IRQLine()
+        with self.assertRaises(KeyError):
+            intc.find_index(line)
+
+
+class GenericInterruptControllerTestCase(unittest.TestCase):
+    def test_wrong_width(self):
+        with self.assertRaisesRegex(ValueError,
+                r"Width must be a strictly positive integer, not 'foo'"):
+            intc = GenericInterruptController(width="foo")
+        with self.assertRaisesRegex(ValueError,
+                r"Width must be a strictly positive integer, not 0"):
+            intc = GenericInterruptController(width=0)
+
+    def test_add_irq_wrong_index(self):
+        intc = GenericInterruptController(width=8)
+        line = IRQLine()
+        with self.assertRaisesRegex(ValueError,
+                r"IRQ index must be an integer ranging from 0 to 7, not 8"):
+            intc.add_irq(line, 8)
+
+    def test_passthrough(self):
+        dut = GenericInterruptController(width=8)
+        line_0 = IRQLine()
+        line_1 = IRQLine()
+        dut.add_irq(line_0, 0)
+        dut.add_irq(line_1, 1)
+
+        def process():
+            self.assertEqual((yield dut.ip), 0b00)
+
+            yield line_0.eq(1)
+            yield Delay(1e-6)
+            self.assertEqual((yield dut.ip), 0b01)
+
+            yield line_1.eq(1)
+            yield Delay(1e-6)
+            self.assertEqual((yield dut.ip), 0b11)
+
+        with Simulator(dut, vcd_file=open("test.vcd", "w")) as sim:
+            sim.add_process(process)
+            sim.run()