Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/nmigen-soc
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 120ea5387c85
Choose a base ref
...
head repository: m-labs/nmigen-soc
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 8662815e1ea6
Choose a head ref
  • 3 commits
  • 6 files changed
  • 1 contributor

Commits on Oct 25, 2019

  1. csr.bus: add Multiplexer.

    whitequark committed Oct 25, 2019
    Copy the full SHA
    2a634b3 View commit details
  2. csr.bus.{Multiplexer↔Decoder}

    This reverses the rename done in commit 5520e0d. Commit 2a634b3
    introduced a Multiplexer that doesn't actually do multiplexing, so
    revert that to make everything less confusing.
    whitequark committed Oct 25, 2019
    Copy the full SHA
    7d76126 View commit details
  3. wishbone.bus: add Interface.

    whitequark committed Oct 25, 2019
    Copy the full SHA
    8662815 View commit details
Showing with 416 additions and 17 deletions.
  1. +104 −9 nmigen_soc/csr/bus.py
  2. +7 −2 nmigen_soc/memory.py
  3. +80 −6 nmigen_soc/test/test_csr_bus.py
  4. +87 −0 nmigen_soc/test/test_wishbone_bus.py
  5. +1 −0 nmigen_soc/wishbone/__init__.py
  6. +137 −0 nmigen_soc/wishbone/bus.py
113 changes: 104 additions & 9 deletions nmigen_soc/csr/bus.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import enum
from nmigen import *
from nmigen.utils import log2_int

from ..memory import MemoryMap


__all__ = ["Element", "Interface", "Decoder"]
__all__ = ["Element", "Interface", "Decoder", "Multiplexer"]


class Element(Record):
@@ -103,15 +104,20 @@ class Interface(Record):
Address width. At most ``(2 ** addr_width) * data_width`` register bits will be available.
data_width : int
Data width. Registers are accessed in ``data_width`` sized chunks.
alignment : int
Register and window alignment. See :class:`MemoryMap`.
name : str
Name of the underlying record.
Attributes
----------
memory_map : MemoryMap
Map of the bus.
addr : Signal(addr_width)
Address for reads and writes.
r_data : Signal(data_width)
Read data. Valid on the next cycle after ``r_stb`` is asserted.
Read data. Valid on the next cycle after ``r_stb`` is asserted. Otherwise, zero. (Keeping
read data of an unused interface at zero simplifies multiplexers.)
r_stb : Signal()
Read strobe. If ``addr`` points to the first chunk of a register, captures register value
and causes read side effects to be performed (if any). If ``addr`` points to any chunk
@@ -126,7 +132,7 @@ class Interface(Record):
nothing.
"""

def __init__(self, *, addr_width, data_width, name=None):
def __init__(self, *, addr_width, data_width, alignment=0, name=None):
if not isinstance(addr_width, int) or addr_width <= 0:
raise ValueError("Address width must be a positive integer, not {!r}"
.format(addr_width))
@@ -135,6 +141,8 @@ def __init__(self, *, addr_width, data_width, name=None):
.format(data_width))
self.addr_width = addr_width
self.data_width = data_width
self.memory_map = MemoryMap(addr_width=addr_width, data_width=data_width,
alignment=alignment)

super().__init__([
("addr", addr_width),
@@ -145,8 +153,8 @@ def __init__(self, *, addr_width, data_width, name=None):
], name=name, src_loc_at=1)


class Decoder(Elaboratable):
"""CSR bus decoder.
class Multiplexer(Elaboratable):
"""CSR register multiplexer.
An address-based multiplexer for CSR registers implementing atomic updates.
@@ -185,17 +193,16 @@ class Decoder(Elaboratable):
data_width : int
Data width. See :class:`Interface`.
alignment : int
Register alignment. The address assigned to each register will be a multiple of
``2 ** alignment``.
Register alignment. See :class:`Interface`.
Attributes
----------
bus : :class:`Interface`
CSR bus providing access to registers.
"""
def __init__(self, *, addr_width, data_width, alignment=0):
self.bus = Interface(addr_width=addr_width, data_width=data_width)
self._map = MemoryMap(addr_width=addr_width, data_width=data_width, alignment=alignment)
self.bus = Interface(addr_width=addr_width, data_width=data_width, alignment=alignment)
self._map = self.bus.memory_map

def align_to(self, alignment):
"""Align the implicit address of the next register.
@@ -266,3 +273,91 @@ def elaborate(self, platform):
m.d.comb += self.bus.r_data.eq(r_data_fanin)

return m


class Decoder(Elaboratable):
"""CSR bus decoder.
An address decoder for subordinate CSR buses.
Usage
-----
Although there is no functional difference between adding a set of registers directly to
a :class:`Multiplexer` and adding a set of registers to multiple :class:`Multiplexer`s that are
aggregated with a :class:`Decoder`, hierarchical CSR buses are useful for organizing
a hierarchical design. If many peripherals are directly served by a single
:class:`Multiplexer`, a very large amount of ports will connect the peripheral registers with
the decoder, and the cost of decoding logic would not be attributed to specific peripherals.
With a decoder, only five signals per peripheral will be used, and the logic could be kept
together with the peripheral.
Parameters
----------
addr_width : int
Address width. See :class:`Interface`.
data_width : int
Data width. See :class:`Interface`.
alignment : int
Window alignment. See :class:`Interface`.
Attributes
----------
bus : :class:`Interface`
CSR bus providing access to subordinate buses.
"""
def __init__(self, *, addr_width, data_width, alignment=0):
self.bus = Interface(addr_width=addr_width, data_width=data_width, alignment=alignment)
self._map = self.bus.memory_map
self._subs = dict()

def align_to(self, alignment):
"""Align the implicit address of the next window.
See :meth:`MemoryMap.align_to` for details.
"""
return self._map.align_to(alignment)

def add(self, sub_bus, *, addr=None):
"""Add a window to a subordinate bus.
See :meth:`MemoryMap.add_resource` for details.
"""
if not isinstance(sub_bus, Interface):
raise TypeError("Subordinate bus must be an instance of csr.Interface, not {!r}"
.format(sub_bus))
if sub_bus.data_width != self.bus.data_width:
raise ValueError("Subordinate bus has data width {}, which is not the same as "
"multiplexer data width {}"
.format(sub_bus.data_width, self.bus.data_width))

start, end, ratio = window_range = self._map.add_window(sub_bus.memory_map, addr=addr)
assert ratio == 1
pattern = "{:0{}b}{}".format(start >> sub_bus.addr_width,
self.bus.addr_width - sub_bus.addr_width,
"-" * sub_bus.addr_width)
self._subs[pattern] = sub_bus
return window_range

def elaborate(self, platform):
m = Module()

# See Multiplexer.elaborate above.
r_data_fanin = 0

with m.Switch(self.bus.addr):
for sub_pat, sub_bus in self._subs.items():
m.d.comb += sub_bus.addr.eq(self.bus.addr[:sub_bus.addr_width])

# The CSR bus interface is defined to output zero when idle, allowing us to avoid
# adding a multiplexer here.
r_data_fanin |= sub_bus.r_data
m.d.comb += sub_bus.w_data.eq(self.bus.w_data)

with m.Case(sub_pat):
m.d.comb += sub_bus.r_stb.eq(self.bus.r_stb)
m.d.comb += sub_bus.w_stb.eq(self.bus.w_stb)

m.d.comb += self.bus.r_data.eq(r_data_fanin)

return m
9 changes: 7 additions & 2 deletions nmigen_soc/memory.py
Original file line number Diff line number Diff line change
@@ -166,7 +166,7 @@ def add_resource(self, resource, *, size, addr=None, alignment=None):
Arguments
---------
resource
resource : object
Arbitrary object representing a resource.
addr : int or None
Address of the resource. If ``None``, the implicit next address will be used.
@@ -291,7 +291,12 @@ def add_window(self, window, *, addr=None, sparse=None):
ratio = self.data_width // window.data_width
else:
ratio = 1
size = (1 << window.addr_width) // ratio
size = (1 << window.addr_width) // ratio
# For resources, the alignment argument of add_resource() affects both address and size
# of the resource; aligning only the address should be done using align_to(). For windows,
# changing the size (beyond the edge case of the window size being smaller than alignment
# of the bus) is unlikely to be useful, so there is no alignment argument. The address of
# a window can still be aligned using align_to().
alignment = max(self.alignment, window.addr_width // ratio)

addr_range = self._compute_addr_range(addr, size, ratio, alignment=alignment)
86 changes: 80 additions & 6 deletions nmigen_soc/test/test_csr_bus.py
Original file line number Diff line number Diff line change
@@ -71,20 +71,20 @@ def test_layout(self):
("w_stb", 1),
]))

def test_addr_width_wrong(self):
def test_wrong_addr_width(self):
with self.assertRaisesRegex(ValueError,
r"Address width must be a positive integer, not -1"):
Interface(addr_width=-1, data_width=8)

def test_data_width_wrong(self):
def test_wrong_data_width(self):
with self.assertRaisesRegex(ValueError,
r"Data width must be a positive integer, not -1"):
Interface(addr_width=16, data_width=-1)


class DecoderTestCase(unittest.TestCase):
class MultiplexerTestCase(unittest.TestCase):
def setUp(self):
self.dut = Decoder(addr_width=16, data_width=8)
self.dut = Multiplexer(addr_width=16, data_width=8)
Fragment.get(self.dut, platform=None) # silence UnusedElaboratable

def test_add_4b(self):
@@ -195,9 +195,9 @@ def sim_test():
sim.run()


class DecoderAlignedTestCase(unittest.TestCase):
class MultiplexerAlignedTestCase(unittest.TestCase):
def setUp(self):
self.dut = Decoder(addr_width=16, data_width=8, alignment=2)
self.dut = Multiplexer(addr_width=16, data_width=8, alignment=2)
Fragment.get(self.dut, platform=None) # silence UnusedElaboratable

def test_add_two(self):
@@ -253,3 +253,77 @@ def sim_test():
sim.add_clock(1e-6)
sim.add_sync_process(sim_test())
sim.run()


class DecoderTestCase(unittest.TestCase):
def setUp(self):
self.dut = Decoder(addr_width=16, data_width=8)
Fragment.get(self.dut, platform=None) # silence UnusedElaboratable

def test_add_wrong_sub_bus(self):
with self.assertRaisesRegex(TypeError,
r"Subordinate bus must be an instance of csr\.Interface, not 1"):
self.dut.add(1)

def test_add_wrong_data_width(self):
mux = Multiplexer(addr_width=10, data_width=16)
Fragment.get(mux, platform=None) # silence UnusedElaboratable

with self.assertRaisesRegex(ValueError,
r"Subordinate bus has data width 16, which is not the same as "
r"multiplexer data width 8"):
self.dut.add(mux.bus)

def test_sim(self):
mux_1 = Multiplexer(addr_width=10, data_width=8)
self.dut.add(mux_1.bus)
elem_1 = Element(8, "rw")
mux_1.add(elem_1)

mux_2 = Multiplexer(addr_width=10, data_width=8)
self.dut.add(mux_2.bus)
elem_2 = Element(8, "rw")
mux_2.add(elem_2, addr=2)

elem_1_addr, _, _ = self.dut.bus.memory_map.find_resource(elem_1)
elem_2_addr, _, _ = self.dut.bus.memory_map.find_resource(elem_2)
self.assertEqual(elem_1_addr, 0x0000)
self.assertEqual(elem_2_addr, 0x0402)

bus = self.dut.bus

def sim_test():
yield bus.addr.eq(elem_1_addr)
yield bus.w_stb.eq(1)
yield bus.w_data.eq(0x55)
yield
yield bus.w_stb.eq(0)
yield
self.assertEqual((yield elem_1.w_data), 0x55)

yield bus.addr.eq(elem_2_addr)
yield bus.w_stb.eq(1)
yield bus.w_data.eq(0xaa)
yield
yield bus.w_stb.eq(0)
yield
self.assertEqual((yield elem_2.w_data), 0xaa)

yield elem_1.r_data.eq(0x55)
yield elem_2.r_data.eq(0xaa)

yield bus.addr.eq(elem_1_addr)
yield bus.r_stb.eq(1)
yield
yield bus.addr.eq(elem_2_addr)
yield
self.assertEqual((yield bus.r_data), 0x55)
yield
self.assertEqual((yield bus.r_data), 0xaa)

m = Module()
m.submodules += self.dut, mux_1, mux_2
with Simulator(m, vcd_file=open("test.vcd", "w")) as sim:
sim.add_clock(1e-6)
sim.add_sync_process(sim_test())
sim.run()
87 changes: 87 additions & 0 deletions nmigen_soc/test/test_wishbone_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import unittest
from nmigen import *
from nmigen.hdl.rec import *

from ..wishbone.bus import *


class InterfaceTestCase(unittest.TestCase):
def test_simple(self):
iface = Interface(addr_width=32, data_width=8)
self.assertEqual(iface.addr_width, 32)
self.assertEqual(iface.data_width, 8)
self.assertEqual(iface.granularity, 8)
self.assertEqual(iface.memory_map.addr_width, 32)
self.assertEqual(iface.memory_map.data_width, 8)
self.assertEqual(iface.layout, Layout.cast([
("adr", 32, DIR_FANOUT),
("dat_w", 8, DIR_FANOUT),
("dat_r", 8, DIR_FANIN),
("sel", 1, DIR_FANOUT),
("cyc", 1, DIR_FANOUT),
("stb", 1, DIR_FANOUT),
("we", 1, DIR_FANOUT),
("ack", 1, DIR_FANIN),
]))

def test_granularity(self):
iface = Interface(addr_width=30, data_width=32, granularity=8)
self.assertEqual(iface.addr_width, 30)
self.assertEqual(iface.data_width, 32)
self.assertEqual(iface.granularity, 8)
self.assertEqual(iface.memory_map.addr_width, 32)
self.assertEqual(iface.memory_map.data_width, 8)
self.assertEqual(iface.layout, Layout.cast([
("adr", 30, DIR_FANOUT),
("dat_w", 32, DIR_FANOUT),
("dat_r", 32, DIR_FANIN),
("sel", 4, DIR_FANOUT),
("cyc", 1, DIR_FANOUT),
("stb", 1, DIR_FANOUT),
("we", 1, DIR_FANOUT),
("ack", 1, DIR_FANIN),
]))

def test_optional(self):
iface = Interface(addr_width=32, data_width=32,
optional={"rty", "err", "stall", "cti", "bte"})
self.assertEqual(iface.layout, Layout.cast([
("adr", 32, DIR_FANOUT),
("dat_w", 32, DIR_FANOUT),
("dat_r", 32, DIR_FANIN),
("sel", 1, DIR_FANOUT),
("cyc", 1, DIR_FANOUT),
("stb", 1, DIR_FANOUT),
("we", 1, DIR_FANOUT),
("ack", 1, DIR_FANIN),
("err", 1, DIR_FANIN),
("rty", 1, DIR_FANIN),
("stall", 1, DIR_FANIN),
("cti", CycleType, DIR_FANOUT),
("bte", BurstTypeExt, DIR_FANOUT),
]))

def test_wrong_addr_width(self):
with self.assertRaisesRegex(ValueError,
r"Address width must be a non-negative integer, not -1"):
Interface(addr_width=-1, data_width=8)

def test_wrong_data_width(self):
with self.assertRaisesRegex(ValueError,
r"Data width must be one of 8, 16, 32, 64, not 7"):
Interface(addr_width=0, data_width=7)

def test_wrong_granularity(self):
with self.assertRaisesRegex(ValueError,
r"Granularity must be one of 8, 16, 32, 64, not 7"):
Interface(addr_width=0, data_width=32, granularity=7)

def test_wrong_granularity(self):
with self.assertRaisesRegex(ValueError,
r"Granularity 32 may not be greater than data width 8"):
Interface(addr_width=0, data_width=8, granularity=32)

def test_wrong_optional(self):
with self.assertRaisesRegex(ValueError,
r"Optional signal\(s\) 'foo' are not supported"):
Interface(addr_width=0, data_width=8, optional={"foo"})
1 change: 1 addition & 0 deletions nmigen_soc/wishbone/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .bus import *
137 changes: 137 additions & 0 deletions nmigen_soc/wishbone/bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from enum import Enum
from nmigen import *
from nmigen.hdl.rec import Direction
from nmigen.utils import log2_int

from ..memory import MemoryMap


__all__ = ["CycleType", "BurstTypeExt", "Interface"]


class CycleType(Enum):
"""Wishbone Registered Feedback cycle type."""
CLASSIC = 0b000
CONST_BURST = 0b001
INCR_BURST = 0b010
END_OF_BURST = 0b111


class BurstTypeExt(Enum):
"""Wishbone Registered Feedback burst type extension."""
LINEAR = 0b00
WRAP_4 = 0b01
WRAP_8 = 0b10
WRAP_16 = 0b11


class Interface(Record):
"""Wishbone interface.
See the `Wishbone specification <https://opencores.org/howto/wishbone>`_ for description
of the Wishbone signals. The ``RST_I`` and ``CLK_I`` signals are provided as a part of
the clock domain that drives the interface.
Note that the data width of the underlying memory map of the interface is equal to port
granularity, not port size. If port granularity is less than port size, then the address width
of the underlying memory map is extended to reflect that.
Parameters
----------
addr_width : int
Width of the address signal.
data_width : int
Width of the data signals ("port size" in Wishbone terminology).
One of 8, 16, 32, 64.
granularity : int
Granularity of select signals ("port granularity" in Wishbone terminology).
One of 8, 16, 32, 64.
optional : iter(str)
Selects the optional signals that will be a part of this interface.
alignment : int
Resource and window alignment. See :class:`MemoryMap`.
name : str
Name of the underlying record.
Attributes
----------
The correspondence between the nMigen-SoC signals and the Wishbone signals changes depending
on whether the interface acts as an initiator or a target.
adr : Signal(addr_width)
Corresponds to Wishbone signal ``ADR_O`` (initiator) or ``ADR_I`` (target).
dat_w : Signal(data_width)
Corresponds to Wishbone signal ``DAT_O`` (initiator) or ``DAT_I`` (target).
dat_r : Signal(data_width)
Corresponds to Wishbone signal ``DAT_I`` (initiator) or ``DAT_O`` (target).
sel : Signal(data_width // granularity)
Corresponds to Wishbone signal ``SEL_O`` (initiator) or ``SEL_I`` (target).
cyc : Signal()
Corresponds to Wishbone signal ``CYC_O`` (initiator) or ``CYC_I`` (target).
stb : Signal()
Corresponds to Wishbone signal ``STB_O`` (initiator) or ``STB_I`` (target).
we : Signal()
Corresponds to Wishbone signal ``WE_O`` (initiator) or ``WE_I`` (target).
ack : Signal()
Corresponds to Wishbone signal ``ACK_I`` (initiator) or ``ACK_O`` (target).
err : Signal()
Optional. Corresponds to Wishbone signal ``ERR_I`` (initiator) or ``ERR_O`` (target).
rty : Signal()
Optional. Corresponds to Wishbone signal ``RTY_I`` (initiator) or ``RTY_O`` (target).
stall : Signal()
Optional. Corresponds to Wishbone signal ``STALL_I`` (initiator) or ``STALL_O`` (target).
cti : Signal()
Optional. Corresponds to Wishbone signal ``CTI_O`` (initiator) or ``CTI_I`` (target).
bte : Signal()
Optional. Corresponds to Wishbone signal ``BTE_O`` (initiator) or ``BTE_I`` (target).
"""
def __init__(self, *, addr_width, data_width, granularity=None, optional=frozenset(),
alignment=0, name=None):
if not isinstance(addr_width, int) or addr_width < 0:
raise ValueError("Address width must be a non-negative integer, not {!r}"
.format(addr_width))
if data_width not in (8, 16, 32, 64):
raise ValueError("Data width must be one of 8, 16, 32, 64, not {!r}"
.format(data_width))
if granularity is None:
granularity = data_width
elif granularity not in (8, 16, 32, 64):
raise ValueError("Granularity must be one of 8, 16, 32, 64, not {!r}"
.format(granularity))
if granularity > data_width:
raise ValueError("Granularity {} may not be greater than data width {}"
.format(granularity, data_width))
self.addr_width = addr_width
self.data_width = data_width
self.granularity = granularity
granularity_bits = log2_int(data_width // granularity)
self.memory_map = MemoryMap(addr_width=max(1, addr_width + granularity_bits),
data_width=data_width >> granularity_bits,
alignment=alignment)

optional = set(optional)
unknown = optional - {"rty", "err", "stall", "cti", "bte"}
if unknown:
raise ValueError("Optional signal(s) {} are not supported"
.format(", ".join(map(repr, unknown))))
layout = [
("adr", addr_width, Direction.FANOUT),
("dat_w", data_width, Direction.FANOUT),
("dat_r", data_width, Direction.FANIN),
("sel", data_width // granularity, Direction.FANOUT),
("cyc", 1, Direction.FANOUT),
("stb", 1, Direction.FANOUT),
("we", 1, Direction.FANOUT),
("ack", 1, Direction.FANIN),
]
if "err" in optional:
layout += [("err", 1, Direction.FANIN)]
if "rty" in optional:
layout += [("rty", 1, Direction.FANIN)]
if "stall" in optional:
layout += [("stall", 1, Direction.FANIN)]
if "cti" in optional:
layout += [("cti", CycleType, Direction.FANOUT)]
if "bte" in optional:
layout += [("bte", BurstTypeExt, Direction.FANOUT)]
super().__init__(layout, name=name, src_loc_at=1)