From 1a90549fa345cccd5a792ce09219b142f4d851a1 Mon Sep 17 00:00:00 2001 From: Florent Kermarrec Date: Mon, 20 Jun 2022 19:50:49 +0200 Subject: [PATCH] interconnect/axi/axi_full: Switch to our own AXI Interconnect (Shared & Crossbar). We were not able to simulate verilog_axi interconnect/crossbar correctly since to what seems to be a simulation mismatch. The code also seems to requires fixing some synthesis issues with Yosys. When tested with Vivado, the SoC was also miss-behaving (not booting correctly). The simulation mismatch issue is logged here: https://github.com/enjoy-digital/litex_verilog_axi_test/issues/1 Since we already had our own AXI-Lite interconnect, creating our AXI interconnect can be largely based on it with only minor modifications, so switch to it. This also allow simplification in the interconnect selection/instance. --- litex/soc/integration/soc.py | 18 +- litex/soc/interconnect/axi/axi_full.py | 264 +++++++++++++++++++++++-- litex_setup.py | 1 - 3 files changed, 253 insertions(+), 30 deletions(-) diff --git a/litex/soc/integration/soc.py b/litex/soc/integration/soc.py index b9b2eab0b..cdd2d3cb4 100644 --- a/litex/soc/integration/soc.py +++ b/litex/soc/integration/soc.py @@ -1125,19 +1125,11 @@ class SoC(Module): "shared" : interconnect_shared_cls, "crossbar": interconnect_crossbar_cls, }[self.bus.interconnect] - if interconnect_cls in [axi.AXIInterconnectShared, axi.AXICrossbar]: - # FIXME: WIP, try to have compatibility with other interconnects. - self.submodules.bus_interconnect = interconnect_cls( - platform = self.platform, - masters = list(self.bus.masters.values()), - slaves = [(s, self.bus.regions[n].origin, self.bus.regions[n].size) for n, s in self.bus.slaves.items()] - ) - else: - self.submodules.bus_interconnect = interconnect_cls( - masters = list(self.bus.masters.values()), - slaves = [(self.bus.regions[n].decoder(self.bus), s) for n, s in self.bus.slaves.items()], - register = True, - timeout_cycles = self.bus.timeout) + self.submodules.bus_interconnect = interconnect_cls( + masters = list(self.bus.masters.values()), + slaves = [(self.bus.regions[n].decoder(self.bus), s) for n, s in self.bus.slaves.items()], + register = True, + timeout_cycles = self.bus.timeout) if hasattr(self, "ctrl") and self.bus.timeout is not None: if hasattr(self.ctrl, "bus_error") and hasattr(self.bus_interconnect, "timeout"): self.comb += self.ctrl.bus_error.eq(self.bus_interconnect.timeout.error) diff --git a/litex/soc/interconnect/axi/axi_full.py b/litex/soc/interconnect/axi/axi_full.py index 1fe3ffbdb..53ec8bb1d 100644 --- a/litex/soc/interconnect/axi/axi_full.py +++ b/litex/soc/interconnect/axi/axi_full.py @@ -264,34 +264,266 @@ class AXIConverter(Module): else: self.comb += master.connect(slave) +# AXI Timeout -------------------------------------------------------------------------------------- + +class AXITimeout(Module): + """Protect master against slave timeouts (master _has_ to respond correctly)""" + def __init__(self, master, cycles): + self.error = Signal() + wr_error = Signal() + rd_error = Signal() + + # # # + + self.comb += self.error.eq(wr_error | rd_error) + + wr_timer = WaitTimer(int(cycles)) + rd_timer = WaitTimer(int(cycles)) + self.submodules += wr_timer, rd_timer + + def channel_fsm(timer, wait_cond, error, response): + fsm = FSM(reset_state="WAIT") + fsm.act("WAIT", + timer.wait.eq(wait_cond), + # done is updated in `sync`, so we must make sure that `ready` has not been issued + # by slave during that single cycle, by checking `timer.wait`. + If(timer.done & timer.wait, + error.eq(1), + NextState("RESPOND") + ) + ) + fsm.act("RESPOND", *response) + return fsm + + self.submodules.wr_fsm = channel_fsm( + timer = wr_timer, + wait_cond = (master.aw.valid & ~master.aw.ready) | (master.w.valid & ~master.w.ready), + error = wr_error, + response = [ + master.aw.ready.eq(master.aw.valid), + master.w.ready.eq(master.w.valid), + master.b.valid.eq(~master.aw.valid & ~master.w.valid), + master.b.resp.eq(RESP_SLVERR), + If(master.b.valid & master.b.ready, + NextState("WAIT") + ) + ]) + + self.submodules.rd_fsm = channel_fsm( + timer = rd_timer, + wait_cond = master.ar.valid & ~master.ar.ready, + error = rd_error, + response = [ + master.ar.ready.eq(master.ar.valid), + master.r.valid.eq(~master.ar.valid), + master.r.last.eq(1), + master.r.resp.eq(RESP_SLVERR), + master.r.data.eq(2**len(master.r.data) - 1), + If(master.r.valid & master.r.ready, + NextState("WAIT") + ) + ]) + +# AXI Interconnect Components ---------------------------------------------------------------------- + +class _AXIRequestCounter(Module): + def __init__(self, request, response, max_requests=256): + self.counter = counter = Signal(max=max_requests) + self.full = full = Signal() + self.empty = empty = Signal() + self.stall = stall = Signal() + self.ready = self.empty + + self.comb += [ + full.eq(counter == max_requests - 1), + empty.eq(counter == 0), + stall.eq(request & full), + ] + + self.sync += [ + If(request & response, + counter.eq(counter) + ).Elif(request & ~full, + counter.eq(counter + 1) + ).Elif(response & ~empty, + counter.eq(counter - 1) + ), + ] + +class AXIArbiter(Module): + """AXI arbiter + + Arbitrate between master interfaces and connect one to the target. New master will not be + selected until all requests have been responded to. Arbitration for write and read channels is + done separately. + """ + def __init__(self, masters, target): + self.submodules.rr_write = roundrobin.RoundRobin(len(masters), roundrobin.SP_CE) + self.submodules.rr_read = roundrobin.RoundRobin(len(masters), roundrobin.SP_CE) + + def get_sig(interface, channel, name): + return getattr(getattr(interface, channel), name) + + # Mux master->slave signals + for channel, name, direction in target.layout_flat(): + rr = self.rr_write if channel in ["aw", "w", "b"] else self.rr_read + if direction == DIR_M_TO_S: + choices = Array(get_sig(m, channel, name) for m in masters) + self.comb += get_sig(target, channel, name).eq(choices[rr.grant]) + + # Connect slave->master signals + for channel, name, direction in target.layout_flat(): + rr = self.rr_write if channel in ["aw", "w", "b"] else self.rr_read + if direction == DIR_S_TO_M: + source = get_sig(target, channel, name) + for i, m in enumerate(masters): + dest = get_sig(m, channel, name) + if name == "ready": + self.comb += dest.eq(source & (rr.grant == i)) + else: + self.comb += dest.eq(source) + + # Allow to change rr.grant only after all requests from a master have been responded to. + self.submodules.wr_lock = wr_lock = _AXIRequestCounter( + request = target.aw.valid & target.aw.ready, + response = target.b.valid & target.b.ready + ) + self.submodules.rd_lock = rd_lock = _AXIRequestCounter( + request = target.ar.valid & target.ar.ready, + response = target.r.valid & target.r.ready & target.r.last + ) + + # Switch to next request only if there are no responses pending. + self.comb += [ + self.rr_write.ce.eq(~(target.aw.valid | target.w.valid | target.b.valid) & wr_lock.ready), + self.rr_read.ce.eq(~(target.ar.valid | target.r.valid) & rd_lock.ready), + ] + + # Connect bus requests to round-robin selectors. + self.comb += [ + self.rr_write.request.eq(Cat(*[m.aw.valid | m.w.valid | m.b.valid for m in masters])), + self.rr_read.request.eq(Cat(*[m.ar.valid | m.r.valid for m in masters])), + ] + +class AXIDecoder(Module): + """AXI decoder + + Decode master access to particular slave based on its decoder function. + + slaves: [(decoder, slave), ...] + List of slaves with address decoders, where `decoder` is a function: + decoder(Signal(address_width - log2(data_width//8))) -> Signal(1) + that returns 1 when the slave is selected and 0 otherwise. + """ + def __init__(self, master, slaves, register=False): + # TODO: unused register argument + addr_shift = log2_int(master.data_width//8) + + channels = { + "write": {"aw", "w", "b"}, + "read": {"ar", "r"}, + } + # Reverse mapping: directions[channel] -> "write"/"read". + directions = {ch: d for d, chs in channels.items() for ch in chs} + + def new_slave_sel(): + return {"write": Signal(len(slaves)), "read": Signal(len(slaves))} + + slave_sel_dec = new_slave_sel() + slave_sel_reg = new_slave_sel() + slave_sel = new_slave_sel() + + # We need to hold the slave selected until all responses come back. + # TODO: we could reuse arbiter counters + locks = { + "write": _AXIRequestCounter( + request = master.aw.valid & master.aw.ready, + response = master.b.valid & master.b.ready + ), + "read": _AXIRequestCounter( + request = master.ar.valid & master.ar.ready, + response = master.r.valid & master.r.ready & master.r.last, + ), + } + self.submodules += locks.values() + + def get_sig(interface, channel, name): + return getattr(getattr(interface, channel), name) + + # # # + + # Decode slave addresses. + for i, (decoder, bus) in enumerate(slaves): + self.comb += [ + slave_sel_dec["write"][i].eq(decoder(master.aw.addr[addr_shift:])), + slave_sel_dec["read"][i].eq(decoder(master.ar.addr[addr_shift:])), + ] + + # Change the current selection only when we've got all responses. + for channel in locks.keys(): + self.sync += If(locks[channel].ready, slave_sel_reg[channel].eq(slave_sel_dec[channel])) + # We have to cut the delaying select. + for ch, final in slave_sel.items(): + self.comb += [ + If(locks[ch].ready, + final.eq(slave_sel_dec[ch]) + ).Else( + final.eq(slave_sel_reg[ch]) + ) + ] + + # Connect master->slaves signals except valid/ready. + for i, (_, slave) in enumerate(slaves): + for channel, name, direction in master.layout_flat(): + if direction == DIR_M_TO_S: + src = get_sig(master, channel, name) + dst = get_sig(slave, channel, name) + # Mask master control signals depending on slave selection. + if name in ["valid", "ready"]: + src = src & slave_sel[directions[channel]][i] + self.comb += dst.eq(src) + + # Connect slave->master signals masking not selected slaves. + for channel, name, direction in master.layout_flat(): + if direction == DIR_S_TO_M: + dst = get_sig(master, channel, name) + masked = [] + for i, (_, slave) in enumerate(slaves): + src = get_sig(slave, channel, name) + # Mask depending on channel. + mask = Replicate(slave_sel[directions[channel]][i], len(dst)) + masked.append(src & mask) + self.comb += dst.eq(reduce(or_, masked)) # AXI Interconnect --------------------------------------------------------------------------------- class AXIInterconnectPointToPoint(Module): + """AXI point to point interconnect""" def __init__(self, master, slave): self.comb += master.connect(slave) class AXIInterconnectShared(Module): """AXI shared interconnect""" - def __init__(self, platform, masters, slaves): - # FIXME: WIP. - from verilog_axi.axi.axi_interconnect import AXIInterconnect - self.submodules.interconnect = AXIInterconnect(platform) - for master in masters: - self.interconnect.add_slave(s_axi=master) - for slave, origin, size in slaves: - self.interconnect.add_master(m_axi=slave, origin=origin, size=size) + def __init__(self, masters, slaves, register=False, timeout_cycles=1e6): + shared = AXIInterface(data_width=masters[0].data_width) + self.submodules.arbiter = AXIArbiter(masters, shared) + self.submodules.decoder = AXIDecoder(shared, slaves) + if timeout_cycles is not None: + self.submodules.timeout = AXITimeout(shared, timeout_cycles) class AXICrossbar(Module): """AXI crossbar MxN crossbar for M masters and N slaves. """ - def __init__(self, platform, masters, slaves): - # FIXME: WIP. - from verilog_axi.axi.axi_crossbar import AXICrossbar - self.submodules.crossbar = AXICrossbar(platform) - for master in masters: - self.crossbar.add_slave(s_axi=master) - for slave, origin, size in slaves: - self.crossbar.add_master(m_axi=slave, origin=origin, size=size) + def __init__(self, masters, slaves, register=False, timeout_cycles=1e6): + matches, busses = zip(*slaves) + access_m_s = [[AXIInterface() for j in slaves] for i in masters] # a[master][slave] + access_s_m = list(zip(*access_m_s)) # a[slave][master] + # Decode each master into its access row. + for slaves, master in zip(access_m_s, masters): + slaves = list(zip(matches, slaves)) + self.submodules += AXIDecoder(master, slaves, register) + # Arbitrate each access column onto its slave. + for masters, bus in zip(access_s_m, busses): + self.submodules += AXIArbiter(masters, bus) diff --git a/litex_setup.py b/litex_setup.py index a5ccc9335..7972f8b38 100755 --- a/litex_setup.py +++ b/litex_setup.py @@ -93,7 +93,6 @@ git_repos = { # LiteX Misc Cores. # ----------------- "valentyusb": GitRepo(url="https://github.com/litex-hub/", branch="hw_cdc_eptri"), - "litex_verilog_axi": GitRepo(url="https://github.com/enjoy-digital/", clone="recursive"), # LiteX Boards. # -------------